乐于分享
好东西不私藏

AI for IC设计实践 :从功能正确到微架构对齐

AI for IC设计实践 :从功能正确到微架构对齐

本文在AI的辅助下为ODL(open_digital_lib)仓库增加一个基本模块,通过开源实践观察AI在工程设计中的微观表现。

大模型在生成verilog代码中使用“功能等价”来达到“功能正确”的目标,在某种程度上破坏了微架构设计,需要对生成的代码进行进一步的检视与修正,以对齐原始设计意图。

1. 设计目标:位串行乘法器

输入:图片

预期:输出参数化的设计代码并完成验证。

模型:codex 5.5 - high


2. 功能正确:大模型的基本能力

这次要生成的“位串行乘法器”是教科书级别的经典电路,不存在特殊定制的规格。

在大模型能力迅速迭代的当下,正确识别设计图并生成代码应该是基本能力。

不到12分钟就完成了代码的生成跟验证过程:

原图只有数据通路,模块接口和控制通路都是由大模型推断出来的,直接补齐了几个关键信息:

  • 增加控制输入输出时序的接口:start_i/busy_o/done_o

  • 内部增加了控制位串行计算过程的计数器

  • 增加参数N用于配置乘法器位宽

如果只是Vibe coding,那么看到验证通过也就可以结束了。

但是大部分芯片设计工程师不会把未经检视的RTL代码直接拿去流片。

而这,才是一切的起点。


3. 微架构对齐

为了将生成的代码严格对齐到设计图的结构,以得到一份高质量、高可读性的代码,还做了额外的修正,主要包括:

  • 将行为级加法修正为bit-slice结构

  • 优化FA输入端口的冗余MUX

  • 优化product_o冗余的输出寄存器

  • 将输出端口的done_o修正为valid_o

  • 优化冗余的iter_idx信号

  • 优化冗余的iter_valid信号

  • 优化partial product

  • 优化sum_1d_shift

下面是具体的过程。


  • 将行为级加法修正为bit-slice微架构

打开生成的代码,看到很多控制信号,粗略一看都很正常。但是没有看到由全加器(FA)组成的结构,按照正常的设计思维,这应该是在代码里面一眼就能看到的核心逻辑:

仔细检查,才发现代码藏在一个角落:

assign add_result  = {1'b0, acc_for_add} + ( x_bit ? {1'b0, y_for_add} : '0 );

这里显然是把多个全加器组成的“半脉动阵列”结构直接优化成一个大位宽的加法器,丢失掉最关键的“加法器”和位输出打拍的细节,没有正确对齐设计图,最终会导致频率严重下降。

修正后,RTL数据通路与设计图微架构对齐,而不是仅保持功能等价。

  • 优化FA输入端口的冗余MUX

全加器的输入端被额外增加了两个MUX:

assign y_for_add   = start_accept ? y_i : y;assign acc_for_add = start_accept ? '0  : acc;

意图是:在start信号到来,开启一次乘法计算的时候,给全加器强制赋予一个初始值0。

但是在这个电路中,随着数据的流动,积的最后一比特结果被输出的时候,寄存器的数据都已经被清空,不需要额外的清零:

  • 优化product_o冗余的输出寄存器

FA的输出多寄存一拍后才生成product_o。

可以由sum_1d[0]直接驱动,更符合设计图结构。

  • 将输出端口的done_o修正为valid_o

输出端口使用done_o在最后一拍拉高来表示计算完成,这种方式更加适合结果并行输出的场景。

而 bit-serial multiplier 的输出是连续 2N 拍逐比特输出,因此更合理的方式是用持续拉高的valid_o来指示当前输出数据有效。

  • 优化冗余的iter_idx信号

使用了额外的 iter_idx信号:

assign iter_idx = start_accept ? '0 : cnt;

实际上cnt会在last_cycle后清零,新一轮 start_accept到来前,cnt已经清零,因此iter_idx是冗余的。

  • 优化冗余的iter_valid信号

使用iter_valid表示当前周期是否执行一次迭代:

assign iter_valid = start_accept || busy;

后续代码中iter_valid的使用范围逐渐收缩到只剩一次使用,为了减少中间信号,提高可读性,直接展开:

else if ( start_accept || busy ) begin    sum_1d   <= sum;    carry_1d <= carry;    cnt      <= cnt + CNT_WIDTH'(1);end
  • 优化partial product

中间版本曾使用条件选择生成 partial product:

assign partial_product = multiplier ? multiplicand_i : '0;

虽然功能正确,但不能清晰表达出设计图中的结构。

修正后,将 multiplier 的补零行为和 partial product 的与门结构分开表达:

assign multiplier      = ( cnt < CNT_WIDTH'(N) ) ? multiplier_i : 1'b0;assign partial_product = {N{multiplier}} & multiplicand_i;

其中multiplier表示在输入阶段使用外部串行输入,在flush阶段补 0;而partial_product则明确对应与门阵列。

  • 优化sum_1d_shift

为兼容N=1,使用 generate 引入了不必要的参数分支,降低代码可读性

generate    if ( N == 1 ) begin : gen_sum_1d_shift_n1        assign sum_1d_shift = '0;    end    else begin : gen_sum_1d_shift        assign sum_1d_shift = {1'b0, sum_1d[N-1:1]};    endendgenerate

修正后使用统一的右移表达式:

assign sum_1d_shift = sum_1d >> 1;

附:经过多轮修正后的代码

调整前的代码: