Skip to content

编译流程导览

本页所有地址均适用于 libtpu-0.0.40-cp314 wheel 中的 libtpu.so(wheel 版本 0.0.40;运行时报告的 0.103 无法在二进制中静态验证,因此 build-id 是明确锚点:89edbbe81c5b328a958fe628a9f2207d,781,691,048 字节,未剥离符号 — 完整 C++ 符号;.text VA == file offset)。其他 wheel 版本会不同;应把每个 VA 都视为绑定到该版本。

摘要

本页跟踪一个程序 — 单个 bf16 matmul — 从框架跨过 PjRt 边界交给编译器的 StableHLO module,一直到 TPU TensorCore 发射的打包 VLIW bundle 字节。它是编译器的定向地图:一条叙事主线,命名每个 IR 层级、该层使用的 IR/dialect、该层真正重要的少数 pass,以及拥有完整细节的深入页面。先读这里;再跳入每个阶段链接到的页面。

这条下降路径不是教科书式 MLIR 级联 HLO → MHLO → tpu → LLO。在这个 shared object 内有两棵不同的 lowering tree,且只有其中一棵是 TPU device path。device path(我们的 matmul 采用的路径)是 HLO → [≈97 HLO pre-passes] → HLO → [layout / fusion / scheduling / memory] → HLO → [jellyfish per-op Emitters via LloRegionBuilder] → LLO → [bundle packer] → per-gen bundle bytes。通用 tensor IR 由数百个 C++ *Emitter class 直接 lower 到 LLO — 它从不会变成 tpu-dialect MLIR。mlir::tpu(Mosaic)dialect 和 XTile dialect 都随二进制一起发布,但位于 device path 之外:tpu 只会作为从 Pallas/Mosaic kCustomCall 导入的序列化 MLIR module 出现,而 XTile 是 XLA CPU/GPU tiled-fusion codegen dialect,matmul lowering 永远不会到达它。对这个编译器最常见的地图正是在这一点上出错;下面的逐阶段小节会标出位置。

参考框架是 XLA-on-LLVM。如果你熟悉 XLA 的 HLO optimization pipeline、layout assignment、LatencyHidingSchedulerMemorySpaceAssignment(MSA),你已经理解七个阶段中的四个 — 它们是开源 pass,以 TPU Target 运行,并与约 137 个 TPU-private xla::jellyfish::* pass 交织。真正 TPU-specific 的下降是最后两个阶段:HLO→LLO emitter wall 和按代 bundle packer。LLO 是 TPU 的低层 VLIW IR — 扁平的 LloInstruction,每个 machine op 一个,是 wire 之上的最后一层 IR。硅相关选择(哪种 MXU data-format、哪种 native vreg shape、每个 bundle 多少字节)很晚才进入,几乎都通过一个 xla::jellyfish::Target object 穿针引线,每个阶段都会查询它。

对重新实现而言,定向契约如下:

  • 七个 IR 层级以及驱动每层的单一函数入口:CompilePhase0StablehloToHlo0xf84de60)→ RunHloPasses0x1093a420)→ LayoutAssignment0x169bf440)→ LatencyHidingScheduler0x136321a0)→ RunMemorySpaceAssignment0x12fc3080)→ jellyfish *Emitter / LloRegionBuilderBundlePacker0x13b206a0)。
  • 双树拆分:device path(HLO → LLO direct emit)vs. 已打包但不在路径上的 XTile(CPU/GPU)和 Mosaic-tpu(Pallas-import)树。
  • Target 作为硅开关:data-format、native vreg shape 和 bundle width 都是从同一个 Target 参数读取的按代值,而不是分支到某个全局变量。
