AI辅助设计后量子密码NTT加速器:从一条命令到可综合RTL
发布时间:2026/10/28 23:52:34 锦皓数字建站

前几天我在项目的任务清单里写下了一行字让 AI 设计后量子密码加速器。一开始它只是条随手记录的命令连我自己都没指望它能直接变成能用的电路。结果两天之后这些 AI 生成的 Verilog 代码不仅跑通了仿真还在综合工具里拿到了像样的时序报告。整个过程里我最深的感受是AI 不是替你做芯片设计而是帮你把“从想法到代码”这段路的阻力降到最低。你真正要花心思的地方还是在架构拆分、验证策略和电路细节上。这篇文章就把我这次实操的全过程摊开讲一遍。我会从后量子密码PQC里最核心的 NTT 运算讲起说明为什么需要硬件加速器再拆解 AI 辅助设计的完整工作流包括怎么给 AI 下第一条命令、怎么编排多个 AI Agent 分工写 RTL 和验证代码然后落到电路层讲蝶形单元、模约减、乒乓存储这些关键实现最后给出完整的验证流程和工具链踩坑记录。适合正在搞 FPGA/ASIC 的工程师也适合有密码学基础、想了解硬件实现的朋友。1. 需求拆解后量子密码到底在“加速”什么1.1 不是所有密码运算都值得做硬件先聊一个经常被忽略的问题很多人一听到“后量子密码加速器”以为就是把整个密钥封装协议全部搬进硬件。实际上任何一款密码算法里值得硬件化的部分就那么一小块。ML-KEM也就是 Kyber是当前 NIST 标准化的后量子密钥协商方案它的数学核心是格上的多项式运算。一次密钥封装流程里要反复对维度为 256、模数为 3329 的多项式做数论变换NTT。这个变换和信号处理里的 FFT 在结构上几乎一样只是一切运算都发生在有限域里。我最初在软件里跑这份多项式运算的时候用的是一个还算不错的 ARM 核。单纯计算 NTT 本身确实很快但一旦进入完整的封装、解封装流程加上矩阵-向量乘法、噪声多项式的生成、哈希和串行化开销整个耗时就上来了。尤其在边缘网关上功耗和实时性两头受压硬件加速的理由就变得非常充分。我的生活类比是NTT 之于 Kyber就像 FFT 之于 Wi-Fi、LTE 里的 OFDM。没有 FFTOFDM 就退化成“每个子载波单独收发”的笨办法效率完全不可接受没有硬件化的 NTT多项式乘法就会成为后量子密钥协商的瓶颈。理解了这一层就知道加速器该做哪些事了。1.2 先把范围收敛到 NTT 核心做任何硬件项目第一步都是控制范围。我这次没有妄想一步到位做完整 KEM 协处理器而是先把“NTT/INTT 引擎”做成一个独立 IP。为什么这么收敛Kyber 的多项式乘法可以被拆成“逐点乘法 正变换 逆变换”NTT 引擎一旦稳定后续矩阵乘法、向量运算都只是对这个引擎的反复调用。完整 KEM 里还有哈希、随机数生成、编解码逻辑这些硬件实现难度不大但验证面积很广。先做核心再做周边符合增量交付节奏。单一引擎的接口非常清晰输入一个多项式系数数组启动后执行 NTT完成后输出结果。这种边界明确的模块恰恰是当前 AI 生成 RTL 最擅长处理的任务。于是我给自己定下的第一个交付物是一个参数化、可综合、带完整验证环境的 NTT 加速核。它接收 256 个 16-bit 系数按 Kyber 的约定完成 NTT 正变换和逆变换支持 Batch 连续处理模式。1.3 可量化的设计目标硬件设计没有指标就是空中楼阁。这次我给自己定的目标是工作频率FPGA 上综合后不低于 150 MHz单次 NTT 延迟不超过 600 个时钟周期等效逻辑门控制在 50 万门以内不含存储接口协议配置侧用简单的寄存器读写数据侧用 FIFO 握手。这些指标不是拍脑袋拍出来的。Kyber512 里矩阵维度是 2×2每次封装大概需要 4 次正变换和 2 次逆变换单次 NTT 若能做到 500 周期以内再加上 AXI 总线等开销完整封装流程有望压在几十微秒级别这对边缘设备来说是很有竞争力的数字。2. AI 辅助设计从一条命令到可综合 RTL2.1 我所说的“一条命令”到底有几层含义标题里的“一条命令”我在实际操作里把它切成了三层。第一层是发给 AI 的第一条 Prompt。它是我对整个项目的需求陈述相当于给一名工程师下达任务。第二层是 Makefile 里的make sim。AI 帮我生成完代码之后验证环境搭好整个仿真只需要这一条命令就能跑完回归。第三层是git commit它负责把 AI 生成的每个版本固化下来方便我随时回退和做 diff。这其实是我最近做 AI 辅助硬件设计总结出的心法不要指望 AI 一次给你终稿而是把流程变成“命令→产物→反馈→再命令”的循环让 AI 生成的每个中间版本都可审计、可回滚。2.2 多 AI Agent 编排架构师还是我自己有些朋友会问我你到底用的是哪个大模型我个人认为比模型品牌更重要的是协作方式。这次我采取的是“多 AI 协作”模式开了三个虚拟角色Agent R负责写 RTL 核心模块包括蝶形单元、状态机和存储器接口Agent V负责写验证环境包括 Cocotb 测试脚本和 Makefile 目标Agent A负责做数学参考和测试向量生成用 Python 实现和 NIST 参考一致的 NTT 算法。我自己的角色是架构师和审查者。三个 Agent 之间不直接沟通所有信息都汇总到我这里由我统一决定接口、时序和验证策略。这种方式最大的好处是职责隔离RTL 和测试平台由不同 Agent 编写反而降低了“代码写错了但测试也跟着错”的同源风险。给 AI 的第一条命令我大概是这么写的这是简版实际里的接口信号更多请设计一个可综合的 Verilog 模块 ntt256_top实现 Kyber 中的 NTT 正变换。 接口如下clk、rst_n、start、mode0正向1逆向、din_valid、din[15:0]、 dout_valid、dout[15:0]。数据按 256 点顺序逐点送入内部完成旋转因子查表、 Barrett 约减和蝶形运算。要求同步复位、无 initial、无延时控制存储用 双端口 RAM 实例状态机编码采用独热码。这里我想强调一个细节不要给 AI 过于抽象的需求比如只说“给我一个后量子密码加速器”。它没有能力理解你的项目上下文也不知道你的接口约定。必须显式给出模块名、端口列表、数据位宽、时序协议和设计约束AI 才可能输出接近可用的代码。2.3 五大必写约束缺一条后面都是坑根据这次实践我总结出五条在 Prompt 里必须写清楚的设计约束缺一条后面都会在验证或综合阶段踩雷。第一复位策略。明确写“同步复位”否则不同 Agent 生成的模块很可能一个用async一个用sync顶层集成时出现混用会直接引发功能不一致和综合告警。第二代码风格。要求“无 initial、无#延时、无循环上限不可综合”。AI 有时候为了仿真方便会在 RTL 里插入initial或for (i0; idata_len; ii1)这种不可综合写法。第三接口协议。让 AI 严格使用 Valid/Ready 或者 Valid/Handshake不要让它自己临场发挥。数据通路上一旦出现协议不统一联调时会发生很难排查的死锁。第四存储结构。我明确要求用双端口 RAM并且给出存储深度和位宽。如果放任 AI 自己选它可能生成一堆分散的寄存器堆导致面积浪费和时序变差。第五拒绝魔法数字。旋转因子统一放在单独 ROM 模块里通过参数引用不能散落在状态机或蝶形单元里。2.4 AI 生成代码的审查清单生成不等于完成AI 写出的 RTL 必须过一遍人工审查。我每次收到 Agent R 的代码会重点检查四个位置。第一个位置是运算位宽。NTT 的蝶形运算是模 3329 的加法和减法输入 16-bit中间结果因为乘法会到 32-bit。我需要确认 AI 在约减之前的中间信号位宽足够避免截断错误。第二个位置是状态机的转移条件尤其是IDLE→LOAD→NTT→STORE的转换不能有死锁。第三个位置是存储器的读写冲突双端口 RAM 在同一周期同时读写同一地址时是否处理了 arbitration。第四个位置是组合逻辑环路比如assign out in ? out : other这种自反馈写法。我用一个本地脚本对每个 Agent 提交的代码跑verilator --lint-only和yosys synth快速扫描把告警直接喂给 Agent R 让它解释和修复。这一步非常有效至少帮我省掉了一半的人工检查时间。3. 核心电路NTT 蝶形单元与数据调度3.1 从数学公式到蝶形结构NTT 本质上是在有限域里做分治变换。Kyber 的参数是模数 q3329多项式维度 n256原根取 17。整个变换有 8 层蝶形每一层把上一层的输出重新配对乘上对应的旋转因子然后做一次乘加和乘减。蝶形运算的标准表达式是a_out (a_in w * b_in) mod q b_out (a_in - w * b_in) mod q在硬件里这个表达式的核心代价是w * b_in这一步乘法。模 3329 的乘法约减才是真正的硬件重点。我这次用了 Barrett 约减因为它对固定模数非常友好不需要动态更新模数多周期流水也很容易做。我用一段简化 RTL 展示蝶形单元的核心逻辑实际工程里把% Q换成了完全展开的约减电路这里教学可读性优先module butterfly #( parameter W 16 )( input wire clk, input wire rst_n, input wire [W-1:0] a_in, input wire [W-1:0] b_in, input wire [W-1:0] w_in, output reg [W-1:0] a_out, output reg [W-1:0] b_out ); logic [31:0] prod; logic [31:0] sum_tmp; logic [31:0] diff_tmp; always_comb begin prod b_in * w_in; sum_tmp a_in prod; diff_tmp a_in - prod 32d3329; end always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin a_out 16d0; b_out 16d0; end else begin a_out sum_tmp[15:0]; // 实际会做 Barrett 约减 b_out diff_tmp[15:0]; end end endmodule你可能会问为什么不直接在 RTL 里写% 3329在仿真里完全没问题但综合工具对取模运算的综合质量通常很差会展开成一长串除法电路时序和面积双输。这正是需要 AI 在实现层面对算法做映射的地方也是“从命令到电路”这一过程中最有技术含量的一步。3.2 Barrett 约减的电路实现思路Barrett 约减原理很简单对固定模数 q预先计算一个常数mu floor(2^k / q)然后用乘法和移位代替除法。我的实现里选择 k18把 16-bit 输入扩展到 32-bit 中间态再做两次乘法和一次移位最后条件减法收敛到[0, q-1]。具体的流水线安排是第一级计算prod b * w得到 32-bit 乘积第二级计算约减所需的高位估计t (prod * mu) 18第三级计算res prod - t * q并做一次条件修正第四级蝶形加法/减法输出约减后的 a_out、b_out。这条四级流水线让单次蝶形能够在四个周期内完成但吞吐量可以通过重叠多个蝶形单元来提升。我这次只放了两个蝶形单元做脉动式处理用面积换控制复杂度。对 Kyber 这种 256 点变换两个蝶形单元配合双口 RAM单次 NTT 大概 200 个周期就能跑完远低于我之前定的 600 周期。3.3 数据调度与乒乓存储NTT 的数据流和 FFT 很像每一层都需要把变换数组按固定间隔重排。如果直接 in-place 更新地址生成逻辑会很复杂。所以我采用了 out-of-place 策略也就是在双口 RAM 里同时维护两个 buffer第 n 层从 buffer A 读写 buffer B下一层再从 buffer B 读写 buffer A。乒乓切换只需要一个 1-bit 的控制标志状态机简单很多。地址生成器根据层号和蝶形索引计算读地址和写地址。旋转因子表固化在一个只读 ROM 里按层和组索引直接查询。AI 生成这部分代码时我额外要求它把地址计算逻辑单独抽成一个ntt_addr_gen模块方便我针对地址生成单独做 formal check也方便复用。3.4 状态机LOAD、NTT、STORE 三段式顶层控制状态机我设计为四状态IDLE等待 start 信号LOAD从外部 FIFO 接收 256 个系数写入 bufferNTT执行 8 层蝶形变换期间数据在双口 RAM 中乒乓换入换出STORE把结果逐点从 RAM 读出通过 dout 端口送出完成后拉高 done。这几个状态之间的跳转条件非常明确LOAD 计数到 256 后自动跳 NTTNTT 内部层计数器跑完 8 层再跳 STORESTORE 计数到 256 后回到 IDLE。独热码编码方式在 FPGA 上实现成本很低时序也容易收敛。4. 系统集成把加速核挂进 SoC4.1 寄存器接口设计独立的 NTT 引擎不能直接对外工作它需要一个寄存器配置接口。我这次在顶层加了一个简单的 APB 从设备接口里面放了四个 32-bit 寄存器偏移寄存器名功能0x00CTRLbit0startbit1mode0正变换/1逆变换0x04STATUSbit0busybit1done0x08BATCH连续处理的数据包个数0x0CIRQ_EN中断使能寄存器软件侧写 0x01 到 CTRL 启动一次 NTT之后轮询 STATUS 的 done 位。如果想做中断驱动也可以打开 IRQ_EN。这部分代码本来就是模式化很强的胶水逻辑AI 生成几乎没有出错。4.2 数据通路选型FIFO 优先内核对外数据通信我选择用标准 FIFO 接口没有直接上 AXI Stream。原因是 FIFO 语义更简单din_valid/din_ready握手协议由 AI 生成时不容易出现 IDLE 周期不规范的问题而且我在这次设计里不需要多个 master 并发访问 NTT 引擎。不过如果后续要把这个加速器放进更大的 SoC 里建议在 FIFO 外面再包一层 AXI Stream 转接桥。这一步可以后续再加不影响核心逻辑验证。4.3 时钟复位与功耗管理设计里所有模块都在同一个时钟域复位统一用异步复位同步释放。跨时钟域在这个版本里不存在因为 APB 和 NTT 引擎使用了同源时钟。为了降低空闲功耗STORE 结束回 IDLE 后内部时钟可以直接通过 clock enable 关掉蝶形单元翻转只保留寄存器模块的轻量逻辑。5. 验证闭环从测试向量到覆盖率5.1 用 Cocotb 搭 Python 验证环境“可验证的电路”是我这次项目的验收底线。AI 生成的 RTL 再好没有一套可信的验证环境我也不敢把它放进正式项目。这里我选用了 Cocotb用 Python 写测试激励Simulator 选用 Verilator。Cocotb 的好处是测试逻辑比 SystemVerilog 更容易维护而且可以和 AI 生成的 Python 参考模型直接互操作。我的test_ntt.py大概长这样import cocotb from cocotb.triggers import Timer, RisingEdge from cocotb.clock import Clock cocotb.test() async def test_ntt_forward(dut): 正向 NTT 基本功能测试 cocotb.start_soon(Clock(dut.clk, 10, unitsns).start()) dut.rst_n.value 0 await Timer(100, unitsns) dut.rst_n.value 1 # 预置输入数据 test_data reference_ntt_input() await push_data(dut, test_data) await wait_for_done(dut) expect reference_ntt_forward(test_data) result await read_result(dut) assert compare_poly(result, expect), NTT forward result mismatch这里reference_ntt_forward是 Agent A 用 Python 实现的 NIST 参考算法它充当“黄金模型”。硬件结果和 Python 参考模型逐项比对任何一位系数的差异都会导致断言失败。5.2 测试向量从哪来测试向量不能只靠随机数据。我的做法有三个来源官方 KAT 测试向量从 Kyber 参考实现的配套数据里提取原始的 NTT 输入输出对这是最权威的回归基准。参考模型生成用 Python 参考模型生成 1000 组随机多项式对再通过文件接口灌给仿真环境。边界值补充专门生成向量覆盖全零、全 q-1、相邻地址冲突、位宽溢出等边界场景。在实际仿真中全零和全 q-1 两类向量抓到的 bug 最多。尤其是 Barrett 约减的边界条件差一个 LSB 就会让结果整体偏差而随机数据又很难覆盖到极限情况。5.3 回归、覆盖率和形式化验证我搭建的回归脚本支持在make regress下批量跑 200 组随机测试。每次跑完会输出一份覆盖率报告包括行覆盖率、翻转覆盖率和我们自定义的功能覆盖率点比如“状态机是否完整经过 LOAD→NTT→STORE”。在这个基础上我还用 SymbiYosys 对地址生成器做了形式化验证。因为我们最担心的是乒乓地址在某些组合下发生读写冲突形式化验证把这个性质直接写成断言assert property ((posedge clk) !(wr_en rd_en wr_addr rd_addr));这个断言能证明从状态空间层面根本不可能发生读写同地址的冲突而不是仅仅靠几轮随机回归去“碰运气”。6. 实操实录工具链与三天踩坑日记6.1 环境准备命令如果你也想复现这个流程先准备一个 Linux 环境然后安装下面这些工具sudo apt install -y iverilog verilator gtkwave python3-pip pip install cocotb sudo apt install -y yosys # 逻辑综合用 sudo apt install -y nextpnr-ice40 # 如果你要在 iCE40 上用我强烈建议至少用 Verilator 做一遍 lint它能抓到大量可综合性问题。用 Icarus Verilog 做仿真也可以但 Cocotb 配合 Verilator 的编译速度明显更快。6.2 第一天代码能编仿真全红第一天下午我把三位 Agent 的产出合并到同一个仓库写好 Makefile 后运行make sim。出乎意料的是lint 几乎一遍过模块也能编译通过。我心里还挺高兴心想 AI 是不是真的把 RTL 搞定了。结果跑到仿真阶段前 5 个测试全部失败——输出全是错误的数据。排查过程是这样的先打开波形看 top 状态机发现状态机没有卡死正常走完了 LOAD→NTT→STORE。再把 Python 参考模型的数据 dump 出来和硬件结果对齐发现第一次 NTT 之后的结果第 0 位到第 15 位是对的从第 16 位开始完全不对。最后定位到是数据位序问题Kyber 的 NTT 输入多项式系数是“先低次后高次”但 AI 生成的地址生成器默认按“自然序”写 RAM也就是先写入的是高位系数。双方差了一个基 8 位反序。这个问题叫我一阵后怕如果我没有对前 256 个数据做全量比对只比对前几个样本很容易以为自己成功了。6.3 第二天把参考模型“反向喂养”进修复流程找到问题之后没有手改代码。我把上一步定位到的数据序问题整理成一段话连同波形截图发给 Agent R要求它给出修复方案。它很快就认出是地址位序错误并给出了新的地址生成算法。我执行git diff确认只改动了ntt_addr_gen.v和旋转因子表的初始化文件然后重跑回归200 组随机测试全部通过。从这一天开始我建立起一个稳定节奏出现问题 → 把仿真日志和失败向量发给对应 Agent → 让它给出修复建议 → 人工审查 diff → 重跑回归。AI 在这种闭环里表现得非常像一个能干的初级工程师关键在于我每次都能给出足够精确的反馈信息。6.4 第三天综合与时序优化仿真全部通过后我用 Yosys 做了一次逻辑综合。在默认约束下最高频率只跑到 108 MHz离目标的 150 MHz 还有一大段距离。分析关键路径后发现问题出在 Barrett 约减的第三级乘法链t * q这一项的延迟过长。修复方式是在约减链的乘法结果后面插入一级流水寄存器。这样单次 NTT 的总周期数从 200 增加到 280但最高频率可以提到 180 MHz 以上。硬件设计就是这样延迟和频率之间需要取舍。我用一个参数PIPE_LEVEL控制插入级数后续还可以按不同工艺重新平衡。7. AI 辅助硬件设计常见问题速查这里整理一份排查表都是我这几天实际踩到或看到别人踩过的坑直接按图索骥能省下不少时间。现象可能原因排查方法 / 解决方案仿真结果前 16 项正确后面全错NTT 输入位序 / 基 8 反序问题检查地址生成器对比参考模型的排布方式所有输出都是 0复位没有释放或状态机卡在 IDLE检查 rst_n 时序检查 start 信号是否送达顶层频率上不去关键路径很长Barrett 约减第三级链路过深把乘法链路拆分插流水寄存器AI 代码里出现initial或#10生成的代码是仿真风格不可综合用 Verilator lint 抓出来要求 AI 重写成可综合风格双口 RAM 读写同一地址地址生成器对乒乓缓冲切换有漏洞用 SymbiYosys 写 assert 检查 wr_en rd_en 同地址冲突连续跑 Batch 时第二包数据错乱状态机 STORE 完成清 buffer 不彻底检查 STORE 结束是否把 buffer 有效标志清零模块名和接口位宽不一致不同 Agent 之间缺少公共 schema先定义好接口 JSON再分发给各 Agent组合逻辑环路AI 在计算a_out时依赖了自身旧值检查always_comb是否有反馈赋值把计算拆成中间信号面积爆炸存储全被综合成寄存器堆强制要求 AI 使用双端口 RAM 宏不要用 reg 数组形式化断言跑不过状态机存在未覆盖的非法状态在状态寄存器加case default并检查 illegal state 断言最后聊两句我的实际体会这次做下来我对“AI 设计硬件”这件事的态度变得务实了很多。AI 最大的价值不是在芯片领域突然变成了天才架构师而是把大量重复性、模板化的 RTL 编码工作从几天缩短到几小时。真正决定项目成败的还是人的系统思维、验证意识和电路理解。没有那套 Python 参考模型和充分覆盖率AI 生成得越快我对它越不放心。如果你也想在自己项目里试这个路子我的建议是先别拿复杂模块开刀。找一个 NTT、CRC、FIFO 这类边界清晰、接口明确的小 IP按我上面的流程跑一遍定义接口 → 分配 Agent → lint → 参考模型比对 → 回归 → 综合。等你把这套闭环玩熟了再往完整协议栈上推会比直接让它生成一个大模块稳妥得多。另外别忘了把每一步 AI 修过的版本都git commit下来——有时候改回上一版比说服 AI 改第三次更快。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。