开发者看到的是 torch.randn(1024, 1024, device="npu"),驱动做的事:PCIe 枚举找到 NPU 设备 → 初始化 HBM 页表 → 分配物理内存 → 创建 Stream 上下文 → 提交 DMA 命令到硬件队列 → 返回用户态句柄。每一步出错都会变成 torch.npu 的 RuntimeError 或更糟——静默性能退化。

PCIe 设备枚举:从 Vendor ID 到 Device Handle

// driver/src/pcie/pcie_enum.cpp

// PCIe 枚举:扫描所有 PCIe 总线,找到 Ascend NPU 设备
// Ascend NPU 的 Vendor ID = 0x19E5(华为/昇腾),Device ID 根据型号不同

struct AscendDevice {
    uint16_t vendor_id;        // 0x19E5
    uint16_t device_id;        // 0xD100 = Ascend 910, 0xD110 = 910B, 0xD200 = 950
    uint8_t bus;               // PCIe 总线号
    uint8_t device;            // 设备号
    uint8_t function;          // 功能号

    uint64_t bar0_base;        // BAR0 基地址(寄存器映射,通常 32KB)
    uint64_t bar0_size;
    uint64_t bar2_base;        // BAR2 基地址(HBM 映射,通常 64GB)
    uint64_t bar2_size;

    int numa_node;             // NUMA 节点(PCIe 拓扑决定)
    int core_count;            // AI Core 数量(910 = 32, 910B = 64)
    uint64_t hbm_size;         // HBM 总大小(910 = 56GB)
};

// 枚举所有 Ascend NPU 设备
std::vector<AscendDevice> EnumerateAscendDevices() {
    std::vector<AscendDevice> devices;
    int device_index = 0;

    // 扫描所有 PCIe 总线(bus 0..255, device 0..31, function 0..7)
    for (int bus = 0; bus < 256; bus++) {
        for (int dev = 0; dev < 32; dev++) {
            for (int func = 0; func < 8; func++) {
                // 读取 PCIe 配置空间的 Vendor ID + Device ID
                uint32_t vendor_device = ReadPCIConfig(bus, dev, func, 0x00);
                uint16_t vendor_id = vendor_device & 0xFFFF;
                uint16_t device_id = vendor_device >> 16;

                if (vendor_id != 0x19E5) continue;  // 不是昇腾设备

                AscendDevice d;
                d.vendor_id = vendor_id;
                d.device_id = device_id;
                d.bus = bus;
                d.device = dev;
                d.function = func;
                d.device_index = device_index++;

                // 读取 BAR(Base Address Register)
                // BAR0:offset 0x10 in PCIe config space
                uint64_t bar0 = ReadPCIConfig64(bus, dev, func, 0x10);
                d.bar0_base = bar0 & ~0xF;         // 低 4 位是 BAR 类型标志
                d.bar0_size = GetBARSize(bus, dev, func, 0);

                // BAR2:offset 0x18 in PCIe config space
                uint64_t bar2 = ReadPCIConfig64(bus, dev, func, 0x18);
                d.bar2_base = bar2 & ~0xF;
                d.bar2_size = GetBARSize(bus, dev, func, 2);

                // 通过 BAR0 寄存器查询设备信息
                d.core_count = ReadDeviceRegister(&d, REG_CORE_COUNT);
                d.hbm_size = ReadDeviceRegister(&d, REG_HBM_SIZE);

                // NUMA 节点(从 ACPI SLIT 表查,或从 /sys/bus/pci 查)
                d.numa_node = GetNumaNode(bus, dev, func);

                devices.push_back(d);
            }
        }
    }

    return devices;
}

关键设计:PCIe 枚举不依赖操作系统驱动(OS Driver 在此之前加载),直接读 PCIe 配置空间(MMIO 访问物理地址)。因为 OS Driver 可能在驱动 binfmt 之后才加载,而 CANN runtime 需要在 Docker 容器里直接用 NPU——绕开 OS Driver,直接通过 VFIO/UIO 做用户态驱动。