前门输入跨 PjRt 边界的 StableHLO MLIR bytecode(混合 CHLO/VHLO)
Wire 输出按代 VLIW bundle bytes — JF 41 B / PF 51 B / VF·GL 64 B
单体驱动器DeepseaCompilerBase::RunHloPasses @ 0x1093a420
Separate-compile 阶段phase0_stablehlo_to_hlo · phase1_hlo_opts · phase2a_tlp_lowering · phase2b_deduped_lowering · phase3_linking
硅开关xla::jellyfish::Target const& — 传入每个阶段
我们跟踪的 matmul一个 bf16 × bf16 → f32 kDot,收缩一个维度

阶段地图一览

我们的 matmul 会经过七个 IR 层级、每层使用的 dialect、该处的关键 pass,以及拥有完整叙述的页面。Tree 标记该层是否位于 TPU device path(D)上,或位于某棵已打包但不在路径上的树(X = XLA CPU/GPU XTile,M = Mosaic-tpu Pallas import)。

#IR 层级Dialect / 形式关键 pass(入口 VA)Tree归属深入页面
0前门 bytecodeStableHLO + CHLO/VHLOCompilePhase0StablehloToHlo0xf84de60DHLO 摄入
1HLO pre-passesXLA HLORunHloPasses0x1093a420);TpuHloSupportChecker0x11071480DHLO Pre-Passes · Compile Phases
2Layout / shardingXLA HLO + layoutsLayoutAssignment0x169bf440);TpuLayoutAssignment0x110ace00DLayout Assignment · Sharding Propagation
3Fusion + schedulingXLA HLO(scheduled)LatencyHidingScheduler0x136321a0);TpuInstructionFusionDLHS Core · Fusion Patterns
4Memory-space assignmentXLA HLO + memspaceRunMemorySpaceAssignment0x12fc3080DMSA Overview
5HLO → LLO emitLLO(VLIW IR)jellyfish *Emitter via LloRegionBuilderMatrixMultiplyAccumulateFunctor::operator()0x1310cd80DDot/Conv → MXU Lowering · LLO Opcode Enum
6Bundle packingvector<Bundle>BundlePacker::runOnMachineFunction0x13b206a0DLLO Bundle Packing
7Bundle bytes按代 wire wordEncoder<gen>::EncodeBundleInternalDBundle Model
Mosaic tpu importmlir::tpu MLIRcreateLowerToLLOPass0x11203ba0);通过 GetMlirModuleOpFromCustomCall0x13e327a0)导入Mtpu → LLO ODS · Mosaic Overview
XLA XTile codegenxla::xtile MLIRStablehloLowerToXtilePass0x15060560XMHLO/XTile/tpu Lowering

GOTCHA — 第 0–7 行是被跟踪 matmul 实际走过的路径。最后两行是已打包但不在路径上:普通 kDot 永远不会访问 tpu dialect 或 XTile。tpu dialect 只会出现在框架作为序列化 MLIR 交入的 Pallas/Mosaic kernel 中;XTile 是 XLA CPU/GPU backend 的 tiled-fusion codegen,只依赖 LLVM/CPU dialect set。把其中任意一个误认为 “TPU lowering of MHLO”,是阅读这个编译器时最常见的错误。


Stage 0 — 前门:StableHLO bytecode → HLO

我们的 matmul 不是以 XLA HLO 进入的。它以可移植 MLIR bytecode 进入 — 一个混有 CHLO 和 VHLO op 的 StableHLO module,由框架桥(JAX、TF/XLA bridge 或 PyTorch/XLA)序列化并跨过 PjRt 边界。dot 是一个 stablehlo.dot_general,带有 contracting-dimension numbers 和 bf16 operand element type。

编译器的第一步是一次格式跨越,不是优化。xla::CompilePhase0StablehloToHlo0xf84de60)将 bytecode 解析为内存中的 mlir::ModuleOp,运行一个有序的 MLIR pipeline 来 legalize CHLO→StableHLO→MHLO(createChloLegalize* / createStablehloLegalizeToHlo pass),遍历 MHLO module 并发射 xla::HloProto,随后再把该 proto 解析回 XLA 其余部分所使用的 xla::HloModule / HloInstruction graph。此阶段之后,我们的 stablehlo.dot_general 变成 opcode 为 kDotHloInstruction。从这里向下直到 Stage 5 的所有内容都在该 HLO graph 上操作 — MHLO/StableHLO/VHLO 只存在于此边界,以及作为 JAX-bridge wire format 存在。

