🏠

baseline_pl_packet_kernels 模块技术深度解析

概述:为什么需要这个模块?

想象你正在运营一个大型物流分拣中心。有100个仓库(Compute Units)同时运作,每个仓库都需要接收特定的货物包裹,处理后再将结果发送到指定的出口。问题在于:如何确保每个包裹准确送达正确的仓库,并将处理后的结果路由到正确的目的地?

这就是 baseline_pl_packet_kernels 模块要解决的问题——它是 Versal ACAP 架构中 PL(Programmable Logic)侧的数据包路由枢纽,负责在 DMA 数据流与 100 个 AIE Compute Unit 之间建立高效、可扩展的通信通道。

核心挑战

在 N-Body 仿真系统中,我们需要:

  1. 一对多分发:将输入数据广播到 100 个并行计算单元
  2. 多对一聚合:从 100 个计算单元收集结果并按类型重组
  3. 零拷贝传输:避免不必要的数据复制,保持吞吐率
  4. 包交换语义:利用 AIE 的包交换网络实现灵活路由

解决方案概览

该模块包含两个互补的 HLS Kernel:

  • packet_sender:数据分发器,将 DMA 输入流拆分为 100 路输出,每路附加包头部信息
  • packet_receiver:数据聚合器,从 100 路输入中读取数据,根据包头 ID 路由到 4 个输出通道

架构设计:数据如何流动?

flowchart LR subgraph Host["Host/DMA"] DDR[(DDR Memory)] end subgraph PL["PL (FPGA Fabric)"] PS[packet_sender
1→100 Demux] PR[packet_receiver
100→4 Mux] end subgraph AIE["AIE Array (100 CUs)"] CU0[CU 0] CU1[CU 1] CU99[CU 99] end DDR -->|AXI4-Stream| PS PS -->|tx0-tx99| CU0 PS -->|tx0-tx99| CU1 PS -->|tx0-tx99| CU99 CU0 -->|rx0-rx99| PR CU1 -->|rx0-rx99| PR CU99 -->|rx0-rx99| PR PR -->|tx0-tx3| DDR

数据流详解

发送路径(Host → AIE)

┌─────────────────────────────────────────────────────────────┐
│  packet_sender 工作流                                        │
├─────────────────────────────────────────────────────────────┤
│  外层循环: cu = 0..99 (遍历所有 Compute Units)               │
│  中层循环: h = 0..3   (每个 CU 处理 4 个 packets)           │
│  内层循环: i = 0..223 (每个 packet 224 个数据字)            │
│                                                             │
│  每轮迭代:                                                  │
│    1. 生成 Header (32-bit): [PktType(3b)|ID(5b)|...]       │
│    2. 写入 Header 到 tx[cu]                                 │
│    3. 从 rx 读取 224 个数据字                               │
│    4. 转发到 tx[cu],最后一个字标记 TLAST=1                 │
└─────────────────────────────────────────────────────────────┘

关键设计决策:Header 生成使用 generateHeader() 函数,其中包含奇偶校验位(bit 31),用于硬件层面的数据完整性检查。

接收路径(AIE → Host)

┌─────────────────────────────────────────────────────────────┐
│  packet_receiver 工作流                                      │
├─────────────────────────────────────────────────────────────┤
│  外层循环: cu = 0..99 (遍历所有 Compute Units)               │
│  中层循环: h = 0..3   (每个 CU 返回 4 个 packets)           │
│  内层循环: i = 0..223 (每个 packet 224 个数据字)            │
│                                                             │
│  路由逻辑:                                                  │
│    1. 从 rx[cu] 读取 Header                                 │
│    2. 提取 Packet ID (header[4:0])                          │
│    3. 查表得到 channel = packet_ids[ID]                     │
│    4. 后续 224 个数据字全部路由到 tx[channel]               │
└─────────────────────────────────────────────────────────────┘

关键设计决策:路由表 packet_ids 在编译时从 AIE 工具链生成的头文件导入,确保软硬件路由配置一致。


