Scan 数据通路
本页中的每个地址、oneof 标签、寄存器区带保护、规约字符串 XOR 常量、结构体偏移以及错误字符串,均从
libtpu-0.0.40-cp314wheel(build-id89edbbe81c5b328a958fe628a9f2207d;buildlibtpu_lts_20260413_b_RC00)中的libtpu.so按字节精确读取而来 —— 来自ScanOpLowering/SegmentedScanOpLowering::matchAndRewrite函数体、ConsumeOneTecVexBundleInstruction的逐分支发射、GetVectorMask/GetVMDestregno区带保护、ScanOp/SegmentedScanOp::build的addOperands顺序,以及mlir::tpu::ScanOp::verify。地址适用于此构建;其他版本会不同。
摘要
SparseCore scan 数据通路把一个向量前缀规约(sum/min/max)转换成单个带掩码的 VectorExtended bundle 槽。它是 embedding 流水线的规约阶段:由 VectorLoad 槽收集到 VREG 中的行会被原地扫描,并且每 lane 的参与由 M-register predicate 门控。重新实现者必须复现的规约原语是一个带掩码、硬件分段的前缀扫描 —— 不是软件循环,也不是树形规约。硬件直接消费掩码:bundle 携带一个 5-bit M-register 选择器,inactive INPUT lanes 在硅片中、低于二进制层面贡献规约单位元。
决定性的结构事实是:scan 谓词化是一个两部分的数据通路,lowering 会严格将其分开。scan 内掩码作为 scan op 自身字段被带入 VEX bundle —— proto+0x38(M-register 索引)由每个 scan 发射分支无条件地从 MLIR operand[1] 写入。它不会在 scan 前由 VectorSelect 预先应用。inactive-output 处置 —— 被掩掉的 lane 在 scan 后读到什么 —— 是一个单独的 VectorAlu VectorSelect op(select(M, scan_result, else)),并且它从一个不同且更窄的寄存器文件取掩码(M0..M15,而 scan 使用 M0..M31)。第三个正交字段是整条 op 谓词(Predication 子消息,一个 P-register),它门控整条指令。三个谓词化字段共存于一个 scan bundle;把它们混为一谈会错误建模数据通路。
本页按顺序记录这三层:MLIR ScanOp lowering(规约 × dtype × rank switch、i1→i32 count-active 路径、分段变体);ISA 掩码消费(附加 proto+0x38 的发射体、把它复制到 bundle bit 0x104 的编码器、两个 M-register 区带);以及 verify 约定(mlir::tpu::ScanOp::verify,前端必须满足的约束)。VEX scan opcode 名单和 bit 位置位于 VectorExtended 与 VEX Mask/Dest-Port/Sub-Opcode;M-register predicate word 布局位于 M-Register Predicate;segment-boundary operand frame 位于 Segmented Scan 与 Segmented Add-Scan。本页负责 scan 数据通路:掩码消费、ScanOp lowering,以及 scan-mode 名单。
对于重新实现,约定如下:
- scan 始终带掩码,并且掩码会被带到 HW,而不是预先应用。 每个 scan 发射分支都会无条件设置
proto+0x38 = GetVectorMask(operand[1])和proto+0x11 |= 1(present)。编码器把proto+0x38作为 5-bit 字段复制到 bundle bit0x104。scan 前没有用于掩蔽输入的VectorSelect—— bundle 把 M-register 索引交给硬件,由 HW 门控参与的 lanes。 - 规约在 SC dialect 中由 3 字符串通过 XOR 解码,而不是 switch enum。
getReductionOp()返回StringRef;lowering 测试len==3,然后测试(word0 ^ K0) | (byte2 ^ K1) == 0,其中sum=0x7573|0x6d,min=0x696d|0x6e,max=0x616d|0x78。Mosaictpu.scandialect 则携带一个ReductionKindAttrenum(sum=0,max=1,min=2)。 - intrinsic 是一个三轴函数:规约 × 元素类型 × rank。
{sum,min,max}×{i32,f32,i16/bf16}→tpu_{add,min,max}_scan{1xNi,1xNf}/tpu_*_half_scan2xN/tpu_{min,max}_scan2xN之一。1xN/2xN后缀是 rank(1xN=rank-1 lane vector,2xN=rank-2 packed sublanes);i/f=int/float。 i1(boolean)sum 是 count-active 路径。 带有sum的i1输入会 lowering 成单个tpu_mprefix(population-count prefix),其结果是i32count vector ——tpu_mprefix::create构建一个i32-vector result type 并直接replaceOp;没有后续 convert,也没有tpu_add_scan1xNi。i1输入禁止单独的 mask,禁止非sum规约,并要求i32输出(由 verify 强制)。- Segmented scans 把 boundary 绑定为 operand[1],即 V read-port operand —— 不是 M-register mask。
SegmentedScanOp::build按顺序添加(data, segment);lowering 发射tpu_*_seg_scan*(NOperands<2>)。不存在i1/mprefixsegmented 路径。 - Inactive-OUTPUT lanes 是单独的
VectorSelectVectorAluop,它从 M0..M15 write/select 区带读取 mask,区别于 scan 的 M0..M31 read 区带。
| MLIR ops | sparse_core::ScanOp (sc_tpu.scan), sparse_core::SegmentedScanOp, Mosaic tpu::ScanOp (tpu.scan) |
| SC lowering | ScanOpLowering::matchAndRewrite 0x1358ab00; SegmentedScanOpLowering::matchAndRewrite 0x13589d40 |
| Build(operand 顺序) | ScanOp::build 0x145f92e0 (data, mask); SegmentedScanOp::build 0x145fd4a0 (data, segment) |
| ISA mask consume | ConsumeOneTecVexBundleInstruction 0x13a15ba0 — 每个分支:proto+0x38 = GetVectorMask(op[1]), proto+0x11 |= 1 |
| Mask read 区带 | GetVectorMask 0x13a33320 — [0x5f,0x7e] = M0..M31(32 深),值 regno−0x5f |
| Mask select/write 区带 | GetVMDestregno 0x13a65b20 — [0x5f,0x6e] = M0..M15(16 深) |
| Encoder mask 字段 | proto+0x38 → bundle bit 0x104,5-bit(EncodeAddScanF32 0x1eb32380,gen-stable glc/vfc) |
| Post-scan select | EmitVectorSelect 0x13a1e000 → SparseCoreTecVectorAlu_VectorSelect |
| Verify | mlir::tpu::ScanOp::verify 0x14af7460 |
| Reduction enum | ReductionKindAttr: sum=0, max=1, min=2(Mosaic);3 字符串(SC) |
| 置信度 | CONFIRMED(基于反编译)除非某行或 callout 另有说明 |
NOTE — 本页负责 scan 数据通路:掩码如何被消费、
ScanOplowering,以及 scan-mode 名单。 VEX bundle bit 位置位于 VectorExtended / VEX Mask/Dest-Port;M-register predicate word 布局位于 M-Register Predicate;segment-boundary operand 绑定位于 Segmented Scan / Segmented Add-Scan。它们被链接,而不在此重复。
两部分谓词化数据通路
目的
在逐层细节之前,先固定模型,因为这是朴素重新实现最容易出错的部分。一个 masked scan 有两个问题,而二进制在两个不同位置用两种不同机制回答它们:
- 哪些 INPUT lanes 参与规约? 由 in-scan mask 回答 —— scan op 自身 bundle 字段中携带的 M-register 索引(
proto+0x38→ bit0x104)。硬件读取它,只让 active lanes 贡献;inactive input lanes 贡献规约单位元(add为 0,min/max为 ±inf)。 - 被掩掉的 OUTPUT lane 在 scan 后读到什么? 由一个单独的
VectorAluVectorSelectop 回答,它与 scan 一起发射:select(M, scan_result, else),其 mask 来自更窄的 M0..M15 write/select 文件。
SparseCore masked scan — the predication is TWO ops, not one
data (VREGs from VectorLoad) ─┐
▼
┌──────────────────────────────────────────────────────┐
│ VEX scan slot (e.g. AddScanS32) │
│ proto+0x38 = M-reg idx (in-scan mask) → bit0x104 5b │ ◄── gates INPUT lanes in HW
│ FindAndEmitToUnusedPort(data) → V read port │ (M0..M31 read band)
│ EmitPredicationToSlot(P-reg) → Predication │ ◄── whole-op predicate (orthogonal)
└──────────────────────────────────────────────────────┘
│ scan_result
▼
┌──────────────────────────────────────────────────────┐
│ VEX VectorAlu VectorSelect (SEPARATE op) │
│ select(M, then=scan_result, else) M0..M15 band │ ◄── disposes INACTIVE OUTPUT lanes
└──────────────────────────────────────────────────────┘
```text
> **GOTCHA — 不要在 scan 前用 `VectorSelect` 预先应用 mask。** 如果读者假设“masked scan = 先 select 输入,再 scan”,就会构建错误的数据通路。mask 进入 scan bundle(`proto+0x38`,在每个发射分支中无条件设置),HW 在规约期间门控 lanes。确实出现的 `VectorSelect` 位于 scan *下游*,并且处置的是 *output* lanes —— 使用不同的 M-register 文件(M0..M15,而不是 M0..M31)。两者是独立 op,拥有独立的 mask registers。
### 三个正交字段
每个 VEX scan bundle 都携带三个与谓词化相关的字段。它们从三个不同 MLIR 来源解码,并写入三个不同 proto 位置:
| 字段 | 发射器 | proto / 子消息 | 作用域 | 来源 |
|---|---|---|---|---|
| per-lane **VECTOR MASK**(bit `0x104`,5b) | scan-emit arm (`op[1]`) → `GetVectorMask` (M0..M31) | `proto+0x38`, present `proto+0x11 \|= 1` | 哪些 lanes 参与规约 | `ConsumeOneTecVexBundleInstruction` `0x13a15ba0` |
| whole-op **PREDICATE**(`Predication` 子消息) | `EmitPredicationToSlot`(最后一个 MCInst operand)→ `GetPregno` (P0..P13) | `Predication` 子消息 | 门控整条指令 | `EmitPredicationToSlot` `0x13a4a160` |
| data **READ-PORT**(V port) | `FindAndEmitToUnusedPort` (`op[2]`) → `GetVregno` | 一个 V read-port slot 字段 | 路由 DATA 值 VREG | `0x13a15ba0` 每个分支 |
> **QUIRK — mask 选择一个 predicate register;它不是 predicate 本身。** `proto+0x38` 保存一个 5-bit M-register *索引*(`regno − 0x5f`),而不是 lane bitmask。M-register 保存的 8-byte predicate WORD —— `{s_start, l_start, s_end, l_end}` sub-/lane bounds,或合成的 iota-compare —— 位于 [M-Register Predicate](m-register-predicate.md) 页面。本页只说明该索引被带入 bundle;它指向的 word 在那里解码。
### 两个 M-register 区带
mask 文件的读侧和写侧具有*不同宽度*,区带保护证明了这一点。`GetVectorMask`(scan 的 mask *read*)接受 M0..M31;`GetVMDestregno`(`VectorSelect` 的 mask *select/write*)只接受 M0..M15:
```c
// GetVectorMask<SparsecoreVectorMask>:: (glc 0x13a33320)
// the in-scan per-lane mask READ — 32-deep file
if (!operand.isReg()) LogFatal("operand.isReg()");
regno = operand.getReg();
if (regno <= 0x5E) LogFatal("regno >= llvm::TPU::M0"); // M0 = 0x5f
if (regno >= 0x7F) LogFatal("regno <= llvm::TPU::M31"); // M31 = 0x7e
return regno - 95; // 0x5f → index 0 ⇒ band [0x5f,0x7e] = M0..M31
// GetVMDestregno:: (glc 0x13a65b20)
// the post-scan VectorSelect mask SELECT/WRITE — 16-deep file
if (regno <= 0x5E) LogFatal("regno >= llvm::TPU::M0");
if (regno >= 0x6F) LogFatal("regno <= llvm::TPU::M15"); // M15 = 0x6e
return regno - 95; // band [0x5f,0x6e] = M0..M15NOTE — scan 从 M0..M31 读取;post-scan select 从 M0..M15 读取。 分配 mask registers 的重新实现者必须遵守两个上限:scan input mask 可以位于 M16..M31,但
VectorSelectmask 不可以。这种非对称性由上面的两个区带保护按字节确认(< 0x7F对< 0x6F)。
MLIR Scan-Op Lowering
目的
ScanOpLowering(一个 ConvertToLLVM conversion pattern)把 sparse_core::ScanOp 重写为一个 SC scan intrinsic,该 intrinsic 由规约字符串和输入元素类型选择。它是把通用 prefix-scan 变成具体 tpu_*_scan* op 的单一调度点,之后 VEX emitter 会将该 op 实现为一个 bundle 槽。函数体是一个扁平的 string-XOR → element-type → rank 级联。
入口点
sparse_core::ScanOp (op-name "sc_tpu.scan", AtLeastNOperands<1>, OneResult)
└─ ScanOpLowering::matchAndRewrite (0x1358ab00) ── reduction × dtype × rank → intrinsic
├─ ScanOp::getReductionOp (StringRef, 3-char)
├─ VectorType::getElementType(operand[1]) ── the scanned element type
├─ tpu_mprefix::create / tpu_*_scan*::create
└─ ReplaceWithScanIntrinsic<tpu_*_scan2xN> (0x1358c1c0 / 0x1358bd80 / 0x1358bfa0)
```text
### 算法
规约字符串通过对 3 个字节执行常量 XOR 来解码 —— 没有 `strcmp`,也没有 enum。元素类型从 `operand[1]`(adaptor 的 data operand)取得,并与 builder 的规范 `i32`/`f32`/`i16`/`bf16` 类型比较。`i1` 元素类型首先特殊处理:
```c
function ScanOpLowering_matchAndRewrite(op): // 0x1358ab00
elt = getElementType(op.operand[1]) // scanned element type
red = op.getReductionOp() // StringRef, 3 chars
// --- i1 (boolean) input: count-active path ---
if elt == i1Type: // cmp r13,r14 (ElementType == I1Type)
if len(red) != 3 || (red ^ "sum") != 0: // i1 allows ONLY sum
return failure
i32vec = vector<i32> // built up-front from operand[1] lane count
cnt = tpu_mprefix::create(builder, {i32vec}, i1_input) // 0x14731a40 — population-count prefix, i32 result
replaceOp(op, cnt) // SINGLE op: no convert, no add_scan
return success
// --- sum: (word0 ^ 0x7573) | (byte2 ^ 0x6d) == 0 ---
if len(red) == 3 && red == "sum":
if elt == i32: emit tpu_add_scan1xNi // 0x146d57c0
elif elt == f32: emit tpu_add_scan1xNf // 0x146d4fc0
elif elt in {i16, bf16}:
if !HasGxcHalfScan(): emitError(
"Currently scan add for i16 and bf16 is only supported for GXC")
emit tpu_add_half_scan2xN // 0x146d4400
else: return failure
// --- min: (word0 ^ 0x696d) | (byte2 ^ 0x6e) == 0 ---
elif len(red) == 3 && red == "min":
if elt == i32: emit tpu_min_scan1xNi // 0x14731340
elif elt == f32: emit tpu_min_scan1xNf // 0x14731180
elif elt in {i16, bf16}:
if !HasGxcHalfScan(): emitError(...GXC...)
return ReplaceWithScanIntrinsic<tpu_min_scan2xN> // 0x1358bd80
else: return failure
// --- max: (word0 ^ 0x616d) | (byte2 ^ 0x78) == 0 ---
elif len(red) == 3 && red == "max":
if elt == i32: emit tpu_max_scan1xNi // 0x14730a80
elif elt == f32: return ReplaceWithScanIntrinsic<tpu_max_scan1xNf> // 0x1358bfa0
elif elt in {i16, bf16}:
if !HasGxcHalfScan(): emitError(...GXC...)
return ReplaceWithScanIntrinsic<tpu_max_scan2xN> // 0x1358c1c0
else: return failure
else:
return failure // unknown reduction stringXOR 常量是 little-endian 字节三元组:"sum" = s u=0x7573,m=0x6d;"min" = m i=0x696d,n=0x6e;"max" = m a=0x616d,x=0x78 —— 这些直接从反编译函数体(0x1358ab00 中围绕三次 getReductionOp 调用的行)里的 cmp/xor 立即数读取。
规约 × dtype × rank → intrinsic 映射
| reduction | input elt | → intrinsic | create / leaf @ | emit form |
|---|---|---|---|---|
sum | i1(count) | tpu_mprefix(i32 result,替换 op) | 0x14731a40 | direct(单 op) |
sum | i32 | tpu_add_scan1xNi | 0x146d57c0 | direct |
sum | f32 | tpu_add_scan1xNf | 0x146d4fc0 | direct |
sum | i16/bf16 | tpu_add_half_scan2xN | 0x146d4400 | direct(GXC-gated) |
min | i32 | tpu_min_scan1xNi | 0x14731340 | direct |
min | f32 | tpu_min_scan1xNf | 0x14731180 | direct |
min | i16/bf16 | tpu_min_scan2xN | 0x1358bd80 | ReplaceWithScanIntrinsic(GXC-gated) |
max | i32 | tpu_max_scan1xNi | 0x14730a80 | direct |
max | f32 | tpu_max_scan1xNf | 0x1358bfa0 | ReplaceWithScanIntrinsic |
max | i16/bf16 | tpu_max_scan2xN | 0x1358c1c0 | ReplaceWithScanIntrinsic(GXC-gated) |
命名:1xN = rank-1 lane vector;2xN / half = rank-2,两个 sublanes packed(bf16/16-bit,带 f32-accumulate);i/f = int/float。ReplaceWithScanIntrinsic<T> 是用于 2xN 形式和 max-f32 的模板路径;其他路径直接调用 T::create 生成 LLVMStructType literal。2xN/half 形式是否 1:1 映射到 VEX *PartialSum* sub-opcodes,或通过 VpackFormat attribute 携带 accumulate dtype,归 VEX / Segmented Add-Scan 负责 —— 此处只交叉链接,不重新解码(精确 rank-2 sub-opcode 绑定为 LOW)。
NOTE — i16/bf16 scan-add 受目标能力门控,否则发射 “GXC only” 错误。
i16/bf16分支调用一个 vtable predicate(反编译中的(**(ctx+104)+1920)(ctx+104),一种 per-targetHasGxcHalfScan风格查询),当其为 false 时执行emitError("Currently scan add for i16 and bf16 is only supported for GXC")(InFlightDiagnostic << "Currently scan add for i16 and bf16 is only supported for " << "GXC")。面向非 GXC 代际的重新实现者必须拒绝 half-precision scans,而不是发射 intrinsic。(这个 gate 不在此前的ScanOp文档中;此处由 lowering 函数体 CONFIRMED。)
i1 count-active 路径
i1-sum 情况是 SparseCore 的 population-count-prefix 原语 —— 到每个位置为止的 active lanes 数量,用于 embedding 流水线中的 ragged-row offset 计算。这是 scan op 消费 boolean vector 的唯一位置:
i1 vector ──► tpu_mprefix (OneOperand) ──► i32 vector (the op result)
(cross-lane mask prefix-sum;
asm "vmprefix.xlane")
```text
`tpu_mprefix`(`0x14731a40`,trait `OneOperand`,op-name `"llvm_tpu.mprefix"`)只接受 `i1` 输入*本身* —— 它没有单独的 mask,这正是 verify 禁止 `i1` 输入携带 mask operand 的原因。lowering 预先构建一个 `i32`-vector result type(来自 `operand[1]` lane count),针对该类型构造单个 `tpu_mprefix` op,并直接 `replaceOp` 原始 `ScanOp` —— 没有后续 convert,也没有串接的 `tpu_add_scan1xNi`。`i1`-sum scan 正好是一个 intrinsic。
> **NOTE — `tpu_mprefix` lowering 到 `VectorAlu` op(`VectorMaskPrefixSum`),而不是 `VectorExtended`/VEX op。** 沿 intrinsic 继续追踪解决了长期悬而未决的“mprefix 携带哪个 VEX sub-opcode”问题:它不携带任何 VEX sub-opcode。`llvm_tpu.mprefix`(LLVM intrinsic `13389`,machine mnemonic `scVMPREFIX`,asm `vmprefix.xlane`)指令选择到 `SparseCoreTecVectorAlu_VectorMaskPrefixSum` —— `VectorAlu` 槽中的*cross-lane mask* op,由共享的 `EmitCrossLaneUnop` 函数体(`glc` `0x13a19d40`)发射,而不是由任何 `EncodeSparseCoreTecVectorExtended<...>` encoder 发射(二进制中不存在 `VectorExtended` mprefix/prefix encoder)。它是共享该发射体的三个 M-register cross-lane unops 之一 —— `VectorMaskPrefixSum`、`VectorMaskPopulationCount`、`VectorMaskCountTrailingZeros`(见 [`VectorAlu` opcode 名单](vector-opcode-enum.md))。绑定要点:在 `vfc` 上为 `VectorAlu` opcode `0x54`(84)且 sub-field `1`(单一形式,`EncodeSparseCoreTecVectorAlu0VectorMaskPrefixSum` `0x1e960cc0`;`Matches`: `(opcode & 0x7F00)==0x5400 && sub==1` `0x1e950e40`);在 `gxc` `glc`/`gfc` 上为 opcode `0x80`(128)且 sub-field `2`(=B32)/`3`(=B16)(`EncodeSparseCoreTecVectorAlu0VectorMaskPrefixSumB32` `0x1eab4a40`,`…B16` `0x1eab4b40`)—— bit-width 由 6-bit sub-field 携带,而不是由不同 opcode 携带。单个 MLIR operand 通过 `GetVMDestregno` 到达 M-register(M0..M15 select 区带 —— 与 post-scan `VectorSelect` 使用的相同区带),source 通过 X read-port(`UseVectorXPort`)路由。因此 `i1`-sum scan 是 M-register predicate 上的 `VectorAlu` cross-lane prefix-sum,*不是* `VectorExtended` scan。CONFIRMED。
---
## 分段变体
### 目的
`SegmentedScanOpLowering` 是 embedding-`sum` lowering:一种 prefix scan,会在每个样本的 segment boundary 处*重置*运行中的 accumulator,因此对 packed ragged batch 执行一次 scan 就能产生每行和。它复用相同的 reduction-string XOR switch,但会绑定第二个 operand —— segment-boundary vector —— 并且**没有** `i1`/`mprefix` 路径(segment scan 规约 data,而不是计数 predicate bits)。
### 算法
```c
function SegmentedScanOpLowering_matchAndRewrite(op): // 0x13589d40
red = op.getReductionOp() // same 3-char StringRef
elt = getElementType(op.operand[1])
// same XOR switch: "sum"=0x7573|0x6d, "min"=0x696d|0x6e, "max"=0x616d|0x78
switch (red, elt):
sum / i32 -> tpu_add_seg_scan1xNi // 0x146d5c40
sum / f32 -> tpu_add_seg_scan1xNf // 0x146d5a80
sum / i16,bf16 -> tpu_add_half_seg_scan2xN // 0x146d45c0 (GXC-gated, "...GXC" error)
min / i32 -> tpu_min_seg_scan1xNi // 0x14731880
min / f32 -> tpu_min_seg_scan1xNf // 0x147316c0
max / i32 -> tpu_max_seg_scan1xNi // 0x14730fc0
max / f32 -> tpu_max_seg_scan1xNf // 0x14730e00
// min/max i16,bf16 → tpu_{min,max}_seg_scan2xN (roster)
default -> emitError / failureboundary operand 是 operand[1],不是 mask
segment boundary 被绑定为第二个 SSA operand,并且 SegmentedScanOp::build 证明了该顺序 —— 它发出两次 addOperands 调用,先 data 后 segment,二者都是无条件的:
// SegmentedScanOp::build(OpBuilder, OperationState, Type, Value data, Value segment, StringAttr red)
// (0x145fd4a0)
addOperands(state, &data, 1); // operand[0] = data
addOperands(state, &segment, 1); // operand[1] = segment boundary
state.getOrAddProperties().reduction_op = red; // StringAttr property
```text
对比 `ScanOp::build`(`0x145f92e0`):它保护 data operand(`if (data) addOperands(data)`),然后把 *mask* 添加为 operand[1] —— 因此对于*普通* scan,operand[1] 是 per-lane VECTOR MASK(会变成 `proto+0x38` 的那个),而对于*分段* scan,operand[1] 是 SEGMENT BOUNDARY,一个 V-read-port 值。二者在 SSA 层级都是 operand[1],但它们由 [VEX](vectorextended-vex.md) emitter 路由到不同的 bundle 字段。
> **GOTCHA — operand[1] 对 `ScanOp` 和 `SegmentedScanOp` 意味着两种不同事物。** 在普通 `ScanOp` 中,operand[1] 是 M-register vector mask(→ `proto+0x38`,in-scan predicate)。在 `SegmentedScanOp` 中,operand[1] 是 segment-id boundary(→ 一个 V read port,由 `FindAndEmitToUnusedPort` register-allocate)。连接 operand frame 的重新实现者必须基于 op identity 分支,而不是假设 operand[1] 总是 mask。boundary operand frame 和 `VpackFormat` capability matrix 归 [Segmented Scan](segmented-scan.md) / [Segmented Add-Scan](segmented-add-scan.md) 负责。
---
## ISA 掩码消费
### 目的
`ConsumeOneTecVexBundleInstruction`(glc TEC-VEX bundle emitter)把一个 `MCInst` scan op 转换成 bundle proto。每个 scan 分支都做相同的三件事:构造 per-op proto 子消息(一个进入 `proto+0x50` 的 oneof tag)、从 MCInst `operand[1]` 附加 per-lane mask,并把 data VREG 从 `operand[2]` 路由到空闲 V read port。mask attach 是本页核心 —— 它让 scan 在硬件中带掩码。
### 算法
`AddScanS32` 分支,按字节追踪(`0x13a16ce6`..;oneof tag 6):
```c
// ConsumeOneTecVexBundleInstruction — AddScanS32 arm (0x13a15ba0)
clear_inst(vex_proto); // SparseCoreTecVectorExtended::clear_inst
vex_proto.oneof_tag = 6; // [proto+0x50] = 6
sub = Arena::DefaultConstruct<...AddScanS32>(arena); // proto+0x50 submessage
proto.scan = sub;
// --- the in-scan mask: operand[1] → M-register index, ALWAYS attached ---
sub[0x38] = GetVectorMask<SparsecoreVectorMask>(mcinst.operand[1]); // proto+0x38 = regno - 0x5f
sub[0x11] |= 1; // present flag (or [proto+0x11], 1)
// --- the data value: operand[2] → a free V read port ---
vregno = GetVregno(mcinst.operand[2]);
FindAndEmitToUnusedPort<SparsecoreVregReadPort, ...AddScanS32>(status, slot, vregno, sub);反编译显示该赋值是普通的 mov [sub+0x38], eax,随后是 or [sub+0x11], 1 —— present-bit 在每个 scan 分支中无条件设置。MCInst operand 取值为 *((_QWORD*)inst+2) + 0x10 对应 operand[1],+0x20 对应 operand[2](每个 operand 的 stride 是 0x10)。
QUIRK — mask 会附加到每个 scan 分支,不仅是 masked scans。 发射体中没有“这个 scan 是否带掩码”的分支 ——
proto+0x38 = GetVectorMask(op[1])与proto+0x11 |= 1会对AddScanS32、AddScanBf16PartialSumBf16、AddScanS16PartialSumS16、SegmentedAddScan*family、DuplicateCount{Integer,Float}、MaxIndexScan{F32,U32}、MaxScanF32以及所有其他 scan/reduce 分支无条件运行。“unmasked” scan 只是其 mask M-register 选择所有 lanes;bundle 字段总会被填充。在 encoder 层把 mask 做成 optional 的重新实现者会错误编码每个 scan。
从 proto 字段到 bundle bit
encoder 闭合了回路:proto+0x38(M-register 索引)被复制到 bundle bit 0x104,作为 5-bit 字段。mask 分支是 scan encoder 写入的最后一个字段,位于 V read-port array 之后:
; EncodeSparseCoreTecVectorExtendedAddScanF32 (glc 0x1eb32380), mask arm @0x1eb32470
test byte ptr [rax+0x11], 1 ; f6 40 11 01 — mask present?
; if present:
movsxd rax, dword ptr [rax+0x38] ; 48 63 40 38 — sign-extend the M-register index
mov esi, 0x104 ; be 04 01 00 00 — bundle bit 0x104
xor ecx, ecx
mov r8d, 5 ; 41 b8 05 — 5-bit field
call BitCopy
```text
这个分支在 vfc `EncodeSparseCoreTecVectorExtendedFloatAddScan` encoder 中**按字节相同**(`0x1e9b14a0`,mask arm `0x1e9b1590`:相同的 `be 04 01 00 00` / `41 b8 05`),因此 M-register-selector 到 bundle 字段的映射在 glc↔vfc 上 gen-stable。精确 bit 位置及周围 V-port array 归 [VEX Mask/Dest-Port/Sub-Opcode](vex-mask-destport-subopcode.md) 负责;本页只锚定 `proto+0x38` 是源,bit `0x104`(5b)是目标。
### post-scan VectorSelect
inactive-OUTPUT-lane 处置是一个*单独的* `VectorAlu` op,即由 `EmitVectorSelect`(`0x13a1e000`)发射的 `SparseCoreTecVectorAlu_VectorSelect`。它读取三样东西:`then` value(scan result,由 `GetOperandAndVsEncoding(op, 2)` 解码,再经 `GetVregno` → `proto+0x1c`)、一个通过 `GetVMDestregno` 获取的 mask register(M0..M15 select 区带 → `proto+0x18`),以及一个通过 `UseVectorXPort` 路由的 `else` value(X read port → `proto+0x20`):
```c
// EmitVectorSelect<...VectorSelect> (0x13a1e000)
GetOperandAndVsEncoding(op, 2); // X-port = then / scan_result
then_v = GetVregno(op); // → proto+0x1c (a4+28)
mask = GetVMDestregno(op); // M0..M15 select band → proto+0x18 (a4+24)
proto[0x10] |= 3; // present flags for then + mask
else_v = GetVregno(op); // → proto+0x20 (a4+32), via UseVectorXPort
proto[0x10] |= 4; // present flag for else
// result lane = mask[lane] ? then[lane] : else[lane]else operand 是 zero-vs-preserve 选择:被掩掉的 output lane 读取 else,对于 fresh result 是 identity/zero,对于 preserve-old reduction 是先前值。每个 scan family 的精确 else wiring(index-scan 是否写 sentinel、duplicate-count 是否置零)是 per-emitter 的,此处不穷举(LOW);VectorSelect op、它的 M0..M15 mask,以及它的 two-VREG operand frame 均为 CONFIRMED。
Verify 约定
目的
mlir::tpu::ScanOp::verify(0x14af7460)是前端约定:tpu.scan op 在 lowering 前必须满足的约束。它是 scan 类型规则最清晰的单一来源 —— core placement、rank、element-type、i1 special cases、mask shape,以及 reduction enum。Verify failures 会发出带有下列精确字符串的 opError。
约束(按字节精确的字符串)
| # | 检查 | 字符串 | 反编译锚点 |
|---|---|---|---|
| 1 | parent core type == 2(SC vector subcore) | Scan is supported only on the SC vector subcore | GetCoreTypeOfParentOp != 2(line 51) |
| 2 | i1 input → output is i32 | Output element type must be i32 vector for i1 vector inputs. | isInteger(1) then !isInteger(0x20)(60/63) |
| 3 | non-i1 input/output element types match | Input and output element type mismatch. | getElementType cmp(71/72) |
| 4 | input/output shapes match | Input and output shape mismatch. Input shape: ( | getShape + bcmp(78–84, 163) |
| 5 | input rank 1 or 2(拒绝 ≥3) | Input must be a rank 1 or 2 vector. | rank >= 3(87) |
| 6 | i1 input → reduction is sum(enum 0) | Only sum reduction is supported for i1 vector inputs. | getValue() != 0(101–103) |
| 7 | reduction ∈ {sum=0, max=1, min=2} | Only sum, max and min reductions are supported. | getValue() 0/1/2 chain(108–110, 153) |
| 8 | i1 input → no mask operand | Mask is not supported for i1 vector inputs. | mask present + isInteger(1)(116–118) |
| 9 | mask is rank 1 | Mask must be a rank 1 vector. | mask getShape rank != 1(124, 148) |
| 10 | mask length == input lane count | Mask and input mismatch. Expected mask of length: …, but got … | mask shape cmp(131–141) |
function ScanOp_verify(op): // 0x14af7460
if GetCoreTypeOfParentOp(op) != 2: // SC vector subcore
return opError("Scan is supported only on the SC vector subcore")
in_elt = getElementType(op.operand[0])
out_elt = getElementType(op.result[0])
if in_elt.isInteger(1): // i1 input
if !out_elt.isInteger(32):
return opError("Output element type must be i32 vector for i1 vector inputs.")
else if in_elt != out_elt:
return opError("Input and output element type mismatch.")
if shape(in) != shape(out): // bcmp
return opError("Input and output shape mismatch. Input shape: (")
if rank(in) >= 3:
return opError("Input must be a rank 1 or 2 vector.")
red = op.reduction_kind // ReductionKindAttr enum
if in_elt.isInteger(1) && red != 0: // i1 ⇒ sum only
return opError("Only sum reduction is supported for i1 vector inputs.")
if red not in {0,1,2}: // sum=0, max=1, min=2
return opError("Only sum, max and min reductions are supported.")
if op.numOperands == 1 || op.mask == null: // no mask → done
return success
if in_elt.isInteger(1):
return opError("Mask is not supported for i1 vector inputs.")
if rank(op.mask) != 1:
return opError("Mask must be a rank 1 vector.")
if shape(op.mask)[0] != shape(in)[lane_dim]:
return opError("Mask and input mismatch. Expected mask of length: <N>, but got <M>.")
return success
```text
reduction enum(`sum=0`,`max=1`,`min=2`)从 `ReductionKindAttr::getValue()` 的 `je`/`jne` 链(lines 101/108–110)读取,并由 parse error 名单交叉确认(`expected ::mlir::tpu::ReductionKind to be one of: …`)。mask-presence 测试 `numOperands == 1 || mask == null`(反编译为 `*((_DWORD*)op+17) == 1 || !*(...op+9)+56)`)是精确 gate:只有存在真实 mask operand 时,约束 8–10 才会运行。
> **NOTE — verify 强制两个 lowering 不会重新检查的 mask-shape 约束。** mask 必须是 rank-1,且其长度必须等于输入的 lane count。这些(`"Mask must be a rank 1 vector."`、`"Mask and input mismatch. Expected mask of length: …, but got …"`)在此处的 `verify` 中 CONFIRMED,并且是对先前 mask 文档的补充;先前文档只列出了 `i1`-no-mask 规则。重新实现者的 verifier 必须在 lowering 前拒绝 rank-2 或尺寸不匹配的 mask,因为 lowering 假设 mask 形式正确。
>
> **QUIRK — 存在两个 scan dialect,带有两种 reduction encoding。** Mosaic `tpu.scan`(`tpu::ScanOp`,本 verify)携带 `ReductionKindAttr` ENUM(`sum=0`/`max=1`/`min=2`)。SC `sc_tpu.scan`(`sparse_core::ScanOp`,上面的 lowering)携带由 XOR 解码的 3 字符 `reduction_op` STRING。enum→string bridge(`tpu.scan → sc_tpu.scan` conversion)是一个单独的 pattern,不在本页。重新实现者不能假设只有一种 encoding;前端 op 使用 enum,SC lowering 使用 string。
---
## 函数映射
| Symbol | Address | Role |
|---|---|---|
| `ScanOpLowering::matchAndRewrite` | `0x1358ab00` | reduction × dtype × rank → intrinsic;`i1` count 路径 |
| `SegmentedScanOpLowering::matchAndRewrite` | `0x13589d40` | segmented variant;相同 XOR switch,无 `i1` 路径 |
| `ScanOp::build` (StringAttr) | `0x145f92e0` | `addOperands(data)` then `addOperands(mask)` — operand[0]=data, [1]=mask |
| `ScanOp::create` | `0x145f93e0` | op-name `"sc_tpu.scan"`;调用 `build` |
| `SegmentedScanOp::build` | `0x145fd4a0` | `addOperands(data)` then `addOperands(segment)` — operand[1]=boundary |
| `SegmentedScanOp::create` | `0x145fd5a0` | 构建 `(data, segment, reductionStr)` |
| `ConsumeOneTecVexBundleInstruction` | `0x13a15ba0` | per-arm emit;`proto+0x38 = GetVectorMask(op[1])`, `proto+0x11 \|= 1` |
| `GetVectorMask<SparsecoreVectorMask>` | `0x13a33320` | in-scan mask read;区带 `[0x5f,0x7e]` = M0..M31,值 `regno−0x5f` |
| `GetVMDestregno` | `0x13a65b20` | `VectorSelect` mask select/write;区带 `[0x5f,0x6e]` = M0..M15 |
| `EmitPredicationToSlot<…VectorExtended>` | `0x13a4a160` | whole-op predicate(最后一个 MCInst operand → `Predication` 子消息);`GetPregno` 区带 P0..P13(`0x139f1bc0`) |
| `EmitVectorSelect<…VectorSelect>` | `0x13a1e000` | post-scan `select(M, then, else)`;mask via `GetVMDestregno` |
| `EncodeSparseCoreTecVectorExtendedAddScanF32` | `0x1eb32380` | encoder;`proto+0x38` → bundle bit `0x104`(5b);glc |
| `EncodeSparseCoreTecVectorExtendedFloatAddScan` | `0x1e9b14a0` | vfc encoder;mask arm 与 glc 按字节相同 |
| `BroadcastBoolToVector` | `0x13d9bfa0` | `getBoolAttr(value)` → `BroadcastScalarToVector` — scan-mask producer |
| `tpu_mprefix::create` | `0x14731a40` | `i1` cross-lane mask prefix-sum(`OneOperand`,`"llvm_tpu.mprefix"`,LLVM intrinsic `13389`,mnemonic `scVMPREFIX`) |
| `EmitCrossLaneUnop<…VectorMaskPrefixSum…>` | `0x13a19d40` (glc) / `0x139adba0` (vfc) | `tpu_mprefix` → `VectorAlu` `VectorMaskPrefixSum` 的 emit body;operand → M-reg via `GetVMDestregno`,source via `UseVectorXPort` |
| `EncodeSparseCoreTecVectorAlu0VectorMaskPrefixSum` | `0x1e960cc0` (vfc) / `0x1eab4a40` (glc-B32) / `0x1eab4b40` (glc-B16) | `VectorAlu` encoder:opcode `0x54` sub `1`(vfc)/ opcode `0x80` sub `2`=B32/`3`=B16(glc) |
| `mlir::tpu::ScanOp::verify` | `0x14af7460` | typing/mask/reduction 约定(10 项约束) |
> **NOTE — 先前文档将 `tpu::ScanOp::verify` 标为 `0x14af7460`,这是正确的;SC 侧 `verifyInvariantsImpl`(`0x145f9640`)并不携带这些约束字符串。** 按字节精确的约束名单(包括两个新的 mask-shape 规则)位于 Mosaic `tpu::ScanOp::verify`。在 SC `sparse_core::ScanOp::verifyInvariantsImpl` 中搜索这些消息的重新实现者不会找到它们。
---
## 注意事项
- **scan 在 ISA 层始终带掩码。** 对每个 scan op 编码 `proto+0x38`(M-register selector)并设置 `proto+0x11 |= 1`,不要有条件地做。unmasked scan 选择一个 all-lanes M-register;它不是另一种 encoding。
- **mask consumption 发生在 HW 内,而不是 pre-select。** 不要把 masked scan lowering 为 `VectorSelect(input) → scan`。应 lowering 为 `scan(input, mask)`,mask 被携带在 bundle 中;HW 门控 lanes,inactive inputs 贡献规约单位元。出现的 `VectorSelect` 是一个*单独的下游* op,用于 output lanes。
- **两个 M-register 文件,两个上限。** Scan input masks 可使用 M0..M31(`GetVectorMask`);post-scan `VectorSelect` masks 只能使用 M0..M15(`GetVMDestregno`)。据此分配。
- **operand[1] 对 `ScanOp` 是 mask,对 `SegmentedScanOp` 是 boundary。** 按 op identity 分支;二者都是 SSA operand[1],但会路由到不同 bundle 字段。
- **`i1`-`sum` 是唯一 boolean scan,并且它是单个 `tpu_mprefix` op —— 没有 `add_scan`。** 它 lowering 为一个 `VectorAlu` `VectorMaskPrefixSum`(cross-lane mask prefix-sum),禁止 mask,禁止非 `sum`,并要求 `i32` 输出(verify constraints 2/6/8)。
- **i16/bf16 scan-add 仅限 GXC。** 在 target-capability predicate 上门控 half-precision scans,否则发出 “GXC only” 错误;不要在非 GXC generation 上合成 `*_half_scan2xN` intrinsic。
- **两个 scan dialect,两种 reduction encoding。** Mosaic `tpu.scan` 使用 `ReductionKindAttr`(`sum=0`/`max=1`/`min=2`);SC `sc_tpu.scan` 使用 3 字符串(XOR 解码)。Verify 在 enum 上操作;SC lowering 在 string 上操作。
- **Unmapped / LOW。** in-scan mask 的最低 per-lane HW write-enable(物理 suppress-on-masked-output 对总是物化 `VectorSelect`);post-scan select 中每个 scan family 的精确 `else` operand;rank-2 `2xN`/`half` → VEX `*PartialSum*` sub-opcode 绑定。(`tpu_mprefix` sub-opcode 不再开放 —— 它绑定到 `VectorAlu` `VectorMaskPrefixSum`,opcode `0x54`/`0x80` 取决于 target,已在 §“i1 count-active 路径”中解决。)
---
## 相关组件
| 名称 | 关系 |
|---|---|
| `ScanOpLowering` / `SegmentedScanOpLowering` (`0x1358ab00` / `0x13589d40`) | reduction × dtype × rank → intrinsic switch;segmented variant |
| `ConsumeOneTecVexBundleInstruction` (`0x13a15ba0`) | 附加 in-scan mask(`proto+0x38`)并路由 data port 的 per-arm emit |
| `GetVectorMask` / `GetVMDestregno` (`0x13a33320` / `0x13a65b20`) | M0..M31 read 区带 vs M0..M15 select/write 区带 |
| `EmitVectorSelect` (`0x13a1e000`) | 处置 inactive OUTPUT lanes 的单独 `VectorAlu` op |
| `mlir::tpu::ScanOp::verify` (`0x14af7460`) | 10 项 typing/mask/reduction 约定 |
| `BroadcastBoolToVector` (`0x13d9bfa0`) | MLIR 层的 all-true/all-false scan-mask producer |
## 交叉引用
- [VectorExtended (VEX)](vectorextended-vex.md) — 本数据通路发射到其中的 scan/sort/reduce 槽;VEX opcode 名单、V read ports,以及 scans 实现的 `SourceOne` seed。
- [VEX Mask / Dest-Port / Sub-Opcode](vex-mask-destport-subopcode.md) — bundle bit `0x104`(5-bit mask selector)、dest read-port,以及本页 `proto+0x38` 馈入的 48-encoder sub-opcode map。
- [M-Register Predicate Word (M0–M31)](m-register-predicate.md) — M-register 保存的 8-byte predicate WORD(`{s_start,l_start,s_end,l_end}` / iota-compare)以及 masked-scan inactive-lane output model。
- [Segmented Scan](segmented-scan.md) — per-segment-reset scan;此处总结的 `SegmentedScanOpLowering` reduction switch 与 boundary-operand 绑定。
- [Segmented Add-Scan](segmented-add-scan.md) — `SegmentedAddScan` operand frame 以及 `2xN`/`half` 形式依赖的 `VpackFormat` dtype-attribute capability matrix。
- [VectorLoad Slot](vectorload-slot.md) — 填充本数据通路所 scan 的 VREG 的 read-side slot;其中记录的 `SourceOne` seed selector 与 segment-id operand binding。
- [TEC Vector Opcode Enumeration](vector-opcode-enum.md) — `VectorAlu` opcode 名单(包括 `VectorSelect`)与 opcode-recovery model。
- [SparseCore Overview](overview.md) — 三类 SC engine 以及 TEC vector scan 数据通路所处位置。
- **Binary:** `extracted/libtpu-0.0.40-cp314-cp314-manylinux_2_31_x86_64/libtpu/libtpu.so`(build-id `89edbbe81c5b328a958fe628a9f2207d`)
- **Index entry:** Part IX — SparseCore & BarnaCore / SparseCore datapath (embeddings) — [返回索引](../index.md)