File size: 6,794 Bytes
629b123 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
"""Rule-based blocking for `minimal_matmul_strided_reduce_scatter_async` (the "v2.3 rules").
Fitted against the MMRS block-sweep campaign on 4x8 Blackhole Galaxy (five ff2 shape families:
H3 3584x5376, Wan2.2 3456x5120, LTX 4096x4096, Flux 2304x6144 and 3072x6144; M from ~1,000 to
~15,600) and validated blind on pre-registered predictions: v2 scored 35/40 on its confirmation
round, and the v2.3 short-N amendment below scored a further blind round of stratified Ms
straddling its branch boundary. Retro-scored across every measured shape: 77/79 within 5% of the
swept optimum, ~1.6% mean penalty. Sweep driver: `models/tt_dit/utils/sweep_mm_block_sizes.py`
(use case `mmrs`); per-shape results in the "MMRS Block Geometry" write-up.
The rules:
Grid: always 12x8 for the matmul, with the reduce-scatter on the two rows above. 12x8 won every
full three-grid comparison (by 2-13% over 12x7, 5-25% over 12x9, the 12x9 deficit growing with
M): the optimum trades matmul cores against RS bandwidth and is interior.
Per-core tiles are orientation-aware: the minimal matmul transposes its worker grid only when
M > N, so pcM = ceil(M_tiles / 12), pcN = ceil(N_tiles / 8) when M > N, and the 8/12 roles swap
otherwise. (Fitting without this produced fake "family conflicts".)
M_block = min(6, ceil(pcM / 2)), rounded up to even (floor 2) -- balanced halves, always >= 2
blocks per core so the windowed L1 handoff engages.
K_block = the smallest divisor of K_tiles keeping the K-chunk count (K_tiles / K_block) <= 28.
K is already per-device (row-parallel), so there is no ring-divisibility constraint, unlike
AGMM; too many chunks costs sync overhead, too-large K_block starves pipelining. This is what
separates LTX (128 K-tiles -> 8) from Wan/H3 (-> 4) and Flux-2304 (72 -> 3).
N_block = 8, except in the short-N regime (N_tiles <= 128, which separates LTX 4096x4096 from
Wan/H3/Flux at 160/168/192 without naming the family): there N_block = 6 while pcM < 24, and
N_block = 16 (one block covering the core's width) above, walking K_block down its divisor
ladder until the combo fits the L1 budget -- at K_block = 8 the wide-N CBs are L1-illegal.
Subblock: 2x2 when both blocks are even, else the largest h*w <= 4 dividing both (fp32 dest
accumulation halves the DEST register file to 4 tiles).
Everything here is pure Python with no ttnn/torch imports so it can be unit-tested host-only;
`utils/matmul.py` wraps the result into a `FusedMMRSConfig`.
"""
from __future__ import annotations
TILE = 32
# The matmul grid the rules are fitted for, inside a 12x10 Blackhole worker grid; the
# reduce-scatter occupies the (12x10 - 12x8) zone above it.
MM_GRID = (12, 8)
FULL_GRID = (12, 10)
# Largest acceptable K-chunk count (K_tiles / K_block) before ring-sync overhead dominates.
MAX_K_CHUNKS = 28
# K_block candidate bounds, matching the sweep that produced the fit.
K_BLOCK_MIN, K_BLOCK_MAX = 2, 16
# Boundary of the short-N regime's wide-N branch, in per-core M tiles. Swept shapes at
# pcM = 22/23 prefer N_block 6 and pcM = 25 prefers wide N; the crossover region is flat
# (the two are within ~1.5% of each other there), so the exact threshold is not knife-edge.
WIDE_N_PCM = 24
# L1 circular-buffer budget used to gate the wide-N branch (KB). Blackhole usable L1 is
# ~1464 KB; the margin covers kernel/firmware overhead.
BLACKHOLE_L1_BUDGET_KB = 1400
def estimate_l1_kb(m_blk: int, k_blk: int, n_blk: int) -> int:
"""Estimate L1 circular-buffer footprint in KB for a block config (fused-addcmul epilogue).
Mirrors minimal_matmul_program_factory.cpp CB allocation:
c_0 (in0): 2 * M * K tiles (double-buffered, bf16 = 2 KB/tile)
c_1 (in1): 2 * K * N tiles (double-buffered, bf16)
c_2 (out): 2 * M * N tiles (double-buffered, bf16)
c_3 (interm): M * N tiles (single-buffered, f32 = 4 KB/tile)
c_4 (bias): N tiles (single-buffered, bf16)
plus the fused addcmul's M * N tiles (ternary_a, bf16) + N tiles (ternary_c, bf16).
"""
bf16_kb = 2
f32_kb = 4
return (
2 * m_blk * k_blk * bf16_kb
+ 2 * k_blk * n_blk * bf16_kb
+ 2 * m_blk * n_blk * bf16_kb
+ m_blk * n_blk * f32_kb
+ n_blk * bf16_kb
+ m_blk * n_blk * bf16_kb
+ n_blk * bf16_kb
)
def _ceil(a: int, b: int) -> int:
return -(-a // b)
def _k_divisors(k_tiles: int) -> list[int]:
return sorted(d for d in range(K_BLOCK_MIN, K_BLOCK_MAX + 1) if k_tiles % d == 0)
def pick_subblock(m_block: int, n_block: int) -> tuple[int, int]:
"""Best (sub_h, sub_w) with sub_h | m_block, sub_w | n_block, sub_h * sub_w <= 4 (fp32 dest)."""
if m_block % 2 == 0 and n_block % 2 == 0:
return (2, 2)
best, best_product = (1, 1), 1
for h in range(1, min(m_block, 4) + 1):
if m_block % h:
continue
for w in range(1, min(n_block, 4) + 1):
if n_block % w:
continue
if h * w <= 4 and h * w > best_product:
best, best_product = (h, w), h * w
return best
def pick_v23(
M: int,
K: int,
N: int,
full_grid: tuple[int, int] = FULL_GRID,
l1_budget_kb: int = BLACKHOLE_L1_BUDGET_KB,
) -> dict | None:
"""Resolve the fused-MMRS blocking for a shape. Returns None when the rules don't apply
(K or N not tile-aligned, K_tiles with no divisor in [2, 16], or a device grid other than
the 12x10 the fit was swept on).
Returns {"mm_grid": (12, 8), "blocks": (m, k, n), "subblock": (h, w)}.
"""
if full_grid != FULL_GRID:
return None
if K % TILE or N % TILE:
return None
m_tiles, k_tiles, n_tiles = _ceil(M, TILE), K // TILE, N // TILE
gx, gy = MM_GRID
if M > N:
pc_m = _ceil(m_tiles, gx)
else:
pc_m = _ceil(m_tiles, gy)
m_blk = max(2, min(6, _ceil(pc_m, 2)))
if m_blk % 2:
m_blk = min(6, m_blk + 1)
k_divs = _k_divisors(k_tiles)
if not k_divs:
return None # pathological K; let the caller keep its legacy fallback
k_blk = next((d for d in k_divs if k_tiles // d <= MAX_K_CHUNKS), k_divs[-1])
if n_tiles <= 128:
# Short-N regime (LTX-class shapes).
if pc_m < WIDE_N_PCM:
n_blk = 6
else:
n_blk = 16
while estimate_l1_kb(m_blk, k_blk, n_blk) > l1_budget_kb:
lower = [d for d in k_divs if d < k_blk]
if not lower:
break
k_blk = lower[-1]
else:
n_blk = 8
sub_h, sub_w = pick_subblock(m_blk, n_blk)
return {"mm_grid": MM_GRID, "blocks": (m_blk, k_blk, n_blk), "subblock": (sub_h, sub_w)}
|