前兩天已經沿著舊 loom-dataflow standalone pipeline,把同一份 matmul 從 00 帶到 05:Day27 看 frontend、tensor canonicalization 和 explicit memory access,Day28 接著看 hardware mapping、reuse、broadcast 與 staged ETG。
版本範圍:Day27~Day29 的實作使用舊版 2026-07-31
loom-dataflowstandalone pipeline。完整的 00~08 artifact 可從 examples/mm/IR 瀏覽,本文用 2026-07-31 的 loom-dataflow@39d2b858 檢查腳本與 pass。
今天繼續 06~08,這條舊腳本可以輸出 ETG,卻沒有呼叫 loom-mlar 或 CP-SAT solver。它在 Step 7 直接執行 canonicalize,因此 Materialize 會使用程式碼裡的 placeholder assignment。
| Stage | 在這組舊 matmul artifact 中的位置 |
|---|---|
| 0. Helion Frontend | 00~02,前面已完成 |
| 1. Dataflow Exploration | 03~05,Step 6 再輸出 staged ETG JSON |
| 2. ETG Resolution | 舊 run_pipeline.sh 沒有執行 |
| 3. Block-Size Solve | 舊 run_pipeline.sh 沒有執行 CP-SAT |
| 4. Materialization | Step 7 直接執行 canonicalize,以 placeholder assignment 產生 06;Step 8 再產生 07 |
| TT-oriented rewrite | Step 9 將 07 改寫成 08;仍未產生 TT-Metal executable |
截至 2026-08-12,新版 loom@f65d199b monorepo 的 run_pipeline() 已串接 ETG resolution、CPMpy/CP-SAT 與 integrated materialization。這個新版流程不回頭套用到本文的舊 00~08 snapshot。
linalg.matmul 如何改寫成 loom.matmul
./run_pipeline.sh mm "[7,8,9]"
這個指令延續昨天已產生的 05_after_enumerate_broadcast.mlir。三個 step 對應
7: canonicalize
8: one_shot_bufferize
9: tt-opt
這條舊腳本的 Step 6 只輸出
examples/mm/constraint_space/staged_etg_dump.json
接著 Step 7 直接呼叫 canonicalize,沒有讀取 resolved ETG、solver log 或外部 block-size assignment。

完整 dump:Before:05_after_enumerate_broadcast.mlir|After:06_after_canonicalize.mlir
左側仍是 symbolic plan,右側已把 placeholder binding 寫進 function 名稱、allocation shape 與 view。memref.reinterpret_cast 只重新解讀同一塊底層記憶體,不會搬移資料。
canonicalize driver 的 pass 順序是
Materialize
→ Canonicalizer
→ SymbolDCE
→ BridgeToOSB
05 的 function 還使用
%tm = loom.sym @tile_m
%tn = loom.sym @tile_n
%tk = loom.sym @tile_k
%a = loom.alloc [%tm, %tk] on @L1 : memref<?x?xf16>
06 的 snapshot 已變成
func.func @matmul__...__tile_k512__tile_m64__tile_n64(...)
%a = loom.alloc [64, 512] on @L1 : memref<64x512xf16>
%b = loom.alloc [512, 64] on @L1 : memref<512x64xf16>
%c = loom.alloc [64, 64] on @L1 : memref<64x64xf16>
Materialize pass 本身支援把具體 block-size binding 寫回 IR。不過這次使用的舊 standalone driver 沒有提供外部 binding,因此走到 materialize.cpp 的 placeholder fallback。該 fallback 對三個 tile symbol 固定回傳 {64, 64, 512}。
所以 snapshot 中的數值來源是
tile_m = 64 ← placeholder
tile_n = 64 ← placeholder
tile_k = 512 ← placeholder
這三個值沒有經過 CP-SAT,也不能解讀為 solver 選出的最佳 block size。Materialize 之後確實能計算固定 allocation shape、loop bound 與 view;這些計算只是忠實套用 placeholder。
原本
ceildiv(2048, tile_m)
ceildiv(256, tile_n)
tile size 固定後,可以折疊成具體 trip count。06 的代表性 candidate 仍保留帶 attribute 的 affine.parallel,但 temporal wave 已成為常數
affine.parallel (%x) = (0) to (8) {
affine.parallel (%y) = (0) to (8) {
scf.for %wave = %c0 to %c4
}
}
這裡 4 來自 M 方向
2048 / (tile_m 64 × 8 cores) = 4 waves
這些 spatial affine.parallel 直到 07 的 LowerAffineWithAttr 才會轉成、合併為 scf.parallel。
當 %tm/%tn/%tk 的使用都被常數取代後,對應 loom.sym 就不必留在 function body。這是 Dead Code Elimination,不會改變運算結果
05 的 A tile
loom.subview %arg0[%m0, %k0] [64, 512] [1, 1]
06 會改成
%offset = affine.apply affine_map<(d0, d1) -> (d0 * 256 + d1)>(%m0, %k0)
%a = memref.reinterpret_cast %arg0
to offset: [%offset], sizes: [64, 512], strides: [256, 1]
因為原始 row-major X 的 stride 是 [256, 1],所以元素 (m0, k0) 的線性 offset 是
offset = m0 × 256 + k0
reinterpret_cast 沒有搬資料,只建立另一個 memref view。BridgeToOSB 先把 Loom-specific view 轉成標準 memref 表示,One-Shot Bufferization 才能接手。
原始輸入的 reduction dimension 是
K = 256
但 06~08 committed snapshot 的 function suffix 卻是
tile_k = 512
甚至建立
memref<64x512xf16> from memref<2048x256xf16>
memref<512x64xf16> from memref<256x256xf16>
這超過原始 K 軸範圍。原因已經可以從程式碼確定:舊 run_pipeline.sh 沒有執行 CP-SAT,Step 7 直接觸發 {64,64,512} placeholder。Constraint-space JSON 中還能看到 4096、512 等與 00 的 2048、256 不一致的 problem size,表示這組舊 dump 不能當成端到端驗證結果。
所以這篇只用它們學習 pass 形狀。tile_k=512 並非這個 2048×256 × 256×256 matmul 已驗證可安全執行的設定。之後實際做實驗時應該
2026-08-12 的新版 monorepo 快照另有 M2048_N256_K256 的 Wormhole 8×8 mesh 設定,明確指定 tile_m=256、tile_n=32、tile_k=256。該設定使用 assigned_block_size,會略過 CP-SAT,因此只能當成該版本維護的明確設定。若要取得 solver 結果,仍需移除 override 並保存新的 solver.log。