HBM 页表管理:从虚拟地址到物理 HBM 页

HBM 不是传统的 DDR 内存——它是高带宽存储器,通过硅中介层(interposer)和 NPU Die 封装在一起。驱动需要管理 HBM 的物理页表,分配和释放 2MB 大页。

// driver/src/memory/hbm_allocator.cpp

// HBM 页表:管理 56GB HBM 的分配
// 使用 buddy system(伙伴系统)+ 2MB 大页

struct HBMAllocator {
    static constexpr int PAGE_SHIFT = 21;          // 2MB = 2^21 bytes
    static constexpr int PAGE_SIZE = 1 << 21;      // 2,097,152 bytes
    static constexpr int MAX_ORDER = 16;           // 2^16 × 2MB = 128GB(最大连续分配)
    static constexpr int MAX_NUM_PAGES;            // = HBM_SIZE / 2MB

    // 每阶链表:order 0 = 2MB, order 1 = 4MB, ... order 16 = 128GB
    std::vector<std::list<uint64_t>> free_lists;     // 每个 order 的空闲页链表
    std::vector<uint64_t> page_table;                // 页表:物理页编号 → order

    // 分配 size 字节的连续 HBM
    uint64_t Allocate(size_t size) {
        // 1. 计算需要的最小 order
        int order = 0;
        size_t alloc_size = PAGE_SIZE;
        while (alloc_size < size) {
            alloc_size <<= 1;
            order++;
        }

        // 2. 在 free_lists[order] 中查找
        int search_order = order;
        while (search_order <= MAX_ORDER && free_lists[search_order].empty()) {
            search_order++;
        }

        if (search_order > MAX_ORDER) {
            // OOM:没有足够大的连续块
            // 尝试碎片整理(compact):移动已分配的块,释放大块
            Compact();
            search_order = order;
            while (search_order <= MAX_ORDER && free_lists[search_order].empty()) {
                search_order++;
            }
            if (search_order > MAX_ORDER) {
                throw HBMOutOfMemory("Requested " + std::to_string(size) + " bytes");
            }
        }

        // 3. 从 higher order 分裂到 target order
        uint64_t page = free_lists[search_order].front();
        free_lists[search_order].pop_front();

        for (int o = search_order - 1; o >= order; o--) {
            // 分裂:order o+1 的页分裂为两个 order o 的页
            uint64_t buddy = page + (1ULL << (o + PAGE_SHIFT));
            free_lists[o].push_back(buddy);
        }

        page_table[page >> PAGE_SHIFT] = order;
        return page;
    }

    // 释放 HBM 页
    void Free(uint64_t page, int order) {
        page_table[page >> PAGE_SHIFT] = 0;

        // Buddy merge:如果 buddy 也是空闲的,合并为更大的阶
        for (int o = order; o < MAX_ORDER; o++) {
            uint64_t buddy = page ^ (1ULL << (o + PAGE_SHIFT));
            // 找 buddy 是否在 free_lists[o] 中
            auto it = std::find(free_lists[o].begin(), free_lists[o].end(), buddy);
            if (it == free_lists[o].end()) {
                // buddy 不在空闲链表 → 不能合并 → 加入 free_lists[o]
                free_lists[o].push_back(page);
                return;
            }

            // 合并:自己 + buddy → order+1
            free_lists[o].erase(it);
            page = min(page, buddy);  // 合并后的页起始地址
        }

        // 到达 MAX_ORDER,放入最高阶链表
        free_lists[MAX_ORDER].push_back(page);
    }

