Ping-Pong 双缓冲

概述

Ping-Pong 双缓冲为了让"搬数据"和"算数据"同时进行,用两块缓冲区轮流工作,把搬运时间"藏"在计算时间后面。

串行调度的浪费

假设我们要处理一个张量,把它切成 4 块,每块需要:

  • 搬运时间 Tdma=2T_{dma}=2Tdma=2 个时间单位(从 DDR 搬到片上 SRAM)
  • 计算时间 Tcalc=2T_{calc}=2Tcalc=2 个时间单位(在计算单元上算)

串行调度

text 复制代码
步骤:
  搬块1 → 算块1 → 搬块2 → 算块2 → 搬块3 → 算块3 → 搬块4 → 算块4

时间线(每个格子 = 1 个时间单位):
  搬1: ██
  算1:   ██
  搬2:     ██
  算2:       ██
  搬3:         ██
  算3:           ██
  搬4:             ██
  算4:               ██

总时间 = 4 × (2 + 2) = 16 个时间单位

问题:搬运时计算单元闲着,计算时 DMA 闲着。两个硬件资源永远只有一个在工作,利用率只有 50%。

双缓冲的思路

核心思想

准备两块缓冲区(Buffer A 和 Buffer B):

  • 当 DMA 在往 Buffer A 搬第 i 块时,计算单元从 Buffer B 读第 i-1 块并计算。
  • 当 DMA 搬完第 i 块、计算单元算完第 i-1 块后,两者交换角色。
text 复制代码
Buffer A: [搬块1][算块1][搬块3][算块3]
Buffer B:        [搬块2][算块2][搬块4][算块4]
                  ↑
            搬块2 与 算块1 同时进行

时间线

text 复制代码
时间 →  0  1  2  3  4  5  6  7  8  9  10
        ─────────────────────────────────
DMA:    搬1 搬1 搬2 搬2 搬3 搬3 搬4 搬4
计算:              算1 算1 算2 算2 算3 算3 算4 算4

具体:
  t=0~1: DMA 搬块1 到 Buffer A;计算单元空闲(等第一块)
  t=2~3: DMA 搬块2 到 Buffer B;计算单元同时算块1(从 Buffer A 读)
  t=4~5: DMA 搬块3 到 Buffer A;计算单元同时算块2(从 Buffer B 读)
  t=6~7: DMA 搬块4 到 Buffer B;计算单元同时算块3(从 Buffer A 读)
  t=8~9: DMA 空闲;计算单元算块4(从 Buffer B 读)

总时间 = 2(第一块搬运)+ 4 × 2(计算,与后续搬运重叠)= 10 个时间单位

加速比 :16 / 10 = 1.6×

为什么不是 2×?

因为第一块搬运时计算单元必须等待(没有数据可算)。如果块数很多,这个"启动开销"会被摊薄:

总时间=Tdma+N×max⁡(Tdma,Tcalc)T_{dma}+N\times\max(T_{dma},T_{calc})Tdma+N×max(Tdma,Tcalc)

当 N 很大时,TdmaT_{dma}Tdma可以忽略,加速比趋近于:

N×(Tdma+Tcalc)N×max⁡(Tdma,Tcalc)=Tdma+Tcalcmax⁡(Tdma,Tcalc) \frac{N\times(T_{dma}+T_{calc})}{N\times\max(T_{dma},T_{calc})}=\frac{T_{dma}+T_{calc}}{\max(T_{dma},T_{calc})} N×max(Tdma,Tcalc)N×(Tdma+Tcalc)=max(Tdma,Tcalc)Tdma+Tcalc

当 Tdma=TcalcT_{dma} = T_{calc}Tdma=Tcalc 时,加速比 = 2x

完整数值例子:4 块数据

参数

参数
数据块数 NNN 4
每块搬运时间 TdmaT_{dma}Tdma 2
每块计算时间 TcalcT_{calc}Tcalc 2
缓冲区数量 2(A 和 B)

串行调度逐步计算

text 复制代码
块1: 搬2 + 算2 = 4
块2: 搬2 + 算2 = 4
块3: 搬2 + 算2 = 4
块4: 搬2 + 算2 = 4
总时间 = 16

双缓冲调度逐步计算

text 复制代码
t=0~1: DMA 搬块1 → Buffer A
t=2~3: DMA 搬块2 → Buffer B  |  计算单元算块1(Buffer A)
t=4~5: DMA 搬块3 → Buffer A  |  计算单元算块2(Buffer B)
t=6~7: DMA 搬块4 → Buffer B  |  计算单元算块3(Buffer A)
t=8~9: DMA 空闲              |  计算单元算块4(Buffer B)

总时间 = 10

对比

调度方式 总时间 加速比 DMA 利用率 计算单元利用率
串行 16 50% 50%
双缓冲 10 1.6× 80% 80%

当搬运和计算时间不相等时

场景 A:搬运是瓶颈(Tdma=2,Tcalc=1)(Tdma=2,Tcalc=1)(Tdma=2,Tcalc=1)

text 复制代码
t=0~1: 搬块1 → A
t=2~3: 搬块2 → B  |  算块1(A)
t=4~5: 搬块3 → A  |  算块2(B)
t=6~7: 搬块4 → B  |  算块3(A)
t=8~9: 搬块5 → A  |  算块4(B)
...

