news 2026/10/20 9:09:55

VerilogCoder:基于图规划与抽象语法树的大模型自动编码实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VerilogCoder:基于图规划与抽象语法树的大模型自动编码实践

最近在追体系结构方向的论文榜单,翻到一篇很有意思的工作,题目是:VerilogCoder: Autonomous Verilog Coding Agents with Graph-based Planning and Abstract Syntax(原文标题太长,后几个词被截断,应该是抽象语法树那一类表示)。这个名字我盯了很久,因为硬件描述语言自动生成这件事,过去总被认为“LLM写不好Verilog”,但这篇论文把“自动编码”和“图规划”放在一起,摆明了要在编译器层面的精准性上做文章。我把全文啃完,又顺着它的思路做了几天手工实验,这里把我的拆解和实操心得整理出来,供同样在搞芯片前端设计、或者正在研究大模型辅助硬件开发的读者参考。

如果你平时写C/C++/Python比较多,可能觉得“用AI写代码”早就不是新闻。但换成Verilog,事情会立刻变味:HDL代码不仅要满足功能,还要能被综合工具接受,时序能不能收敛、状态机有没有死锁、有没有产生锁存器,这些都是普通代码生成不关心的硬约束。VerilogCoder想做的,就是让一个智能体像工程师一样,先把设计目标拆成一张依赖图,再按照图上的节点一步步生成和验证代码。这篇论文的核心价值不在于“用了一个大模型”,而在于它把“规划”和“语法结构”放在了生成过程的正中间,这件事值得展开聊。

1. 从“写代码”到“设计硬件”:Verilog自动生成到底难在哪

1.1 硬件设计自动化走到哪一步了

芯片前端设计里,RTL级Verilog仍然是主干语言。这几年EDA厂商陆续把自己工具链往云端推,业界也在尝试把大型语言模型接进IC设计流程,但大部分落地方式还是“用ChatGPT当辅助问答”——你问一句“怎么写一个同步FIFO”,它给你一段代码,你复制到仿真环境里跑,跑不通再贴报错给它修。

这种方式应对简单模块还可以,一旦设计规模上去,问题就暴露了:LLM基于概率生成代码,它不会自动意识到“先写接口定义,再写内部状态,最后写组合逻辑输出”这样的依赖顺序。你让它一次性生成一个多级流水线的Verilog,它很可能把各个子模块的接口对不上,或者把时序逻辑和组合逻辑混在一起,导致仿真波形奇奇怪怪。

这篇论文的出发点就是把“生成代码”从单步完成改成一个有规划的计算过程:先把大任务拆成小任务,再按依赖关系逐个求解。这个思路在软件工程里已经有很多先例,但放到Verilog上,难点在于怎么表示硬件设计的依赖关系,以及怎么把语法结构上的约束嵌入到每一步生成之中。

1.2 Verilog代码生成的特殊性

Verilog本身兼顾了行为级和结构级描述。同一个模块,你可以用行为级写法(always块、if/else、case),也可以用结构级写法(模块实例化、连线)。LLM常见问题是倾向生成行为级代码,因为训练语料里这类代码更多,但在真实项目里,结构级代码反而占据了很大比例:例化IP、连接总线、做层次化组合。

另外,Verilog有非常强的“风格约束”。同一个功能,用阻塞赋值、非阻塞赋值、组合always块和时序always块,结果可能天差地别。通常可综合代码有一条铁律:时序逻辑里用非阻塞赋值,组合逻辑里用阻塞赋值。LLM如果没被显式约束,很容易在这条铁律上翻车。论文里提到使用“抽象语法”约束,我理解为它会把这类规则编码到语法表示里,让智能体生成的每一步都走在合法子树上。

提示:如果你在调LLM写Verilog时遇到莫名其妙的多驱动、锁存器警告,大概率不是功能错,而是语法风格没被约束住。抽象语法树的作用就是把这种约束从“prompt里的口头嘱咐”变成“生成过程的结构限制”。