    // 碎片整理(compact)
    void Compact() {
        // 移动已分配的块,释放大的连续空间
        // 注:compact 通常只在 OOM 时做(代价高,需暂停所有 Stream)
        for (int order = MAX_ORDER - 1; order >= 0; order--) {
            for (auto it = free_lists[order].begin(); it != free_lists[order].end();) {
                uint64_t buddy = *it ^ (1ULL << (order + PAGE_SHIFT));
                auto buddy_it = std::find(free_lists[order].begin(),
                                          free_lists[order].end(), buddy);
                if (buddy_it != free_lists[order].end()) {
                    uint64_t merged = min(*it, buddy);
                    free_lists[order].erase(it);
                    free_lists[order].erase(buddy_it);
                    free_lists[order + 1].push_back(merged);
                    it = free_lists[order].begin();
                } else {
                    ++it;
                }
            }
        }
        }
    }
};

为什么用 Buddy System 而不是 C malloc?HBM 分配的是 GPU 内存,需要物理连续且 2MB 对齐(TLB 最小粒度)。C malloc 的分配粒度是 8 bytes,碎片小但地址不连续(需要 IOMMU 映射,NPU 不支持 IOMMU 重映射)。Buddy System 天生对齐 2MB 边界,且合并/分裂是 O(1) 的。

DMA 命令提交:从用户态到硬件队列

// driver/src/dma/dma_engine.cpp

// DMA 引擎:提交 HBM↔Host 的数据搬运命令
// 用户态(CANN runtime)通过 MMIO BAR0 寄存器直接提交命令

struct DMACommand {
    enum OpCode {
        H2D = 0x01,  // Host → HBM(CPU 内存 → NPU HBM)
        D2H = 0x02,  // HBM → Host(NPU HBM → CPU 内存)
        D2D = 0x03,  // HBM → HBM(同 NPU 内拷贝)
        P2P = 0x04,  // HBM → HBM(跨 NPU,通过 NVLink/HCCS)
    };

    OpCode opcode;
    uint64_t src_addr;     // 源地址(Host 物理地址 或 HBM 偏移)
    uint64_t dst_addr;     // 目标地址
    uint32_t size;         // 搬运大小(bytes)
    uint32_t stream_id;    // 关联的 Stream
    uint64_t event_id;     // 完成后触发的 Event
};

class DMAEngine {
private:
    // 硬件队列:环形缓冲区(Ring Buffer)
    // 基地址在 BAR0 寄存器中(HOST_DMA_QUEUE_BASE)
    static constexpr int QUEUE_DEPTH = 4096;    // 4K 个命令
    static constexpr int QUEUE_ENTRY_SIZE = 32;  // 32 bytes per command

    volatile uint32_t* queue_base;   // MMIO 映射的硬件队列
    uint32_t producer_idx;            // 软件维护的生产者指针
    volatile uint32_t* consumer_idx;  // 硬件维护的消费者指针(只读)

    // 影子队列:存放未提交的命令
    DMACommand shadow_queue[QUEUE_DEPTH];

public:
    Status Init(uint64_t bar0_base) {
        // 步骤 1:通过 MMIO 映射硬件队列
        // HOST_DMA_QUEUE_BASE 在 BAR0 的偏移 0x1000 处
        uint64_t queue_phys = ReadBAR0(bar0_base, 0x1000);
        queue_base = (volatile uint32_t*)MapPhysicalMemory(
            queue_phys, QUEUE_DEPTH * QUEUE_ENTRY_SIZE
        );

        // 步骤 2:读取硬件维护的消费者指针寄存器
        uint64_t consumer_reg = ReadBAR0(bar0_base, 0x1008);
        consumer_idx = (volatile uint32_t*)MapPhysicalMemory(consumer_reg, 4);

        producer_idx = 0;
        return Status::OK;
    }

