MxuOpHoldIssues 停顿递推
地址适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so。其他版本会有所不同。
摘要
当两个 MXU 操作在同一个脉动阵列上背靠背发射时,第二个操作必须等到它需要的子单元空闲后才能开始,这是典型的 reservation-station 结构冒险。libtpu 用一个三函数模型为此定价,其顶层是 MxuLatencyTable::GetLatencyBetween(A, B):它计算 A 发射后 B 必须等待的周期数,方式是在 B 持有的 MXU 子单元上取 最大值,即 A 仍然占用每个子单元的时长。这是 MXU 内部版的 SchedModel WriteRes/ReadAdvance 预留,但粒度是具名 MXU 资源(kMatmulAccumulationPipe、kMsrAOverrunCheck0..3、kFusedLoadReserved、kLmrReserved),而不是抽象 proc-resource。
该递推是 max 归约,不是串行求和。两个触及不相交子单元的 MXU op 可以自由流水(停顿 0);两个存在争用的 op 在争用最严重的端口处共享。模型由三部分构成:MxuOpResourceReservations(A) 返回 A 的逐资源持有周期向量(array<int,N>,N=19 VF / 11 GL);MxuOpHoldIssues(B) 返回 B 持有的子单元集合(一个 flat_hash_set<MxuResource>,即“发射足迹”);GetLatencyBetween 迭代 B 的持有集合,读取 A 对每个资源的预留,并保留最大值。本页重构这三者,包括两个世代的逐字节精确归约循环和边界检查。
有两个门控决定何时查询这个矩阵。逐边结构门控位于 LatencyTableViperfish/Ghostlite::LatencyBetweenInternal:MXU 预留只精确用于 非真实数据依赖的 MXU-MXU op 对;真实 RAW 依赖使用普通 op 延迟;非 MXU 对使用 XLU 冲突惩罚表。第二个是编译环境标志 MxuLatencyBalancingUseSequenceDependencies,它在延迟平衡路径和更简单排序路径之间切换 MXU 序列分配器(AssignMxusForSequenceGroup)。两者均在此记录。
对于重新实现,契约是:
- 三函数拆分:
MxuOpResourceReservations(持有周期向量)、MxuOpHoldIssues(持有资源集合)、GetLatencyBetween(对共享资源取最大)。 - 闭式停顿递推:同 MXU guard、vlxmr→matmul 可用性 seed(以及 matmul→matres cost-map 旁路)、max 归约循环、逐世代资源数量边界(≤18 VF / ≤10 GL)。
- 决定矩阵是否触发的逐边结构门控(
IsTrueDependencyBetween+LloOpcodeUsesMxu)。 MxuLatencyBalancing环境标志门控及其选择的分配器路径。
| 递推 | MxuLatencyTable::GetLatencyBetween(A,B) — VF @0x1c8ae320, GL @0x1c8b71e0 |
| A 的持有周期向量 | MxuOpResourceReservations(A) — VF @0x1c8ad080, GL @0x1c8b6300 (array<int,N>) |
| B 的发射足迹 | MxuOpHoldIssues(B) — VF @0x1c8ad3a0, GL @0x1c8b6760 (flat_hash_set<MxuResource>) |
| 资源边界 | resource < kNumMxuResources → VF ≤ 18(19 个资源),GL ≤ 10(11 个资源);否则 LogMessageFatal |
| 归约 | 对 k ∈ B.held_set 执行 stall = max(seed, A.reservation[k])(cmovg) |
| 逐边门控 | LatencyTableViperfish::LatencyBetweenInternal @0x1c8a4ac0(GL @0x1c8b22e0) |
| 真实依赖测试 | IsTrueDependencyBetween @0x1c89fdc0 — FindOperandIndexFull ≥ 0 或两者 opcode 均为 0x14e |
| MXU-op 测试 | LloOpcodeUsesMxu @0x10a433e0 — op ∈ [0x8d,0xa5] ∪ [0xa8,0xab] ∪ [0x152,0x153] |
| 环境标志门控 | MxuLatencyBalancingUseSequenceDependencies @0x1d6b9c80 — env +0xbe8, (~v & 0x101)==0 |
Issue→Stall 递推
目的
GetLatencyBetween(A, B) 返回调度器必须在两个发射到同一 MXU 的 MXU 操作之间插入的结构停顿。A 是较早(producer)op,B 是较晚 op。结果是 B 在复用 A 仍然持有的共享 MXU 子单元前需要等待的周期数。这不是 A 的结果延迟(那是 GetLatency,用于真实依赖边);它是共享脉动阵列端口产生的结构冒险。
入口点
LatencyTableViperfish::LatencyBetweenInternal (@0x1c8a4ac0) ── scheduler edge-latency
└─ [MXU-MXU non-true-dep edge]
MxuLatencyTable::GetLatencyBetween(A, B) (@0x1c8ae320) ── the recurrence
├─ MxuOpResourceReservations(A) (@0x1c8ad080) ── A's array<int,N> hold vector
└─ MxuOpHoldIssues(B) (@0x1c8ad3a0) ── B's held-resource set
```text
### 算法
```c
function GetLatencyBetween(A, B): // VF @0x1c8ae320 / GL @0x1c8b71e0
// same-MXU guard: unit_id from LloInstruction+0xb (bits 8-9 = mxu id, bit 0x400 = has-mxu)
idA = (A[+0xb]&0x400) ? ((A[+0xb]>>8)&3 | 0x100000000) : 0
idB = (B[+0xb]&0x400) ? ((B[+0xb]>>8)&3 | 0x100000000) : 0
if !(idA.has_mxu && idB.has_mxu):
if idA.has_mxu != idB.has_mxu: return 0 // one has no MXU → no structural conflict
else if idA != idB: // distinct MXU instances → no conflict
return 0
seed = 0
// availability seed: a matrix-load A (vlxmr/MSR, opcode 0xa9) feeding a matmul B
if A.opcode == 0xa9 && (B.opcode - 0x9b) < 0xb: seed = 1 // A=0xa9 (vlxmr), B in [0x9b,0xa5]
else if A.opcode in [0x9b,0xa5] && B.opcode == 0x152: // matmul A, matres B
return MatmulDataFormat-keyed cost-map[A] (this+0x80) // direct map lookup, not a seed
// (every other pair: seed = 0, fall through to the max-reduction below)
resA = MxuOpResourceReservations(A) // A's array<int,N> hold-cycle vector
heldB = MxuOpHoldIssues(B) // set of MxuResources B holds
stall = seed
for k in heldB: // iterate B's held-resource set
CHECK(k < N) // VF: k<=18 ; GL: k<=10 ; else LogMessageFatal
if stall < resA[k]: // cmovg
stall = resA[k] // MAX over shared resource
return stall已验证的 VF 归约循环(@0x1c8ae4c9):movzx edi,[rdx](下一个持有资源 k)、cmp rdi,0x12 ; ja FATAL(k ≤ 18)、mov eax,[rbp+rdi*4-0xa0](A 的预留数组在 rbp-0xa0)、cmp r12d,eax ; cmovg eax,r12d(stall = max(stall, resA[k]))。GL 循环(@0x1c8b73a0)相同,但边界为 cmp rdi,0xa(k ≤ 10),数组在 rbp-0x6c。CHECK 字符串是 resource < to_underlying(MxuResource::kNumMxuResources)(VF mxu_latency_table_vf.cc:536,GL mxu_latency_table_gl.cc:431)。
QUIRK — 归约是 MAX,不是 SUM。朴素实现若把争用持有时长相加,会高估停顿:独立 MXU 子单元可以流水。模型只在单个争用最严重的子单元上串行化。这与更高层
ResourceVector::MaxResourceCycles的普通 max 组对应,但粒度是 MXU 内部子资源。
MxuOpResourceReservations — A 的持有周期向量
MxuOpResourceReservations(A) @0x1c8ad080 对每个 MxuResource 返回 A 占用它多少周期(VF 为 array<int,19> / GL 为 array<int,11>)。它是逐 opcode dispatch:构造 modifier key,并从逐类 flat-hash map 中复制匹配的 array<int,N>:
function MxuOpResourceReservations(A): // VF @0x1c8ad080
switch on A.opcode:
[0x8d,0x96] matpush → MatpushModifier key → find in map(this+0x00) → copy array
[0x9b,0xa5] matmul → MatmulModifier key → find in map(this+0x20) → copy array
0xa9 (vlxmr/MSR) → key from .rodata → find in map(this+0x40)
0xaa (vlxmr) → key from .rodata → find in map(this+0x40)
0x152 (matres) → chunk-index key → find in map(this+0x60)
// map miss → ThrowStdOutOfRange / LogMessageFatal (a valid MXU op always hits)
```text
`MatmulModifier` key 由指令字段逐字节精确构造:`{byte0 = matmul_data_format(), byte1 = (opcode==0xa5), byte2 = (done_with_gains_mode()!=2), byte3 = 0, word4 = matrix_staging_register()|0x100}`。`MatpushModifier` key 是 `{byte0 = GainLatchModeToMatmulDataFormat(latch_mode()), byte1 = LatchModeIsTranspose(latch_mode()), byte2 = 0, byte3 = LatchInstructionToMsr(instr)}`。这些 map 持有的逐格式预留值见 [MXU Latency: GF](mxu-latency-gf.md)。
### MxuOpHoldIssues — B 的发射足迹
`MxuOpHoldIssues(B) @0x1c8ad3a0` 返回 `B` 在发射时锁定的 MXU 子单元*集合*(一个 `flat_hash_set<MxuResource>`)。同样是逐 opcode dispatch,但 matpush 有一个额外细节:它按 `latch_index_in_sequence()`(0..3)分支,以选择逐序列步骤的持有集合。三个 opcode 范围及各自构造的持有集合:
```c
function MxuOpHoldIssues(B): // VF @0x1c8ad3a0
if (B.opcode - 0x9b) <= 0xA: // matmul [0x9b,0xa5]
seed_set = { from .rodata @0xb43b344 } // kFusedLoadReserved-class seed
if done_with_gains_mode(B) != 2:
insert MsrReservedForLgmr(matrix_staging_register(B)) // kLmrReserved (14)
if B.opcode == 0xa5 || LloInstructionIsIntegerMatmul(B):
insert kMatmulAccumulationPipe (16)
else:
insert kLmrReserved (14)
return set
if (B.opcode - 0x8d) > 9: // vlxmr / matres (not matpush, not matmul)
switch B.opcode:
case 0xa9: seed = .rodata @0xb43b347, 2 // vlxmr
case 0xaa: seed = .rodata @0xb43b345, 2
case 0x152: seed = .rodata @0xb43b349, 1 // matres
default: LogMessageFatal("Unsupported MXU op") // :508
return flat_hash_set(seed ...)
// else: matpush [0x8d,0x96]
seed_set = { from .rodata @0xb43b200, 2 } // SOO scratch seed
if !MxuSeqPredicate(latch_mode(B)): // vtable +0x358 gate
return seed_set
switch latch_index_in_sequence(B): // 0..3
case 0: insert (4*(msr!=0)) | 2 // kMsr{A,B}OverrunCheck0 (:447)
case 1: insert (4*(msr!=0)) | 3 // ...Check1 (:454)
case 2: insert (4*(msr!=0)) + 4 // ...Check2 (:461)
case 3: insert (4*(msr!=0)) + 5 // ...Check3 (:468)
return seed_setGOTCHA — 注意哪个 opcode band 拥有哪个 seed。Matmul
[0x9b,0xa5]((v6-155)<=0xA)构造@0xb43b344seed,加上done_with_gains/kMatmulAccumulationPipe/kLmrReserved逻辑;matpush[0x8d,0x96]是 fall-throughelse,它 seed@0xb43b200并运行latch_index_in_sequenceOverrunCheckswitch。两者都从连续 opcode 范围构造 held-set,因此很容易互换错。
held-set 元素是具名 MxuResource 枚举值(kMsrAOverrunCheck0..3 / kMsrBOverrunCheck0..3,transpose bit 通过 (4*(msr!=0)) 选择 A 与 B bank;kFusedLoadReserved、kLmrReserved、kMatmulAccumulationPipe),这由构造函数中的 CHECK 字符串确认(holds.insert(MxuResource::kMatmulAccumulationPipe).second :491、holds.insert(MxuResource::kLmrReserved).second :488、kMsrAOverrunCheck0..3 :447/454/461/468、kFusedLoadReserved :483、mxu_latency_table_vf.cc)。
GOTCHA — matpush held set 依赖序列步骤。同一个 matpush opcode 在序列步骤 0 持有
kMsr*OverrunCheck0,但在步骤 3 持有...Check3;transpose 标志通过+4翻转资源序号(A-bank vs B-bank,即(4*(msr!=0))项)。把 matpush 当作持有固定资源集合来实现,会错误定价 latch 序列内部步骤的背靠背停顿。
Worked Recurrence
同一 MXU 上两个独立的 VF bf16 matmul A、B,无真实数据依赖(例如两个 K-tile 累加到不同 MRB chunk;B 不消费 A 的输出):
ISSUE (footprints):
A: matmul fmt1 → MxuOpResourceReservations(A) = {res1:15, res15:8, res17:7, res16:14}
(MatmulIssue 15cy, AccA 8, AccC 7, AccB 14)
B: matmul fmt1 → MxuOpHoldIssues(B) held set = {res1, res15, res16, res17}
STALL (recurrence): GetLatencyBetween(A, B):
same MXU [yes]
seed = 0 (matmul-matmul pair: A is matmul-class, not 0xa9, so no availability seed)
for k in {res1, res15, res16, res17}: stall = max(stall, resA[k])
= max(15, 8, 14, 7) = 15
⇒ B waits 15 cycles after A issues before re-using the MatmulIssue slot.
If B is instead a bf16 matpush (fmt1, narrow):
resA(A=matpush) = {res0:2, MSR-A:1, MSR-B:1}
heldB(B=matpush) ⊇ {res0, MSR-A, MSR-B}
stall = max(2, 1, 1) = 2 ⇒ back-to-back bf16 matpushes pipeline at 2-cycle issue.
For int8/x8 (GLM_*_S8 → fmt6):
matpush array {res0:8, MSR-A:7, MSR-B:6} → back-to-back stall 8
matmul grp4 {res15:32, res17:31, res16:38} → stall 32 (4× bf16 = the x8 4-plane sequence)
```text
retire 半边是独立的:下游 op 若*真实*消费 matmul 结果,会等待完整的基础 TOTAL 延迟(bf16 ≈ 212、fp8 ≈ 204 周期,来自逐世代 Performance array),它通过真实依赖边路由,而不是结构停顿。因此发射率由预留停顿(2/8/15/32)门控,而结果可用性由 `GetLatency` 门控。
---
## 逐边结构门控
### 目的
`LatencyBetweenInternal` 是调度器的边延迟函数(vtable slot `+0x18`)。它为有序对 `(A, B)` 决定哪个成本模型定价该边:真实数据依赖 → 普通 op 延迟;MXU-MXU 结构冒险 → 预留矩阵;跨 lane unit 冲突 → XLU 冲突惩罚表。MXU 预留只在 MXU-MXU 非真实依赖场景中查询,这正是预留矩阵所建模的内容。
### 算法
```c
function LatencyBetweenInternal(A, B): // VF @0x1c8a4ac0 / GL @0x1c8b22e0
if (A.opcode - 233) < 4: return 0 // 0xe9..0xec pseudo class
// (pop-and-then-push-same-FIFO consistency CHECK elided)
if IsPseudo(A) || IsPseudo(B): return LatencyBetweenPseudo(A, B)
base = GetLatency(A) // A's intrinsic op latency
if vtable[+0x20](A, B): // IsTrueDependencyBetween @0x1c89fdc0
return base // B genuinely consumes A's result
if LloOpcodeUsesMxu(A.opcode) && LloOpcodeUsesMxu(B.opcode):
return MxuLatencyTable::GetLatencyBetween(this.mxu_table /*[this+0x1d8]*/, A, B)
// else: XLU / permute conflict path
if HasSetPermutePatternReservation(A, B): ... GetXluPathReservation + XluConflictPenaltyBetween
... final clamps (min floor, SetIar→indexed-load) ...
return max(stall, GetResourceLatency(A, B))IsTrueDependencyBetween(A, B) @0x1c89fdc0 是 FindOperandIndexFull(B, A) >= 0(B 将 A 列为 operand)或 A 与 B 的 opcode 均为 0x14e(自配对 pseudo)。LloOpcodeUsesMxu(op) @0x10a433e0 对 op ∈ [0x8d, 0xa5] ∪ [0xa8, 0xab] ∪ [0x152, 0x153] 为真(编码为 (op-141) < 0x19 || (op-168) < 4 || (op-338) < 2)。MXU table 本身位于 LatencyTable+0x1d8(*((_QWORD*)this+59))。XLU 冲突表位于 LatencyTable+0x18,为非 MXU permute/transpose-FIFO 冲突定价。
NOTE — 两个世代在结构上完全相同。GL
LatencyBetweenInternal @0x1c8b22e0尾调用 GLGetLatencyBetween @0x1c8b71e0,读取同一个IsTrueDependencyBetween(vtable+0x20)和位于this+0x1d8的 MXU table。唯一的逐世代差异是归约内部的资源数量边界(19 VF vs 11 GL)。
MxuLatencyBalancing 环境标志门控
目的
第二个正交门控控制 MXU sequence-group 分配是否使用预留推导的 op 间停顿,还是使用更简单的排序。它是编译环境 knob,不是逐边测试:上面的逐边结构门控始终对 MXU-MXU 非真实依赖边触发;此标志只改变序列分配器如何把这些停顿组合成 MXU 分配。
算法
function MxuLatencyBalancingUseSequenceDependencies(env): // @0x1d6b9c80
p = env[+0xbe8] // AutoProto* (TpuCompilationEnvironment+0xbe8)
if !p: p = &AutoProto_globals_ // default @0x223c8968
v = AutoOr<bool>::FromProtoOrDie(*p) // @0xf795300
return (~v & 0x101) == 0 // true iff v.bit8 (present) AND v.bit0 (value)
```text
唯一调用者是 `AssignMxusForSequenceGroup @0x10f753c0`。当标志为 ON(`@0x10f75410` 处 `al != 0`)时,该函数构造一个以 MRB 地址为 key 的 `linked_hash_map`,并使用 `CycleTable` 参数应用延迟平衡的 MXU-sequence 分配;当 OFF 时,它分支到更简单的排序路径。
> **NOTE —** accessor 及其 bit 编码(`bit8` present,`bit0` value)逐字节精确,默认 `AutoProto*` 是 `AutoProto_globals_ @0x223c8968`。嵌入的默认 proto 字节未解码,因此 v0.0.40 中 sequence-dependency balancing 默认是 ON 还是 OFF 为 **(LOW confidence)**;门控机制确定,默认值未钉牢。
---
## Function Map
| Function | Address | Role |
|---|---|---|
| `MxuLatencyTable::GetLatencyBetween` (VF) | `0x1c8ae320` | 对共享资源取最大值的停顿递推 |
| `MxuLatencyTable::GetLatencyBetween` (GL) | `0x1c8b71e0` | 相同,边界为 res ≤ 10 |
| `MxuOpResourceReservations` (VF / GL) | `0x1c8ad080` / `0x1c8b6300` | A 的 `array<int,N>` 持有周期向量 |
| `MxuOpHoldIssues` (VF / GL) | `0x1c8ad3a0` / `0x1c8b6760` | B 的 `flat_hash_set<MxuResource>` 发射足迹 |
| `LatencyTableViperfish::LatencyBetweenInternal` | `0x1c8a4ac0` | 逐边门控(vtable +0x18) |
| `LatencyTableGhostlite::LatencyBetweenInternal` | `0x1c8b22e0` | GL 逐边门控 |
| `IsTrueDependencyBetween` | `0x1c89fdc0` | `FindOperandIndexFull ≥ 0` ∨ 两者 op 均为 `0x14e` |
| `LloOpcodeUsesMxu` | `0x10a433e0` | op ∈ `[0x8d,0xa5] ∪ [0xa8,0xab] ∪ [0x152,0x153]` |
| `MxuLatencyBalancingUseSequenceDependencies` | `0x1d6b9c80` | env `+0xbe8` AutoOr<bool> 门控 |
| `AssignMxusForSequenceGroup` | `0x10f753c0` | 环境标志的唯一消费者 |
| `flat_hash_set<MxuResource>` held-set ctor | `0x1c8ae0a0` | 构造 B 的 footprint set |
---
## 注意事项
- **资源数量分歧。** VF 携带 19 个 `MxuResource` 值,GL/GF 携带 11 个。CHECK 边界(`≤18` / `≤10`)是归约中唯一的行为差异;重新实现必须按 gen 设置 `A` 的预留数组大小,否则边界检查会触发 `LogMessageFatal`。
- **方向很重要。** `GetLatencyBetween(A, B)` 不对称:`A` 提供预留向量(它持有每个端口多久),`B` 提供 held set(它需要哪些端口)。交换参数会定价另一条不同的边。
- **Seed。** 预归约 seed 只有一种非零情况:matrix-load `A`(vlxmr/MSR,opcode `0xa9`)喂给 matmul `B`(`B.opcode ∈ [0x9b,0xa5]`)时 seed `stall = 1`(`@0x1c8ae320` 处 `v14`)。matmul `A` 对 matres `B`(`0x152`)会完全旁路归约,并返回一个以 `MatmulDataFormat` 为 key 的 cost-map 值(`this+0x80`)。其他所有 pair,包括 matmul-to-matmul 和 matpush-to-matpush,seed 都是 0,因此停顿纯粹是对 held set 的 max。
- **未钉牢的内容。** `MxuOpHoldIssues` SOO-scratch seed(`@0xb43b200`)的字面内容按 4 字节 stride 解码有歧义;起作用的 held set 来自 modifier-map 枚举,seed 的功能角色(scratch)已确认,但其字面字节为 **(LOW confidence)**。`MxuLatencyBalancing` 环境标志的默认值同样未钉牢(见上文)。
## 交叉引用
- [MXU 延迟概览](mxu-latency-overview.md) — 本页使用的 `MxuResource` 枚举和预留矩阵概念
- [MXU 延迟:GF](mxu-latency-gf.md) — `6acc60406` 逐格式 matmul/matpush 预留值和 conv 成本三元组
- [MXU 延迟:GL](mxu-latency-gl.md) — Ghostlite 预留矩阵(11 资源边界)
- [Performance 系列概览](performance-overview.md) — `GetLatency`(retire-half 基础延迟)和逐 gen Performance grid
- [MXU Slot](../isa/slot-mxu.md) — held resources 命名的物理 MXU 子单元