深入页面拥有两侧的 bytecode 格式、CHLO/VHLO version-skew 处理,以及 proto round-trip。

HLO Ingestion · 下降路径的框架:How to Read This Book


Stage 1 — HLO pre-passes:≈97 行清理

HLO graph 现在运行长长的 pre-lowering pipeline,该 pipeline 由 xla::jellyfish::DeepseaCompilerBase::RunHloPasses0x1093a420)构造并分派 — 这个函数位于公开 C ABI export 背后,也位于 separate-compilation 流程中的 xla::CompilePhase1HloOptimizations0xf84ee00)背后。pipeline 容器是普通的 xla::HloPassPipeline;不存在 TPU-private pipeline class。大约 97 个 pass 运行在六个概念阶段中,每个 pass 都直接派生自 xla::HloPassInterface,并由 MaybeAddInvariantCheckers0x10944600)在每个 pass 之后重新验证结构(HloVerifier、scheduling-annotation 合法性、cycle detection)。

这些 pass 中的大多数会忽略我们的 matmul — 它们展开 custom-call(TpuCholeskyExpanderTpuQrExpander、RNG、gather/scatter)、分解 dynamic shape(DynamicPadder),并执行规范化。两个 pass 会直接触及它。xla::jellyfish::XPrecisionRewriter 会在 dot 携带 x6 / x9 / x128 高精度 annotation 时触发,把它拆成由较低精度 dot 组成的 2、3 或 8 步 accumulation chain;普通 bf16 dot 原样通过。规范接收 gate xla::TpuHloSupportChecker::RunImpl0x11071480,pass name tpu_hlo_support_checker)会遍历每个 computation,并通过 ShapeUtil::ValidateShapeWithOptionalLayout 验证每个 result Shape — 它从不修改 module,只拒绝 TPU backend 无法 lower 的 HLO。我们的 bf16 dot 通过该 gate。在约 97 个 pass 中,约 33 个是明确的 TPU-private(xla::jellyfish::*);其余是以 TPU Target 运行的开源 XLA。

HLO Pre-Passes · Compile Phases · HLO Pass Registry · Algebraic Simplifier


Stage 2 — Layout 与 sharding:数据落在哪里

Sharding 先运行(在 pipeline 两半之间):xla::ShardingPropagation 处理手动 annotation,或 xla::TpuAutoSharding + xla::sdy::ShardyXLA 处理 auto / Shardy 流,随后 xla::jellyfish::TpuSpmdPartitioner 执行每个 partition 的 rewrite。我们的单设备 matmul 是 Replicated,所以 sharding 对它是 no-op,但多芯片 matmul 会在这里获得自己的 collective。

然后 layout assignment 为每个数组决定物理 minor-to-major order。驱动器是开源的 xla::LayoutAssignment::RunImpl0x169bf440),并由 xla::jellyfish::TpuLayoutAssignment::RunImpl0x110ace00)针对 TPU 专门化,后者增加 TPU-specific constraint — 包括 gather/scatter 的 indices-layout constraint,以及 MXU 偏好的 tiling rule。对我们的 matmul 而言,这是一个关键决策:为 LHS、RHS 和 result 选择的 layout 决定 systolic array 是否可以不经 transpose 直接 stream operand,而 tiling assignment(TpuTilingAssignmentWindowConfigAssignment)固定 MXU emitter 稍后会循环遍历的 tile shape。此阶段是硅相关选择第一次进入的位置 — native tile geometry — 从 Target 读取。

Layout Assignment · Sharding Propagation · Auto-Sharding / SPMD


