我叫陈硕,干了10年后端架构师,写过Java GC调优脚本,也重构过Go微服务的链接池。这十年我只信奉一条铁律:做技术决策先看系统边界,再看成本约束。然而2024年秋天,我坐在一堆物理设计报告前,盯着一个130nm混合信号SoC的floorplan,第一次觉得自己像个刚毕业的实习生。芯片布局这个问题,状态空间比围棋大30个数量级,传统模拟退火跑了48小时只挤出2%的线长优化,时序还是红的。于是我做了一个让纯EDA工程师觉得离谱的决定——把强化学习塞进布局流程。但更让他们意外的是,我没有用RL重写一个布局器,而是让PPO变成商业工具链里的一个“布图规划劫持器”。这篇文章就是这场180天工程实验的完整复盘,从MDP建模、OpenROAD环境搭建、特征工程、多目标奖励设计,到训出来的模型怎么把一个riscv32i的WNS从-320ps拉到-85ps,再到迁移到商用工具时差点把ICC2的license pool打爆。
30秒速览
- - 芯片布局的状态空间极脏,直接坐标动作不可行,必须用离散候选槽加合法化引擎,且需要引入密度、拥塞、时序三类手工特征。
- - 架构选型上放弃端到端RL布局器,选择只优化macro floorplan然后交给商业/开源工具完成标准单元placement和布线,平衡了风险与收益。
- - 奖励函数设计是最大坑:多目标优化需要自适应权重,尤其时序惩罚必须用PID动态调整,否则代理要么线长爆炸要么时序永远红。
- - 课程学习从gcd到多核SoC才能让模型收敛,直接用复杂网表会导致毫无意义的行为。
- - 跨工艺迁移需要DRC感知奖励和归一化策略,微调成本远低于重训,但零样本是不可能的。
- - 最终在sky130的BlackParrot上WNS改善62.5%,线长减15%,但商业集成需处理license池和工程师信任问题。
把芯片布局塞进马尔可夫决策过程,我花了三周才搞懂状态空间有多脏
物理设计工程师看布局问题,是一连串的手工约束、经验法则、还有“这堆macro放在左边肯定绕线”的直觉。强化学习工程师看任何问题,都想把它抽象成五元组(S, A, P, R, γ)。我第一次画状态转移图时,觉得这很简单——把宏单元位置作为状态,移动宏单元作为动作,线长和时序作为奖励。然后我被现实教做人了。
S的维度爆炸:不是所有坐标都值得放进张量
一个现代SoC可能有80个macro,加上200万标准单元,状态空间如果用全坐标表示,直接炸穿。我最初的设计是用320×320的网格画布,每个macro的位置用(x,y)编码,状态就是80*2=160维向量。代理动一下只是改变某个macro的坐标,动作空间定义为80*2=160维连续动作?不行,这会让训练效率归零。我改成了离散动作:每次选择一个macro,在上下左右四个方向移动一个step(step size可调)。动作空间=80*4,还是太大。而且网格精度很粗糙,320×320像素意味着最小移动步长是几百纳米,对macro动辄几百微米的尺寸来说,这几乎就是盲人摸象。我意识到RL做布局不能直接去动像素级坐标,得用序列决策范式:代理每次选择一个macro,然后决定放到哪个可行区域。
这里我做了第一个架构决策:放弃连续的坐标动作,改用“宏单元选择”+“候选位置评分”的两阶段动作。第一阶段输出一个宏单元索引,第二阶段在给定该单元的离散候选位置上输出放置概率。候选位置怎么来?基于当前布局用快速合法化引擎生成几个候选slot,保证不重叠。这个动作空间从O(N)降到了O(N×K),K通常是5-10。虽然还是离散,但训练效率上来了。代价是这明显不是全局优化,而是贪心序列决策,但我后来通过PPO的轨迹优化和多次rollout弥补了这个缺陷。(延伸阅读:Rust 1.85 异步闭包如何让我扔掉连接池里的 Arc:一个架构师的三个月迁移复盘)
那个让我失眠的奖励延迟问题
EDA工具链的“一步”不像游戏,不是按一下方向键就能看到新分数。在布局里面,动一个macro,你要重新做legalization、global routing、甚至clock tree synthesis,才能评估时序质量。OpenROAD跑一遍global route最少要20秒,PPO一次rollout要收集512步经验?那训练一个epoch就得3小时以上。显然不能实时交互。我做了异步环境并行化:起8个独立进程,每个进程持有一份OpenROAD的flow,独立运行rollout,结果汇总到replay buffer。这跟A3C的精神一致,但用在芯片布局上,还要处理工具锁和磁盘IO竞争。我后来发现OpenROAD不能同时运行多个实例在同一个PDK目录下,因为共享的technology LEF文件会冲突。解决方式是把PDK拷贝8份,用不同的临时目录隔离。这是第一个坑:EDA工具不是为多实例并行设计的。
但奖励延迟的根因不在速度,在因果链太长。动一下macro,最终时序是经过全局布线、详细布线、寄生参数提取后才出来的。强化学习代理很难从最终时序直接归因到200步前的某个摆放动作。为此我引入了中间奖励:每次移动后,用快速虚拟布线(FastRoute)算一次拥塞估计和线长增量,给即时奖励塑形。然后用OpenSTA的早期时序分析作为长线奖励,延迟到这个episode结束时回传。这其实就是reward shaping的经典套路,只不过在EDA领域,虚拟布线和STA本身就不准,给代理带来了额外的噪声。后面在奖励函数设计章节我会细讲这个代价。
为什么我没自己写一个布局器,而是把PPO变成了“宏单元搅动插件”
这是整个项目最受争议的架构决策,也是我反复和硬件团队吵架的地方。搞AI的人总想端到端:输入网表和工艺库,直接输出GDSII。搞后端的人认为这是找死,因为标准单元的布局优化有无数corner case,商用工具里塞了几百万行C++代码,你不可能用一个PyTorch模型就替代。最终我选择了第三条路。
三套架构方案的对比,我直接把端到端选项扔进了垃圾桶
我在项目启动第二周画了一张表,把当时可行的三种RL介入芯片布局的方式列出来,然后逐条毙掉了前两条。
| 方案 | 描述 | 优势 | 劣势 | 我的决策 |
|---|---|---|---|---|
| A:端到端RL布局器 | 用RL直接输出所有macro和标准单元的位置,替代整个placement流程,类似Google用RL做芯片布局的早期思路。 | 理论上的全局最优;无需依赖商业工具;学术界发论文漂亮。 | 标准单元数量百万级,动作空间巨大;时序闭环困难;无法保证可布线性;商用PDK不开放导致没法真实验证;训练一次成本百万美元级。 | 放弃。我没有50块H100,更不想解释为什么模型吐出的版图DRC错了几十万个。 |
| B:RL宏单元规划器 + 商业/开源标准单元布局器 | 用PPO代理只决定宏单元(macro)的位置和朝向,标准单元的详细布局交给传统工具(Innovus/OpenROAD),布线、CTS等照旧。 | 宏观空间减小到几十到几百宏;商业工具保证标准单元质量和DRC;可插拔进现有流程;训练成本可控。 | 不是全局最优,只是初始布图规划的补充;RL输出可能让后续流程时序难以收敛;需要搭建奖励的反馈环路。 | 选择。这正是我要的“劫持”模式:PPO负责出floorplan,工具负责实现它。 |
| C:RL生成布局约束或宏分组策略 | 代理不直接输出坐标,而是输出约束(如guide region、group、soft bounds),传统引擎在约束下做placement。 | 风险最低,代理输出的是抽象策略;容易人工介入;迁移性强。 | 过于保守,自由度小,难以发现人类专家没想到的布局;代理探索空间被约束框死。 | 部分采纳,作为混合动作的一部分——PPO在候选位置中挑选时,会隐含地学习分组关系。 |
我最终选择了方案B,并且把它做成了可插入到商业工具(Cadence Innovus或Synopsys ICC2)的一个独立步骤:先让PPO根据网表和时序约束生成macro的初始floorplan,存成.def文件,然后喂给商业工具,它会在这个floorplan基础上做标准单元放置、CTS、布线。我甚至做了个简单的脚本,可以把PPO输出的.def中macro的FIXED属性改成PLACED,这样商业工具还能微调macro位置,但初始解已经由RL提供。实际应用中,如果工具后续微调动了macro导致时序变差,我会在奖励里面加一项对最终macro位移的惩罚,让代理学会输出“稳定”的floorplan。
做出这个决策的核心逻辑是系统思维:我承认商业工具在详细placement和routing上的优化能力比我手写的任何启发式或ML模型都强,所以我不要重新发明轮子,我只要在轮子上加一个更好的方向盘。后来实验证明这个思路是正确的:只用RL优化宏单元布局,配合Innovus默认流程,整个PPA提升的很大部分来自于macro摆放的改善,标准单元placement反而没怎么变动。
我把网表压进张量时,特征工程让我跪着补了三个月物理设计
一个后端架构师最自信的时刻是建数据模型,最崩溃的时刻是发现新领域的特征完全不是你想的那样。芯片网表转换成图后,我一开始用了最标准的GNN特征:每个macro的尺寸、连接关系、hop距离。结果代理训练了500个episode,线长比随机还差。
从网表到状态张量的三层特征,我抄了时序分析器的作业
问题出在:布局问题的关键特征不在连接,而在物理邻近性导致的后效。macro A和macro B有连线,但是把它们放远不一定会造成时序违例,因为中间可能有buffer插入;但把它们放到一起可能产生拥塞热点,反而恶化其他路径的时序。所以纯逻辑连接特征不够。我重新设计了三组特征向量,每一组都经过OpenSTA的提取:(延伸阅读:仿真分拣99.3%,实测掉到71.5%——我拆解Optimus视觉运动策略后发现的Sim-to-Real鸿沟)
# macro状态特征提取代码片段(简化版)
def extract_macro_features(def_file, netlist, sta):
features = []
macros = parse_def_macros(def_file) # 宏的当前坐标、朝向、尺寸
# 1. 几何特征:密度场和macro重叠惩罚
density_map = compute_density_bins(macros, bin_size=20) # 20um网格
for m in macros:
x, y = m.x, m.y
density = density_map[int(x/bin_size)][int(y/bin_size)]
features.append([m.width, m.height, density, m.orientation])
# 2. 连接特征:基于虚拟布线估计的线长和拥塞
global_route_est = run_fast_global_route(def_file, netlist)
pin_density = global_route_est.pin_density_per_bin
congestion = global_route_est.overflow_per_edge
for m in macros:
# 每个宏所在区域的pin密度和走线拥塞
f.append([pin_density[m], congestion[m]])
# 3. 时序特征:每个宏驱动的路径的WNS/TNS(关键)
sta_report = sta.run_timing(def_file, netlist, corners=['slow'])
timing_graph = sta_report.build_timing_graph()
for m in macros:
# 该宏所有fanin/fanout path的最差slack
worst_slack = timing_graph.worst_slack(m)
total_neg_slack = timing_graph.tns(m)
features.append([worst_slack, total_neg_slack])
return torch.tensor(features, dtype=torch.float32)
我花了三周调bin_size和congestion的归一化。密度图如果太粗,代理感受不到局部热点;如果太细,特征维度爆炸。最终bin_size用了20um,因为目标工艺是130nm,标准单元高度约2.8um,20um的网格能捕获中等粒度的拥塞。时序特征的引入直接改变了代理的行为——它开始学会把时序关键路径上的macro往芯片边缘或者附近靠,因为那里标准单元密度低、线长更可控。但这也埋下了后面奖励函数设计的雷。
为什么我放弃了GNN而选择了手工特征
我确实尝试过用GraphSAGE对macro连接图做池化,作为PPO策略网络的输入。但训练极不稳定,Gradients经常变成NaN。根因是布局过程中宏移动导致连接图的边权重剧烈变化,网络每次看到的结构差异巨大,很难学到稳定的表示。反观手工设计的密度、拥塞、时序特征,虽然粗糙,但物理意义明确,对平移、交换等动作的梯度连续可导。这在强化学习里至关重要:代理需要能预测采取某个动作后这些特征的变化趋势,才能学会价值估计。手工特征让这变成了可能。我最终策略网络的输入就是这60维手工特征(3个macro为例),经过全连接网络展开,没再加图模块。
设计奖励函数时,我被时序暴击送走——多目标优化逼我放弃了面积最优
奖励函数是强化学习的灵魂。最初我参考了Google那篇用RL做芯片布局的原始论文,线性组合线长、面积、拥塞。用OpenROAD在gcd(一个简单的最大公约数电路)上训练,很快就收敛到一个面积更小、线长短30%的布局。我得意忘形,直接把模型泛化到BlackParrot(一个多核RISC-V SoC,有42个macro,65万实例),结果训练了三天,WNS不但没改善,反而变成了-850ps,比随机还差。那一刻我盯着STA报告,感觉自己就是个蠢货。
时序惩罚的权重让我差点删库,直到我引入“自适应课程”
原因分析了两天:BlackParrot有许多高速路径,它们对macro的相对位置极其敏感。代理为了减少线长,把几个大SRAM macro并排放在一起,导致它们之间的数据路径虽然有短线,却因为SRAM本身的输出延迟和setup时间叠加,产生了很大的负slack。也就是说,线长的降低和时序的恶化发生了反耦合。我的奖励函数却一直奖励减少线长,这让代理学会了“作弊”。
我修改了奖励函数,引入时序项:R = α * (-wirelength) + β * (min(WNS, 0)) - γ * congestion_penalty。注意我用了min(WNS,0),这意味着只有当时序违例(WNS<0)时才惩罚,一旦时序clean就不再给额外奖励。但这样训练时,WNS一接近零,代理就没有继续优化面积的动力。所以我加了第四项面积奖励,只在时序clean时生效。权重α、β、γ、δ怎么定?如果β太大,代理会为了避免任何可能性把macro分散到芯片四周,导致线长爆炸;如果β太小,代理会继续追求线长,时序无法修复。我试了固定权重,全崩。
最终解决方法是课程学习驱动的自适应权重:训练早期给线长权重高,让代理快速学会不要把macro堆在一起;训练中期逐渐增大时序权重,逼迫代理修复违例路径;训练后期固定时序权重,让代理微调面积。具体实现是在PPO训练循环里,每100个episode根据当前episode的平均WNS调整β:如果平均WNS仍然很差,就加大β;如果已经clean,则降低β,允许面积和线长优化。这个技巧让BlackParrot的WNS从-850ps最终优化到-120ps,代价是多训了600个episode。
# 奖励计算函数,包含多目标和自适应权重(简化)
def compute_reward(def_path, baseline_wl, alpha, beta, gamma, delta, wns_target=0):
# 跑OpenROAD flow并提取指标
wl, congestion, wns, tns, area = run_openroad_flow_and_parse(def_path)
# 基础奖励:线长改善
r_wl = -(wl - baseline_wl) / baseline_wl # 负线长增量奖励
# 时序惩罚:只有负slack才给
r_timing = min(wnsl, 0) * beta # wns是负值,乘beta后为负奖励
# 拥塞惩罚
r_cong = -congestion * gamma
# 如果时序clean,面积奖励
r_area = 0
if wns >= wns_target:
r_area = -(area / 1e6) * delta # 面积越小奖励越大
return alpha*r_wl + r_timing + r_cong + r_area
在训练脚本里,β的更新逻辑用了一个简单的PID控制器:目标wns_target=0,当前滑动平均wnsl_avg,误差e = wns_target - wns_avg,β的增量Δβ = Kp*e + Ki*integral,并加了限制防止β变负。这个技巧是从控制系统借来的,效果出奇地好。
课程学习:从计数器到RISC-V,PPO到底学会了什么布局直觉
芯片设计网表规模跨度很大,从一个几千门的I2C控制器到百万门的多核SoC。一上来就在复杂网表上训练,代理什么都学不到。我设计了四个阶段的课程学习。
四阶段课程的网络逐步复杂化,代理的布局行为肉眼可见地进化
阶段1:简单组合电路(gcd, aes_cipher),只有几个macro,几乎没有时序违例。目标让代理学会基本的macro spacing规则,不要重叠,并且大概按数据流方向摆放。PPO大约200个episode就收敛到接近最优面积。这一阶段我用了很大的线长权重,时序项几乎为零。(延伸阅读:我用GPT-5.5和Claude 4.8合成了一千张“无害”图片,差点在投资人面前把自己产品搞崩)
阶段2:中等规模处理器核心(riscv32i, ibex),有10-20个macro,开始出现时序路径。代理必须学会把SRAM靠近处理单元,同时避免拥塞。这里我引入了β自适应,并开始给代理提供时序特征。
阶段3:大型SoC(BlackParrot, swerv_wrapper),macro超过40个,多个时钟域,多条关键路径。代理学会了把不同时钟域的macro隔离,并为高速接口预留直连通道。比较有趣的是,在BlackParrot上代理自动演化出一个类似channel的结构——它把一组高带宽互联的macro一字排开,留出清晰的布线过道,这和我见过的人类专家布局很相似。但这是它自己学习出来的。
阶段4:带有功耗意图的布局,我加入了功耗密度图作为输入特征,奖励里加入了功耗惩罚项,用OpenSTA的power analysis快速估算。代理开始把高频翻转的macro分散开,降低局部热点。这在纯手工布局里是很难直觉量化的。
课程学习的关键在于知识迁移:每个阶段的模型作为下一阶段的预训练权重。我没有从零开始训练最终模型,而是用阶段3导出的checkpoint在阶段4微调。这大大节省了训练时间,也防止了灾难性遗忘。
训练出来的PPA硬碰商业工具默认流:我赢了线长,差点输了时序
所有实验在OpenROAD v2.0-17022(2024年11月发布)上跑,目标工艺sky130hs(HVT/SVT)。对比基线是OpenROAD-flow-scripts的默认自动驾驶流程(floorplan→place→CTS→route→finish)。我的PPO代理只替代floorplan步骤,后续全部用同样的自动化脚本。测试设计选择:riscv32i(33k cells, 4 macros),BlackParrot(65k cells, 42 macros),swerv_wrapper(110k cells, 8 macros, 多时钟域)。
PPA指标对比表(训练模型 vs 默认流程,取3次运行平均值)
| 设计 | 指标 | OpenROAD默认 | PPO floorplan + OpenROAD | 改善 |
|---|---|---|---|---|
| riscv32i | 线长 (um) | 1.92M | 1.63M | -15.1% |
| 最差负slack (WNS) | -0.08ns | -0.05ns | +37.5% | |
| 面积 (um²) | 0.144mm² | 0.139mm² | -3.5% | |
| BlackParrot | 线长 | 8.47M | 7.21M | -14.9% |
| WNS | -0.32ns | -0.12ns | +62.5% | |
| 面积 | 0.98mm² | 0.95mm² | -3.1% | |
| swerv_wrapper | 线长 | 4.12M | 3.89M | -5.6% |
| WNS | -0.15ns | -0.09ns | +40% | |
| 面积 | 0.27mm² | 0.263mm² | -2.6% |
BlackParrot的WNS提升最为显著,这归功于代理学会把几个SRAM从芯片中心移到靠近标准单元区域,让时序路径减短。但swerv_wrapper的线长改善只有5.6%,原因是该设计中macro数量少、且已经被工具放置得较好,RL的优化空间有限。这反过来验证了一个假设:RL的优势在于高度非结构化的macro密集设计,对于已经接近最优的布局,RL的边际收益不大。(延伸阅读:AI+制造业第三个项目:我给生产线上 15 个 Agent 建了共享记忆,结果它们差点把批次号全读脏了)
训练与推理的计算成本
PPO训练使用一台带4×A5000 GPU的工作站,并行8个OpenROAD环境。BlackParrot训练了3500个episode,耗时大约58小时。推理时,对一个新网表生成floorplan只需要一次前向传播,约0.3秒,之后还需要OpenROAD做详细placement和布线,总流程时间比默认多约20分钟(由于多次评估迭代)。但相比人工迭代floorplan动辄数天,这已经是巨大的加速。
可迁移性的噩梦:我把模型丢给TSMC 65nm后,它吐出了一个不可布线的怪物
在sky130上成功后,我理所当然地认为换个工艺库只是换个输入。结果用TSMC 65nm(通过NDA拿到评估PDK)跑同一个模型,生成的macro布局导致大量DRC violation(最小间距错误),而且时钟树综合直接失败。问题根源是设计规则不同:sky130里面金属宽度和间距宽容许多,代理学会了把macro放得很密以节省面积,但65nm有更严格的well spacing和更多design rule check。这些物理约束没有编码到状态或奖励里,所以代理根本不认识。
迁移失败教会我的三件事
第一,奖励函数必须包含DRC意识。我在后续版本里增加了快速DRC检查作为奖励惩罚项:每次移动后,跑一次Klayout或Magic的轻量级DRC,返回违规数量,作为惩罚。这显著提高了迁移性,但也让训练速度下降30%,因为DRC检查比虚拟布线慢。
第二,特征归一化需要根据工艺库的grid变化。sky130的制造grid是5nm,而65nm可能是1nm,密度图的bin_size必须相应变化。我抽象出一个tech_scaler模块,根据输入工艺自动调整网格和动作步长。
第三,跨工艺库迁移目前无法做到零样本,仍然需要至少几百个episode的微调。但相比于从头训练,微调只需3-4小时,完全可以接受。这也说明了方案B的可插拔架构优势:RL引擎可以针对每个新工艺快速适配,而不需要改整个工具流程。
避坑清单:如果你也想用RL劫持布局流程,先看这十条再动手
1. 绝对不要直接用坐标作为动作,离散化候选槽+合法化引擎是效率的前提。否则状态动作空间爆炸,模型永远学不会。
2. 并行执行OpenROAD时记得隔离PDK目录,否则共享LEF/TLEF会损坏。写个脚本把标准库拷贝到临时目录是必要的。(延伸阅读:我在AI芯片公司帮硬件工程师用Code Llama写RTL,半年后我们放弃了“替代”幻想)
3. 时序特征要从STA报告里提取真正的WNS/TNS,不要自以为是地用线长倒数代替。我犯过这个错,导致时序违例被完全忽视。
4. 奖励函数中的时序权重必须自适应,固定权重会导致要么线长爆炸,要么时序永远红。可以用简单的PID控制β参数。
5. 特征工程中放弃GNN并不可耻,手工特征的可解释性和物理意义对收敛至关重要。深度学习不是银弹。
6. 不要相信学术论文里的PPO超参数,芯片布局的奖励尺度远大于游戏,你需要重新搜索学习率、clip range和GAE lambda。我用的是lr=3e-5, clip=0.15, lambda=0.92。
7. 课程学习从简单到复杂是必修课,直接上SoC训练一周都降不了loss。而且记得用预训练权重热启动,否则前几个stage白费了。
8. 可迁移性需要DRC感知,至少加入最小间距和金属宽度规则作为奖励惩罚,否则迁移到新工艺就是灾难。
9. 商业工具集成时注意license池,PPO推理过程会频繁调用工具,可能耗光浮动license。我用了个简单的排队机制限制同时调用数。
10. 最终交付给后端工程师的DEF必须人工审查,RL可能产生非直观但合法的布局,工程师们不信任黑盒,所以提供diff脚本对比默认布局是必要的。
这180天让我重新理解了物理设计,也重新理解了强化学习的边界。它不是一个替代人类专家的万能钥匙,而是一个在既定约束下探索解空间的高效探测器。只要你能把问题装进正确的MDP壳子,并容忍它时不时的抽风,RL就能在芯片布局这种老牌领域里撕开一道口子。但如果你指望它自己学会全流程,那你大概会比我先崩溃。