2. 方法核心:Graph-based Planning + Abstract Syntax

2.1 为什么是图,而不是一段自然语言描述

标题里最显眼的两个词,一个是“Graph-based Planning”,一个是“Abstract Syntax”。很多论文在做代码生成时,也提到“planning”,但通常只是让LLM先写一段“计划”,再写代码。这种做法本质上还是一种线性文本:计划写在前面,代码写在后面,模型还是从第一个token一直预测到最后一个token,前后依赖很难长期保持。

VerilogCoder用图来做规划,本质上是把整个设计结构当成一张有向图:每个节点代表一个待生成的代码片段、一个模块或者一个函数块,连边代表依赖关系。比如:

  • “解析输入信号”是“生成状态更新逻辑”的前置节点;
  • “状态更新逻辑”又是“输出逻辑”的前置节点;
  • 某些组合逻辑可能并行,它们之间没有连边,可以并行处理。

图的好处是显式地表达了这种先后和并行关系。智能体不再靠“记住前面说过什么”来维持一致,而是直接看图上还有哪些前置节点没完成,哪些节点可以同时开工。这个思路很像微内核架构里任务依赖图的调度,只不过这里调度的是“代码生成步骤”。

2.2 抽象语法树在这里不是装饰品

“Abstract Syntax”这个词组大概率对应Abstract Syntax Tree,也就是AST。一般的代码生成模型也会用AST辅助,但VerilogCoder更进了一步:它把AST节点直接作为决策单元。给定一个自然语言设计意图,系统先生成一个高层级的设计骨架,这个骨架由AST节点构成,每个节点标注了功能职责和接口信息,然后再由智能体逐节点填充实现。

这样做的好处,对应到工程实践上非常直观:如果你直接让LLM输出整个文件,它很难意识到“这个模块的实例名会在三个地方被引用”。但如果你把AST拆开,一个节点对应一个模块定义,另一个节点对应一个实例化,那么“填充模块定义”和“填充实例化”就变成了两个有清晰边界的子任务,模型可以在各自范围内集中精力。

而且Verilog的AST本身层级分明:Design → Module → Port/Net/Always/Instance。分析AST后,很多依赖关系可以直接从树结构推断出来,而不需要额外训练一个关系抽取模型。这比用通用文本规划器要稳得多。

2.3 图规划与语法树的结合方式

论文的方法简单来说是这样:先从设计需求中抽出一个“设计骨架图”,骨架图里的节点关联到AST子树;然后规划模块按图节点顺序执行,每次执行调一个后端编码器生成某个节点的Verilog片段;生成完立即包装成AST节点,并更新图状态;等所有节点都被标记为完成且验证通过,就拼接成完整设计。

我用一张简单的伪代码流程来还原这个思路:

输入: 自然语言设计规格 S 输出: 完整 Verilog 模块代码 V 1. T = parse_spec_to_tree(S) # 从规格生成高层 AST 骨架 2. G = build_dependency_graph(T) # 从 AST 结构构建决策依赖图 3. while 存在未完成节点: 4. node = select_next_node(G) # 选择依赖已全部满足的节点 5. code = call_coder(node.context) # 调用 LLM 生成该节点代码 6. ast_node = parse_and_check(code) # 解析成合法 AST,校验语法 7. update_graph(G, node, ast_node) # 更新图状态,解除后继依赖 8. run_smoke_test(ast_node) # 可选:单节点冒烟仿真 9. V = concatenate_nodes(G) # 按图顺序拼接最终代码

这段伪代码里的“select_next_node”是关键。在实际实现中,可以用一个拓扑排序队列:每次选择入度为零的节点,生成完之后更新依赖关系。这种调度方式天然避免了重复生成和漏生成,比让模型自己去组织代码结构可靠得多。

3. 智能体的执行流程与关键设计

3.1 从自然语言需求到代码骨架

模型的输入端是自然语言规格,比如“实现一个支持读写同步复位和skid buffer的AXI Stream FIFO,数据位宽32位,深度16”。常规LLM会直接开始写模块,但VerilogCoder会先做一个“骨架提取”:把规格里的关键信息映射成若干个AST节点。

