跳转至

程序序(Program Order, PO)

1. 什么是程序序

灵犀处理器按收到的指令与外部事件的先后顺序推进内部状态:你给它什么指令、以什么顺序给,它就按这个顺序“理解并生效”。 为了把这种“应该按什么顺序理解执行”的语义说清楚,我们定义了程序序(Program Order, PO)

  • PO = 从代码角度看,硬件对外表现应当等价的执行顺序
  • 硬件内部可能乱序或并行,但对外部可见的效果(寄存器、内存、Tile 的最终值)必须与 PO 一致。

2. 从“块头顺序”展开成“程序序”

灵犀的块指令由两部分组成:

  • 块头:配置“这块要干什么”(数据类型、维度、输入/输出 Tile 等)。
  • 块身:真正执行的步骤(搬运/乘累加/转换/回写等)。

编译时,块头会被排成一个线性顺序(可以理解为“调用点”的顺序)。之后,把每个块头展开成它的块身步骤,得到最终的程序序 PO。

两种展开方式

  1. 随机替换(不强制内部顺序) 块身里的几个动作可以并行或调度顺序不固定,只要整块在其他块之前/之后的大顺序不变即可。

  2. 顺序替换(内部顺序固定) 块身里的动作有明确先后,必须逐步执行——例如“先搬数据,再乘累加,最后回写”。

多数矩阵/向量计算块的块身都是顺序替换,因此最终 PO 通常是一个清晰的全序。


3. 用三个真实代码片段看 PO

下面给出三个常见块的“块头 → 展开 → PO”对照,完全不需要抽象记号。

例 1:纯 GEMM(16×16×16)并把结果写回 Tile

块头(出现顺序,就是 L1-IPO):

BSTART.PAR TMATMUL, FP16       ; 配置:做 FP16 的 A×B
B.DIM     rM, 128, ->M        ; M=128
B.DIM     rN, 128, ->N        ; N=128
B.DIM     rK, 256, ->K        ; K=256
B.IOT     [TA, TB], group=0, ->ACC<64KB>   ; 绑定 A/B 到 ACC
B.ARG     CD2RD                ; 结果默认行主序,无额外变换

展开后的块身步骤(顺序替换):

  1. 读取/预取 A、B 的分片到内部缓冲;
  2. CUBE Core 以 16×16×16 的粒度做乘累加,结果累加到 ACC;
  3. 把 ACC 写回目标 Tile(行主序)。

最终 PO(语义顺序):

BSTART.PAR → B.DIM → B.DIM → B.DIM → B.IOT → B.ARG → 
加载分片 → 乘累加到 ACC → ACC 写回 Tile

例 2:GEMM + TCVT 随路转换(ACC→Tile 之前做量化/重排)

块头:

BSTART.PAR TMATMUL, FP16
B.DIM     zero, 64,  ->M
B.DIM     zero, 64,  ->N
B.DIM     zero, 256, ->K
B.IOT     [TA, TB], group=0, ->ACC<64KB>
B.ARG     ZZ2RD                  ; 指定 TCVT 的布局变换(例)

展开后的块身(顺序替换):

  1. 读取/预取;
  2. CUBE 乘累加到 ACC;
  3. TCVT FixPipe:在“ACC→Tile 写回”的道路上,边搬边做格式转换(如反量化、激活、布局变换等);
  4. 将转换后的数据写入 Tile Register。

最终 PO:

块头顺序 → 读取/预取 → 乘累加到 ACC → TCVT 边搬边转 → 写回 Tile

这里的关键是:TCVT 在“ACC→Tile 的路径上”执行,省掉了“先落地再另起一段转换”的往返。


例 3:TMATMULMX(微缩放)+ 可选 C Tile 累加

块头:

BSTART.PAR TMATMULMX, INT8
B.DIM     zero, 128, ->M
B.DIM     zero, 128, ->N
B.DIM     zero, 256, ->K
B.IOT     [TA, TSA], group=0, ->ACC<64KB>  ; A 与 ScaleA
B.IOT     [TB, TSB], group=1               ; B 与 ScaleB
; 可选:B.IOT [TC], group=2               ; 若做 +C
B.ARG     CD2RD

块身(顺序替换):

  1. 装入 A、ScaleA 以及 B、ScaleB;
  2. 在乘法前,对 A、B 按 Tile 广播缩放A' = A * ScaleAB' = B * ScaleB
  3. 对缩放后的 A'、B' 做乘累加到 ACC;
  4. 如果存在 C/ACC 累加,执行相应加法;
  5. 将 ACC 写回 Tile,或继续由 TCVT 转换后写回。

最终 PO:

块头顺序 → 读 A/B 与缩放 Tile → A/B 微缩放 → 乘累加到 ACC → (+C/ACC) → 写回

4. PO 与“真实执行”的关系(再强调)

  • PO 是“应该表现成的顺序”:编译器、验证和上层框架都以此为准去理解程序语义。
  • 硬件内部可乱序/并行,但对外可见效果必须与 PO 等价
  • 后续诸如寄存器距离计算、内存一致性/顺序、屏障等约束,均以 PO 为基础。

5. 微观到宏观:如何落地到你的代码

  • 块头理解为“在时间线上占一个位置”的调用点
  • 顺序替换:把这个调用点用“具体步骤”替换掉;
  • 随机替换:你只强调“这是一组动作”,但这组内部顺序不重要(或由调度器决定);
  • 所有块按块头出现顺序排好,再逐个展开,你看到的就是整个程序的程序序

6. 总结

程序序(PO)= 代码层面,块头按出现顺序 → 用块身动作(顺序/随机)展开后的整体顺序。 硬件再怎么优化,外部观察到的行为都必须与这条“顺序线”一致。

这样写完,你在读任何一段灵犀代码时,都能清楚回答三件事:

  1. 先后关系:谁在前、谁在后;
  2. 展开内容:一个块头到底代表哪些具体动作;
  3. 可见语义:不管底层怎么并行,外部看到的结果与这条顺序一致。