🏠

normalization_v1_pl_kernels 技术深度解析

概述:这个模块解决了什么问题?

想象你正在调试一条高速公路的通行能力,但入口和出口的车道本身成了瓶颈——你无法判断是高速公路(AIE-ML阵列)的设计问题,还是进出口(PL侧数据搬运)限制了整体吞吐量。normalization_v1_pl_kernels 模块正是为了解决这个测试基础设施问题而存在的。

该模块包含两个HLS(高层次综合)内核,作为 normalization_v1_performance_flow 教程中的数据源头和数据汇点。它们的设计目标是:以最大可能速率向AIE-ML图注入测试数据,并以同样高的速率消费输出数据,从而确保任何性能瓶颈都来自于AIE-ML图本身,而非PL侧的I/O限制。

核心设计洞察在于:在性能分析中,测试基础设施绝不能成为被测系统的制约因素。这就像用一根粗水管测试水泵——如果水管太细,你测量的是水管而非水泵的性能。


心智模型:如何理解这个模块?

将AIE-ML系统想象成一个精密的数据处理工厂:

  • AIE-ML阵列是工厂的生产线,执行z-score归一化计算(均值→标准差→归一化)
  • MemTile是工厂的原材料仓库,存储192KB的输入帧数据
  • PL内核则是工厂的进货码头和出货码头
┌─────────────────────────────────────────────────────────────────────────────┐
│                         完整系统数据流视图                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌──────────┐      ┌──────────────┐      ┌─────────────┐      ┌─────────┐ │
│   │ datagen  │─────▶│   MemTile    │─────▶│  AIE-ML     │─────▶│  s2ss   │ │
│   │ (数据源)  │      │  (共享缓冲)   │      │  归一化图    │      │ (数据汇) │ │
│   └──────────┘      └──────────────┘      └─────────────┘      └─────────┘ │
│        │                  │                      │                  │      │
│        ▼                  ▼                      ▼                  ▼      │
│   生成bfloat16        多播到3个              3阶段流水线           消费并   │
│   值: 0,1,2...7       内核: mean            处理数据              丢弃数据  │
│                       deviation                                          │
│                       norm                                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

核心抽象

  1. datagen —— 数据喷泉(Data Fountain)

    • 持续不断地产生确定性的测试数据
    • 每个128-bit AXI-Stream beat包含8个bfloat16值(0.0到7.0)
    • 通过AXI-Lite接口配置传输大小
  2. s2ss —— 数据黑洞(Data Sink)

    • 以II=1(每周期一个beat)的速率消费数据
    • 不存储、不处理,仅读取并丢弃
    • 确保AIE-ML输出端无反压(backpressure)

架构与数据流

组件交互图