总时间 = 2 + N × max(2, 1) = 2 + 4 × 2 = 10
串行总时间 = N × (2 + 1) = 12
加速比 = 12 / 10 = 1.2×

结论:当搬运是瓶颈时,双缓冲只能把计算"藏"在搬运后面,但搬运总量没变,总时间仍由搬运主导。

场景 B:计算是瓶颈(Tdma=1,Tcalc=2)(Tdma=1,Tcalc=2)(Tdma=1,Tcalc=2)

text 复制代码
t=0: 搬块1 → A
t=1~2: 搬块2 → B  |  算块1(A)
t=3~4: 搬块3 → A  |  算块2(B)
t=5~6: 搬块4 → B  |  算块3(A)
t=7~8: 搬块5 → A  |  算块4(B)
...

总时间 = 1 + N × max(1, 2) = 1 + 4 × 2 = 9
串行总时间 = N × (1 + 2) = 12
加速比 = 12 / 9 = 1.33×

结论:当计算是瓶颈时,双缓冲把搬运完全藏住了,总时间由计算主导。

此时应该考虑减少计算量(如量化、更高效的算子)或增加并行度(更多 Lane、更大阵列)。

Python 模拟代码

python 复制代码
def simulate(n_blocks, t_dma, t_calc, double_buffer=True):
    """模拟串行和双缓冲两种调度下的总耗时"""
    if not double_buffer:
        # 串行: 搬完一块才能算一块
        return n_blocks * (t_dma + t_calc)
    # 双缓冲: 第一块搬运先行, 之后搬运与计算重叠
    return t_dma + n_blocks * max(t_dma, t_calc)

# 三组场景
for t_dma, t_calc in [(1, 1), (2, 1), (1, 2)]:
    serial = simulate(100, t_dma, t_calc, double_buffer=False)
    pp = simulate(100, t_dma, t_calc, double_buffer=True)
    print(f"t_dma={t_dma} t_calc={t_calc}  "
          f"串行={serial:6.0f}  双缓冲={pp:6.0f}  加速比={serial/pp:.2f}x")

输出

text 复制代码
t_dma=1 t_calc=1  串行=   200  双缓冲=   101  加速比=1.98x   ← 平衡,几乎翻倍
t_dma=2 t_calc=1  串行=   300  双缓冲=   201  加速比=1.49x   ← 搬运是瓶颈
t_dma=1 t_calc=2  串行=   300  双缓冲=   201  加速比=1.49x   ← 计算是瓶颈

M1684X 的实际应用

BM1684X 每个 Lane 有 256KB 局部存储。

编译器通常把它拆成两块(甚至更多):

text 复制代码
每个 Lane 的 256KB 局部存储:
┌─────────────────────────────────────┐
│  Buffer A: 128KB                    │
│  Buffer B: 128KB                    │
└─────────────────────────────────────┘

工作流程:
  GDMA 搬第 i+1 块 → Buffer A
  计算单元算第 i 块 ← Buffer B
  下一轮交换:
  GDMA 搬第 i+2 块 → Buffer B
  计算单元算第 i+1 块 ← Buffer A

代价:每块数据最大只能占 128KB(因为要留一半给另一块)。如果块太大,放不下,就需要缩小分块,可能增加搬运次数。

编译器要权衡:

分块大小 Buffer 数量 搬运次数 片上空间 重叠效果
大块 2 刚好
小块 2 充裕 好,但搬运开销占比高
大块 1(无重叠) 充裕 无重叠,串行

总结

概念 含义
串行调度 搬完才能算,算完才能搬下一块,硬件利用率 50%
双缓冲 两块 buffer 轮流工作,搬第 i+1 块的同时算第 i 块
总时间 Tdma+N×max⁡(Tdma,Tcalc)T_{dma} + N \times \max(T_{dma}, T_{calc})Tdma+N×max(Tdma,Tcalc)
加速比 接近 2×2\times2×(当 Tdma≈TcalcT_{dma} \approx T_{calc}Tdma≈Tcalc 时)
瓶颈 若 Tdma>TcalcT_{dma} > T_{calc}Tdma>Tcalc,总时间由搬运主导,需减少搬运量
代价 片上空间翻倍,分块大小受限制

一句话:Ping-Pong 双缓冲让"搬"和"算"同时进行,把搬运时间藏在计算时间后面。

但它只能消除"等待",不能消除"总量"------如果搬运本身是瓶颈,还需要从量化、融合、数据复用等角度减少搬运量。

相关推荐
三十岁老牛再出发1 小时前
9月18日总结
python·深度学习·机器学习
m0_734571761 小时前
深入理解C++ RAII
开发语言·c++
泡海椒1 小时前
jquick-pdf 表格实战:动态数据 PDF 报表生成
java·开发语言·pdf
bmxy小明同学1 小时前
2026-09-18-embedding选型
人工智能
superxxd1 小时前
基于rust的多平台原生GIS引擎
人工智能·物联网·实时音视频
就叫你天选之人啦1 小时前
安装torch+vllm+flash_attn的prompt
人工智能·pytorch·python
aramae1 小时前
模拟实现memset()(C语言)
c语言·开发语言·后端
音视频牛哥1 小时前
从 LLM、VLA、LLA、SLIM 到实时音视频感知底座:具身智能真正需要的不只是大模型
人工智能·llm·机器人视觉·slam·vla·多模态感知·机器人音视频
code2cat2 小时前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能