Stage 3 — Fusion 与 scheduling:工作的顺序

layout 固定后,fusion 会把 producer/consumer op 分组进 fusion computation。TPU 的主 fusion pass 是 xla::jellyfish::TpuInstructionFusion(pass name tpu_fusion),它在 “Pre main fusion” / “Main fusion” / “Post main fusion” pipeline 阶段中驱动 priority-fusion queue。如果我们的 matmul 后面跟着 elementwise bias-add 或 activation,fusion 会将它们折叠成一个 fusion node,使 emitter 能把结果保留在 vreg 中,而不是往返内存。

随后 scheduling 将 graph 线性化。规范 pass 是 xla::LatencyHidingScheduler::RunImpl0x136321a0,name latency-hiding-scheduler)— 这是其他地方也使用的同一个 XLA LHS,在这里以 TPU cost model 运行,以便把我们的 matmul 的 MXU 工作与供给和排出它的 DMA 重叠。对于 greedy list scheduling 重叠不足的情况,还存在一个带 ILP 风味的变体和一个 per-layer scheduler(LatencyHidingLayerScheduler);scheduler 阶段运行 async_scheduling HloPassPipeline(携带 LHS 本身的经字节验证的 pipeline name),位于 base memory-scheduling pass 之后。这里产生的 schedule 是很久之后的 bundle packer 消费 IR 顺序的来源 — 二者是在不同粒度上回答同一个“何时运行什么”的问题(HLO instruction order vs. VLIW slot fill)。

NOTE — macro scheduler(LHS,本阶段)和 micro scheduler(Stage 6 的 per-region greedy bundle packer)彼此独立。LHS 重排 HLO instruction,以便在整个 computation 范围内隐藏 latency;packer 在一个 region 内按 IR 顺序局部填充 VLIW slot。二者都不是 modulo scheduling — inner-loop software pipelining 是第三条独立路径。

LatencyHidingScheduler Core · LHS ILP Variant · Fusion Patterns · Fusion Cost Model


Stage 4 — Memory-space assignment:VMEM vs. HBM

已经 scheduled 的 HLO graph 仍然没有说明每个 buffer 在哪里。Memory-space assignment(MSA)把每个 value 放进 HBM、VMEM 或 CMEM,并插入 copy/prefetch,在需要之前把数据移入快速片上 scratch。TPU 驱动器是 xla::jellyfish::RunMemorySpaceAssignment0x12fc3080),它配置并运行开源引擎 xla::memory_space_assignment::MemorySpaceAssignment::Run0x1dc2e200)及其 best-fit repacker。对我们的 matmul 而言,MSA 决定 LHS 和 RHS tile 被预取到 VMEM,使 MXU 不会因 HBM latency 停顿,并根据从 Target 读取的按代预算确定 VMEM working set 大小。每个版本的默认预算和 HBM reservation policy 位于各自页面。

MSA Overview · MSA Per-Version Defaults · MSA Reservation / HBM Policy · VMEM Allocator


Stage 5 — HLO → LLO:emitter wall(以及 MHLO 不会去哪里)

这是货真价实的 TPU-specific 下降,也是最容易被误描述的阶段。scheduled、laid-out、memory-assigned 的 HLO graph 由数百个 C++ *Emitter class 直接 lower 到 LLO — TPU 的低层 VLIW IR;每个 class 拥有一个 HLO op family,并通过 xla::jellyfish::LloRegionBuilder 在 region 中构造 LLO。通用程序不存在 MHLO→tpu-dialect conversion pass;tpu MLIR dialect 只由 Pallas/Mosaic frontend 产生,并被单独导入(见下方 callout)。

