AI 写 HLS 靠谱吗?我让 Codex 生成了一套 AXI-Stream Softmax 加速核
AI 写 HLS 靠谱吗?我让 Codex 生成了一套 AXI-Stream Softmax 加速核
AI 写 HLS 靠谱吗?我让 Codex 生成了一套 AXI-Stream Softmax 加速核
前两篇,我连续试用了一个专门面向 Verilog 开发的 Codex Skill。
第一次是 64×64 Conway Game of Life,主要观察单模块 RTL、状态机、双缓冲和代码可读性;第二次把难度继续往上拉,让它生成了一套带 AXI-Stream、TX/RX FIFO、分数 NCO、过采样和错误上报的全双工 UART。
这两次测试都在回答同一个问题:
AI 能不能不只是“吐一段看起来像 RTL 的代码”,而是给出一份能够继续阅读、审查和验证的工程结果?
这一次,我换了一条路线。
不让它继续写 Verilog,而是直接从算法层开始,试用 HLS Generator Skill 生成一套面向 AMD/Xilinx Vitis HLS 的 Softmax 加速核。
GitHub:https://github.com/Eriemon/hls-generator
我原本最担心的是,它最后只会给出一个带 std::exp() 的普通 C++ 函数,然后随手加几个 HLS pragma。
但实际拿到的生成结果比这个完整得多:
- AXI4-Stream 输入与输出;
- TLAST 可变长帧;
- 定点输入、指数、累加、倒数和概率输出;
- 数值稳定的 max-subtraction;
- 192 段 PWL 指数近似表;
- 三遍 Softmax 数据路径;
- 协议错误侧带;
- 基于 double 参考模型的自检 testbench;
- Vitis HLS
.cfg工程配置; - 面向数值误差、概率和、TLAST 与错误帧的验收条件。
一句话结论:
它没有只生成一个“能算 Softmax 的 C++ 函数”,而是在尝试生成一份可综合、可检查、可继续送进 Vitis HLS 验证的硬件工程。
不过先把边界说清楚:我目前拿到的是源码与配置,还没有同步拿到本次运行对应的 csim、csynth、cosim、资源和时序报告。因此下面能确认的是生成设计的结构与验证意图,不能直接写成已经完成全部 Vitis HLS 验证。
一、这次给它的任务是什么
这次测试目标不是固定长度的软件 Softmax,而是一套能够接入流式 FPGA 数据通路的 AXI4-Stream Softmax kernel。
本次生成结果中的主要配置如下:
| 配置项 | 本次设置 |
|---|---|
| HLS 工具链 | AMD/Xilinx Vitis HLS |
| Top function | softmax_axis |
| 输入接口 | AXI4-Stream,16 bit 数据 |
| 输出接口 | AXI4-Stream,16 bit 数据 + 1 bit TUSER |
| 帧边界 | TLAST |
| 最小帧长 | 1 |
| 最大帧长 | 256 |
| 输入格式 | ap_fixed<16, 6> |
| 输出格式 | ap_ufixed<16, 1> |
| 指数格式 | ap_ufixed<18, 1> |
| 倒数格式 | ap_ufixed<24, 1> |
| 指数累加格式 | ap_ufixed<28, 9> |
| 指数近似 | 192 段 PWL,区间 [-12, 0] |
| 目标循环 II | 1 |
| HLS 时钟约束 | 5 ns(200 MHz) |
| 控制协议 | ap_ctrl_none |
| 计划测试 case | 43 组 manifest 事务 |
Softmax 的数学表达式并不复杂:
y_i = exp(x_i) / Σ exp(x_j)
但把它做成硬件,并没有公式看起来这么简单。
首先,指数运算本身就不适合直接照搬软件实现;其次,Softmax 需要先知道整帧最大值和指数和,因此它天然包含帧级归约;再加上可变长度、TLAST、定点误差、异常帧和 AXIS 侧带,已经足够用来观察一个 HLS 生成 Skill 是否真的理解硬件约束。

二、这次不是只生成一个 softmax.cpp
最终生成结果被拆成了几个职责比较明确的文件:
softmax_project/
├── softmax_profile.hpp
├── softmax_pwl_table.hpp
├── softmax_core.hpp
├── softmax_top.cpp
├── softmax_tb.cpp
└── hls_config.cfg
其中:
softmax_profile.hpp:集中保存位宽、定点策略、帧长、误差阈值、PWL 范围和 AXIS 类型;softmax_pwl_table.hpp:保存指数近似使用的 192 组锚点与斜率;softmax_core.hpp:实现接收、最大值搜索、PWL 指数、求和、倒数与概率输出;softmax_top.cpp:提供真正交给 Vitis HLS 的 AXI4-Stream top function;softmax_tb.cpp:读取向量清单,计算 double 参考结果,并检查数值与协议;hls_config.cfg:声明 top、综合源文件、testbench、5 ns 时钟和 cosim 端口跟踪。
这个拆分方式是我比较认可的地方。
以前直接让模型写 HLS,数值位宽、接口定义、算法实现和测试阈值经常全部混在一个文件里。短代码看起来很省事,但一旦修改位宽或接口,代码、testbench 和配置很容易失去一致性。
这次至少把“设计画像”单独收进了 softmax_profile.hpp。