核心抽象:理解代码的思维模型

1. 包格式(Packet Format)

// 32-bit Header 布局(MSB -> LSB)
// ┌────┬─────────┬─────────┬──────┬─────┬─────┬─────┐
// │ 31 │ 30:28   │ 27:21   │20:16 │ 15  │14:12│11:5 │4:0 │
// ├────┼─────────┼─────────┼──────┼─────┼─────┼─────┤
// │Parity│Reserved│Src Col  │Src Row│Reserved│Type │Reserved│ID  │
// └────┴─────────┴─────────┴──────┴─────┴─────┴─────┘
// Parity = XOR reduction of bits[30:0]

2. 流接口抽象

两个 Kernel 都使用 hls::stream<axis_pkt> 作为核心抽象:

  • axis_pkt = ap_axiu<32, 0, 0, 0> —— AXI4-Stream 协议封装
    • data: 32-bit 有效载荷
    • keep: 字节使能(全1表示有效)
    • last: 包结束标记(TLAST)

3. 计算单元索引(CU Indexing)

#define NUM_CU 100      // 支持的并行计算单元数量
#define PACKET_NUM 4    // 每个 CU 处理的 packet 数量
#define PACKET_LEN 224  // 每个 packet 的数据字数

关键设计决策与权衡

决策 1:显式 Switch-Case vs 数组索引

观察到的实现:代码使用了长达 100 个 case 的 switch 语句来选择流:

switch(cu) {
    case 0: tx0.write(tmp); break;
    case 1: tx1.write(tmp); break;
    // ... 直到 case 99
}

为什么不使用数组?

// 更简洁但不适用于 HLS 的写法:
tx[cu].write(tmp);  // ❌ 无法综合为独立 AXI Stream 端口

权衡分析

  • Switch-Case:HLS 可以为每个 case 生成独立的 AXI4-Stream 接口,满足 AIE 连接需求
  • 数组索引:会被综合为共享总线,无法满足 100 路并发独立流的需求
  • 💡 代价:代码冗长(200+ 行 switch),但保证了硬件并行性

决策 2:Header 先行模式

每个 packet 的结构:[Header][Data0][Data1]...[Data223]

为什么选择这种格式?

方案 优点 缺点
Header 先行 AIE 可以提前知道 packet 类型和长度,预分配资源 增加 1 个时钟周期的延迟
Header 嵌入 零开销 需要额外的同步机制告知 Header 位置

结论:对于 224 字节的 payload,1/224 的开销可以忽略不计,换取了更清晰的协议语义。

决策 3:静态路由表

static const unsigned int packet_ids[PACKET_NUM] = {
    in_i0_0, in_i0_1, in_i0_2, in_i0_3  // 宏定义来自 AIE 编译输出
};

设计意图

  • 路由表在编译时确定,避免运行时查找开销
  • 与 AIE 工具链生成的 packet_ids_c.h 保持一致,确保软硬件协同
  • 支持 4 种 packet 类型映射到 4 个输出通道

决策 4:Pipeline II=1 约束

for(int i = 0; i < PACKET_LEN; i++) {
#pragma HLS PIPELINE II=1
    x = rx.read();
    // ... write to tx
}

目标:每个时钟周期处理一个 32-bit 字,维持峰值带宽。

潜在瓶颈

  • Switch-case 可能引入组合逻辑延迟
  • 100-to-1 的 mux 在 packet_receiver 中可能成为关键路径

子模块详情

packet_sender —— 数据分发器

职责:将单一 DMA 输入流广播到 100 个 AIE Compute Unit,为每个 packet 添加路由头部。

接口规格

端口 方向 类型 说明
rx Input hls::stream<axis_pkt> DMA 数据源
tx0-tx99 Output hls::stream<axis_pkt> 到各 CU 的输出流

核心算法

// 伪代码表示
for each cu in 0..99:
    for each packet in 0..3:
        header = generateHeader(PKTTYPE, packet_ids[packet])
        tx[cu].write(header)
        for each word in 0..223:
            data = rx.read()
            tx[cu].write(data with TLAST=(word==223))