flowchart LR subgraph PL["PL (Programmable Logic)"] DG["datagen
数据生成器"] S2SS["s2ss
流式接收器"] end subgraph AIE["AIE-ML Array"] IN["input_plio
Datain0"] MT["shared_buffer
mtxA"] K1["k_mean"] K2["k_deviation"] K3["k_norm"] OUT["output_plio
Dataout0"] end subgraph Host["Host CPU"] HOST["host.exe
控制程序"] end DG -->|"AXI-Stream
128-bit"| IN IN --> MT MT -->|"多播"| K1 MT -->|"多播"| K2 MT -->|"多播"| K3 K1 -->|"mean值"| K2 K2 -->|"mean+deviation"| K3 K3 --> OUT OUT -->|"AXI-Stream
128-bit"| S2SS HOST -->|"XRT控制"| DG HOST -->|"XRT控制"| S2SS HOST -->|"图控制"| AIE

数据流详细追踪

1. 数据生成阶段(datagen

// datagen.cpp 核心逻辑
for(int i=0; i<size; i++){
    ap_axis<128,0,0,0> tmp;
    // 打包8个bfloat16值到一个128-bit beat
    tmp.data(15,0)   = 0x0;     // bfloat16 = 0.0
    tmp.data(31,16)  = 0x3f80;  // bfloat16 = 1.0
    tmp.data(47,32)  = 0x4000;  // bfloat16 = 2.0
    tmp.data(63,48)  = 0x4040;  // bfloat16 = 3.0
    tmp.data(79,64)  = 0x4080;  // bfloat16 = 4.0
    tmp.data(95,80)  = 0x40a0;  // bfloat16 = 5.0
    tmp.data(111,96) = 0x40c0;  // bfloat16 = 6.0
    tmp.data(127,112)= 0x40e0;  // bfloat16 = 7.0
    tmp.keep = -1;  // 所有字节有效
    tmp.last = 0;   // 非包尾(由size控制结束)
    out.write(tmp);
}

设计意图:使用简单的递增序列(0-7)作为测试数据,便于验证归一化结果的正确性。经过z-score归一化后,这些值的均值为0、标准差为1,易于人工验证。

2. 数据传输阶段(PL → AIE-ML)

  • 数据通过 plio_128_bits 接口进入AIE-ML阵列
  • 写入MemTile共享缓冲区(256×384×2字节 = 192KB)
  • MemTile尝试多播到3个消费者内核:k_meank_deviationk_norm

3. AIE-ML处理阶段

三个内核形成生产者-消费者链

内核 功能 输入 输出 重复次数
k_mean 计算均值 32KB数据块 单个bfloat16均值 6次
k_deviation 计算标准差 32KB数据块 + 均值 mean+deviation(2个值) 6次
k_norm 执行归一化 32KB数据块 + mean/deviation 归一化后的32KB 6次

关键设计约束:每个AIE-ML Tile只有64KB内存,因此192KB帧必须分6次迭代处理(每次32KB)。

4. 数据消费阶段(s2ss

// s2ss.cpp 核心逻辑
for(int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1  // 每周期读取一个beat
    ap_axis<128, 0, 0, 0> x = s.read();  // 读取并丢弃
}

关键特性

  • II=1(Initiation Interval = 1)确保每个时钟周期都能消费一个128-bit beat
  • 这是理论上的最大吞吐率,确保不会成为系统瓶颈

组件深度解析

datagen —— 确定性数据生成器

文件: datagen.cpp

接口定义

void datagen(
    hls::stream<ap_axis<128,0,0,0>> & out,  // AXI-Stream输出
    int size                                 // 通过AXI-Lite配置的传输大小
)

内部机制

属性 说明
数据宽度 128-bit 对应plio_128_bits接口
数据类型 bfloat16 × 8 每个beat包含8个16位脑浮点数
数值模式 0.0, 1.0, 2.0, ..., 7.0 确定性递增序列
控制接口 s_axilite 运行时配置传输大小

为什么使用这些特定值?

bfloat16编码解析:

  • 0x0 = 0.0(符号位0,指数0,尾数0)
  • 0x3f80 = 1.0(符号位0,指数127,尾数0)
  • 0x4000 = 2.0(符号位0,指数128,尾数0)
  • ...以此类推

选择0-7的序列是因为:

  1. 简单可预测:便于手动验证归一化结果
  2. 覆盖典型范围:包含小整数,适合测试数值稳定性
  3. 确定性:每次运行相同输入,便于回归测试

s2ss —— 流式数据接收器

文件: s2ss.cpp

接口定义

void s2ss(
    hls::stream<ap_axis<128, 0, 0, 0>> & s,  // AXI-Stream输入
    int size                                  // 消费的数据beat数
)

关键编译指令

#pragma HLS PIPELINE II=1

这条指令告诉HLS工具:必须以每个时钟周期一个beat的速率处理数据。这是保证PL侧不成为瓶颈的核心设计决策。

为什么没有输出?

s2ss是一个纯消费型内核。它的唯一目的是:

  1. 从AIE-ML图接收输出数据
  2. 以足够快的速率读取,避免阻塞上游
  3. 不执行任何会引入延迟的处理

在真实应用中,这个位置可能是DMA将数据写回DDR,或者是下游处理逻辑。但在性能测试场景中,我们只想测量AIE-ML图的能力。


构建系统

文件: Makefileconfig.cfg

构建流程采用Vitis HLS的批量编译模式:

# 为每个.cpp文件生成对应的.xo目标
all: \((subst .cpp,.xo,\)(wildcard *.cpp))

%.xo: %.cpp
    # 动态替换config.cfg中的顶层模块名
    sed 's/s2mm/\((basename \)<)/' config.cfg > tmp.cfg
    v++ -c --mode hls --platform ${PLATFORM} --config tmp.cfg

关键设计

  • 使用同一个config.cfg模板,通过sed动态替换顶层模块名
  • 支持批量编译多个内核(当前有datagens2ss
  • 目标频率:400MHz(freqhz=400000000

设计权衡与决策

1. 确定性数据 vs 随机数据

选择的方案:使用固定的0-7递增序列

替代方案:使用随机数生成器

权衡分析

  • 确定性:便于调试和结果验证
  • 可重复:相同的输入产生相同的输出,便于回归测试
  • 不覆盖边界情况:没有测试极大/极小值、NaN、Inf等

为什么这样选择:这是一个教学示例,首要目标是可理解性可调试性,而非全面的鲁棒性测试。


2. 纯消费型Sink vs 带验证的Sink

选择的方案s2ss只读取数据,不做任何验证

替代方案:读取数据并与预期结果比较

权衡分析

  • 最大化吞吐率:没有任何比较逻辑带来的延迟
  • 职责分离:性能测试和正确性测试解耦
  • 无法检测输出错误:需要其他机制验证正确性

为什么这样选择:在版本1的设计中,主要目标是发现死锁问题(见下文),而非验证数值正确性。后续版本可以添加独立的验证步骤。


3. AXI-Lite控制 vs 固定大小

选择的方案:通过AXI-Lite接口在运行时配置size

替代方案:编译时固定传输大小

权衡分析

  • 灵活性:同一比特流可运行不同规模的测试
  • 参数化:host代码可以动态决定迭代次数
  • 额外资源:AXI-Lite接口消耗少量PL资源

为什么这样选择:教程需要运行不同迭代次数来测量吞吐量,运行时配置是必要的。


依赖关系

本模块调用/提供

方向 组件 关系说明
被调用 host.cpp XRT通过xrt::kernel接口启动PL内核
被调用 graph.h AIE图通过input_plio/output_plio连接PL内核
提供 datagen.xo 生成的Xilinx对象文件,链接到最终xclbin
提供 s2ss.xo 生成的Xilinx对象文件,链接到最终xclbin

系统级依赖图

normalization_v1/
├── pl_kernels/
│   ├── datagen.cpp ────────┐
│   ├── s2ss.cpp ───────────┤──► .xo文件 ───┐
│   ├── Makefile ───────────┤               │
│   └── config.cfg ─────────┘               │
│                                           ▼
├── aie/                              ┌──────────┐
│   ├── graph.h ─────────────────────▶│ xclbin   │
│   ├── mean.cc                       │ 构建     │
│   ├── deviation.cc                  └──────────┘
│   ├── norm.cc                             ▲
│   └── graph.cpp                           │
│                                           │
├── sw/                                     │
│   ├── host.cpp ───────────────────────────┘
│   └── Makefile
│
└── system.cfg ─────────────────────────────┘

已知问题与教学要点

版本1的死锁问题

normalization_v1的核心教学价值在于展示一个典型的AIE-ML死锁场景

死锁原因

  1. MemTile只有一个输出端口(out[0])多播到3个内核
  2. 3个内核以不同速率消费数据
  3. k_deviation等待k_mean的输出,但k_mean无法完成因为MemTile被阻塞
  4. 形成循环依赖死锁

死锁诊断信息(来自x86仿真):

x86simulator: Detected deadlock
Deadlock diagnosis:
  1. main() is waiting on kernel 'gr.k_mean'
     because Node 'gr.k_mean' is blocked while reading port 'gr.k_mean.in[0]'
  2. Node 'gr.k_mean' is blocked while reading port 'gr.k_mean.in[0]'
     because Data unavailable from port 'gr.k_mean.in[0]'
  ...

解决方案(在normalization_v2中实现):

  • 使用MemTile的3个独立输出通道(out[0]out[1]out[2]
  • 每个内核有自己的独立数据通路

对新贡献者的建议

  1. 不要修复版本1的死锁:这是有意为之的教学案例
  2. 如需修改数据模式:编辑datagen.cpp中的硬编码值,注意保持bfloat16格式
  3. 如需调整频率:修改config.cfg中的freqhz参数
  4. 扩展新内核:复制现有.cpp文件,Makefile会自动识别并编译

操作指南

单独编译PL内核

cd normalization_v1/pl_kernels
make all

输出:datagen.xos2ss.xo

清理构建产物

make clean

硬件运行

# 在完整系统构建后
./host.exe a.xclbin 9999

预期输出:

Throughput of the graph:1344.51M Bytes/s

参考文档


总结

normalization_v1_pl_kernels模块虽小,但承载了明确的架构职责:为AIE-ML性能分析提供无瓶颈的测试基础设施。它的设计哲学是最小干预——只做必要的数据搬运,不做任何可能影响性能测量的处理。

理解这个模块的关键在于认识到:它不是为了解决业务问题,而是为了暴露系统问题。就像医生用的听诊器,本身不产生治疗价值,但却是诊断的必要工具。版本1的死锁不是缺陷,而是这个诊断工具成功发现的第一个"病灶"。

On this page