完整 dump:Before:06_after_canonicalize.mlir|After:07_after_osb.mlir
紅框顯示資料表示方式的改變:loom.bufferize_to_tensor 與 tensor SSA result 消失,linalg.fill、linalg.matmul 直接讀寫 %c_l1。因此後端能以 buffer、alias 與生命週期來分析資料。
OSB 的完整名稱是 One-Shot Bufferization。Driver 先降低帶 attribute 的 affine loop,再註冊 MLIR 標準 dialect 與 Loom operation 的 BufferizableOpInterface,接著執行:
LowerAffineWithAttr
→ OneShotBufferize
→ Canonicalizer
→ CSE
→ LowerLinalgCopyToLoomCopy
它還設定:
allowUnknownOps = false
bufferizeFunctionBoundaries = true
function boundary 使用 identity layout
allowUnknownOps = false 表示遇到沒有 bufferization model 的 operation 就應失敗,避免 tensor operation 未經處理便穿過 pipeline。
06 的 compute path 仍混合 tensor 與 memref
%a_tensor = loom.bufferize_to_tensor %a_l1[64, 512]
%b_tensor = loom.bufferize_to_tensor %b_l1[512, 64]
%next = linalg.matmul
ins(%a_tensor, %b_tensor : tensor<...>, tensor<...>)
outs(%acc : tensor<...>) -> tensor<...>
07 直接讓 linalg.matmul 使用 memref
linalg.matmul
ins(%a_l1, %b_l1 : memref<64x512xf16>, memref<512x64xf16>)
outs(%c_l1 : memref<64x64xf16>)
這表示 tensor SSA result 與 iter_args accumulator 已被改寫成 in-place buffer update。後端現在可以直接追蹤
07 由約 984 行降到約 780 行,主要原因是 tensor/memref bridge、tensor result 與冗餘 expression 被清掉。Mapping candidate 仍是 16 個,沒有在這一步收斂成 1 個。
如果要證明 candidate selection,應直接數 function、找 selection pass 或檢查 solver 輸出,不能只用檔案行數推論。