三、它先把 Softmax 的数值策略固定了下来
HLS 设计里一个很容易被忽略的问题是:
算法公式确定,不代表硬件数值格式已经确定。
这次生成结果没有把所有变量都写成 float,而是针对不同阶段使用了不同的定点格式:
using input_t = ap_fixed<16, 6, AP_RND_CONV, AP_SAT>;
using output_t = ap_ufixed<16, 1, AP_RND_CONV, AP_SAT>;
using exp_t = ap_ufixed<18, 1, AP_RND_CONV, AP_SAT>;
using reciprocal_t = ap_ufixed<24, 1, AP_RND_CONV, AP_SAT>;
using delta_t = ap_fixed<18, 7, AP_RND_CONV, AP_SAT>;
using sum_t = ap_ufixed<28, 9, AP_RND_CONV, AP_SAT>;
可以看到,它至少区分了六类数值:
- 原始 logits;
- 减去最大值后的有符号差值;
- PWL 指数结果;
- 帧级指数和;
- 指数和的倒数;
- 最终概率输出。
同时,profile 中还明确记录了三项验收阈值:
#define SOFTMAX_MAX_ABS_ERROR 0.002
#define SOFTMAX_L1_ERROR 0.01
#define SOFTMAX_SUM_ERROR 0.005
也就是说,testbench 不只是检查“程序有没有退出”,还要检查:
- 单个概率的最大绝对误差;
- 整帧概率向量的 L1 误差;
- 输出概率之和与 1 的偏差。
这比只比较几个手写样例更接近真正的定点算法验收。
当然,这些位宽是不是最优,还不能只靠代码判断。后续仍然要结合 csynth 报告,看 DSP、LUT、FF、BRAM、延迟和目标频率,再决定是否收窄或放宽某一级格式。
四、核心不是“算指数”,而是三遍帧级数据路径
为了避免指数溢出,Softmax 一般会先减去整帧最大值:
m = max(x_i)
e_i = exp(x_i - m)
s = Σ e_i
y_i = e_i / s
这次生成结果采用了一条比较直观的三遍路线:
AXIS logits frame
│
▼
第一遍:接收整帧、检查侧带、缓存输入、寻找最大值
│
▼
第二遍:计算 PWL exp(x - max)、缓存指数、累加分母
│
▼
第三遍:计算 1 / sum,逐项归一化并输出 AXIS 概率帧
在源码里,这三步分别由下面几个函数承担:
receive_frame(...);
compute_exp(...);
emit_probs(...);
这种实现并不是追求“最炫”的结构,而是先把帧级依赖关系讲清楚。
Softmax 必须先得到最大值,之后才能计算稳定指数;又必须先得到完整指数和,之后才能输出最终概率。因此,在没有引入更复杂的分块、在线归约或多核重叠之前,本地缓存是很自然的选择。
本次最大帧长为 256,所以核心保留了两组静态片上缓存:
static input_t arr_in_buf[256];
static exp_t arr_exp_buf[256];
第二遍和第三遍循环都写了:
#pragma HLS PIPELINE II=1
这里也要注意一个很容易被宣传文案混淆的点:
写了
II=1,代表设计目标是让对应循环每周期启动一次迭代;并不等于 Vitis HLS 一定已经实现 II=1,更不等于整个 Softmax 帧可以每周期完成一次。
最终 achieved II、单帧延迟和帧间吞吐,仍然必须以综合报告为准。
五、PWL 指数近似,是这次最值得看的部分
如果直接在 HLS C++ 里使用软件式 exp(),虽然写起来简单,但最后的资源、延迟和可控性未必理想。
这次生成结果采用了分段线性近似:
exp(delta) ≈ exp_anchor[index] + exp_slope[index] × local_offset
其中:
delta = x_i - max(x);delta >= 0时直接返回 1;delta < -12时直接截断为 0;[-12, 0]被划分为 192 段;- 每个单位区间包含 16 段;
- 单段宽度为
0.0625。
每一段保存两个定点系数:
struct PwlSegment {
exp_t exp_anchor;
exp_t exp_slope;
};
运行时只需要完成索引、局部偏移和一次线性插值。
这个思路的优点是,近似范围、表项数量和误差之间的关系比较清楚,也方便后续继续调节:
更多分段
→ 通常误差更小
→ 但系数存储和选择逻辑可能增加
更宽定点
→ 通常量化误差更小
→ 但乘法、加法和存储资源可能增加
不过现在还不能直接断言这张表最终会被映射成哪类 ROM,也不能断言这套 PWL 一定比目标器件上的其他指数实现更省资源。这个结论必须结合 Vitis HLS 的资源映射、延迟报告和替代方案对比来判断。
六、AXI4-Stream 不是只在 top function 上加两行 pragma
顶层接口比较简洁:
void softmax_axis(
hls::stream<input_word_t>& stream_in,
hls::stream<output_word_t>& stream_out
) {
#pragma HLS INTERFACE axis port=stream_in
#pragma HLS INTERFACE axis port=stream_out
#pragma HLS INTERFACE ap_ctrl_none port=return
softmax_core<Profile>(stream_in, stream_out);
}
但真正值得看的是核心内部对帧协议的处理。
输入 word 使用:
ap_axiu<16, 0, 0, 0>
输出 word 使用:
ap_axiu<16, 1, 0, 0>
输出额外保留了 1 bit TUSER,用于错误上报。
正常帧要求:
TKEEP和TSTRB必须与 16 bit 数据宽度匹配;- 输入以
TLAST结束; - 输出 beat 数量与输入帧长度一致;
- 只有最后一个输出 beat 携带
TLAST; - 正常概率帧的
TUSER错误位必须为 0。
如果发现侧带非法、帧长度超过 256,或者指数和退化为 0,设计不会继续输出一串看似正常的概率,而是产生一个单拍错误帧:
TDATA = 0
TUSER.error = 1
TLAST = 1
这说明生成器至少没有把 AXIS 理解成“函数参数换成 hls::stream 就结束了”,而是把帧边界与错误语义也写进了实现。
当然,C++ testbench 对 stream 的调用还不能完全替代 RTL 级 AXIS 背压验证。TVALID/TREADY 长时间停顿、输出阻塞和连续帧间隔,最好继续通过 cosim 波形检查。

