iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Software Development

在 AI Compiler 工程師的路上系列 第 30

Day29:Loom-dataflow:Materialize、One-Shot Bufferization 和 TT opt

  • 分享至 

  • xImage
  •  

前兩天已經沿著舊 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-dataflow standalone 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 0002,前面已完成
1. Dataflow Exploration 0305,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。這個新版流程不回頭套用到本文的舊 0008 snapshot。

本篇大綱

  • 檢查 placeholder assignment 如何進入 Materialize
  • 比較 tensor dataflow 與 memref in-place dataflow
  • 追 zero-filled linalg.matmul 如何改寫成 loom.matmul
  • 用 shape、candidate 數量與 artifact 來源檢查這組舊 pipeline

如何重現

./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。

06_after_canonicalize

https://ithelp.ithome.com.tw/upload/images/20260830/20183319neCtEiae8Y.png

完整 dump:Before:05_after_enumerate_broadcast.mlirAfter:06_after_canonicalize.mlir

左側仍是 symbolic plan,右側已把 placeholder binding 寫進 function 名稱、allocation shape 與 view。memref.reinterpret_cast 只重新解讀同一塊底層記憶體,不會搬移資料。

canonicalize driver 的 pass 順序是

Materialize
  → Canonicalizer
  → SymbolDCE
  → BridgeToOSB

1. Materialize:把 tile symbol 換成常數

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。

2. Canonicalizer:折疊已知常數

原本

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

3. SymbolDCE:刪除不再使用的符號

%tm/%tn/%tk 的使用都被常數取代後,對應 loom.sym 就不必留在 function body。這是 Dead Code Elimination,不會改變運算結果

4. BridgeToOSB:把多維 subview offset 線性化

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 才能接手。

一個不能跳過的 artifact 健全性問題

原始輸入的 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 中還能看到 4096512 等與 00 的 2048256 不一致的 problem size,表示這組舊 dump 不能當成端到端驗證結果。

所以這篇只用它們學習 pass 形狀。tile_k=512 並非這個 2048×256 × 256×256 matmul 已驗證可安全執行的設定。之後實際做實驗時應該

  1. 從 00 開始清掉舊輸出並完整重跑
  2. 改用新版 monorepo 的完整 pipeline
  3. 核對 exploration ETG、resolved ETG、solver log 與原始 M/N/K 是否使用同一組 problem size
  4. 檢查每個 materialized tile 不超出 memref 邊界,或確認已有明確 padding/boundary lowering
  5. 再做數值正確性測試。

2026-08-12 的新版 monorepo 快照另有 M2048_N256_K256 的 Wormhole 8×8 mesh 設定,明確指定 tile_m=256tile_n=32tile_k=256。該設定使用 assigned_block_size,會略過 CP-SAT,因此只能當成該版本維護的明確設定。若要取得 solver 結果,仍需移除 override 並保存新的 solver.log

07:OSB 把 tensor dataflow 轉成 memref dataflow

https://ithelp.ithome.com.tw/upload/images/20260830/20183319ffxdPaMnCy.png
完整 dump:Before:06_after_canonicalize.mlirAfter:07_after_osb.mlir

紅框顯示資料表示方式的改變:loom.bufferize_to_tensor 與 tensor SSA result 消失,linalg.filllinalg.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 到 07 最重要的差異

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。後端現在可以直接追蹤

  • 哪個 operation 讀哪塊 L1
  • 哪個 operation 寫哪塊 L1
  • buffer 是否 alias
  • buffer 的生命週期是否能重用

行數變少不代表 candidate 被淘汰

07 由約 984 行降到約 780 行,主要原因是 tensor/memref bridge、tensor result 與冗餘 expression 被清掉。Mapping candidate 仍是 16 個,沒有在這一步收斂成 1 個。

如果要證明 candidate selection,應直接數 function、找 selection pass 或檢查 solver 輸出,不能只用檔案行數推論。

08:TT opt 做了哪些具體 rewrite

https://ithelp.ithome.com.tw/upload/images/20260830/20183319yKZRfYtACb.png

完整 dump:Before:07_after_osb.mlirAfter: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.matmul

07 的概念形狀是

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。

08 仍然保留完整 data movement

代表性 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 還看不到

  • 實際 TT-Metal kernel source
  • circular buffer configuration
  • NoC command sequence
  • kernel binary
  • host-side program launch
  • 裝置上的數值正確性與效能結果

所以比較準確的說法是 IR 已把固定 shape 的 compute 與 data movement 表達成 Loom operations。這些 snapshot 尚未證明已產生可在 Tenstorrent 裝置執行的程式。

final.mlir 是獨立硬體模型

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 關係。

複習一下舊 standalone pipeline

階段 實際主要作用
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 後面

參考資料


上一篇
Day28:Loom-dataflow:hardware mapping、reuse analysis、broadcast
下一篇
Day30:複習 Tenstorrent、TT-Metal、TT-MLIR、Triton Flow、TileLoom
系列文
在 AI Compiler 工程師的路上31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言