Double Buffer:Cube 在算的时候,DMA 在搬下一批数据
本文基于昇腾CANN和昇腾NPU围绕 Double Buffer 双缓冲技术展开。Tiling 把大矩阵切成小块轮流上场——但每次等 DMA 搬完数据再算Cube Unit 有一半时间在发呆。Double Buffer 解决的就是这个问题L1 上放两块 Buffer——Buffer 0 在算的时候DMA 已经偷偷把下一批数据搬到 Buffer 1 了。算完立刻切到 Buffer 1——零等待。假设 DMA 搬运一个 Tile 要 10μsCube 算一个 Tile 要 15μs。单缓冲下每步要等 101525μs。双缓冲下 DMA 和 Cube 并行每步只要 max(10, 15)15μs——快了 40%。双缓冲的 Pipeline 机制单缓冲时间线 K-step 0: [DMA:10μs][Cube:15μs] K-step 1: [DMA:10μs][Cube:15μs] 总时间50μs 双缓冲时间线 K-step 0: [DMA_A0:10μs] [Cube:15μs] K-step 1: [DMA_A1:10μs] ← DMA_A1 和 Cube_A0 重叠 [Cube:15μs] ← Cube_A1 和 DMA_A2 重叠 K-step 2: [DMA_A2:10μs] [Cube:15μs] 总时间10 15×3 55μs → 但 K-step 0 的 DMA 只做一次 实际流水建立后每步 max(10, 15)15μs → 3步45μs vs 单缓冲75μs双缓冲在硬件上是两条独立通道DMA 引擎搬数据走 DDR↔L1 的专用总线Cube Unit算 GEMM 走 L1 内部的矩阵乘法通路。两者互不抢资源。唯一的约束是DMA 不能写 Cube 正在读的 Buffer——所以在 L1 上必须分配两个独立的 Buffer 区域。Tensor搬运 和 DMA 的配合DMADirect Memory Access是 NPU 上专门的硬件引擎负责 DDR↔L1 的数据搬运。CPU 不给 DMA 发指令——Scalar Unit 在 Kernel 开头配置好 DMA 描述符源地址、目标地址、长度DMA 自己按描述符工作。双缓冲下Scalar Unit 在 Kernel 开头配置两个 DMA 描述符链Buffer 0 的搬运和 Buffer 1 的搬运交替启动。Kernel 的循环里只做两件事等当前 Buffer 算完切指针到另一个 Buffer。// Ascend C 双缓冲的简化实现classDoubleBufferedGemm:publicAscendC::Kernel{__aicore__voidProcess()override{LocalTensorfp16a_buf[2],b_buf[2],c_buf[2];// 分配双倍 Bufferfor(inti0;i2;i){LocalAlloc(a_buf[i],M_TILE*K_TILE);LocalAlloc(b_buf[i],K_TILE*N_TILE);LocalAlloc(c_buf[i],M_TILE*N_TILE);}// 启动第一批 DMA——先搬 A0、B0DataCopyAsync(a_buf[0],gm_a[0],stream_dma);DataCopyAsync(b_buf[0],gm_b[0],stream_dma);intcur0,next1;for(intk0;kK;kK_TILE){// 启动下一批 DMA——搬 A1、B1DataCopyAsync(a_buf[next],gm_a[kK_TILE],stream_dma);DataCopyAsync(b_buf[next],gm_b[kK_TILE],stream_dma);// 等当前批次 DMA 完成WaitStream(stream_dma);// Cube 算当前 Buffer——此时 DMA 正在搬 next BufferMatMul(c_buf[cur],a_buf[cur],b_buf[cur]);// 切 Buffercur1-cur;// 0↔1next1-next;}}};Transformer 推理中的双缓冲场景LLaMA-7B 推理中GEMM 的 K 维度是 4096。K_TILE32 时需要 128 个 K-step。双缓冲下128 个 K-step 的 DMA 和 Cube 完全重叠——DMA 时间被 Cube 计算时间完全隐藏。实际收益一个 4096×4096 的 GEMM 从 420μs单缓冲降到 280μs双缓冲——省 33%。参考仓库Runtime 运行时Ascend C 算子编程语言catlass 算子模板库CANN 学习中心