4、协同计算架构设计:异构计算平台(CPU+FPGA+GPU)的拓扑结构、数据流设计、任务划分策略

各位同学,今天我们来聊聊异构计算平台的核心——CPU+FPGA+GPU这三兄弟怎么搭伙过日子。

说实话,我最早接触异构计算时,觉得不就是把三个芯片焊在一块板子上嘛。后来被现实狠狠教育了一顿——拓扑结构没选对,数据流卡成狗;任务分不好,性能还不如单颗CPU。嗯,这里面的门道,我今天一次性讲透。

4.1 拓扑结构:三种主流玩法

我个人习惯把异构拓扑分成三类,每一类都有它的脾气。

4.1.1 共享总线型(入门级)

CPU、FPGA、GPU都挂在同一条PCIe总线上。结构简单,成本低。

┌──────┐     ┌──────┐     ┌──────┐
│ CPU  │     │ FPGA │     │ GPU  │
└──┬───┘     └──┬───┘     └──┬───┘
   └────────────┼────────────┘
                │
          ┌─────┴─────┐
          │  PCIe总线  │
          └───────────┘

我在项目中遇到过这种方案,优点是开发快,缺点是带宽共享。你想想看,FPGA正在疯狂搬运数据,GPU那边要拉模型权重,两边抢总线,谁也别想好过。

⚠️ 避坑指南:我曾经在一个视频处理项目里用了共享总线,结果FPGA和GPU同时读写时,PCIe带宽被吃满,CPU连控制指令都发不出去。后来我加了个DMA优先级仲裁才解决。

4.1.2 直连型(进阶级)

CPU和FPGA之间走PCIe,FPGA和GPU之间走NVLink或CXL。说白了,就是把最频繁的数据通路单独拉出来。

┌──────┐  PCIe  ┌──────┐  NVLink  ┌──────┐
│ CPU  │◄──────►│ FPGA │◄────────►│ GPU  │
└──────┘        └──────┘          └──────┘

这种结构的好处是:FPGA做预处理,直接把结果喂给GPU,不用经过CPU中转。延迟能降一个数量级。

我建议做VLA模型推理加速时优先考虑这种。VLA模型里视觉编码器那部分,FPGA做完特征提取,直接通过NVLink塞给GPU做LLM推理,爽得很。

4.1.3 交换网络型(骨灰级)

所有节点通过高速交换网络互联,比如InfiniBand或Ethernet RoCE。适合多节点集群。

        ┌─────────── 交换网络 ───────────┐
        │                                 │
   ┌────┴────┐                      ┌────┴────┐
   │ CPU+FPGA│                      │ GPU集群 │
   │  节点1   │                      │  节点N   │
   └─────────┘                      └─────────┘

这种方案我在云端推理集群里用过。说实话,成本高,但扩展性好。如果你要做大规模VLA模型服务,这是终极形态。

4.2 数据流设计:让数据跑起来

拓扑结构定了,接下来就是数据怎么流。我总结了一个口诀:流水线、乒乓缓冲、异步传输

4.2.1 典型数据流路径

以VLA模型为例,数据流是这样的:

  1. 输入阶段:CPU接收用户请求(文本+图像),做初步解析
  2. 预处理阶段:FPGA做图像缩放、归一化、token化
  3. 推理阶段:GPU运行LLM主干网络
  4. 后处理阶段:FPGA做beam search、采样
  5. 输出阶段:CPU组装响应,返回给用户

关键点:FPGA在这里扮演了两个角色——前端的"加速预处理"和后端的"加速解码"。GPU只管做最重的矩阵运算。

4.2.2 乒乓缓冲设计

我习惯在FPGA内部用双缓冲(ping-pong buffer)。一个缓冲区在写数据,另一个在读数据,交替进行。

// 伪代码示意
always @(posedge clk) begin
    if (toggle) begin
        // 写 ping 缓冲区
        ping_buffer[addr] <= data_in;
        // 读 pong 缓冲区给GPU
        data_out <= pong_buffer[rd_addr];
    end else begin
        // 写 pong 缓冲区
        pong_buffer[addr] <= data_in;
        // 读 ping 缓冲区给GPU
        data_out <= ping_buffer[rd_addr];
    end
end

这样做的好处是:FPGA和GPU可以同时工作,不用互相等。我曾经在项目里没做乒乓缓冲,结果GPU每次都要等FPGA写完才能读,吞吐量直接腰斩。

4.2.3 异步传输与DMA

千万别让CPU参与数据搬运!用DMA控制器。FPGA内部例化一个XDMA IP,直接和GPU的显存做点对点传输。

💡 小技巧:我建议把DMA描述符环设计成256深度,这样CPU只需要每256次传输才中断一次,大大降低CPU负载。

4.3 任务划分策略:谁该干什么活

这是最考验经验的地方。任务分得好,1+1+1>3;分不好,1+1+1<1。

4.3.1 任务划分原则

计算单元 适合的任务 不适合的任务
CPU 控制逻辑、分支判断、动态调度 大规模并行计算、固定流水线
FPGA 固定流水线、低延迟预处理、位宽变换 复杂控制流、动态分支
GPU 大规模矩阵运算、Transformer推理 小批量、强依赖的串行任务

说白了,CPU管脑子,FPGA管手脚,GPU管力气活

4.3.2 VLA模型的具体划分

以我最近做的一个VLA模型为例:

  • CPU负责:请求调度、token管理、异常处理、结果组装
  • FPGA负责:图像预处理(resize、normalize)、视觉token嵌入、KV cache管理、beam search加速
  • GPU负责:LLM的self-attention、FFN、softmax等核心计算

为什么这么分?FPGA做KV cache管理特别香。因为KV cache的读写模式固定,FPGA可以用HBM带宽做到极低延迟。GPU做这个反而浪费算力。

4.3.3 动态负载均衡

任务不是一成不变的。我建议在CPU上跑一个轻量级的调度器,实时监控各单元的负载。

// 调度策略伪代码
if (FPGA负载 < 30%) {
    // 把部分GPU的预处理任务挪给FPGA
    migrate_task(GPU_PREPROCESS, FPGA);
} else if (GPU利用率 > 95%) {
    // 启动FPGA的投机解码
    enable_speculative_decoding(FPGA);
}

嗯,这里要注意:动态调度不能太频繁,否则调度开销会吃掉收益。我一般设置100ms为一个调度周期。

4.4 实战经验总结

最后,分享几个我踩过的坑:

  • 别让FPGA做浮点运算:FPGA做float32效率极低,用INT8或BF16。我见过有人非要在FPGA里做float32的softmax,结果面积爆炸。
  • PCIe带宽是瓶颈:实测PCIe Gen4 x16的理论带宽是32GB/s,实际能跑到25GB/s就不错了。如果数据量超过这个,考虑用CXL或NVLink。
  • 热数据要就近存放:FPGA的BRAM和URAM很宝贵,只放最热的数据。比如VLA模型的视觉token嵌入表,放BRAM里;模型权重放HBM里。

💡 我的习惯:每次设计异构系统,我都会先画一张数据流图,标出每个节点的带宽需求和延迟要求。然后对着图问自己三个问题:数据从哪里来?到哪里去?中间有没有瓶颈?

好了,这一章的内容就到这里。记住,异构计算不是把三个芯片拼在一起就完事了,而是要让它们各司其职、协同作战。下一章我们会深入FPGA内部的微架构设计,讲讲怎么把VLA模型的计算图映射到FPGA的LUT和DSP上。


公众号:蓝海资料掘金营,微信deep3321