1. 后量子密码为什么要一块真正的加速电路
这几年我接到的项目里,越来越多的客户把后量子密码和AI放在同一页需求文档里;他们嘴上说的是同一件事:希望我用更低功耗、更高吞吐的方式跑新一代密码算法。我的回答通常很直接——先不要急着让 AI 写电路,先搞清楚你的瓶颈到底在哪个环节,否则一块加速器做出来只会给整机添麻烦。
后量子密码目前的代表是格密码,尤其是 NIST 已经标准化的 ML-KEM(原 Kyber)和 ML-DSA(原 Dilithium)。和 RSA/ECC 相比,它们在量子计算模型下仍然安全,但代价是计算模式完全变了。RSA 和 ECC 依赖的是大整数模幂和椭圆曲线点乘,后量子方案的核心却是多项式乘法。多项式乘法的计算量大得多,而且天生适合并行和流水线,这就为硬件加速提供了明确空间。
1.1 从软件到硬件:瓶颈到底在哪里
如果你在 x86 服务器上跑 ML-KEM-768,一次密钥封装大约需要几十微秒,听起来并不慢。但真正的问题是组合场景:一个 TLS 网关每秒可能要建立几百甚至上千条会话,每条会话要执行封装、解封装、密钥派生,更别提 ML-DSA 的签名和验签往往还要再翻上几倍开销。CPU 算力在这种场景下会迅速被吞掉,而且功耗也会直线上升。
嵌入式场景更明显。很多安全设备的主控仍是 Cortex-M4/M7 级别的 MCU,没有硬件加速的话,一次 Kyber 操作可能达到几毫秒甚至十几毫秒。对电池供电或发热受限的终端来说,这不仅是延迟问题,还是产品能不能满足功耗预算的问题。硬件加速器的作用不是把 CPU 干掉,而是把最重的那一层多项式运算卸载出去,让主控干主控的活。
恒时性(constant-time)也是硬件加速的一个重要理由。软件实现为了避免时间侧信道,要做大量掩码和常数时间分支处理,既难写又难审。硬件里如果算法路径固定、乘法单元固定节拍,时间侧信道的风险天然要低很多。这也是很多安全设备宁可增加一颗协处理器也要把 PQC 放到硬件里的原因。
1.2 NIST 标准与 Kyber/Dilithium 的数学骨架
后量子密码的硬件设计者必须熟悉三个数学对象:多项式环、模数和数论变换。ML-KEM 和 ML-DSA 都在环 R_q = Z_q[x]/(x^256+1) 上工作,q=3329,多项式次数是 256。所谓密钥、密文、签名,说穿了都是多项式或多项式向量的组合。
多项式乘法如果直接暴力做,256×256 次乘法加加法,硬件虽然能堆,但效率太低。于是标准方案都转向NTT(数论变换):把多项式从时域表示变成频域表示,乘法变成逐点相乘。这样一次多项式乘法被拆成两次正变换、一次逐点乘、一次逆变换,硬件实现时真正需要优化的就是 NTT 蝶形单元和逐点乘模块。
对硬件工程师来说,这是个好消息:NTT 的结构和 FFT 很像,有固定的蝶形运算、旋转因子和级间数据重排。数据通路看起来不复杂,真正的复杂度在流水线调度、存储访问和模约简。只要把这三件事想清楚,加速器的骨架就立住了。
2. 需求压缩成一条命令:AI 辅助生成 RTL 工程
很多讨论 AI 辅助硬件的文章喜欢强调“让 AI 生成完整芯片”,但实践中我建议你把目标缩小:与其让 AI 从零憋出一个顶层设计,不如把需求压缩成一条命令,让 AI 先产出可综合的 RTL 骨架,然后你再带着验证和时序约束去打磨它。这个项目里“一条命令”不是修辞,而是我实际工作流的起点。
我把这条命令封装成一个 CLI 入口,输入规格后它会组装出一个完整的提示词,交给后端生成 Verilog 工程。你可以把它理解成产品设计里的需求穿透:先固定算法、接口、实现风格,再让 AI 动手。
ai-hw --task mlkem768 --rtl verilog --top mlkem768_top \ --interface axi-stream --fmax 100 --tool yosys \ --ntt radix2 --pipeline 2 --reduce barrett参数不多,但每个参数都会改变生成结果。有人可能觉得这样限制了 AI 的自由度,实际上硬件项目最怕的就是自由发挥。你给它明确边界,它才可能给出能落地的代码。
2.1 提示词里必须写清楚的五件事
我试过各种长度的提示词,最后发现最影响结果的是五类信息,少一个后面都得返工。
第一是算法和模数参数。必须写明 ML-KEM-768、q=3329、N=256,否则 AI 很可能给你生成一个“看起来很像但完全对不上”的通用设计。第二是接口协议。顶层是 AXI-Stream、APB 还是简单的握手信号,直接决定后续怎么集成。第三是目标工艺和频率约束。虽然 AI 不会真的做时序优化,但目标频率会影响它选择的流水线深度。第四是综合工具链。我在这个项目里用 Yosys + ECP5,提示词里明确要求避免initial块和非综合语句。第五是验证目标,我会要求它同时生成一个最小测试用例,用于后续替换成官方 KAT 向量。
把这五件事写清楚之后,AI 生成的代码质量会有质的提升。否则它可能给你输出一个行为级模型,仿真看着对,一综合全是问题。
2.2 交给 AI 后得到的工程骨架长什么样
我这个项目生成的目录结构大致是这样:
mlkem768/ ├── rtl/ │ ├── mlkem768_top.sv │ ├── ntt_pipeline.sv │ ├── butterfly.sv │ ├── mod_mul_montgomery.sv │ ├── mod_add_sub.sv │ ├── sha3_keccak.sv │ └── zeta_rom.sv ├── sim/ │ ├── tb_mlkem768_top.sv │ └── kat_vectors.mem └── scripts/ ├── run_verilator.sh └── synth_yosys.shAI 生成的文件名基本符合硬件工程的直觉:顶层、NTT 流水线、蝶形单元、模乘、模加减、Keccak、旋转因子 ROM 分开。这种拆分方式对后续调试很重要,因为你可以在仿真里单独检查蝶形单元的输出,再检查 NTT 单元,而不用每次都在顶层看乱七八糟的波形。
当然,AI 生成的“直觉”不等于正确。我拿到代码后的第一件事是拿 lint 工具过一遍,把没有使用信号的模块、悬空的输入、位宽不一致的地方全部标出来。这个步骤绝不能省。
2.3 从一开始就锁定的验证目标
我一开始就把验证目标定成“必须通过官方 KAT 测试向量”,不是随便跑几个波形看个趋势。KAT(Known Answer Test)向量是 NIST 在算法标准化时发布的,里面包含随机种子对应的密钥、密文、共享密钥,是判断实现正确性的权威依据。
所以在项目启动时,我要求 AI 生成的不是一个花哨的演示,而是一个能和 KAT 对上的仿真环境。我会把 KAT 文件解析成 Testbench 里可以直接读取的格式,用$readmemh加载种子,跑完一轮 keygen/encaps/decaps,然后自动比对输出。这一步如果通过,功能正确性就基本稳了;后面再谈性能和面积。
这个策略也让我后续敢于让 AI 反复改代码。每次修改之后重新仿真 KAT,几秒钟就能知道新版本有没有把功能改坏。验证闭环存在,你才敢信任自动生成代码带来的效率。
3. 拆开 AI 写的 NTT 流水线:架构选择与可综合代码细节
AI 帮我搭好工程骨架之后,真正的重头戏是 NTT 流水线。要知道 Kyber 的多项式乘法实际上由 NTT 完成:系数向量经过正变换后在频域逐点相乘,最后逆变换回时域。这个过程中最核心的单元就是蝶形运算。
3.1 用 NTT 把多项式乘法变成逐点运算
在环 R_q 上做多项式卷积,直接卷积需要 O(N^2) 次乘法,NTT 把它降为 O(N log N)。具体做法类似 FFT:先把长度为 256 的多项式拆成奇偶两组,用旋转因子逐层合并。每层合并就是蝶形运算,数学上可写成:
b_i = (a_i + z · a_j) mod qb_j = (a_i - z · a_j) mod q
其中z是旋转因子,预先存在 ROM 里。Kyber 用的 256 次循环群有 128 个旋转因子,配合拆半结构完成整个变换。
这个式子看起来简单,硬件里却有两个敏感点:一是z · a_j需要一个模乘,二是+和-之后都必须做模 q 的归一化。很多 AI 生成的代码在这两步出错,因为软件写法里直接% 3329,到了硬件必须换成可综合的 Barrett 或 Montgomery 约简算法。
3.2 蝶形单元与模约简的实现方式
AI 为这个项目生成过一版蝶形单元,代码风格基本如下:
module butterfly #( parameter WIDTH = 16, parameter Q = 3329 )( input wire clk, input wire rst_n, input wire [WIDTH-1:0] a_in, input wire [WIDTH-1:0] b_in, input wire [WIDTH-1:0] w_in, output reg [WIDTH-1:0] a_out, output reg [WIDTH-1:0] b_out ); reg [WIDTH-1:0] mul_tmp; always @(posedge clk) begin if (!rst_n) begin mul_tmp <= 16'd0; a_out <= 16'd0; b_out <= 16'd0; end else begin mul_tmp <= montgomery_mul(b_in, w_in); a_out <= add_mod_q(a_in, mul_tmp); b_out <= sub_mod_q(a_in, mul_tmp); end end endmodule这一版用了 16 位数据通路,因为 q=3329 需要 12 位以上才能表示,16 位便于对齐。montgomery_mul是我在提示词里点名的模块,因为没有它,b_in * w_in产生的是 32 位中间结果,直接取模会让组合逻辑链过长。使用 Montgomery 约简可以保持运算过程一直停留在 16 位附近,代价是输入输出要额外乘一个缩放因子,整个设计需要在变换前后统一处理。
这里有个经验:不要指望 AI 自动帮你选 Montgomery 还是 Barrett。两种都能做,但数据流的约定不一样。Montgomery 需要把数据先变换到 Montgomery 域,Barrett 则更接近普通整数运算。我最后选了 Montgomery,因为它对硬件乘法的利用率更稳定,流水线更容易做长。
3.3 流水线深度、数据带宽与 BRAM 的取舍
流水线深度是 NTT 加速器设计里最影响面积和频率的选择。基 2 的 256 点 NTT 总共要做约 1024 次蝶形运算,如果每次蝶形用一个周期,延迟约 1024 拍;如果做成两级流水线,每个蝶形需要两个周期完成,但关键路径会变短,主频能拉高。
我让 AI 生成过两个版本对比:
| 版本 | 流水线深度 | 单次蝶形延迟 | 预计 LUT 消耗 | BRAM 需求 |
|---|---|---|---|---|
| 紧凑版 | 1 级 | 1 周期 | 低 | 低 |
| 性能版 | 2 级 | 2 周期 | 中 | 中 |
| 激进版 | 3 级 | 3 周期 | 高 | 中 |
实际选型不是越大越好。我最后停在两级流水线:一级给模乘,一级给模加减和旋转因子读取。这样乘法器的输出有完整一个周期来稳定,寄存器之间的路径不会卡在 16 位乘 16 位再取模的长链上。
数据带宽的问题同样重要。NTT 每一级都会把系数重新读写一遍,这时候存储访问就是瓶颈。系数存放在 FPGA 的 BRAM 里,一次只能读两个数算一个蝶形,吞吐就受限。后来我把 BRAM 拆成两组,交替存放奇偶系数,让蝶形单元可以在一个周期内同时读到a_in和b_in。这个小改动直接把 NTT 吞吐提升了一倍,属于典型的“算法没变、存储变”带来的收益。
4. 从功能仿真通过到布局布线:验证电路的完整闭环
“可验证的电路”在我这个项目里有两层含义:第一层是功能正确性,能通过 KAT 测试;第二层是物理可实现性,能完成综合、布局布线并达到目标频率。两层都过,才敢说电路是可用的。
4.1 用官方 KAT 测试向量校准正确性
NIST 发布的 KAT 文件是 text 格式,里面有count、seed、pk、sk、ss等字段。我写了一个小解析脚本,把它转成 Verilog 仿真能用的.mem文件。Testbench 不搞花哨的随机激励,就是逐条读 KAT、跑完整流程、逐字节比对。
verilator --binary -j 32 --timing \ sim/tb_mlkem768_top.sv \ rtl/mlkem768_top.sv rtl/*.sv ./obj_dir/Vtb_mlkem768_topVerilator 的编译型仿真比传统交互式仿真快一个数量级,KAT 向量跑完只需要几秒。如果某个向量没过,我会回到butterfly.sv或mod_mul_montgomery.sv加临时断言,把中间结果打印出来和 Python 参考实现逐级比对。AI 生成的代码常见的错误是某个旋转因子算错、某个阶段的顺序颠倒,这种定位方法能很快缩小范围。
4.2 行为仿真时的三个不确定点
行为级仿真过了,不代表电路就能用。我总结过三个最容易在仿真里“漏掉”的问题。
第一个是x态传播。AI 生成的寄存器如果初始化靠initial,在仿真里可能一切正常,但综合后上电瞬间是什么状态完全未知,一旦控制信号依赖这个状态,可能进入死锁。第二个是位宽截断。仿真中 Verilog 允许大位宽临时值参与运算,综合时乘法和加法器会按最大位宽展开,面积和时序很容易超标。第三个是异步复位释放。仿真时钟边沿和实际复位释放的异步性不一样,必须做复位释放同步,否则状态机有机会跑到意外分支。
我的处理方式是:仿真时打开-Wall之类的高告警级别,把所有位宽隐式扩展和异步赋值的警告全部当成错误处理。这些警告看起来很烦,但它们恰恰是 AI 生成代码最容易埋雷的地方。
4.3 Yosys + nextpnr:把“命令”转成物理电路
功能仿真完成后,我用的是开源工具链完成综合和布局布线。Yosys 读入 Verilog,综合成 ECP5 的网表,nextpnr 负责布局布线,最后ecppack生成可以烧录的 bit 流。
yosys -p "read_verilog -sv rtl/; synth_ecp5 -top mlkem768_top -json mlkem768.json" nextpnr-ecp5 --json mlkem768.json --lpf devboard.lpf --freq 50 \ --placer-heap --router router2 ecppack --bit mlkem768.bit这三条命令就是“可验证电路”的落地闭环。Yosys 会报告 LUT、FF、BRAM、DSP 的资源占用,nextpnr 会给出关键路径延迟和 Fmax。如果 Fmax 达不到目标,或者布线资源爆了,就得回到 RTL 改设计。
我在这个阶段遇到过最典型的情况:功能仿真用了 16 位乘法器,AI 生成时没有意识到可以把部分模乘拆成更小的常量乘法,结果综合后 DSP 用量爆炸。后来把旋转因子 ROM 改成带符号的 12 位存储,乘法器位宽降下来,DSP 数量立刻合理了。这类问题只有真做一次物理综合才看得到,单纯停留在仿真层面永远发现不了。
5. 我踩过的三个坑:时序、复位、面积失控
AI 给硬件项目带来了效率,但也带来了一批“看起来合理、实际上反综合”的代码。这一节我记录这次项目中印象最深的三次翻车,希望大家绕开。
5.1 高扇出复位与未初始化的状态
第一版 AI 生成代码里,几乎每个寄存器都挂在同一个异步复位上,而且复位信号的负载高得离谱。综合后 Yosys 报告复位网络扇出超过几百,关键路径直接被复位树拖慢。我一开始还想用约束去救,后来发现根子在于设计本身不需要那么多异步复位。
我改成同步复位,并且只对控制状态机、流水线握手这类关键寄存器做复位,数据通路寄存器允许上电为任意值,靠后续使能信号冲刷。这一改,不仅面积下来,时序也舒服多了。AI 生成的“全复位”看起来安全,实际上是把所有资源都绑在一条容易成为瓶颈的网络上。
5.2 只有功能仿真,没有时序仿真的代价
有一版 NTT 流水线,功能仿真跑 KAT 全过,我非常高兴,结果一综合发现 Fmax 只有 40MHz,离目标 100MHz 差得远。问题出在模乘模块:AI 把 Montgomery 乘法里的 32 位乘法结果直接拉回 16 位,中间没有插入流水线寄存器,组合逻辑链长得吓人。
修法不是一句“加两级流水线”就完事。需要在乘法器输出后先打拍,再做减法归约,最后再做条件修正;每一步都要重新检查数值范围。我加完流水线后 KAT 仍然能过,但代码结构和第一版已经完全不一样了。
这件事给我的教训是:AI 生成 RTL 的“正确性”停留在行为级,它不知道Fmax是根据寄存器到寄存器的延迟算出来的。拿到代码后,第一件事应该是立刻跑综合,而不是反复看波形。综合报告才是真正的一手意见。
5.3 AI 生成代码的“文档化承诺”与实际情况
AI 很喜欢在事务级模型和行为级描述之间摇摆。它给你写接口注释时,可能承诺支持背压、支持多通道并发,但真正生成的模块里只是简单把 ready 信号接到了1'b1。仿真看不出来,因为 Testbench 不会故意等几个周期再继续。
我后来加了一轮“极端握手测试”,让上游随机插入等待周期,下游随机拉低 ready。结果没一会儿就把状态机卡死在一次未完成的握手上。这种问题靠看代码很难发现,必须靠约束严苛的 Testbench 逼出来。现在我把这条规矩写死在项目脚本里:任何 AI 生成模块,先跑 30 分钟随机背压测试,再谈正常流程。
6. 这类项目怎么做才算真正跑通:我的工作流与边界感
项目到最后,我留下的不只是比特流,还有一套可以复用的工作流。AI 确实全程参与,但真正把握方向的还是人。
6.1 从提示的第一行到交付物,一条完整的命令链
我的流程可以压缩成下面这条命令链:
- 用 CLI 把算法需求打包成生成提示词
- 让 AI 输出 RTL 骨架 + 最小 Testbench
- 跑 Verilator lint 和编译仿真
- 替换官方 KAT 向量做功能回归
- 用 Yosys 综合,检查面积和关键路径
- 用 nextpnr 布局布线,检查 Fmax 和资源占用
- 上板验证实际握手流程
每一步都有对应的脚本和判决标准。步骤不过就回到上一步,最多的是从第 5 步回到第 2 步:注意 AI 生成代码后立即检查综合,而不是先沉浸在仿真通过中。
6.2 哪些环节我会继续交给 AI,哪些坚决自己写
经过这次项目,我的分工很明确。AI 适合生成骨架代码、常规握手逻辑、Keccak 这类标准化模块的移植、还有 Testbench 的初始脚本。这些工作模式固定、容错空间大,让人工写属于浪费时间。
不能完全交给 AI 的部分包括:模约减算法的数值边界、NTT 数据重排的地址生成、跨时钟域处理和约束文件。这些地方出错很隐蔽,而且大部分 AI 模型会倾向于“按常见的写法写”,而不是“按你的芯片验证策略写”。我要求这些模块必须有我亲手加的数据宽度断言和范围检查。
6.3 给想要复现这个流程的人的建议
如果你想在自己的项目上复现这个流程,我建议先不要从 ML-KEM 起步。先拿一个更简单的模块,比如 SHA-256 或 AES S-box,把提示词、验证闭环和综合脚本全部跑通,再升级到 NTT 加速器。这样做的好处是:当 AI 输出错误时,你比较容易判断是哪里出了问题,而不至于在一个复杂的格密码算法里迷失方向。
工具的版本也要固定下来。Yosys、NextPNR、Verilator 的版本我全部锁在项目脚本里,避免未来某天更新后综合结果默默变化。硬件项目最怕“昨天能过,今天不能过”而没有原因。
另外一个容易被忽略的是功耗。虽然 FPGA 原型不强调功耗,但如果目标是 ASIC,AI 生成代码里的多余翻转、无脑清零,最后都会变成功耗浪费。我建议养成看activity的习惯:哪个模块翻转率高,就用工具抓出来,让 AI 针对它重写数据结构。
这个项目给我的最大收益,不是有人替我写了多少行 Verilog,而是建立了一个快速验证假设的回路。以前改一次 NTT 结构要改半天 RTL,现在改提示词、跑回归、看报告,半小时能完成一次迭代。硬件设计正变得像软件迭代一样快,前提是你把“验证”做成流水线而不是事后补救。
我自己现在面对这种项目,会先把数学参考实验写好,让 AI 在参考实现上做翻译,而不是直接凭记忆生成电路。生成式工具适合放大你的专业判断,不适合取代它。你越清楚电路应该长什么样,AI 越能帮你少走弯路;你越指望它替你思考,就越容易在综合报告里面对一个无法收场的意外。