Download code/models/tt_dit/utils/agmm_rules.py from sunandbean/diffrhythm-1.2-p100a: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/models/tt_dit/utils/agmm_rules.py
- Command line
-
hf download hf://sunandbean/diffrhythm-1.2-p100a/code/models/tt_dit/utils/agmm_rules.py
-
curl -L -o agmm_rules.py https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/models/tt_dit/utils/agmm_rules.py
10.8 kB
| # SPDX-FileCopyrightText: © 2026 Tenstorrent USA, Inc. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """Rule-based layout and blocking for `all_gather_minimal_matmul_async` (the "v3 rules"). | |
| Fitted against ~4,600 block-sweep measurements on 4x8 Blackhole Galaxy (three H3 AGMM | |
| projection shapes, K in {5376, 7168}, N in {1344, 5376, 7168}, M from 320 to 15,356) and | |
| validated blind on two rounds of 20 stratified-random tile-unaligned Ms each: 86/89 blind | |
| shapes within 5% of the swept optimum (round 1: 28/29, round 2: 58/60), ~1% mean penalty. | |
| Sweep driver: `models/tt_dit/utils/sweep_mm_block_sizes.py`; per-shape results table in the | |
| "AGMM Block Geometry" write-up. | |
| The rules, in the order they are applied: | |
| Layout (`pick_layout`) | |
| score(layout) = ceil(M_tiles / M_axis_cores) * ceil(N_tiles / N_axis_cores) | |
| - M > N: the op auto-transposes (`transpose_core_grid = M > N`, no override exists), so the | |
| only legal worker grid keeps the bottom device row free for the fabric muxes: (x, y-1). | |
| - Tiny M with large N (M_tiles <= 20, N_tiles >= 100): the ring gather dominates and hides | |
| compute imbalance; the full-width (x, y-1) grid wins even when its score is worse. | |
| - Otherwise strict argmin over the two non-transposed grids -- (x-1, y) (muxes in the free | |
| column) vs (x, y-1) (muxes in the free row) -- with a tie-break to full-width (x, y-1) | |
| when within 2% (15% for fused-SwiGLU shapes, whose narrow-grid N split consistently | |
| underperforms its score). | |
| Blocking (`pick_blocks`): fewest, most balanced blocks per core that fit L1. | |
| - K_block: the largest divisor of K_tiles_per_device in [6, 8] (a hard op constraint is | |
| that K_block divides K_tiles_per_device; 6-8 amortizes reload without starving L1). | |
| - N_block: split count j ascending; within j the largest even n <= min(per_core_N, 16) | |
| whose last block is at least n/2 (a 16+3-style degenerate tail loses 20-35%). Odd n only | |
| when nothing even qualifies, and never for fused SwiGLU (gate/up tile pairs interleave | |
| along N; a block must not split a pair). | |
| - M_block: split count i ascending (a single block only when per_core_M <= 6); | |
| m = ceil(per_core_M / i), with odd non-divisors rounded up to even when the split count | |
| is preserved and total padding stays <= 1.25x. Accept m <= 10 that fits L1 and meets the | |
| demand -- at least 6, relaxed to 3 when one N block covers all of per_core_N (the | |
| L1-tight full-strip case). A skinny m under a fat n costs up to 37% on tall shapes. | |
| - 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 `MinimalMatmulConfig`. | |
| """ | |
| from __future__ import annotations | |
| TILE = 32 | |
| # Block-size candidate bounds, matching the sweep that produced the fit. | |
| MN_BLOCK_MIN, MN_BLOCK_MAX = 2, 16 | |
| K_BLOCK_MIN = 2 | |
| # L1 circular-buffer budget used to pre-filter block combos (KB). Blackhole usable L1 is | |
| # ~1464 KB; the margin covers kernel/firmware overhead. The rules are only validated on | |
| # Blackhole -- callers on other archs should stay on their legacy config path. | |
| BLACKHOLE_L1_BUDGET_KB = 1400 | |
| def estimate_l1_kb(m_blk: int, k_blk: int, n_blk: int, use_addcmul: bool = False) -> int: | |
| """Estimate L1 circular-buffer footprint in KB for a block config. | |
| 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) | |
| Fused addcmul adds: M * N tiles (ternary_a, bf16) + N tiles (ternary_c, bf16). | |
| """ | |
| bf16_kb = 2 | |
| f32_kb = 4 | |
| kb = ( | |
| 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 | |
| ) | |
| if use_addcmul: | |
| kb += m_blk * n_blk * bf16_kb + n_blk * bf16_kb | |
| return kb | |
| def get_k_block_candidates(k_per_device_tiles: int) -> list[int]: | |
| """Divisors of K_tiles_per_device (hard op constraint), floored at K_BLOCK_MIN.""" | |
| return sorted(d for d in range(K_BLOCK_MIN, k_per_device_tiles + 1) if k_per_device_tiles % d == 0) | |
| def get_mn_block_candidates(per_core_tiles: int) -> list[int]: | |
| """Evens in [MN_BLOCK_MIN, MN_BLOCK_MAX] union divisors of the per-core tile count.""" | |
| evens = set(range(MN_BLOCK_MIN, MN_BLOCK_MAX + 1, 2)) | |
| divisors = set(d for d in range(MN_BLOCK_MIN, MN_BLOCK_MAX + 1) if per_core_tiles % d == 0) | |
| return sorted(evens | divisors) | |
| def pick_subblock(m_block: int, n_block: int, fp32_dest: bool = True) -> tuple[int, int]: | |
| """Best (sub_h, sub_w) with sub_h | m_block, sub_w | n_block, sub_h * sub_w <= DEST capacity. | |
| With fp32 dest the DEST register file holds 4 tiles; 2x2 is preferred among h*w == 4 | |
| candidates (better math-LLK tile reuse than 4x1 / 1x4). | |
| """ | |
| max_dest_volume = 4 if fp32_dest else 8 | |
| if fp32_dest and 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, max_dest_volume) + 1): | |
| if m_block % h: | |
| continue | |
| for w in range(1, min(n_block, max_dest_volume) + 1): | |
| if n_block % w: | |
| continue | |
| if h * w <= max_dest_volume and h * w > best_product: | |
| best, best_product = (h, w), h * w | |
| return best | |
| def _ceil(a: int, b: int) -> int: | |
| return -(-a // b) | |
| def pick_layout( | |
| M: int, K: int, N: int, full_grid: tuple[int, int] = (12, 10), fuse_swiglu: bool = False | |
| ) -> tuple[tuple[int, int], bool]: | |
| """Pick the AGMM worker grid for the shape. Returns ((grid_x, grid_y), transposed). | |
| `full_grid` is the device's full worker grid; the op needs either the last column | |
| (non-transposed muxes) or the last row (transposed muxes, and the full-width | |
| non-transposed variant) kept free. | |
| """ | |
| gx, gy = full_grid | |
| m_tiles, n_tiles = _ceil(M, TILE), _ceil(N, TILE) | |
| full_width = (gx, gy - 1) # muxes on the free bottom row | |
| narrow = (gx - 1, gy) # muxes in the free last column | |
| if M > N: | |
| return full_width, True # the op auto-transposes; M runs over grid x | |
| if m_tiles <= 20 and n_tiles >= 100: | |
| return full_width, False # gather-latency regime | |
| score_narrow = _ceil(m_tiles, narrow[1]) * _ceil(n_tiles, narrow[0]) | |
| score_wide = _ceil(m_tiles, full_width[1]) * _ceil(n_tiles, full_width[0]) | |
| tie = 1.15 if fuse_swiglu else 1.02 | |
| if score_wide <= tie * score_narrow: | |
| return full_width, False | |
| return narrow, False | |
| def _n_candidates(per_core_n: int, fuse_swiglu: bool) -> list[int]: | |
| out = [] | |
| for j in range(1, per_core_n + 1): | |
| group = [ | |
| n | |
| for n in range(2, min(per_core_n, MN_BLOCK_MAX) + 1) | |
| if _ceil(per_core_n, n) == j | |
| and (j == 1 or per_core_n - (j - 1) * n >= _ceil(n, 2)) | |
| and not (fuse_swiglu and n % 2) | |
| ] | |
| group.sort(key=lambda n: (n % 2, -n)) # evens first (descending), then odds | |
| out.extend(group) | |
| return out | |
| def _m_search(per_core_m, k_blk, n_blk, m_univ, demand, use_addcmul, l1_budget_kb): | |
| if per_core_m <= 2: | |
| return 2 if estimate_l1_kb(2, k_blk, n_blk, use_addcmul) <= l1_budget_kb else None | |
| i = 1 if per_core_m <= 6 else 2 | |
| while i <= per_core_m: | |
| m = max(2, _ceil(per_core_m, i)) | |
| if ( | |
| m % 2 | |
| and per_core_m % m != 0 | |
| and m + 1 <= MN_BLOCK_MAX | |
| and _ceil(per_core_m, m + 1) == i | |
| and _ceil(per_core_m, m + 1) * (m + 1) <= 1.25 * per_core_m | |
| ): | |
| m += 1 | |
| if m <= 10 and m in m_univ and estimate_l1_kb(m, k_blk, n_blk, use_addcmul) <= l1_budget_kb: | |
| return m if m >= demand else None | |
| if m not in m_univ or m > 10: | |
| for d in sorted((d for d in m_univ if per_core_m % d == 0 and 2 <= d <= 10), reverse=True): | |
| if d >= max(demand, 6) and estimate_l1_kb(d, k_blk, n_blk, use_addcmul) <= l1_budget_kb: | |
| return d | |
| i += 1 | |
| return None | |
| def pick_blocks( | |
| per_core_m: int, | |
| per_core_n: int, | |
| k_per_device_tiles: int, | |
| fuse_swiglu: bool = False, | |
| use_addcmul: bool = False, | |
| l1_budget_kb: int = BLACKHOLE_L1_BUDGET_KB, | |
| ) -> tuple[int, int, int, int, int] | None: | |
| """Pick (M_block, K_block, N_block, sub_h, sub_w) for the per-core work, or None.""" | |
| k_cands = [d for d in get_k_block_candidates(k_per_device_tiles) if 6 <= d <= 8] | |
| if not k_cands: | |
| return None # pathological K; let the caller keep its legacy fallback | |
| k_blk = max(k_cands) | |
| m_univ = set(get_mn_block_candidates(per_core_m)) | |
| n_univ = set(get_mn_block_candidates(per_core_n)) | |
| n_cands = [n for n in _n_candidates(per_core_n, fuse_swiglu) if n in n_univ] | |
| for relax in (False, True): | |
| for n_blk in n_cands: | |
| demand = 2 if relax else min(3 if n_blk >= per_core_n else 6, per_core_m) | |
| m_blk = _m_search(per_core_m, k_blk, n_blk, m_univ, demand, use_addcmul, l1_budget_kb) | |
| if m_blk: | |
| sub_h, sub_w = pick_subblock(m_blk, n_blk) | |
| return (m_blk, k_blk, n_blk, sub_h, sub_w) | |
| return None | |
| def pick_v3( | |
| M: int, | |
| K: int, | |
| N: int, | |
| cluster_size: int, | |
| full_grid: tuple[int, int] = (12, 10), | |
| fuse_swiglu: bool = False, | |
| use_addcmul: bool = False, | |
| l1_budget_kb: int = BLACKHOLE_L1_BUDGET_KB, | |
| ) -> dict | None: | |
| """Resolve layout + blocking for an AGMM shape. Returns None when the rules don't apply | |
| (K not tile-aligned, K_tiles not divisible by the ring size, or no K_block in [6, 8]). | |
| Returns {"core_grid": (x, y), "transposed": bool, "blocks": (m, k, n), "subblock": (h, w)}. | |
| """ | |
| if K % TILE: | |
| return None | |
| k_tiles = K // TILE | |
| if k_tiles % cluster_size: | |
| return None | |
| (gx, gy), transposed = pick_layout(M, K, N, full_grid, fuse_swiglu) | |
| m_tiles, n_tiles = _ceil(M, TILE), _ceil(N, TILE) | |
| per_core_m = _ceil(m_tiles, gx if transposed else gy) | |
| per_core_n = _ceil(n_tiles, gy if transposed else gx) | |
| blocks = pick_blocks(per_core_m, per_core_n, k_tiles // cluster_size, fuse_swiglu, use_addcmul, l1_budget_kb) | |
| if blocks is None: | |
| return None | |
| m_blk, k_blk, n_blk, sub_h, sub_w = blocks | |
| return { | |
| "core_grid": (gx, gy), | |
| "transposed": transposed, | |
| "blocks": (m_blk, k_blk, n_blk), | |
| "subblock": (sub_h, sub_w), | |
| } | |