我们的 bf16 matmul 通过与 convolution 共用的一条下降路径到达 systolic array:上游 pass 将 kDot rewrite 为 kConvolution,因此二者共享同一个 lowering。per-window tile-cost comparator 选择 systolic tiling,emission-strategy dispatch(GetEmitFunctorFromEmitFunctorEnum 0x130e8de0,一个 19-case switch)选择某个 MXU strategy,而 codegen 主体 xla::jellyfish::MatrixMultiplyAccumulateFunctor::operator()0x1310cd80,接受 LloRegionBuilder)生成由 LLO matmul op 组成的 tiled loop nest。matmul atom 是每个 native chunk 严格的 3 或 4 条 instruction 序列 — llo.vmatprepllo.vmatmul →(可选 pack)→ llo.vmatreskVectorMatprep* / kVectorMatmul / kVectorMatres opcode band)。data-format 选择(bf16×bf16→f32 vs. 某个 f8int8 变体)以及 MXU register-bank assignment 在这里根据 operand element type 和 Target 选出 — 这是硅相关行为第二次进入的位置,现在粒度是 instruction。融合到 matmul 上的 elementwise op 通过 scalar/vector LLO atom table lower;LLO-level allocation(LloAllocation,interval-tree live-range allocator)和 region analysis 作为此阶段的一部分运行。

NOTE — device-path HLO matmul emitter(MatrixMultiplyAccumulateFunctor::operator() 0x1310cd80,构建到 LloRegionBuilder 中)与 Mosaic-path atom emitter mlir::tpu::(anonymous namespace)::EmitMatmuls0x11241460)/ EmitLatches0x112403c0)都会产生相同的 kVectorMatprep*/kVectorMatmul/kVectorMatres band,但它们是从不同输入到达的不同入口。EmitMatmuls/EmitLatches 接受 mlir::ConversionPatternRewriter,并且只从 createLowerToLLOPass0x11203ba0)内部的 mlir::tpu::MatmulOp / MatmulAccLhsOp conversion pattern 调用 — 也就是下面不在路径上的 Mosaic tpu→LLO import,而不是我们的 bf16 HLO matmul。两条下降路径在 LLO level 汇合到同一个 LLO atom;它们不共享函数。

GOTCHA — tpu 和 XTile dialect 不在这条路径上。 通用 MHLO 永远不会 lower 到 tpu MLIR。mlir::tpu(Mosaic)dialect 只会作为序列化 MLIR module 进入,该 module 由 xla::jellyfish::mlir_utils::GetMlirModuleOpFromCustomCall0x13e327a0)从 Pallas/Mosaic kCustomCall 中提取;该 module 随后被 canonicalize、推断 vector layout(createInferVectorLayoutPass 0x132c2c20createApplyVectorLayoutPass 0x1325cda0),再由 mlir::tpu::createLowerToLLOPass0x11203ba0)lower 到 LLO — 在 LLO level 汇入我们 matmul 的流。另一路,xla::xtile dialect(StablehloLowerToXtilePass 0x15060560)是 XLA CPU/GPU tiled-fusion codegen,只依赖 LLVM/CPU dialect set;它被打包进 libtpu.so,但 jellyfish TPU lowering 从不调用它。

Dot/Conv → MXU Lowering · LLO Opcode Enum · Slot — MXU · Mosaic-import path: tpu → LLO ODS · Mosaic Overview · XTile(off-path):MHLO/XTile/tpu Lowering


Stage 6 — Bundle packing:LLO ops → VLIW slots

扁平的 LloInstruction 列表 — 我们的 matmul 现在是由 vmatprep/vmatmul/vmatres 加上其 prefetch DMA 以及任何融合 elementwise op 组成的 loop nest — 会被 BundlePacker::runOnMachineFunction0x13b206a0,LLVM-backend tpu-bundle-packer pass)打包进定宽 VLIW bundle。算法是forward greedy list scheduling:按 IR 顺序遍历 LLO op,根据 read-after-write dependency 计算每个 op 最早合法的 bundle,询问按代 SlotTracker 找到第一个剩余容量可容纳该 op 的 BundleRequirement 的 bundle,若没有合适 bundle 则追加空 bundle,然后提交。每一代都有不同的 slot grid — MXU slot 数量、vector source port、immediate slot、predicate field — 因此相同的 LLO stream 会在不同硅上打包成不同数量的 bundle。空 slot 会被盖上 kNeverExecute = 31 predicate,使缺失 slot 成为定义明确的 no-op。输出是由 typed sub-field 构成的 vector<Bundle>