完整 dump:Before:07_after_osb.mlir|After:08_tt-opt.mlir
這一步辨識 zero-init matmul pattern,將兩個 generic linalg operation 改成單一 loom.matmul。資料搬移仍留在 IR 中;改寫結果是 backend-oriented MLIR,尚未產生可直接執行的 TT-Metal binary。
tt-opt driver 的 pass 順序是
ConvertZeroFillLinalgMatmulToLoom
→ FoldZeroFillLinalg
→ SplitBinaryScalarChain
→ Canonicalizer
固定 tile size 與 memref.reinterpret_cast 已在 06 出現,08 處理的是後端導向的 operation rewrite。
linalg.fill + linalg.matmul 變成 loom.matmul07 的概念形狀是
linalg.fill ins(%zero) outs(%c_l1)
linalg.matmul ins(%a_l1, %b_l1) outs(%c_l1)
08 變成
loom.matmul
ins(%a_l1, %b_l1
: memref<64x512xf16>, memref<512x64xf16>)
outs(%c_l1 : memref<64x64xf16>)
專用 loom.matmul 把 backend 關心的矩陣運算語意集中在一個 operation 上。後續 lowering 不必重新從任意 linalg indexing map 猜測這是否為支援的 matmul pattern。
這裡還要注意 Zero fill 被折疊的前提:這個 rewrite 針對已知的 zero-init matmul pattern。如果 accumulator 要保留既有值,或 matmul 帶有其他 fused semantics,就不能不加判斷地刪掉 fill。
代表性 compute body 可以縮寫為
%a_l1 = loom.alloc [64, 512] on @L1
%b_l1 = loom.alloc [512, 64] on @L1
%c_l1 = loom.alloc [64, 64] on @L1
loom.copy %a_dram_view, %a_l1
src_mem_space @mem_DRAM dst_mem_space @mem_L1,
area : [1, 8]
loom.copy %b_dram_view, %b_l1
src_mem_space @mem_DRAM dst_mem_space @mem_L1,
area : [8, 1]
loom.matmul ins(%a_l1, %b_l1) outs(%c_l1)
loom.copy %c_l1, %out_dram_view
src_mem_space @mem_L1 dst_mem_space @mem_DRAM,
area : [1, 1]
這裡還有一次 L1 → L1 copy,承接前面 destination specialization 所建立的 output buffer。它是否能被消除或與 writeback 合併,是後端最佳化可以繼續研究的問題。
08 已是 backend-oriented Loom IR,但從這些 snapshot 還看不到
所以比較準確的說法是 IR 已把固定 shape 的 compute 與 data movement 表達成 Loom operations。這些 snapshot 尚未證明已產生可在 Tenstorrent 裝置執行的程式。
final.mlir 使用另一組 df dialect operation
df.mat "FPU" {shape = [32, 32, 32], throughput = 128}
df.vec "SFPU" {shape = [32]}
df.spatial_dim "x", 8
df.spatial_dim "y", 8
df.memory "L1" {size = 1499136, bandwidth = 15}
df.memory "DRAM" {size = 34359738368, bandwidth = 288}
df.interconnects "horizontal_links"
df.interconnects "vertical_links"
df.interconnects "NoC"
它描述 compute resource、8×8 core grid、L1、DRAM 與 interconnect,是架構/成本模型的輸入或參考表示。它沒有 func.func @matmul,也沒有 A/B/C dataflow。
因此正確關係比較接近
matmul program IR ──────────────┐
├─→ mapping / ETG / cost analysis
hardware model (`final.mlir`) ──┘
08_tt-opt.mlir → final.mlir 不構成這份 matmul 的 lowering 關係。
| 階段 | 實際主要作用 |
|---|---|
| 00 frontend | 產生帶符號 tile 的 affine.parallel + linalg.matmul |
| 01 tensor canonicalized | 統一 destination 與 Loom tensor/memref bridge |
| 02 explicit memory access | 加入 L1 allocation、DRAM/L1 copy、semaphore |
| 03 hardware mapping | 合併硬體描述並展開 16 個 mapping candidate |
| 04 reuse analysis | 從 subview offset dependency 標出 reuse |
| 05 enumerate broadcast | 把 spatial reuse 轉成 copy area/region |
| staged ETG | 舊 Step 6 只產生 constraint-space JSON,沒有接著執行 MLAR resolution 與 CP-SAT |
| 06 materialize + canonicalize + bridge | 使用 {64,64,512} placeholder,再折疊常數、把 subview 轉成 reinterpret cast |
| 07 One-Shot Bufferization | tensor-based compute 改成 memref-based in-place compute |
| 08 TT opt | zero-fill matmul pattern 改成 loom.matmul 等 backend rewrite |
final.mlir |
獨立硬體模型,沒有接在 matmul program IR 後面 |