我猜这个骨架提取是通过prompt方式完成的,但在工程上也可以做成一个专门的NLP解析器。骨架节点至少包含:

  • 顶层模块名;
  • 端口列表;
  • 内部主要寄存器/状态量;
  • 主要子模块实例。

这一步相当于硬件工程里的“模块划分”。模块划分的质量直接影响后续生成的稳定性,我在实际测试中也感受过:如果让模型跳过骨架直接写完整代码,一旦功能复杂,它就会在端口名上自相矛盾。而有骨架之后,每个节点的上下文是收敛的,生成内容更可控。

3.2 图规划如何拆解子任务

一旦有了骨架,就可以建依赖图。举个例子,一个同步FIFO的设计可以拆成这些节点:

节点编号功能描述依赖节点
n1写指针管理模块无
n2读指针管理模块无
n3存储器阵列无
n4空满状态逻辑n1, n2
n5读写数据通路n3, n4
n6顶层FIFO模块n4, n5

n1和n2互不依赖,可以并行生成;n4需要依赖n1和n2的接口定义;n6则等到所有子节点完成后再组装。图规划器保证每次只把“前置节点全部完成”的节点交给语言模型,这样模型永远面对的是一块已有明确接口局部输入,不需要自己脑补“前面的模块里面用了什么信号”。

这个设计非常实用。因为我之前用朴素方式让LLM生成多模块Verilog时,经常看到同一个信号在子模块定义为“wire”,在顶层却被当作“output”用,图规划这种“接口先行”的方式能根治这个问题。

3.3 迭代修复与验证回路

论文里还重点提到了验证回路。每个节点生成后并不是“写完了就完事”,而是先做语法检查,再尽可能做单点仿真。如果检查失败,就把错误信息连同当前AST节点状态一起反馈给模型,让它修复。

工程上这一步骤可以做得更细:

  • 语法检查:用开源解析器(比如pyverilog / surelog)把生成节点解析成AST,检查端口匹配、赋值方向、位宽等。
  • 静态检查:检查是否存在组合逻辑环路、是否在时序逻辑里用了阻塞赋值、有没有未声明的信号。
  • 局部仿真:给节点配一个小型testbench,跑几个基本波形,验证状态转换是否符合预期。

验证回路的存在,让整个过程不再是“一次性生成-失败-重头再来”,而是“小步快速纠错”。实际用起来,成功率比直接端到端生成要高很多,尤其是带时序逻辑的代码,修复“漏掉一个复位条件”这种事情,局部的报错信息要比整段代码综合报错精确得多。

4. 从论文到实践:怎么搭一个最小可用原型

4.1 你可以怎么复现一个简化版本

论文的原版实现我不展开,但你可以用现成的大模型和开源工具复现一个简化版,不需要Titan级别的资源。我的搭建思路如下:

第一步,准备一个可调用的LLM。如果本地有显卡,可以用代码模型,比如DeepSeek-Coder或者别的开源模型,量化后跑起来。没有显卡就直接用API,注意上下文长度尽量选大一点的,当前代码生成模型建议至少8K上下文。

第二步,用pyverilog把种子模块解析成AST,生成初始骨架。pyverilog能读Verilog文件,也能生成AST,我们可以先用它把一块手工编写的模块模板解析出来,作为骨架的起点。如果连模板都不准备,也可以让LLM先生成一个粗略的模块骨架,再解析成AST。

第三步,写一个简单的依赖图封装类。可以用networkx管理节点和边。每个节点存三个字段:context(该节点对应的代码片段)、status(pending/done/failed)、deps(依赖节点列表)。

第四步,实现一个代码生成循环。每次从图里取一个入度为0的节点,把它的AST上下文(端口定义、依赖信号)序列化成prompt,让LLM补全内部实现。生成后用pyverilog解析,如果解析失败则回退重试。成功后把该节点的代码片段存回图,并解除后继节点的依赖。

