昨天的 02_explicit_memory_access.mlir 已經有 DRAM、L1 和 loom.copy,但所有 copy 的範圍仍是 [1, 1],也不知道 tile 要在哪一顆 core 執行。
版本範圍:今天使用舊
loom-dataflowstandalone pipeline。完整的00~08artifact 可從 examples/mm/IR 瀏覽,本文以 2026-07-31 的 loom-dataflow@39d2b858 核對 pass 行為。
今天內容主要位於 Stage 1:Dataflow Exploration
| Stage | 在這份 matmul 範例中的位置 |
|---|---|
| 0. Helion Frontend | 00~02 已完成 frontend、tensor canonicalization 與 explicit memory access |
| 1. Dataflow Exploration | 今天的 03 hardware mapping → 04 reuse analysis → 05 broadcast → staged ETG |
| 2. ETG Resolution | 完整架構中的下一階段;本篇舊 run_pipeline.sh 沒有執行 |
| 3. Block-Size Solve | 完整架構中的 CP-SAT 階段;本篇舊 run_pipeline.sh 沒有執行 |
| 4. Materialization | Day29 的舊 snapshot 使用 placeholder assignment;新版 monorepo 才能接 solver binding |
| TT-oriented rewrite | Day29 觀察舊 Step 9 的 tt-opt;完整 TT backend lowering 屬於另一條後續路徑 |
在完整架構中,Stage 2、3 會產生 resolved ETG 與 solver artifact,不會各自對應一份新的 program MLIR。本篇使用的舊 standalone 流程停在 staged ETG。
最後再看舊 pipeline 如何產生 staged ETG constraint JSON,以及新版 ecolab-nus/loom monorepo 如何串接 ETG resolution、block-size search 與 materialization。
這裡執行的是舊 standalone run_pipeline.sh。Step 6 只產生 staged ETG JSON,不會接著執行 MLAR resolution 或 CP-SAT。新版端到端流程可參考 2026-08-12 的 loom@f65d199b
run_pipeline()。
./run_pipeline.sh mm "[3,4,5,6]"
這個指令假設昨天的 02 artifact 已經存在,Step 3 還需要硬體規格
build/tool/loom-opt/single_stage/enumerate_hw_mapping \
--input examples/mm/IR/02_explicit_memory_access.mlir \
--hw_spec ../loom-mlar/tests/2d_mesh/2d_mesh_torus.mlir
Step 6 不會產生另一份 MLIR,而是把 05 轉成 constraint-space JSON,稍後會再說明。
以下三張對照圖直接摘錄同一個代表性 candidate 的真實 dump。圖中保留原本的 SSA 名稱、型別、attribute 與 operation 語法;... 只表示省略未顯示的行。

