Skip to content

Scan 数据通路

本页中的每个地址、oneof 标签、寄存器区带保护、规约字符串 XOR 常量、结构体偏移以及错误字符串,均从 libtpu-0.0.40-cp314 wheel(build-id 89edbbe81c5b328a958fe628a9f2207d;build libtpu_lts_20260413_b_RC00)中的 libtpu.so 按字节精确读取而来 —— 来自 ScanOpLowering/SegmentedScanOpLowering::matchAndRewrite 函数体、ConsumeOneTecVexBundleInstruction 的逐分支发射、GetVectorMask/GetVMDestregno 区带保护、ScanOp/SegmentedScanOp::buildaddOperands 顺序,以及 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 位置位于 VectorExtendedVEX Mask/Dest-Port/Sub-Opcode;M-register predicate word 布局位于 M-Register Predicate;segment-boundary operand frame 位于 Segmented ScanSegmented Add-Scan。本页负责 scan 数据通路:掩码消费、ScanOp lowering,以及 scan-mode 名单。

对于重新实现,约定如下:

  • scan 始终带掩码,并且掩码会被带到 HW,而不是预先应用。 每个 scan 发射分支都会无条件设置 proto+0x38 = GetVectorMask(operand[1])proto+0x11 |= 1(present)。编码器把 proto+0x38 作为 5-bit 字段复制到 bundle bit 0x104。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|0x6dmin=0x696d|0x6emax=0x616d|0x78。Mosaic tpu.scan dialect 则携带一个 ReductionKindAttr enum(sum=0max=1min=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 路径。 带有 sumi1 输入会 lowering 成单个 tpu_mprefix(population-count prefix),其结果是 i32 count vector —— tpu_mprefix::create 构建一个 i32-vector result type 并直接 replaceOp;没有后续 convert,也没有 tpu_add_scan1xNii1 输入禁止单独的 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/mprefix segmented 路径。
  • Inactive-OUTPUT lanes 是单独的 VectorSelect VectorAlu op,它从 M0..M15 write/select 区带读取 mask,区别于 scan 的 M0..M31 read 区带。
MLIR opssparse_core::ScanOp (sc_tpu.scan), sparse_core::SegmentedScanOp, Mosaic tpu::ScanOp (tpu.scan)
SC loweringScanOpLowering::matchAndRewrite 0x1358ab00; SegmentedScanOpLowering::matchAndRewrite 0x13589d40
Build(operand 顺序)ScanOp::build 0x145f92e0 (data, mask); SegmentedScanOp::build 0x145fd4a0 (data, segment)
ISA mask consumeConsumeOneTecVexBundleInstruction 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 selectEmitVectorSelect 0x13a1e000SparseCoreTecVectorAlu_VectorSelect
Verifymlir::tpu::ScanOp::verify 0x14af7460
Reduction enumReductionKindAttr: sum=0, max=1, min=2(Mosaic);3 字符串(SC)
置信度CONFIRMED(基于反编译)除非某行或 callout 另有说明

NOTE — 本页负责 scan 数据通路:掩码如何被消费、ScanOp lowering,以及 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 有两个问题,而二进制在两个不同位置用两种不同机制回答它们:

  1. 哪些 INPUT lanes 参与规约?in-scan mask 回答 —— scan op 自身 bundle 字段中携带的 M-register 索引(proto+0x38 → bit 0x104)。硬件读取它,只让 active lanes 贡献;inactive input lanes 贡献规约单位元(add 为 0,min/max 为 ±inf)。
  2. 被掩掉的 OUTPUT lane 在 scan 后读到什么? 由一个单独的 VectorAlu VectorSelect op 回答,它与 scan 一起发射:select(M, scan_result, else),其 mask 来自更窄的 M0..M15 write/select 文件。
text
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..M15

NOTE — scan 从 M0..M31 读取;post-scan select 从 M0..M15 读取。 分配 mask registers 的重新实现者必须遵守两个上限:scan input mask 可以位于 M16..M31,但 VectorSelect mask 不可以。这种非对称性由上面的两个区带保护按字节确认(< 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 级联。

入口点

text
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 string

XOR 常量是 little-endian 字节三元组:"sum" = s u=0x7573m=0x6d"min" = m i=0x696dn=0x6e"max" = m a=0x616dx=0x78 —— 这些直接从反编译函数体(0x1358ab00 中围绕三次 getReductionOp 调用的行)里的 cmp/xor 立即数读取。

规约 × dtype × rank → intrinsic 映射

reductioninput elt→ intrinsiccreate / leaf @emit form
sumi1(count)tpu_mprefix(i32 result,替换 op)0x14731a40direct(单 op)
sumi32tpu_add_scan1xNi0x146d57c0direct
sumf32tpu_add_scan1xNf0x146d4fc0direct
sumi16/bf16tpu_add_half_scan2xN0x146d4400direct(GXC-gated)
mini32tpu_min_scan1xNi0x14731340direct
minf32tpu_min_scan1xNf0x14731180direct
mini16/bf16tpu_min_scan2xN0x1358bd80ReplaceWithScanIntrinsic(GXC-gated)
maxi32tpu_max_scan1xNi0x14730a80direct
maxf32tpu_max_scan1xNf0x1358bfa0ReplaceWithScanIntrinsic
maxi16/bf16tpu_max_scan2xN0x1358c1c0ReplaceWithScanIntrinsic(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-target HasGxcHalfScan 风格查询),当其为 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 的唯一位置:

text
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 / failure

boundary operand 是 operand[1],不是 mask

segment boundary 被绑定为第二个 SSA operand,并且 SegmentedScanOp::build 证明了该顺序 —— 它发出两次 addOperands 调用,先 data 后 segment,二者都是无条件的:

c
// 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 会对 AddScanS32AddScanBf16PartialSumBf16AddScanS16PartialSumS16SegmentedAddScan* 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 之后:

asm
; 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::verify0x14af7460)是前端约定:tpu.scan op 在 lowering 前必须满足的约束。它是 scan 类型规则最清晰的单一来源 —— core placement、rank、element-type、i1 special cases、mask shape,以及 reduction enum。Verify failures 会发出带有下列精确字符串的 opError

约束(按字节精确的字符串)

#检查字符串反编译锚点
1parent core type == 2(SC vector subcore)Scan is supported only on the SC vector subcoreGetCoreTypeOfParentOp != 2(line 51)
2i1 input → output is i32Output element type must be i32 vector for i1 vector inputs.isInteger(1) then !isInteger(0x20)(60/63)
3non-i1 input/output element types matchInput and output element type mismatch.getElementType cmp(71/72)
4input/output shapes matchInput and output shape mismatch. Input shape: (getShape + bcmp(78–84, 163)
5input rank 1 or 2(拒绝 ≥3)Input must be a rank 1 or 2 vector.rank >= 3(87)
6i1 input → reduction is sum(enum 0)Only sum reduction is supported for i1 vector inputs.getValue() != 0(101–103)
7reduction ∈ {sum=0, max=1, min=2}Only sum, max and min reductions are supported.getValue() 0/1/2 chain(108–110, 153)
8i1 input → no mask operandMask is not supported for i1 vector inputs.mask present + isInteger(1)(116–118)
9mask is rank 1Mask must be a rank 1 vector.mask getShape rank != 1(124, 148)
10mask length == input lane countMask and input mismatch. Expected mask of length: …, but got …mask shape cmp(131–141)
c
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/108110)读取,并由 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 时,约束 810 才会运行。

> **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)