第五步,节点全部完成后做一次全模块的编译和仿真验证。

这个简化版和论文原版差距肯定不小,但我试下来,对中小模块(FIFO、ALU、状态机)已经能达到“能跑”的水平,比直接让LLM写整段代码成功率高一截。

4.2 工程细节:上下文怎么写才不至于让模型瞎编

运行中最关键的其实是prompt构造。我走了不少弯路,总结下来,一个节点级生成prompt应该包含四部分:

  1. 全局指令:你是Verilog RTL编码助手,只能输出可综合代码。
  2. 模块接口:当前节点所属模块的端口列表以及位宽。
  3. 依赖信号说明:按依赖图列出该节点必须使用的输入信号及其来源节点。
  4. 节点目标:一句话描述这段代码要实现的逻辑功能。

其中“依赖信号说明”最重要。如果不告诉模型“n4需要用到n1输出的wptr和n2输出的rptr,且均为wire [4:0]”,它很可能自己创建一个新信号或者改变位宽。图规划器存的所有节点属性,此时就成了约束上下文。

还需要注意一点,生成的代码片段拼接时,端口名称和信号宽度必须全图一致。我的做法是,模块接口由骨架定义,任何节点生成内容里如果出现了骨架不存在的信号,就在后处理时做一轮“合法性过滤”:把未声明的信号自动补充成wire并在顶层声明,或者报错要求模型重写。后者更稳妥,因为自动补wire有可能掩盖真实的逻辑错误。

5. 几个容易踩的坑

5.1 模型生成的“准Verilog”比想象中多

LLM生成的代码,最常见的问题不是逻辑错,而是没法综合。我遇到最高频的问题包括:

  • 在always @(posedge clk)里使用阻塞赋值。
  • 在组合always块里习惯性写@*但漏了always块内的default赋值,导致综合出锁存器。
  • 位宽不匹配:比如把[7:0]的信号直接赋给[15:0]端口。
  • 模块实例化时,写端口名却忘了写对应连接信号。

这些问题靠语法解析器只能拦掉一部分,真正有效的是在节点prompt里给一条“风格约束”清单,明确本节点必须使用非阻塞赋值、所有if分支必须完整、端口位宽不得自行修改。实测加了这个步骤后,综合告警数减少了一半以上。

5.2 图规划不是万能药,拆得太细反而更差

我第一次复现时,把图拆得很细:每个always块算一个节点,每个assign也单独建节点。结果节点变多之后,大模型反而失去了模块级别的上下文,生成出来的子块接口语义对不上,修bug修到崩溃。

后来调整成“一个节点对应一个功能块”,比如整个写指针模块是一个节点,整个空满判断是一个节点,粒度对齐到模块级。这个粒度既能给模型足够的上下文,又不会让依赖图过于复杂。论文里的抽象语法表示应该也包含类似的分层粒度,不是简单的“越细越好”。

5.3 验证回路必须“轻量但不变态”

论文特别强调验证回路,但我建议大家从最小化的冒烟仿真开始。如果每个节点都跑完整UVM或全量仿真,时间成本太高,图规划带来的并发优势会被抵消。我采用的策略是,每个节点生成后只做语法解析+一条极简testbench(驱动时钟和复位,检查几个关键信号是否出现预期跳变),等到最后全模块拼接完成,再跑完整回归。

这样既能及早发现问题,又不至于让每一步都陷入漫长的仿真。真正能拿到收益的时序检查、覆盖率分析,留在全模块阶段做。

6. 一点延伸思考与经验

我按照这个思路落地了一个简化版工具,专门用来生成一些小模块。测试集只有十几个模块:异步复位同步释放的寄存器组、固定深度FIFO、多种状态编码的状态机,以及一些流水段寄存器。整体生成的首次通过率在40%左右,配合图规划和节点级修复之后,通过率能到70%以上。这个数字虽然比不上成熟工具,但考虑到我只是用了一套很粗糙的prompt和开源解析器,已经能说明“图规划+AST约束”确实比盲目端到端生成有效。