LLO Bundle Packing · Bundle Modulo Scheduling · Scheduler Resource Model · Bundle-Aware Cost


Stage 7 — Bundle bytes:按代 wire word

每个 typed Bundle 都由按代 encoder 序列化为 raw bytes。这是硅发挥作用的最后位置,也是影响最大的位置:bundle width 本身是按代常量,由以 (TpuVersion, TpuSequencerType) 为键的 codec-metadata table inline 返回。

代(代号)Bundle widthCodec-metadata anchor
Jellyfish(≤ v3)41 B / 328 bitJellyfishCodecMetadata::BundleSizeBytes @ 0x1ecf7460 → 41
Pufferfish(v4)51 B / 408 bitEncoderPfTensorCore::BundleSizeBytes @ 0x1d227740 → 0x33
Viperfish(TPU v5 / v5e / v5p)64 B / 512 bitViperfishCodecMetadata::BundleSizeBytes @ 0x1ee71320 → 64
Ghostlite(TPU v6 lite / v6e)64 B / 512 bitGhostliteCodecMetadata::BundleSizeBytes @ 0x1eeb7640 → 0x40

encoder 本身不仅在宽度上不同,在机制上也分叉。Jellyfish 是 direct-pack encoder:EncoderJf::EncodeBundleInternalshl/and/or 算术构造一个 53 字节 scratch struct,并剥掉前 12 字节(output byte N == struct byte 0x0C+N)。Pufferfish 以及每个 V5+ generation 则会 memset 一个零 buffer,并用共享 bit-packing primitive BitCopy(dst, dst_bit, src, src_bit, nbits)0x1fa0a900)写入每个字段,因此字段的绝对 bundle bit 就是其 BitCopy 参数。同一个 64-byte 宽度由 Viperfish、Ghostlite 和 6acc60406(三个 V5+ generation)共享,但 slot bit layout 不同(Ghostlite 将 opcode 从 7 bit 扩到 8 bit,并把 scalar/sequencer region 后移 +3 bit)。我们的 matmul 的 vmatmul op 会落在 VectorExtended/MXU slot 中,其精确 bit 位置属于这些按代 map 之一。

此阶段之后,程序就是一串准备供 TensorCore fetch 和 issue 的 bundle bytes — 下降完成。

Bundle Model Overview · Jellyfish 41-B Bundle · Pufferfish 51-B Bundle · Viperfish 64-B Bundle · Ghostlite Bundle · MC-Emitter · 代号词汇表:Codename Cheat-Sheet


从这里阅读本书其余部分

上面的每个阶段都是一篇深入页面的一段概述;本节是通往完整算法的索引。

如果你想了解…从这里开始
精确的 ≈97-pass 顺序和逐 pass rewriteHLO Pre-Passes
阶段分类(phaseN_*、monolithic vs. separate compile)Compile Phases
layout 如何选择,以及 MXU tiling constraintLayout Assignment
scheduler 定价所用的 latency cost modelLatencyHidingScheduler Core · Cost Overview
buffer 位于何处,以及 prefetch 如何插入MSA Overview
完整 matmul → MXU emission 以及 19 种 strategyDot/Conv → MXU Lowering
LLO opcode spaceLLO Opcode Enum
Mosaic/Pallas tpu-dialect import pathtpu → LLO ODS · Mosaic Overview
bit-exact bundle wire formatBundle Model Overview 以及按代 bundle 页面
“Jellyfish / Pufferfish / Viperfish / Ghostlite” 映射到什么Codename Cheat-Sheet

交叉引用