完整 dump:Before:02_explicit_memory_access.mlir|After:03_after_hardware_mapping.mlir
左側的 02 只有 logical tile index,右側的 03 加入 8×8 硬體 dimension,將 loop 拆成多層 spatial mapping,並用 temporal wave 補足 core mesh 一次容納不下的 tile,紅框是這一步真正新增的結構,matmul 的 logical shape 並沒有改變。
開頭合併了 ADL 硬體描述
adl.spatial_dim "dim_x", 8
adl.spatial_dim "dim_y", 8
adl.arch.scale "arch_mesh", [%dim_x, %dim_y]
adl.memory.array "mem_array_L1", [%dim_x, %dim_y]
adl.processor.dmover @proc_dram_l1_noc0
adl.processor.dmover @proc_dram_l1_noc1
IR 裡附上規格只是第一步,mapping pass 還需要知道
02 只有一個 @matmul,03 的 snapshot 則有 16 個 matmul__... function。每個 function 是 mapping candidate,還不是已經挑出來的最佳解。
像是
matmul__x8_y1y8__d0i0_d1i0_d2i1__f01
pass 會把追蹤資訊寫進 function suffix,方便區分候選
affine.parallel (%x) = (0) to (8) {
affine.parallel (%y0) = (0) to (1) {
affine.parallel (%y1) = (0) to (8) {
...
} {loom.iter_type = #loom.iter_type<spatial>,
loom.logical_level = 1 : i64,
loom.physical_dim = @dim_y}
} {loom.iter_type = #loom.iter_type<spatial>,
loom.logical_level = 0 : i64,
loom.physical_dim = @dim_y}
} {loom.iter_type = #loom.iter_type<spatial>,
loom.logical_level = 0 : i64,
loom.physical_dim = @dim_x}
這個 candidate 把 logical iteration 拆到 x=8、y=1×8。大小為 1 的 level 看起來沒有增加平行度,但保留了 mapping 的階層資訊。
硬體只有 8 個 x 座標,但 M 方向可能有超過 8 個 tile。因此 pass 另外產生 temporal loop,再重建原本的 logical index
%m_wave_count = arith.ceildivui %m_tiles, %c8
scf.for %wave = %c0 to %m_wave_count step %c1 {
%mi = affine.apply affine_map<(d0, d1) -> (d0 + d1 * 8)>(%x, %wave)
}
也就是
logical_m_tile = core_x + wave * 8
同一顆 core 在不同 wave 處理不同 output tile,這就是 spatial mapping 與 temporal tiling 同時存在的原因。

完整 dump:Before:03_after_hardware_mapping.mlir|After:04_after_reuse_analyzation.mlir
這一步幾乎不動 loop,只更新 loom.subview 的 reuse 屬性。A、B 的 offset 各自少依賴一個空間方向,因此出現 spatial reuse;代表性 candidate 中,A、B 也可跨 temporal wave 重用,C 則會隨 wave 寫到不同 output tile。
03 與 04 的 loop 結構、candidate 數量幾乎不變。主要差異在 loom.subview 的 reuse attribute
reuse : [seq = false, spat = true, temp = true]
這裡會直接分析 subview offset 是否依賴周圍 loop 的 induction variable,再決定能不能 reuse,不會看到 matmul 就硬套同一條規則。
A 的 subview 可寫成
A[m_tile * tm, k_tile * tk]
它依賴 m_tile、k_tile,但不依賴 n_tile。所以當不同 core 只改變 N 座標時,它們仍然需要同一塊 A
C[m, n0] 需要 A[m, k]
C[m, n1] 也需要 A[m, k]
如果 N 被映射到某個 spatial dimension,A 就沿該方向有 spatial reuse。
B 的 subview 是
B[k_tile * tk, n_tile * tn]
它不依賴 m_tile,所以不同 M 座標的 core 可以共用 B。A 與 B 的 broadcast 方向因此通常互相垂直。
假設 M tile grid 大於 dim_x=8,同一顆 core 會跑多個 temporal wave。若某個 operand 的 offset 不依賴該 wave,它就有 temporal reuse。
這裡要小心:temp = true 只表示分析發現「資料相同」,不保證程式已經把資料留在 L1,也不保證實際效能一定改善。是否利用這個機會,要看後面的 data movement 與容量 constraint。
代表性 candidate 的 output subview 是
reuse : [seq = false, spat = true, temp = false]
每個 temporal wave 通常對應不同 output tile,因此不能把前一個 wave 的 C 當成下一個 wave 的 C。這正好說明 reuse attribute 是逐一分析 memory access,不是整個 kernel 共用一組答案。