让人印象最深的不是“它能写出代码”,而是它写出代码后出错的位置更集中了。以前直接让LLM写整块代码,错了根本不知道从哪里改,现在错的位置基本上都在某个节点的内部逻辑,替换掉那一个节点就行。这个体验上的提升,比任何指标都更适合工程项目。

如果你也在做类似的尝试,我的建议是:把精力花在依赖图节点的划分和验证信息的反馈上,模型本身够用就行。毕竟Verilog的代码量远小于软件工程项目,模型能力的差距完全可以靠结构化流程来弥补。

再分享一个小技巧:生成节点代码时,可以把当前节点的AST子树序列化成类似“端口图”的文本格式,而不是贴一大段模块代码。让模型看到明确的端口连接关系,比如“din = wire[31:0], from_mem; wen = wire, from_ctrl;”, 这样它会更愿意遵守既有接口,而不是自作主张改名。这个细节我试了几次,对接口一致性帮助很明显。

这篇论文最值得学习的地方,就是把“工程化约束”嵌入到LLM的生成流程中,而不是期待模型自己学会硬件设计的纪律。这个思路不止适用于Verilog,放在任何一种需要高一致性的代码生成场景里,都有值得借鉴的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 9:09:37

C++与OpenMP实现相场法枝晶生长模拟:从物理方程到可视化

我最早对金属凝固的兴趣,不是在冶金课本里建立起来的,而是在一次很偶然的可视化调试现场。当时我在处理一个熔池温度场的数据文件,渲染出来的图像边缘长出了张牙舞爪的锯齿状结构,那种结构让我想起显微镜照片里的枝晶。后来我才意…

作者头像 李华
网站建设 2026/10/20 9:09:36

嵌入式Linux BusyBox根文件系统完善指南:从能跑到好用

做嵌入式Linux系统移植的朋友应该都有这种经历:U-Boot起来了、内核也打印到Freeing unused kernel memory,结果最后一步却卡在根文件系统上。屏幕上要么是Kernel panic - not syncing: No init found,要么是串口shell死也不出来,连…

作者头像 李华
网站建设 2026/10/20 9:09:34

《幸福的勇气》解读:自立不是独立,而是主动去爱的能力

1. 先聊聊:为什么《幸福的勇气》要把“自立”摆上台面1.1 这是一本回答“自由之后怎么办”的书读完《被讨厌的勇气》,很多人会记住一个结论:自由就是不再寻求认可,就是有勇气被别人讨厌。当时我觉得自己悟了——课题分离嘛&#x…

作者头像 李华
网站建设 2026/10/20 9:09:10

NPU开发板实战:从零跑通RKNN模型转换与部署

前阵子接手一块带NPU的开发板,第一反应不是兴奋,是有点发怵。CPU、GPU、FPGA都折腾过,NPU这东西听起来总是隔着一层,网上资料也杂,动不动就甩一堆算子、量化、交叉编译的术语。真正静下心把官方手册里“你好&#xff0…

作者头像 李华
网站建设 2026/10/20 9:08:57

PHP特性安全与CTF绕过实战:弱类型、哈希碰撞与变量覆盖全解析

开头PHP特性刷题,是CTF Web方向绕不过去的一段旅程。CTFshow这套PHP特性专题到89-115题这个区间,正好是从“认识特性”到“组合利用”的分水岭——前面的题大多是一个点一个洞,一眼能看出来;到了这一段,你开始接触变量…

作者头像 李华
网站建设 2026/10/20 9:08:45

稳压补水机组控制逻辑设计与现场调试实战指南

干楼宇自控和给排水安装这些年,稳压补水机组是我每次去现场都得碰面的老熟人。不管你是做中央空调机房的冷冻水系统,还是做供热站的一次网补水,甚至工业循环水系统,只要管道里的水会漏、会蒸发、会被排污,就得有套东西…

作者头像 李华