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>;

可以看到,它至少区分了六类数值:

  1. 原始 logits;
  2. 减去最大值后的有符号差值;
  3. PWL 指数结果;
  4. 帧级指数和;
  5. 指数和的倒数;
  6. 最终概率输出。

同时,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,用于错误上报。

正常帧要求:

  • TKEEPTSTRB 必须与 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。

后续至少还要继续检查下面这些内容:

  1. vitis-runvitis_hls 能否完整解析全部源文件和 .cfg
  2. 43 组 vector manifest 是否真实存在,profile hash 是否匹配;
  3. C Simulation 是否全部通过,失败 case 能否给出足够诊断;
  4. 两个 PIPELINE II=1 循环的 achieved II 是否真的是 1;
  5. 最大长度 256 时的单帧 latency 和帧间吞吐是多少;
  6. 1 / sum 最终综合成什么结构,延迟和资源是否过高;
  7. 192 段 PWL 表被映射到 LUT、ROM 还是其他资源;
  8. 输入缓存与指数缓存分别占用多少 LUTRAM/BRAM;
  9. AXIS 背压、连续帧、异常侧带和超长帧在 cosim 中是否正确;
  10. 5 ns 时钟约束下是否满足时序;
  11. 与 float 版本、直接 hls::exp 版本或更小 PWL 表相比,精度—资源—延迟的取舍如何;
  12. 不同帧长、极端 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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