packet_receiver —— 数据聚合器

职责:从 100 个 AIE Compute Unit 收集结果,根据 packet ID 路由到 4 个输出通道。

接口规格

端口 方向 类型 说明
rx0-rx99 Input hls::stream<axis_pkt> 来自各 CU 的输入流
tx0-tx3 Output hls::stream<axis_pkt> 按类型聚合的输出流

核心算法

// 伪代码表示
for each cu in 0..99:
    for each packet in 0..3:
        header = rx[cu].read()
        id = getPacketId(header)
        channel = packet_ids[id]
        for each word in 0..223:
            data = rx[cu].read()
            tx[channel].write(data)

跨模块依赖关系

flowchart TD A[baseline_pl_packet_kernels] -->|包含| B[packet_sender.cpp] A -->|包含| C[packet_receiver.cpp] B -->|引用| D[Module_02_aie/build/Work_x4_x100/temp/packet_ids_c.h] C -->|引用| D E[baseline_full_system_packet_connectivity] -.->|实例化| A F[x1_design_packetized_pl_aie_integration] -.->|实例化| A G[x10_design_packetized_pl_aie_integration] -.->|实例化| A

上游依赖

  • Module_02_aie:提供 packet_ids_c.h,定义 packet ID 到路由通道的映射

下游使用者


新贡献者必读:陷阱与注意事项

⚠️ 1. Header/Payload 边界对齐

问题packet_receiver 期望第一个字是 Header,但如果 AIE Kernel 没有正确发送 Header,整个路由会错位。

调试技巧

// 在 testbench 中验证 Header 格式
std::cout << "Header: " << std::hex << header << std::endl;
// 预期格式: 0x8fff0000, 0x0fff0001, 0x0fff0002, 0x8fff0003

⚠️ 2. packet_ids 数组越界

getPacketId() 提取 header[4:0] 作为 ID(范围 0-31),但 packet_ids 只有 4 个元素。

契约:AIE 必须只发送 ID ∈ {0,1,2,3} 的 packets,否则数组越界行为未定义。

⚠️ 3. TLAST 信号处理

packet_sender 设置 x.last = (i==PACKET_LEN-1),这会影响 DMA 的 packet 边界检测。

注意:如果 TLAST 丢失或错位,DMA 可能挂起或产生错误中断。

⚠️ 4. 时钟域交叉

TCL 脚本设置时钟周期为 2.5ns(400MHz):

create_clock -period 2.5ns

确保与 AIE 阵列的时钟频率匹配,否则可能出现时序违例。

⚠️ 5. HLS 综合限制

修改代码时注意:

  • 不要尝试用循环生成 tx 端口数组,HLS 无法将其映射到独立 AXI Stream
  • 保持 #pragma HLS INTERFACE axis 对每个端口的显式声明
  • 修改 NUM_CU 需要同步更新 switch-case 的所有分支

性能特征

指标 数值 说明
时钟频率 400 MHz 2.5ns 周期
单通道吞吐 1.6 GB/s 32-bit @ 400MHz
总聚合吞吐 160 GB/s 100 通道 × 1.6 GB/s(理论峰值)
延迟 \(O(NUM\_CU \times PACKET\_NUM \times PACKET\_LEN)\) 流水线填充时间

总结

baseline_pl_packet_kernels 是 N-Body 仿真系统的数据高速公路收费站——它不负责计算,但决定了数据能否高效地到达正确的计算单元。理解它的关键在于把握 "显式并行 vs 代码简洁" 的权衡:那些看起来冗长的 switch-case 正是实现硬件级并行的必要代价。

当你需要扩展或修改这个模块时,问自己三个问题:

  1. 新的设计是否保持了 1-to-100 或 100-to-4 的拓扑结构?
  2. 路由表是否需要与 AIE 工具链重新同步?
  3. HLS 综合后的接口数量是否仍然匹配 graph 连接配置?
On this page