循环 - 2023.2 简体中文

AI 引擎内核与计算图编程指南 (UG1079)

Document ID
UG1079
Release Date
2023-12-04
Version
2023.2 简体中文

AI 引擎具有零开销循环结构,不会发生任何用于比较和分支的分支控制开销,因此可以减少内层循环周期计数。流水打拍允许编译器添加前导码和后导码,使指令流水线在循环执行期间始终处于已满状态。对于流水打拍循环,在前一次迭代结束前即可启动新的迭代,从而实现更高的指令级并行度。

下图显示了零开销循环的汇编代码。请注意,在不同时隙内按顺序显示了两次矢量加载、一次矢量存储、一次标量指令、两次数据移动和一次矢量指令。

图 1. 零开销循环的汇编代码

将以下编译指示搭配使用即可指令编译器对循环进行流水打拍,并告知编译器,循环将始终执行至少三次。

for (int i=0; i<N; i+=2)
   chess_prepare_for_pipelining
   chess_loop_range(3,)

chess_loop_range(<minimum>, <maximum>) 会告知编译器,对应循环至少执行 <minimum> 次,最多执行 <maximum> 次,其中 <minimum><maximum> 均为非负常量表达式,或者可省略。省略时,<minimum> 默认为 0,<maximum> 默认为编译器中预设的最大值。虽然 <maximum> 与流水线实现不相关,但 <minimum> 可用于指引流水线的实现。

<minimum> 数值可定义每次执行循环时,执行循环迭代的最少次数。随后,通过微调软件流水线即可支持不少于该数量的迭代并行执行(如果可能)。它还可确定在完成执行 <minimum> 次迭代前,无需检查循环边界。

如果循环范围是编译时常量,则无需循环范围编译指示。总之,AI 引擎编译器会报告最适合算法的最优流水打拍的理论最佳数量。如果范围规范并非最优规范,那么编译器会发出警告并提供最优范围建议。为此,最初可将 <minimum> 设为 1,即 [chess_loop_range(1,)],并观察编译器报告的最适合的 <minimum>

Warning in "matmul_vec16.cc", line 10: (loop #39)
further loop software pipelining (to 4 cycles) is feasible with `chess_prepare_for_pipelining'
but requires a minimum loop count of 3
... consider annotating the loop with `chess_loop_range(3,)' if applicable,
... or remove the current `chess_loop_range(1,)` pragma

此时,您可选择将 <minimum> 数值更新为报告的最优值。

如果实际迭代次数未达到 <minimum> 次,那么这第二部分的流水线实现可能造成 AI 引擎内核中出现死锁。因此,您必须确保迭代次数始终至少为 chess_loop_range 指令中所指定的值。

循环进位依赖关系会影响代码矢量化。如果无法移除内层循环依赖关系,则会采用相应策略来单步跳出某个级别并手动展开,使内层循环的多个副本能高效并行运行。

循环处理数据时,要按特定偏移递增或递减,请对循环缓冲器使用 cyclic_add 内部函数。fft_data_incr 内部函数会启用指针迭代,该指针迭代即为蝶形操作的当前目标。相比于在标准 C 语言下对等效功能进行编码,使用这些函数可以节省多个时钟周期。根据数据类型,您可能需要对参数和返回类型进行强制转换。

以下示例使用 fft_data_incr 内部函数在实数矩阵上进行运算。

pC = (v8float*)fft_data_incr( (v4cfloat*)pC, colB_tiled, pTarget);

请尝试避免顺序加载操作在使用矢量寄存器之前已将其完全填满。最好利用 MAC 内部函数对加载进行交织,在此类函数中当前 MAC 与下一次加载可在同一个周期内完成。

acc = mul4_sym(lbuff, 4, 0x3210, 1, rbuff, 11, coeff, 0, 0x0000, 1);
lbuff = upd_w(lbuff, 0, *left);
acc = mac4_sym(acc, lbuff, 8, 0x3210, 1, rbuff, 7, coeff, 4, 0x0000, 1);

在某些用例中,使用循环轮转来轮转循环内部指令可能会很有效。请考虑尝试在循环之前为首次迭代加载数据块,然后在接近循环末尾时为下一次迭代加载数据块,而不是将数据加载到循环开始处的矢量中。这样将添加额外指令,但是能缩短循环的依赖关系长度,从而有助于以可能更低的循环范围来实现理想的循环。

// Load starting data for first iteration
sbuff = upd_w(sbuff, 0, window_readincr_v8(cb_input)); // 0..7
 
for ( int l=0; l<LSIZE; ++l )
chess_loop_range(5,)
chess_prepare_for_pipelining
{
   sbuff = upd_w(sbuff, 1, window_readincr_v8(cb_input)); // 8..15
   acc0 = mul4_sym(     sbuff,5 ,0x3210,1 ,12 ,coe,4,0x0000,1);

   sbuff = upd_w(sbuff, 2, window_readdecr_v8(cb_input)); // 16..23
   acc0 = mac4_sym(acc0,sbuff,1 ,0x3210,1 ,16,coe,0,0x0000,1);
   acc1 = mul4_sym(     sbuff,5 ,0x3210,1 ,20,coe,0,0x0000,1);
   window_writeincr(cb_output, srs(acc0, shift));
   // Load data for next iteration
   sbuff = upd_w(sbuff, 0, window_readincr_v8(cb_input)); // 0..7
   acc1 = mac4_sym(acc1,sbuff,9,0x3210,1,16,coe,4,0x0000,1);
   window_writeincr(cb_output, srs(acc1, shift));
}