七、这次最让我意外的,是 testbench 不只是“喂几个数”
softmax_tb.cpp 的体量甚至明显大于 top wrapper。
它计划从生成的 vector manifest 中读取 43 组事务,并使用 profile hash 检查“测试向量是否仍然对应当前数值配置”。
这一点很有意义。
HLS 工程里经常会发生这种情况:源码位宽已经改了,但旧测试向量仍然被继续使用,最后得到一个“testbench 通过”的假象。这里通过 PROFILE_HASH 把向量清单和当前 profile 绑定,可以减少这类错配。
对于正常帧,testbench 会先使用 double 计算稳定 Softmax 参考:
expected[i] = std::exp(value[i] - maximum);
expected[i] /= sum;
之后逐项检查:
输出数量是否等于输入长度
最大绝对误差是否 <= 0.002
整帧 L1 误差是否 <= 0.01
输出概率和误差是否 <= 0.005
正常帧是否错误置位 TUSER
TLAST 是否只出现在最后一个 beat
对于异常帧,则检查是否只输出一个带错误侧带的 beat。
最终 transcript 采用明确的 PASS/FAIL 形式:
> INFO: [HLS] PASS 43
或者:
> ERR: [HLS] FAIL <case_id>
不过这里仍然要区分“代码里写了这些检查”和“这些检查已经真实执行通过”。我目前没有拿到 vector manifest 本体和本次 Vitis HLS 运行 transcript,因此不能把上面的 PASS 示例写成已经发生的实验结果。

八、它和“直接让 AI 写一段 HLS”有什么区别
看完这套生成结果后,我觉得 HLS Generator 的价值不只是代码模板更多,而是它在尝试把下面几件原本容易散落的事情串起来:
需求与行为契约
↓
接口与帧协议
↓
数值类型与近似策略
↓
HLS C/C++ 实现与 pragma
↓
自检 testbench 与向量清单
↓
Vitis HLS 配置
↓
静态检查 / csim / csynth / cosim / 板级证据
仓库当前工作流也不只有 Create,还包括:
Create:根据已确认的 HLS 契约生成 kernel;Write:修改 C/C++、pragma、接口、DATAFLOW 或工程配置;Review:检查源码、接口和 Vitis 报告;Annotate:在保持行为不变的前提下补充中文语义注释;Validate:执行静态检查,并在环境具备时调用 Vitis HLS 验证。
我比较认可的一点,是它把“静态就绪”和“真实工具执行”分开。
代码结构合理
≠ C Simulation 已通过
≠ C Synthesis 已通过
≠ achieved II = 1
≠ 时钟达到 5 ns
≠ 资源占用合理
≠ RTL Cosimulation 已通过
≠ 上板可用
对于 AI 生成 HLS,这条边界尤其重要。
九、这套 Softmax 目前还有哪些问题需要继续查
从源码看,工程骨架、数值策略和 testbench 已经比较完整,但我不会因为文件齐全就直接把它当成可交付 IP。
后续至少还要继续检查下面这些内容:
vitis-run或vitis_hls能否完整解析全部源文件和.cfg;- 43 组 vector manifest 是否真实存在,profile hash 是否匹配;
- C Simulation 是否全部通过,失败 case 能否给出足够诊断;
- 两个
PIPELINE II=1循环的 achieved II 是否真的是 1; - 最大长度 256 时的单帧 latency 和帧间吞吐是多少;
1 / sum最终综合成什么结构,延迟和资源是否过高;- 192 段 PWL 表被映射到 LUT、ROM 还是其他资源;
- 输入缓存与指数缓存分别占用多少 LUTRAM/BRAM;
- AXIS 背压、连续帧、异常侧带和超长帧在 cosim 中是否正确;
- 5 ns 时钟约束下是否满足时序;
- 与 float 版本、直接
hls::exp版本或更小 PWL 表相比,精度—资源—延迟的取舍如何; - 不同帧长、极端 logits、全相等输入和大动态范围输入是否都覆盖到。
其中我最想先看的,是下面四项:
C Simulation:功能和误差门限
C Synthesis:Latency / II / LUT / FF / DSP / BRAM
RTL Cosimulation:AXIS 帧与背压
对照实验:PWL 定点版 VS float/库函数版
只有这些结果出来以后,才能回答这套 Softmax 到底是“代码组织得不错”,还是已经具备真正有竞争力的硬件实现。
十、总体感受
前两篇测试 Verilog Generator 时,我关注的是模型能不能把接口、状态机、握手、错误路径和模块边界讲清楚。
换到 HLS 后,问题变了。
HLS 最容易制造的一种错觉是:
C++ 写出来了,软件结果也对,所以硬件大概没问题。
但真正进入 FPGA 设计以后,还必须面对数值位宽、循环依赖、存储结构、接口协议、流水线、吞吐、资源和时钟。
这次 Softmax 生成结果至少没有停留在“写一个 C++ 算法”这一层,而是把:
稳定 Softmax
+ 定点数值画像
+ PWL 指数近似
+ AXI4-Stream 帧协议
+ 异常路径
+ 自检 testbench
+ Vitis HLS 配置
放进了同一套工程中。
对我来说,它当前最有价值的地方不是替代 HLS 工程师,而是把 AI 生成 HLS 从:
给我一段看起来能综合的 C++
往下面这个方向推进了一步:
给我一份设计意图可见、数值策略明确、接口能够检查、
并且可以继续进入真实 Vitis HLS 验证的工程初版
它当然还不能代替 csim、csynth、cosim、实现、时序分析和人工 review。
但作为 HLS 设计的起点,这种“先把契约、数值、接口和验证一起搭出来”的方式,确实比直接让模型写一个函数更容易控制。
十一、项目地址与调用方式
GitHub:https://github.com/Eriemon/hls-generator
仓库当前版本:v0.5.1
在 Codex 中可以这样描述任务:
请从 https://github.com/Eriemon/hls-generator 安装 HLS Generator,
并使用 $readable-hls-generator 设计一个面向 AMD/Xilinx Vitis HLS 的
AXI4-Stream Softmax kernel。
要求:
1. 使用 TLAST 支持 1~256 个元素的可变长度帧;
2. 输入为 16 bit 有符号定点 logits,输出为 16 bit 无符号定点概率;
3. 使用 max-subtraction 保证数值稳定;
4. 不直接使用软件式 std::exp,给出可综合的指数近似策略;
5. 明确输入、差值、指数、累加、倒数和输出的定点位宽;
6. 输出使用 TUSER 上报非法侧带、超长帧或数值退化;
7. 提供 double 参考 testbench,检查最大绝对误差、L1 误差、概率和、
输出长度、TLAST 和错误侧带;
8. 生成 Vitis HLS 配置,并将目标时钟设为 5 ns;
9. 先展示行为契约、数值 profile、接口、PWL 方案和测试计划,
等我确认后再生成最终代码;
10. 没有实际执行 csim、csynth、cosim 或实现时,不要写成已经通过。
后面我还会继续用真实的 Vitis HLS 报告检查这套 Softmax,包括 achieved II、单帧 latency、资源占用、PWL 误差和 AXIS cosim 波形。
如果它最终能够把“生成代码—执行 HLS—读取报告—根据真实瓶颈继续优化”连起来,那会比单次生成一个 kernel 更有意义。
#FPGA #VitisHLS #HLS #Softmax #AXIStream #Codex #AI4EDA #AMD #硬件加速 #AgentSkill
更多推荐




所有评论(0)