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)}