完整 dump:Before:04_after_reuse_analyzation.mlir|After:05_after_enumerate_broadcast.mlir
reuse analysis 只回答「資料相同」,broadcast enumeration 才把這個機會寫成資料搬移範圍。A 從單點 copy 擴成 [1, 8],B 擴成 [8, 1];C 仍由自己的 owner 寫回,因此維持 [1, 1]。
05 的代表性 candidate 名稱多了
__dim_y_level1_bc8_dim_x_level0_bc8_n
目前實作不會列舉所有 broadcast dimension 的子集合。它會保留 no-broadcast 選項,並在找到可 broadcast 的 dimension 時,建立啟用所有可用 dimension 的最佳候選,以免產生組合爆炸。
loom.copy %a_src, %a_l1
src_mem_space @mem_DRAM
dst_mem_space @mem_L1,
area : [1, 8]
region : (UL : [%x, %c0], LR : [%x, %c7])
可把它讀成固定 x,y 從 0 到 7,總接收區域為 1 × 8
loom.copy %b_src, %b_l1
src_mem_space @mem_DRAM
dst_mem_space @mem_L1,
area : [8, 1]
region : (UL : [%c0, %y], LR : [%c7, %y])
這次固定 y,x 從 0 到 7。A/B broadcast 方向互換,正好對應前面從 subview dependency 得到的 reuse。
area : [8, 1] 描述區域大小。UL / LR 描述這個區域在 mesh 上的左上與右下座標。相同 area 可以出現在不同位置,因此 compiler 需要兩者才能表示完整的搬移計畫。
Output writeback 仍是單點
area : [1, 1]
region : (UL : [%x, %y], LR : [%x, %y])
因為每個 C tile 有自己的 owner,最後寫回 DRAM 不需要送給整排 core。
staged_etg 讀取 05 與硬體規格,產生
examples/mm/constraint_space/staged_etg_dump.json
其中一個 stage 會把運算與搬移表示成可估成本的 function
{
"name": "matmul_f16",
"symbols": ["K", "M", "N"]
}
兩個輸入搬移則會帶上 broadcast coefficient
{
"name": "dram_to_l1_bcst",
"symbols": ["M", "N", "bcst_x", "bcst_y"]
}
這份 JSON 的角色是 constraint/ETG 輸入,它把 IR 中的
loop trip count
compute stage
memory stage
symbolic tile size
broadcast coefficient
整理成後續 architecture model 與 solver 能消化的結構。在 2026-08-12 的新版快照中,完整 solver 位於 ecolab-nus/loom monorepo 的 loom/solver/,loom/pipeline.py 會依序執行
loom-dataflow exploration
→ p01_exploration_etg.json
→ loom-mlar 解析硬體效能模型
→ p02_resolved_etg.json
→ loom.solver:CPMpy + OR-Tools CP-SAT
→ block-size assignments
→ loom-dataflow materialization
loom.solver 會把 resolved ETG 裡的 symbol domain、hard constraint 與 timing objective 建成 CPMpy model,再交給 CP-SAT solver 求解。它尋找的是可行且成本較低的 block-size assignment。
explored MLIR + exploration ETG
→ loom-mlar resolution
→ resolved ETG
→ CPMpy/CP-SAT solver
→ block-size assignments
→ integrated materialization
→ bufferized Loom MLIR
新版 TileLoom 整合 API 會把 solver 產生的 block-size assignments 交給 Materialize。要驗證一次可追溯的求解與 materialization,可以從 ecolab-nus/loom 的 loom/pipeline.py 執行,並核對 exploration ETG、resolved ETG、solver log 與最後寫回 IR 的 block sizes。這段是新版流程補充,不是本篇舊 run_pipeline.sh 的執行結果。
讀 dump 時可以依序問
core_id + wave × core_count 是否重建正確 logical tile index?[1, 1]?三個階段的因果關係是
hardware mapping
決定 logical loop 落在哪些 physical dimension
↓
reuse analysis
從 subview offset dependency 找出相同資料
↓
broadcast enumeration
把 reuse 轉成具體 area 與 region
↓
staged ETG
產生 constraint model
↓
MLAR resolution → CP-SAT 求解 → integrated materialization
明天會來看 materialization、One-Shot Bufferization 和 TT-specific optimization,並分清楚固定 tile size、tensor 轉 memref 與 linalg.matmul 轉 loom.matmul 各在哪一步發生。