    Status SubmitDMA(const DMACommand& cmd) {
        // 步骤 1:检查队列是否满
        uint32_t next_producer = (producer_idx + 1) % QUEUE_DEPTH;
        if (next_producer == *consumer_idx) {
            // 队列满 → 自旋等待硬件消费
            int retry = 0;
            while (next_producer == *consumer_idx && retry < 1000) {
                __builtin_arm_isb();  // 内存屏障(ARM 架构)
                retry++;
            }
            if (retry == 1000) {
                return Status::DMAQueueFull;
            }
        }

        // 步骤 2:写入命令到影子队列
        shadow_queue[producer_idx] = cmd;

        // 步骤 3:写入硬件队列(MMIO store)
        // 命令格式:32 bytes,8 个 uint32
        // [0]: opcode | stream_id << 8 | size >> 2
        // [1-2]: src_addr (64-bit)
        // [3-4]: dst_addr (64-bit)
        // [5]: event_id (32-bit)
        // [6-7]: reserved

        uint32_t* qentry = (uint32_t*)(queue_base + producer_idx * (QUEUE_ENTRY_SIZE / 4));

        qentry[0] = cmd.opcode | (cmd.stream_id << 8) | (cmd.size >> 2);
        qentry[1] = cmd.src_addr & 0xFFFFFFFF;
        qentry[2] = cmd.src_addr >> 32;
        qentry[3] = cmd.dst_addr & 0xFFFFFFFF;
        qentry[4] = cmd.dst_addr >> 32;
        qentry[5] = cmd.event_id;

        // 步骤 4:推进生产者指针(硬件看到后开始执行)
        // 内存屏障确保命令写入完成后再推进指针
        __builtin_arm_dmb();  // 数据内存屏障
        producer_idx = next_producer;

        // 步骤 5:通知硬件(doorbell)
        WriteBAR0(bar0_base, 0x1010, producer_idx);  // HOST_DMA_DOORBELL

        return Status::OK;
    }

    Status WaitForEvent(uint64_t event_id) {
        // 轮询 Event 寄存器,直到事件完成
        uint64_t event_reg = bar0_base + 0x2000 + event_id * 8;
        volatile uint64_t* event_ptr = (volatile uint64_t*)MapPhysicalMemory(event_reg, 8);

        while (*event_ptr != 1) {
            // 轮询等待(生产环境用中断,这里简化)
        }

        // 清理事件
        *event_ptr = 0;
        return Status::OK;
    }
};

DMA 命令提交的关键路径:MMIO store(~150ns)→ doorbell(~30ns)→ 硬件队列消费(DMA 引擎读队列,~200ns 启动延迟)→ 实际传输(1.2TB/s HBM 带宽)。

踩坑一:PCIe BAR 大小不够导致的 MMIO 配置失败

Ascend 910 的 BAR0 是 32KB——这是 PCIe 配置的 BAR 大小,不能动态扩。如果用户态驱动注册了太多 MMIO 映射(如 64 个 Stream × 32KB register space = 2MB > 32KB),BAR0 映射失败 → torch.npu.init() 抛 RuntimeError。

// ❌ 每个 Stream 映射 32KB register space → BAR0 32KB 只能容纳 1 个 Stream
for (int s = 0; s < MAX_STREAMS; s++) {
    uint64_t stream_base = reg_base + s * 32768;  // 32KB per stream
    RegisterMMIO(stream_base, 32768);  // 第 2 个 Stream 就失败
}

// ✅ 按需映射:只有活跃的 Stream 才占用 BAR0 slot
// 90% 的情况下活跃 Stream < 4 → 4 × 32KB = 128KB > 32KB → 还是不够
// 改进:bar0 只是窗口,实际映射用 BAR2(64GB HBM 映射)
// Stream 的寄存器空间映射到 HBM 地址(BAR2),bar0 只保留 1 个窗口

解决方案:stream register space 不映射到 BAR0——映射到 BAR2 的保留区域(HBM 物理地址)。bar0 只保留一个 4KB 窗口用于全局寄存器(中断、设备状态)。

踩坑二:Buddy System 的外部碎片和 OOM 假警报

