我在AI芯片公司帮硬件工程师用Code Llama写RTL,半年后我们放弃了“替代”幻想

那天下班前,隔壁数字前端组的张博拍了拍我肩膀:“林默,听说你天天用AI写Python,能不能帮我们搞点Verilog?我们模块级设计太重复了,FIFO、仲裁器、状态机,一遍遍手写,出错率还高。”

我当时在键盘上敲Cursor敲得正欢,满口答应。心想无非是把Python代码生成换成Verilog,换个模型微调一下,能有多难?

半年之后,我不得不承认我错了。不是大模型不行,而是数字芯片前端设计的容错率太低,反馈回路太长。你写一行Python出bug,pytest两秒告诉你结果。你写一行RTL出bug,可能要到综合阶段才发现,甚至要到形式验证才暴露,中间隔了半小时到半天。所以这篇文章不是要吹嘘大模型能“取代硬件工程师”,而是老老实实记录我在这个过程中做的尝试:微调Code Llama提升一次性综合通过率,用编译反馈和形式验证工具搭建自修复回路,以及最后卡在什么地方。

30秒速览

  • - 针对性构建高质量可综合RTL数据集是提升大模型RTL生成能力的关键,互联网原始代码需经过Verilator+Yosys双重过滤
  • - LoRA微调Code Llama 13B相比StarCoder2 15B在指令遵循和综合通过率上略优,微调后首次综合通过率可达71%
  • - 加入编译器反馈的DPO强化学习能将综合通过率进一步提升至89%,模型习得了避免常见不可综合错误的能力
  • - 形式验证工具联动的自修复回路可将功能正确率从34%提升到74%,但复杂反例解析仍是瓶颈
  • - 大模型在时序约束、跨模块接口协议、面积功耗优化等方面几乎无能为力,实际落地仅限于简单模块级的代码辅助

芯片设计前端的瓶颈,我为什么盯上了代码生成

重复劳动与“可综合”诅咒

