ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI生成Verilog为何偏爱单模块?层次化生成破解RTL结构缺陷

AI生成Verilog为何偏爱单模块?层次化生成破解RTL结构缺陷 AI 写 Verilog 时有一个特别容易观察到的现象只要让你写一个“看起来有点规模的模块”——带 AXI 接口的 DMA 控制器、带 FIFO 的多通道 SPI 从设备、带 Cache 的访存通路——它交回来的代码几乎总是同一种形态一个巨大的module上百根端口信号铺在最上面几十个reg和wire堆在中间最后是几个几百行的always块把状态机、数据通路、寄存器配置、跨时钟域处理全部塞进去。整个设计没有任何子模块边界芯片的结构信息被“拍扁”进了一个二维的、线性展开的代码块里。我在过去一年多里评估过不少 LLM 生成的 RTL也拿这些代码跑过综合和仿真。坦率说这个现象的比例高到让我觉得它不是偶发而是当前大模型生成硬件描述语言时的结构性缺陷。ICLAD 2025 的 Best Paper《HiVeGen》的核心议题恰好就是这件事为什么大模型生成复杂芯片时总倾向于把逻辑塞进一个代码块以及怎么破解它这篇文章我会先把问题的临床表现列全再往 LLM 的工作机理里挖一层然后把 HiVeGen 的思路拆开讲最后给你一套看完马上能用的实践清单。无论你是前端 RTL 工程师、验证工程师还是想把 AI 真正用进芯片设计流程的工程负责人这篇文章应该都对得上你的痛点。1. “立体芯片被拍成一张二维版图”单块代码的典型症状1.1 一个典型 AI 输出的解剖我先给一个非常典型的例子。假设你让大模型实现一个带 AXI4-Lite 配置口和 AXI4-Full 数据口的 DMA 控制器它很可能给出这样的结构代码我做了浓缩但形态和真实输出基本一致module dma_controller_top ( // AXI4-Lite slave 接口用于CPU配置寄存器 input wire s_axi_aclk, input wire s_axi_aresetn, input wire [31:0] s_axi_awaddr, input wire s_axi_awvalid, output reg s_axi_awready, input wire [31:0] s_axi_wdata, input wire [3:0] s_axi_wstrb, input wire s_axi_wvalid, output reg s_axi_wready, // ... 后面还跟着几十根AXI-Lite信号 // AXI4-Full master 接口用于搬数据 input wire m_axi_aclk, input wire m_axi_aresetn, output reg [31:0] m_axi_awaddr, output reg [7:0] m_axi_awlen, output reg m_axi_awvalid, input wire m_axi_awready, input wire [31:0] m_axi_rdata, input wire m_axi_rlast, input wire m_axi_rvalid, output reg m_axi_rready, // ... 这里又是上百根master接口信号 // 直接把寄存器输出和对GPIO的控制也挂在顶层 output reg [31:0] reg0_ctrl, output reg [31:0] reg1_src_addr, output reg [31:0] reg2_dst_addr, output reg [31:0] reg3_xfer_len, output reg irq, inout wire [7:0] gpio ); // 内部状态定义 localparam IDLE 3d0; localparam WAIT_SRC 3d1; localparam START_TX 3d2; localparam BURST 3d3; localparam DRAIN 3d4; localparam DONE 3d5; // 一大坨混合的寄存器和wire reg [2:0] state, next_state; reg [31:0] addr_cnt, burst_len; reg [31:0] fifo_data; wire fifo_full, fifo_has_data; // ... 十几个内部信号和一个FIFO全部堆在这里 // 巨型always块1把所有接口时序和状态机做在一起 always (posedge s_axi_aclk or negedge s_axi_aresetn) begin if (!s_axi_aresetn) begin state IDLE; // 同时复位几十个寄存器 end else begin case (state) // 这里混着AXI写通道握手、读通道握手、内部状态变化 // 还要把中断逻辑一起写进来 // 500行打底 endcase end end // 巨型always块2组合逻辑读数据通路 always (*) begin // ... end // 巨型always块3挨个写配置寄存器 always (posedge s_axi_aclk or negedge s_axi_aresetn) begin // ... end // 某些FIFO的实例化也直接出现在这里没有封装成子模块 endmodule这个形态在 AI 生成的 RTL 里极其常见。端口信号全部裸露在顶层state 和 fifo 信号混在一个命名空间里三个巨型 always 块把 AXI 从机时序、DMA 搬运状态机、寄存器写入逻辑全部耦合在一起。你问它“为什么不分几个模块”它往往会回答“您的需求没有要求拆分我默认实现为一个模块”。1.2 “没有边界”远比“规模大”更致命有人可能觉得代码大就大一点反正功能仿真能过就行。但 PPA 和可维护性的问题恰恰在于“边界消失”而不在于“行数很多”。第一独立验证变成奢望。DMA 数据通路、寄存器配置逻辑、AXI 握手时序耦合在同一个模块里你没法单独给“寄存器配置模块”写一个定向测试也没办法只替换其中的数据通路实现来对比性能。验证工程师只能对着整个大模块写用例一旦仿真失败定位问题的成本会成倍上涨。第二接口复用等于零。DMA 里的 AXI-Lite 寄存器从机逻辑理论上在 SPI 控制器、I2C 控制器里也能复用。但当你把 AXI 握手逻辑直接揉进 DMA 顶层时这段逻辑就“锁死”在这个模块里了。下次你需要一个类似的 AXI 寄存器接口只能让 AI 再生成一遍然后又是新的潜在 bug。第三综合和布局布线工具会很痛苦。EDA 工具在做跨模块优化、时钟门控插入、面积/时序优化时非常依赖清晰的层次边界。一个巨型模块意味着综合器要面对一个超大级别的控制流这通常会导致时序收敛困难、关键路径优化空间变小。尤其在现代先进工艺下设计层次本身就是一种工程约束不是写代码时的可选风格。第四状态机和数据通路的混合会让 QoR 完全失控。当状态机的 next_state 组合逻辑和动辄 128bit 的数据比较器写进同一个always (*)块里工具很难重定时也很难做有效的面积优化。但从 LLM 的视角看这些逻辑放在一起单纯是因为“写起来顺”并不是因为电路意义上该放在一起。所以我们应该认清一个问题AI 生成 Verilog 时“塞进一个代码块”不是风格问题而是它没有理解硬件设计的核心——层级结构本身就是设计的一部分。2. LLM 为什么“偏爱”单模块从概率机制看生成习惯这一节我们来回答“为什么”。不是我猜的“因为模型菜”而是从 LLM 的训练目标、数据分布、自回归生成机制三个层面看这个行为在工程上有多根深蒂固。2.1 训练数据塑造的“标准答案”先想想 LLM 见到的大多数 Verilog 代码是什么样子的。网上能爬到的大量和 RTL 相关的数据主要来自教学教程、开源 IP、竞赛代码、Stack Overflow 讨论。这些代码有一个共同特点以单模块、单文件、独立功能为主。比如一个教学实验通常是“写一个 UART 发送模块”“写一个 FIFO”“写一个按键消抖模块”这种题目天然是单模块。大量开源 IP 虽然复杂但为了方便用户集成往往也把核心逻辑放在一个顶层模块文件里子模块则被打包成单独的 IP 目录LLM 在按“文本片段”学习时更容易学到的是“顶层模块那一大坨代码”而不是工程里的目录结构。所以模型从数据分布里学到的“标准答案”本身就更偏向单模块形态。它并不是不懂子模块而是在它的先验统计里一个回答对应一个 module 的代码是出现概率最高的模式。这不是推理能力的缺陷而是数据偏差的体现。2.2 远距离依赖与自回归模型的“最小阻力路径”自回归语言模型的核心限制是它一次只能看一个 token并预测下一个。要生成一个由多个子模块互相instance的复杂设计它必须同时维护很多跨文件、跨模块的“接口一致性”子模块端口名要匹配、位宽要对齐、例化时信号连接不能错。这要求在相当长的生成过程中模型始终锚定一大堆远距离的 token 约束。相比之下写一个巨大的单模块模型只需要在“当前窗口”内部维护一致性就行。即便整体很长它每时每刻要关心的只是最近的几百个 token。自回归机制的“最小阻力路径”就是这个相对于维持十几处跨模块引用的一致性糊出一个语法上自洽的大模块要“省力”得多。这也是为什么你按“实现一个 DMA 控制器”去问它它很容易给你一个长代码但如果你先让它“设计 DMA 的模块树先输出每个子模块的端口定义”再让它逐一实现效果会好很多。因为后者把跨模块一致性问题拆小了。2.3 目标函数里不存在的两个词可综合性与可维护性模型在训练时做的是“下一个 token 预测”对齐阶段最多用人类偏好打分。但人类打分往往也只看“代码像不像能运行”不会真的去跑 DC 综合、检查 reg2reg 时序、统计代码复用度。也就是说LLM 的优化目标里根本没有“可综合性”和“可维护性”这两项指标。它没有在“生成巨大粗糙模块”时收到过惩罚也没有在“生成层级清晰模块”时收到过奖励。相比之下一个人类工程师如果交出一坨 800 行的无层次代码评审会上会被同事和工具一起教育。但 LLM 从来没上过这个“评审会”。这就解释了为什么即便你明确告诉它“代码要模块化”它也会很快滑回单模块形态——因为这个行为在它的“经验”里永远不会被标记为错误。2.4 提示词的默认语境把模型推向了单 module最后是提示词的锅。很多人让 AI 写 Verilog 时指令就是“Write a Verilog module for ...”或者“实现一个SPI master”。注意这里明确写的是 a module不是 a design。模型的指令遵循机制是会抠字眼的。你让它“写一个 module”它就真的只给一个 module。大多数现成的 RTL 生成提示模板也都是这么写的等于默认把所有功能塞进了一个模块。如果你在提示词里提供的是完整芯片的 RTL 结构和模块树模型生成的代码就会明显更接近你给的结构。这不是玄学而是“上下文塑造输出”在起作用。3. HiVeGen 的方法拆解把层级变成生成约束HiVeGen 这条工作的名字直白地讲就是Hierarchical Verilog Generation它要做的不是“继续让模型使劲写更长的代码”而是把层次化设计本身变成生成过程的结构约束。下面按我的理解拆解它的核心思路。3.1 第一步从需求文本到模块树HiVeGen 的设计理念是一个复杂芯片的 RTL 不应该由 LLM 一次性“写”出来而应该先由 LLM 像架构师一样从功能需求里抽出一棵模块树。比如对 DMA 控制器它会被拆成若干个有独立功能的子模块寄存器配置块、DMA 状态机、数据通路、读写 FIFO 管理、中断控制器。模块树定义了每个节点的功能边界、端口方向、位宽、协议类型。这里的关键是“端口契约”在写任何一行 RTL 之前就定了下来。这和我实际做项目时的经验一致芯片 RTL 最怕的不是某个模块内部写得烂而是模块之间没有明确的接口约定。接口先定实现才能解耦。HiVeGen 把这件事变成了生成流程的第一步而且用结构化的方式输出不依赖模型在长代码中“自我约束”。3.2 先接口后实现的拓扑生成顺序有了模块树之后HiVeGen 的生成顺序很有讲究不是从顶层往下一层层写也不是按模块树从左到右写而是按拓扑顺序先叶子模块、后父模块。这背后的原因很合理。叶子模块功能单一、端口数量有限、内部逻辑独立LLM 生成它的难度最低、正确率最高。先把这些“确定性强”的模块写出来再用它们作为上下文去生成上层模块上层模块在例化时只需要面对已经固定的端口名和位宽不再需要凭空脑补子模块行为。这样生成的父模块代码里基本不会出现“正在例化一个不存在/改动过端口名的子模块”这类错误。我自己的实测也印证了这一点如果让 AI 先设计一个“子模块端口清单”再依照清单逐模块实现再写顶层例化最后的综合通过率远高于让它一次性写完整个设计。3.3 层级化的验证闭环HiVeGen 还强调验证的层级化。简单说它不是在全部代码生成完才开始统一仿真而是在每个叶子模块生成后就进行接口层面的一致性校验再生成父模块。这个过程有点像软件工程里的“持续集成”每一层都是可验证的单元生成一版、验证一版、再往上叠加。这实际上是在补偿 LLM 缺失的那部分“工具反馈”。如果模型生成一个子模块的端口位宽错了传统的单块生成模式要到整个大模块完成后才会暴露HiVeGen 的逐级验证能更早发现问题减少错误在后续模块间的传播。3.4 为什么这样改能解决问题回到最开始的问题模型为什么倾向于把复杂芯片塞进一个代码块本质上是因为生成单一代码块对模型的计算负担最小。HiVeGen 的方法实际上是在“替模型分担负担”——模块树分好了、端口契约定好了、拓扑顺序排好了模型每个时刻面对的都是一个规模和复杂度都可控的子任务不需要在 2000 行代码里维持全局一致性。单模块生成的诱惑自然就不存在了。这个方法能够拿到 ICLAD 2025 的 Best Paper我觉得它的里程碑意义在于不是强迫 LLM 变得更聪明而是承认 LLM 当下能力的边界然后用工程方法去绕开这个边界。这比无脑提示“请写得模块化一点”要可靠得多。4. 看完论文后直接能用的工程实践HiVeGen 是一个研究性质的工作但它的思想可以马上落进日常开发。下面这几条是我实测有效的操作建议你不需要跑论文的代码也能用上。4.1 改变提示词把“架构要求”写进前提条件不要只写“实现一个 DMA 控制器”而是把层次要求变成提示词的硬约束。我常用的一个模板长这样设计一个用于 SoC 的 DMA 控制器功能需求如下 - 支持 AXI4-Lite 寄存器配置接口 - 支持 AXI4-Full 数据搬运 - 支持中断输出 - 支持软件配置源地址、目的地址、传输长度 请在写任何 RTL 之前先完成以下三步 1. 输出模块树说明每个子模块的职责和边界 2. 输出每个子模块的端口列表名称、方向、位宽、协议说明 3. 说明模块间的数据流和控制流关系。 确认以上结构无误后按叶子模块到顶层模块的顺序逐个输出每个模块的完整 Verilog 代码。不要在顶层模块中写任何业务逻辑顶层只做例化和互联。这个提示词的效果比“请实现一个DMA控制器注意模块化”要好非常多。原因是它把“模块树”“端口列表”“生成顺序”这三件事变成了模型的显式输出目标模型必须在这个框架里作答就没那么容易直接给你一坨大代码。4.2 架构审查清单AI 输出先看这六个问题拿到 AI 的代码之后先别急着仿真我建议按下面这个清单做一次“架构体检”。任何一条命中都说明它的结构可能出了问题顶层模块里有没有出现always块如果有说明顶层不干净。单个模块里是否同时包含了超过两个协议域的握手逻辑例如一个模块既写 AXI 从机握手又写内部 FIFO 控制。状态机相关的状态寄存器是否和数据通路寄存器混在同一个always块里是否存在超过 10 个功能完全无关的输出端口全部裸露在同一个模块边界上模块内的引脚数量是否明显超过子模块边界所需比如一个 SPI Master 的顶层出现了中断控制器的输出。代码里是否存在大量没有层次含义的连续assign把深处的信号直接拉到了顶层这六条本质上是在验证一件事AI 有没有悄悄把层级的边界抹掉。如果命中两条以上直接要求它“按照模块树重构”而不是手工去拆。4.3 判断 AI 生成质量的几个硬指标除了“看着舒服”我建议团队用几个可量化的指标衡量 AI 生成 RTL 的结构质量指标建议目标说明顶层模块中的 always 块数量0顶层只做例化、连线、时钟/复位分发单个模块的最大行数不超过 200~300 行超过这个量级大概率是边界被吞掉了模块树深度至少 3 层top - subsystem - leaf防止单层堆叠端口数量每个子模块控制在 20 个以内超过则考虑用接口类型打包每个模块的功能职责数1一个模块只做一件事综合后的关键路径归属能定位到某个叶子模块如果关键路径横跨全部模块说明层次失效这几个指标不是论文里的死标准而是我实际项目里用来“卡”AI 输出的经验线。你不一定照搬但它能帮你快速判断一次生成是“合格的结构化设计”还是“漂亮的大代码坨”。4.4 我在项目里落地这套思路的实际体验我把这套方法用在一个内部测试项目上让 AI 实现一个带 AXI 接口的 SPI 控制器包含发送 FIFO、接收 FIFO、中断控制器和寄存器组。第一版直接让它“实现一个完整设计”拿回来是一个 700 多行的单模块综合直接 warning 一堆。后来我换上 4.1 里的提示词让它先输出模块树和端口列表再逐个生成模块结构质量明显改观顶层只有例化寄存器逻辑被单独封装成axi_reg_interfaceFIFO 被抽成独立的sync_fifo主状态机被隔离在spi_fsm里。更让我意外的是后一种方式的仿真调试成本也低了很多。发现发送 FIFO 的 almost_full 信号时序不对我可以直接对着sync_fifo模块修改和测试不用再在 700 行代码里大海捞针。这就是层级边界带来的实打实的收益。如果你正在用 AI 辅助写 RTL尤其项目里已经出现“AI 生成的模块越来越难改”的情况我强烈建议你先从“建立模块树 先接口后实现”这两个习惯改起。不要指望模型自觉做到这两点它写代码的语言能力再强也不会主动放弃那条“最小阻力路径”。结构约束只能由定了设计边界的人来给——那个人就是你。HiVeGen 这篇论文最可贵的价值就是把这件事从一句模糊的经验变成了一套可解释、可复用的方法。顺着它的思路走AI 生成 Verilog 的流程才能真正从“大模型写代码”进化成“大模型参与芯片架构设计”。
返回列表