56GB HBM 分配成 1GB + 2GB + 1GB + 52GB 后释放 2GB 那个块 → buddy 系统有 1GB + 1GB + 52GB(连续),但请求 3GB → 找不到连续 3GB 块(1+1+52 不连续,需要 52GB 那个完整块分裂)。OOM 假警报——实际上有 54GB 空闲。

// ❌ 分配/释放模式导致外部碎片
// 分配:1GB (order 9), 2GB (order 10), 1GB (order 9)
// 释放 2GB(中间的 order 10 块)
// 空闲:1GB (order 9) | 2GB (order 10, free) | 1GB (order 9) | 52GB (order 16)
// ↑ 1+1=2GB,但不在同一 buddy pair → 不能合并成 order 10
// 申请 3GB → 找不到连续 order 11+ → 失败

// ✅ compact:移动 1GB 块,重组 buddy pair
Compact();  // 把 1GB 块移动到 52GB 区域的末尾
// 空闲:4GB (order 11) | 50GB (order 16)
// 申请 3GB → 4GB 分裂 → 成功

HBM compact 的代价:暂停所有 DMA 传输(H2D/D2H 暂停),结束当前 kernel(等最后一个 kernel 完成),移动 HBM 块(D2D 内部拷贝),恢复 DMA。整个过程 ~5-10ms,但只在 OOM 时触发。

踩坑三:轮询 Event 寄存器的 CPU 浪费

上面的 WaitForEvent 用轮询(spin-wait)——如果 Event 在 100μs 后完成,轮询了 100μs × 3GHz CPU = 300K 个 cycle。如果有 32 个 Stream 都在等不同 Event → 32 × 100μs 的 CPU 时间 → CPU 占用 100%。

// ❌ spin-wait:CPU 空转 100μs(浪费 CPU 时间)
while (*event_ptr != 1) {}  // 3GHz CPU = 100μs = 300K 次读 + 300K 次分支预测

// ✅ 中断驱动(Edge-triggered MSI)
// 配置 MSI-X 中断向量
void SetupMSIX(int interrupt_vector) {
    // 写 MSI-X 表(PCIe Capability 0x11)
    uint64_t msix_table = ReadPCICapability(bus, dev, func, 0x11);
    WriteMSIXEntry(msix_table, interrupt_vector,
                   event_base + event_id * 8,  // 中断产生时赋值为 1
                   0);                           // 中断清除时赋值为 0

    // 注册中断处理函数
    RegisterInterruptHandler(interrupt_vector, [event_id]() {
        // 中断到达 → 设置 Event = 1 → WaitForEvent 返回
        *event_ptr = 1;
    });
}

// ✅ 中断驱动的等待(CPU 不空转)
Status WaitForEventIRQ(uint64_t event_id) {
    if (*event_ptr == 1) {
        *event_ptr = 0;
        return Status::OK;  // 已经完成,不用等
    }
    // 阻塞直到中断到达
    sem_wait(&event_semaphores[event_id]);
    // 中断到达 → 自动唤醒
    *event_ptr = 0;
    return Status::OK;
}

中断驱动的等待:CPU 不空转,但这增加了上下文切换开销(~5μs)。100μs 的 DMA 等待 → 中断开销占比 5%。如果用轮询,CPU 占用 100%。权衡:短等待(< 10μs)用轮询,长等待(> 50μs)用中断


driver 层从 PCIe 枚举(直接读配置空间,不依赖 OS 驱动)→ HBM 页表(Buddy System + 2MB 大页,合并/分裂 O(1))→ DMA 命令提交(MMIO store 150ns → doorbell 30ns → 硬件队列启动 200ns)。三个踩坑:BAR0 32KB 映射不够用 → 映射到 BAR2 HBM 保留区、Buddy System 外部碎片 OOM 假警报 → compact 移动块重组 buddy pair(5-10ms)、spin-wait 轮询 Event 寄存器占满 CPU → 短等待轮询 + 长等待中断的混合策略。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