跟张博聊完,我花了两天蹲在数字前端组看他们干活。模块级设计确实有很多模式化的东西:一个同步FIFO,接口信号就那么几个,逻辑也是固定的——满信号判断、空信号判断、读写指针、格雷码转换。但你真让工程师手写,照样可能出bug:多时钟域没处理好、复位逻辑写漏了、综合出的门级电路面积偏大。更烦的是,不同项目规范还不一样,有的用Verilog,有的用VHDL,命名风格、编码规范各不相同。(延伸阅读:我用Copilot Agent给10万行Java单体画了张依赖图,生成的拆分方案差点让CTO以为我通宵了三个月

“可综合”更是硬约束。你在软件里写个while(1)死循环,没问题。在RTL里,综合工具直接报错——不可综合。你写个initial块做初始化,仿真能过,综合直接忽略。大模型如果不懂这些,生成出来的代码跟玩具没区别。所以我的目标很明确:不是让AI凭空创造架构,而是让它做那些有明确设计模式的模块,并且保证一次综合通过。

大模型能帮什么忙?模块级辅助,不是架构设计

我很快划定了边界:只做模块级、行为级描述的生成。比如给定接口信号定义和功能需求说明,让模型生成完整的Verilog模块。架构设计、跨模块接口、PPA优化这些,暂时别碰。说白了,就是让AI当个能听懂规格的代码抄写员。

这个定位很关键。因为我看到一些论文和PR稿声称“AI自动生成芯片RTL”,但实际一看,要么是简单组合逻辑,要么是拿开源IP拼凑。真要面对DDR控制器、USB PHY这种复杂模块,AI连接口信号都理不清。所以我一开始就没打算让模型做它能力之外的事。

构建微调数据集,我从开源IP和教材里“淘金”

数据的质量比数量重要一个数量级

微调需要数据。GitHub上能爬到的Verilog/VHDL非常多,但大部分是学生作业、验证代码、或是不可综合的testbench。直接拿这些数据喂模型,等于喂垃圾。所以我决定自己构建一个高纯度、全可综合的数据集。

我花了两周从三个渠道收集:

  • 开源IP仓库:像OpenCores、GitHub上的知名RISC-V Core(如PicoRV32、NEORV32)、Efabless平台上的开源设计。这些代码经过流片验证,质量有保障。
  • 经典数字设计教材的配套代码:比如《Verilog HDL高级数字设计》习题解答,《SystemVerilog for Design》的示例,以及一些大学公开课的Lab代码。这些代码规范,而且有详细注释。
  • 公司内部经评审的历史模块:张博帮我搞了些脱敏的FIFO、仲裁器、CRC校验、I2C/SPI从控制器等模块。这部分最珍贵,因为风格和约束与公司现行规范一致。

每条数据的格式我设计成“指令-响应”对。指令是一段自然语言描述,包含模块接口、功能、可综合约束、时钟域、复位策略等;响应就是完整的RTL代码。举个例子:

{
  "instruction": "Generate a synthesizable synchronous FIFO in Verilog. Parameters: DATA_WIDTH=8, DEPTH=16. Interface: input clk, rst_n, wr_en, rd_en, [DATA_WIDTH-1:0] din; output [DATA_WIDTH-1:0] dout, full, empty. Use gray code for read/write pointers. Include a registered output.",
  "output": "module sync_fifo #(...) (...);n // code ...nendmodule"
}

总共整理了大约4200条高质量样本,覆盖组合逻辑、时序逻辑、状态机、存储接口、简单协议控制器等类别。我还特意加了一个“失败案例”集合:标注了某段代码为什么不可综合,以及如何修改。这部分后面会用到。

数据清洗和“可综合性”校验脚本

光靠人工审核不够。我写了个自动化管道,每段代码自动通过开源工具Verilator做lint和综合检查(虽然Verilator主要是仿真和lint,但可以做简单综合语法检查),然后再用Yosys做一次综合尝试。如果Yosys报出“unsynthesizable construct”,这代码就得回去改。(延伸阅读:Rust 1.85 异步闭包如何让我扔掉连接池里的 Arc:一个架构师的三个月迁移复盘

这个过程让我发现一个惊人的数字:从GitHub原始爬取的数据,经过Verilator+Yosys过滤后,只剩不到30%直接可用。剩下的要么有initial块,要么有#延迟,要么用了system task如$display。可见互联网上的Verilog代码质量有多差。

LoRA微调Code Llama vs StarCoder,我选哪个?

实验环境:单卡A6000 48GB

我手头有一台工作站,NVIDIA RTX A6000 48GB。考虑到内存和微调效率,我选择用LoRA去做参数高效微调。基座模型我选了当时(2024年初)两个候选:Meta的Code Llama 13B 或 Code Llama 13BB Instruct,和BigCode的StarCoder2 15B。两者都是开源的代码生成模型,但预训练数据分布不同:Code Llama偏通用编程语言,StarCoder训练数据有The Stack v2,包含少量Verilog,但远不是主流。

微调框架用HuggingFace的PEFT+TRL,量化使用QLoRA(4-bit NormalFloat)以节省显存。我分别微调了3个epoch,学习率2e-4,rank=16,alpha=32,目标模块是q_proj, v_proj。训练数据就是我那4200条指令对。

实验数据对比

我用200条未在训练集中出现的测试用例,涵盖同步FIFO、异步FIFO、固定优先级仲裁器、循环计数器、SPI主发送状态机等典型模块,分别测量两个模型的首次综合通过率(用Yosys综合,只要zero error就算通过)和功能正确性(用Verilator跑简单的定向测试,输出与黄金模型比对)。结果如下:

模型 首次Yosys综合通过率 功能测试一次通过率 平均生成时间(秒)
Code Llama 13B 或 Code Llama 13BB (原始) 23% 11% 8.2
Code Llama 13B 或 Code Llama 13BB (微调后) 71% 58% 8.5
StarCoder2 15B (原始) 19% 9% 9.1
StarCoder2 15B (微调后) 66% 52% 9.3

微调后两个模型的综合通过率都大幅提升到70%左右,这说明针对性数据集确实有效。Code Llama稍好一点,我猜是因为它在通用代码结构上更灵活,指令遵循更好。StarCoder2虽然参数量略大,但它原本代码训练里硬件描述语言占比太低,导致微调时有些概念没吃透。

但70%通过率在芯片设计里还是不够。你让工具生成一个简单的加法器,十个里错了三个,硬件工程师还得一个个改,效率提升有限。所以我需要一个反馈机制,让模型能从错误中学习。

加入编译反馈的强化学习微调,让模型学会“可综合”

RLHF的思路移植到RTL

语言模型RLHF用的是人类偏好反馈。我把它改成了“编译器反馈”。具体做法是:模型生成一段RTL,我立刻用Yosys综合,并解析错误信息。如果综合通过就给正向奖励,不通过就给负向奖励,错误信息作为“批评”一起喂回去。这类似于RLHF中的PPO,不过我用的是DPO(Direct Preference Optimization),因为DPO不需要训练独立的reward model,更稳定。(延伸阅读:仿真分拣99.3%,实测掉到71.5%——我拆解Optimus视觉运动策略后发现的Sim-to-Real鸿沟

数据集构建成“好生成”和“坏生成”对。对于每个prompt,我让模型用不同温度采样生成4个候选,然后跑Yosys综合,选出综合通过且功能正确的作为chosen,综合失败的作为rejected。我收集了大概2000对这样的偏好数据,在LoRA微调后的Code Llama基础上再做一轮DPO。

这轮训练后,Yosys综合通过率从71%提升到了89%。更有意思的是,对于一些高频错误,比如漏写default case导致锁存器推断,或者组合逻辑块里不小心生成了循环,模型明显“学乖了”——它会主动补上default语句,避免敏感信号列表遗漏。

RL微调的工程细节

DPO训练比普通SFT慢,而且需要更多显存,因为每个样本要同时加载chosen和rejected。我的A6000勉强能跑batch size 2,用梯度累积。优化器用RMSprop,学习率5e-6。训练过程中,我发现一个很烦的问题:Yosys综合错误信息的格式非常工具特定,比如“ERROR: Syntax error near token ’#’ (2:1)”,这种对人类有用,对模型来说可能是个噪音。于是我写了个简单的错误分类器,把原始错误映射到更结构化的描述,比如“non-synthesizable construct: delay statement used”,再送给模型。这个预处理对DPO效果提升明显。

另外,因为数字芯片对时序敏感,我还尝试在奖励里加入时序违规的惩罚——用Yosys的粗略静态时序分析结果,但效果并不好,因为时序违规的原因常常很微妙,简单的静态分析报出来的warning太多,反而把模型带偏了。我决定这个留给后续迭代。

与形式验证工具联动的自修复回路,我的实操全记录

第一次尝试:生成-综合-反馈-修复的自动化管道

综合通过不意味着功能正确。一个FIFO可能综合出来,但满信号在特定条件下永远不拉高,这是个逻辑bug。传统的做法是写断言,用形式验证工具(比如JasperGold或SymbiYosys)去证明属性。我决定把这个也加入回路。

我的操作实录是这样的:

首先,我写了一个轻量级specification解析器,把用自然语言写的属性(比如“FIFO full标志在写入直到深度等于DEPTH时必须拉高”)转换成SystemVerilog Assertion(SVA)。这个解析器本身也调了一个微调过的Code Llama,因为写SVA是另一个头疼的活儿,不过这不是重点。

然后,对于模型生成的RTL模块,我自动生成对应的SVA属性,调用SymbiYosys做形式验证。如果验证发现property failure,就会返回反例(counterexample)——一串波形,显示在哪个时刻、什么信号值导致断言失败。(延伸阅读:我用GPT-5.5和Claude 4.8合成了一千张“无害”图片,差点在投资人面前把自己产品搞崩

我设计了一个自修复回路:将反例转成自然语言描述,附加到原始指令后面,重新让模型生成RTL,以此迭代最多5轮。举个例子:

原始指令: "Generate a parameterized priority arbiter with N=4 inputs..."
第一轮生成的RTL: 综合通过,但形式验证发现:当两个请求同时到达且优先级相同时,grant信号维持上上一次的值而不是清零。
系统生成修复提示: "The formal tool found a bug: when req[2] and req[1] are both high at the same cycle, grant doesn't clear before asserting a new grant. Please fix."
第二轮: 模型重新生成,问题修复。

整个管道我用Python和Makefile搭的,大概流程:

def auto_fix_pipeline(prompt, model, max_iter=5):
    code = model.generate(prompt)
    for i in range(max_iter):
        synth_ok, err_msg = run_yosys_synthesis(code)
        if not synth_ok:
            prompt += f"nSynthesis error: {err_msg}nPlease correct."
            code = model.generate(prompt)
            continue
        prop_ok, cex = run_symbiyosys_check(code)
        if prop_ok:
            return code, "success"
        else:
            prompt += f"nFormal property failed. Counterexample: {cex}nFix the bug."
            code = model.generate(prompt)
    return code, "max_iter_reached"

效果:形式验证通过率的大幅提升

在我准备的120个测试模块中,经过DPO微调的Code Llama初始生成后,形式验证一次通过率只有约34%。加入最多5轮的自动修复回路后,这个数字爬到了74%。虽然不是100%,但确实减少了大量人工debug时间。

但这里有两个巨大的坑。第一,有些生成错误根本不是简单的修修补补能解决的,模型可能会反复生成同一种错误模式的代码,陷入“颠簸”。第二,形式验证的反例解析本身就是个大工程。复杂的时序property反例可能包含几十个时钟周期的信号轨迹,要提炼成简短的自然语言描述非常难。我现在的做法是:只处理最简单的安全类属性(safety property),比如“满信号在任意时钟周期不该和空信号同时为高”。即便如此,依然有约15%的反例转化出错,导致模型得到误导性的修复指令。

这些坑,大模型暂时填不上——时序约束、跨模块接口与PPA

时序约束:不是代码的问题,是物理实现的问题

我前面提到的可综合性、功能正确性,都还停留在RTL行为层面。但芯片设计真正的恶梦是时序收敛。一个FIFO用纯寄存器写,综合出来可能延迟太大,频率上不去。你需要在RTL里就考虑流水线设计、重定时、加时序约束。但大模型生成代码时,完全不管这些。

我曾经尝试在指令里加上“target frequency: 500MHz, process: TSMC 28nm”,模型依然生成了一个单周期读写的简单FIFO,Yosys综合报出的关键路径延迟高达2.3ns。硬件工程师一看直接摇头:“这能用?”后来我尝试用合成器的时序报告作为反馈,但模型很难理解物理实现相关的约束,比如时钟树延迟、工艺角影响。这些知识超出了纯代码生成的范畴。

跨模块接口:一片混乱的信号名和握手协议

一旦模块超过单个层次,需要和其他模块连接,模型就开始乱来了。我给它一个AHB lite slave的接口规范,让它生成一个简单的寄存器文件。模型生成的代码里,HTRANS、HREADY的处理逻辑基本正确,但信号名的后缀一会是_o,一会是_out,一会干脆没有。这在芯片集成里会直接导致连线错误。

更深层的是协议理解。我试着让它生成一个带有反压的流水线接口,valid/ready握手经常写错:ready信号组合逻辑里引用了valid,或者反压链断掉。这类错误在仿真时如果激励不够巧合,可能根本发现不了,流片回来才发现功能bug。(延伸阅读:AI+制造业第三个项目:我给生产线上 15 个 Agent 建了共享记忆,结果它们差点把批次号全读脏了

要解决这个问题,需要模型对整个系统有层次化理解,这目前不是代码生成模型的强项。

PPA意识:面积、功耗、性能的权衡,模型一脸茫然

硬件工程师写RTL时,脑子里始终在权衡:用状态机还是微码?用寄存器还是SRAM?加法器用carry lookahead还是ripple?这些决策取决于PPA目标。我试图让模型在生成时考虑这些,比如要求“低功耗设计,使用clock gating”,模型确实添加了门控时钟使能,但它不理解何时该插入、插入几级,导致功耗没降反而可能引入毛刺。

说到底,PPA优化需要综合后的反馈,并且是物理设计工具的反馈,而不是简单的一个lint pass。目前我的工作流还做不到闭环。也许随着LLM与EDA工具更深度的集成,未来有希望。但在2025年的当下,这部分能力还只是早期探索。

避坑清单:如果你也想在公司试试AI生成RTL

折腾了半年,我踩过的坑比生成的代码还多。如果你或者你的团队想尝试类似的事情,下面这些总结可能帮你省下几百个小时的无效努力。

  • 别碰不可综合的代码训练模型。哪怕你过滤了initial和$display,模型还是可能从训练数据里学到不规范的写法。一定要用Yosys/Verilator双重过滤,且过滤后人工抽检。
  • DPO比RLHF更靠谱。数字设计的错误反馈是确定性的,不需要人类偏好模型,DPO实现简单且稳定。但数据对的质量决定了效果天花板,一定要精选chosen和rejected。
  • 从最简单的模块开始,别好高骛远。先从同步FIFO、计数分频器、简单的状态机开始,建立起整个管道(生成-综合-验证-修复)的流程,再往复杂模块走。一开始就想搞定USB控制器,只会让你怀疑人生。
  • 形式验证的反例转自然语言是瓶颈。除非你用JasperGold这种商业工具的高级debug功能,否则简单的反例波形翻译很难让模型理解复杂时序错误。可以从断言简单的安全属性入手。
  • 别让模型直接生成整个SoC。AI目前在RTL辅助上只能做模块级,而且必须在人的review下使用。别指望它能帮你设计缓存一致性协议。
  • 硬件工程团队必须参与数据构建与验证。纯软件背景的人很难理解“可综合”的微妙之处,比如assign里的三态逻辑、锁存器的意外推断。团队合作是必须的。
  • 版本管理所有生成代码和prompt。一次微调后,模型的行为就变了。你需要清楚地记录每个版本的prompt模板和生成质量,否则后面debug会疯掉。

最后,我必须说,AI辅助RTL生成不是炒作,它确实能在特定场景下提升效率——把工程师从重复的模块编码中解放出来,去做更关键的架构决策和时序收敛。但它离“替代”还远得很,目前更像是你工具箱里的一把智能锤子,锤得准不准,还得看你拿锤子的手。我后来把这套流程封装成了一个内部工具,取名叫“RTL Copilot”,张博他们组开始用起来,平均每两周能省下大约15%的编码时间。不过他们依旧保持人工code review,毕竟,流片的代价太高,没人敢把命运完全交给AI。

我亲手操作Code Llama生成FIFO的完整记录

第二天上午,我打开终端,拉下Code Llama 34B的GGUF量化版,用llama.cpp起了本地推理服务。张博发来一份spec:深度16、位宽64bit、支持almost_full和almost_empty标志的同步FIFO。

我先敲了一次prompt:”Generate a synthesizable Verilog synchronous FIFO with depth 16, width 64bit, and programmable almost_full/almost_empty thresholds.” 模型吐了90行代码,语法正确,但读写指针用了integer类型——这在综合时会被工具干成32位寄存器堆,面积直接爆炸。

我改了prompt,加上”use explicitly sized registers, no integer type”,又强调”use gray-code for cross-clock safety”。这次输出好多了,但它把格雷码转换逻辑写在读时钟域里,跨时钟路径变成了异步电路,综合工具会报timing violation。

我不得不在prompt里逐条约束:”gray code conversion must happen in the source clock domain before the pointer crosses the boundary”。反复三次prompt,加上手动改了三行assign语句,才拿到一份能过Lint的代码。

那一刻我意识到:不是模型不会写Verilog,而是它不懂物理实现。它不知道什么会炸面积、什么会炸时序、什么会让Synopsys DC吐三千条warning。这些知识不在语言模型里,在那些熬夜看综合报告、跟后端撕timing corner的工程师脑子里。

本文由 AI 辅助生成,经人工审核后发布。内容由 林默 基于实战经验指导完成。

觉得有用?

林默

全栈开发者,写了8年代码,从jQuery时代一路写到AI Copilot。目前专注AI编程工具链的深度使用和评测,相信好的工具能让开发者事半功倍。喜欢用实际项目验证技术方案,不写没踩过坑的教程。

发表评论