kernels-bot commited on
Commit
4a5c820
·
verified ·
1 Parent(s): 64a16f9

Uploaded using `kernel-builder`.

Browse files
build/torch-rocm/__init__.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from .matmul import matmul_2d
17
+ from .batched import matmul_batched
18
+ from .grouped import matmul_grouped
19
+ from .moe import moe_fused_batched, moe_fused_grouped
20
+ from . import backward
21
+ from .formats import get_supported_act_fns
22
+ from .norm import get_supported_norms, rms_norm_rows
23
+ from .swizzle import swizzle_mx_scales, unswizzle_mx_scales
24
+ from .quant import mxfp4_act_quant, mxfp8_act_quant, nvfp4_act_quant
25
+
26
+ __all__ = [
27
+ # the three GEMM dispatchers (the weight format is read off the tensors; `activation_format`
28
+ # names the activations', the gate|up fusion rides as kwargs)
29
+ "matmul_2d",
30
+ "matmul_batched",
31
+ "matmul_grouped",
32
+ # the fused MoE forwards over them
33
+ "moe_fused_batched",
34
+ "moe_fused_grouped",
35
+ # importing it registers the dgrad formulas on the ops, so a forward call differentiates
36
+ "backward",
37
+ "get_supported_act_fns",
38
+ # the per-expert output norm a model can hand the forwards by name, and its standalone op
39
+ "get_supported_norms",
40
+ "rms_norm_rows",
41
+ # load-time helpers: the swizzled scale layout the SM100 scaled-MMA reads, and the row-wise
42
+ # quantizers a loader uses to quantize weights into the group formats
43
+ "swizzle_mx_scales",
44
+ "unswizzle_mx_scales",
45
+ "mxfp8_act_quant",
46
+ "mxfp4_act_quant",
47
+ "nvfp4_act_quant",
48
+ ]
build/torch-rocm/_ops.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ def get_backend() -> str:
4
+ """Detect the backend by inspecting torch."""
5
+ import torch
6
+
7
+ if hasattr(torch, "neuron"):
8
+ # Needs to be sorted before specific Torch builds, since Neuron
9
+ # extension can be loaded into e.g. CUDA Torch builds.
10
+ return "neuron"
11
+ elif torch.version.cuda is not None:
12
+ return "cuda"
13
+ elif torch.version.hip is not None:
14
+ return "rocm"
15
+ elif torch.backends.mps.is_available():
16
+ return "metal"
17
+ elif hasattr(torch.version, "xpu") and torch.version.xpu is not None:
18
+ return "xpu"
19
+ else:
20
+ return "cpu"
21
+
22
+
23
+ def _find_ops_name() -> str:
24
+ kernel_name = "finegrained_kernels"
25
+ unique_id = "1348465"
26
+ backend = get_backend()
27
+ return f"_{kernel_name}_{backend}_{unique_id}"
28
+
29
+
30
+ _OPS_NAME = _find_ops_name()
31
+
32
+ ops = getattr(torch.ops, _OPS_NAME)
33
+
34
+ def add_op_namespace_prefix(op_name: str) -> str:
35
+ """
36
+ Prefix op by namespace.
37
+ """
38
+ return f"{_OPS_NAME}::{op_name}"
build/torch-rocm/backward.py ADDED
@@ -0,0 +1,1052 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Backward for the quantized ops: dgrad only, registered on the forward ops themselves.
15
+
16
+ ``torch.library.register_autograd`` on the ops themselves rather than an ``autograd.Function``
17
+ wrapper: the ops take the RAW activation (they quantize it in one pass internally), so the op
18
+ boundary is exactly where a usable gradient lives, and hanging autograd there means an ordinary
19
+ ``matmul_2d`` call differentiates — through compile, export and AOTAutograd, with no second
20
+ entry point to keep in sync. ``compile_time_only_triton_op``'s dispatch routes to the op
21
+ whenever an input wants a gradient, so this fires in eager as well.
22
+
23
+ dgrad ONLY: ``dA = dY @ B``, contracting N. The quantized weight never receives a gradient, and
24
+ not merely by omission — autograd accumulates into a leaf, and an E4M3 leaf has no ``add``
25
+ ("ufunc_add_CUDA not implemented for Float8_e4m3fn"), so an fp8/fp4 tensor cannot be an autograd
26
+ parameter at all. A frozen quantized base with trainable high-precision adapters in front of it
27
+ (QLoRA) is therefore the shape this serves, and the only gradient it needs is the one that flows
28
+ THROUGH the weight to whatever precedes it.
29
+
30
+ Block-FP8 and per-tensor formats reach that product on the forward kernel via transposed views
31
+ (their scale grids subdivide both axes). MX/NVFP4 cannot be reoriented by a view — group scales
32
+ bind to K and packed E2M1 packs along K — so they contract N against the weight's natural (N, K)
33
+ tile, dequantized per-tile in-register; see ``backward.py``. Either way the FORWARD-oriented
34
+ weight is the only copy that exists.
35
+
36
+ ── the gradient products ──
37
+
38
+ Forward contracts K (``C = A @ B.T``, both operands K-contiguous). dgrad contracts N::
39
+
40
+ dX[m, k] = sum_n dY[m, n] * W[n, k]
41
+
42
+ The weight is STORED ``(N, K)`` K-contiguous, so the ``[BN, BK]`` tile this reduction wants is
43
+ already a natural contiguous tile — n leads, k is contiguous. Nothing is transposed and no second
44
+ quantized copy is kept: the tile is dequantized in-register and fed to a plain ``tl.dot``, exactly
45
+ as the weight-only forward's ``dot`` arm does. That is what makes a 4-bit frozen weight
46
+ differentiable at no memory cost, which is the whole QLoRA case.
47
+
48
+ The group axis stays K, which here is the OUTPUT axis rather than the contraction axis. That is
49
+ why ``dot_scaled`` cannot serve this pass — the tensor core's scaled MMA requires the scaled axis
50
+ to be the one it reduces over — so dgrad is plain-dot only. The tuner already prefers ``dot`` over
51
+ ``dot_scaled`` on the gate|up tile, so little is given up.
52
+
53
+ Format genericity lives in ``_dgrad_weight_tile`` alone. A format is fully described to this
54
+ kernel by three numbers plus the value dtype:
55
+
56
+ SCALE_GROUP_K scale extent along K 32 (MX) 128 (block-FP8)
57
+ SCALE_ROW_DIV rows sharing a scale 1 (MX) 128 (block-FP8)
58
+ WEIGHT_VALUES_PER_BYTE 2 for packed E2M1, else 1
59
+
60
+ so MX (per-row K-groups) and block-FP8 (128x128 blocks) are the same code path at different
61
+ divisors, and no new arm is needed to add a format whose scales tile the same way.
62
+ """
63
+
64
+ import torch
65
+ import triton
66
+ import triton.language as tl
67
+
68
+ from .bayesian_autotuner import bayesian_autotune
69
+ from .compat import (
70
+ add_op_namespace_prefix,
71
+ compile_time_only_triton_op,
72
+ compile_time_only_triton_wrap,
73
+ device_context,
74
+ get_accelerator_autotuning_configs,
75
+ sm_count,
76
+ )
77
+ from .descriptors import maybe_descriptor, rebind_dgrad_descriptors, rebind_dgrad_grouped_descriptors
78
+ from .matmul import matmul_2d
79
+ from .pruners import (
80
+ PATH_ANCHOR_AXES,
81
+ block_within_dim_pruner,
82
+ config_filter,
83
+ descriptor_box_pruner,
84
+ compose_pruners,
85
+ smem_pruner,
86
+ warp_spec_compile_guard_pruner,
87
+ )
88
+ from .formats import global_scale_stride, normalize_global_scale
89
+ from .quant import e2m1_cols_to_bf16, e2m1_cols_to_f32
90
+ from .loading.scales import decode_group_scale
91
+ from .loading.tiles import load_grouped_act_tile, operand_tile_ptrs
92
+ from .scheduling import build_tile_layout, resolve_grouped_tile
93
+
94
+
95
+ # Internal: these are the gradient PRODUCTS, not API. Backward hangs off the forward ops via
96
+ # ``register_autograd`` (the table at the bottom of this file), so an ordinary forward call
97
+ # differentiates and there is
98
+ # no second entry point to keep in sync. Nothing here is re-exported from the package root.
99
+
100
+
101
+ @triton.jit
102
+ def _dgrad_load_weight(
103
+ b_ptrs, b_descriptor, row0, n_off, kb_off,
104
+ BLOCK_ROWS: tl.constexpr, BLOCK_COLS: tl.constexpr,
105
+ B_MEMORY_MODE: tl.constexpr, GROUPED: tl.constexpr = False,
106
+ ):
107
+ """One ``[BN, BK_bytes]`` weight tile: the explicit pointer tile, or the host-TMA box at the
108
+ same absolute offset. The box is a natural contiguous window — unlike the forward's gated
109
+ tile there is no interleaved span to express — so the descriptor arm needs no doubling.
110
+ Single return; only the taken arm compiles."""
111
+ if B_MEMORY_MODE == "pointer":
112
+ w = tl.load(b_ptrs)
113
+ elif GROUPED: # (E, N, K_bytes) box — the expert leads
114
+ w = tl.reshape(b_descriptor.load([row0, n_off, kb_off]), BLOCK_ROWS, BLOCK_COLS)
115
+ else:
116
+ w = b_descriptor.load([n_off, kb_off])
117
+ return w
118
+
119
+
120
+ @triton.jit
121
+ def _dgrad_weight_tile(
122
+ w,
123
+ w_scale,
124
+ BLOCK_SIZE_N: tl.constexpr,
125
+ BLOCK_SIZE_K: tl.constexpr,
126
+ SCALE_GROUP_K: tl.constexpr,
127
+ OUT_DTYPE: tl.constexpr,
128
+ ):
129
+ """One natural ``[BLOCK_SIZE_N, BLOCK_SIZE_K]`` weight tile, dequantized. The K-oriented
130
+ counterpart of ``mx_weight_upcast`` — the group axis is already last here, so unlike the
131
+ forward no transpose is needed.
132
+
133
+ The scale broadcast happens INSIDE the multiply: the real (contiguous) weight reshapes to
134
+ ``[BN, ng, g]`` and the ``[BN, ng, 1]`` scale broadcasts across each group's columns.
135
+ Materializing the broadcast first and reshaping it does not lower — the same trap
136
+ ``mx_weight_upcast`` documents.
137
+
138
+ ``OUT_DTYPE`` picks the arm, and both are this codebase's measured verdicts:
139
+ bf16 for the MMA consumers (exact — E2M1/E4M3 codes and power-of-two UE8M0 scales all fit in
140
+ bf16, and the multiply is a cheap exponent shift), fp32 for the scalar reduce, which widens
141
+ anyway and for which the bf16/E4M3 hop is pure cost. Single return; only the taken arm
142
+ compiles."""
143
+ NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
144
+ if OUT_DTYPE == tl.float32: # scalar-reduce arm: unpack straight to fp32, no narrow hop
145
+ vals = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32)
146
+ sc = decode_group_scale(w_scale).to(tl.float32)[:, :, None]
147
+ else: # MMA arm: dequantize in bf16, exactly as the forward's weight upcast does
148
+ vals = e2m1_cols_to_bf16(w) if w.dtype == tl.uint8 else w.to(tl.bfloat16)
149
+ sc = decode_group_scale(w_scale).to(tl.bfloat16)[:, :, None]
150
+ tile = vals.reshape(BLOCK_SIZE_N, NG, SCALE_GROUP_K) * sc
151
+ return tile.reshape(BLOCK_SIZE_N, BLOCK_SIZE_K).to(OUT_DTYPE)
152
+
153
+
154
+ @bayesian_autotune(
155
+ # Plain-dot loop over the N reduction — same physics as the full-precision 2D kernel (tile +
156
+ # WS), no memory-mode axis: the dequantize is in-tile so both operands read through pointers,
157
+ # and no COMPUTE_MODE axis because the scaled MMA cannot reduce over an unscaled axis.
158
+ get_accelerator_autotuning_configs(
159
+ tune_block_nk=True,
160
+ tune_block_m=True,
161
+ warp_spec=True,
162
+ a_memory_modes=("descriptor", "pointer"),
163
+ b_memory_modes=("descriptor", "pointer"),
164
+ pre_hook=rebind_dgrad_descriptors,
165
+ ),
166
+ ["N", "K", "m_bit_length"],
167
+ n_trials=100,
168
+ path_anchor_axes=PATH_ANCHOR_AXES,
169
+ finite_check_args=("C",),
170
+ prune_configs_by={
171
+ "early_config_prune": compose_pruners(
172
+ block_within_dim_pruner("N"), # exact N-loop: no tail mask, constant-stride advance
173
+ config_filter( # the scale row advances by BN // SCALE_ROW_DIV — must be whole
174
+ lambda c, a: c.kwargs["BLOCK_SIZE_N"] % max(a.get("SCALE_ROW_DIV", 1), 1) == 0
175
+ ),
176
+ config_filter( # BK below one scale group gives NG=0 -> tl.arange(0, 0)
177
+ lambda c, a: c.kwargs["BLOCK_SIZE_K"] % max(a.get("SCALE_GROUP_K", 1), 1) == 0
178
+ ),
179
+ warp_spec_compile_guard_pruner(),
180
+ descriptor_box_pruner("BLOCK_SIZE_K"),
181
+ smem_pruner("BLOCK_SIZE_K"),
182
+ )
183
+ },
184
+ )
185
+ @triton.jit
186
+ def dgrad_matmul_2d_kernel(
187
+ A, # (M, N) upstream gradient, raw bf16/fp16
188
+ ADescriptor, # host TMA descriptor over A, box (BM, BN); read iff A_MEMORY_MODE != "pointer"
189
+ B, # (N, K) quantized weight, K-contiguous — read in its FORWARD orientation
190
+ BDescriptor, # host TMA descriptor over B, box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
191
+ Bs, # (N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
192
+ BsGlobal, # fp32 NVFP4 second-level weight global; folded on the accumulator, None folds out
193
+ C, # (M, K) output gradient
194
+ # Shape
195
+ M,
196
+ N,
197
+ K,
198
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
199
+ # Strides
200
+ stride_a_m,
201
+ stride_a_n,
202
+ stride_b_n,
203
+ stride_b_k,
204
+ stride_bs_n,
205
+ stride_bs_k,
206
+ stride_c_m,
207
+ stride_c_k,
208
+ # Format
209
+ SCALE_GROUP_K: tl.constexpr,
210
+ SCALE_ROW_DIV: tl.constexpr,
211
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
212
+ # Meta-parameters
213
+ A_MEMORY_MODE: tl.constexpr,
214
+ B_MEMORY_MODE: tl.constexpr,
215
+ BLOCK_SIZE_M: tl.constexpr,
216
+ BLOCK_SIZE_N: tl.constexpr,
217
+ BLOCK_SIZE_K: tl.constexpr,
218
+ WARP_SPEC: tl.constexpr,
219
+ ):
220
+ pid_m = tl.program_id(0)
221
+ pid_k = tl.program_id(1)
222
+
223
+ offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
224
+ offs_k = pid_k * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K)
225
+ offs_kb = tl.minimum(
226
+ pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
227
+ + tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
228
+ K // WEIGHT_VALUES_PER_BYTE - 1,
229
+ )
230
+ offs_kg = tl.minimum(
231
+ (pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
232
+ K // SCALE_GROUP_K - 1,
233
+ )
234
+ m_mask = offs_m < M
235
+ k_mask = offs_k < K
236
+
237
+ # BLOCK_SIZE_N divides N (block_within_dim_pruner), so the reduction loop is EXACT: no tail
238
+ # mask on the activation, no clamp on the weight rows, and the addresses advance by a constant
239
+ # stride instead of being rebuilt from offsets each step — the forward's loop shape.
240
+ offs_n0 = tl.arange(0, BLOCK_SIZE_N)
241
+ # SWAP_AB is the WEIGHT's viewpoint, inverted for activations: True = rows-major [BM, BN]
242
+ dy_ptrs = operand_tile_ptrs(A, offs_m, offs_n0, stride_a_m, stride_a_n, A_MEMORY_MODE, True)
243
+ b_ptrs = operand_tile_ptrs(B, offs_n0, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, True)
244
+ bs_ptrs = (
245
+ Bs + (offs_n0[:, None] // SCALE_ROW_DIV) * stride_bs_n + offs_kg[None, :] * stride_bs_k
246
+ )
247
+
248
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_K), dtype=tl.float32)
249
+ for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N), warp_specialize=WARP_SPEC):
250
+ n_off = n * BLOCK_SIZE_N
251
+ dy = load_grouped_act_tile(
252
+ dy_ptrs, ADescriptor, pid_m * BLOCK_SIZE_M, n_off, m_mask, 0, A_MEMORY_MODE
253
+ )
254
+ # the tile the reduction wants is the weight's NATURAL layout: n leads, k contiguous
255
+ w = _dgrad_load_weight(
256
+ b_ptrs, BDescriptor, 0, n_off,
257
+ pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
258
+ BLOCK_SIZE_N, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE, B_MEMORY_MODE,
259
+ )
260
+ w_scale = tl.load(bs_ptrs)
261
+ accumulator += tl.dot(
262
+ dy.to(tl.bfloat16),
263
+ _dgrad_weight_tile(
264
+ w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.bfloat16
265
+ ),
266
+ )
267
+ dy_ptrs += BLOCK_SIZE_N * stride_a_n
268
+ b_ptrs += BLOCK_SIZE_N * stride_b_n
269
+ bs_ptrs += (BLOCK_SIZE_N // SCALE_ROW_DIV) * stride_bs_n
270
+
271
+ if BsGlobal is not None: # NVFP4's second level — one matrix, so one value
272
+ accumulator = accumulator * tl.load(BsGlobal)
273
+
274
+ tl.store(
275
+ C + offs_m[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
276
+ accumulator.to(C.dtype.element_ty),
277
+ mask=m_mask[:, None] & k_mask[None, :],
278
+ )
279
+
280
+
281
+ @compile_time_only_triton_op("finegrained::dgrad_matmul_2d", mutates_args=())
282
+ def dgrad_matmul_2d(
283
+ dY: torch.Tensor,
284
+ W: torch.Tensor,
285
+ Ws: torch.Tensor,
286
+ scale_group_k: int,
287
+ scale_row_div: int,
288
+ output_dtype: torch.dtype | None = None,
289
+ w_global_scale: torch.Tensor | None = None,
290
+ ) -> torch.Tensor:
291
+ """``dX = dY @ W`` against the FORWARD-oriented quantized weight — the gradient a frozen
292
+ quantized base weight passes to whatever trains in front of it (LoRA adapters, earlier
293
+ layers).
294
+
295
+ ``W`` is ``(N, K)`` exactly as the forward holds it; nothing is transposed or re-quantized.
296
+ ``scale_group_k``/``scale_row_div`` describe the scale grid (MX: ``32, 1``; block-FP8:
297
+ ``128, 128``), which is the only thing that differs between formats."""
298
+ assert dY.ndim == 2 and W.ndim == 2, f"expected 2D operands, got {dY.ndim}D and {W.ndim}D"
299
+ M, N = dY.shape
300
+ values_per_byte = 2 if W.dtype == torch.uint8 else 1
301
+ K = W.shape[1] * values_per_byte
302
+ assert W.shape[0] == N, f"weight rows {W.shape[0]} != dY's N {N}"
303
+ assert K % scale_group_k == 0, (
304
+ f"K={K} must be a whole number of scale groups ({scale_group_k})"
305
+ )
306
+
307
+ dX = torch.empty(M, K, device=dY.device, dtype=output_dtype or dY.dtype)
308
+ grid = lambda META: ( # noqa: E731
309
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
310
+ triton.cdiv(K, META["BLOCK_SIZE_K"]),
311
+ )
312
+ with device_context(dY.device):
313
+ # built INSIDE the device context: the tensormap is created against the CURRENT context,
314
+ # so constructing it before the switch fails as "invalid device context" whenever the
315
+ # caller's device is not the active one. Boxes are placeholders; the pre_hook rebinds
316
+ # them to the tuned tile per config.
317
+ a_descriptor = maybe_descriptor(dY, [1, 128])
318
+ b_descriptor = maybe_descriptor(W, [1, 128])
319
+ compile_time_only_triton_wrap(dgrad_matmul_2d_kernel)[grid](
320
+ dY,
321
+ a_descriptor,
322
+ W,
323
+ b_descriptor,
324
+ Ws,
325
+ w_global_scale,
326
+ dX,
327
+ M,
328
+ N,
329
+ K,
330
+ max(int(M).bit_length(), 1),
331
+ dY.stride(0),
332
+ dY.stride(1),
333
+ W.stride(0),
334
+ W.stride(1),
335
+ Ws.stride(0),
336
+ Ws.stride(1),
337
+ dX.stride(0),
338
+ dX.stride(1),
339
+ SCALE_GROUP_K=scale_group_k,
340
+ SCALE_ROW_DIV=scale_row_div,
341
+ WEIGHT_VALUES_PER_BYTE=values_per_byte,
342
+ )
343
+ return dX
344
+
345
+
346
+ @bayesian_autotune(
347
+ # Persistent grouped loop, plain dot over the N reduction. No memory-mode axis (the
348
+ # dequantize is in-tile, so both operands read through pointers) and no COMPUTE_MODE axis
349
+ # (the scaled MMA cannot reduce over an unscaled axis — see the module docstring).
350
+ get_accelerator_autotuning_configs(
351
+ tune_block_nk=True,
352
+ tune_block_m=True,
353
+ warp_spec=True,
354
+ a_memory_modes=("descriptor", "pointer"),
355
+ b_memory_modes=("descriptor", "pointer"),
356
+ pre_hook=rebind_dgrad_grouped_descriptors,
357
+ ),
358
+ ["N", "K", "tokens_per_expert_bit_length"],
359
+ n_trials=100,
360
+ path_anchor_axes=PATH_ANCHOR_AXES,
361
+ finite_check_args=("C",),
362
+ # C is ACCUMULATED into (tl.atomic_add: several routed rows share a source token), so the
363
+ # kernel is not idempotent. Benchmarking calls each config many times against one buffer, and
364
+ # without this every repetition adds again — the tune reads a correct kernel as 1.7e5x wrong.
365
+ reset_to_zero=("C",),
366
+ prune_configs_by={
367
+ "early_config_prune": compose_pruners(
368
+ block_within_dim_pruner("N"), # exact N-loop: no tail mask, constant-stride advance
369
+ config_filter( # the scale row advances by BN // SCALE_ROW_DIV — must be whole
370
+ lambda c, a: c.kwargs["BLOCK_SIZE_N"] % max(a.get("SCALE_ROW_DIV", 1), 1) == 0
371
+ ),
372
+ config_filter( # BK below one scale group gives NG=0 -> tl.arange(0, 0)
373
+ lambda c, a: c.kwargs["BLOCK_SIZE_K"] % max(a.get("SCALE_GROUP_K", 1), 1) == 0
374
+ ),
375
+ warp_spec_compile_guard_pruner(),
376
+ descriptor_box_pruner("BLOCK_SIZE_K"),
377
+ smem_pruner("BLOCK_SIZE_K"),
378
+ )
379
+ },
380
+ )
381
+ @triton.jit
382
+ def dgrad_matmul_grouped_kernel(
383
+ A, # (S, N) upstream gradient in the forward's OUTPUT row order
384
+ ADescriptor, # host TMA descriptor over A; box (1|BM, BN) — 1 row when gathering
385
+ B, # (E, N, K) quantized weights, K-contiguous — the forward orientation
386
+ BDescriptor, # host TMA descriptor over B, box (1, BN, BK_bytes)
387
+ Bs, # (E, N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
388
+ BsGlobal, # fp32 NVFP4 weight global, per tensor or per expert; folded on the accumulator
389
+ stride_bs_global, # expert stride of BsGlobal (0 broadcasts a scalar)
390
+ C, # (S, K) output gradient, written in the forward's INPUT row order
391
+ GatherIdx, # (S,) sorted position -> source row of the forward's A
392
+ ScatterIdx, # (S,) sorted position -> destination row of the forward's C
393
+ ExpertStart, # (NUM_EXPERTS_POW2 + 1,) cumulative row starts, S sentinel
394
+ # Shape
395
+ S,
396
+ N,
397
+ K,
398
+ tokens_per_expert_bit_length, # autotune key only; unused in body
399
+ # Strides
400
+ stride_a_m,
401
+ stride_a_n,
402
+ stride_b_e,
403
+ stride_b_n,
404
+ stride_b_k,
405
+ stride_bs_e,
406
+ stride_bs_n,
407
+ stride_bs_k,
408
+ stride_c_m,
409
+ stride_c_k,
410
+ # Format
411
+ SCALE_GROUP_K: tl.constexpr,
412
+ SCALE_ROW_DIV: tl.constexpr,
413
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
414
+ # Meta-parameters
415
+ A_MEMORY_MODE: tl.constexpr,
416
+ NUM_EXPERTS_POW2: tl.constexpr,
417
+ NUM_SMS: tl.constexpr,
418
+ BLOCK_SIZE_M: tl.constexpr,
419
+ BLOCK_SIZE_N: tl.constexpr,
420
+ BLOCK_SIZE_K: tl.constexpr,
421
+ WARP_SPEC: tl.constexpr,
422
+ B_MEMORY_MODE: tl.constexpr,
423
+ ROUTED_OUT: tl.constexpr = False,
424
+ ):
425
+ """Grouped dgrad. The tile resolver is reused verbatim with K-tiles in its N-tile slot —
426
+ it is generic in that second axis, so ``offs_bn`` comes back as this kernel's K offsets and
427
+ ``pid_n`` as its K-tile index (GATE=False: the gradient of a gate|up GEMM is an UNGATED
428
+ product at the doubled N extent, so no gated arm exists here).
429
+
430
+ The two row maps SWAP roles against the forward: the forward gathered A at ``in_row`` and
431
+ scattered C to ``out_row``, so its gradient reads ``A`` at ``out_row`` and writes ``C`` at
432
+ ``in_row``. Both are already real parameters, which is why routing needs no new machinery."""
433
+ start_pid = tl.program_id(axis=0)
434
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
435
+ ExpertStart, NUM_EXPERTS_POW2, BLOCK_SIZE_M
436
+ )
437
+ num_k_tiles = tl.cdiv(K, BLOCK_SIZE_K)
438
+
439
+ for tile_id in tl.range(start_pid, total_m_tiles * num_k_tiles, NUM_SMS):
440
+ pid_k, _, expert_id64, in_row, out_row, row_mask, offs_k, row0, _, m_start = (
441
+ resolve_grouped_tile(
442
+ tile_id,
443
+ num_k_tiles,
444
+ exp_start,
445
+ freqs,
446
+ tile_start_excl,
447
+ e_offs,
448
+ GatherIdx,
449
+ ScatterIdx,
450
+ BLOCK_SIZE_K,
451
+ BLOCK_SIZE_M,
452
+ False,
453
+ )
454
+ )
455
+ offs_kb = tl.minimum(
456
+ pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
457
+ + tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
458
+ K // WEIGHT_VALUES_PER_BYTE - 1,
459
+ )
460
+ offs_kg = tl.minimum(
461
+ (pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
462
+ K // SCALE_GROUP_K - 1,
463
+ )
464
+ b_base = B + expert_id64 * stride_b_e
465
+ bs_base = Bs + expert_id64 * stride_bs_e
466
+
467
+ # exact N-loop (BLOCK_SIZE_N divides N): maskless weight, constant-stride advance
468
+ offs_n0 = tl.arange(0, BLOCK_SIZE_N)
469
+ # SWAP_AB is the WEIGHT's viewpoint and activation callers pass it INVERTED: False builds
470
+ # [K, rows], so both of these want True to get the rows-major tiles the dot expects
471
+ # ([BM, BN] and [BN, BK]). Only the pointer arm is affected — the descriptor arm returns
472
+ # the scalar base and reads its box from the descriptor.
473
+ dy_ptrs = operand_tile_ptrs(
474
+ A, out_row, offs_n0, stride_a_m, stride_a_n, A_MEMORY_MODE, True
475
+ )
476
+ b_ptrs = operand_tile_ptrs(
477
+ b_base, offs_n0, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, True
478
+ )
479
+ bs_ptrs = (
480
+ bs_base
481
+ + (offs_n0[:, None] // SCALE_ROW_DIV) * stride_bs_n
482
+ + offs_kg[None, :] * stride_bs_k
483
+ )
484
+
485
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_K), dtype=tl.float32)
486
+ for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N), warp_specialize=WARP_SPEC):
487
+ n_off = n * BLOCK_SIZE_N
488
+ # the forward gathered A at in_row and scattered C to out_row, so its gradient READS
489
+ # at out_row — ScatterIdx is this pass's gather map. The box anchor must be out_row's
490
+ # tile start too: the resolver's `m_start` is min(in_row) — the FORWARD's read side —
491
+ # and with a gather-only launch (ScatterIdx None, in_row = the accumulation targets)
492
+ # it anchors the box at a TOKEN id, reading the wrong dY rows.
493
+ dy = load_grouped_act_tile(
494
+ dy_ptrs, ADescriptor, tl.min(out_row).to(tl.int32), n_off, row_mask, out_row,
495
+ A_MEMORY_MODE, ScatterIdx is not None,
496
+ )
497
+ w = _dgrad_load_weight(
498
+ b_ptrs, BDescriptor, row0, n_off,
499
+ pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
500
+ BLOCK_SIZE_N, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE, B_MEMORY_MODE, True,
501
+ )
502
+ w_scale = tl.load(bs_ptrs)
503
+ accumulator += tl.dot(
504
+ dy.to(tl.bfloat16),
505
+ _dgrad_weight_tile(
506
+ w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.bfloat16
507
+ ),
508
+ )
509
+ dy_ptrs += BLOCK_SIZE_N * stride_a_n
510
+ b_ptrs += BLOCK_SIZE_N * stride_b_n
511
+ bs_ptrs += (BLOCK_SIZE_N // SCALE_ROW_DIV) * stride_bs_n
512
+
513
+ # Fallback when the rows are NOT token-major (no scatter map): accumulate straight into
514
+ # the source token. `in_row` is that token, and with top_k > 1 several routed rows share
515
+ # one, so a store would keep whichever tile landed last and drop the rest — invisible to
516
+ # any test whose gather map is the identity (S == M), which is how it survived one.
517
+ #
518
+ # The FORWARD reduces without atomics (`weighted_reduce`: (S, H) -> (T, H)) — but its
519
+ # routed-space tensor already exists, because the down GEMM's natural output IS (S, H), and
520
+ # its reduce also applies routing weights and skips EP sentinels, which an atomic could not
521
+ # express. dgrad is the transpose: its natural output is SOURCE space, so mirroring the
522
+ # forward would mean materializing a (S, K) buffer purely to avoid atomics.
523
+ #
524
+ # Measured and rejected: store-into-(S, K) + index_add_ ran 4038 vs 4181us at DSV3 top_k=8
525
+ # (-3.4%), 1278 vs 1294 at GPT-OSS top_k=4, and SLOWER at decode (411 vs 404) — to buy that
526
+ # you pay a top_k x fp32 intermediate, ~940MB at DSV3 shapes. top_k-way contention is not
527
+ # the bottleneck here; the GEMM is.
528
+ if BsGlobal is not None: # NVFP4's second level, this tile's expert
529
+ accumulator = accumulator * tl.load(BsGlobal + expert_id64 * stride_bs_global)
530
+
531
+ if ROUTED_OUT:
532
+ # Plain store at the TOKEN-MAJOR routed row (`out_row`), then the caller sums the
533
+ # top_k rows per token. This is the FORWARD's shape — it stores routed rows and
534
+ # reduces them with `weighted_reduce` — and it is 1.40x faster than atomics at DSV3
535
+ # top_k=8 (3003 vs 4215us), 1.13x at GPT-OSS top_k=4, break-even at decode. top_k-way
536
+ # atomic contention is the difference; the reduce itself is nearly free.
537
+ tl.store(
538
+ C + out_row[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
539
+ accumulator,
540
+ mask=row_mask[:, None] & (offs_k[None, :] < K),
541
+ )
542
+ else:
543
+ tl.atomic_add(
544
+ C + in_row[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
545
+ accumulator,
546
+ mask=row_mask[:, None] & (offs_k[None, :] < K),
547
+ )
548
+
549
+
550
+ @compile_time_only_triton_op("finegrained::dgrad_matmul_grouped", mutates_args=())
551
+ def dgrad_matmul_grouped(
552
+ dY: torch.Tensor,
553
+ W: torch.Tensor,
554
+ Ws: torch.Tensor,
555
+ expert_start: torch.Tensor,
556
+ scale_group_k: int,
557
+ scale_row_div: int,
558
+ gather_idx: torch.Tensor | None = None,
559
+ scatter_idx: torch.Tensor | None = None,
560
+ output_dtype: torch.dtype | None = None,
561
+ num_input_rows: int | None = None,
562
+ w_global_scale: torch.Tensor | None = None,
563
+ ) -> torch.Tensor:
564
+ """Routed ``dX = dY @ W[expert]`` over expert-sorted positions — the MoE counterpart of
565
+ ``dgrad_matmul_2d``.
566
+
567
+ ``gather_idx`` / ``scatter_idx`` are the FORWARD's maps, passed unchanged: the kernel swaps
568
+ their roles itself (reads ``dY`` at the forward's scatter destination, writes ``dX`` at its
569
+ gather source), so callers hand over the same two tensors the forward got."""
570
+ assert dY.ndim == 2 and W.ndim == 3, f"expected (S,N) and (E,N,K), got {dY.shape}, {W.shape}"
571
+ S, N = dY.shape
572
+ values_per_byte = 2 if W.dtype == torch.uint8 else 1
573
+ K = W.shape[2] * values_per_byte
574
+ assert W.shape[1] == N, f"weight N {W.shape[1]} != dY's N {N}"
575
+ assert K % scale_group_k == 0, (
576
+ f"K={K} must be a whole number of scale groups ({scale_group_k})"
577
+ )
578
+
579
+ num_experts = W.shape[0]
580
+ num_sms = sm_count(dY.device.index)
581
+ # (num_input_rows, K), ZERO-initialised: the kernel accumulates each routed row's gradient
582
+ # into its source token, so the buffer is indexed in SOURCE space and must start empty.
583
+ # fp32 because that is what tl.atomic_add takes.
584
+ rows = S if num_input_rows is None else num_input_rows
585
+ # Token-major store + a top_k sum beats atomics whenever the scatter map makes the rows
586
+ # token-major (see the kernel). It costs a top_k x fp32 intermediate, which is why the atomic
587
+ # arm stays as the fallback for gather-only launches.
588
+ routed_out = scatter_idx is not None and rows > 0 and S % rows == 0
589
+ dX = torch.zeros(S if routed_out else rows, K, device=dY.device, dtype=torch.float32)
590
+ with device_context(dY.device):
591
+ # inside the context — see dgrad_matmul_2d; placeholder boxes, rebound by the pre_hook
592
+ a_descriptor = maybe_descriptor(dY, [1, 128])
593
+ b_descriptor = maybe_descriptor(W, [1, 1, 128])
594
+ compile_time_only_triton_wrap(dgrad_matmul_grouped_kernel)[(num_sms,)](
595
+ dY,
596
+ a_descriptor,
597
+ W,
598
+ b_descriptor,
599
+ Ws,
600
+ w_global_scale,
601
+ global_scale_stride(w_global_scale),
602
+ dX,
603
+ gather_idx,
604
+ scatter_idx,
605
+ expert_start,
606
+ S,
607
+ N,
608
+ K,
609
+ max(int(max(S // max(num_experts, 1), 1)).bit_length(), 1),
610
+ dY.stride(0),
611
+ dY.stride(1),
612
+ W.stride(0),
613
+ W.stride(1),
614
+ W.stride(2),
615
+ Ws.stride(0),
616
+ Ws.stride(1),
617
+ Ws.stride(2),
618
+ dX.stride(0),
619
+ dX.stride(1),
620
+ SCALE_GROUP_K=scale_group_k,
621
+ SCALE_ROW_DIV=scale_row_div,
622
+ WEIGHT_VALUES_PER_BYTE=values_per_byte,
623
+ NUM_EXPERTS_POW2=triton.next_power_of_2(num_experts),
624
+ NUM_SMS=num_sms,
625
+ ROUTED_OUT=routed_out,
626
+ )
627
+ if routed_out:
628
+ dX = dX.view(rows, S // rows, K).sum(1)
629
+ return dX.to(output_dtype or dY.dtype)
630
+
631
+
632
+ @triton.jit
633
+ def _glu_grad(g, u, dh, ACT_FN: tl.constexpr, SWIGLU_ALPHA: tl.constexpr,
634
+ SWIGLU_LIMIT: tl.constexpr):
635
+ """(dgate, dup) for one tile, mirroring ``epilogue.glu`` arm for arm.
636
+
637
+ The alpha arm is ``(u + 1) * g * sigmoid(ALPHA * g)`` — the ``+1`` is load-bearing and easy to
638
+ drop: without it the gate gradient is wrong on exactly the GPT-OSS / MiniMax path and nowhere
639
+ else. ``SWIGLU_LIMIT`` clamps gate ABOVE and up to ``[-LIMIT, LIMIT]``, so the gradient is zero
640
+ wherever the forward saturated. Single return; only the taken arms compile."""
641
+ gc = tl.minimum(g, SWIGLU_LIMIT) if SWIGLU_LIMIT is not None else g
642
+ uc = (
643
+ tl.minimum(tl.maximum(u, -SWIGLU_LIMIT), SWIGLU_LIMIT)
644
+ if SWIGLU_LIMIT is not None
645
+ else u
646
+ )
647
+ if SWIGLU_ALPHA is not None:
648
+ sig = tl.sigmoid(gc * SWIGLU_ALPHA)
649
+ act = gc * sig
650
+ dact = sig + gc * SWIGLU_ALPHA * sig * (1.0 - sig)
651
+ dg = dh * (uc + 1.0) * dact
652
+ elif ACT_FN == "silu":
653
+ sig = tl.sigmoid(gc)
654
+ act = gc * sig
655
+ dg = dh * uc * (sig + gc * sig * (1.0 - sig))
656
+ elif ACT_FN == "relu":
657
+ act = tl.maximum(gc, 0.0)
658
+ dg = dh * uc * (gc > 0.0).to(tl.float32)
659
+ else: # gelu, exact via erf — the forward's form
660
+ cdf = 0.5 * (1.0 + tl.erf(gc * 0.7071067811865476))
661
+ act = gc * cdf
662
+ dg = dh * uc * (cdf + gc * tl.exp(-0.5 * gc * gc) * 0.3989422804014327)
663
+ du = dh * act
664
+ if SWIGLU_LIMIT is not None: # saturated inputs pass no gradient
665
+ dg = tl.where(g > SWIGLU_LIMIT, 0.0, dg)
666
+ du = tl.where(tl.abs(u) > SWIGLU_LIMIT, 0.0, du)
667
+ return dg, du
668
+
669
+
670
+ @triton.jit
671
+ def _glu_backward_kernel(
672
+ Z, # (M, 2*INTER) pre-activation, gate at even columns / up at odd
673
+ DH, # (M, INTER) gradient w.r.t. the GLU output
674
+ DZ, # (M, 2*INTER) output, interleaved to match Z
675
+ M, INTER,
676
+ stride_z_m, stride_z_n, stride_dh_m, stride_dh_n, stride_dz_m, stride_dz_n,
677
+ ACT_FN: tl.constexpr, SWIGLU_ALPHA: tl.constexpr, SWIGLU_LIMIT: tl.constexpr,
678
+ BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_I: tl.constexpr,
679
+ ):
680
+ """Elementwise GLU backward over the interleaved layout. Gate ``j`` sits at column ``2j`` and
681
+ up at ``2j+1``, so one tile reads both halves with a stride-2 pair of loads and writes them
682
+ back the same way — no split, no concatenate, no materialized halves."""
683
+ offs_m = tl.program_id(0) * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
684
+ offs_i = tl.program_id(1) * BLOCK_SIZE_I + tl.arange(0, BLOCK_SIZE_I)
685
+ mask = (offs_m[:, None] < M) & (offs_i[None, :] < INTER)
686
+
687
+ z_gate = Z + offs_m[:, None] * stride_z_m + (2 * offs_i[None, :]) * stride_z_n
688
+ g = tl.load(z_gate, mask=mask, other=0.0).to(tl.float32)
689
+ u = tl.load(z_gate + stride_z_n, mask=mask, other=0.0).to(tl.float32)
690
+ dh = tl.load(
691
+ DH + offs_m[:, None] * stride_dh_m + offs_i[None, :] * stride_dh_n,
692
+ mask=mask, other=0.0,
693
+ ).to(tl.float32)
694
+
695
+ dg, du = _glu_grad(g, u, dh, ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT)
696
+
697
+ dz_gate = DZ + offs_m[:, None] * stride_dz_m + (2 * offs_i[None, :]) * stride_dz_n
698
+ tl.store(dz_gate, dg.to(DZ.dtype.element_ty), mask=mask)
699
+ tl.store(dz_gate + stride_dz_n, du.to(DZ.dtype.element_ty), mask=mask)
700
+
701
+
702
+ def glu_backward(
703
+ dH: torch.Tensor,
704
+ Z: torch.Tensor,
705
+ act_fn: str = "silu",
706
+ swiglu_alpha: float | None = None,
707
+ swiglu_limit: float | None = None,
708
+ ) -> torch.Tensor:
709
+ """Gradient of the fused GLU epilogue: ``dH`` (grad w.r.t. ``H = f(Z_gate) * Z_up``) and the
710
+ 2I-wide pre-activation ``Z`` -> ``dZ``, interleaved.
711
+
712
+ One pass over Z, no materialized halves. The GEMM that consumes ``dZ`` is then an UNGATED
713
+ product at the doubled N extent — the forward kernel with no gate flag, because
714
+ ``pid_n * 2BN == 2 * pid_n * BN`` — so no gated backward kernel exists or is needed."""
715
+ assert dH.ndim == 2 and Z.ndim == 2, f"expected 2D, got {dH.shape}, {Z.shape}"
716
+ M, INTER = dH.shape
717
+ assert Z.shape == (M, 2 * INTER), f"Z must be (M, 2I) = {(M, 2 * INTER)}, got {tuple(Z.shape)}"
718
+ dZ = torch.empty_like(Z)
719
+ grid = lambda META: ( # noqa: E731
720
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
721
+ triton.cdiv(INTER, META["BLOCK_SIZE_I"]),
722
+ )
723
+ with device_context(Z.device):
724
+ compile_time_only_triton_wrap(_glu_backward_kernel)[grid](
725
+ Z, dH, dZ, M, INTER,
726
+ Z.stride(0), Z.stride(1), dH.stride(0), dH.stride(1), dZ.stride(0), dZ.stride(1),
727
+ ACT_FN=act_fn, SWIGLU_ALPHA=swiglu_alpha, SWIGLU_LIMIT=swiglu_limit,
728
+ BLOCK_SIZE_M=32, BLOCK_SIZE_I=64, num_warps=4,
729
+ )
730
+ return dZ
731
+
732
+
733
+ @bayesian_autotune(
734
+ # Neither a BLOCK_SIZE_M nor a WARP_SPEC axis. One row per program (the rows are not
735
+ # expert-sorted, so a tile cannot span them) and the reduce is FMA rather than MMA, so there is
736
+ # no M tile to choose — and WS, which needs ``(BN if swapped else BM) >= 64`` of M work, has no
737
+ # M operand to satisfy it. Emitting either axis would search configs that cannot compile.
738
+ get_accelerator_autotuning_configs(tune_block_nk=True),
739
+ ["N", "K", "S"],
740
+ n_trials=100,
741
+ path_anchor_axes=PATH_ANCHOR_AXES,
742
+ finite_check_args=("C",),
743
+ # No pruners. The smem model is fit to MMA loops — ``num_stages`` buffers of an A tile and a B
744
+ # tile, keyed on BLOCK_SIZE_M — and this loop has neither: the activation is a ``[BN]`` vector
745
+ # and the reduce is FMA, so there is no M dimension for it to reason about. A config that
746
+ # genuinely will not fit raises OutOfResources, which the tuner already inf-forgives.
747
+ )
748
+ @triton.jit
749
+ def dgrad_matmul_batched_kernel(
750
+ A, # (S, N) upstream gradient
751
+ B, # (E, N, K) quantized weights, K-contiguous — the forward orientation
752
+ Bs, # (E, N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
753
+ BsGlobal, # fp32 NVFP4 weight global, per tensor or per expert; folded on the accumulator
754
+ stride_bs_global, # expert stride of BsGlobal (0 broadcasts a scalar)
755
+ C, # (S, K) output gradient
756
+ ExpertIds, # (S,) which expert each row routed to
757
+ # Shape
758
+ S,
759
+ N,
760
+ K,
761
+ # Strides
762
+ stride_a_m,
763
+ stride_a_n,
764
+ stride_b_e,
765
+ stride_b_n,
766
+ stride_b_k,
767
+ stride_bs_e,
768
+ stride_bs_n,
769
+ stride_bs_k,
770
+ stride_c_m,
771
+ stride_c_k,
772
+ # Format
773
+ SCALE_GROUP_K: tl.constexpr,
774
+ SCALE_ROW_DIV: tl.constexpr,
775
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
776
+ # Meta-parameters
777
+ BLOCK_SIZE_N: tl.constexpr,
778
+ BLOCK_SIZE_K: tl.constexpr,
779
+ ):
780
+ """Batched dgrad: ``dX[s] = dY[s] @ B[ExpertIds[s]]``.
781
+
782
+ Unlike the grouped form the rows are NOT expert-sorted — each row names its own expert — so a
783
+ tile must cover one expert. ``BLOCK_SIZE_M`` is pinned to 1 by the wrapper for that reason,
784
+ which is also the shape the batched forward runs at."""
785
+ pid_m = tl.program_id(0)
786
+ pid_k = tl.program_id(1)
787
+ row_ok = pid_m < S
788
+ expert_id = tl.load(ExpertIds + pid_m, mask=row_ok, other=0).to(tl.int64)
789
+
790
+ offs_k = pid_k * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K)
791
+ offs_kb = tl.minimum(
792
+ pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
793
+ + tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
794
+ K // WEIGHT_VALUES_PER_BYTE - 1,
795
+ )
796
+ offs_kg = tl.minimum(
797
+ (pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
798
+ K // SCALE_GROUP_K - 1,
799
+ )
800
+ b_base = B + expert_id * stride_b_e
801
+ bs_base = Bs + expert_id * stride_bs_e
802
+
803
+ # CUDA-core FMA reduce, NOT tl.dot: one row per program is an M=1 GEVM, and feeding the MMA a
804
+ # broadcast [1, BN] operand runs it emulated at a fraction of peak. The forward reaches the same
805
+ # conclusion for its decode path (``mx_weight_only_scalar_swapped``) — the scalar arm wins at
806
+ # M=1 and the tuner picks it there. Same reduction, no MMA, no padded M dimension.
807
+ accumulator = tl.zeros((BLOCK_SIZE_K,), dtype=tl.float32)
808
+ for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N)):
809
+ offs_n = n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
810
+ dy = tl.load(
811
+ A + pid_m * stride_a_m + offs_n * stride_a_n,
812
+ mask=row_ok & (offs_n < N),
813
+ other=0.0,
814
+ ).to(tl.float32)
815
+ offs_nc = tl.minimum(offs_n, N - 1) # clamp: dy is already zeroed past N
816
+ w = tl.load(b_base + offs_nc[:, None] * stride_b_n + offs_kb[None, :] * stride_b_k)
817
+ w_scale = tl.load(
818
+ bs_base
819
+ + (offs_nc[:, None] // SCALE_ROW_DIV) * stride_bs_n
820
+ + offs_kg[None, :] * stride_bs_k
821
+ )
822
+ wdeq = _dgrad_weight_tile(
823
+ w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.float32
824
+ )
825
+ accumulator += tl.sum(dy[:, None] * wdeq, axis=0)
826
+
827
+ if BsGlobal is not None: # NVFP4's second level, this row's expert
828
+ accumulator = accumulator * tl.load(BsGlobal + expert_id * stride_bs_global)
829
+
830
+ tl.store(
831
+ C + pid_m * stride_c_m + offs_k * stride_c_k,
832
+ accumulator.to(C.dtype.element_ty),
833
+ mask=row_ok & (offs_k < K),
834
+ )
835
+
836
+
837
+ @compile_time_only_triton_op("finegrained::dgrad_matmul_batched", mutates_args=())
838
+ def dgrad_matmul_batched(
839
+ dY: torch.Tensor,
840
+ W: torch.Tensor,
841
+ Ws: torch.Tensor,
842
+ expert_ids: torch.Tensor,
843
+ scale_group_k: int,
844
+ scale_row_div: int,
845
+ output_dtype: torch.dtype | None = None,
846
+ w_global_scale: torch.Tensor | None = None,
847
+ ) -> torch.Tensor:
848
+ """``dX[s] = dY[s] @ W[expert_ids[s]]`` — the batched (per-row expert) counterpart of
849
+ ``dgrad_matmul_grouped``. Registered because the batched experts forward is a SELECTABLE
850
+ dispatch in transformers, not a decode-only path: without this, finetuning against it
851
+ silently produces no gradient."""
852
+ assert dY.ndim == 2 and W.ndim == 3, f"expected (S,N) and (E,N,K), got {dY.shape}, {W.shape}"
853
+ S, N = dY.shape
854
+ values_per_byte = 2 if W.dtype == torch.uint8 else 1
855
+ K = W.shape[2] * values_per_byte
856
+ assert W.shape[1] == N, f"weight N {W.shape[1]} != dY's N {N}"
857
+
858
+ dX = torch.empty(S, K, device=dY.device, dtype=output_dtype or dY.dtype)
859
+ grid = lambda META: (S, triton.cdiv(K, META["BLOCK_SIZE_K"])) # noqa: E731
860
+ with device_context(dY.device):
861
+ compile_time_only_triton_wrap(dgrad_matmul_batched_kernel)[grid](
862
+ dY, W, Ws, w_global_scale, global_scale_stride(w_global_scale), dX, expert_ids, S, N, K,
863
+ dY.stride(0), dY.stride(1),
864
+ W.stride(0), W.stride(1), W.stride(2),
865
+ Ws.stride(0), Ws.stride(1), Ws.stride(2),
866
+ dX.stride(0), dX.stride(1),
867
+ SCALE_GROUP_K=scale_group_k,
868
+ SCALE_ROW_DIV=scale_row_div,
869
+ WEIGHT_VALUES_PER_BYTE=values_per_byte,
870
+ )
871
+ return dX
872
+
873
+
874
+
875
+ # ── gradient products, per format family ─────────────────────────────────────────
876
+ # What each format's dgrad actually computes. ``backward.py`` holds only the table saying which
877
+ # op gets which of these — compute lives here, attachment lives there.
878
+
879
+ def _dgrad_mx(dY, B, Bs, b_global, out_dtype):
880
+ """dA for an MX/NVFP4 weight: contract N on the forward-oriented weight (see backward.py).
881
+
882
+ The scale grid is DERIVED FROM SHAPES, not from a format constant — ``Bs`` is
883
+ ``(N // row_div, K // group)``, so the divisors read straight off it. That covers group-32
884
+ UE8M0 (MX) and group-16 E4M3 (NVFP4) without either being named here, and cannot drift when a
885
+ format's block changes.
886
+
887
+ Containers are normalized, not reinterpreted: packed E2M1 reaches the kernel as ``int8`` or
888
+ ``uint8`` depending on the producer, and UE8M0 as ``float8_e8m0fnu`` or ``uint8``. The kernel
889
+ keys its unpack/decode off ``uint8``, and decoding UE8M0 bytes as floats is off by orders of
890
+ magnitude — the failure mode is a non-finite dX, which the tuner's numerics veto turns into an
891
+ all-inf tune rather than a wrong answer.
892
+
893
+ The NVFP4 second-level global rides into the kernel, which folds it on the accumulator —
894
+ one value here, a 2D weight being one matrix."""
895
+ if B.dtype == torch.int8:
896
+ B = B.view(torch.uint8)
897
+ if Bs.dtype == torch.float8_e8m0fnu:
898
+ Bs = Bs.view(torch.uint8)
899
+ values_per_byte = 2 if B.dtype == torch.uint8 else 1
900
+ K = B.shape[-1] * values_per_byte
901
+ group = K // Bs.shape[-1]
902
+ row_div = max(B.shape[-2] // Bs.shape[-2], 1)
903
+ dX = dgrad_matmul_2d(
904
+ dY.contiguous(), B, Bs, group, row_div, output_dtype=torch.float32,
905
+ w_global_scale=normalize_global_scale(b_global, 1),
906
+ )
907
+ return dX.to(out_dtype)
908
+
909
+ def _dgrad_transposed_weight(dY, B, Bs, out_dtype):
910
+ """dA on the QUANTIZED weight, reoriented by transposition. Valid when the scale does not
911
+ bind a group to K: a per-tensor scalar is orientation-free, and a BLOCK grid subdivides both
912
+ axes, so transposing it is the same reindexing as transposing the tile.
913
+
914
+ The scale grid is made CONTIGUOUS, not just transposed: reading it column-major cost 79us on
915
+ a 4096x8192x6144 dgrad (405.7 -> 326.5us) while the copy is ~3KB, because every tile re-reads
916
+ it strided. The weight itself stays a view — materializing it saves a further 47us but costs
917
+ 211us to produce, so that only pays when the caller already keeps both orientations."""
918
+ Bs_t = Bs if Bs.numel() == 1 else Bs.t().contiguous()
919
+ return matmul_2d(dY.contiguous(), B.t(), None, Bs_t, output_dtype=out_dtype)
920
+
921
+ def _dgrad_grouped(ctx, dY, B, Bs, expert_start, gather_idx, scatter_idx, b_global):
922
+ """Routed dgrad. GATE is not a case to handle: the gradient of a gate|up GEMM is an UNGATED
923
+ product at the doubled N extent, which is what a 2N-row weight already is. The forward's two
924
+ row maps are passed through unchanged — the kernel swaps their roles itself. The NVFP4
925
+ global folds in the kernel, which already resolves each tile's expert."""
926
+ if B.dtype == torch.int8:
927
+ B = B.view(torch.uint8)
928
+ if Bs.dtype == torch.float8_e8m0fnu:
929
+ Bs = Bs.view(torch.uint8)
930
+ values_per_byte = 2 if B.dtype == torch.uint8 else 1
931
+ K = B.shape[-1] * values_per_byte
932
+ dX = dgrad_matmul_grouped(
933
+ dY.contiguous(), B, Bs, expert_start,
934
+ K // Bs.shape[-1], max(B.shape[-2] // Bs.shape[-2], 1),
935
+ gather_idx=gather_idx, scatter_idx=scatter_idx, output_dtype=torch.float32,
936
+ num_input_rows=getattr(ctx, "a_rows", None),
937
+ w_global_scale=normalize_global_scale(b_global, B.shape[0]),
938
+ )
939
+ return dX.to(ctx.a_dtype)
940
+
941
+
942
+ __all__: list[str] = []
943
+
944
+ def _register(name: str, saved: tuple[int, ...], dgrad):
945
+ """Attach a backward to one op. ``saved`` names the positional inputs backward needs — they
946
+ differ per op, e.g. the MX signature is ``(A, B, As, Bs, ...)`` so its weight scale is index
947
+ 3, not 2.
948
+
949
+ Everything but the activation gets None — weights and scales take no gradient, the rest is
950
+ non-differentiable config."""
951
+
952
+ def setup_context(ctx, inputs, output):
953
+ ctx.save_for_backward(*(inputs[i] for i in saved))
954
+ ctx.a_dtype = inputs[0].dtype
955
+ # routed dgrad accumulates into SOURCE tokens, which is a different row count from the
956
+ # routed rows the op consumed whenever a gather map is in play
957
+ ctx.a_rows = inputs[0].shape[0]
958
+
959
+
960
+ def backward(ctx, *grads):
961
+ # torch matches backward's return against `tree_flatten(args, is_leaf=not_list_of_tensor)`
962
+ # of the args AS INVOKED — which setup_context cannot see (it gets the bound args, with
963
+ # defaults filled). `needs_input_grad` is sized to that same flattened list plus the
964
+ # trailing metadata arg, so it is the one place the right count is observable. torch
965
+ # appends the metadata's own None itself, so this returns exactly that many.
966
+ n_grads = len(ctx.needs_input_grad)
967
+ if not ctx.needs_input_grad[0]:
968
+ return (None,) * n_grads
969
+ # the ops return list[Tensor] (a second entry under output_format), so autograd hands the
970
+ # gradient back in that shape — unwrap to the gradient of the primary output
971
+ dY = grads[0]
972
+ while isinstance(dY, (list, tuple)):
973
+ dY = dY[0]
974
+ return (dgrad(ctx, dY, *ctx.saved_tensors),) + (None,) * (n_grads - 1)
975
+
976
+ torch.library.register_autograd(
977
+ add_op_namespace_prefix(name), backward, setup_context=setup_context
978
+ )
979
+
980
+
981
+ # block-FP8 (128x128 / 1x128, fp32 or UE8M0 scales): the grid subdivides both axes, so dgrad
982
+ # reuses the quantized weight through a transposed view.
983
+ _register(
984
+ "w8a8_block_dynamic_fp8_matmul", saved=(1, 2),
985
+ dgrad=lambda ctx, dY, B, Bs: _dgrad_transposed_weight(dY, B, Bs, ctx.a_dtype),
986
+ )
987
+
988
+ # per-tensor FP8: a scalar scale is orientation-free, so the same route needs no reorientation.
989
+ _register(
990
+ "w8a8_tensor_dynamic_fp8_matmul", saved=(1, 2),
991
+ dgrad=lambda ctx, dY, B, Bs: _dgrad_transposed_weight(dY, B, Bs, ctx.a_dtype),
992
+ )
993
+
994
+ # MX / NVFP4: per-row group scales bind to K and packed E2M1 packs along K, so the weight cannot
995
+ # be reoriented by a VIEW — but it does not need to be. The reduction runs over N against the
996
+ # weight's natural (N, K) tile, dequantized per-tile in-register, so the FORWARD-oriented weight
997
+ # serves dgrad as-is: no transposed copy, no second quantized orientation, no fp32 materialization.
998
+ _register(
999
+ "mx_dynamic_matmul", saved=(1, 3, 13),
1000
+ dgrad=lambda ctx, dY, B, Bs, b_global: _dgrad_mx(dY, B, Bs, b_global, ctx.a_dtype),
1001
+ )
1002
+
1003
+ # Weight-only (W4A16 / W8A16) — the QLoRA format: a frozen 4-bit base weight with raw bf16
1004
+ # activations. Same route; the weight takes no gradient, so nothing here needs a master copy.
1005
+ _register(
1006
+ "mx_weight_only_matmul_2d", saved=(1, 2, 9),
1007
+ dgrad=lambda ctx, dY, B, Bs, b_global: _dgrad_mx(dY, B, Bs, b_global, ctx.a_dtype),
1008
+ )
1009
+
1010
+
1011
+ def _dgrad_batched(ctx, dY, B, Bs, expert_ids, b_global):
1012
+ """Per-row-expert dgrad. Same shape-derived scale grid as the others; the only difference from
1013
+ ``_dgrad_grouped`` is that the expert comes from a per-row index rather than sorted ranges."""
1014
+ if B.dtype == torch.int8:
1015
+ B = B.view(torch.uint8)
1016
+ if Bs.dtype == torch.float8_e8m0fnu:
1017
+ Bs = Bs.view(torch.uint8)
1018
+ values_per_byte = 2 if B.dtype == torch.uint8 else 1
1019
+ K = B.shape[-1] * values_per_byte
1020
+ dX = dgrad_matmul_batched(
1021
+ dY.contiguous(), B, Bs, expert_ids,
1022
+ K // Bs.shape[-1], max(B.shape[-2] // Bs.shape[-2], 1),
1023
+ output_dtype=torch.float32,
1024
+ w_global_scale=normalize_global_scale(b_global, B.shape[0]),
1025
+ )
1026
+ return dX.to(ctx.a_dtype)
1027
+
1028
+
1029
+ # Routed MoE, weight-only — QLoRA on an MoE model. Positional slots: B=1, Bs=2, expert_start=3,
1030
+ # gather_idx=10, scatter_idx=11, b_global_scale=12.
1031
+ _register(
1032
+ "mx_weight_only_matmul_grouped", saved=(1, 2, 3, 10, 11, 12),
1033
+ dgrad=_dgrad_grouped,
1034
+ )
1035
+
1036
+ # Routed MoE, block-FP8. Same product as the weight-only grouped op — a 128x128 block grid is
1037
+ # the per-row K-group layout at wider divisors, which `_dgrad_grouped` derives from `Bs.shape` —
1038
+ # but it is a DIFFERENT op, and only registering the weight-only one left the grouped block-FP8
1039
+ # path with no grad_fn at all. That is the path a PEFT LoRA on fp8 experts actually runs.
1040
+ # Positional slots: B=1, Bs=3, expert_start=4, gather_idx=14, scatter_idx=15; no global scale.
1041
+ _register(
1042
+ "w8a8_block_dynamic_fp8_matmul_grouped", saved=(1, 3, 4, 14, 15),
1043
+ dgrad=lambda ctx, dY, B, Bs, es, gi, si: _dgrad_grouped(ctx, dY, B, Bs, es, gi, si, None),
1044
+ )
1045
+
1046
+ # The batched experts forward is a SELECTABLE dispatch in transformers, not a decode-only path, so
1047
+ # it needs a formula too — without one, finetuning against `batched_mm` silently trains nothing.
1048
+ # Rows are not expert-sorted here: slot 3 is per-row `expert_ids`, not `expert_start`.
1049
+ _register(
1050
+ "mx_weight_only_matmul_batched", saved=(1, 2, 3, 12),
1051
+ dgrad=_dgrad_batched,
1052
+ )
build/torch-rocm/batched.py ADDED
@@ -0,0 +1,1947 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+
17
+ import torch
18
+ import triton
19
+ import triton.language as tl
20
+
21
+
22
+ from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
23
+
24
+ from .bayesian_autotuner import bayesian_autotune
25
+
26
+ from .compat import add_op_namespace_prefix, FP8_DTYPE, MX_SCALE_GROUP_K, NIBBLES_PER_BYTE, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, get_accelerator_autotuning_configs, tl_dtype, decode_pdl
27
+ from .descriptors import rebind_batched_mx_bs_descriptor
28
+ from .formats import check_activation_format, global_scale_stride, normalize_global_scale, e2m1_as_uint8, expert_weight_shape, is_mx, mx_scale_family, normalize_per_expert_scale, resolve_activation_format, resolve_output_dtype, ue8m0_as_uint8, validate_dense_operands, weight_block_size, weight_format
29
+ from .epilogue import fused_glu
30
+ from .quant import MX_ACT_QUANT, fp8_act_quant_block_dynamic, fp8_act_quant_tensor_wide
31
+ from .swizzle import swizzled_scale_descriptor
32
+ from .mma import block_dynamic_dot, fp8_dot, mx_compute, mx_weight_only_compute, static_dot
33
+ from .loading.tiles import (
34
+ advance_ptrs,
35
+ load_act_block_dynamic,
36
+ load_act_mx,
37
+ load_act_plain,
38
+ load_act_static,
39
+ load_weight_block_dynamic,
40
+ load_weight_mx,
41
+ load_weight_plain,
42
+ load_weight_static,
43
+ operand_tile_ptrs,
44
+ oriented_tile_ptrs,
45
+ weight_tile_ptrs,
46
+ )
47
+ from .epilogue import acc_finalize, acc_init, add_bias, bias_strides, gemm_epilogue
48
+ from .pruners import PATH_ANCHOR_AXES, fp8_dot_warp_pruner, dot_scaled_staging_pruner, block_fits_dim_pruner, block_within_dim_pruner, compose_pruners, mx_config_pruner, require_moe_dims_aligned, scale_subblock_pruner, smem_pruner, swizzled_scale_config_pruner, weight_only_swap_scope_pruner
49
+
50
+
51
+ @triton.jit
52
+ def expert_setup(
53
+ A,
54
+ B,
55
+ C,
56
+ Bs,
57
+ ExpertIds,
58
+ GatherIdx,
59
+ ScatterIdx,
60
+ stride_a_m,
61
+ stride_b_e,
62
+ stride_c_m,
63
+ stride_bs_e,
64
+ stride_eid,
65
+ ADVANCE_BS: tl.constexpr = True,
66
+ ):
67
+ """Per-(row, expert) prologue shared by the batched kernels: read the program
68
+ ids, look up the routed expert, and advance the A/B/C/Bs base pointers to this
69
+ row's slice. Returns ``(batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row)`` — the
70
+ resolved source/destination rows fold the gather/scatter out of the kernel bodies.
71
+
72
+ ``ADVANCE_BS=False`` leaves ``Bs`` at the buffer base (the mx scale leaf applies the expert
73
+ offset itself — its swizzled path indexes by 128-row block, not the row-major expert stride).
74
+
75
+ ``A``'s source row is ``GatherIdx[batch_id]`` when ``GatherIdx`` is not None (the gate_up reading
76
+ unexpanded activations, many-to-one for top_k > 1) else ``batch_id``; ``C``'s destination
77
+ row is ``ScatterIdx[batch_id]`` when ``ScatterIdx`` is not None else ``batch_id`` — the same virtual
78
+ gather/scatter ``matmul_grouped`` does, so the routed rows need no materialized copy.
79
+
80
+ The caller must early-return on the EP sentinel (``expert_id >= num_experts``)
81
+ before any load — the pointer arithmetic itself is harmless, only the loads on a
82
+ non-local expert would be out of bounds."""
83
+ batch_id = tl.program_id(axis=0)
84
+ pid_n = tl.program_id(axis=1)
85
+ # Cast to int64 to prevent overflow on expert_id * stride_b_e.
86
+ expert_id = tl.load(ExpertIds + batch_id * stride_eid).to(tl.int64)
87
+ in_row = tl.load(GatherIdx + batch_id).to(tl.int64) if GatherIdx is not None else batch_id
88
+ out_row = tl.load(ScatterIdx + batch_id).to(tl.int64) if ScatterIdx is not None else batch_id
89
+ A = A + in_row * stride_a_m
90
+ B = B + expert_id * stride_b_e
91
+ C = C + out_row * stride_c_m
92
+ if ADVANCE_BS:
93
+ Bs = Bs + expert_id * stride_bs_e
94
+ return batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row
95
+
96
+
97
+ @triton.jit
98
+ def store_row(
99
+ C,
100
+ accumulator,
101
+ pid_n,
102
+ stride_c_n,
103
+ BLOCK_SIZE_M: tl.constexpr,
104
+ BLOCK_SIZE_N: tl.constexpr,
105
+ ):
106
+ """Output epilogue shared by the batched kernels (``C`` already advanced to the
107
+ row). The fake-batch trick aliases all ``BLOCK_SIZE_M`` lanes to the same C row,
108
+ so a plain store would issue ``BLOCK_SIZE_M`` duplicate-address writes — benign on
109
+ NVIDIA WGMMA (last-write-wins of identical bytes) but hardware-undefined on Intel
110
+ XPU, where it corrupts the output. Mask so only lane 0 stores; the accumulator
111
+ rows are mathematically identical (same A row × same B), so lane 0 is correct."""
112
+ c = accumulator.to(C.dtype.element_ty)
113
+ offs_cm = tl.arange(0, BLOCK_SIZE_M)
114
+ offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
115
+ # offs_cm[:, None] * 0: broadcast to a [BM, BN] pointer tile (all rows alias the one C row)
116
+ # so the lane-0 mask below has a row axis to select; the M stride is deliberately 0.
117
+ c_ptrs = C + offs_cm[:, None] * 0 + stride_c_n * offs_cn[None, :]
118
+ tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
119
+
120
+
121
+ @bayesian_autotune(
122
+ get_accelerator_autotuning_configs(swap_ab=True, tune_block_n=True),
123
+ # one winner per (shape, requant): requant narrows the legal tiles to the quant block, so a
124
+ # shared entry could replay a sub-block winner on a launch whose output scale needs the block.
125
+ # GATE stacks a 2*BN-wide dot — a distinct config space that must not share a winner.
126
+ # BLOCK_N/BLOCK_K (the launch-pinned quant block) bound the legal tiles the same way —
127
+ # two checkpoints at one shape with different blocks must not share a winner.
128
+ ["N", "K", "S", "OUTPUT_FORMAT", "GATE", "BLOCK_N", "BLOCK_K"],
129
+ n_trials=100,
130
+ path_anchor_axes=PATH_ANCHOR_AXES,
131
+ prune_configs_by={
132
+ "early_config_prune": compose_pruners(
133
+ scale_subblock_pruner(),
134
+ fp8_dot_warp_pruner(),
135
+ )
136
+ },
137
+ )
138
+ @triton.jit
139
+ def w8a8_block_dynamic_fp8_matmul_batched_kernel(
140
+ A, # (S, K) E4M3 activations (pre-quantized once by the wrapper)
141
+ As, # (S, K // BLOCK_SIZE_K) fp32 per-row, per-K-block activation scales
142
+ B, # (num_experts, N, K) FP8 weights; under GATE the (num_experts, 2N, K) gate|up stack
143
+ Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (2N under GATE)
144
+ C, # (S, N) output; under an OUTPUT_FORMAT the FP8-requantized intermediate
145
+ Cs, # (S, N // BLOCK_SIZE_N) per-(row, block) output scale; written iff OUTPUT_FORMAT
146
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
147
+ ExpertIds, # (S,) — which expert each batch element routes to
148
+ GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
149
+ ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
150
+ # Shape
151
+ S,
152
+ N,
153
+ K,
154
+ # Strides
155
+ stride_a_m,
156
+ stride_a_k,
157
+ stride_as_m,
158
+ stride_b_e,
159
+ stride_b_k,
160
+ stride_b_n,
161
+ stride_bs_e,
162
+ stride_bs_k,
163
+ stride_bs_n,
164
+ stride_c_m,
165
+ stride_c_n,
166
+ stride_cs_m,
167
+ stride_cs_n,
168
+ stride_bias_e,
169
+ stride_bias_n,
170
+ stride_eid,
171
+ num_experts,
172
+ # Meta-parameters. BLOCK_N/BLOCK_K are the QUANT block (one scale per (BLOCK_N, BLOCK_K) tile of
173
+ # the weight); BLOCK_SIZE_* are the tuned COMPUTE tile. The N tile may subdivide the quant block
174
+ # (BLOCK_SIZE_N <= BLOCK_N); the K tile is the quant block (one scale step per K iteration).
175
+ BLOCK_SIZE_M: tl.constexpr,
176
+ BLOCK_SIZE_N: tl.constexpr,
177
+ BLOCK_SIZE_K: tl.constexpr,
178
+ BLOCK_N: tl.constexpr,
179
+ BLOCK_K: tl.constexpr,
180
+ SWAP_AB: tl.constexpr = False,
181
+ # Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
182
+ GATE: tl.constexpr = False,
183
+ ACT_FN: tl.constexpr = "silu",
184
+ SWIGLU_ALPHA: tl.constexpr = None,
185
+ SWIGLU_LIMIT: tl.constexpr = None,
186
+ # the output format name (None | "fp8")
187
+ OUTPUT_FORMAT: tl.constexpr = None,
188
+ SIMULATE_UNFUSED: tl.constexpr = False,
189
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
190
+ PDL: tl.constexpr = False,
191
+ ):
192
+ """Block-scale batched FP8 expert matmul kernel.
193
+
194
+ Each program handles one routed token row and one N-tile, looking up the
195
+ owning expert from ``ExpertIds``. Activations arrive pre-quantized (one wrapper
196
+ pass — an inline quant would repeat per N-tile and pay a per-tile amax reduction).
197
+
198
+ ``SWAP_AB`` (tuner axis, M=1 decode): load the weight output-rows-major ``[BN, BK]`` and put
199
+ those rows in the MMA M dim, padding the single token to the N=16 atom; column 0 of the
200
+ ``[BN, 16]`` accumulator is the result. No-swap keeps the token in M (padded to 16).
201
+
202
+ ``GATE`` fuses the gate|up projection: ``B`` is the ``(E, 2N, K)`` stack (gate rows [0,N),
203
+ interleaved rows), run as two dots (the decode-validated form), SwiGLU-combined, and — under
204
+ an ``OUTPUT_FORMAT`` — FP8-requantized into ``C`` + a per-(row, block) scalar ``Cs``. Every gate arm
205
+ folds out at compile time; ``GATE=False`` is the plain GEMM, bit-identical."""
206
+ if PDL:
207
+ gdc_wait()
208
+ batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
209
+ A,
210
+ B,
211
+ C,
212
+ Bs,
213
+ ExpertIds,
214
+ GatherIdx,
215
+ ScatterIdx,
216
+ stride_a_m,
217
+ stride_b_e,
218
+ stride_c_m,
219
+ stride_bs_e,
220
+ stride_eid,
221
+ )
222
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
223
+ if expert_id >= num_experts:
224
+ return
225
+
226
+ # One scale per quant block, broadcast over the tile, so an N tile narrower than BLOCK_N just
227
+ # reads its own block: tile pid_n sits in block (pid_n * BLOCK_SIZE_N) // BLOCK_N, which is pid_n
228
+ # when the tile IS the block. Narrower tiles multiply the N grid (see scale_subblock_pruner).
229
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
230
+ offs_bn = pid_n * n_width + tl.arange(0, n_width)
231
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
232
+ a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
233
+ as_ptrs = As + in_row * stride_as_m + tl.zeros((BLOCK_SIZE_M,), tl.int32)
234
+ # One gate|up weight tile + one block-scale pointer, like every other kernel: the n_width
235
+ # span shares a tile + a single dot, each row's scale block following from its global row.
236
+ b_ptrs = weight_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
237
+ bs_ptr = Bs + (pid_n * n_width // BLOCK_N) * stride_bs_n
238
+ bs_off = ((pid_n * n_width % BLOCK_N + tl.arange(0, n_width)) // BLOCK_N) * stride_bs_n
239
+ acc = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
240
+
241
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
242
+ a, a_s = load_act_block_dynamic(
243
+ a_ptrs, as_ptrs, None, None, 0, 0, 0, 0, 0, "pointer", False, False, False
244
+ )
245
+ w, b_s = load_weight_block_dynamic(
246
+ b_ptrs, b_ptrs, bs_ptr + bs_off, None, 0, 0, 0, 0, 0, 0,
247
+ GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K,
248
+ )
249
+ acc = block_dynamic_dot(acc, a, a_s, w, b_s, BLOCK_SIZE_K, SWAP_AB, False, True)
250
+ a_ptrs, as_ptrs, b_ptrs, bs_ptr, _, _ = advance_ptrs(
251
+ a_ptrs, as_ptrs, b_ptrs, bs_ptr, b_ptrs, bs_ptr,
252
+ BLOCK_SIZE_K * stride_a_k, 1, BLOCK_SIZE_K * stride_b_k, stride_bs_k,
253
+ "pointer", "pointer", True, True, False,
254
+ )
255
+
256
+ if PDL:
257
+ gdc_launch_dependents()
258
+ gemm_epilogue(
259
+ C, Cs, acc, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
260
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, BLOCK_K,
261
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
262
+ COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
263
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
264
+ global_row=expert_id,
265
+ )
266
+
267
+
268
+ @bayesian_autotune(
269
+ get_accelerator_autotuning_configs(swap_ab=True, tune_block_n=True),
270
+ # keyed like the block-dynamic sibling: requant narrows the legal tiles to the quant block
271
+ ["N", "K", "S", "OUTPUT_FORMAT", "GATE", "BLOCK_N", "BLOCK_K"],
272
+ n_trials=100,
273
+ path_anchor_axes=PATH_ANCHOR_AXES,
274
+ prune_configs_by={
275
+ "early_config_prune": compose_pruners(
276
+ scale_subblock_pruner(),
277
+ fp8_dot_warp_pruner(),
278
+ )
279
+ },
280
+ )
281
+ @triton.jit
282
+ def w8a8_block_static_fp8_matmul_batched_kernel(
283
+ A, # (S, K) E4M3 activations (pre-quantized against the static scale by the wrapper)
284
+ As, # scalar — static per-tensor activation scale (calibration-time)
285
+ B, # (num_experts, N, K) FP8 weights; under GATE the (num_experts, 2N, K) gate|up stack
286
+ Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (2N under GATE)
287
+ C, # (S, N) output; under an OUTPUT_FORMAT the FP8-requantized intermediate
288
+ Cs, # (S, N // BLOCK_SIZE_N) per-(row, block) output scale; written iff OUTPUT_FORMAT
289
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
290
+ ExpertIds, # (S,) — which expert each batch element routes to
291
+ GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
292
+ ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
293
+ # Shape
294
+ S,
295
+ N,
296
+ K,
297
+ # Strides
298
+ stride_a_m,
299
+ stride_a_k,
300
+ stride_b_e,
301
+ stride_b_k,
302
+ stride_b_n,
303
+ stride_bs_e,
304
+ stride_bs_k,
305
+ stride_bs_n,
306
+ stride_c_m,
307
+ stride_c_n,
308
+ stride_cs_m,
309
+ stride_cs_n,
310
+ stride_bias_e,
311
+ stride_bias_n,
312
+ stride_eid,
313
+ num_experts,
314
+ # Meta-parameters
315
+ BLOCK_SIZE_M: tl.constexpr,
316
+ BLOCK_SIZE_N: tl.constexpr,
317
+ BLOCK_SIZE_K: tl.constexpr,
318
+ BLOCK_N: tl.constexpr,
319
+ BLOCK_K: tl.constexpr,
320
+ SWAP_AB: tl.constexpr = False,
321
+ # Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
322
+ GATE: tl.constexpr = False,
323
+ ACT_FN: tl.constexpr = "silu",
324
+ SWIGLU_ALPHA: tl.constexpr = None,
325
+ SWIGLU_LIMIT: tl.constexpr = None,
326
+ OUTPUT_FORMAT: tl.constexpr = None, # None | "fp8" (per-(row, block) requant of the intermediate)
327
+ SIMULATE_UNFUSED: tl.constexpr = False,
328
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
329
+ PDL: tl.constexpr = False,
330
+ ):
331
+ """Block-scale batched FP8 expert matmul with a static (per-tensor) activation scale — the
332
+ block-dynamic batched sibling (one program per routed token + N-tile, fake-batch decode,
333
+ ``SWAP_AB``, ``GATE`` gate|up fusion) with the 2D ``block_static`` format: ``A`` arrives
334
+ pre-quantized against the calibrated scalar, per-block weight scales apply per-K-tile
335
+ (``accumulate`` ``"static"``, ``FAKE_BATCH``), and the scalar activation scale multiplies the
336
+ accumulator once after the loop. bf16 GLU output only (no fused requant). GATE=False is the plain GEMM."""
337
+ a_s_static = tl.load(As) # per-tensor static activation scale, applied post-loop
338
+ if PDL:
339
+ gdc_wait()
340
+ batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
341
+ A,
342
+ B,
343
+ C,
344
+ Bs,
345
+ ExpertIds,
346
+ GatherIdx,
347
+ ScatterIdx,
348
+ stride_a_m,
349
+ stride_b_e,
350
+ stride_c_m,
351
+ stride_bs_e,
352
+ stride_eid,
353
+ )
354
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
355
+ if expert_id >= num_experts:
356
+ return
357
+
358
+ # the N tile may subdivide the quant block — see the dynamic sibling / scale_subblock_pruner
359
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
360
+ offs_bn = pid_n * n_width + tl.arange(0, n_width)
361
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
362
+ a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
363
+ # One gate|up weight tile over the interleaved rows + one block-scale pointer;
364
+ # each weight row's scale block follows from its global row.
365
+ b_ptrs = weight_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
366
+ bs_ptr = Bs + (pid_n * n_width // BLOCK_N) * stride_bs_n
367
+ bs_off = ((pid_n * n_width % BLOCK_N + tl.arange(0, n_width)) // BLOCK_N) * stride_bs_n
368
+ acc = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
369
+
370
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
371
+ a, _ = load_act_static(a_ptrs, 0, 0, 0, None, 0, 0.0, "pointer", False) # pre-quantized E4M3 token (fake-batch replicated)
372
+ w, b_s = load_weight_static(
373
+ b_ptrs, b_ptrs, bs_ptr + bs_off, None, 0, 0, 0, 0, 0, 0,
374
+ GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K,
375
+ )
376
+ acc = static_dot(acc, a, w, b_s, SWAP_AB, BLOCK_SIZE_K, True)
377
+ a_ptrs, _, b_ptrs, bs_ptr, _, _ = advance_ptrs(
378
+ a_ptrs, a_ptrs, b_ptrs, bs_ptr, b_ptrs, bs_ptr,
379
+ BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, stride_bs_k,
380
+ "pointer", "pointer", False, True, False,
381
+ )
382
+
383
+ acc = acc * a_s_static
384
+ if PDL:
385
+ gdc_launch_dependents()
386
+ gemm_epilogue(
387
+ C, Cs, acc, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
388
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, BLOCK_K,
389
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
390
+ COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
391
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
392
+ global_row=expert_id,
393
+ )
394
+
395
+
396
+ @bayesian_autotune(
397
+ # S (routed rows) keyed like the block-dynamic/mx batched siblings — decode re-tunes per batch.
398
+ get_accelerator_autotuning_configs(tune_block_nk=True, swap_ab=True),
399
+ ["N", "K", "S"],
400
+ n_trials=100,
401
+ path_anchor_axes=PATH_ANCHOR_AXES,
402
+ # BLOCK_SIZE_K/N are tuned axes; the K-loop is maskless and the N-tile store is
403
+ # row-masked only — veto non-dividing tiles on both.
404
+ prune_configs_by={
405
+ "early_config_prune": compose_pruners(
406
+ fp8_dot_warp_pruner(),
407
+ block_within_dim_pruner("K"),
408
+ block_within_dim_pruner("N", "BLOCK_SIZE_N"),
409
+ )
410
+ },
411
+ )
412
+ @triton.jit
413
+ def w8a8_tensor_dynamic_fp8_matmul_batched_kernel(
414
+ A, # (S, K) pre-quantized FP8 activations
415
+ As, # (S,) per-token activation scales
416
+ B, # (num_experts, N, K) FP8 weight matrices
417
+ Bs, # (num_experts, 1, 1) per-tensor weight scales
418
+ C, # (S, N) output
419
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
420
+ ExpertIds, # (S,) — which expert each batch element routes to
421
+ GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
422
+ ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
423
+ # Shape
424
+ S,
425
+ N,
426
+ K,
427
+ # Strides
428
+ stride_a_m,
429
+ stride_a_k,
430
+ stride_as_m,
431
+ stride_b_e,
432
+ stride_b_k,
433
+ stride_b_n,
434
+ stride_bs_e,
435
+ stride_c_m,
436
+ stride_c_n,
437
+ stride_bias_e,
438
+ stride_bias_n,
439
+ stride_eid,
440
+ num_experts,
441
+ # Meta-parameters
442
+ BLOCK_SIZE_M: tl.constexpr,
443
+ BLOCK_SIZE_N: tl.constexpr,
444
+ BLOCK_SIZE_K: tl.constexpr,
445
+ SWAP_AB: tl.constexpr = False,
446
+ PDL: tl.constexpr = False,
447
+ ):
448
+ """Tensor-scale batched FP8 expert matmul kernel.
449
+
450
+ Activations are already quantized; the kernel applies per-token activation
451
+ scales and per-expert tensor weight scales.
452
+
453
+ ``SWAP_AB`` (tuner axis, M=1 decode): weight output rows in the MMA M dim (``B`` as ``[BN, BK]``,
454
+ single token padded to N=16); column 0 of the ``[BN, 16]`` accumulator is the result. Both
455
+ scales are per-token/per-tensor scalars, applied once after the loop, orientation-agnostic."""
456
+ if PDL:
457
+ gdc_wait()
458
+ batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
459
+ A,
460
+ B,
461
+ C,
462
+ Bs,
463
+ ExpertIds,
464
+ GatherIdx,
465
+ ScatterIdx,
466
+ stride_a_m,
467
+ stride_b_e,
468
+ stride_c_m,
469
+ stride_bs_e,
470
+ stride_eid,
471
+ )
472
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
473
+ if expert_id >= num_experts:
474
+ return
475
+
476
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
477
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
478
+ a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
479
+ b_ptrs = oriented_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
480
+ b_s = tl.load(Bs)
481
+ a_s = tl.load(As + in_row * stride_as_m)
482
+
483
+ accumulator = acc_init("dot", BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB)
484
+ for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
485
+ a, _ = load_act_plain(a_ptrs, 0, 0, 0, None, 0, "pointer", False)
486
+ b, _ = load_weight_plain(
487
+ b_ptrs, b_ptrs, 0, 0, 0, False, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K
488
+ )
489
+ accumulator = accumulator + fp8_dot(a, b, SWAP_AB, BLOCK_SIZE_K)
490
+ a_ptrs, _, b_ptrs, _, _, _ = advance_ptrs(
491
+ a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
492
+ BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
493
+ "pointer", "pointer", False, False, False,
494
+ )
495
+
496
+ accumulator = acc_finalize(accumulator, "dot", BLOCK_SIZE_N, SWAP_AB) * a_s * b_s
497
+ # this split keeps its own store path (ungated, per-tensor dequant) but takes the same bias
498
+ accumulator = add_bias(
499
+ accumulator, Bias, stride_bias_e, stride_bias_n, expert_id, pid_n, BLOCK_SIZE_N
500
+ )
501
+ if PDL:
502
+ gdc_launch_dependents()
503
+ store_row(C, accumulator, pid_n, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N)
504
+
505
+
506
+ # The MXFP4/MXFP8 (and packed-activation) splits key themselves — the tuner appends every tensor
507
+ # arg's dtype to its cache key. BLOCK_SIZE_M is always 1 here (per-token decode), so plain `dot`
508
+ # is excluded (only scalar / dot_scaled-swap are emitted); the swapped dot helper stays
509
+ # implemented for future shapes but is not fielded. Swap verdicts are B200 (sm_100) — re-measure
510
+ # on H100 or the target device before inheriting.
511
+ @bayesian_autotune(
512
+ get_accelerator_autotuning_configs(
513
+ mx=True,
514
+ tune_block_nk=True,
515
+ compute_modes=("dot_scaled", "scalar"),
516
+ swap_ab=True,
517
+ pre_hook=rebind_batched_mx_bs_descriptor,
518
+ ),
519
+ # ACTIVATION_FORMAT keys the inline act-quant grid: A stays raw bf16 under every
520
+ # format, so the tuner's dtype-appended key can't split W4A8 from W4A4 itself.
521
+ # SWIZZLED_SCALES keys the weight-scale load: it constrains the config space (BK % 128 == 0)
522
+ # and picks a different optimum (full-block descriptor vs the pointer scale), so the swizzled
523
+ # and un-swizzled launches of one shape must not share a tune.
524
+ # GATE keys the tune AND the GATE-conditional prunes (the packed-fp4 silent-zeros
525
+ # fence in mx_config_pruner runs once per key, so a GATE=False-first tune must not
526
+ # hand its winner to a GATE=True launch at the same shape).
527
+ # OUTPUT_FORMAT keys the requant epilogue explicitly (the C/Cs dtype append splits it
528
+ # incidentally today; this kernel's requant Cs is always row-major — no SWIZZLED_OUT axis).
529
+ ["N", "K", "S", "ACTIVATION_FORMAT", "SWIZZLED_SCALES", "GATE", "OUTPUT_FORMAT"],
530
+ n_trials=100,
531
+ path_anchor_axes=PATH_ANCHOR_AXES,
532
+ # BK-within-K + the sm_10x MMA-shape guards (swapped dot_scaled needs BN >= 128 for the
533
+ # native scaled-MMA; smaller-BN swap configs never win and mislead the TPE).
534
+ prune_configs_by={
535
+ "early_config_prune": compose_pruners(
536
+ mx_config_pruner("K", "N"), swizzled_scale_config_pruner(allow_gate_subblock=True), smem_pruner(),
537
+ dot_scaled_staging_pruner(),
538
+ )
539
+ },
540
+ )
541
+ @triton.jit
542
+ def mx_dynamic_matmul_batched_kernel(
543
+ A, # (S, K) activations: raw BF16/FP16 (inline-quant) or E4M3 (pre-quantized, As set)
544
+ As, # (S, K // SCALE_GROUP_K) UE8M0 act scales; None ⇒ inline-quant, read iff not None
545
+ B, # (num_experts, N, K[/2]); under GATE the (num_experts, 2N, K[/2]) gate|up stack
546
+ Bs, # (num_experts, N, K // SCALE_GROUP_K) UE8M0 weight scales (2N under GATE)
547
+ BSDescriptor, # host TMA descriptor over Bs when SWIZZLED (BN=128 bulk load); dummy otherwise
548
+ C, # (S, N[/2]) output; under an OUTPUT_FORMAT the MX-requantized intermediate
549
+ Cs, # (S, N // SCALE_GROUP_K) UE8M0 output scale; written iff OUTPUT_FORMAT
550
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
551
+ AsGlobal, # (1,) fp32 NVFP4 activation global g_a — SOLELY normalizes the inline raw-A quant (A/g_a); read iff not None
552
+ AsBsGlobal, # (num_experts,) fp32 NVFP4 WEIGHT global g_b — recovers on the accumulator together with AsGlobal (in-register product); read iff not None
553
+ CsGlobal, # (1,) fp32 NVFP4 output global (next proj's provided input_scale); normalizes the requant; read iff not None
554
+ ExpertIds, # (S,) — which expert each routed row uses
555
+ GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
556
+ ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
557
+ # Shape
558
+ S,
559
+ N,
560
+ K,
561
+ # Strides
562
+ stride_a_m,
563
+ stride_a_k,
564
+ stride_as_m,
565
+ stride_b_e,
566
+ stride_b_k,
567
+ stride_b_n,
568
+ stride_bs_e,
569
+ stride_bs_k,
570
+ stride_bs_n,
571
+ stride_c_m,
572
+ stride_c_n,
573
+ stride_cs_m,
574
+ stride_cs_n,
575
+ stride_bias_e,
576
+ stride_bias_n,
577
+ stride_as_global, # expert stride of AsGlobal (0 broadcasts a shared scalar)
578
+ stride_bs_global, # expert stride of AsBsGlobal (``global_scale_stride``)
579
+ stride_cs_global, # expert stride of CsGlobal (0 broadcasts a shared scalar)
580
+ stride_eid,
581
+ num_experts,
582
+ # Meta-parameters
583
+ BLOCK_SIZE_M: tl.constexpr,
584
+ BLOCK_SIZE_N: tl.constexpr,
585
+ BLOCK_SIZE_K: tl.constexpr,
586
+ SCALE_GROUP_K: tl.constexpr,
587
+ COMPUTE_MODE: tl.constexpr,
588
+ SWAP_AB: tl.constexpr = False,
589
+ # Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
590
+ GATE: tl.constexpr = False,
591
+ ACT_FN: tl.constexpr = "silu",
592
+ SWIGLU_ALPHA: tl.constexpr = None,
593
+ SWIGLU_LIMIT: tl.constexpr = None,
594
+ # the output format name (None | "mxfp8" | "mxfp4" | "nvfp4")
595
+ OUTPUT_FORMAT: tl.constexpr = None,
596
+ SIMULATE_UNFUSED: tl.constexpr = False,
597
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
598
+ ACTIVATION_FORMAT: tl.constexpr = "mxfp8",
599
+ # SWIZZLED_SCALES: Bs arrives pre-swizzled (SWIZZLE_32_4_4) — the checkpoint stores one layout,
600
+ # shared with the grouped (prefill) kernel. Read via load_weight's per-expert scale leaf off the
601
+ # single Bs pointer (+ BSDescriptor for the BN=128 bulk load); un-swizzled Bs takes the affine
602
+ # arm in the same leaf. The op never swizzles — a 3D caller runs un-swizzled at no penalty.
603
+ SWIZZLED_SCALES: tl.constexpr = False,
604
+ PDL: tl.constexpr = False,
605
+ ):
606
+ """Unified batched microscaled expert matmul (MXFP8/MXFP4/NVFP4, W4A8/W4A4) with
607
+ fused act quant.
608
+
609
+ One routed row + one N-tile per program; expert looked up from ``ExpertIds``. ``A`` is
610
+ quantized to E4M3 per K-group inline (UE8M0 scale). The weight dtype picks the
611
+ weight format (2 = packed E2M1 / MXFP4, 1 = unpacked E4M3 / MXFP8); ``COMPUTE_MODE``
612
+ picks ``tl.dot_scaled`` (native M=128) vs the scalar CUDA-core reduce (wins at decode).
613
+
614
+ ``SWAP_AB`` (tuner axis, M=1 decode): weight output rows in the MMA M dim (``B`` as ``[BN, BK]``,
615
+ single token padded to N=16); column 0 of the ``[BN, 16]`` accumulator is the result. dot_scaled
616
+ uses the swapped scaled-MMA; scalar reduces over K with the weight output-rows-major.
617
+ """
618
+ if PDL:
619
+ gdc_wait()
620
+ batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
621
+ A,
622
+ B,
623
+ C,
624
+ Bs,
625
+ ExpertIds,
626
+ GatherIdx,
627
+ ScatterIdx,
628
+ stride_a_m,
629
+ stride_b_e,
630
+ stride_c_m,
631
+ stride_bs_e,
632
+ stride_eid,
633
+ ADVANCE_BS=False, # scale leaf applies the per-expert offset (swizzled indexes by block)
634
+ )
635
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
636
+ if expert_id >= num_experts:
637
+ return
638
+
639
+ # each operand's format is its dtype: uint8 = packed E2M1 (two values per byte, W4A4
640
+ # for A / MXFP4 for B — it also keys the autotune cache), else E4M3
641
+ ACT_VALUES_PER_BYTE: tl.constexpr = 2 if A.dtype.element_ty == tl.uint8 else 1
642
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr = 2 if B.dtype.element_ty == tl.uint8 else 1
643
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
644
+ # Non-128 N: the partial last N-tile's pointer rows wrap into B (offs_bn % N) so the load
645
+ # never reads past the expert's N rows; the wrapped columns' output is masked off (N_COLS)
646
+ # in the epilogue. Inert when N % BLOCK_SIZE_N == 0 (the affine arm's BN|N veto), so it is
647
+ # load-bearing only for the swizzled arm, whose scale rides pid_n's block index, not offs_bn.
648
+ offs_bn = (pid_n * n_width + tl.arange(0, n_width)) % (2 * N if GATE else N)
649
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
650
+ offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
651
+ offs_ka = tl.arange(0, BLOCK_SIZE_K // ACT_VALUES_PER_BYTE)
652
+ a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_ka, stride_a_m, stride_a_k, "pointer", True)
653
+ # As is not None ⇒ pre-quantized: A is E4M3 + As UE8M0 (the down reading a requantized
654
+ # intermediate). Else A is raw, quantized inline onto ACTIVATION_FORMAT's grid (gate_up / plain —
655
+ # packed E2M1 under fp4, one act row per program so the quant is decode-free); As stays None.
656
+ if As is not None: # build the scale pointers only when the scale is read
657
+ as_ptrs = (
658
+ As
659
+ + in_row * stride_as_m
660
+ + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0
661
+ + offs_sf[None, :]
662
+ )
663
+ else:
664
+ as_ptrs = a_ptrs # dead placeholder so advance_ptrs can take it unconditionally
665
+ # GATE reads one weight tile spanning the interleaved gate|up rows, oriented by
666
+ # SWAP_AB; load_weight reads value + scale (swizzled/un-swizzled hidden) off these pointers.
667
+ b_ptrs = weight_tile_ptrs(
668
+ B, offs_bn, offs_kb, stride_b_n, stride_b_k, SWAP_AB
669
+ )
670
+ accumulator = acc_init(COMPUTE_MODE, BLOCK_SIZE_M, n_width, SWAP_AB)
671
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
672
+ a, a_scale = load_act_mx(
673
+ a_ptrs, as_ptrs, AsGlobal, expert_id * stride_as_global, None, None, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
674
+ "pointer", False, False, False,
675
+ BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, ACTIVATION_FORMAT,
676
+ )
677
+ b, b_s = load_weight_mx(
678
+ b_ptrs, b_ptrs, Bs, None, BSDescriptor, Bs, 0, 0, 0, 0, expert_id, pid_n, k, N, K,
679
+ stride_bs_e, stride_bs_n, stride_bs_k,
680
+ GATE, False, True, "pointer", SWAP_AB, SWIZZLED_SCALES,
681
+ BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
682
+ )
683
+ accumulator = mx_compute(
684
+ accumulator, a, a_scale, b, b_s, COMPUTE_MODE,
685
+ n_width, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB,
686
+ )
687
+ a_ptrs, as_ptrs, b_ptrs, _, _, _ = advance_ptrs(
688
+ a_ptrs, as_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
689
+ (BLOCK_SIZE_K // ACT_VALUES_PER_BYTE) * stride_a_k,
690
+ BLOCK_SIZE_K // SCALE_GROUP_K,
691
+ (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k,
692
+ 0,
693
+ "pointer", "pointer", As is not None, False, False,
694
+ )
695
+
696
+ # NVFP4 two-level: block e4m3 scales rode through the reduce; recover the combined per-tensor
697
+ if PDL:
698
+ gdc_launch_dependents()
699
+ gemm_epilogue(
700
+ C, Cs, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
701
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, SCALE_GROUP_K,
702
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
703
+ COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, FAKE_BATCH=True, N_COLS=N,
704
+ CsGlobal=CsGlobal,
705
+ stride_cs_global=stride_cs_global,
706
+ GlobalScale=AsBsGlobal,
707
+ stride_global_e=stride_bs_global,
708
+ GlobalScaleA=AsGlobal,
709
+ stride_global_a=stride_as_global,
710
+ global_row=expert_id,
711
+ Bias=Bias,
712
+ stride_bias_e=stride_bias_e,
713
+ stride_bias_n=stride_bias_n,
714
+ )
715
+
716
+
717
+ @bayesian_autotune(
718
+ # weight-only plain bf16 dot (fp4/fp8 weight upcast per-group in-loop), plus the swapped
719
+ # decode reduce (weight output rows lead the tile, no MMA — see the scope pruner).
720
+ get_accelerator_autotuning_configs(
721
+ tune_block_nk=True, compute_modes=("dot", "dot_scaled", "scalar"), swap_ab=True
722
+ ),
723
+ ["N", "K", "S", "GATE"],
724
+ n_trials=100,
725
+ path_anchor_axes=PATH_ANCHOR_AXES,
726
+ prune_configs_by={
727
+ # No BK-within-K veto: this kernel's tail tile slides back in bounds (see the K-loop),
728
+ # so any BK the shape can hold is legal — that is the point, it lets a K=2880 decode
729
+ # take a 256-byte burst instead of 32. BK must still FIT (the clamp needs K >= BK).
730
+ "early_config_prune": compose_pruners(
731
+ block_fits_dim_pruner("K"),
732
+ block_within_dim_pruner("N", "BLOCK_SIZE_N"),
733
+ mx_config_pruner("K", "N", block_within_k=False), # dot_scaled shape gates
734
+ weight_only_swap_scope_pruner(),
735
+ )
736
+ },
737
+ )
738
+ @triton.jit
739
+ def mx_weight_only_matmul_batched_kernel(
740
+ A, # (rows, K) raw BF16/FP16 activations — NOT quantized
741
+ B, # (num_experts, N, K[/2]) MXFP4/NVFP4/MXFP8 weights; 2N under GATE
742
+ Bs, # (num_experts, N, K // SCALE_GROUP_K) group scales — UE8M0 (MX) or E4M3 (NVFP4)
743
+ BsGlobal, # (num_experts,) fp32 NVFP4 per-expert global — recovers on the accumulator; read iff not None
744
+ C,
745
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
746
+ ExpertIds,
747
+ GatherIdx,
748
+ ScatterIdx,
749
+ S,
750
+ N,
751
+ K,
752
+ stride_a_m,
753
+ stride_a_k,
754
+ stride_b_e,
755
+ stride_b_k,
756
+ stride_b_n,
757
+ stride_bs_e,
758
+ stride_bs_n,
759
+ stride_bs_k,
760
+ stride_c_m,
761
+ stride_c_n,
762
+ stride_bias_e,
763
+ stride_bias_n,
764
+ stride_bs_global, # expert stride of BsGlobal (``global_scale_stride``)
765
+ stride_eid,
766
+ num_experts,
767
+ BLOCK_SIZE_M: tl.constexpr,
768
+ BLOCK_SIZE_N: tl.constexpr,
769
+ BLOCK_SIZE_K: tl.constexpr,
770
+ SCALE_GROUP_K: tl.constexpr,
771
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
772
+ COMPUTE_MODE: tl.constexpr = "dot",
773
+ SWAP_AB: tl.constexpr = False,
774
+ GATE: tl.constexpr = False,
775
+ ACT_FN: tl.constexpr = "silu",
776
+ SWIGLU_ALPHA: tl.constexpr = None,
777
+ SWIGLU_LIMIT: tl.constexpr = None,
778
+ SIMULATE_UNFUSED: tl.constexpr = False,
779
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
780
+ PDL: tl.constexpr = False,
781
+ ):
782
+ """weight-only batched (decode) expert matmul: raw bf16 activations against MXFP4/MXFP8 weights upcast
783
+ to bf16 in-loop (unpack + per-group group-scale), plain ``tl.dot``. One routed row + one N-tile
784
+ per program (expert from ``ExpertIds``). Pointer/affine. ``SWAP_AB`` (tuner axis, M=1 decode):
785
+ weight output rows lead the tile and the CUDA-core reduce replaces the MMA. ``GATE`` fuses gate|up."""
786
+ if PDL:
787
+ gdc_wait()
788
+ batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
789
+ A, B, C, Bs, ExpertIds, GatherIdx, ScatterIdx,
790
+ stride_a_m, stride_b_e, stride_c_m, stride_bs_e, stride_eid, ADVANCE_BS=False,
791
+ )
792
+ if expert_id >= num_experts: # EP sentinel: non-local expert, output left uninit
793
+ return
794
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
795
+ # non-128 N: the last N-tile's rows run past B (N=320, n_width=256 -> tile 2 wants rows
796
+ # 512..767 of 640), so wrap them back into the tensor. The wrapped columns are masked off by
797
+ # the epilogue's N_COLS. An unwrapped read is out of bounds and faults once the allocation
798
+ # layout puts an unmapped page there — invisible in a short run, an illegal access in a long one.
799
+ offs_bn = (pid_n * n_width + tl.arange(0, n_width)) % (2 * N if GATE else N)
800
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
801
+ a_ptrs = operand_tile_ptrs(
802
+ A, tl.arange(0, BLOCK_SIZE_M) * 0, tl.arange(0, BLOCK_SIZE_K),
803
+ stride_a_m, stride_a_k, "pointer", True,
804
+ )
805
+ b_ptrs = weight_tile_ptrs(
806
+ B, offs_bn, offs_kb, stride_b_n, stride_b_k, SWAP_AB
807
+ )
808
+ accumulator = acc_init(COMPUTE_MODE, BLOCK_SIZE_M, n_width, SWAP_AB)
809
+ # Each trip indexes off the BASE tile by the scalar ``k_start`` rather than advancing a
810
+ # pointer tile in place: an advanced tile stays live across the loop, and at [2*BN, BK/2]
811
+ # int64 that is ~128 registers per thread of addresses alone — enough to halve resident
812
+ # CTAs and starve the weight stream (measured n_regs 128 with spills vs 55 for the same
813
+ # math indexed from a base). The clamp below already computes ``k_start``, so the walk is
814
+ # the same addresses either way.
815
+ # The K-loop is maskless, which normally pins BK to a divisor of K — leaving a
816
+ # non-power-of-two K (gpt-oss 2880) with BK=64, a 32-byte burst per weight row over 45
817
+ # trips. Instead the LAST tile slides back to end at K (``k_start`` clamp): every load
818
+ # stays in bounds, the tile re-reads positions the previous trip already accumulated, and
819
+ # zeroing those activation lanes cancels them for every arm (a zero operand contributes
820
+ # nothing to the dot, the scaled MMA, or the reduce). BK | K makes the clamp a no-op, so
821
+ # aligned shapes keep today's pointer walk bit-for-bit.
822
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
823
+ k_start = tl.minimum(k * BLOCK_SIZE_K, K - BLOCK_SIZE_K)
824
+ overlap = k * BLOCK_SIZE_K - k_start
825
+ a, _ = load_act_plain(
826
+ a_ptrs + k_start * stride_a_k, 0, 0, 0, None, 0, "pointer", False
827
+ )
828
+ if overlap > 0: # only the slid tail trip pays the select
829
+ a = tl.where(tl.arange(0, BLOCK_SIZE_K)[None, :] >= overlap, a, 0.0)
830
+ b_k = b_ptrs + (k_start // WEIGHT_VALUES_PER_BYTE) * stride_b_k
831
+ w, w_s = load_weight_mx(
832
+ b_k, b_k, Bs, None, None, Bs, 0, 0, 0, 0, expert_id, pid_n, k, N, K,
833
+ stride_bs_e, stride_bs_n, stride_bs_k,
834
+ GATE, False, True, "pointer", SWAP_AB, False,
835
+ BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
836
+ k_col_off=-(overlap // SCALE_GROUP_K),
837
+ )
838
+ accumulator = mx_weight_only_compute(
839
+ accumulator, a, w, w_s, COMPUTE_MODE, BLOCK_SIZE_K, n_width, SCALE_GROUP_K,
840
+ SWAP_AB,
841
+ )
842
+
843
+ if PDL:
844
+ gdc_launch_dependents()
845
+ gemm_epilogue(
846
+ C, None, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, 1, 1,
847
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
848
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
849
+ COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, FAKE_BATCH=True, N_COLS=N,
850
+ GlobalScale=BsGlobal,
851
+ stride_global_e=stride_bs_global,
852
+ global_row=expert_id,
853
+ Bias=Bias,
854
+ stride_bias_e=stride_bias_e,
855
+ stride_bias_n=stride_bias_n,
856
+ )
857
+
858
+
859
+ @bayesian_autotune(
860
+ get_accelerator_autotuning_configs(tune_block_nk=True, swap_ab=True),
861
+ # S (routed rows) keyed like the fp8/mx batched siblings — decode re-tunes per batch;
862
+ # GATE keys the gate|up arm separately (its stacked dot is 2*BN wide).
863
+ ["N", "K", "S", "GATE"],
864
+ n_trials=100,
865
+ path_anchor_axes=PATH_ANCHOR_AXES,
866
+ # BLOCK_SIZE_K/N are tuned axes; the K-loop is maskless and the N-tile store is
867
+ # row-masked only — veto non-dividing tiles on both.
868
+ prune_configs_by={
869
+ "early_config_prune": compose_pruners(
870
+ block_within_dim_pruner("K"),
871
+ block_within_dim_pruner("N", "BLOCK_SIZE_N"),
872
+ )
873
+ },
874
+ )
875
+ @triton.jit
876
+ def full_precision_matmul_batched_kernel(
877
+ A, # (rows, K) BF16/FP16 activations
878
+ B, # (num_experts, N, K) weights in A's dtype; under GATE the (num_experts, 2N, K) gate|up stack
879
+ C, # (S, N) output; under GATE the GLU intermediate
880
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
881
+ ExpertIds, # (S,) — which expert each batch element routes to
882
+ GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
883
+ ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
884
+ # Shape
885
+ S,
886
+ N,
887
+ K,
888
+ # Strides
889
+ stride_a_m,
890
+ stride_a_k,
891
+ stride_b_e,
892
+ stride_b_k,
893
+ stride_b_n,
894
+ stride_c_m,
895
+ stride_c_n,
896
+ stride_bias_e,
897
+ stride_bias_n,
898
+ stride_eid,
899
+ num_experts,
900
+ # Meta-parameters
901
+ BLOCK_SIZE_M: tl.constexpr,
902
+ BLOCK_SIZE_N: tl.constexpr,
903
+ BLOCK_SIZE_K: tl.constexpr,
904
+ SWAP_AB: tl.constexpr = False,
905
+ # Gate|up fusion epilogue (GATE=False -> plain batched GEMM). No requant arm: the
906
+ # full-precision chain has no quantized intermediate — down consumes the GLU output as is.
907
+ GATE: tl.constexpr = False,
908
+ ACT_FN: tl.constexpr = "silu",
909
+ SWIGLU_ALPHA: tl.constexpr = None,
910
+ SWIGLU_LIMIT: tl.constexpr = None,
911
+ SIMULATE_UNFUSED: tl.constexpr = False,
912
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
913
+ PDL: tl.constexpr = False,
914
+ ):
915
+ """Full-precision batched expert matmul kernel: plain ``tl.dot`` over unquantized
916
+ BF16/FP16 activations and weights, fp32 accumulation, no scales anywhere. ``GATE``
917
+ computes gate|up as ONE stacked tile + dot (straight-line, both orientations) and
918
+ applies the ``ACT_FN``/SwiGLU ``glu``. ``SWAP_AB`` (tuner axis, M=1 decode): weight
919
+ output rows in the MMA M dim, the single token padded to the N=16 atom."""
920
+ if PDL:
921
+ gdc_wait()
922
+ batch_id, pid_n, expert_id, A, B, C, _, in_row, out_row = expert_setup(
923
+ A,
924
+ B,
925
+ C,
926
+ None, # no scales on the full-precision path
927
+ ExpertIds,
928
+ GatherIdx,
929
+ ScatterIdx,
930
+ stride_a_m,
931
+ stride_b_e,
932
+ stride_c_m,
933
+ 0,
934
+ stride_eid,
935
+ ADVANCE_BS=False,
936
+ )
937
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
938
+ if expert_id >= num_experts:
939
+ return
940
+
941
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
942
+ offs_bn = pid_n * n_width + tl.arange(0, n_width)
943
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
944
+ a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
945
+ # GATE reads one weight tile spanning the interleaved gate|up rows;
946
+ # GATE=False -> the plain oriented tile.
947
+ b_ptrs = weight_tile_ptrs(
948
+ B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB
949
+ )
950
+
951
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
952
+ for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
953
+ a, _ = load_act_plain(a_ptrs, 0, 0, 0, None, 0, "pointer", False)
954
+ w, _ = load_weight_plain(
955
+ b_ptrs, b_ptrs, 0, 0, 0, GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K
956
+ )
957
+ accumulator = accumulator + fp8_dot(a, w, SWAP_AB, BLOCK_SIZE_K)
958
+ a_ptrs, _, b_ptrs, _, _, _ = advance_ptrs(
959
+ a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
960
+ BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
961
+ "pointer", "pointer", False, False, False,
962
+ )
963
+
964
+ if PDL:
965
+ gdc_launch_dependents()
966
+ gemm_epilogue(
967
+ C, None, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, 1, 1,
968
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
969
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
970
+ COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
971
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
972
+ global_row=expert_id,
973
+ )
974
+
975
+
976
+ GATE_UNSTACK_MAX_S = 16 # the unstacked-gate decode band (see the dispatch in the wrapper)
977
+
978
+
979
+ @compile_time_only_triton_op(
980
+ add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul_batched"),
981
+ mutates_args=(),
982
+ opaque=True,
983
+ )
984
+ def w8a8_block_dynamic_fp8_matmul_batched(
985
+ A: torch.Tensor,
986
+ B: torch.Tensor,
987
+ As: torch.Tensor | None,
988
+ Bs: torch.Tensor,
989
+ expert_ids: torch.Tensor,
990
+ block_size: list[int],
991
+ gate: bool = False,
992
+ act_fn: str = "silu",
993
+ swiglu_alpha: float | None = None,
994
+ swiglu_limit: float | None = None,
995
+ simulate_unfused: bool = False,
996
+ activation_format: str | None = None,
997
+ quantize_output: bool = False,
998
+ output_dtype: torch.dtype | None = None,
999
+ gather_idx: torch.Tensor | None = None,
1000
+ scatter_idx: torch.Tensor | None = None,
1001
+ bias: torch.Tensor | None = None,
1002
+ ) -> list[torch.Tensor]:
1003
+ """Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T; activations
1004
+ quantized offline in one pass. The ``gate``/``act_fn``/``swiglu_*``/``requant``/``output_dtype``
1005
+ flags are the dispatcher's gate|up fusion kwargs (torch custom ops take only primitives —
1006
+ ``matmul_batched`` unpacks the bundle). ``gather_idx``/``scatter_idx`` map the source row of A
1007
+ / destination row of C per program (None = row s). Returns ``[C]``, or ``[C, Cs]`` under
1008
+ ``requant``.
1009
+
1010
+ A: (rows, K) raw bf16/fp16 activations — rows addressed via ``gather_idx``
1011
+ B: (num_experts, N, K) FP8 weights; under ``gate`` the (num_experts, 2N, K) gate|up stack
1012
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales (2N under gate)
1013
+ """
1014
+ validate_dense_operands(A, B)
1015
+
1016
+ output_dtype = resolve_output_dtype(output_dtype, A, As)
1017
+ # S is the routed-row count (one program per expert_id); A may hold fewer rows when
1018
+ # gather_idx maps many programs to one source row (gate_up reading unexpanded hidden).
1019
+ K = A.shape[1]
1020
+ S = expert_ids.shape[0]
1021
+ num_experts, n_rows, N = expert_weight_shape(B, gate)
1022
+
1023
+ assert len(block_size) == 2, (
1024
+ f"block_size must be [block_n, block_k], got {block_size}"
1025
+ )
1026
+ block_n, block_k = block_size[0], block_size[1]
1027
+ require_moe_dims_aligned(N, K, block_n, block_k)
1028
+ assert Bs.shape == (num_experts, n_rows // block_n, K // block_k), (
1029
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {n_rows // block_n}, {K // block_k})"
1030
+ )
1031
+
1032
+ bs_u8 = ue8m0_as_uint8(Bs)
1033
+ # Offline quant wins here even at decode. An inline quant would rerun once per N-tile
1034
+ # of the (S x N-tiles) grid, and block-FP8 quant is an fp32 amax+div per element, so
1035
+ # the redundant work outweighs the extra launch down to T=1 (inline only edges ahead
1036
+ # near T=64). UE8M0 quant is ~free per pass, which is why the MX kernels do it inline.
1037
+ assert activation_format in (None, "fp8"), (
1038
+ f"block-dynamic activations are E4M3 ('fp8'), got {activation_format!r}"
1039
+ )
1040
+ output_format = "fp8" if quantize_output else None
1041
+ requant = output_format is not None
1042
+ # the requantized intermediate's scale groups follow gate_up's block_n, and the
1043
+ # down consumes per-block_k — a non-square block format would misalign them
1044
+ assert not requant or block_size[0] == block_size[1], (
1045
+ f"the fused 'fp8' requant needs square quant blocks, got {block_size}"
1046
+ )
1047
+ # The decode band runs the ``gate`` contract UNSTACKED: this same op without the gate
1048
+ # (one plain GEMM over the stacked weight), then the one-kernel ``fused_glu`` — bit-
1049
+ # identical rounding, and exactly the unfused-reference order, so ``simulate_unfused``
1050
+ # needs no carve-out. Holding the gate AND up tiles per CTA doubles the smem footprint
1051
+ # and halves occupancy precisely where the launch is weight-bandwidth-bound, and the
1052
+ # fp8 ``tl.dot`` — unlike the MX/NVFP4 scaled MMA, whose wide M operand earns the
1053
+ # native instruction — gains nothing back: DSV3 shape, same 235MB weight read, stacked
1054
+ # 69.8µs vs unstacked 49.8µs at S=8, a wash by S=32. A requant is the same offline
1055
+ # quant the raw activation gets below, applied to the GLU output — quantizing the bf16
1056
+ # intermediate (the unfused-reference order) where the stacked epilogue quantizes its
1057
+ # fp32 accumulator, a sub-quantum difference consistent with the band's semantics.
1058
+ if gate and S <= GATE_UNSTACK_MAX_S:
1059
+ [gate_up] = w8a8_block_dynamic_fp8_matmul_batched(
1060
+ A, B, As, Bs, expert_ids, block_size,
1061
+ activation_format=activation_format, output_dtype=output_dtype,
1062
+ gather_idx=gather_idx, scatter_idx=scatter_idx,
1063
+ )
1064
+ out = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit,
1065
+ quant_group=block_n if requant else None,
1066
+ use_ue8m0=bs_u8.dtype == torch.uint8)
1067
+ return list(out) if requant else [out]
1068
+ # A raw (As is None) -> quantize here (offline); else pre-quantized (As given, e.g. the
1069
+ # requantized intermediate handed to the down projection).
1070
+ if As is None:
1071
+ A_q, A_s = fp8_act_quant_block_dynamic(
1072
+ A, block_k, use_ue8m0=bs_u8.dtype == torch.uint8
1073
+ )
1074
+ else:
1075
+ A_q, A_s = A, As
1076
+ if requant:
1077
+ C = A.new_empty(S, N, dtype=FP8_DTYPE)
1078
+ # UE8M0 model (ue8m0 weights) -> UE8M0 intermediate scales (whole-model contract);
1079
+ # the kernel infers the requant format from this dtype. fp32 weights keep fp32.
1080
+ cs_dtype = bs_u8.dtype # uint8 (UE8M0) or float32 — the whole-model scale format
1081
+ Cs = torch.empty(S, N // block_n, device=A.device, dtype=cs_dtype)
1082
+ else:
1083
+ C = A.new_empty(S, N, dtype=output_dtype)
1084
+ Cs = None # unread without an OUTPUT_FORMAT; strides literal below
1085
+
1086
+ # the N tile is tuned (it may subdivide the block scale — see scale_subblock_pruner)
1087
+ grid = lambda meta: (S, triton.cdiv(N, meta["BLOCK_SIZE_N"])) # noqa: E731
1088
+
1089
+ with device_context(A.device):
1090
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1091
+ compile_time_only_triton_wrap(w8a8_block_dynamic_fp8_matmul_batched_kernel)[
1092
+ grid
1093
+ ](
1094
+ A_q,
1095
+ A_s,
1096
+ B,
1097
+ bs_u8,
1098
+ C,
1099
+ Cs,
1100
+ bias,
1101
+ expert_ids,
1102
+ gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
1103
+ scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
1104
+ S,
1105
+ N,
1106
+ K,
1107
+ A_q.stride(0),
1108
+ A_q.stride(1),
1109
+ A_s.stride(0),
1110
+ B.stride(0),
1111
+ B.stride(2),
1112
+ B.stride(1),
1113
+ bs_u8.stride(0),
1114
+ bs_u8.stride(2),
1115
+ bs_u8.stride(1),
1116
+ C.stride(0),
1117
+ C.stride(1),
1118
+ Cs.stride(0) if requant else 1,
1119
+ Cs.stride(1) if requant else 1,
1120
+ bias_stride_e,
1121
+ bias_stride_n,
1122
+ expert_ids.stride(0),
1123
+ BLOCK_SIZE_K=block_k,
1124
+ BLOCK_N=block_n,
1125
+ BLOCK_K=block_k,
1126
+ num_experts=num_experts,
1127
+ GATE=gate,
1128
+ ACT_FN=act_fn,
1129
+ SWIGLU_ALPHA=swiglu_alpha,
1130
+ SWIGLU_LIMIT=swiglu_limit,
1131
+ OUTPUT_FORMAT=output_format,
1132
+ SIMULATE_UNFUSED=simulate_unfused,
1133
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1134
+ PDL=decode_pdl(),
1135
+ launch_pdl=decode_pdl(),
1136
+ )
1137
+
1138
+ return [C, Cs] if requant else [C]
1139
+
1140
+
1141
+ @compile_time_only_triton_op(
1142
+ add_op_namespace_prefix("w8a8_block_static_fp8_matmul_batched"),
1143
+ mutates_args=(),
1144
+ opaque=True,
1145
+ )
1146
+ def w8a8_block_static_fp8_matmul_batched(
1147
+ A: torch.Tensor,
1148
+ B: torch.Tensor,
1149
+ As: torch.Tensor,
1150
+ Bs: torch.Tensor,
1151
+ expert_ids: torch.Tensor,
1152
+ block_size: list[int],
1153
+ gate: bool = False,
1154
+ act_fn: str = "silu",
1155
+ swiglu_alpha: float | None = None,
1156
+ swiglu_limit: float | None = None,
1157
+ simulate_unfused: bool = False,
1158
+ activation_format: str | None = None,
1159
+ quantize_output: bool = False,
1160
+ output_dtype: torch.dtype | None = None,
1161
+ gather_idx: torch.Tensor | None = None,
1162
+ scatter_idx: torch.Tensor | None = None,
1163
+ bias: torch.Tensor | None = None,
1164
+ ) -> list[torch.Tensor]:
1165
+ """Block-scale batched FP8 matmul with a static (per-tensor calibrated) activation scale — the
1166
+ block-dynamic batched sibling with the 2D ``block_static`` format. ``A`` is raw here: the op
1167
+ quantizes it against the scalar ``As`` (offline), the kernel applies the per-block weight scales
1168
+ in the K-loop and the scalar once post-loop. Returns ``[C]``, or ``[C, Cs]`` under
1169
+ ``output_format="fp8"`` (the per-row output scale is independent of the per-tensor input scale).
1170
+
1171
+ A: (rows, K) raw bf16/fp16 activations — rows addressed via ``gather_idx``
1172
+ B: (num_experts, N, K) FP8 weights; under ``gate`` the (num_experts, 2N, K) gate|up stack
1173
+ As: scalar / (1,) — the calibrated per-tensor (static) activation scale
1174
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales (2N under gate)
1175
+ """
1176
+ validate_dense_operands(A, B)
1177
+
1178
+ output_dtype = resolve_output_dtype(output_dtype, A, None)
1179
+ K = A.shape[1]
1180
+ S = expert_ids.shape[0]
1181
+ num_experts, n_rows, N = expert_weight_shape(B, gate)
1182
+
1183
+ assert len(block_size) == 2, (
1184
+ f"block_size must be [block_n, block_k], got {block_size}"
1185
+ )
1186
+ block_n, block_k = block_size[0], block_size[1]
1187
+ require_moe_dims_aligned(N, K, block_n, block_k)
1188
+ assert Bs.shape == (num_experts, n_rows // block_n, K // block_k), (
1189
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {n_rows // block_n}, {K // block_k})"
1190
+ )
1191
+ assert activation_format in (None, "fp8"), (
1192
+ f"block-static activations are E4M3 ('fp8'), got {activation_format!r}"
1193
+ )
1194
+ output_format = "fp8" if quantize_output else None
1195
+ requant = output_format is not None
1196
+ assert not requant or block_n == block_k, (
1197
+ f"the fused 'fp8' requant needs square quant blocks, got {block_size}"
1198
+ )
1199
+
1200
+ As = As.reshape(1).to(torch.float32)
1201
+ bs_u8 = ue8m0_as_uint8(Bs)
1202
+ # Pre-quantize the raw activations against the calibrated scalar (offline; the kernel folds
1203
+ # the scalar back post-loop).
1204
+ A_q = (A.to(torch.float32) / As).to(FP8_DTYPE)
1205
+ if requant:
1206
+ C = A.new_empty(S, N, dtype=FP8_DTYPE)
1207
+ Cs = torch.empty(S, N // block_n, device=A.device, dtype=bs_u8.dtype)
1208
+ else:
1209
+ C = A.new_empty(S, N, dtype=output_dtype)
1210
+ Cs = None # unread without an OUTPUT_FORMAT; strides literal below
1211
+
1212
+ # the N tile is tuned (it may subdivide the block scale — see scale_subblock_pruner)
1213
+ grid = lambda meta: (S, triton.cdiv(N, meta["BLOCK_SIZE_N"])) # noqa: E731
1214
+
1215
+ with device_context(A.device):
1216
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1217
+ compile_time_only_triton_wrap(w8a8_block_static_fp8_matmul_batched_kernel)[
1218
+ grid
1219
+ ](
1220
+ A_q,
1221
+ As,
1222
+ B,
1223
+ bs_u8,
1224
+ C,
1225
+ Cs,
1226
+ bias,
1227
+ expert_ids,
1228
+ gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
1229
+ scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
1230
+ S,
1231
+ N,
1232
+ K,
1233
+ A_q.stride(0),
1234
+ A_q.stride(1),
1235
+ B.stride(0),
1236
+ B.stride(2),
1237
+ B.stride(1),
1238
+ bs_u8.stride(0),
1239
+ bs_u8.stride(2),
1240
+ bs_u8.stride(1),
1241
+ C.stride(0),
1242
+ C.stride(1),
1243
+ Cs.stride(0) if requant else 1,
1244
+ Cs.stride(1) if requant else 1,
1245
+ bias_stride_e,
1246
+ bias_stride_n,
1247
+ expert_ids.stride(0),
1248
+ num_experts,
1249
+ BLOCK_SIZE_K=block_k,
1250
+ BLOCK_N=block_n,
1251
+ BLOCK_K=block_k,
1252
+ GATE=gate,
1253
+ ACT_FN=act_fn,
1254
+ SWIGLU_ALPHA=swiglu_alpha,
1255
+ SWIGLU_LIMIT=swiglu_limit,
1256
+ OUTPUT_FORMAT=output_format,
1257
+ SIMULATE_UNFUSED=simulate_unfused,
1258
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1259
+ PDL=decode_pdl(),
1260
+ launch_pdl=decode_pdl(),
1261
+ )
1262
+
1263
+ return [C, Cs] if requant else [C]
1264
+
1265
+
1266
+ @compile_time_only_triton_op(
1267
+ add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul_batched"),
1268
+ mutates_args=(),
1269
+ opaque=True,
1270
+ )
1271
+ def w8a8_tensor_dynamic_fp8_matmul_batched(
1272
+ A: torch.Tensor,
1273
+ B: torch.Tensor,
1274
+ As: torch.Tensor | None,
1275
+ Bs: torch.Tensor,
1276
+ expert_ids: torch.Tensor,
1277
+ output_dtype: torch.dtype | None = None,
1278
+ gather_idx: torch.Tensor | None = None,
1279
+ scatter_idx: torch.Tensor | None = None,
1280
+ bias: torch.Tensor | None = None,
1281
+ ) -> torch.Tensor:
1282
+ """Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T. ``A`` raw
1283
+ (``As`` None) -> quantized here (offline, per-token); else pre-quantized (``As`` given).
1284
+ ``gather_idx``/``scatter_idx`` map the source row of A / destination row of C per program
1285
+ (None = row s).
1286
+
1287
+ A: (rows, K) raw or pre-quantized FP8 activations — rows addressed via ``gather_idx``
1288
+ B: (num_experts, N, K) FP8 expert weights
1289
+ As: (rows,) per-token scales, or None when A is raw
1290
+ Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
1291
+ """
1292
+ validate_dense_operands(A, B)
1293
+
1294
+ output_dtype = resolve_output_dtype(output_dtype, A, As)
1295
+ K = A.shape[1]
1296
+ S = expert_ids.shape[0]
1297
+ num_experts, N, _ = B.shape
1298
+
1299
+ # Normalize Bs to (num_experts, 1, 1)
1300
+ Bs = normalize_per_expert_scale(Bs, num_experts)
1301
+
1302
+ bs_u8 = ue8m0_as_uint8(Bs)
1303
+ if As is None:
1304
+ qA, As = fp8_act_quant_tensor_wide(A, K)
1305
+ else:
1306
+ qA = A
1307
+ C = A.new_empty(S, N, dtype=output_dtype)
1308
+
1309
+ def grid(META):
1310
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
1311
+
1312
+ with device_context(A.device):
1313
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1314
+ compile_time_only_triton_wrap(w8a8_tensor_dynamic_fp8_matmul_batched_kernel)[
1315
+ grid
1316
+ ](
1317
+ qA,
1318
+ As,
1319
+ B,
1320
+ bs_u8,
1321
+ C,
1322
+ bias,
1323
+ expert_ids,
1324
+ gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
1325
+ scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
1326
+ S,
1327
+ N,
1328
+ K,
1329
+ qA.stride(0),
1330
+ qA.stride(1),
1331
+ As.stride(0),
1332
+ B.stride(0),
1333
+ B.stride(2),
1334
+ B.stride(1),
1335
+ bs_u8.stride(0),
1336
+ C.stride(0),
1337
+ C.stride(1),
1338
+ bias_stride_e,
1339
+ bias_stride_n,
1340
+ expert_ids.stride(0),
1341
+ num_experts=num_experts,
1342
+ PDL=decode_pdl(),
1343
+ launch_pdl=decode_pdl(),
1344
+ )
1345
+
1346
+ return C
1347
+
1348
+
1349
+ @compile_time_only_triton_op(
1350
+ add_op_namespace_prefix("mx_dynamic_matmul_batched"), mutates_args=(), opaque=True
1351
+ )
1352
+ def mx_dynamic_matmul_batched(
1353
+ A: torch.Tensor,
1354
+ B: torch.Tensor,
1355
+ As: torch.Tensor | None,
1356
+ Bs: torch.Tensor,
1357
+ expert_ids: torch.Tensor,
1358
+ gate: bool = False,
1359
+ act_fn: str = "silu",
1360
+ swiglu_alpha: float | None = None,
1361
+ swiglu_limit: float | None = None,
1362
+ simulate_unfused: bool = False,
1363
+ activation_format: str | None = None,
1364
+ quantize_output: bool = False,
1365
+ output_dtype: torch.dtype | None = None,
1366
+ gather_idx: torch.Tensor | None = None,
1367
+ scatter_idx: torch.Tensor | None = None,
1368
+ a_global_scale: torch.Tensor | None = None,
1369
+ b_global_scale: torch.Tensor | None = None,
1370
+ output_global_scale: torch.Tensor | None = None,
1371
+ bias: torch.Tensor | None = None,
1372
+ ) -> list[torch.Tensor]:
1373
+ """Batched MX matmul ``C[s] = A[s] @ B[expert_ids[s]].T``; activations quantized
1374
+ inline in the kernel (decode: one act row per program, inline is free). The
1375
+ ``gate``/``act_fn``/``swiglu_*``/``requant``/``output_dtype`` flags are ``matmul_batched``'s
1376
+ gate|up fusion kwargs. ``gather_idx``/``scatter_idx`` map the
1377
+ source row of A / destination row of C per program (None = row s). Returns ``[C]``, or
1378
+ ``[C, Cs]`` under ``requant``. Weight format is detected from ``B.dtype``: ``int8`` → packed
1379
+ E2M1 (MXFP4, ``B`` is ``(num_experts, N, K//2)``); ``float8_e4m3fn`` → unpacked E4M3 (MXFP8);
1380
+ both use UE8M0 group-32 scales; under ``gate`` the (num_experts, 2N, K[/2]) gate|up stack.
1381
+
1382
+ A: (rows, K) activations — raw bf16/fp16/fp32 (inline-quant) or pre-quantized E4M3
1383
+ expert_ids: (S,) which expert each routed row uses
1384
+ """
1385
+ assert A.ndim == 2 and B.ndim == 3 and Bs.ndim in (3, 5) # 5D = pre-swizzled
1386
+ assert expert_ids.ndim == 1
1387
+ # A raw (As None) -> quantized inline in the kernel (decode-free UE8M0); pre-quantized
1388
+ # (As given, e.g. the down reading a requantized intermediate) -> loaded with its scales
1389
+ # (the kernel folds on As is None).
1390
+ # the kernel quantizes raw A inline on this grid (fp4 formats pack in-register);
1391
+ # NVFP4 batched runs on the software arms — decode grid BM <= 16 < the native
1392
+ # mxf4nvf4 M=128 staging (scalar / swap-scalar column-unpack + E4M3 scale decode)
1393
+ activation_format = resolve_activation_format(activation_format, B, Bs)
1394
+ output_format = activation_format if quantize_output else None
1395
+ requant = quantize_output
1396
+ if As is not None:
1397
+ assert (As.dtype == torch.float8_e4m3fn) == (Bs.dtype == torch.float8_e4m3fn), (
1398
+ f"activation scales ({As.dtype}) must match the weight scale family ({Bs.dtype})"
1399
+ )
1400
+ pre_quantized = As is not None
1401
+ assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
1402
+ f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
1403
+ )
1404
+ WEIGHT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
1405
+ # NVFP4 raw activations pre-quantize HERE, above the layout derivation below — the quant
1406
+ # packs A to E2M1, so ACT_VALUES_PER_BYTE and K must be read off the packed tensor. Offline
1407
+ # beats the inline arm at every M for NVFP4 (unlike the UE8M0 formats, whose exponent-only
1408
+ # inline quant is free): its E4M3 block scale costs an amax divide plus a global normalize
1409
+ # per group, and the decode grid re-runs that in every N-tile program against the SAME
1410
+ # routed row. Measured bit-identical, GLM decode gate_up 34.2 -> 31.9us, down 18.9 -> 16.9us.
1411
+ # The act scale stays affine under a swizzled weight: load_act_mx reads it off as_ptrs
1412
+ # row-major, SWIZZLED_SCALES governs only the weight side.
1413
+ if As is None and activation_format == "nvfp4" and A.dtype not in (torch.int8, torch.float8_e4m3fn):
1414
+ # a per-expert g_a quantizes each routed row against ITS expert's global; the rows are
1415
+ # routed slots, so the row -> expert map is this op's own `expert_ids`
1416
+ A, As = MX_ACT_QUANT["nvfp4"](
1417
+ A,
1418
+ global_scale=normalize_global_scale(a_global_scale, B.shape[0]),
1419
+ expert_index=expert_ids,
1420
+ )
1421
+ pre_quantized = True
1422
+ # int8 A = caller-provided packed-E2M1 activations (W4A4, native mxf4 MMA): K is two
1423
+ # values per stored byte and the scales are mandatory (nothing left to quantize).
1424
+ ACT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if A.dtype == torch.int8 else 1
1425
+ if ACT_VALUES_PER_BYTE == NIBBLES_PER_BYTE:
1426
+ assert As is not None, "packed-E2M1 activations need their UE8M0 scales (As)"
1427
+
1428
+ output_dtype = resolve_output_dtype(output_dtype, A, As)
1429
+ K = A.shape[1] * ACT_VALUES_PER_BYTE
1430
+ S = expert_ids.shape[0]
1431
+ num_experts, n_rows, N = expert_weight_shape(B, gate)
1432
+ K_b = B.shape[2]
1433
+ assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
1434
+ f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[2] (={K_b})"
1435
+ )
1436
+ # Bs arrives either row-major (num_experts, n_rows, K // scale_group) — read affine — or
1437
+ # already SWIZZLE_32_4_4 (5D: 1, num_experts * n_rows // 128, cols // 4, 2, 256), the shared
1438
+ # checkpoint layout swizzled once at model load (the deployment contract, no per-call
1439
+ # rearrange). The format is the scale dtype (E4M3 = NVFP4 group-16, UE8M0 = MX group-32); the
1440
+ # swizzled cols encode (K // scale_group) // 4.
1441
+ swizzled_scales = Bs.ndim == 5
1442
+ scale_group = mx_scale_family(Bs, K)
1443
+ if not swizzled_scales:
1444
+ assert Bs.shape == (num_experts, n_rows, K // scale_group), (
1445
+ f"Bs shape {tuple(Bs.shape)} != ({num_experts}, {n_rows}, {K // scale_group})"
1446
+ )
1447
+ else:
1448
+ # the artifact's expert count and per-expert row blocks must match THIS weight stack — a
1449
+ # wrong-layer artifact with a matching K would otherwise dequantize with garbage scales silently
1450
+ assert Bs.shape[:2] == (num_experts, triton.cdiv(n_rows, 128)), (
1451
+ f"swizzled Bs carries {tuple(Bs.shape[:2])} (experts, 128-row blocks), expected "
1452
+ f"{(num_experts, triton.cdiv(n_rows, 128))} for ({n_rows}, K) weights — wrong artifact"
1453
+ )
1454
+
1455
+ a_u8 = e2m1_as_uint8(A)
1456
+ as_u8 = ue8m0_as_uint8(As) # None when raw (A quantized inline)
1457
+ b_u8 = e2m1_as_uint8(B)
1458
+ bs_u8 = ue8m0_as_uint8(Bs)
1459
+ # The op never swizzles: Bs is read in whatever layout it arrives (format-agnostic — MX or
1460
+ # NVFP4). A pre-swizzled SWIZZLE_32_4_4 Bs (5D, the shared checkpoint layout the grouped kernel
1461
+ # also consumes) takes the fast descriptor/gather path; a row-major (3D) Bs takes the affine
1462
+ # path at no penalty. Callers swizzle once at load (public swizzle_mx_scales) to opt into perf.
1463
+ # The descriptor is built only on the swizzled path — the un-swizzled arm never reads it (None).
1464
+ bs_descriptor = swizzled_scale_descriptor(bs_u8) if swizzled_scales else None
1465
+ # Requant scales are written ROW-MAJOR (never SWIZZLE_32_4_4), unlike the grouped/2D
1466
+ # requant which fuse the swizzle in-epilogue. This is deliberate, not a gap: batched is the
1467
+ # decode kernel — one distinct routed row per program (FAKE_BATCH replicates it across the BM
1468
+ # lanes), so it never forms the 128-distinct-row MMA tile the tcgen05 swizzled scaled-MMA
1469
+ # fast path needs. Swizzled scales give decode no speedup, and the 128-row swizzle block
1470
+ # can't be written from a one-row-per-program grid without cross-program collisions. The
1471
+ # down projection reads this row-major intermediate directly (its As is row-major too).
1472
+ if output_format in ("mxfp4", "nvfp4"):
1473
+ # packed E2M1 intermediate (nibble pairs along N) + group scales (UE8M0 for MX,
1474
+ # E4M3 for NVFP4) — feeds a W4A4 down as-is
1475
+ assert N % (2 * scale_group) == 0, (
1476
+ f"N (={N}) must be a multiple of {2 * scale_group} to pack E2M1 pairs"
1477
+ )
1478
+ C = a_u8.new_empty((S, N // 2), dtype=torch.int8)
1479
+ Cs = torch.empty(
1480
+ S,
1481
+ N // scale_group,
1482
+ device=a_u8.device,
1483
+ dtype=bs_u8.dtype, # UE8M0 -> uint8, NVFP4 -> e4m3 (the binder-safe weight-scale dtype)
1484
+ )
1485
+ elif requant:
1486
+ C = a_u8.new_empty((S, N), dtype=FP8_DTYPE)
1487
+ Cs = torch.empty(S, N // MX_SCALE_GROUP_K, device=a_u8.device, dtype=torch.uint8)
1488
+ else:
1489
+ C = a_u8.new_empty((S, N), dtype=output_dtype)
1490
+ Cs = None # unread without an OUTPUT_FORMAT; strides literal below
1491
+
1492
+ def grid(META):
1493
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
1494
+
1495
+ # NVFP4 accumulator correction: the per-expert g_a·g_b product folded onto the fp32 accumulator.
1496
+ # g_b per expert and g_a scalar go down SEPARATELY; the kernel multiplies them in-register
1497
+ # (the old host-side g_a*g_b product was a torch launch per GEMM per call)
1498
+ input_global_scale = normalize_global_scale(b_global_scale, B.shape[0])
1499
+ act_global_scale = normalize_global_scale(a_global_scale, B.shape[0])
1500
+ out_global_scale = normalize_global_scale(output_global_scale, B.shape[0])
1501
+ with device_context(a_u8.device):
1502
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1503
+ compile_time_only_triton_wrap(mx_dynamic_matmul_batched_kernel)[grid](
1504
+ a_u8,
1505
+ as_u8, # None when raw (A quantized inline)
1506
+ b_u8,
1507
+ bs_u8,
1508
+ bs_descriptor,
1509
+ C,
1510
+ Cs,
1511
+ bias,
1512
+ act_global_scale, # AsGlobal: g_a per tensor or per expert (acc, and the inline-quant arm's A/g_a)
1513
+ input_global_scale, # AsBsGlobal = g_b per expert (acc; g_a folds in-kernel via AsGlobal)
1514
+ out_global_scale, # CsGlobal: requant output normalization (next proj's provided input_scale); None folds out
1515
+ expert_ids,
1516
+ gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
1517
+ scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
1518
+ S,
1519
+ N,
1520
+ K,
1521
+ a_u8.stride(0),
1522
+ a_u8.stride(1),
1523
+ as_u8.stride(0) if pre_quantized else 1,
1524
+ b_u8.stride(0),
1525
+ b_u8.stride(2),
1526
+ b_u8.stride(1),
1527
+ bs_u8.stride(0),
1528
+ bs_u8.stride(2),
1529
+ bs_u8.stride(1),
1530
+ C.stride(0),
1531
+ C.stride(1),
1532
+ Cs.stride(0) if requant else 1,
1533
+ Cs.stride(1) if requant else 1,
1534
+ bias_stride_e,
1535
+ bias_stride_n,
1536
+ global_scale_stride(act_global_scale),
1537
+ global_scale_stride(input_global_scale),
1538
+ global_scale_stride(out_global_scale),
1539
+ expert_ids.stride(0),
1540
+ SCALE_GROUP_K=scale_group,
1541
+ num_experts=num_experts,
1542
+ SWIZZLED_SCALES=swizzled_scales,
1543
+ ACTIVATION_FORMAT=activation_format,
1544
+ GATE=gate,
1545
+ ACT_FN=act_fn,
1546
+ SWIGLU_ALPHA=swiglu_alpha,
1547
+ SWIGLU_LIMIT=swiglu_limit,
1548
+ OUTPUT_FORMAT=output_format,
1549
+ SIMULATE_UNFUSED=simulate_unfused,
1550
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1551
+ PDL=decode_pdl(),
1552
+ launch_pdl=decode_pdl(),
1553
+ )
1554
+ return [C, Cs] if requant else [C]
1555
+
1556
+
1557
+ @compile_time_only_triton_op(
1558
+ add_op_namespace_prefix("full_precision_matmul_batched"),
1559
+ mutates_args=(),
1560
+ opaque=True,
1561
+ )
1562
+ def full_precision_matmul_batched(
1563
+ A: torch.Tensor,
1564
+ B: torch.Tensor,
1565
+ expert_ids: torch.Tensor,
1566
+ gate: bool = False,
1567
+ act_fn: str = "silu",
1568
+ swiglu_alpha: float | None = None,
1569
+ swiglu_limit: float | None = None,
1570
+ simulate_unfused: bool = False,
1571
+ activation_format: str | None = None,
1572
+ quantize_output: bool = False,
1573
+ output_dtype: torch.dtype | None = None,
1574
+ gather_idx: torch.Tensor | None = None,
1575
+ scatter_idx: torch.Tensor | None = None,
1576
+ bias: torch.Tensor | None = None,
1577
+ ) -> list[torch.Tensor]:
1578
+ """Full-precision (BF16/FP16) batched matmul: C[s] = A[s] @ B[expert_ids[s]].T — no
1579
+ quantization anywhere, fp32 accumulation. ``gate``/``act_fn``/``swiglu_*``/
1580
+ ``simulate_unfused`` are the dispatcher's gate|up fusion kwargs (GLU only; ``requant`` is
1581
+ meaningless without a quantized format). ``gather_idx``/``scatter_idx`` map the source
1582
+ row of A / destination row of C per program (None = row s).
1583
+
1584
+ A: (rows, K) BF16/FP16 activations — rows addressed via ``gather_idx``
1585
+ B: (num_experts, N, K) expert weights in A's dtype; under ``gate`` the (num_experts, 2N, K) stack
1586
+ """
1587
+ validate_dense_operands(A, B)
1588
+ assert A.dtype == B.dtype and A.dtype in (torch.bfloat16, torch.float16), (
1589
+ f"full-precision path needs matching BF16/FP16 A and B, got {A.dtype} / {B.dtype}"
1590
+ )
1591
+ assert activation_format in (None, "bf16") and not quantize_output, (
1592
+ "the full-precision path quantizes nothing — no quantized activation format or requant applies"
1593
+ )
1594
+
1595
+ output_dtype = resolve_output_dtype(output_dtype, A, None)
1596
+ K = A.shape[1]
1597
+ S = expert_ids.shape[0]
1598
+ num_experts, _, N = expert_weight_shape(B, gate)
1599
+ C = A.new_empty(S, N, dtype=output_dtype)
1600
+
1601
+ def grid(META):
1602
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
1603
+
1604
+ with device_context(A.device):
1605
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1606
+ compile_time_only_triton_wrap(full_precision_matmul_batched_kernel)[grid](
1607
+ A,
1608
+ B,
1609
+ C,
1610
+ bias,
1611
+ expert_ids,
1612
+ gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
1613
+ scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
1614
+ S,
1615
+ N,
1616
+ K,
1617
+ A.stride(0),
1618
+ A.stride(1),
1619
+ B.stride(0),
1620
+ B.stride(2),
1621
+ B.stride(1),
1622
+ C.stride(0),
1623
+ C.stride(1),
1624
+ bias_stride_e,
1625
+ bias_stride_n,
1626
+ expert_ids.stride(0),
1627
+ num_experts=num_experts,
1628
+ GATE=gate,
1629
+ ACT_FN=act_fn,
1630
+ SWIGLU_ALPHA=swiglu_alpha,
1631
+ SWIGLU_LIMIT=swiglu_limit,
1632
+ SIMULATE_UNFUSED=simulate_unfused,
1633
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1634
+ PDL=decode_pdl(),
1635
+ launch_pdl=decode_pdl(),
1636
+ )
1637
+
1638
+ return [C]
1639
+
1640
+
1641
+ @compile_time_only_triton_op(
1642
+ add_op_namespace_prefix("mx_weight_only_matmul_batched"),
1643
+ mutates_args=(),
1644
+ opaque=True,
1645
+ )
1646
+ def mx_weight_only_matmul_batched(
1647
+ A: torch.Tensor,
1648
+ B: torch.Tensor,
1649
+ Bs: torch.Tensor,
1650
+ expert_ids: torch.Tensor,
1651
+ gate: bool = False,
1652
+ act_fn: str = "silu",
1653
+ swiglu_alpha: float | None = None,
1654
+ swiglu_limit: float | None = None,
1655
+ simulate_unfused: bool = False,
1656
+ output_dtype: torch.dtype | None = None,
1657
+ gather_idx: torch.Tensor | None = None,
1658
+ scatter_idx: torch.Tensor | None = None,
1659
+ b_global_scale: torch.Tensor | None = None,
1660
+ bias: torch.Tensor | None = None,
1661
+ ) -> list[torch.Tensor]:
1662
+ """weight-only batched (decode) matmul: ``C[s] = A[s] @ B[expert_ids[s]].T`` with raw bf16/fp16
1663
+ activations against MXFP4/NVFP4/MXFP8 weights upcast to bf16 in-loop — the ``matmul_ogs`` format.
1664
+ Affine (3D) weight scales — UE8M0 group-32 (MX) or E4M3 group-16 (NVFP4, whose per-expert
1665
+ fp32 ``b_global_scale`` ``(E,)`` recovers on the accumulator; ``None`` = single-level).
1666
+ Returns ``[C]``."""
1667
+ assert A.dtype in (torch.bfloat16, torch.float16, torch.float32), (
1668
+ f"weight-only takes a raw bf16/fp16/fp32 activation, got {A.dtype}"
1669
+ )
1670
+ assert Bs.ndim == 3, f"weight-only batched takes affine (3D) weight scales, got ndim={Bs.ndim}"
1671
+ # The ``gate`` contract runs UNSTACKED at every S: this same op without the gate (one
1672
+ # plain GEMM over the stacked weight), then the one-kernel ``fused_glu`` — bit-identical
1673
+ # rounding, the unfused-reference order, so ``simulate_unfused`` rides through. The
1674
+ # upcast weight feeds a plain bf16 dot, so the stacked tile buys no native MMA and only
1675
+ # halves occupancy on a weight-bandwidth-bound loop; unlike the block-FP8 band this
1676
+ # never inverts (GPT-OSS shape: 48.0->39.7µs at S=4, still ahead at S=512; 2026-08-06).
1677
+ if gate:
1678
+ # fp32 intermediate = the exact GEMM accumulators, so the GLU keeps the gated
1679
+ # epilogue's FUSED-order rounding (bf16 operands would drift steep-sigmoid elements
1680
+ # past the weight-only tests' exact-ish tolerances). Under ``simulate_unfused`` the
1681
+ # caller is asking for the UNFUSED order instead, where the reference lands its gate_up
1682
+ # in the activation dtype before the GLU — carrying fp32 there leaves the two a bf16 ULP
1683
+ # apart (256.0 absolute at magnitude 2^15), which the parity tolerance rejects.
1684
+ inter_dtype = (
1685
+ resolve_output_dtype(output_dtype, A, None) if simulate_unfused else torch.float32
1686
+ )
1687
+ [gate_up] = mx_weight_only_matmul_batched(
1688
+ A, B, Bs, expert_ids, output_dtype=inter_dtype,
1689
+ gather_idx=gather_idx, scatter_idx=scatter_idx, b_global_scale=b_global_scale,
1690
+ )
1691
+ return [fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit,
1692
+ out_dtype=resolve_output_dtype(output_dtype, A, None))]
1693
+ output_dtype = resolve_output_dtype(output_dtype, A, None)
1694
+ K = A.shape[1]
1695
+ S = expert_ids.shape[0]
1696
+ WEIGHT_VALUES_PER_BYTE = 2 if B.dtype == torch.int8 else 1
1697
+ num_experts, rows, K_b = B.shape
1698
+ assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
1699
+ f"K ({K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[2] ({K_b})"
1700
+ )
1701
+ N = rows // 2 if gate else rows
1702
+ scale_group = mx_scale_family(Bs, K)
1703
+ b_global_scale = normalize_global_scale(b_global_scale, num_experts)
1704
+ C = A.new_empty(S, N, dtype=output_dtype)
1705
+ b_u8 = e2m1_as_uint8(B)
1706
+ bs_u8 = ue8m0_as_uint8(Bs)
1707
+
1708
+ def grid(META):
1709
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
1710
+
1711
+ with device_context(A.device):
1712
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1713
+ compile_time_only_triton_wrap(mx_weight_only_matmul_batched_kernel)[grid](
1714
+ A,
1715
+ b_u8,
1716
+ bs_u8,
1717
+ b_global_scale,
1718
+ C,
1719
+ bias,
1720
+ expert_ids,
1721
+ gather_idx,
1722
+ scatter_idx,
1723
+ S,
1724
+ N,
1725
+ K,
1726
+ A.stride(0),
1727
+ A.stride(1),
1728
+ b_u8.stride(0),
1729
+ b_u8.stride(2),
1730
+ b_u8.stride(1),
1731
+ bs_u8.stride(0),
1732
+ bs_u8.stride(1),
1733
+ bs_u8.stride(2),
1734
+ C.stride(0),
1735
+ C.stride(1),
1736
+ bias_stride_e,
1737
+ bias_stride_n,
1738
+ global_scale_stride(b_global_scale),
1739
+ expert_ids.stride(0),
1740
+ num_experts=num_experts,
1741
+ SCALE_GROUP_K=scale_group,
1742
+ WEIGHT_VALUES_PER_BYTE=WEIGHT_VALUES_PER_BYTE,
1743
+ GATE=gate,
1744
+ ACT_FN=act_fn,
1745
+ SWIGLU_ALPHA=swiglu_alpha,
1746
+ SWIGLU_LIMIT=swiglu_limit,
1747
+ SIMULATE_UNFUSED=simulate_unfused,
1748
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1749
+ PDL=decode_pdl(),
1750
+ launch_pdl=decode_pdl(),
1751
+ )
1752
+
1753
+ return [C]
1754
+
1755
+
1756
+ def matmul_batched(
1757
+ A: torch.Tensor,
1758
+ B: torch.Tensor,
1759
+ As: torch.Tensor | None = None,
1760
+ Bs: torch.Tensor | None = None,
1761
+ *,
1762
+ expert_ids: torch.Tensor,
1763
+ bias: torch.Tensor | None = None, # (E, N_out) per-expert output bias; (N_out,) for 2D
1764
+ activation_format: str | None = None,
1765
+ gate: bool = False,
1766
+ act_fn: str = "silu",
1767
+ swiglu_alpha: float | None = None,
1768
+ swiglu_limit: float | None = None,
1769
+ quantize_output: bool = False,
1770
+ simulate_unfused: bool = False,
1771
+ output_dtype: torch.dtype | None = None,
1772
+ gather_idx: torch.Tensor | None = None,
1773
+ scatter_idx: torch.Tensor | None = None,
1774
+ a_global_scale: torch.Tensor | None = None,
1775
+ b_global_scale: torch.Tensor | None = None,
1776
+ output_global_scale: torch.Tensor | None = None,
1777
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1778
+ """Batched matmul dispatcher (W8A8 FP8, W4A8/W4A4 FP4, or full-precision). Routes one
1779
+ program per routed row (``expert_ids`` gives its expert).
1780
+
1781
+ ``As`` marks ``A`` as already quantized (framework-precomputed scales, or a requantized
1782
+ intermediate handed to the down projection); a per-tensor scalar ``As`` is instead the static
1783
+ (calibrated) activation scale for block-scale FP8 weights — the op quantizes raw ``A`` against
1784
+ it; ``None`` = raw ``A``, quantized dynamically by the op in ``activation_format`` (``None`` =
1785
+ the weights' own format — fp8/E4M3 offline for bd/tensor, inline for MX; ``"bf16"`` = weight-only;
1786
+ ``"mxfp4"`` = packed E2M1 activations). ``Bs`` ``None`` = unquantized BF16/FP16 weights.
1787
+ ``quantize_output`` requantizes the output into ``activation_format`` — the return is then
1788
+ ``(C, Cs)``. ``gate``/``act_fn``/``swiglu_alpha``/``swiglu_limit`` fuse the gate|up GLU into the
1789
+ epilogue. ``As``/``Bs`` are each a bare block-scale tensor; the two-level NVFP4
1790
+ second-level scales ride the separate ``a_global_scale``/``b_global_scale`` (fp32 per-tensor,
1791
+ weights per-expert ``(E,)``; from ``nvfp4_quantize_two_level``), and the op folds ``g_a · g_b``
1792
+ onto the accumulator. The activation global ``g_a`` is CALIBRATED (the checkpoint's
1793
+ ``input_scale``): ``a_global_scale=g_a`` with a raw ``A`` has the op quantize ``A / g_a`` per
1794
+ block, and rides a pre-quantized ``As`` the same way. Under an NVFP4 requant the
1795
+ fused requant normalizes the GLU intermediate by the PROVIDED ``output_global_scale`` (the next
1796
+ proj's calibrated ``input_scale``) before the block quant and returns ``[C, Cs]``; the down
1797
+ consumes it as ``As=Cs, a_global_scale=output_global_scale``. ``gather_idx``/``scatter_idx`` (each None or a ``(S,)`` map)
1798
+ address the source row of ``A`` / destination row of ``C`` per program — None means row
1799
+ ``s``; the gather lets the gate_up read unexpanded activations (source row
1800
+ ``s // num_top_k``) with no copy.
1801
+
1802
+ Routes by what the weight tensors themselves say (there is no ``block_size``
1803
+ parameter — the quantization block is derived from the scale shape,
1804
+ ``weight_block_size``):
1805
+ - ``Bs`` None → ``full_precision_matmul_batched`` (plain dot, no scales anywhere).
1806
+ - MX weights — ``int8`` (packed E2M1) or ``float8_e4m3fn`` (E4M3) with UE8M0
1807
+ group-32 ``Bs`` → ``mx_dynamic_matmul_batched``.
1808
+ - one scale per expert (``Bs`` ``(E,)``/``(E, 1, 1)``) →
1809
+ ``w8a8_tensor_dynamic_fp8_matmul_batched``.
1810
+ - block scales (``Bs`` ``(E, N/bn, K/bk)``) → ``w8a8_block_dynamic_fp8_matmul_batched``.
1811
+ """
1812
+ check_activation_format(activation_format, quantize_output)
1813
+ assert (a_global_scale is None and b_global_scale is None) or (
1814
+ Bs is not None and weight_format(B, Bs) == "nvfp4"
1815
+ ), "two-level globals (a_global_scale / b_global_scale) are NVFP4-only"
1816
+ assert output_global_scale is None or (
1817
+ Bs is not None and quantize_output and resolve_activation_format(activation_format, B, Bs) == "nvfp4"
1818
+ ), (
1819
+ "output_global_scale is the NVFP4 requant second level — it requires quantize_output=True on NVFP4 "
1820
+ "(the epilogue would otherwise normalize by it with nothing downstream to compensate)"
1821
+ )
1822
+ if As is not None and As.numel() == 1:
1823
+ # static (per-tensor calibrated) activation quant: a per-tensor scalar As for block-scale FP8
1824
+ # weights — the caller hands raw A, the op quantizes it against the scalar (As IS the scale).
1825
+ assert Bs is not None and not is_mx(B, Bs) and weight_block_size(B, Bs) is not None, (
1826
+ "a per-tensor scalar As (static activation scale) needs block-scale FP8 weights"
1827
+ )
1828
+ out = w8a8_block_static_fp8_matmul_batched(
1829
+ A,
1830
+ B,
1831
+ As,
1832
+ Bs,
1833
+ expert_ids,
1834
+ weight_block_size(B, Bs),
1835
+ gate,
1836
+ act_fn,
1837
+ swiglu_alpha,
1838
+ swiglu_limit,
1839
+ simulate_unfused,
1840
+ activation_format,
1841
+ quantize_output,
1842
+ output_dtype,
1843
+ gather_idx,
1844
+ scatter_idx,
1845
+ bias=bias,
1846
+ )
1847
+ return out[0] if len(out) == 1 else tuple(out)
1848
+
1849
+ if Bs is None:
1850
+ assert As is None, (
1851
+ "the full-precision path (Bs=None) takes no activation scales"
1852
+ )
1853
+ out = full_precision_matmul_batched(
1854
+ A,
1855
+ B,
1856
+ expert_ids,
1857
+ gate,
1858
+ act_fn,
1859
+ swiglu_alpha,
1860
+ swiglu_limit,
1861
+ simulate_unfused,
1862
+ activation_format,
1863
+ quantize_output,
1864
+ output_dtype,
1865
+ gather_idx,
1866
+ scatter_idx,
1867
+ bias=bias,
1868
+ )
1869
+ elif is_mx(B, Bs) and activation_format == "bf16": # weight-only: raw bf16 acts, MX weight upcast in-MMA
1870
+ assert As is None and a_global_scale is None and not quantize_output, (
1871
+ "weight-only (activation_format='bf16') takes a raw activation, no As/global/requant"
1872
+ )
1873
+ assert output_global_scale is None, (
1874
+ "weight-only has no requant epilogue for output_global_scale to normalize"
1875
+ )
1876
+ out = mx_weight_only_matmul_batched(
1877
+ A,
1878
+ B,
1879
+ Bs,
1880
+ expert_ids,
1881
+ gate,
1882
+ act_fn,
1883
+ swiglu_alpha,
1884
+ swiglu_limit,
1885
+ simulate_unfused,
1886
+ output_dtype,
1887
+ gather_idx,
1888
+ scatter_idx,
1889
+ b_global_scale=b_global_scale,
1890
+ bias=bias,
1891
+ )
1892
+ elif is_mx(B, Bs):
1893
+ out = mx_dynamic_matmul_batched(
1894
+ A,
1895
+ B,
1896
+ As,
1897
+ Bs,
1898
+ expert_ids,
1899
+ gate,
1900
+ act_fn,
1901
+ swiglu_alpha,
1902
+ swiglu_limit,
1903
+ simulate_unfused,
1904
+ activation_format,
1905
+ quantize_output,
1906
+ output_dtype,
1907
+ gather_idx,
1908
+ scatter_idx,
1909
+ a_global_scale,
1910
+ b_global_scale,
1911
+ output_global_scale,
1912
+ bias=bias,
1913
+ )
1914
+ elif (block_size := weight_block_size(B, Bs)) is None:
1915
+ assert not gate, (
1916
+ "the batched op has no tensor-wide gate|up fusion (grouped and 2D support it)"
1917
+ )
1918
+ assert activation_format in (None, "fp8") and not quantize_output, (
1919
+ "tensor-wide supports neither packed activations nor a fused requant"
1920
+ )
1921
+ out = w8a8_tensor_dynamic_fp8_matmul_batched(
1922
+ A, B, As, Bs, expert_ids, output_dtype, gather_idx, scatter_idx, bias=bias
1923
+ )
1924
+ else:
1925
+ out = w8a8_block_dynamic_fp8_matmul_batched(
1926
+ A,
1927
+ B,
1928
+ As,
1929
+ Bs,
1930
+ expert_ids,
1931
+ block_size,
1932
+ gate,
1933
+ act_fn,
1934
+ swiglu_alpha,
1935
+ swiglu_limit,
1936
+ simulate_unfused,
1937
+ activation_format,
1938
+ quantize_output,
1939
+ output_dtype,
1940
+ gather_idx,
1941
+ scatter_idx,
1942
+ bias=bias,
1943
+ )
1944
+ # bd/mx/full-precision ops return a list ([C] or [C, Cs]); the tensor op returns a bare tensor.
1945
+ if isinstance(out, (list, tuple)):
1946
+ return out[0] if len(out) == 1 else tuple(out)
1947
+ return out
build/torch-rocm/bayesian_autotuner.py ADDED
@@ -0,0 +1,940 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Bayesian-optimization autotuner: benches a budgeted sample of the config grid instead of
16
+ the full grid, via a Tree-structured Parzen Estimator (TPE) over the config *dimensions*
17
+ (``num_warps`` / ``num_stages`` / tile sizes / flags). After a short random seed phase it
18
+ models the good (top-``gamma`` by measured time) vs bad per-dimension value densities and
19
+ benches the unmeasured config that maximizes ``l(x)/g(x)`` — the Expected-Improvement proxy
20
+ — then a coordinate-descent pass polishes the best. Subclasses Triton's ``Autotuner`` and
21
+ uses only the Python stdlib (no external optimizer dependency).
22
+
23
+ Each new key's search is warm-started from the most recently tuned key's best config (nearby
24
+ workloads share tile-shape preferences). Grids smaller than ``n_trials`` defer to the stock
25
+ exhaustive bench-all.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import logging
32
+ import json
33
+ import math
34
+ import os
35
+ import traceback
36
+ import random
37
+ import time
38
+ from collections import defaultdict
39
+ from contextlib import contextmanager
40
+
41
+ import torch
42
+
43
+ from typing import Dict, List
44
+
45
+ from triton.runtime.autotuner import (
46
+ Autotuner,
47
+ Config,
48
+ JITFunction,
49
+ driver,
50
+ get_cache_invalidating_env_vars,
51
+ get_cache_manager,
52
+ knobs,
53
+ triton_key,
54
+ )
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ # Env-gated host-execution counter (FINEGRAINED_AUTOTUNE_CALLCOUNT=<path>): every
59
+ # BayesianAutotuner.run() call increments its kernel's tally, dumped to <path> at exit.
60
+ # Discriminates cudagraph REPLAY (host code runs only at capture) from per-token
61
+ # re-execution — the decode-gap forensics tool.
62
+ _CALL_COUNTS = None
63
+ if os.environ.get("FINEGRAINED_AUTOTUNE_CALLCOUNT"):
64
+ import atexit
65
+
66
+ _CALL_COUNTS = {}
67
+
68
+ def _dump_call_counts(path=os.environ["FINEGRAINED_AUTOTUNE_CALLCOUNT"]):
69
+ with open(path, "w") as f:
70
+ json.dump(_CALL_COUNTS, f, indent=1, sort_keys=True)
71
+
72
+ atexit.register(_dump_call_counts)
73
+
74
+
75
+ class BayesianAutotuner(Autotuner):
76
+ """Drop-in replacement for ``triton.runtime.autotuner.Autotuner`` that
77
+ benches ~``n_trials`` configs per key via TPE Bayesian optimization +
78
+ coordinate-descent refinement, instead of the full grid."""
79
+
80
+ def __init__(
81
+ self,
82
+ *args,
83
+ n_trials: int = 80,
84
+ max_failures: int | None = None,
85
+ n_startup_trials: int = 12,
86
+ gamma: float = 0.25,
87
+ refine: bool = True,
88
+ max_refine_iters: int = 5,
89
+ log_path: str | None = None,
90
+ path_anchor_axes: tuple[str, ...] = (),
91
+ finite_check_args: tuple[str, ...] = (),
92
+ **kwargs,
93
+ ):
94
+ super().__init__(*args, **kwargs)
95
+ self.fn_name = getattr(self.fn, "__name__", str(self.fn))
96
+ # Bayesian trial budget — the per-decorator default, overridable via the
97
+ # FINEGRAINED_AUTOTUNE_TRIALS env var (quick sweeps / exhaustive runs without touching
98
+ # the decorators; set it >= grid size to fall back to stock exhaustive bench-all).
99
+ self.n_trials = int(os.environ.get("FINEGRAINED_AUTOTUNE_TRIALS") or n_trials)
100
+ # A tune aborts once this many configs have failed to compile/run without the measured
101
+ # budget being met. Defaults to n_trials: a grid that cannot land its measurements without
102
+ # that many rejects has a pruner gap, and the fix is to fence the dead region rather than
103
+ # to tolerate the compiles. Its own axis so a kernel can raise it deliberately.
104
+ self.max_failures = int(
105
+ os.environ.get("FINEGRAINED_AUTOTUNE_MAX_FAILURES")
106
+ or max_failures
107
+ or self.n_trials
108
+ )
109
+ self.n_startup_trials = n_startup_trials
110
+ # top fraction of measured configs the TPE treats as "good"
111
+ self.gamma = gamma
112
+ self.refine = refine
113
+ self.max_refine_iters = max_refine_iters
114
+ # Branch axes (by kwarg name) that relocate the TILE optimum — the caller's
115
+ # declaration, since it is configuration knowledge (the tuner never hardcodes
116
+ # axis names). Each distinct value combination of these axes gets one guaranteed
117
+ # max-tile anchor; empty = median anchors only.
118
+ self.path_anchor_axes = tuple(path_anchor_axes)
119
+ # Output arg NAMES (a declaration — the tuner never hardcodes kernel details) whose
120
+ # values are checked for NaN/inf after each config's bench: a config that writes
121
+ # non-finite output scores inf and can never be crowned. The tuner times, it never
122
+ # validates — this veto closes the NaN half of the "wrong-answer configs win
123
+ # tunes" class at negligible cost (one isfinite pass per benched config).
124
+ self.finite_check_args = tuple(finite_check_args)
125
+ # JSONL log of every benched (key, config, ms) — set here or via the
126
+ # FINEGRAINED_AUTOTUNE_LOG env var. Analyse offline to prune bad configs.
127
+ self.log_path = log_path or os.environ.get("FINEGRAINED_AUTOTUNE_LOG")
128
+ # steady-state launch support: key extraction without dict builds (eager decode
129
+ # is host-bound; the cache-hit path of run() is on the per-token critical path)
130
+ self._arg_position = {name: i for i, name in enumerate(self.arg_names)}
131
+ self._arg_name_set = frozenset(self.arg_names)
132
+ self._dtype_tag_cache: Dict[tuple, str] = {}
133
+ self._dtype_str_cache: Dict[torch.dtype, str] = {}
134
+
135
+ def _dtype_str(self, dtype) -> str:
136
+ s = self._dtype_str_cache.get(dtype)
137
+ if s is None:
138
+ s = self._dtype_str_cache[dtype] = str(dtype)
139
+ return s
140
+
141
+ # substrings marking a COMPILE-stage failure — the only class safe to memoize on
142
+ # disk (a benching/CUDA error can be transient or sticky-context contamination;
143
+ # persisting one would permanently fence a healthy config for this source version)
144
+ _COMPILE_FAILURE_MARKS = (
145
+ "PassManager",
146
+ "CompilationError",
147
+ "MLIR",
148
+ "ConvertTritonGPUToLLVM",
149
+ "TritonGPUAccelerateMatmul",
150
+ )
151
+
152
+ def _bench(self, *args, config, **meta):
153
+ """Score any failing config as inf instead of raising — a compile failure is data
154
+ for the search, not a fatal error. Stock Triton forgives only OutOfResources, but
155
+ e.g. Triton 3.7.1's ``warp_specialize`` raises RuntimeError at unsupported
156
+ (shape, config) combos; unguarded, one such config kills the whole tune when a
157
+ small grid falls through to the stock exhaustive path below. Every failure is
158
+ recorded and ``_report_bench_failures`` reports every distinct failure —
159
+ inf-scoring must not silently hide a broken path behind a healthy one.
160
+ (Stock already inf's OutOfResources / CompileTimeAssertionFailure / PTXASError
161
+ internally without reaching this handler — those are deliberate guard classes,
162
+ e.g. our own ``tl.static_assert`` fences; the reporter covers the UNEXPECTED
163
+ failure classes stock would otherwise let kill the tune.)
164
+
165
+ Compile-stage failures are memoized on disk keyed by everything that determines
166
+ compilation (kernel source hash + config + constexpr arg values + tensor dtypes +
167
+ invalidating env), so the next fresh tune of ANY shape skips the doomed compile
168
+ outright (~40-66 wasted compiles per nvfp4 tune before this). The memo dies with
169
+ the kernel source (``fn.cache_key``); bench-stage errors are never persisted."""
170
+ sig = self._compile_signature(config, meta)
171
+ memo = self._failed_compile_memo()
172
+ if sig is not None and sig in memo:
173
+ self._failures.append((config, memo[sig] + " [memoized]"))
174
+ return [float("inf")] * 3
175
+ try:
176
+ timing = super()._bench(*args, config=config, **meta)
177
+ bad = self._nonfinite_output(meta)
178
+ if bad is not None:
179
+ self._failures.append((config, f"non-finite output in {bad} (numerics veto)"))
180
+ return [float("inf")] * 3
181
+ return timing
182
+ except Exception as e:
183
+ # triton's CompilationError carries only the location + source snippet in str(e);
184
+ # the actual reason lives in the chained cause, so without it every compile failure
185
+ # reads as an anonymous caret. Keep the head of the location and the cause verbatim.
186
+ msg = str(e)[:250]
187
+ # triton nests a CompilationError per @jit frame, each carrying only its own location
188
+ # and source — the actual reason is at the BOTTOM of the chain. Walk to it, else every
189
+ # compile failure reads as an anonymous caret and cannot be fenced.
190
+ root, seen = e, 0
191
+ while (root.__cause__ or root.__context__) is not None and seen < 12:
192
+ root = root.__cause__ or root.__context__
193
+ seen += 1
194
+ if root is not e:
195
+ msg += f" || root: {type(root).__name__}: {str(root)[-300:]}"
196
+ err = f"{type(e).__name__}: {msg}"
197
+ if os.environ.get("FINEGRAINED_AUTOTUNE_TRACEBACKS"):
198
+ logger.warning(
199
+ "[autotune] %s failed on %s:\n%s",
200
+ self.fn_name,
201
+ config,
202
+ "".join(traceback.format_exception(type(e), e, e.__traceback__)),
203
+ )
204
+ self._failures.append((config, err))
205
+ if sig is not None and any(m in err for m in self._COMPILE_FAILURE_MARKS):
206
+ memo[sig] = err
207
+ self._persist_failed_compile_memo()
208
+ return [float("inf")] * 3
209
+
210
+ def _nonfinite_output(self, meta=None) -> str | None:
211
+ """Name of the first declared output arg holding NaN/inf after a bench, else None.
212
+ fp8 tensors byte-check the E4M3 NaN pattern (no ``isfinite`` kernel for fp8);
213
+ integer/packed outputs are skipped (every bit pattern is a value)."""
214
+ if not self.finite_check_args:
215
+ return None
216
+ nargs = {**(getattr(self, "nargs", None) or {}), **(meta or {})}
217
+ for name in self.finite_check_args:
218
+ v = nargs.get(name)
219
+ if not isinstance(v, torch.Tensor) or v.numel() == 0:
220
+ continue
221
+ if v.dtype == torch.float8_e4m3fn:
222
+ flat = v.reshape(-1).view(torch.uint8)
223
+ bad = bool((((flat & 0x7F) == 0x7F)).any())
224
+ elif v.is_floating_point():
225
+ bad = bool((~v.reshape(-1).float().isfinite()).any())
226
+ else:
227
+ continue
228
+ if bad:
229
+ return name
230
+ return None
231
+
232
+ def _compile_signature(self, config, meta=None) -> str | None:
233
+ """Hash of the compile determinants for one config: the config itself plus the
234
+ constexpr argument VALUES and tensor argument dtypes from this launch (both feed
235
+ specialization — e.g. GATE flips arms, a uint8 A packs the loads). Source/env
236
+ live in the memo FILE's key, not here.
237
+
238
+ ``meta`` carries the launch kwargs: every constexpr is kwarg-passed at the call
239
+ sites, so ``self.nargs`` (positional-only) alone would hash GATE/format flags as
240
+ None and collide memo entries across arms."""
241
+ nargs = {**(getattr(self, "nargs", None) or {}), **(meta or {})}
242
+ if not nargs:
243
+ return None
244
+ fn = self.fn
245
+ while not isinstance(fn, JITFunction):
246
+ fn = fn.fn
247
+ parts = [str(sorted(config.all_kwargs().items()))]
248
+ for param in fn.params:
249
+ v = nargs.get(param.name)
250
+ if hasattr(v, "dtype"):
251
+ parts.append(f"{param.name}:{v.dtype}")
252
+ elif param.is_constexpr and isinstance(
253
+ v, (bool, int, float, str, type(None))
254
+ ):
255
+ parts.append(f"{param.name}={v}")
256
+ elif isinstance(v, int) and not isinstance(v, bool):
257
+ # non-constexpr ints feed Triton's specialization (==1 / %16 divisibility),
258
+ # which is (shape, config)-dependent for WS lowering failures — hashing the
259
+ # bucket keeps one shape's PassManager failure from fencing every shape
260
+ parts.append(
261
+ f"{param.name}:i{1 if v == 1 else (16 if v % 16 == 0 else 0)}"
262
+ )
263
+ return hashlib.sha256("-".join(parts).encode("utf-8")).hexdigest()
264
+
265
+ def _failed_compile_memo(self) -> dict:
266
+ """The per-(kernel source, backend, env) failed-compile dict, loaded from
267
+ Triton's on-disk cache once per autotuner instance."""
268
+ if getattr(self, "_failed_memo", None) is not None:
269
+ return self._failed_memo
270
+ try:
271
+ from triton.compiler.compiler import make_backend
272
+
273
+ fn = self.fn
274
+ while not isinstance(fn, JITFunction):
275
+ fn = fn.fn
276
+ group = hashlib.sha256(
277
+ "-".join(
278
+ [
279
+ triton_key(),
280
+ make_backend(driver.active.get_current_target()).hash(),
281
+ fn.cache_key,
282
+ str(sorted(get_cache_invalidating_env_vars().items())),
283
+ ]
284
+ ).encode("utf-8")
285
+ ).hexdigest()
286
+ self._failed_memo_cache = get_cache_manager(group)
287
+ self._failed_memo_file = f"{fn.__name__[:150]}.failed_compiles.json"
288
+ path = self._failed_memo_cache.get_file(self._failed_memo_file)
289
+ self._failed_memo = json.load(open(path)) if path else {}
290
+ except Exception:
291
+ self._failed_memo_cache = None
292
+ self._failed_memo = {}
293
+ return self._failed_memo
294
+
295
+ def _persist_failed_compile_memo(self):
296
+ if getattr(self, "_failed_memo_cache", None) is None:
297
+ return
298
+ try:
299
+ self._failed_memo_cache.put(
300
+ json.dumps(self._failed_memo), self._failed_memo_file, binary=False
301
+ )
302
+ except Exception:
303
+ pass # persistence is best-effort; the in-memory memo still holds
304
+
305
+ def _report_bench_failures(self):
306
+ """After every tune, report every UNIQUE failure — a failure is never silent:
307
+ inf-scoring keeps the search alive, but a human must see what broke (e.g. a code
308
+ change that kills one compute path would otherwise silently degrade into "the other
309
+ path wins"). Distinct errors are deduped with a count and an example config; the
310
+ JSONL autotune log has per-config detail."""
311
+ if not self._failures:
312
+ return
313
+ by_err = defaultdict(list)
314
+ for c, err in self._failures:
315
+ by_err[err].append(c)
316
+ for err, cfgs in by_err.items():
317
+ c = cfgs[0]
318
+ example = ", ".join(f"{k}={v}" for k, v in c.kwargs.items())
319
+ logger.warning(
320
+ "[autotune] %s: %d config(s) failed to compile/run — %s (e.g. %s, w%d s%d)",
321
+ self.fn_name,
322
+ len(cfgs),
323
+ err,
324
+ example,
325
+ c.num_warps,
326
+ c.num_stages,
327
+ )
328
+
329
+ def run(self, *args, **kwargs):
330
+ if _CALL_COUNTS is not None:
331
+ _CALL_COUNTS[self.fn_name] = _CALL_COUNTS.get(self.fn_name, 0) + 1
332
+ self._failures = []
333
+ # Small grid → defer to parent (stock exhaustive bench-all). Steady state
334
+ # still short-circuits the parent's per-call dict builds: a crowned config
335
+ # with no pre_hook launches directly. The lookup key must be STOCK format
336
+ # (key values + untagged dtype strings) — it is the parent that populates
337
+ # this branch's cache entries.
338
+ if len(self.configs) <= 1 or self.n_trials >= len(self.configs):
339
+ config = self.configs[0] if len(self.configs) == 1 else None
340
+ if config is None and self.cache:
341
+ n_positional = len(args)
342
+ key = []
343
+ for k in self.keys:
344
+ i = self._arg_position.get(k)
345
+ if i is not None and i < n_positional:
346
+ key.append(args[i])
347
+ elif k in kwargs and k in self._arg_name_set:
348
+ key.append(kwargs[k])
349
+ for i in range(n_positional):
350
+ dtype = getattr(args[i], "dtype", None)
351
+ if dtype is not None:
352
+ key.append(self._dtype_str(dtype))
353
+ for k, v in kwargs.items():
354
+ if k in self._arg_name_set and self._arg_position[k] >= n_positional:
355
+ dtype = getattr(v, "dtype", None)
356
+ if dtype is not None:
357
+ key.append(self._dtype_str(dtype))
358
+ config = self.cache.get(tuple(key))
359
+ if config is not None and config.pre_hook is None:
360
+ self.best_config = config
361
+ config_kwargs = getattr(config, "_all_kwargs_memo", None)
362
+ if config_kwargs is None:
363
+ config_kwargs = config._all_kwargs_memo = config.all_kwargs()
364
+ return self.fn.run(*args, **kwargs, **config_kwargs)
365
+ ret = super().run(*args, **kwargs)
366
+ self._report_bench_failures()
367
+ return ret
368
+
369
+ # Key extraction runs on every launch and eager decode is host-bound, so it
370
+ # avoids dict builds: positional args by precomputed index, then kwargs —
371
+ # the same (arg_names-prefix, kwargs-order) iteration the dict merge gave,
372
+ # keeping keys byte-compatible with existing disk crown caches.
373
+ arg_names = self.arg_names
374
+ n_positional = len(args)
375
+ tags = self._dtype_tag_cache
376
+ key = []
377
+ for k in self.keys:
378
+ i = self._arg_position.get(k)
379
+ if i is not None and i < n_positional:
380
+ key.append(args[i])
381
+ elif k in kwargs and k in self._arg_name_set:
382
+ key.append(kwargs[k])
383
+ # name-tagged, with an explicit token for absent optional tensors: a positional
384
+ # dtype list lets a gather-only and a scatter-only launch alias to one entry,
385
+ # while descriptor-arm legality (descriptor_box_pruner) hangs off GatherIdx
386
+ # presence — the aliased replay would read wrong rows without ever re-pruning
387
+ for i in range(n_positional):
388
+ v = args[i]
389
+ dtype = getattr(v, "dtype", None) if v is not None else None
390
+ if v is not None and dtype is None:
391
+ continue
392
+ tag_key = (arg_names[i], dtype)
393
+ tag = tags.get(tag_key)
394
+ if tag is None:
395
+ tag = tags[tag_key] = f"{tag_key[0]}:{dtype}"
396
+ key.append(tag)
397
+ for k, v in kwargs.items():
398
+ if k not in self._arg_name_set or self._arg_position[k] < n_positional:
399
+ continue
400
+ dtype = getattr(v, "dtype", None) if v is not None else None
401
+ if v is not None and dtype is None:
402
+ continue
403
+ tag_key = (k, dtype)
404
+ tag = tags.get(tag_key)
405
+ if tag is None:
406
+ tag = tags[tag_key] = f"{tag_key[0]}:{dtype}"
407
+ key.append(tag)
408
+ key = tuple(key)
409
+
410
+ first_launch = key not in self.cache
411
+ if first_launch:
412
+ self.nargs = dict(zip(self.arg_names, args))
413
+ pruned = self.prune_configs(kwargs)
414
+
415
+ def benchmark():
416
+ t0 = time.time()
417
+ self.cache[key] = self._bayesian_search(pruned, args, kwargs, key)
418
+ self._report_bench_failures()
419
+ self.bench_time = time.time() - t0
420
+ if knobs.autotuning.print:
421
+ fn_name = getattr(self.fn, "__name__", str(self.fn))
422
+ print(
423
+ f"[bayesian-autotune] {fn_name} tuned "
424
+ f"{len(self.configs_timings)} configs in {self.bench_time:.1f}s — "
425
+ f"key={key}, best={self.cache[key].all_kwargs()}"
426
+ )
427
+ full_nargs = {**self.nargs, **kwargs, **self.cache[key].all_kwargs()}
428
+ self.pre_hook(full_nargs, reset_only=True)
429
+
430
+ if self.cache_results:
431
+ self.check_disk_cache(key, pruned, benchmark)
432
+ else:
433
+ benchmark()
434
+
435
+ config = self.cache[key]
436
+ self.best_config = config
437
+ config_kwargs = getattr(config, "_all_kwargs_memo", None)
438
+ if config_kwargs is None:
439
+ config_kwargs = config._all_kwargs_memo = config.all_kwargs()
440
+ if config.pre_hook is not None:
441
+ if not first_launch: # tuned path already built nargs for prune_configs
442
+ self.nargs = dict(zip(self.arg_names, args))
443
+ config.pre_hook({**self.nargs, **kwargs, **config_kwargs})
444
+ ret = self.fn.run(*args, **kwargs, **config_kwargs)
445
+ if first_launch and self.finite_check_args:
446
+ # First launch after a key miss: the one regime where a crowned launch has been
447
+ # observed returning non-finite output while every subsequent identical launch
448
+ # is correct (large multi-device models; root cause open — see the DSV3
449
+ # investigation record). Detect via the declared output args and relaunch once,
450
+ # loudly. Sentinel/padding rows can false-trigger (uninitialized by contract);
451
+ # the cost is one duplicate launch per key, never a dropped config.
452
+ bad = self._nonfinite_output(kwargs)
453
+ if bad is not None:
454
+ logger.warning(
455
+ "[autotune] %s: non-finite %s on the FIRST launch of key=%s — relaunching once",
456
+ self.fn_name, bad, key,
457
+ )
458
+ ret = self.fn.run(*args, **kwargs, **config.all_kwargs())
459
+ self.nargs = None
460
+ return ret
461
+
462
+ def _bayesian_search(self, configs: List[Config], args, kwargs, key) -> Config:
463
+ timings: Dict[int, float] = {}
464
+ sigs = [tuple(sorted(c.all_kwargs().items())) for c in configs]
465
+
466
+ def bench_idx(idx: int) -> float:
467
+ if idx in timings:
468
+ return timings[idx]
469
+ try:
470
+ ms = self._bench(*args, config=configs[idx], **kwargs)
471
+ if isinstance(ms, (tuple, list)):
472
+ ms = ms[0]
473
+ timings[idx] = float(ms)
474
+ except Exception:
475
+ timings[idx] = float("inf")
476
+ self._log_result(key, configs[idx], timings[idx])
477
+ return timings[idx]
478
+
479
+ # Distinct values per config dimension, for the TPE's Laplace smoothing.
480
+ dim_vals: Dict = defaultdict(set)
481
+ for sig in sigs:
482
+ for d, v in sig:
483
+ dim_vals[d].add(v)
484
+
485
+ # Seed phase: one BASIN ANCHOR per (COMPUTE_MODE, SWAP_AB) group, the most recent
486
+ # cached key's best (warm start), then seeded-random fill. The anchors guarantee every
487
+ # categorical basin gets at least one real measurement — without them the TPE's
488
+ # per-dimension model can write off a whole axis it never saw succeed (two dsv4 tunes
489
+ # shipped 25-60% slow winners because their random seeds only sampled a basin's dead
490
+ # configs), and coordinate descent can't recover a winner two coupled flips away.
491
+ n_startup = max(2, min(self.n_startup_trials, self.n_trials))
492
+ anchors = self._basin_anchor_indices(configs)
493
+ order = list(range(len(configs)))
494
+ random.Random(0).shuffle(order)
495
+ warm_idx = self._warm_start_index(configs)
496
+ head = anchors + ([warm_idx] if warm_idx is not None else [])
497
+ order = list(dict.fromkeys(head + order))
498
+ for idx in order[: max(n_startup, len(head))]:
499
+ bench_idx(idx)
500
+
501
+ # TPE: split measured configs into good (top-gamma) / bad, build per-dimension value
502
+ # densities for each, and bench the unmeasured config maximizing log l(x) - log g(x)
503
+ # (Expected-Improvement proxy), updating the model after each measurement.
504
+ # inf (failed-to-compile) configs are EXCLUDED from the densities: a compile failure is
505
+ # evidence about that one joint shape (usually shared memory), not about its dimension
506
+ # values — counting them as "bad" buried SWAP_AB under a wall of BN=256 smem failures
507
+ # and made the tuner ship a 53µs winner while the 41µs swap config sat unbenched.
508
+ # They don't consume the trial budget either (n_trials = MEASURED configs; a failure
509
+ # stays in ``timings`` only as a skip-list entry) — the compile it burned is the one
510
+ # cost that can't be refunded here, which is what the smem/compile-guard pruners avoid.
511
+ warned_failures = False
512
+ while sum(1 for t in timings.values() if t != float("inf")) < self.n_trials:
513
+ # Failures don't consume the trial budget (above), which is right for a handful of
514
+ # smem/regs rejects but unbounded if a launch is broadly broken: the loop keeps
515
+ # picking, each pick costs an O(configs) TPE rescan, and the tune degenerates into a
516
+ # silent multi-hour spin instead of a diagnosable error. Cap the failures at the same
517
+ # budget as the measurements and fail loudly.
518
+ if (
519
+ not warned_failures
520
+ and sum(1 for t in timings.values() if t == float("inf")) >= self.max_failures
521
+ ):
522
+ # Loud, once, and non-fatal. Aborting here would break tunes that DO find a winner
523
+ # after many rejects (mxfp4 W4A4 at a non-128 N burns >100), trading a silent
524
+ # inefficiency for a hard failure. Warning keeps the waste visible — this is how
525
+ # ~100 dead compiles per mx tune (an IndexError in load_act_mx's inline-quant arm)
526
+ # were found at all — without gating the kernel on a diagnostic.
527
+ warned_failures = True
528
+ self._report_bench_failures()
529
+ logger.warning(
530
+ "[autotune] %s: %d config(s) scored inf before the trial budget was met "
531
+ "(%d of them UNEXPECTED failures — the rest are deliberate guard rejections: "
532
+ "OutOfResources and tl.static_assert fences, which stock inf's internally). "
533
+ "The unexpected ones are dead compile time every tune, and a pruner gap "
534
+ "worth closing.",
535
+ self.fn_name,
536
+ sum(1 for t in timings.values() if t == float("inf")),
537
+ len(self._failures),
538
+ )
539
+ ranked = sorted(
540
+ (i for i, t in timings.items() if t != float("inf")), key=timings.get
541
+ )
542
+ if not ranked: # nothing compiled yet — keep seeding in shuffled order
543
+ nxt = next((i for i in order if i not in timings), None)
544
+ if nxt is None:
545
+ break
546
+ bench_idx(nxt)
547
+ continue
548
+ n_good = max(1, round(self.gamma * len(ranked)))
549
+ good_c: Dict = defaultdict(lambda: defaultdict(int))
550
+ bad_c: Dict = defaultdict(lambda: defaultdict(int))
551
+ for j, i in enumerate(ranked):
552
+ tgt = good_c if j < n_good else bad_c
553
+ for d, v in sigs[i]:
554
+ tgt[d][v] += 1
555
+ n_bad = len(ranked) - n_good
556
+ best_i, best_score = None, -math.inf
557
+ for i in range(len(configs)):
558
+ if i in timings:
559
+ continue
560
+ score = 0.0
561
+ for d, v in sigs[i]:
562
+ V = len(dim_vals[d])
563
+ lp = (good_c[d][v] + 1.0) / (
564
+ n_good + V
565
+ ) # P(v | good), Laplace-smoothed
566
+ gp = (bad_c[d][v] + 1.0) / (n_bad + V) # P(v | bad)
567
+ score += math.log(lp) - math.log(gp)
568
+ if score > best_score:
569
+ best_score, best_i = score, i
570
+ if best_i is None:
571
+ break
572
+ bench_idx(best_i)
573
+
574
+ # Coordinate-descent refinement: try single-dim perturbations around
575
+ # the current best until no neighbor improves.
576
+ if self.refine:
577
+ for _ in range(self.max_refine_iters):
578
+ best_idx = min(timings, key=timings.get)
579
+ best_sig = sigs[best_idx]
580
+ best_ms = timings[best_idx]
581
+ improved = False
582
+ for i, s in enumerate(sigs):
583
+ if i in timings:
584
+ continue
585
+ diff = sum(1 for (_, a), (_, b) in zip(best_sig, s) if a != b)
586
+ if diff != 1:
587
+ continue
588
+ if bench_idx(i) < best_ms:
589
+ improved = True
590
+ if not improved:
591
+ break
592
+
593
+ # Crown RUNOFF: the search's single-shot timings carry enough noise to flip crowns
594
+ # between same-code tune rolls (gpt-oss prefill grouped rolled 1727-2150us on identical
595
+ # grids; the tuner's one RNG site is seeded, so noise is the only roll source). Re-time
596
+ # the top candidates back-to-back — same warmed process, adjacent in time, so they see
597
+ # the same clocks — and crown the winner of THAT comparison. Costs ~3 extra benches.
598
+ finite = sorted((t, i) for i, t in timings.items() if t != float("inf"))
599
+ if len(finite) > 1:
600
+ for _, i in finite[:3]:
601
+ timings.pop(i, None)
602
+ bench_idx(i) # memoizing closure: re-benches and re-records the popped entry
603
+
604
+ self.configs_timings = {configs[i]: t for i, t in timings.items()}
605
+ best = min(timings, key=timings.get)
606
+ if timings[best] == float("inf"):
607
+ # crowning an arbitrary broken config would persist it as `best` on disk and
608
+ # every later healthy process would replay it without re-tuning — the exact
609
+ # sticky-contaminated-context scenario inf-forgiveness exists to survive
610
+ self._report_bench_failures()
611
+ raise RuntimeError(
612
+ f"[bayesian-autotune] {self.fn_name}: every benched config failed "
613
+ "(all-inf tune) — nothing correct to crown or persist; the context may "
614
+ "be trap-contaminated (retry in a fresh process) or the grid is broken "
615
+ "for this launch (see the failure report above)."
616
+ )
617
+ return configs[best]
618
+
619
+ def _log_result(self, key, config: Config, ms: float):
620
+ """Append one ``(key, config, ms)`` record as JSONL for offline analysis —
621
+ e.g. pruning configs that are consistently far off the per-key best. ``inf`` ms
622
+ marks a config that failed to compile/run (out of resources, etc.)."""
623
+ if not self.log_path:
624
+ return
625
+ try:
626
+ rec = {
627
+ "timestamp": time.time(),
628
+ "fn_name": getattr(self.fn, "__name__", str(self.fn)),
629
+ "key": list(key),
630
+ "kwargs": config.kwargs,
631
+ "num_warps": config.num_warps,
632
+ "num_stages": config.num_stages,
633
+ "ms": ms,
634
+ }
635
+ with open(self.log_path, "a") as f:
636
+ f.write(json.dumps(rec, default=str) + "\n")
637
+ except Exception:
638
+ pass
639
+
640
+ def _basin_anchor_indices(self, configs: List[Config]) -> List[int]:
641
+ """One representative config index per (COMPUTE_MODE, SWAP_AB) basin — the MEDIAN in
642
+ tile-sort order, a mid-sized tile with mid warps/stages. Not the smallest: a basin's
643
+ minimal corner (min BK x 2 warps) can be latency-bound pathological (a 131µs anchor in
644
+ a basin whose peak is 41µs re-poisons the axis it was meant to protect). Coordinate
645
+ descent climbs BN/BK/warps/stages from wherever the TPE lands within the basin.
646
+ Returns [] when the grid has no such axes (single basin)."""
647
+ # Basin axes are DERIVED, not declared: a config kwarg with string or boolean
648
+ # values is a branch axis (different code path — compute mode, operand swap,
649
+ # warp specialization), which partitions the grid into disjoint performance
650
+ # basins. Numeric kwargs (tiles/warps/stages) are ordinal — the TPE's densities
651
+ # and coordinate descent handle those.
652
+ basin_axes = sorted(
653
+ {
654
+ k
655
+ for c in configs
656
+ for k, v in c.kwargs.items()
657
+ if isinstance(v, (bool, str))
658
+ }
659
+ )
660
+ if not basin_axes:
661
+ return []
662
+
663
+ # A basin = the compute-path axes (constexprs selecting different compiled code).
664
+ groups: Dict = {}
665
+ for i, c in enumerate(configs):
666
+ basin = tuple(c.kwargs.get(k) for k in basin_axes)
667
+ groups.setdefault(basin, []).append(i)
668
+
669
+ if len(groups) <= 1:
670
+ return []
671
+
672
+ def tile_order(i):
673
+ return (
674
+ configs[i].kwargs.get("BLOCK_SIZE_N", 0),
675
+ configs[i].kwargs.get("BLOCK_SIZE_K", 0),
676
+ configs[i].num_warps,
677
+ configs[i].num_stages,
678
+ )
679
+
680
+ # Median anchor per basin, PLUS one max-tile anchor per declared PATH-axis combo
681
+ # (``path_anchor_axes``). The median alone left the big-tile end unsampled: TPE
682
+ # densities lock onto the first-timed mid-tile region and coordinate descent cannot
683
+ # cross DIAGONAL ridges — dsv4 mxfp8 prefill gate_up's BN=128/BK=256 winner (840us)
684
+ # needs BN and warps to move together, so descent from the BN=64 crown rejects the
685
+ # lone BN step and n_trials 100/200/400 all missed it; one guaranteed big-tile
686
+ # sample fixed it (crown 958 -> 880 at the default budget). The max anchor rides
687
+ # the declared COARSE grouping, NOT the full branch-axis cross: per-basin max
688
+ # anchors on the weight-only grouped grid (32 basins: modes x WS x PS x 2 memory
689
+ # modes each way) put 64 forced trials into a 100-trial budget and starved the
690
+ # search — gpt-oss prefill regressed 3463 -> 4099us on a quiet box before this was
691
+ # scoped. Which axes qualify is the KERNEL's declaration (configuration knowledge);
692
+ # axes absent from this grid are ignored.
693
+ anchors = []
694
+ coarse_seen = set()
695
+ coarse_axes = [k for k in self.path_anchor_axes if k in basin_axes]
696
+ for basin, idxs in groups.items():
697
+ ordered = sorted(idxs, key=tile_order)
698
+ anchors.append(ordered[len(ordered) // 2])
699
+ if not self.path_anchor_axes:
700
+ continue # no declaration -> median anchors only
701
+ # declared axes absent from THIS grid still coarse-group to () — one global
702
+ # max-tile anchor, the big-tile guarantee without per-basin anchor spam
703
+ coarse = tuple(basin[basin_axes.index(k)] for k in coarse_axes)
704
+ if coarse not in coarse_seen and ordered[-1] != anchors[-1]:
705
+ coarse_seen.add(coarse)
706
+ anchors.append(ordered[-1])
707
+ return anchors
708
+
709
+ def _warm_start_index(self, configs: List[Config]):
710
+ """Return the index in ``configs`` matching the most recently cached
711
+ key's best config (or ``None`` if no prior tune or no match in the
712
+ current pruned list)."""
713
+ if not self.cache:
714
+ return None
715
+ # Python 3.7+ dicts preserve insertion order; last entry = most recent tune.
716
+ prev_best = next(reversed(self.cache.values()))
717
+ prev_kwargs = prev_best.all_kwargs()
718
+ for i, c in enumerate(configs):
719
+ if c.all_kwargs() == prev_kwargs:
720
+ return i
721
+ return None
722
+
723
+ def check_disk_cache(self, tuning_key, configs, bench_fn):
724
+ """Persist the tuned best config to Triton's on-disk cache so a later run (or process)
725
+ skips the whole minutes-long search+compile — most of which is Triton codegen, not
726
+ benching. Keyed exactly like Triton's own autotune cache (triton version + backend +
727
+ kernel source hash + invalidating env + key + the full config grid), so any of those
728
+ changing re-tunes.
729
+
730
+ Unlike Triton's stock version we do NOT bail when configs carry a ``pre_hook`` (ours
731
+ always do): we persist each config's ``all_kwargs()`` — serializable and pre_hook-free —
732
+ and on a hit re-match it to the live ``Config`` object, so the pre_hook is preserved from
733
+ code rather than lost in (de)serialization. A hit launches only the winning config."""
734
+ if not tuning_key:
735
+ bench_fn()
736
+ return False
737
+
738
+ from triton.compiler.compiler import make_backend
739
+
740
+ fn = self.fn
741
+ while not isinstance(fn, JITFunction):
742
+ fn = fn.fn
743
+ env_vars = get_cache_invalidating_env_vars()
744
+ cache_key = [
745
+ triton_key(),
746
+ make_backend(driver.active.get_current_target()).hash(),
747
+ fn.cache_key,
748
+ str(sorted(env_vars.items())),
749
+ # Key NAMES and the search budget are determinants too: tuning_key holds
750
+ # values only (renaming a key or swapping two same-typed keys would collide),
751
+ # and FINEGRAINED_AUTOTUNE_TRIALS is not in Triton's invalidating-env list —
752
+ # without n_trials a 3-trial exploratory winner gets replayed at 100 trials.
753
+ str(self.keys),
754
+ f"n_trials={self.n_trials},n_startup_trials={self.n_startup_trials},gamma={self.gamma}",
755
+ str(tuning_key),
756
+ ] + [str(c) for c in configs] # str(Config) is pre_hook-free and process-stable
757
+ cache_key = hashlib.sha256("-".join(cache_key).encode("utf-8")).hexdigest()
758
+ cache = get_cache_manager(cache_key)
759
+ file_name = f"{fn.__name__[:150]}.bayes_autotune.json"
760
+ keylog = os.environ.get("FINEGRAINED_AUTOTUNE_KEYLOG")
761
+ if keylog:
762
+ with open(keylog, "a") as f:
763
+ f.write(json.dumps({
764
+ "fn": fn.__name__,
765
+ "hit": cache.get_file(file_name) is not None,
766
+ "tuning_key": str(tuning_key),
767
+ "keys": str(self.keys),
768
+ "n_configs": len(configs),
769
+ "env": str(sorted(env_vars.items())),
770
+ "fn_cache_key": fn.cache_key[:16],
771
+ "hash": cache_key[:16],
772
+ }) + "\n")
773
+
774
+ # signature -> live Config (carries the pre_hook); used to re-match the cached winner
775
+ by_sig = {tuple(sorted(c.all_kwargs().items())): c for c in configs}
776
+
777
+ def load_crown() -> bool:
778
+ path = cache.get_file(file_name)
779
+ if not path:
780
+ return False
781
+ try:
782
+ with open(path) as f:
783
+ data = json.load(f)
784
+ best = by_sig.get(tuple(sorted(data["best"].items())))
785
+ if best is None:
786
+ return False
787
+ self.cache[tuning_key] = best
788
+ self.configs_timings = {
789
+ by_sig[s]: t
790
+ for kw, t in data["timings"]
791
+ for s in (tuple(sorted(kw.items())),)
792
+ if s in by_sig
793
+ }
794
+ return True
795
+ except Exception:
796
+ return False # corrupt/stale cache file → re-tune
797
+
798
+ if load_crown():
799
+ return True
800
+
801
+ # Cross-process tuning lock. Without it, TP/EP ranks sharing this cache dir all tune the
802
+ # same key CONCURRENTLY and each crowns its own winner: the search is seeded-random and
803
+ # near-ties break on measurement noise, so ranks end up on different tiles, round
804
+ # differently, and greedy decoding turns that into different tokens (measured: an 8-rank
805
+ # MXFP8 run where 3 ranks emitted a different token). Serializing means one rank tunes and
806
+ # the rest read the same crown — identical configs, and ~world_size less tuning wall-clock.
807
+ # Best-effort: any lock failure falls through to tuning locally (correct, just redundant).
808
+ with _tuning_lock(cache, file_name):
809
+ if load_crown(): # another rank tuned this key while we waited
810
+ return True
811
+ bench_fn()
812
+ # publish INSIDE the lock: a rank released to an empty cache would tune again
813
+ try:
814
+ cache.put(
815
+ json.dumps(
816
+ {
817
+ "best": self.cache[tuning_key].all_kwargs(),
818
+ "timings": [
819
+ (c.all_kwargs(), t) for c, t in self.configs_timings.items()
820
+ ],
821
+ }
822
+ ),
823
+ file_name,
824
+ binary=False,
825
+ )
826
+ except Exception:
827
+ pass
828
+ return False
829
+
830
+
831
+ @contextmanager
832
+ def _tuning_lock(cache, file_name: str):
833
+ """Serialize tuning of ONE key across every process sharing the Triton cache directory.
834
+
835
+ Held while a key is tuned and its crown published, so concurrent TP/EP ranks converge on the
836
+ same config instead of each crowning its own (see ``check_disk_cache``). Advisory ``flock`` on
837
+ a sidecar file next to the crown; entirely best-effort — no cache dir, no ``fcntl``, or a lock
838
+ that cannot be taken within ``FINEGRAINED_TUNE_LOCK_TIMEOUT`` (default 30 min, since the holder
839
+ is doing a real tune) all fall through to tuning locally. Losing the lock costs redundant work,
840
+ never correctness."""
841
+ lock_file = None
842
+ try:
843
+ import fcntl
844
+
845
+ cache_dir = getattr(cache, "cache_dir", None)
846
+ if cache_dir:
847
+ os.makedirs(cache_dir, exist_ok=True)
848
+ deadline = time.time() + float(
849
+ os.environ.get("FINEGRAINED_TUNE_LOCK_TIMEOUT") or 1800
850
+ )
851
+ lock_file = open(os.path.join(cache_dir, f"{file_name}.lock"), "w")
852
+ while True:
853
+ try:
854
+ fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
855
+ break
856
+ except BlockingIOError:
857
+ if time.time() > deadline:
858
+ logger.warning(
859
+ "[autotune] %s: tuning lock held elsewhere for >%.0fs — tuning locally",
860
+ file_name,
861
+ time.time() - (deadline - 1800),
862
+ )
863
+ lock_file.close()
864
+ lock_file = None
865
+ break
866
+ time.sleep(1.0)
867
+ except Exception:
868
+ lock_file = None
869
+ try:
870
+ yield
871
+ finally:
872
+ if lock_file is not None:
873
+ try:
874
+ import fcntl
875
+
876
+ fcntl.flock(lock_file, fcntl.LOCK_UN)
877
+ finally:
878
+ lock_file.close()
879
+
880
+
881
+
882
+ def bayesian_autotune(
883
+ configs,
884
+ key,
885
+ *,
886
+ n_trials: int = 80,
887
+ max_failures: int | None = None,
888
+ n_startup_trials: int = 12,
889
+ gamma: float = 0.25,
890
+ refine: bool = True,
891
+ max_refine_iters: int = 5,
892
+ log_path: str | None = None,
893
+ path_anchor_axes: tuple[str, ...] = (),
894
+ finite_check_args: tuple[str, ...] = (),
895
+ cache_results: bool = True,
896
+ reset_to_zero=None,
897
+ restore_value=None,
898
+ **kwargs,
899
+ ):
900
+ """Decorator mirroring ``@triton.autotune``. Extra kwargs:
901
+ n_trials: successfully measured configs per key (TPE budget; configs
902
+ that fail to compile/run are skipped without consuming it)
903
+ n_startup_trials: random seed configs before the TPE model kicks in
904
+ gamma: top fraction of measured configs treated as "good"
905
+ refine, max_refine_iters: coordinate-descent refinement after the TPE
906
+ path_anchor_axes: branch axes (kwarg names) whose values relocate the tile
907
+ optimum — one guaranteed max-tile anchor per value combo
908
+ (see _basin_anchor_indices); the kernel declares them, the
909
+ tuner stays independent of configuration details
910
+ max_failures: abort the tune once this many configs fail to compile/run before
911
+ the measured budget is met (default n_trials; env override
912
+ FINEGRAINED_AUTOTUNE_MAX_FAILURES). Without it a broadly broken
913
+ grid spins: failures don't consume the trial budget, so the search
914
+ keeps picking and each pick costs an O(configs) rescan.
915
+ log_path: JSONL of benched configs (or FINEGRAINED_AUTOTUNE_LOG)
916
+ cache_results: persist the tuned best config to disk (on by default) so later
917
+ runs skip the search+compile — see BayesianAutotuner.check_disk_cache"""
918
+
919
+ def decorator(fn):
920
+ return BayesianAutotuner(
921
+ fn,
922
+ fn.arg_names,
923
+ configs,
924
+ key,
925
+ reset_to_zero,
926
+ restore_value,
927
+ n_trials=n_trials,
928
+ max_failures=max_failures,
929
+ n_startup_trials=n_startup_trials,
930
+ gamma=gamma,
931
+ refine=refine,
932
+ max_refine_iters=max_refine_iters,
933
+ log_path=log_path,
934
+ path_anchor_axes=path_anchor_axes,
935
+ finite_check_args=finite_check_args,
936
+ cache_results=cache_results,
937
+ **kwargs,
938
+ )
939
+
940
+ return decorator
build/torch-rocm/compat.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import contextvars
17
+ import functools
18
+ from contextlib import contextmanager
19
+
20
+
21
+ import os
22
+
23
+ import torch
24
+ import triton
25
+ import triton.language as tl
26
+ from torch.library import triton_op, wrap_triton
27
+
28
+ # Programmatic dependent launch for the decode chain (batched GEMMs, GLU, act quant, reduce): each
29
+ # kernel waits on its grid dependency at the top and releases its dependents just before the
30
+ # epilogue. The LATE trigger is load-bearing: released early, the next kernel's CTAs co-reside and
31
+ # steal issue slots (-15us on a single-wave chain); released late they only hide launch latency
32
+ # (GLM/DSV4/GPT-OSS decode -4.5..-4.9%, bit-identical). Prefill grids are multi-wave and never
33
+ # take it. FINEGRAINED_PDL=0 disables.
34
+ DECODE_PDL = os.environ.get("FINEGRAINED_PDL", "1") == "1"
35
+
36
+
37
+ def decode_pdl() -> bool:
38
+ """PDL for this launch: the flag, and not under torch.compile — dynamo's TTIR access analysis
39
+ cannot read the ``griddepcontrol`` inline asm and would mark every input mutated (extra copies,
40
+ no fusion). Deployment decode runs eager launches under cudagraphs, where PDL applies."""
41
+ return DECODE_PDL and not torch.compiler.is_compiling()
42
+
43
+ # kernel-builder generates ``_ops.py`` into the built variant (the op namespace carries the build's
44
+ # unique id) and refuses to build over a tracked one, so the source tree has none: an unbuilt
45
+ # checkout (tests, bench, FINEGRAINED_KERNELS_PATH) registers its ops under the plain package name.
46
+ try:
47
+ from ._ops import add_op_namespace_prefix
48
+ except ImportError:
49
+
50
+ def add_op_namespace_prefix(name: str) -> str:
51
+ return f"finegrained_kernels::{name}"
52
+
53
+
54
+
55
+
56
+
57
+ # ── Format constants ──────────────────────────────────────────────────────────
58
+
59
+ # FP8 (E4M3) is the main format for weights and activations;
60
+ FP8_DTYPE = torch.float8_e4m3fn
61
+
62
+ # FP4 (E2M1) packs two 4-bit nibbles per byte. MX formats (MXFP4 weights, MXFP8
63
+ # E4M3 weights/activations) share one UE8M0 scale per 32-element K-group — the OCP
64
+ # MX block size, consumed by ``tl.dot_scaled``. Format constants, not tunables.
65
+ NIBBLES_PER_BYTE = 2
66
+
67
+ MX_SCALE_GROUP_K = 32
68
+
69
+ NVFP4_SCALE_GROUP_K = 16
70
+
71
+
72
+
73
+ # ── Host-side helpers ─────────────────────────────────────────────────────────
74
+
75
+
76
+ # set ONLY while an opaque op's registered fake impl runs (shape inference); read by
77
+ # compile_time_only_triton_wrap to no-op the kernel launches within
78
+ _SKIP_LAUNCHES = contextvars.ContextVar("finegrained_kernels_skip_launches", default=False)
79
+ # Plain mirror of the contextvar, read on the launch path. Dynamo cannot trace `ContextVar.get`
80
+ # ("Unsupported method call ... method `get` of class `ContextVar`"), which broke tracing of every
81
+ # non-opaque op; a module-level bool it can constant-fold. The contextvar stays authoritative for
82
+ # set/reset (nesting, async isolation) — this only mirrors it for the read.
83
+ _SKIP_LAUNCHES_MIRROR = False
84
+
85
+
86
+
87
+ def compile_time_only_triton_wrap(kernel):
88
+ """``wrap_triton`` while torch.compile is tracing (required to capture a raw Triton
89
+ launch into the graph), the bare kernel in eager. Every kernel launch goes through
90
+ this: eager skips ``wrap_triton``'s per-call dispatch overhead, and it dodges a
91
+ correctness trap — eager ``wrap_triton`` of a stock ``@triton.autotune`` kernel that
92
+ has ``prune_configs_by`` re-runs the FULL tune on every call (torch's wrapper rebuilds
93
+ a fresh autotuner around the pruned configs per call, ``triton_kernel_wrap.py``:
94
+ ``autotune(configs=pruned, key=[])``), so nothing ever lands in the original
95
+ ``Autotuner.cache`` (~2.3s/call for ``_mx_act_quant_kernel``) and the mid-capture
96
+ tune invalidates cudagraph capture. Pruner-less kernels and the ``bayesian_autotune``
97
+ kernels reuse their cache through the wrapper (measured) — but eager never needs the
98
+ wrapper at all.
99
+
100
+ Inside an OPAQUE op's fake impl (see ``compile_time_only_triton_op``) the launch is
101
+ skipped outright: the op function has already allocated the correctly-shaped
102
+ outputs, which is all fake mode needs. The skip is an EXPLICIT contextvar, never
103
+ ambient fake-mode detection — ``triton_op``'s capture also runs under FakeTensor
104
+ mode, and skipping there would compile graphs with the kernel MISSING (uninitialized
105
+ outputs; found as identical garbage across formats, 2026-07-16). The skip is also
106
+ load-bearing in the other direction — probed: without it the opaque ops' fake impls
107
+ reach the launcher with FakeTensors ("RuntimeError when making fake tensor call";
108
+ ``is_compiling`` is still true during fake prop, so they'd take the wrap_triton
109
+ branch and hit the pre_hook assert that made them opaque in the first place)."""
110
+ if _SKIP_LAUNCHES_MIRROR:
111
+
112
+ class _NoLaunch:
113
+ def __getitem__(self, grid):
114
+ return lambda *args, **kwargs: None
115
+
116
+ return _NoLaunch()
117
+ return wrap_triton(kernel) if torch.compiler.is_compiling() else kernel
118
+
119
+
120
+
121
+ def compile_time_only_triton_op(name, mutates_args=(), opaque=False):
122
+ """``@triton_op`` under torch.compile (the registered custom op is what dynamo
123
+ captures), the plain function in eager: the torch.library op dispatch stack costs
124
+ ~160µs per eager call — the dominant decode CPU cost in the eager-breakdown probe —
125
+ and eager needs none of it. The op is still registered at import time, so compiled
126
+ callers and ``torch.ops`` introspection see it unchanged. Sibling of
127
+ ``compile_time_only_triton_wrap`` (same rule one level down, at the kernel launch)."""
128
+
129
+ def decorator(fn):
130
+ if opaque:
131
+ # kernels whose configs carry a pre_hook (TMA descriptor rebinders) cannot
132
+ # be traced by triton_op's capture; register OPAQUE instead — the fake impl
133
+ # is the op fn itself, whose launches no-op under FakeTensor mode (see
134
+ # compile_time_only_triton_wrap), leaving just the shape-correct allocations
135
+ op = torch.library.custom_op(name, mutates_args=mutates_args)(fn)
136
+
137
+ @functools.wraps(fn)
138
+ def fake_impl(*args, **kwargs):
139
+ global _SKIP_LAUNCHES_MIRROR
140
+ token = _SKIP_LAUNCHES.set(True)
141
+ prev, _SKIP_LAUNCHES_MIRROR = _SKIP_LAUNCHES_MIRROR, True
142
+ try:
143
+ return fn(*args, **kwargs)
144
+ finally:
145
+ _SKIP_LAUNCHES.reset(token)
146
+ _SKIP_LAUNCHES_MIRROR = prev
147
+
148
+ op.register_fake(fake_impl)
149
+ else:
150
+ op = triton_op(name, mutates_args=mutates_args)(fn)
151
+
152
+ @functools.wraps(fn)
153
+ def dispatch(*args, **kwargs):
154
+ # The op path is also the AUTOGRAD path (`register_autograd` hangs off the op, and
155
+ # these ops take the RAW activation, so that is where a usable gradient exists).
156
+ # Calling `fn` skips the dispatcher entirely, so a differentiable caller would
157
+ # silently get no gradient — route through the op whenever a tensor wants one.
158
+ # Training runs at prefill shapes where the ~160us dispatch is noise; inference
159
+ # runs under no_grad and keeps the bare-function fast path.
160
+ if torch.compiler.is_compiling() or (
161
+ torch.is_grad_enabled()
162
+ and any(getattr(a, "requires_grad", False) for a in args)
163
+ ):
164
+ return op(*args, **kwargs)
165
+ return fn(*args, **kwargs)
166
+
167
+ dispatch._triton_op = op
168
+ return dispatch
169
+
170
+ return decorator
171
+
172
+
173
+
174
+ @contextmanager
175
+ def device_context(device: torch.device):
176
+ """Context manager that sets the active device for any backend (cuda, xpu, etc.)."""
177
+ backend = getattr(torch, device.type, None)
178
+ if backend is not None and hasattr(backend, "device"):
179
+ with backend.device(device):
180
+ yield
181
+ else:
182
+ yield
183
+
184
+
185
+
186
+ @functools.lru_cache(maxsize=8)
187
+ def sm_count(device_index: int) -> int:
188
+ """Processor (CUDA SM / XPU Xe-core) count for a device (a constant) — cached so callers
189
+ don't re-query it each launch. Read from Triton's active driver so it works on any backend,
190
+ with a CUDA fallback."""
191
+ try:
192
+ return triton.runtime.driver.active.utils.get_device_properties(device_index)[
193
+ "multiprocessor_count"
194
+ ]
195
+ except Exception:
196
+ active_device = get_active_device_type()
197
+ if active_device == "cuda":
198
+ return torch.cuda.get_device_properties(device_index).multi_processor_count
199
+ elif active_device == "xpu":
200
+ return torch.xpu.get_device_properties(device_index).multi_processor_count
201
+ else:
202
+ raise RuntimeError(
203
+ f"Unsupported device type {active_device} for sm_count; only cuda/xpu are supported."
204
+ )
205
+
206
+
207
+
208
+ def tl_dtype(dtype: torch.dtype) -> tl.dtype:
209
+ """The ``tl`` dtype matching a torch dtype (``torch.bfloat16`` → ``tl.bfloat16``) — the
210
+ attribute names line up, so no table. For passing a tensor's dtype as a kernel constexpr
211
+ when the kernel can't read it off a pointer argument."""
212
+ return getattr(tl, str(dtype).removeprefix("torch."))
213
+
214
+
215
+
216
+ def get_active_device_type() -> str:
217
+ """Active torch device type for the current Triton backend (``"cuda"``, ``"xpu"``, ...).
218
+
219
+ Falls back to ``"cuda"`` when no driver is loaded — Triton raises
220
+ ``RuntimeError: 0 active drivers ([])`` on driverless build boxes, and the
221
+ autotune-config builder is evaluated at module-import time under the
222
+ ``@triton.autotune`` decorator (no kernel launches there, so the default is
223
+ only used to shape the config list).
224
+ """
225
+ try:
226
+ return triton.runtime.driver.active.get_active_torch_device().type
227
+ except RuntimeError:
228
+ return "cuda"
229
+
230
+
231
+
232
+ def resolve_memory_modes(memory_modes):
233
+ """Resolve the generic ``"descriptor"`` B_MEMORY_MODE to the device's flavor: host-built
234
+ (NVIDIA TMA) on CUDA, device-built in-kernel tensormap on XPU. Installs the XPU
235
+ scratch allocator device-built tensormaps need."""
236
+ descriptor = (
237
+ "device_descriptor" if get_active_device_type() == "xpu" else "host_descriptor"
238
+ )
239
+ memory_modes = tuple(descriptor if m == "descriptor" else m for m in memory_modes)
240
+ if "device_descriptor" in memory_modes:
241
+ # device-side descriptors build in-kernel tensormaps that need a scratch
242
+ # allocator — on whichever accelerator is active (XPU always; CUDA when a kernel
243
+ # requests "device_descriptor" explicitly to skip the host-descriptor plumbing).
244
+ device_type = get_active_device_type()
245
+ triton.set_allocator(
246
+ lambda size, alignment, stream: torch.empty(
247
+ size, dtype=torch.int8, device=device_type
248
+ )
249
+ )
250
+ return memory_modes
251
+
252
+
253
+
254
+ def get_accelerator_autotuning_configs(
255
+ *,
256
+ mx: bool = False,
257
+ tune_block_m: bool = False,
258
+ tune_block_n: bool = False,
259
+ tune_block_nk: bool = False,
260
+ swap_ab: bool = False,
261
+ warp_spec: bool = False,
262
+ packed_schedule: bool = False,
263
+ compute_modes=None,
264
+ a_memory_modes=None,
265
+ b_memory_modes=None,
266
+ pre_hook=None,
267
+ ):
268
+ """Autotune search grid for the current accelerator — the single generator for every
269
+ GEMM kernel. One union span serves both families: num_warps {2,4,8,16} x num_stages
270
+ {2..6} (XPU: warps {8,16}); the tile comes from the caller's ``block_size``, or
271
+ ``tune_block_nk`` spans BN {32,64,128,256} x BK {64,128,256,512}. ``mx=True`` (group-32
272
+ scale formats, MXFP4/MXFP8) requires ``compute_modes`` (BK is coupled per mode) and
273
+ ``tune_block_nk`` (MX kernels have no caller block_size — the tile is always tuned).
274
+
275
+ ``compute_modes`` emits the ``COMPUTE_MODE`` axis (``None`` — the default — emits no
276
+ axis, like ``b_memory_modes``), tuner-selected per workload (token count is in the
277
+ key): ``"dot_scaled"`` native group-32 scaled MMA (wide BK {128,256}; wins once the
278
+ grid saturates, ~S>=32), ``"dot"`` fp8 ``tl.dot`` + per-group software rescale
279
+ (under ``mx`` its BK is pinned to the 32-group), ``"scalar"`` CUDA-core FMA reduce
280
+ (no MMA M->16 pad; wins memory-bound decode, only emit where implemented — it is
281
+ pinned to BM=1 here because a bigger-BM scalar config at BN == BM COMPILES and
282
+ silently computes garbage the tuner would time and pick).
283
+
284
+ Axes, crossed per flag:
285
+
286
+ - ``tune_block_m``: BLOCK_SIZE_M in {16,32,64,128} instead of a fixed
287
+ launch-time choice.
288
+ - ``tune_block_n``: independent BLOCK_SIZE_N in {64,128,256} for the block-scale
289
+ kernels whose BK is pinned by the caller (activation scale groups are per
290
+ block_k) but whose N tile may grow past the scale granularity.
291
+ - ``swap_ab``: marks the single-token decode GEVM kernels — crosses in the coupled
292
+ decode ``(BLOCK_SIZE_M, SWAP_AB)`` pairs (see below). BK=512 earns its union-span
293
+ seat here: at decode's tiny memory-parallelism-bound grids, longer per-iteration
294
+ bursts are the one in-block lever that helps (+12% on the dsv4 swap GEMV; deeper
295
+ num_stages and split-K both measured WORSE). Prefill tunes carry the same rows as
296
+ trial-budget dilution (descriptor_box_pruner drops the illegal e4m3 boxes).
297
+ - ``warp_spec`` (CUDA only): warp-specialize the K-loop, crossed over every mode —
298
+ pair it with ``warp_spec_compile_guard_pruner``, which owns the can't-compile
299
+ regions (including dot_scaled + WS, a PassManager failure). Compile support is
300
+ (shape, config)-dependent on Triton 3.7.1, so it is a tuner axis — failures score
301
+ inf and self-prune; where it compiles it is both faster and (bd grouped gate_up)
302
+ load-bearing for correctness.
303
+ - ``a_memory_modes``: the A_MEMORY_MODE activation-load axis (descriptor legal only
304
+ without a gather — the tile's rows are the contiguous sorted positions; the pruner
305
+ fences descriptor rows when GatherIdx is passed).
306
+ - ``b_memory_modes``: the B_MEMORY_MODE weight-load axis — pass
307
+ ``("descriptor", "pointer")`` to let the tuner pick the device's tensor-descriptor
308
+ flavor vs explicit pointers; ``None`` (default) emits no axis. In the plain-dot
309
+ kernels the descriptor arm is the SWAPPED loop (see the bd 2D kernel).
310
+
311
+ A given ``pre_hook`` is attached to every config and must self-guard on
312
+ ``B_MEMORY_MODE``.
313
+
314
+ Winner census over 743 tunes (B200, all kernels/shapes), useful when reading tuner
315
+ logs: every value wins somewhere — MX lives in stages 5/6 (69 wins) and never picked
316
+ warps 16; plain-dot picked warps 16 (19 wins, e.g. the bd gate_up decode) and warps 2
317
+ (24 wins, the tensor batched decode 9/11) and concentrated in stages 3/4.
318
+ """
319
+ assert not (mx and compute_modes is None), (
320
+ "the MX grid couples BLOCK_SIZE_K to COMPUTE_MODE — pass compute_modes"
321
+ )
322
+ assert not (mx and not tune_block_nk), (
323
+ "MX kernels have no caller block_size — their (BN, BK) tile is always tuned"
324
+ )
325
+ is_xpu = get_active_device_type() == "xpu"
326
+
327
+ # ONE union span for both families — the per-family sets were stock-tuner-era cost
328
+ # caps; the Bayesian tuner samples a fixed trial budget, values measured dead for
329
+ # one family (MX warps=16, MX BK=64; plain-dot stages 5/6 were simply never tested)
330
+ # just never win, and can't-compile configs (smem) score inf and stay out of the
331
+ # TPE densities. The 743-tune winner census (see docstring) maps where winners
332
+ # actually live per family. BK=512 is in the union span for every tune_block_nk
333
+ # kernel — its win is decode-only (see the swap_ab docstring bullet), and prefill
334
+ # rows are accepted trial-budget dilution.
335
+ #
336
+ # Axes measured DEAD and deliberately absent (do not re-add without fresh evidence):
337
+ # num_ctas slower everywhere + miscompiles with TMA at num_stages<=4
338
+ # flatten (tl.range) proxy +17% never survived e2e (<=1%; gate_up regressed)
339
+ # persistent 2D grid-stride 2D prototype's win was an inline-vs-helper artifact
340
+ # batched WARP_SPEC decode loops are launch/latency-bound; WS never won a batched
341
+ # cell, so the axis is not emitted at the six batched sites
342
+ num_warps = [8, 16] if is_xpu else [2, 4, 8, 16]
343
+ num_stages = [2, 3, 4, 5, 6]
344
+ bn_span = (128,) if is_xpu else (32, 64, 128, 256)
345
+ bk_span = (128,) if is_xpu else (64, 128, 256, 512)
346
+
347
+ # no tuned tile -> one empty meta-dict (the tile comes from the launch kwargs)
348
+ blocks = (
349
+ [{"BLOCK_SIZE_N": bn, "BLOCK_SIZE_K": bk} for bn in bn_span for bk in bk_span]
350
+ if tune_block_nk
351
+ else [{}]
352
+ )
353
+
354
+ if compute_modes is not None:
355
+ # generic COMPUTE_MODE axis; MX "dot"'s BK is STRUCTURALLY the UE8M0 scale group
356
+ # (one software rescale per K-step — a format constraint, not a tile choice), so
357
+ # it overrides the wide-BK span and the dedup below collapses the copies.
358
+ blocks = [
359
+ {
360
+ **b,
361
+ "COMPUTE_MODE": mode,
362
+ **({"BLOCK_SIZE_K": MX_SCALE_GROUP_K} if mx and mode == "dot" else {}),
363
+ }
364
+ for b in blocks
365
+ for mode in compute_modes
366
+ ]
367
+
368
+ if tune_block_m:
369
+ # BM=256 is OPEN, not disproven (2026-07-29). A plain fp8 GEMM at the dsv4 attn prefill
370
+ # shape clearly prefers BM=BN=256 (350-367us vs 420-478us at 128, 7 interleaved repeats,
371
+ # no overlap), so the basin is real for an unscaled loop. Our kernel measured 523us vs
372
+ # 495us against it — but the run-to-run spread at BM=128 on this box is 13.9%, so a 5.8%
373
+ # difference decides nothing. Left out because nothing has shown it WINNING here and the
374
+ # axis widens every tune 25%; re-add only with repeated tunes that clear the noise floor.
375
+ blocks = [{**b, "BLOCK_SIZE_M": bm} for b in blocks for bm in (16, 32, 64, 128)]
376
+
377
+ if tune_block_n:
378
+ blocks = [{**b, "BLOCK_SIZE_N": bn} for b in blocks for bn in (64, 128, 256)]
379
+
380
+ if warp_spec and get_active_device_type() == "cuda":
381
+ blocks = [{**b, "WARP_SPEC": ws} for b in blocks for ws in (False, True)]
382
+
383
+ if packed_schedule:
384
+ # grouped M-tile resolution axis: packed-table loads vs the E-wide register-resident
385
+ # layout — pure register relief, bit-identical tiles; wins only where the inline
386
+ # resolve is pressure-walled (dsv4 BM128/BK256 regs 244->187, +8%), so the tuner picks
387
+ blocks = [{**b, "PACKED_SCHEDULE": ps} for b in blocks for ps in (False, True)]
388
+
389
+ # (descriptor-mode can't-win/can't-serve regions are fenced per kernel: descriptor_box_pruner,
390
+ # matched_memory_modes_pruner, descriptor_needs_prequant_pruner)
391
+ if a_memory_modes is not None:
392
+ blocks = [
393
+ {**b, "A_MEMORY_MODE": mm}
394
+ for b in blocks
395
+ for mm in resolve_memory_modes(a_memory_modes)
396
+ ]
397
+ if b_memory_modes is not None:
398
+ blocks = [
399
+ {**b, "B_MEMORY_MODE": mm}
400
+ for b in blocks
401
+ for mm in resolve_memory_modes(b_memory_modes)
402
+ ]
403
+
404
+ # The coupled decode (BLOCK_SIZE_M, SWAP_AB) pairs — the single source of the swap/BM
405
+ # coupling. At M=1 the MMA 16-atom is filled either by replicating the token (non-swap
406
+ # BM=16, ~40% over the degenerate BM=1 on plain tl.dot) or by putting the weight's
407
+ # output rows in M (swap, BM=1). Swap REQUIRES BM=1 (its rhs is the single token
408
+ # padded to the N=16 atom), so (16, swap) never exists; the tuner picks. Prefill
409
+ # kernels leave SWAP_AB unset — the prefill swap arms measured losers everywhere
410
+ # (see the bd 2D kernel's tuner note).
411
+ if swap_ab and tune_block_m:
412
+ # tuned-BM kernels (the 2D mx GEMM): the prefill BM sweep keeps no-swap; the swap
413
+ # decode rows are APPENDED at their structural BM=1 (dedup collapses the copies)
414
+ blocks = [{**b, "SWAP_AB": False} for b in blocks] + [
415
+ {**b, "BLOCK_SIZE_M": 1, "SWAP_AB": True} for b in blocks
416
+ ]
417
+ elif swap_ab:
418
+ blocks = [
419
+ {**b, "BLOCK_SIZE_M": bm, "SWAP_AB": sw}
420
+ for b in blocks
421
+ for bm, sw in ((1, False), (16, False), (1, True))
422
+ ]
423
+
424
+ # scalar's BM is STRUCTURALLY 1 (a single-row GEVM reduce — a bigger-BM scalar
425
+ # config at BN == BM even COMPILES and silently computes garbage the tuner would
426
+ # time and pick); the dedup below collapses the pinned copies of the BM sweep.
427
+ blocks = [
428
+ {**b, "BLOCK_SIZE_M": 1}
429
+ if b.get("COMPUTE_MODE") == "scalar" and "BLOCK_SIZE_M" in b
430
+ else b
431
+ for b in blocks
432
+ ]
433
+
434
+ configs = [
435
+ triton.Config(b, num_warps=w, num_stages=s, pre_hook=pre_hook)
436
+ for b in blocks
437
+ for w in num_warps
438
+ for s in num_stages
439
+ ]
440
+ # dedup: the scalar BM=1 pin and the MX "dot" BK override collapse swept axes onto
441
+ # one value, leaving identical copies that would each be benched.
442
+ return list({str(c): c for c in configs}.values())
443
+
444
+
445
+
446
+ @functools.cache
447
+ def is_sm10x() -> bool:
448
+ """Whether the CURRENT device is Blackwell-datacenter (sm_10x, TMEM scaled-MMA) —
449
+ the target the ``dot_scaled`` compiler-bug guards are scoped to. False off-CUDA and on
450
+ driverless build boxes (``torch.cuda.is_available()`` short-circuits before the
451
+ capability query, which would otherwise raise); cached (pruners call this per tune, and
452
+ a process is pinned to one device)."""
453
+ return torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10
454
+
455
+
456
+ @functools.cache
457
+ def is_sm90() -> bool:
458
+ """Whether the CURRENT device is Hopper (sm_90) — the target the measured NVIDIA-backend
459
+ MMA-lowering guards are scoped to. A POSITIVE arch test on purpose: ``not is_sm10x()`` is
460
+ also true on XPU, ROCm and driverless build boxes, none of which run the CUDA codegen those
461
+ guards were probed against, so scoping by negation would restrict backends no one measured.
462
+ Same driverless-safe short-circuit and caching as ``is_sm10x``."""
463
+ return torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0)
464
+
465
+
466
+
467
+ @functools.lru_cache(maxsize=None)
468
+ def sm_shared_memory_limit() -> int:
469
+ """Max dynamic shared memory per block (bytes) for the CURRENT device — the cap
470
+ Triton reports as the 'Hardware limit' on an ``out of resource: shared memory``
471
+ failure (~232 KB on B200, ~227 KB on H100, much less on older/consumer parts).
472
+ Queried from the driver so the prune adapts to the hardware instead of hardcoding
473
+ one GPU; cached (a process is pinned to one device)."""
474
+ dev = get_active_device_type()
475
+ device_index = (
476
+ torch.cuda.current_device()
477
+ if dev == "cuda"
478
+ else torch.xpu.current_device()
479
+ if dev == "xpu"
480
+ else 0
481
+ )
482
+ try:
483
+ return triton.runtime.driver.active.utils.get_device_properties(device_index)[
484
+ "max_shared_mem"
485
+ ]
486
+ except Exception:
487
+ if dev == "cuda":
488
+ return torch.cuda.get_device_properties(
489
+ device_index
490
+ ).shared_memory_per_block_optin
491
+ elif dev == "xpu":
492
+ return torch.xpu.get_device_properties(
493
+ device_index
494
+ ).shared_memory_per_block_optin
495
+ else:
496
+ raise RuntimeError(
497
+ f"Unsupported device type {dev} for sm_shared_memory_limit; only cuda/xpu are supported."
498
+ )
build/torch-rocm/descriptors.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Host-TMA descriptors for the kernels' descriptor memory arms. ``maybe_descriptor`` builds one
16
+ where the operand layout can back it; the ``rebind_*`` functions are the autotuner per-config
17
+ ``pre_hook``s that size each descriptor's box to the tuned tile (they MUST mutate ``block_shape`` in
18
+ place — a rebind never reaches the launch) and are no-ops for the pointer arms, whose descriptor
19
+ argument is a dead placeholder."""
20
+
21
+ import torch
22
+ from triton.tools.tensor_descriptor import TensorDescriptor
23
+
24
+
25
+ def rebind_operand_box(nargs, mode_key, desc_key, rows, cols):
26
+ """Set one operand's host-TMA box to ``[rows, cols]`` in place — MUST mutate (a rebind never
27
+ reaches the launch). No-op for a pointer config, whose descriptor is a dead int placeholder."""
28
+ desc = nargs[desc_key]
29
+ # None = the operand's layout cannot back a descriptor (see `maybe_descriptor`); those
30
+ # configs are pruned, so reaching here with one would mean a pruner gap, not a rebind target
31
+ if nargs.get(mode_key, "pointer") != "pointer" and not isinstance(desc, int) and desc is not None:
32
+ desc.block_shape = [rows, cols]
33
+
34
+
35
+ def maybe_descriptor(t: torch.Tensor, box: list[int]) -> TensorDescriptor | None:
36
+ """A host-TMA descriptor over ``t``, or None when the layout cannot back one — a TMA box
37
+ needs a unit-stride innermost dim, which a transposed view (as a backward pass hands in to
38
+ contract over the other axis) does not have. ``None`` is the same signal the pointer arms
39
+ already take for an unused descriptor, and ``descriptor_box_pruner`` drops the configs that
40
+ would read it, so the tuner is left with the pointer arms the kernel serves stride-generally."""
41
+ if t is None or (t.ndim >= 2 and t.stride(-1) != 1):
42
+ return None
43
+ return TensorDescriptor.from_tensor(t, box)
44
+
45
+
46
+ def rebind_bd_descriptors(nargs):
47
+ """Per-config pre_hook: set the A and B host-TMA boxes to the tuned tile over the
48
+ ``(rows, K)`` matrices — ``[BLOCK_SIZE_M, block_k]`` and ``[BLOCK_SIZE_N, block_k]``."""
49
+ rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], nargs["block_k"])
50
+ rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], nargs["block_k"])
51
+
52
+
53
+ def rebind_weight_only_descriptors(nargs):
54
+ """Per-config pre_hook for the weight-only kernels — set the A/B host-TMA boxes to the tuned tile:
55
+ ``[BM, BK]`` over the (M, K) bf16 activation and ``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` over the
56
+ packed (N, K_bytes) weight (bytes: uint8 = packed E2M1, two values/byte). No scale boxes — weight-only
57
+ scales are always affine (never swizzled), read through the pointer arm."""
58
+ wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
59
+ bk = nargs["BLOCK_SIZE_K"]
60
+ rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], bk)
61
+ rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], bk // wvpb)
62
+
63
+
64
+ def rebind_mx_descriptors(nargs):
65
+ """Per-config pre_hook for the MX kernel — set the A/B host-TMA boxes to the tuned tile
66
+ in BYTES over the (rows, K_bytes) packed matrices: ``[BM, BK // ACT_VALUES_PER_BYTE]`` and
67
+ ``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` (values-per-byte read off the operand dtype; uint8 =
68
+ packed E2M1), plus the SWIZZLE_32_4_4 scale boxes on the offline (pre-quantized A) path."""
69
+ avpb = 2 if nargs["A"].dtype == torch.uint8 else 1
70
+ wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
71
+ bk = nargs["BLOCK_SIZE_K"]
72
+ rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], bk // avpb)
73
+ rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], bk // wvpb)
74
+ # SWIZZLE_32_4_4 scale boxes: [1, BLOCK//128, (BK // SCALE_GROUP_K) // 4, 2, 256]. Only where the
75
+ # scale is actually swizzled — else the SA/SB descriptor is a dummy aliased to the operand
76
+ # descriptor, and stamping a scale box would clobber its [BM, BK] operand box. The weight is
77
+ # swizzled iff SWIZZLED_SCALES; the act only when it was also offline-quantized (E4M3 / packed
78
+ # E2M1) — inline (raw bf16 A) stays affine even under a swizzled weight.
79
+ if nargs["SWIZZLED_SCALES"]:
80
+ # Sub-128 tiles read scales via the per-row pointer gather, leaving the descriptor
81
+ # unread — clamp its box to one block so it keeps a valid, non-degenerate shape
82
+ # (a 0-block box traps the descriptor-encoding pass; same clamp as the grouped hook).
83
+ rep_k = max((nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4, 1)
84
+ bn_blocks = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
85
+ nargs["BSDescriptor"].block_shape = [1, bn_blocks, rep_k, 2, 256]
86
+ if nargs["A"].dtype in (torch.float8_e4m3fn, torch.uint8):
87
+ bm_blocks = max(nargs["BLOCK_SIZE_M"] // 128, 1)
88
+ nargs["ASDescriptor"].block_shape = [1, bm_blocks, rep_k, 2, 256]
89
+ # swizzled requant output (Cs is a descriptor): store tile [1, 1, rep_n, 2, 256], rep_n per config
90
+ if nargs["CSDescriptor"] is not None:
91
+ rep_n = (nargs["BLOCK_SIZE_N"] // nargs["SCALE_GROUP_K"]) // 4
92
+ nargs["CSDescriptor"].block_shape = [1, 1, rep_n, 2, 256]
93
+
94
+
95
+ def rebind_batched_mx_bs_descriptor(nargs):
96
+ """Per-config pre_hook: size the swizzled weight-scale descriptor box to the tile's 128-row
97
+ blocks (doubled under GATE, whose tile spans 2*BN interleaved rows). BN<128 (fp8 scalar)
98
+ pointer-gathers instead and never reads the descriptor. Only under SWIZZLED_SCALES; the
99
+ un-swizzled path keeps its dummy box."""
100
+ if not nargs.get("SWIZZLED_SCALES"):
101
+ return
102
+ rep = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
103
+ rep_k = (nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4
104
+ nargs["BSDescriptor"].block_shape = [1, rep, rep_k, 2, 256]
105
+
106
+
107
+ def rebind_grouped_weight_descriptor(nargs):
108
+ """Per-config pre_hook: set the MX weight descriptor box to the tuned
109
+ ``[1, (2 if GATE else 1) * BLOCK_SIZE_N, BLOCK_SIZE_K // values_per_byte]`` over the
110
+ ``(E, 2N|N, K_bytes)`` weight view. MUST mutate ``block_shape`` in place — a rebind never reaches
111
+ the launch. No-op for pointer configs (they never read the descriptor)."""
112
+ if nargs.get("B_MEMORY_MODE", "pointer") == "pointer" or isinstance(
113
+ nargs["BDescriptor"], int
114
+ ):
115
+ return
116
+ values_per_byte = 2 if nargs["B"].dtype == torch.uint8 else 1
117
+ nargs["BDescriptor"].block_shape = [
118
+ 1,
119
+ (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"],
120
+ nargs["BLOCK_SIZE_K"] // values_per_byte,
121
+ ]
122
+
123
+
124
+ def rebind_grouped_act_descriptor(nargs):
125
+ """Per-config pre_hook: set the activation descriptor box to the tuned
126
+ ``[BLOCK_SIZE_M, BLOCK_SIZE_K // act_values_per_byte]`` over the ``(rows, K_bytes)``
127
+ activation matrix. In-place mutate; no-op for pointer-A configs."""
128
+ if nargs.get("A_MEMORY_MODE", "pointer") == "pointer" or isinstance(
129
+ nargs["ADescriptor"], int
130
+ ):
131
+ return
132
+ act_values_per_byte = 2 if nargs["A"].dtype == torch.uint8 else 1
133
+ # tma gather4 loads N independent rows: descriptor_gather requires a 1-row box;
134
+ # the contiguous (no-gather) arm loads the whole [BM, BK_bytes] tile in one box
135
+ nargs["ADescriptor"].block_shape = [
136
+ 1 if nargs.get("GatherIdx") is not None else nargs["BLOCK_SIZE_M"],
137
+ nargs["BLOCK_SIZE_K"] // act_values_per_byte,
138
+ ]
139
+
140
+
141
+ def rebind_grouped_descriptors(nargs):
142
+ """Composite pre_hook: both weight and activation descriptor boxes."""
143
+ rebind_grouped_weight_descriptor(nargs)
144
+ rebind_grouped_act_descriptor(nargs)
145
+
146
+
147
+ def build_grouped_operand_descriptors(a_operand, b_operand):
148
+ """Operand host-TMA descriptors for a grouped launch: A box ``[16, 64]``, B box
149
+ ``[1, 128, 64]`` over the ``(E, 2N|N, K_bytes)`` weight view. Placeholder boxes, re-bound to
150
+ the tuned tile per config by ``rebind_grouped_descriptors``."""
151
+ return (
152
+ TensorDescriptor.from_tensor(a_operand, block_shape=[16, 64]),
153
+ TensorDescriptor.from_tensor(b_operand, block_shape=[1, 128, 64]),
154
+ )
155
+
156
+
157
+ def rebind_grouped_mx_descriptors(nargs):
158
+ """MX composite pre_hook: the operand boxes plus the two SWIZZLE_32_4_4 scale boxes
159
+ ``[1, BLOCK // 128, (BK // SCALE_GROUP_K) // 4, 2, 256]`` over the swizzled
160
+ ``(1, rows // 128, cols // 4, 2, 256)`` views. Activation box is BM // 128 (BM pinned 128);
161
+ the weight box is ``(2 if GATE else 1) * BN // 128`` — the stacked gate|up tile is one 2*BN
162
+ block (BN pinned 128 under GATE by ``swizzled_scales_bm_pruner``). Mutate in place. Both scale
163
+ descriptors are None on the un-swizzled arm (affine read — one SWIZZLED_SCALES flag governs both
164
+ operands), so their boxes are skipped."""
165
+ rebind_grouped_descriptors(nargs)
166
+ # BK below 4 scale groups (BK=64 rows the pruner rejects) still passes through here when a
167
+ # descriptor is unread — clamp like bn_blocks below so it keeps a valid, non-degenerate
168
+ # shape (a 0-block box traps the descriptor-encoding pass).
169
+ rep_k = max((nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4, 1)
170
+ if nargs["ASDescriptor"] is not None:
171
+ nargs["ASDescriptor"].block_shape = [1, nargs["BLOCK_SIZE_M"] // 128, rep_k, 2, 256]
172
+ if nargs["BSDescriptor"] is not None:
173
+ # One bulk-load spans (2 if GATE) * BN//128 blocks: GATE reads the block-interleaved gate|up
174
+ # pair ([g,u] adjacent) as one 2*BN tile. BN<128 (non-gate, non-128 N) reads via the per-row
175
+ # pointer gather instead — clamp the box to one block so the (unread) descriptor keeps a valid,
176
+ # non-degenerate shape (a 0-block box traps the descriptor-encoding pass).
177
+ bn_blocks = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
178
+ nargs["BSDescriptor"].block_shape = [1, bn_blocks, rep_k, 2, 256]
179
+ # Swizzled requant output (Cs is a descriptor): the store tile is [1, 1, rep_n, 2, 256], and
180
+ # rep_n = (BN // SCALE_GROUP_K) // 4 depends on the tuned BLOCK_SIZE_N and the group size — so
181
+ # rebind per config, else nvfp4 (group-16 -> rep_n=2) mismatches the [.,.,1,.,.] build default.
182
+ if nargs["CSDescriptor"] is not None:
183
+ rep_n = (nargs["BLOCK_SIZE_N"] // nargs["SCALE_GROUP_K"]) // 4
184
+ nargs["CSDescriptor"].block_shape = [1, 1, rep_n, 2, 256]
185
+
186
+
187
+ def rebind_dgrad_descriptors(nargs):
188
+ """Per-config pre_hook: set the dgrad boxes to the tuned tile — ``[BM, BN]`` over the (M, N)
189
+ gradient and ``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` over the (N, K_bytes) weight. Both are
190
+ natural contiguous windows in the FORWARD-oriented operands, which is why dgrad can take a
191
+ descriptor at all: a transposed view has no unit-stride innermost dim (see
192
+ ``maybe_descriptor``). Scales stay affine on the pointer arm, as weight-only does."""
193
+ wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
194
+ rebind_operand_box(
195
+ nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], nargs["BLOCK_SIZE_N"]
196
+ )
197
+ rebind_operand_box(
198
+ nargs, "B_MEMORY_MODE", "BDescriptor",
199
+ nargs["BLOCK_SIZE_N"], nargs["BLOCK_SIZE_K"] // wvpb,
200
+ )
201
+
202
+
203
+ def rebind_dgrad_grouped_descriptors(nargs):
204
+ """Grouped dgrad boxes. The activation box is 1 row when the pass gathers (tma gather4 needs a
205
+ 1-row box) and ``[BM, BN]`` otherwise — ScatterIdx is the gather map here, since dgrad reads
206
+ dY at the forward's SCATTER destination. The weight box is ``[1, BN, BK_bytes]`` over
207
+ (E, N, K_bytes): the expert leads, so this descriptor takes THREE offsets."""
208
+ wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
209
+ gathering = nargs.get("ScatterIdx") is not None
210
+ rebind_operand_box(
211
+ nargs, "A_MEMORY_MODE", "ADescriptor",
212
+ 1 if gathering else nargs["BLOCK_SIZE_M"], nargs["BLOCK_SIZE_N"],
213
+ )
214
+ desc = nargs["BDescriptor"]
215
+ if nargs.get("B_MEMORY_MODE", "pointer") != "pointer" and not isinstance(desc, int) and desc is not None:
216
+ desc.block_shape = [1, nargs["BLOCK_SIZE_N"], nargs["BLOCK_SIZE_K"] // wvpb]
build/torch-rocm/epilogue.py ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ import triton
17
+ import triton.language as tl
18
+ from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
19
+
20
+ from .compat import compile_time_only_triton_wrap, decode_pdl, device_context
21
+ from .mma import MMA_N_ATOM
22
+ from .quant import fp8_act_quant_inline, mx_act_quant_inline
23
+ from .loading.scales import apply_global_scale
24
+
25
+
26
+ @triton.jit
27
+ def store_masked(
28
+ C,
29
+ accumulator,
30
+ pid_m,
31
+ pid_n,
32
+ M,
33
+ N,
34
+ stride_c_m,
35
+ stride_c_n,
36
+ BLOCK_SIZE_M: tl.constexpr,
37
+ BLOCK_SIZE_N: tl.constexpr,
38
+ ):
39
+ """Shared output epilogue of the kernels below: cast the fp32 accumulator to
40
+ ``C``'s dtype and store the ``(BLOCK_SIZE_M, BLOCK_SIZE_N)`` tile at the swizzled
41
+ ``(pid_m, pid_n)``, masked to the ``(M, N)`` bounds. (A descriptor-store arm was
42
+ measured an EXACT tie at the store-heavy gap shape and dropped, 2026-07-16 —
43
+ stores are fire-and-forget, TMA has nothing to hide there. B200-only verdict;
44
+ re-measure on H100 or the target device.)"""
45
+ c = accumulator.to(C.dtype.element_ty)
46
+ offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
47
+ offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
48
+ c_ptrs = C + stride_c_m * offs_cm[:, None] + stride_c_n * offs_cn[None, :]
49
+ c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
50
+ tl.store(c_ptrs, c, mask=c_mask)
51
+
52
+
53
+ @triton.jit
54
+ def store_masked_oriented(
55
+ C,
56
+ accumulator,
57
+ pid_m,
58
+ pid_n,
59
+ M,
60
+ N,
61
+ stride_c_m,
62
+ stride_c_n,
63
+ BLOCK_SIZE_M: tl.constexpr,
64
+ BLOCK_SIZE_N: tl.constexpr,
65
+ SWAP_AB: tl.constexpr,
66
+ ):
67
+ """``store_masked`` with the (m, n) roles swapped under ``SWAP_AB``, where the
68
+ accumulator is ``(BLOCK_SIZE_N, BLOCK_SIZE_M)``. Only the taken branch compiles."""
69
+ if SWAP_AB:
70
+ store_masked(
71
+ C,
72
+ accumulator,
73
+ pid_n,
74
+ pid_m,
75
+ N,
76
+ M,
77
+ stride_c_n,
78
+ stride_c_m,
79
+ BLOCK_SIZE_N,
80
+ BLOCK_SIZE_M,
81
+ )
82
+ else:
83
+ store_masked(
84
+ C,
85
+ accumulator,
86
+ pid_m,
87
+ pid_n,
88
+ M,
89
+ N,
90
+ stride_c_m,
91
+ stride_c_n,
92
+ BLOCK_SIZE_M,
93
+ BLOCK_SIZE_N,
94
+ )
95
+
96
+
97
+ @triton.jit
98
+ def acc_init(
99
+ COMPUTE_MODE: tl.constexpr,
100
+ BLOCK_SIZE_M: tl.constexpr,
101
+ BLOCK_SIZE_N: tl.constexpr,
102
+ SWAP_AB: tl.constexpr,
103
+ ):
104
+ """Zero accumulator shaped for the layout: swapped scalar reduces into ``[1, N]``; any other
105
+ swapped mode keeps the ``[N, M]`` MMA acc (weight rows in M, act tile in N — padded up to
106
+ the ``MMA_N_ATOM`` when the caller is the single-token decode GEVM, col 0 taken after the
107
+ K-loop); no-swap uses ``[M, N]``. ``COMPUTE_MODE`` matters only under swap — kernels with no
108
+ mode axis (fp8 ``tl.dot``) pass ``"dot"``. ``N`` is the weight-output tile (``BLOCK_SIZE_H``
109
+ for the fp8 down projection). Single return: only the taken branch compiles."""
110
+ if SWAP_AB and COMPUTE_MODE == "scalar":
111
+ acc = tl.zeros((1, BLOCK_SIZE_N), dtype=tl.float32)
112
+ elif SWAP_AB and BLOCK_SIZE_M < MMA_N_ATOM:
113
+ acc = tl.zeros((BLOCK_SIZE_N, MMA_N_ATOM), dtype=tl.float32)
114
+ elif SWAP_AB:
115
+ acc = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32)
116
+ else:
117
+ acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
118
+ return acc
119
+
120
+
121
+ @triton.jit
122
+ def acc_finalize(
123
+ acc, COMPUTE_MODE: tl.constexpr, ROWS: tl.constexpr, SWAP_AB: tl.constexpr
124
+ ):
125
+ """Bookend to ``acc_init``: when the acc was built as the persistent ``[ROWS, MMA_N_ATOM]`` MMA
126
+ tile (any swapped non-scalar mode), collapse the padded token dim to column 0 → ``[1, ROWS]``.
127
+ Swapped scalar (already ``[1, ROWS]``) and no-swap pass through unchanged. ``COMPUTE_MODE``
128
+ matches ``acc_init``'s (fp8 ``tl.dot`` kernels, which have no mode axis, pass ``"dot"``)."""
129
+ if SWAP_AB and COMPUTE_MODE != "scalar":
130
+ # take column 0: the padded token dim collapses back to the single real token
131
+ acc = tl.reshape(
132
+ tl.sum(acc * (tl.arange(0, MMA_N_ATOM)[None, :] == 0), axis=1), (1, ROWS)
133
+ )
134
+ return acc
135
+
136
+
137
+ def bias_strides(bias: torch.Tensor | None) -> tuple[int, int]:
138
+ """(expert, column) strides for a per-expert output bias, for ``add_bias``. A 1-D bias is the
139
+ single-expert 2D case, so its expert stride is 0. ``None`` gives (0, 0) — the kernel arm folds
140
+ out at trace time and never reads them."""
141
+ if bias is None:
142
+ return 0, 0
143
+ if bias.dim() == 1:
144
+ return 0, bias.stride(0)
145
+ return bias.stride(0), bias.stride(1)
146
+
147
+
148
+ @triton.jit
149
+ def add_bias(acc, Bias, stride_bias_e, stride_bias_n, bias_row, pid_n,
150
+ BLOCK_SIZE_N: tl.constexpr, N_COLS: tl.constexpr = 0,
151
+ SWAP_AB: tl.constexpr = False):
152
+ """Add this expert's slice of a per-expert output bias to an output tile. Gated callers pass
153
+ the doubled extent — an interleaved gate|up tile is an ungated tile of width ``2*BN``.
154
+ ``None`` folds out at trace time.
155
+
156
+ Two shapes reach here. Post-``acc_finalize`` tiles are ``[rows, BN]`` for every orientation
157
+ (``SWAP_AB=False``, what ``gemm_epilogue`` passes) — finalize is what collapses the swapped
158
+ MMA accumulator, so adding before it would broadcast against the wrong axis. The 2D kernels'
159
+ ungated arm stores straight from the accumulator with no finalize, and there ``SWAP_AB``
160
+ leaves it transposed as ``(BN, BM)``, so the bias varies down axis 0 instead."""
161
+ if Bias is not None:
162
+ offs_bias = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
163
+ ptrs = Bias + bias_row * stride_bias_e + offs_bias * stride_bias_n
164
+ if N_COLS > 0: # dense N isn't BN-aligned; the tail columns are off the end of the bias
165
+ bias = tl.load(ptrs, mask=offs_bias < N_COLS, other=0.0)
166
+ else:
167
+ bias = tl.load(ptrs)
168
+ if SWAP_AB:
169
+ out = acc + bias.to(acc.dtype)[:, None]
170
+ else:
171
+ out = acc + bias.to(acc.dtype)[None, :]
172
+ else:
173
+ out = acc
174
+ return out
175
+
176
+
177
+ @triton.jit
178
+ def split_gate_up(flat, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, SWAP_AB: tl.constexpr):
179
+ """De-interleave a finalized ``[rows, 2*BN]`` gate|up tile into the ``(gate, up)`` pair, each
180
+ ``[rows, BN]`` (rows = 1 under swap, else BM). The N axis alternates gate/up per column, so
181
+ this is a trailing-axis split. The caller finalizes (and biases) before calling."""
182
+ rows: tl.constexpr = 1 if SWAP_AB else BLOCK_SIZE_M
183
+ g, u = tl.split(tl.reshape(flat, (rows, BLOCK_SIZE_N, 2)))
184
+ return g, u
185
+
186
+
187
+ @triton.jit
188
+ def glu(
189
+ gate,
190
+ up,
191
+ ACT_FN: tl.constexpr = "silu",
192
+ SWIGLU_ALPHA: tl.constexpr = None,
193
+ SWIGLU_LIMIT: tl.constexpr = None,
194
+ SIMULATE_UNFUSED: tl.constexpr = False,
195
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.float32,
196
+ ):
197
+ """Gated linear unit on the gate/up matmul accumulators. ``SWIGLU_LIMIT`` clamps gate above and up
198
+ to ``[-LIMIT, LIMIT]``; ``SWIGLU_ALPHA`` gives the clamped/scaled SwiGLU ``(up + 1) * gate * sigmoid(ALPHA *
199
+ gate)`` (GPT-OSS / MiniMax), else ``ACT_FN(gate) * up`` (``ACT_FN`` in {silu, gelu, relu}, gelu exact
200
+ via erf). ``SIMULATE_UNFUSED`` rounds each materialized value through ``INTERMEDIATE_DTYPE`` (the dtype the unfused path lands intermediates in) to match the
201
+ unfused (separate-kernel) path, where every intermediate lands in that dtype."""
202
+ g = gate
203
+ u = up
204
+
205
+ if SIMULATE_UNFUSED:
206
+ g = g.to(INTERMEDIATE_DTYPE).to(tl.float32)
207
+ u = u.to(INTERMEDIATE_DTYPE).to(tl.float32)
208
+
209
+ if SWIGLU_LIMIT is not None:
210
+ g = tl.minimum(g, SWIGLU_LIMIT)
211
+ u = tl.minimum(tl.maximum(u, -SWIGLU_LIMIT), SWIGLU_LIMIT)
212
+
213
+ if SWIGLU_ALPHA is not None:
214
+ gate_scaled = g * SWIGLU_ALPHA
215
+ if SIMULATE_UNFUSED:
216
+ gate_scaled = gate_scaled.to(INTERMEDIATE_DTYPE).to(tl.float32)
217
+ sig = tl.sigmoid(gate_scaled)
218
+ if SIMULATE_UNFUSED:
219
+ sig = sig.to(INTERMEDIATE_DTYPE).to(tl.float32)
220
+ act = g * sig
221
+ u = u + 1.0
222
+ elif ACT_FN == "silu":
223
+ sig = tl.sigmoid(g)
224
+ # SIMULATE_UNFUSED must be bit-exact vs the unfused ``apply_glu`` (``g * torch.sigmoid(g)``),
225
+ # where torch.sigmoid returns bf16 — i.e. the sigmoid is rounded before the multiply. Round
226
+ # it here to match (the fp32 sigmoid otherwise flips e4m3 requant bits, ~35% on MXFP4 down).
227
+ if SIMULATE_UNFUSED:
228
+ sig = sig.to(INTERMEDIATE_DTYPE).to(tl.float32)
229
+ act = g * sig
230
+ elif ACT_FN == "gelu":
231
+ if SIMULATE_UNFUSED:
232
+ # Bit-match the unfused ``apply_glu`` gelu ``0.5 * g * (1 + erf(g * c))``, which rounds
233
+ # to bf16 at every torch op (input to erf, erf, 1+erf, 0.5*g, final mul). Rounding only
234
+ # a subset diverges (~0.7 rel) on the MX requant — round each op, like torch does.
235
+ gc = (g * 0.7071067811865476).to(INTERMEDIATE_DTYPE).to(tl.float32)
236
+ e = tl.erf(gc).to(INTERMEDIATE_DTYPE).to(tl.float32)
237
+ one_plus = (1.0 + e).to(INTERMEDIATE_DTYPE).to(tl.float32)
238
+ half_g = (0.5 * g).to(INTERMEDIATE_DTYPE).to(tl.float32)
239
+ act = half_g * one_plus
240
+ else:
241
+ act = 0.5 * g * (1.0 + tl.erf(g * 0.7071067811865476))
242
+ elif ACT_FN == "relu":
243
+ act = tl.maximum(g, 0.0)
244
+ else:
245
+ tl.static_assert(
246
+ False, "unsupported ACT_FN; expected 'silu', 'gelu', or 'relu'"
247
+ )
248
+
249
+ if SIMULATE_UNFUSED:
250
+ act = act.to(INTERMEDIATE_DTYPE).to(tl.float32)
251
+ u = u.to(INTERMEDIATE_DTYPE).to(tl.float32)
252
+
253
+ gated = act * u
254
+
255
+ if SIMULATE_UNFUSED:
256
+ gated = gated.to(INTERMEDIATE_DTYPE).to(tl.float32)
257
+
258
+ return gated
259
+
260
+
261
+ @triton.jit
262
+ def _glu_kernel(
263
+ GateUp, # (S, 2*INTER) interleaved [g0,u0,g1,u1,...] GEMM output; fp32 = exact accumulators
264
+ Out, # (S, INTER) activation-dtype GLU result; the E4M3 tensor under the requant arm
265
+ QuantScales, # block-FP8 requant arm (``fused_glu(quant_group=...)``): per-``QUANT_GROUP``
266
+ # scales (fp32, or UE8M0 exponent bytes); ``None`` folds the arm out — the GLU
267
+ # result then rounds through bf16 (the reference two-kernel order) and quantizes
268
+ n_out,
269
+ INTER: tl.constexpr,
270
+ ACT_FN: tl.constexpr,
271
+ SWIGLU_ALPHA: tl.constexpr,
272
+ SWIGLU_LIMIT: tl.constexpr,
273
+ BLOCK: tl.constexpr,
274
+ QUANT_GROUP: tl.constexpr = 128,
275
+ UE8M0: tl.constexpr = False,
276
+ PDL: tl.constexpr = False,
277
+ ):
278
+ """Fused GLU over a contiguous (S, 2*INTER) interleaved gate|up tensor -> (S, INTER), covering every
279
+ ``apply_glu`` variant (silu/gelu/relu, ``SWIGLU_ALPHA``/``SWIGLU_LIMIT`` clamped-SwiGLU
280
+ — ``None`` folds the arm out, the in-kernel ``glu``'s convention). Rounds through the
281
+ output dtype after each op exactly where the torch chain does (each torch tensor op
282
+ materializes), so the two are bit-identical."""
283
+ if PDL:
284
+ gdc_wait()
285
+ offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
286
+ mask = offs < n_out
287
+ # gate|up columns alternate (gate 2j, up 2j+1) — the interleaved row order the GEMM emits
288
+ base = (offs // INTER) * 2 * INTER + 2 * (offs % INTER)
289
+ # fp32 input = exact GEMM accumulators (the unstacked fused-order path): compute the
290
+ # GLU in fp32 and round ONCE at the store — the gated epilogue's math. Narrow inputs
291
+ # keep the torch-chain per-op rounding (bit-identical to apply_glu).
292
+ FUSED_ORDER: tl.constexpr = GateUp.dtype.element_ty == tl.float32
293
+ dt = tl.float32 if FUSED_ORDER else (
294
+ tl.bfloat16 if QuantScales is not None else Out.dtype.element_ty)
295
+ g = tl.load(GateUp + base, mask=mask).to(tl.float32)
296
+ u = tl.load(GateUp + base + 1, mask=mask).to(tl.float32)
297
+ if SWIGLU_LIMIT is not None:
298
+ g = tl.minimum(g, SWIGLU_LIMIT)
299
+ u = tl.clamp(u, -SWIGLU_LIMIT, SWIGLU_LIMIT)
300
+ if SWIGLU_ALPHA is not None: # (up + 1) * gate * sigmoid(alpha * gate)
301
+ sig = tl.sigmoid((g * SWIGLU_ALPHA).to(dt).to(tl.float32)).to(dt).to(tl.float32)
302
+ act = (g * sig).to(dt).to(tl.float32)
303
+ out = ((u + 1.0).to(dt).to(tl.float32) * act).to(dt)
304
+ elif ACT_FN == "silu":
305
+ sig = tl.sigmoid(g).to(dt).to(tl.float32)
306
+ act = (g * sig).to(dt).to(tl.float32)
307
+ out = (act * u).to(dt)
308
+ elif ACT_FN == "gelu": # exact via erf, torch's evaluation order
309
+ e = tl.erf((g * 0.7071067811865476).to(dt).to(tl.float32)).to(dt).to(tl.float32)
310
+ d = (1.0 + e).to(dt).to(tl.float32)
311
+ a = (0.5 * g).to(dt).to(tl.float32)
312
+ out = ((a * d).to(dt).to(tl.float32) * u).to(dt)
313
+ else: # relu
314
+ out = (tl.maximum(g, 0.0) * u).to(dt)
315
+ if QuantScales is None:
316
+ tl.store(Out + offs, out.to(Out.dtype.element_ty), mask=mask)
317
+ else:
318
+ grp = tl.reshape(out.to(tl.float32), (BLOCK // QUANT_GROUP, QUANT_GROUP))
319
+ amax = tl.max(tl.abs(grp), axis=1)
320
+ if UE8M0:
321
+ sc = tl.exp2(tl.ceil(tl.log2(tl.maximum(amax, 1e-10) / 448.0)))
322
+ else:
323
+ sc = tl.maximum(amax, 1e-10) / 448.0
324
+ q = tl.clamp(grp / sc[:, None], -448.0, 448.0)
325
+ tl.store(Out + offs, tl.reshape(q, (BLOCK,)).to(tl.float8e4nv), mask=mask)
326
+ soffs = tl.program_id(0) * (BLOCK // QUANT_GROUP) + tl.arange(0, BLOCK // QUANT_GROUP)
327
+ smask = soffs * QUANT_GROUP < n_out
328
+ if UE8M0:
329
+ tl.store(QuantScales + soffs, (tl.log2(sc) + 127.0).to(tl.uint8), mask=smask)
330
+ else:
331
+ if PDL:
332
+ gdc_launch_dependents()
333
+ tl.store(QuantScales + soffs, sc, mask=smask)
334
+
335
+
336
+ def fused_glu(
337
+ gate_up: torch.Tensor,
338
+ act_fn: str = "silu",
339
+ swiglu_alpha: float | None = None,
340
+ swiglu_limit: float | None = None,
341
+ quant_group: int | None = None,
342
+ use_ue8m0: bool = False,
343
+ out_dtype: torch.dtype | None = None,
344
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
345
+ """GLU over the interleaved (S, 2*INTER) gate|up GEMM output — the one-kernel ``_glu_kernel``
346
+ on a contiguous tensor, the torch ``apply_glu`` on any fallback layout; bit-identical
347
+ either way. ``quant_group`` additionally block-FP8-requantizes the result per group in
348
+ the same kernel (the unstacked decode band's handoff to the down projection) and
349
+ returns ``(q, scales)`` — scales fp32, or UE8M0 exponent bytes under ``use_ue8m0``;
350
+ non-SiLU/clamped variants take the two-step order (same bits — the fused arm rounds
351
+ through bf16 first for exactly this equivalence)."""
352
+ inter_dim = gate_up.shape[-1] // 2
353
+ if quant_group is not None:
354
+ if act_fn == "silu" and swiglu_alpha is None and swiglu_limit is None and gate_up.is_contiguous():
355
+ S = gate_up.shape[0]
356
+ q = torch.empty(S, inter_dim, dtype=torch.float8_e4m3fn, device=gate_up.device)
357
+ sc = torch.empty(S, inter_dim // quant_group,
358
+ dtype=torch.uint8 if use_ue8m0 else torch.float32,
359
+ device=gate_up.device)
360
+ n = q.numel()
361
+ with device_context(gate_up.device):
362
+ compile_time_only_triton_wrap(_glu_kernel)[(triton.cdiv(n, 1024),)](
363
+ gate_up, q, sc, n, inter_dim, "silu", None, None, BLOCK=1024,
364
+ QUANT_GROUP=quant_group, UE8M0=use_ue8m0,
365
+ PDL=decode_pdl(),
366
+ launch_pdl=decode_pdl(),
367
+ )
368
+ return q, sc
369
+ from .quant import fp8_act_quant_block_dynamic # deferred: module import order
370
+
371
+ inter = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit)
372
+ return fp8_act_quant_block_dynamic(inter, quant_group, use_ue8m0=use_ue8m0)
373
+ if gate_up.is_contiguous() and act_fn in ("silu", "gelu", "relu"):
374
+ out = torch.empty(*gate_up.shape[:-1], inter_dim,
375
+ dtype=out_dtype or gate_up.dtype, device=gate_up.device)
376
+ n = out.numel()
377
+ with device_context(gate_up.device):
378
+ compile_time_only_triton_wrap(_glu_kernel)[(triton.cdiv(n, 1024),)](
379
+ gate_up, out, None, n, inter_dim, act_fn, swiglu_alpha, swiglu_limit, BLOCK=1024,
380
+ PDL=decode_pdl(),
381
+ launch_pdl=decode_pdl(),
382
+ )
383
+ return out
384
+ gate, up = gate_up[..., 0::2], gate_up[..., 1::2]
385
+ return apply_glu(gate, up, act_fn, swiglu_alpha, swiglu_limit)
386
+
387
+
388
+ def apply_glu(
389
+ gate: torch.Tensor,
390
+ up: torch.Tensor,
391
+ act_fn: str = "silu",
392
+ swiglu_alpha: float | None = None,
393
+ swiglu_limit: float | None = None,
394
+ ) -> torch.Tensor:
395
+ """Host-side (torch) gated linear unit — the unfused path's activation, mirroring the triton
396
+ ``glu``. ``swiglu_limit`` clamps gate above / up to ``[-limit, limit]``; ``swiglu_alpha`` gives
397
+ the clamped/scaled SwiGLU ``(up + 1) * gate * sigmoid(alpha * gate)`` (GPT-OSS / MiniMax), else
398
+ ``act_fn(gate) * up`` (``act_fn`` in {silu, gelu, relu}, gelu exact via erf)."""
399
+ if swiglu_limit is not None:
400
+ gate = gate.clamp(max=swiglu_limit)
401
+ up = up.clamp(min=-swiglu_limit, max=swiglu_limit)
402
+ if swiglu_alpha is not None:
403
+ return (up + 1.0) * (gate * torch.sigmoid(gate * swiglu_alpha))
404
+ if act_fn == "silu":
405
+ act = gate * torch.sigmoid(gate)
406
+ elif act_fn == "gelu":
407
+ act = 0.5 * gate * (1.0 + torch.erf(gate * 0.7071067811865476))
408
+ elif act_fn == "relu":
409
+ act = gate.clamp(min=0.0)
410
+ else:
411
+ raise ValueError(
412
+ f"unsupported act_fn {act_fn!r}; expected 'silu', 'gelu', or 'relu'"
413
+ )
414
+ return act * up
415
+
416
+
417
+ @triton.jit
418
+ def split_gate_up_glu(
419
+ flat,
420
+ BLOCK_SIZE_M: tl.constexpr,
421
+ BLOCK_SIZE_N: tl.constexpr,
422
+ SWAP_AB: tl.constexpr,
423
+ ACT_FN: tl.constexpr = "silu",
424
+ SWIGLU_ALPHA: tl.constexpr = None,
425
+ SWIGLU_LIMIT: tl.constexpr = None,
426
+ SIMULATE_UNFUSED: tl.constexpr = False,
427
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.float32,
428
+ ):
429
+ """Gate|up epilogue in one step: split the finalized stacked tile into its (gate, up) pair
430
+ (``split_gate_up``) and apply the ``ACT_FN``/SwiGLU gated linear unit (``glu``), returning
431
+ the combined intermediate. See those two for the orientation and activation details."""
432
+ gate, up = split_gate_up(flat, BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB)
433
+ return glu(
434
+ gate,
435
+ up,
436
+ ACT_FN,
437
+ SWIGLU_ALPHA,
438
+ SWIGLU_LIMIT,
439
+ SIMULATE_UNFUSED,
440
+ INTERMEDIATE_DTYPE,
441
+ )
442
+
443
+
444
+ @triton.jit
445
+ def _store_out(
446
+ C, acc, out_row, pid_n, row_mask, stride_c_m, stride_c_n,
447
+ BLOCK_SIZE_M: tl.constexpr, WIDTH: tl.constexpr, FAKE_BATCH: tl.constexpr,
448
+ N_COLS: tl.constexpr = 0,
449
+ ):
450
+ """Cast + store one output tile of N-width ``WIDTH`` (halved when the format packs nibble
451
+ pairs). ``FAKE_BATCH`` (batched decode): the BM lanes alias one C row (``C`` pre-advanced), so
452
+ a plain store would duplicate-write the same bytes (hardware-undefined on Intel XPU) — mask to
453
+ lane 0 (the replicated rows are identical). Else a real scatter to global rows ``out_row`` under
454
+ ``row_mask``. ``N_COLS`` > 0 also masks the column tail (the 2D dense output isn't ``BN``-aligned
455
+ like the ``N % BN == 0`` grouped/batched MoE outputs); 0 skips it. Single return."""
456
+ c = acc.to(C.dtype.element_ty)
457
+ offs_cm = tl.arange(0, BLOCK_SIZE_M)
458
+ offs_cn = pid_n * WIDTH + tl.arange(0, WIDTH)
459
+ col_ok = (offs_cn < N_COLS) if N_COLS > 0 else (offs_cn >= 0)
460
+ if FAKE_BATCH:
461
+ c_ptrs = C + offs_cm[:, None] * 0 + stride_c_n * offs_cn[None, :]
462
+ tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None] & col_ok[None, :])
463
+ else:
464
+ c_ptrs = C + stride_c_m * out_row[:, None] + stride_c_n * offs_cn[None, :]
465
+ tl.store(c_ptrs, c, mask=row_mask[:, None] & col_ok[None, :])
466
+
467
+
468
+ @triton.jit
469
+ def _epilogue_requant_fp8(
470
+ C, Cs, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
471
+ BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, FAKE_BATCH: tl.constexpr,
472
+ N_COLS: tl.constexpr,
473
+ ):
474
+ """Requantize the SwiGLU intermediate to fp8 with a per-(row, N-tile) scalar scale. UE8M0
475
+ intermediate scales under a UE8M0 model (inferred from Cs's dtype) keep the down proj's activation
476
+ scales power-of-two so its dot_scaled arm fires. FAKE_BATCH collapses the replicated rows to one
477
+ scalar per (row, N-tile)."""
478
+ q, q_s = fp8_act_quant_inline(out, UE8M0=Cs.dtype.element_ty == tl.uint8)
479
+ _store_out(C, q, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
480
+ cs_ptr = Cs + out_row * stride_cs_m + pid_n * stride_cs_n
481
+ if FAKE_BATCH: # replicated rows -> one scalar per (row, N-tile)
482
+ tl.store(cs_ptr, tl.max(q_s))
483
+ else:
484
+ tl.store(cs_ptr, q_s, mask=row_mask)
485
+
486
+
487
+ @triton.jit
488
+ def _epilogue_requant_mx(
489
+ C, Cs, out, out_row, pid_n, pid_m, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
490
+ CSDescriptor, CsGlobal, cs_global_row,
491
+ BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
492
+ OUTPUT_FORMAT: tl.constexpr, SWIZZLED_OUT: tl.constexpr, FAKE_BATCH: tl.constexpr,
493
+ N_COLS: tl.constexpr,
494
+ ):
495
+ """Requantize the SwiGLU intermediate to MX group-``SCALE_GROUP_K`` (mxfp8 UE8M0 / mxfp4 / nvfp4
496
+ E4M3 — the fp4 formats pack nibble pairs so ``C`` halves). ``SWIZZLED_OUT`` writes ``Cs`` straight
497
+ into the down proj's SWIZZLE_32_4_4 descriptor at block ``(pid_m, pid_n)`` (BM/BN pinned 128), else
498
+ a row-major affine store. NVFP4 two-level: normalize by the next proj's provided input_scale
499
+ (``CsGlobal[cs_global_row]`` — per tensor or this expert's) before the block quant. FAKE_BATCH
500
+ collapses replicated rows via the row-max."""
501
+ if CsGlobal is not None:
502
+ # NVFP4 two-level requant: normalize the fp32 GLU intermediate by the NEXT proj's provided
503
+ # (calibrated) input_scale before the block quant — the canonical two-step. The down folds it
504
+ # back via its As pair ([Cs, g_out]); nothing is computed at runtime.
505
+ out = out / tl.load(CsGlobal + cs_global_row).to(tl.float32)
506
+ q, q_s = mx_act_quant_inline(out, BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K, OUTPUT_FORMAT)
507
+ width: tl.constexpr = BLOCK_SIZE_N if OUTPUT_FORMAT == "mxfp8" else BLOCK_SIZE_N // 2
508
+ _store_out(C, q, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, width, FAKE_BATCH,
509
+ N_COLS if OUTPUT_FORMAT == "mxfp8" else N_COLS // 2)
510
+ if SWIZZLED_OUT:
511
+ # group scales straight into the down proj's SWIZZLE_32_4_4 layout (inverse of
512
+ # load_swizzled_scale) at block (pid_m, pid_n) — BM/BN pinned 128.
513
+ REP_K_CS: tl.constexpr = (BLOCK_SIZE_N // SCALE_GROUP_K) // 4
514
+ sw = (
515
+ q_s.reshape(1, 4, 32, REP_K_CS, 4)
516
+ .trans(0, 3, 2, 1, 4)
517
+ .reshape(1, 1, REP_K_CS, 2, 256)
518
+ )
519
+ CSDescriptor.store([0, pid_m, pid_n * REP_K_CS, 0, 0], sw)
520
+ else:
521
+ offs_sc = pid_n * (BLOCK_SIZE_N // SCALE_GROUP_K) + tl.arange(0, BLOCK_SIZE_N // SCALE_GROUP_K)
522
+ if FAKE_BATCH:
523
+ # replicated rows -> the row-max IS the row's scale (f32-exact for UE8M0 exponent bytes
524
+ # and E4M3 values alike)
525
+ tl.store(
526
+ Cs + out_row * stride_cs_m + offs_sc[None, :] * stride_cs_n,
527
+ tl.reshape(tl.max(q_s.to(tl.float32), axis=0), (1, BLOCK_SIZE_N // SCALE_GROUP_K)),
528
+ )
529
+ else:
530
+ tl.store(
531
+ Cs + out_row[:, None] * stride_cs_m + offs_sc[None, :] * stride_cs_n,
532
+ q_s,
533
+ mask=row_mask[:, None],
534
+ )
535
+
536
+
537
+ @triton.jit
538
+ def gemm_epilogue(
539
+ C,
540
+ Cs, # row-major requant-scale pointer (None under SWIZZLED_OUT — CSDescriptor writes instead)
541
+ acc,
542
+ out_row,
543
+ pid_n,
544
+ pid_m,
545
+ row_mask,
546
+ stride_c_m,
547
+ stride_c_n,
548
+ stride_cs_m,
549
+ stride_cs_n,
550
+ BLOCK_SIZE_M: tl.constexpr,
551
+ BLOCK_SIZE_N: tl.constexpr,
552
+ GATE: tl.constexpr,
553
+ OUTPUT_FORMAT: tl.constexpr,
554
+ SCALE_GROUP_K: tl.constexpr,
555
+ ACT_FN: tl.constexpr,
556
+ SWIGLU_ALPHA: tl.constexpr,
557
+ SWIGLU_LIMIT: tl.constexpr,
558
+ SIMULATE_UNFUSED: tl.constexpr,
559
+ INTERMEDIATE_DTYPE: tl.constexpr,
560
+ COMPUTE_MODE: tl.constexpr = "dot",
561
+ SWAP_AB: tl.constexpr = False,
562
+ SWIZZLED_OUT: tl.constexpr = False,
563
+ FAKE_BATCH: tl.constexpr = False,
564
+ N_COLS: tl.constexpr = 0, # >0 masks the column tail (2D dense N isn't BN-aligned); 0 = no mask
565
+ CSDescriptor=0, # SWIZZLE_32_4_4 requant-scale descriptor; read only under SWIZZLED_OUT (else dummy)
566
+ CsGlobal=None, # fp32 NVFP4 output global (the NEXT proj's provided input_scale), per tensor or per expert; normalizes the requant, None folds out
567
+ stride_cs_global=0, # expert stride of CsGlobal (0 broadcasts a scalar)
568
+ GlobalScale=None, # fp32 NVFP4 weight global g_b, per tensor or per expert; applied PRE-GLU, None folds out
569
+ stride_global_e=0, # expert stride of GlobalScale (global_scale_stride; 0 broadcasts a scalar)
570
+ global_row=0, # expert id indexing the globals and the bias (0 for the 2D case)
571
+ GlobalScaleA=None, # fp32 NVFP4 activation global g_a, per tensor or per expert; multiplied in-register with GlobalScale
572
+ stride_global_a=0, # expert stride of GlobalScaleA (0 broadcasts a scalar)
573
+ PreAct=None, # (M, 2N) pre-activation buffer; written iff not None — the GLU backward's Z
574
+ stride_pa_m=0,
575
+ stride_pa_n=0,
576
+ Bias=None, # (E, N_out) per-expert output bias, N_out = 2N under GATE; row is `global_row`
577
+ stride_bias_e=0,
578
+ stride_bias_n=0,
579
+ ):
580
+ """Unified output epilogue for grouped (a real scatter tile) and batched (fake-batch decode:
581
+ one token replicated across the BM lanes) GEMMs. Plain: cast + store the accumulator. ``GATE``:
582
+ split the stacked gate|up accumulator + SwiGLU (``split_gate_up_glu``); ``OUTPUT_FORMAT`` — the
583
+ ``activation_format`` vocabulary — then requantizes into ``C`` + ``Cs``: ``"fp8"`` (per-(row, N-tile)
584
+ scalar), or MX group-``SCALE_GROUP_K`` (UE8M0/E4M3 — the fp4 formats pack nibble pairs so ``C``
585
+ halves), with ``SWIZZLED_OUT`` writing the MX ``Cs`` straight into the down proj's SWIZZLE_32_4_4
586
+ descriptor at block ``(pid_m, pid_n)`` (the tcgen05 fast path, BM/BN pinned 128). ``FAKE_BATCH``
587
+ shims the store: value masks to lane 0 (``C`` pre-advanced), the scale collapses the replicated
588
+ rows with ``tl.max``; else a real BM-row scatter (``out_row`` + ``row_mask``).
589
+ ``COMPUTE_MODE``/``SWAP_AB`` orient the decode GLU/finalize (grouped passes ``"dot"``/no-swap,
590
+ both no-ops there). Every arm is constexpr-pruned."""
591
+ # The globals fold on the FINALIZED tile: a swapped accumulator still carries the token dim
592
+ # padded to the MMA atom, so folding before the collapse costs 16x the multiplies.
593
+ if GATE:
594
+ # Finalize + bias once, up here rather than inside the split: the gate|up bias belongs to
595
+ # the pre-activation, so PreAct below has to see it too.
596
+ flat = add_bias(
597
+ apply_global_scale(
598
+ acc_finalize(acc, COMPUTE_MODE, 2 * BLOCK_SIZE_N, SWAP_AB),
599
+ GlobalScale, stride_global_e, GlobalScaleA, stride_global_a, global_row,
600
+ ),
601
+ Bias, stride_bias_e, stride_bias_n, global_row, pid_n,
602
+ 2 * BLOCK_SIZE_N, 2 * N_COLS if N_COLS > 0 else 0,
603
+ )
604
+ # SAVE_PREACT: the GLU backward needs Z, the 2*BN-wide pre-activation. Interleaved, that
605
+ # tile IS an ungated output tile at doubled width (pid_n * 2BN == 2 * pid_n * BN), so the
606
+ # ordinary store serves it — no separate path, no gated store arm.
607
+ if PreAct is not None:
608
+ _store_out(
609
+ PreAct, flat,
610
+ out_row, pid_n, row_mask, stride_pa_m, stride_pa_n,
611
+ BLOCK_SIZE_M, 2 * BLOCK_SIZE_N, FAKE_BATCH,
612
+ 2 * N_COLS if N_COLS > 0 else 0,
613
+ )
614
+ out = split_gate_up_glu(
615
+ flat, BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB,
616
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
617
+ )
618
+ if OUTPUT_FORMAT == "fp8":
619
+ _epilogue_requant_fp8(
620
+ C, Cs, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
621
+ BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS,
622
+ )
623
+ elif OUTPUT_FORMAT is not None: # "mxfp8" | "mxfp4" | "nvfp4"
624
+ _epilogue_requant_mx(
625
+ C, Cs, out, out_row, pid_n, pid_m, row_mask, stride_c_m, stride_c_n, stride_cs_m,
626
+ stride_cs_n, CSDescriptor, CsGlobal, global_row * stride_cs_global,
627
+ BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K, OUTPUT_FORMAT, SWIZZLED_OUT, FAKE_BATCH, N_COLS,
628
+ )
629
+ else: # bf16 (unquantized) SwiGLU output
630
+ _store_out(C, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
631
+ else: # plain GEMM: cast + store the accumulator (no fused requant on the non-gate path)
632
+ acc = add_bias(
633
+ apply_global_scale(
634
+ acc_finalize(acc, COMPUTE_MODE, BLOCK_SIZE_N, SWAP_AB),
635
+ GlobalScale, stride_global_e, GlobalScaleA, stride_global_a, global_row,
636
+ ),
637
+ Bias, stride_bias_e, stride_bias_n, global_row, pid_n, BLOCK_SIZE_N, N_COLS,
638
+ )
639
+ _store_out(C, acc, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
build/torch-rocm/finegrained_kernels/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch-rocm/formats.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ import triton
17
+
18
+ from .compat import MX_SCALE_GROUP_K, NIBBLES_PER_BYTE, NVFP4_SCALE_GROUP_K
19
+
20
+ def ue8m0_as_uint8(scale: torch.Tensor | None) -> torch.Tensor | None:
21
+ """View UE8M0 (``float8_e8m0fnu``) weight scales as ``uint8`` for the Triton
22
+ binder, which doesn't recognize the dtype; kernels decode ``2^(exp-127)``
23
+ inline. fp32 (non-UE8M0) scales pass through unchanged; ``None`` (an absent
24
+ optional scale) passes through as ``None`` — kernels take the arg as None."""
25
+ if scale is None:
26
+ return None
27
+ return scale.view(torch.uint8) if scale.dtype == torch.float8_e8m0fnu else scale
28
+
29
+ def e2m1_as_uint8(weight: torch.Tensor) -> torch.Tensor:
30
+ """View an ``int8``-stored MXFP4 (packed E2M1) weight as ``uint8`` — a zero-cost
31
+ reinterpret. ``tl.dot_scaled`` requires the packed rhs as ``uint8``, so do it once here
32
+ instead of casting in-kernel at every load. E4M3 (MXFP8) weights pass through unchanged."""
33
+ return weight.view(torch.uint8) if weight.dtype == torch.int8 else weight
34
+
35
+ # UE8M0 group-32 scales arrive either as ``float8_e8m0fnu`` or as raw ``uint8`` — the same 8
36
+ # exponent bits, and a common on-disk encoding (e.g. group-32 "mxfp8" checkpoints store the
37
+ # scale tensor as uint8). Both are valid MX scales: ``ue8m0_as_uint8`` reinterprets to uint8
38
+ # and the kernels decode ``2^(exp-127)`` inline, so the detectors accept either dtype.
39
+ UE8M0_SCALE_DTYPES = (torch.float8_e8m0fnu, torch.uint8)
40
+
41
+ def _shapes_match(weight: torch.Tensor, scale: torch.Tensor, group: int) -> bool:
42
+ """Shape leg of the family predicates: matching leading dims and a last dim of
43
+ one scale per ``group`` unpacked values. Early-return ``if``s, not an ``and``
44
+ chain: callers compare predicate results (``is_x(gate) != is_x(down)``), and an
45
+ ``and`` chain hands them a lazy SymBool under dynamo — the resulting nested
46
+ symbolic Eq crashes ``evaluate_expr``. Control flow forces each comparison to a
47
+ real bool (weight shapes are static parameters, so the guards are correct)."""
48
+ packed = NIBBLES_PER_BYTE if weight.dtype == torch.int8 else 1
49
+ if scale.shape[:-1] != weight.shape[:-1]:
50
+ return False
51
+ if scale.shape[-1] != (weight.shape[-1] * packed) // group:
52
+ return False
53
+ return True
54
+
55
+ def is_mxfp8(weight: torch.Tensor, scale: torch.Tensor) -> bool:
56
+ """MXFP8 weight/scale pair: E4M3 weights with UE8M0 group-32 scales — last dim
57
+ ``scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K``, matching leading dims.
58
+ Works for 2D ``(N, K)`` and 3D ``(E, N, K)`` weights. The group-32 layout is what
59
+ separates MXFP8 from 128-block FP8 (which may also carry UE8M0 scales)."""
60
+ return (
61
+ weight.dtype == torch.float8_e4m3fn
62
+ and scale.dtype in UE8M0_SCALE_DTYPES
63
+ and _shapes_match(weight, scale, MX_SCALE_GROUP_K)
64
+ )
65
+
66
+ def is_mxfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
67
+ """MXFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with
68
+ UE8M0 group-32 scales — ``scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE //
69
+ MX_SCALE_GROUP_K`` (unpacked K = ``2 * K_half``), matching leading dims. 2D or 3D."""
70
+ return (
71
+ weight.dtype == torch.int8
72
+ and scale.dtype in UE8M0_SCALE_DTYPES
73
+ and _shapes_match(weight, scale, MX_SCALE_GROUP_K)
74
+ )
75
+
76
+ def is_nvfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
77
+ """NVFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with E4M3
78
+ group-16 block scales — the scale DTYPE is the format carrier (UE8M0 = MX, E4M3 = NV), and
79
+ the group falls out of the shape. This predicate reads the block scale; the per-tensor
80
+ second-level global is a separate ``b_global_scale`` argument (``nvfp4_quantize_two_level``).
81
+ Layout-agnostic questions ("is this NVFP4, swizzled or not?") go through ``weight_format``,
82
+ which carries the same dtype rule without the shape check."""
83
+ return (
84
+ weight.dtype == torch.int8
85
+ and scale.dtype == torch.float8_e4m3fn
86
+ and _shapes_match(weight, scale, NVFP4_SCALE_GROUP_K)
87
+ )
88
+
89
+ def is_preswizzled_mx(weight: torch.Tensor, scale: torch.Tensor) -> bool:
90
+ """A weight scale already in the SWIZZLE_32_4_4 layout, swizzled once at model load (the
91
+ deployment contract — the same checkpoint feeds grouped prefill and batched decode with no
92
+ per-call rearrange). The 5D shape ``(G, row_blocks, cols//4, 2, 256)`` is the marker; the scale is
93
+ a 1-byte block scale (UE8M0 for MXFP8/MXFP4, E4M3 for NVFP4) against an MX weight (E4M3 or
94
+ packed E2M1). Format-agnostic — NVFP4 pre-swizzles the same way (the layout cuBLAS wants).
95
+ 6D is the gate|up artifact: the same blocks with the gate-interleave carried in the shape."""
96
+ return (
97
+ weight.dtype in (torch.float8_e4m3fn, torch.int8)
98
+ and scale.ndim in (5, 6)
99
+ and scale.element_size() == 1
100
+ )
101
+
102
+ def is_mx(weight: torch.Tensor, scale: torch.Tensor) -> bool:
103
+ """Any microscaled weight/scale pair — MXFP8 (``float8_e4m3fn`` values), MXFP4
104
+ (``int8``, two E2M1 codes/byte), both UE8M0 group-32, or NVFP4 (packed E2M1 + E4M3
105
+ group-16); also an already-swizzled MX scale (``is_preswizzled_mx``). The dispatchers route
106
+ on this into the ``mx_*`` kernels; the op picks the format from the dtypes."""
107
+ return (
108
+ is_mxfp8(weight, scale)
109
+ or is_mxfp4(weight, scale)
110
+ or is_nvfp4(weight, scale)
111
+ or is_preswizzled_mx(weight, scale)
112
+ )
113
+
114
+ def is_tensor_wide(block_size, weight: torch.Tensor) -> bool:
115
+ """True when ``block_size`` selects per-tensor (tensor-dynamic) scaling: ``None`` or
116
+ equal to the weight's full ``(N, K)`` — one scale block spanning the whole matrix.
117
+ Handles 2D ``(N, K)`` and 3D ``(E, N, K)`` weights via the last two dims. (2D path
118
+ only — the grouped/batched dispatchers derive the format from the SCALE shape via
119
+ ``weight_block_size``.)"""
120
+ return block_size is None or (
121
+ block_size[0] == weight.shape[-2] and block_size[1] == weight.shape[-1]
122
+ )
123
+
124
+ def weight_block_size(B: torch.Tensor, Bs: torch.Tensor) -> list[int] | None:
125
+ """The fp8 weight-quantization block ``[block_n, block_k]``, derived from the scale
126
+ tensor's shape — the data already says how it was quantized, so no ``block_size``
127
+ parameter exists to disagree with it. ``None`` = tensor-wide (one scalar per expert:
128
+ ``Bs`` ``(E,)`` or ``(E, 1, 1)`` spanning the full ``(N, K)``). Expects a 3D
129
+ ``(E, N, K)`` fp8 ``B``; MX weights never reach here (format keyed by scale dtype)."""
130
+ num_experts, n_rows, K = B.shape
131
+ if Bs.numel() == num_experts:
132
+ return None
133
+ assert Bs.ndim == 3 and K % Bs.shape[2] == 0, (
134
+ f"Bs shape {tuple(Bs.shape)} does not tile B {tuple(B.shape)} along K"
135
+ )
136
+ # K tiles evenly, so block_k is exact; N (n_rows) may be non-aligned (a partial last block,
137
+ # n_rows % Bs.shape[1] != 0) — recover block_n from the even K dim (square FP8 blocks). The
138
+ # routed kernels then reject the non-aligned N via ``require_moe_dims_aligned``; matmul_2d masks it.
139
+ block_k = K // Bs.shape[2]
140
+ block_n = n_rows // Bs.shape[1] if n_rows % Bs.shape[1] == 0 else block_k
141
+ return [block_n, block_k]
142
+
143
+ def validate_dense_operands(A: torch.Tensor, B: torch.Tensor) -> None:
144
+ """Shared (rows, K) x (num_experts, N, K) operand checks for the unpacked formats —
145
+ the packed-E2M1 ops do their own (K spans two values per stored byte)."""
146
+ assert A.ndim == 2, f"A must be 2D (rows, K), got ndim={A.ndim}"
147
+ assert A.is_contiguous(), "A must be contiguous"
148
+ assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
149
+ assert B.is_contiguous(), "B must be contiguous"
150
+ assert A.shape[1] == B.shape[2], (
151
+ f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
152
+ )
153
+
154
+ def validate_dense_2d_operands(A: torch.Tensor, B: torch.Tensor) -> None:
155
+ """Shared (rows, K) x (N, K) operand checks for the 2D dense unpacked-fp8 wrappers — matching
156
+ K, contiguous A, 2D B. The packed-E2M1 (MX) 2D op does its own (K is two values per
157
+ stored byte, so B is (N, K // 2)).
158
+
159
+ B may be row-major or a transposed view of a row-major tensor. The kernels read both of B's
160
+ strides, and ``descriptor_box_pruner`` keeps the TMA arms — which require a unit-stride
161
+ innermost dim — off the transposed form, so a caller can contract over the other axis by
162
+ passing ``B.t()`` (with its scale grid transposed to match) instead of materializing a copy."""
163
+ assert A.shape[-1] == B.shape[-1], (
164
+ f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
165
+ )
166
+ assert A.is_contiguous(), "A must be contiguous"
167
+ assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
168
+ assert B.is_contiguous() or B.t().is_contiguous(), (
169
+ "B must be contiguous or a transposed view of a contiguous tensor, got strides "
170
+ f"{tuple(B.stride())} for shape {tuple(B.shape)}"
171
+ )
172
+
173
+ def expert_weight_shape(B: torch.Tensor, gate: bool) -> tuple[int, int, int]:
174
+ """(num_experts, n_rows, N) of an expert weight stack — under a gate epilogue B is
175
+ the (E, 2N, K) gate|up stack, so the per-projection width N is half the stored rows."""
176
+ num_experts, n_rows, _ = B.shape
177
+ return num_experts, n_rows, (n_rows // 2 if gate else n_rows)
178
+
179
+ def routed_rows(A, gather_idx, scatter_idx, expert_start, num_experts) -> int:
180
+ """S (routed rows) for a grouped launch, carried by the (S,) maps — A's rows are
181
+ gather SOURCES and under-count S whenever top_k > 1 (gate_up reading raw hidden);
182
+ only with no maps at all is A itself the expert-sorted (S, K) matrix. Validates the
183
+ maps and the ``(next_power_of_2(E) + 1,)`` ``expert_start`` schedule."""
184
+ if gather_idx is not None:
185
+ S = gather_idx.numel()
186
+ elif scatter_idx is not None:
187
+ S = scatter_idx.numel()
188
+ else:
189
+ S = A.shape[0]
190
+ for perm_map in (gather_idx, scatter_idx):
191
+ assert perm_map is None or (perm_map.numel() == S and perm_map.is_contiguous())
192
+ assert (
193
+ expert_start.is_contiguous()
194
+ and expert_start.numel() == triton.next_power_of_2(num_experts) + 1
195
+ ), "expert_start must be contiguous (next_power_of_2(num_experts) + 1,)"
196
+ return S
197
+
198
+ def tokens_per_expert_bucket(S: int, num_experts: int) -> int:
199
+ """log2 bucket of the average routed rows per expert — the grouped kernels' autotune
200
+ key (raw S would retune per unique token count)."""
201
+ return int((S + num_experts - 1) // num_experts).bit_length()
202
+
203
+ def normalize_global_scale(
204
+ g: torch.Tensor | None, num_experts: int
205
+ ) -> torch.Tensor | None:
206
+ """An NVFP4 second-level global in one of the kernels' fp32 load layouts: ``(1,)`` per tensor
207
+ or ``(num_experts,)`` per expert (``(E, 1, 1)`` accepted for the latter). The kernels index it
208
+ through ``global_scale_stride``, so a shared value broadcasts by stride and nothing is
209
+ materialized per call. ``None`` passes through.
210
+
211
+ A gate|up stack calibrated per half (modelopt's ``(E, 2)`` ``weight_scale_2``) is merged to one
212
+ global per expert at load — SwiGLU is linear in the up half, so its global folds into the down
213
+ projection's — never here: the ops take one global per matrix."""
214
+ if g is None:
215
+ return None
216
+ g = g.float()
217
+ if g.numel() == 1:
218
+ return g.reshape(1)
219
+ assert g.numel() == num_experts, (
220
+ f"global scale of shape {tuple(g.shape)}: expected a scalar or ({num_experts},)"
221
+ )
222
+ return g.reshape(num_experts).contiguous()
223
+
224
+ def is_per_expert_global(g: torch.Tensor | None) -> bool:
225
+ """Whether an NVFP4 second-level global carries one value per expert rather than one for the
226
+ whole tensor. The activation quant branches on it: a per-expert global has to be applied by
227
+ each quantized row's own expert, which is what decides the grid that quant runs on."""
228
+ return g is not None and g.numel() > 1
229
+
230
+ def global_scale_stride(g: torch.Tensor | None) -> int:
231
+ """The expert stride of a ``normalize_global_scale`` layout: 0 for a scalar (or ``None``),
232
+ which broadcasts it to every expert, else 1."""
233
+ return 0 if g is None or g.numel() == 1 else 1
234
+
235
+ def normalize_per_expert_scale(Bs: torch.Tensor, num_experts: int) -> torch.Tensor:
236
+ """One per-tensor scale per expert, normalized to ``(num_experts, 1, 1)`` from
237
+ either that or a bare ``(num_experts,)``."""
238
+ if Bs.ndim == 1:
239
+ assert Bs.shape[0] == num_experts, (
240
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts},)"
241
+ )
242
+ return Bs.reshape(num_experts, 1, 1)
243
+ assert Bs.shape == (num_experts, 1, 1), (
244
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, 1, 1)"
245
+ )
246
+ return Bs
247
+
248
+ def mx_scale_family(Bs: torch.Tensor, K: int) -> int:
249
+ """The group size of an MX/NV weight-scale tensor, in either layout — the wrapper hands ``Bs``
250
+ as-is and this reads the group off its shape. Row-major (2D/3D): ``K // Bs.shape[-1]``.
251
+ SWIZZLE_32_4_4 (5D ``(G, row_blocks, cols // 4, 2, 256)``): ``K // (Bs.shape[2] * 4)``. The scale
252
+ dtype IS the format carrier (E4M3 = NVFP4 group-16, UE8M0 = MX group-32) and the pairing is
253
+ validated; callers that need the format read it off ``Bs.dtype`` (``== torch.float8_e4m3fn``
254
+ is NVFP4)."""
255
+ nvfp4 = Bs.dtype == torch.float8_e4m3fn
256
+ assert nvfp4 or Bs.dtype in UE8M0_SCALE_DTYPES, (
257
+ f"Bs must be UE8M0 (float8_e8m0fnu/uint8) or E4M3 (NVFP4), got {Bs.dtype}"
258
+ )
259
+ scale_group = K // (Bs.shape[2] * 4) if Bs.ndim == 5 else K // Bs.shape[-1]
260
+ assert scale_group == (NVFP4_SCALE_GROUP_K if nvfp4 else MX_SCALE_GROUP_K), (
261
+ f"scale group {scale_group} does not match the scale dtype {Bs.dtype}"
262
+ )
263
+ assert K % scale_group == 0, f"K (={K}) must be a multiple of {scale_group}"
264
+ return scale_group
265
+
266
+ def get_supported_act_fns() -> tuple[str, ...]:
267
+ """The activation names the gate|up epilogue fuses in-kernel; any other activation is applied
268
+ on the host by the MoE forwards (``act_fn`` as a callable)."""
269
+ return ("silu", "gelu", "relu")
270
+
271
+
272
+ ACTIVATION_FORMATS = (None, "bf16", "fp8", "mxfp8", "mxfp4", "nvfp4")
273
+
274
+
275
+ def check_activation_format(activation_format: str | None, quantize_output: bool) -> None:
276
+ """Validate the dispatchers' activation kwargs. ``activation_format`` is the activations'
277
+ quantization, in the same vocabulary transformers' ``FineGrainedConfig`` uses; the weights are
278
+ never named — their format is read off the weight/scale dtypes (``weight_format``).
279
+
280
+ - ``None`` (default): the weights' own format — W8A8 on FP8/MXFP8 weights, the all-fp4 W4A4
281
+ chain on MXFP4/NVFP4 weights.
282
+ - ``"bf16"``: weight-only (W4A16 / W8A16) — the activation stays high precision.
283
+ - ``"fp8"`` / ``"mxfp8"`` / ``"mxfp4"`` / ``"nvfp4"``: an explicit activation format, e.g.
284
+ ``"mxfp8"`` on MXFP4 weights is the W4A8 chain. A raw ``A`` is quantized to it by the op; an
285
+ already-quantized ``A`` arrives with ``As`` (its dtype carries the format).
286
+
287
+ ``quantize_output`` stores the (GLU) output requantized into that same format — bit-identical
288
+ to quantizing the values offline — so it feeds the next op's activation side directly; the op
289
+ then returns ``(C, Cs)``. A weight-only activation never requantizes.
290
+
291
+ Per weight family:
292
+
293
+ ================== ===============================================================
294
+ weight family activation_format
295
+ ================== ===============================================================
296
+ block-dynamic FP8 None / "fp8" (E4M3 + per-block scales); quantize_output ok
297
+ tensor-wide FP8 None / "fp8" (E4M3 + per-token scales); no quantize_output
298
+ MXFP8 / MXFP4 None / "mxfp8" (E4M3 + UE8M0) / "mxfp4" (packed E2M1 + UE8M0) / "bf16"
299
+ NVFP4 None / "nvfp4" (packed E2M1 + E4M3 group-16) / "bf16"
300
+ full-precision None (nothing to quantize)
301
+ ================== ===============================================================
302
+
303
+ (Tensor-wide's whole-row activation scale can't be formed by a tile-local epilogue; the unfused
304
+ path quantizes on the host between GEMMs.) A format the weight family doesn't support fails
305
+ loudly at the op."""
306
+ assert activation_format in ACTIVATION_FORMATS, (
307
+ f"unknown activation_format {activation_format!r}; expected one of {ACTIVATION_FORMATS}"
308
+ )
309
+ assert not (quantize_output and activation_format == "bf16"), (
310
+ "quantize_output needs quantized activations (activation_format='bf16' is weight-only)"
311
+ )
312
+
313
+
314
+ def weight_format(B: torch.Tensor, Bs: torch.Tensor) -> str:
315
+ """The format the weights are ALREADY in, read off their dtypes — what ``activation_format=None``
316
+ resolves to. E4M3 scales -> ``"nvfp4"``; else packed E2M1 -> ``"mxfp4"``, MX E4M3 -> ``"mxfp8"``,
317
+ block/tensor-scaled E4M3 -> ``"fp8"``."""
318
+ if Bs.dtype == torch.float8_e4m3fn:
319
+ return "nvfp4"
320
+ if not is_mx(B, Bs):
321
+ return "fp8"
322
+ # packed E2M1 is a WEIGHT dtype fact; is_mxfp4's shape check can't see it through a
323
+ # pre-swizzled (5D) scale, which would silently downgrade a W4A4 default to W4A8.
324
+ return "mxfp4" if B.dtype in (torch.int8, torch.uint8) else "mxfp8"
325
+
326
+
327
+
328
+
329
+ def resolve_activation_format(activation_format: str | None, B: torch.Tensor, Bs: torch.Tensor) -> str:
330
+ """The format an MX/NVFP4 GEMM quantizes its activations to. ``None`` resolves to the weights'
331
+ own format (``weight_format``): NVFP4 weights pin ``"nvfp4"`` (the MMA kind needs matching
332
+ scale formats on both operands), packed E2M1 -> ``"mxfp4"`` (the all-fp4 W4A4 chain), else
333
+ ``"mxfp8"``. An explicit name overrides (e.g. ``"mxfp8"`` on MXFP4 weights = the W4A8 chain)
334
+ and is validated against the family. ``"bf16"`` never reaches here — a weight-only chain is
335
+ routed to its own kernel by the dispatcher."""
336
+ if Bs.dtype == torch.float8_e4m3fn:
337
+ assert activation_format in (None, "nvfp4"), (
338
+ f"NVFP4 activations are packed E2M1 + E4M3 scales, got {activation_format!r}"
339
+ )
340
+ return "nvfp4"
341
+ assert activation_format in (None, "mxfp8", "mxfp4"), (
342
+ f"MX activations are E4M3 ('mxfp8') or packed E2M1 ('mxfp4'), got {activation_format!r}"
343
+ )
344
+ return activation_format if activation_format is not None else weight_format(B, Bs)
345
+
346
+
347
+ def resolve_output_dtype(
348
+ output_dtype: torch.dtype | None,
349
+ activation: torch.Tensor,
350
+ act_scale: torch.Tensor | None,
351
+ ) -> torch.dtype:
352
+ """Output element type for a quantized matmul: the explicit ``output_dtype`` if given, else
353
+ the raw activation dtype (``act_scale`` is None -> ``activation`` is high precision), else
354
+ ``bfloat16`` (``activation`` is pre-quantized FP8, whose dtype is not a valid output)."""
355
+ if output_dtype is not None:
356
+ return output_dtype
357
+ return activation.dtype if act_scale is None else torch.bfloat16
build/torch-rocm/grouped.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-rocm/loading/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """How a K-step's operands get from memory into registers: ``tiles`` loads the activation and
16
+ weight value tiles (pointer / host-TMA / gather arms), ``scales`` loads the block scales (affine or
17
+ SWIZZLE_32_4_4) and quantizes a raw activation tile inline."""
build/torch-rocm/loading/scales.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import triton
16
+ import triton.language as tl
17
+
18
+ from ..quant import fp8_act_quant_inline, mx_act_quant_inline
19
+
20
+
21
+ @triton.jit
22
+ def load_block_fp8_act_tile(
23
+ a_ptrs,
24
+ as_ptrs,
25
+ a_descriptor=0,
26
+ m_off=0,
27
+ k_off=0,
28
+ A_MEMORY_MODE: tl.constexpr = "pointer",
29
+ as_mask=None,
30
+ TRANSPOSED: tl.constexpr = False,
31
+ ):
32
+ """Block-FP8 counterpart of ``load_mx_act_tile``: load one activation K-tile as
33
+ ``(a_fp8, a_scale_f32)`` — the arm folds off ``A_MEMORY_MODE`` and the pointer dtype at
34
+ compile time. Descriptor mode loads the pre-quantized ``(BM, BK)`` host-TMA box at
35
+ ``(m_off, k_off)`` (``a_ptrs`` unread); pointer fp8 loads the pre-quantized-offline tile;
36
+ raw bf16/fp16 pointers quantize inline (``as_ptrs`` then a constexpr-dead placeholder).
37
+ Either way the per-K-block scales come from ``as_ptrs``. ``TRANSPOSED`` marks a ``(K, M)``
38
+ tile (the swapped pointer arm) so the inline amax reduces the token axis either way.
39
+ Unmasked unless ``as_mask`` given: every caller's rows are %-wrapped, expert-advanced, or
40
+ token-replicated."""
41
+ if A_MEMORY_MODE != "pointer": # pre-quantized, host-TMA box [BM, BK]
42
+ a = a_descriptor.load([m_off, k_off])
43
+ # as_mask None (batched/grouped: %-wrapped/expert-advanced rows) -> unmasked load;
44
+ # a bounds mask (the 2D affine-scale path) -> masked. `other` is illegal without mask.
45
+ if as_mask is None:
46
+ a_s = tl.load(as_ptrs)
47
+ else:
48
+ a_s = tl.load(as_ptrs, mask=as_mask, other=0.0)
49
+ elif a_ptrs.dtype.element_ty == tl.float8e4nv: # pre-quantized offline
50
+ a = tl.load(a_ptrs)
51
+ if as_mask is None:
52
+ a_s = tl.load(as_ptrs)
53
+ else:
54
+ a_s = tl.load(as_ptrs, mask=as_mask, other=0.0)
55
+ else: # raw bf16/fp16 — quantize inline
56
+ a, a_s = fp8_act_quant_inline(tl.load(a_ptrs).to(tl.float32), TRANSPOSED)
57
+ return a, a_s
58
+
59
+
60
+ @triton.jit
61
+ def load_swizzled_scale(
62
+ desc, blk_idx, k_idx,
63
+ REP: tl.constexpr, REP_K: tl.constexpr, BLOCK: tl.constexpr, SCALE_COLS: tl.constexpr,
64
+ ):
65
+ """Bulk-load one scale tile from a ``SWIZZLE_32_4_4`` descriptor and un-swizzle it to the
66
+ ``(BLOCK, SCALE_COLS)`` layout ``tl.dot_scaled`` consumes. ``REP = BLOCK // 128``,
67
+ ``REP_K = SCALE_COLS // 4``; ``blk_idx``/``k_idx`` are the tile's row-block / K-block ids.
68
+ Descriptor is over the swizzled scale viewed ``(1, rows//128, cols//4, 2, 256)``."""
69
+ s = desc.load([0, blk_idx * REP, k_idx * REP_K, 0, 0])
70
+ return s.reshape(REP, REP_K, 32, 4, 4).trans(0, 3, 2, 1, 4).reshape(BLOCK, SCALE_COLS)
71
+
72
+
73
+ @triton.jit
74
+ def load_swizzled_scale_tile(
75
+ descriptor,
76
+ ptr,
77
+ group_id,
78
+ pid,
79
+ k_idx,
80
+ rows,
81
+ K,
82
+ BLOCK: tl.constexpr,
83
+ SCALE_COLS: tl.constexpr,
84
+ SCALE_GROUP_K: tl.constexpr,
85
+ ):
86
+ """One swizzled-scale tile ``(BLOCK, SCALE_COLS)`` for a row-tile of ANY operand — batched
87
+ weight (``group_id = expert``), 2D weight / 2D activation (``group_id = 0``, dense). Scales are
88
+ SWIZZLE_32_4_4 128-row blocks over ``rows`` (the operand's row count, ``rows//128`` blocks per
89
+ group).
90
+
91
+ - ``BLOCK`` a multiple of 128 with whole 4-group K bands (``BK % 128 == 0``): the fast path —
92
+ bulk-load the ``REP = BLOCK//128`` row-blocks via the TMA ``descriptor`` (box
93
+ ``[1, REP, rep_k, 2, 256]``), un-swizzle, feed ``dot_scaled``. Today's pruners cap every
94
+ swizzled tile at 128 (REP=1); the REP>1 form is written for a future >128-tile descriptor
95
+ read, not reachable from the current grids.
96
+ - Otherwise (sub-128 tile — fp8 ``scalar`` decode / small-M offline; or ``BK<128``): the block
97
+ layout can't be TMA-sliced, so pointer-GATHER exactly this tile's rows. The swizzle is a fixed
98
+ permutation: logical (row ``r``, K-group ``col``) → byte
99
+ ``(blk*cols4 + col//4)*512 + (r%32)*16 + ((r%128)//32)*4 + col%4``. Reads only the needed
100
+ bytes — no 128-block over-read, no un-swizzle transpose — the row-major fast path's cost with
101
+ the swizzled layout, so ``scalar`` competes on merit instead of eating a TMA penalty."""
102
+ # Per-expert 128-row-block count is a CEIL: a non-128-multiple ``rows`` (e.g. N=2880) still
103
+ # occupies ceil(rows/128) blocks in the buffer (the swizzle builder pads the partial last block;
104
+ # its tail rows read zero-weight via the TMA OOB clamp, so they don't contribute). ``cdiv == floor``
105
+ # when ``rows`` is 128-aligned, so this is inert for every aligned shape.
106
+ nrb = (rows + 127) // 128
107
+ if BLOCK % 128 == 0 and SCALE_COLS >= 4 and SCALE_COLS % 4 == 0:
108
+ REP: tl.constexpr = BLOCK // 128
109
+ # absolute 128-block base = group_id*nrb + pid*REP; load_swizzled_scale multiplies blk by REP.
110
+ # Non-128 ``rows`` (odd ``nrb``) pins REP=1 (BN=128) in the pruner, so group_id*nrb//REP is exact.
111
+ blk = (group_id * nrb // REP + pid).to(tl.int32)
112
+ return load_swizzled_scale(descriptor, blk, k_idx, REP, SCALE_COLS // 4, BLOCK, SCALE_COLS)
113
+ cols4 = (K // SCALE_GROUP_K + 3) // 4 # cdiv: the buffer pads cols to whole 4-group chunks
114
+ r = pid * BLOCK + tl.arange(0, BLOCK)
115
+ lin = r // 128
116
+ blk = group_id * nrb + lin
117
+ row = r % 128
118
+ col = k_idx * SCALE_COLS + tl.arange(0, SCALE_COLS)
119
+ off = (
120
+ (blk[:, None] * cols4 + col[None, :] // 4) * 512
121
+ + (row[:, None] % 32) * 16
122
+ + (row[:, None] // 32) * 4
123
+ + col[None, :] % 4
124
+ )
125
+ return tl.load(ptr + off)
126
+
127
+
128
+ @triton.jit
129
+ def load_weight_scale_tile(
130
+ SWIZZLED_SCALES: tl.constexpr,
131
+ bs_descriptor,
132
+ bs_ptr,
133
+ expert_id,
134
+ pid_n,
135
+ k_idx,
136
+ N,
137
+ K,
138
+ stride_bs_e,
139
+ stride_bs_n,
140
+ stride_bs_k,
141
+ BLOCK_SIZE_N: tl.constexpr,
142
+ SCALE_COLS: tl.constexpr,
143
+ SCALE_GROUP_K: tl.constexpr,
144
+ GATE: tl.constexpr,
145
+ k_col_off=0,
146
+ ):
147
+ """One batched-decode weight-scale tile ``(n_width, SCALE_COLS)``, hiding the swizzled vs
148
+ un-swizzled choice behind the ``SWIZZLED_SCALES`` flag — the kernel loop reads scales the same
149
+ way either layout. ``bs_ptr`` is the un-advanced buffer base; the per-expert offset is applied
150
+ here (the swizzled path indexes by 128-row block, the un-swizzled by the row-major stride, so
151
+ it can't be pre-advanced uniformly). Under ``GATE`` the gate|up sub-tiles stack into ``2*BN``.
152
+
153
+ - ``SWIZZLED_SCALES``: SWIZZLE_32_4_4 via ``load_swizzled_scale_tile`` (descriptor bulk at BN=128, or
154
+ pointer gather below), scales swizzled over the full ``2N`` rows/expert under GATE.
155
+ - else: affine per-group load off ``(expert, N-tile row, K-group)`` — no in-op swizzle, so an
156
+ un-swizzled caller pays nothing. ``k_col_off`` shifts that read off the tile grid (the
157
+ caller's K-tile was clamped back into bounds for a BK that doesn't divide K); it is 0
158
+ for every aligned caller, and the swizzled layouts don't take it."""
159
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
160
+ if SWIZZLED_SCALES:
161
+ # gate|up needs no arm: the scale grid is row-interleaved with the weight, so the tile's
162
+ # gate and up rows are the contiguous n_width span at the same pid_n, i.e. the plain read
163
+ # over the doubled extent.
164
+ b_s = load_swizzled_scale_tile(
165
+ bs_descriptor, bs_ptr, expert_id, pid_n, k_idx,
166
+ 2 * N if GATE else N, K,
167
+ n_width, SCALE_COLS, SCALE_GROUP_K,
168
+ )
169
+ else:
170
+ # affine per-group load off (expert, N-tile row, K-group) from the un-advanced base.
171
+ # Clamp the row in-bounds instead of masking (a masked load's other=0.0 blocks WS
172
+ # lowering): a partial last N-tile reads a clamped scale the epilogue masks off
173
+ # (N_COLS) — without the clamp the GATE arm's up rows (N + offs_bn) run past 2N.
174
+ base = bs_ptr + expert_id * stride_bs_e
175
+ offs_bn = pid_n * n_width + tl.arange(0, n_width)
176
+ offs_bn = tl.minimum(offs_bn, (2 * N if GATE else N) - 1)
177
+ offs_sf = k_idx * SCALE_COLS + k_col_off + tl.arange(0, SCALE_COLS)
178
+ ptrs = base + offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k
179
+ b_s = tl.load(ptrs)
180
+ return b_s
181
+
182
+
183
+ @triton.jit
184
+ def load_mx_act_tile(
185
+ a_ptrs,
186
+ as_ptrs,
187
+ as_global, # fp32 NVFP4 act global (None off nvfp4); normalizes the raw tile pre-block-quant
188
+ as_global_row, # element offset into ``as_global`` (the expert's entry; 0 for a shared scalar)
189
+ row_mask,
190
+ a_descriptor,
191
+ m_start,
192
+ ka_off,
193
+ gather_rows,
194
+ BLOCK_SIZE_M: tl.constexpr,
195
+ BLOCK_SIZE_K: tl.constexpr,
196
+ SCALE_GROUP_K: tl.constexpr,
197
+ A_MEMORY_MODE: tl.constexpr = "pointer",
198
+ A_GATHER: tl.constexpr = False,
199
+ FORMAT: tl.constexpr = "mxfp8",
200
+ ):
201
+ """Load one MX activation K-tile as ``(a_vals, a_scale)`` — the arm is picked
202
+ off the pointer dtype at compile time: fp8 pointers load pre-quantized E4M3 values +
203
+ UE8M0 scales (``maybe_act_quant``'s offline arm), uint8 pointers load caller-provided
204
+ packed-E2M1 values (W4A4 — the ``a_ptrs`` tile spans ``BLOCK_SIZE_K // 2`` bytes) +
205
+ the same UE8M0 scales, raw bf16/fp16 pointers load and quantize inline onto
206
+ ``FORMAT``'s grid (``mx_act_quant_inline`` — packed E2M1 under the fp4 formats;
207
+ ``as_ptrs`` then points at a dead placeholder and is never read). Under NVFP4
208
+ two-level, ``as_global[as_global_row]`` (the calibrated activation global — per tensor
209
+ or this expert's) normalizes the raw tile before the block quant — bit-identical to the
210
+ offline ``nvfp4_act_quant(x, global_scale=g_a)`` pass. ``row_mask`` may be ``None``
211
+ (unmasked tiles, e.g. the %-wrapped 2D matmul). Callers advance both pointers
212
+ unconditionally."""
213
+ if a_ptrs.dtype.element_ty == tl.float8e4nv or a_ptrs.dtype.element_ty == tl.uint8:
214
+ # pre-quantized (E4M3 offline, or packed E2M1 handed in by the caller); under the
215
+ # descriptor arm the [BM, BK_bytes] box loads the tile's contiguous sorted rows
216
+ # (no gather — tail rows past the tensor clamp to zero and are store-masked)
217
+ if A_MEMORY_MODE != "pointer":
218
+ if A_GATHER:
219
+ # sm_100 tma gather4: bulk-load the tile's ARBITRARY source rows
220
+ a = a_descriptor.gather(gather_rows, ka_off)
221
+ else:
222
+ a = a_descriptor.load([m_start, ka_off])
223
+ if as_ptrs.dtype.element_ty == tl.float8e4nv:
224
+ # NVFP4 scales: 0.0 encodes as byte 0 — padded rows scale to exact 0
225
+ a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0.0)
226
+ else:
227
+ # UE8M0 scales: byte 0 decodes to 2^-127 — padded rows can't make 0*inf
228
+ a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0)
229
+ elif row_mask is None:
230
+ a = tl.load(a_ptrs)
231
+ a_scale = tl.load(as_ptrs)
232
+ else:
233
+ a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
234
+ if as_ptrs.dtype.element_ty == tl.float8e4nv:
235
+ # NVFP4 scales: 0.0 encodes as byte 0 — padded rows scale to exact 0
236
+ a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0.0)
237
+ else:
238
+ # UE8M0 scales: byte 0 decodes to 2^-127 — padded rows can't make 0*inf
239
+ a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0)
240
+ else: # raw bf16/fp16 — quantize inline
241
+ # the descriptor arm mirrors the pre-quantized branch above. Without it every
242
+ # A_MEMORY_MODE != "pointer" config fell through to `tl.load(a_ptrs)`, and under a
243
+ # descriptor arm `operand_tile_ptrs` hands back the bare base pointer -- so that load
244
+ # produced a SCALAR and mx_act_quant_inline's reshape died with "'dtype' object has no
245
+ # attribute 'numel'". Raw-A is exactly the inline-quant (nvfp4/mxfp4 2D) path, so it
246
+ # killed every descriptor config there: 321 dead compiles in one tune.
247
+ if A_MEMORY_MODE != "pointer":
248
+ if A_GATHER:
249
+ a_raw = a_descriptor.gather(gather_rows, ka_off).to(tl.float32)
250
+ else:
251
+ a_raw = a_descriptor.load([m_start, ka_off]).to(tl.float32)
252
+ elif row_mask is None:
253
+ a_raw = tl.load(a_ptrs).to(tl.float32)
254
+ else:
255
+ a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
256
+ if as_global is not None: # NVFP4 two-level: normalize by the calibrated act global
257
+ a_raw = a_raw / tl.load(as_global + as_global_row).to(tl.float32)
258
+ a, a_scale = mx_act_quant_inline(
259
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, FORMAT
260
+ )
261
+ return a, a_scale
262
+
263
+
264
+ @triton.jit
265
+ def apply_global_scale(acc, GlobalScale, stride_gs_e, GlobalScaleA, stride_gsa_e, expert_id):
266
+ """Fold the NVFP4 second-level globals onto the fp32 accumulator: the weight global ``g_b``
267
+ and the activation global ``g_a``, each per tensor (stride 0, broadcast) or per expert.
268
+ ``None`` folds the arm out."""
269
+ if GlobalScale is not None:
270
+ acc = acc * tl.load(GlobalScale + expert_id * stride_gs_e).to(tl.float32)
271
+ if GlobalScaleA is not None:
272
+ acc = acc * tl.load(GlobalScaleA + expert_id * stride_gsa_e).to(tl.float32)
273
+ return acc
274
+
275
+
276
+ @triton.jit
277
+ def decode_group_scale(scale):
278
+ """Decode a group scale to fp32 by its dtype: ``uint8`` = UE8M0 exponent bits
279
+ (``value = 2^(exp - 127)``, built directly as the fp32 bit pattern), E4M3 = NVFP4's
280
+ direct fp8 value, fp32 (block-dynamic with float scales) passes through. The dtype
281
+ branch is a compile-time constant, so only the taken path is emitted (single return —
282
+ Triton requires all ``return`` statements to share a type)."""
283
+ if scale.dtype == tl.uint8:
284
+ scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
285
+ elif scale.dtype == tl.float8e4nv:
286
+ scale = scale.to(tl.float32)
287
+ return scale
288
+
289
+
290
+ @triton.jit
291
+ def mx_2d_scale_ptrs(
292
+ As,
293
+ Bs,
294
+ pid_m,
295
+ pid_n,
296
+ M,
297
+ N,
298
+ stride_as_m,
299
+ stride_bs_n,
300
+ stride_bs_k,
301
+ BLOCK_SIZE_M: tl.constexpr,
302
+ BLOCK_SIZE_N: tl.constexpr,
303
+ SCALE_COLS: tl.constexpr,
304
+ SWIZZLED_SCALES: tl.constexpr,
305
+ GATE: tl.constexpr = False,
306
+ ):
307
+ """Prologue 2D MX scale-pointer tiles + bounds masks as ``(as_ptrs, bs_ptrs, as_mask,
308
+ bs_mask)``. Affine arm: per-(row, group) ``as``/``bs`` pointer tiles read off AFFINE
309
+ row/col offsets (the %-wrapped operand offsets would make the scale load a non-affine
310
+ gather) with row/col bounds masks. Swizzled arm: the scales are read via the SA/BS
311
+ descriptors in the loop, so these tiles are dead — return the base scalars + null masks.
312
+ Single return — the arms have divergent types (base scalars + null masks vs affine pointer
313
+ tiles + bounds masks), so an early return can't unify them; the constexpr selects one."""
314
+ if SWIZZLED_SCALES:
315
+ as_ptrs, bs_ptrs, as_mask, bs_mask = As, Bs, None, None
316
+ else:
317
+ offs_am_lin = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
318
+ ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
319
+ offs_bn_lin = pid_n * ROWS + tl.arange(0, ROWS)
320
+ offs_sf = tl.arange(0, SCALE_COLS)
321
+ as_ptrs = As + offs_am_lin[:, None] * stride_as_m + offs_sf[None, :]
322
+ bs_ptrs = Bs + (offs_bn_lin[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k)
323
+ as_mask = offs_am_lin < M
324
+ bs_mask = offs_bn_lin < (2 * N if GATE else N)
325
+ return as_ptrs, bs_ptrs, as_mask, bs_mask
build/torch-rocm/loading/tiles.py ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import triton
16
+ import triton.language as tl
17
+
18
+ from .scales import load_block_fp8_act_tile, load_mx_act_tile, load_swizzled_scale, load_swizzled_scale_tile, load_weight_scale_tile
19
+
20
+ # A-sub-tile byte budget for the 1-byte-activation grouped-swizzle depth cap (see
21
+ # swizzle_offsets): the co-scheduled rows' A tile per K-step (depth*BM*BK bytes) must stay
22
+ # L2-hot to reuse, and past this it thrashes and the win collapses. ~512KB is a wave
23
+ # reuse-window, NOT gross L2 (0.4% of the B200's 132MB) — a B200 measurement (2026-07-16), so
24
+ # RE-MEASURE per device. Packed fp4 bypasses this (full grouping wins; see swizzle_offsets).
25
+ SWIZZLE_GROUP_A_BYTES = tl.constexpr(524288)
26
+
27
+
28
+ @triton.jit
29
+ def swizzle_offsets(
30
+ M,
31
+ N,
32
+ BLOCK_SIZE_M: tl.constexpr,
33
+ BLOCK_SIZE_N: tl.constexpr,
34
+ BLOCK_SIZE_K: tl.constexpr,
35
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
36
+ GATE: tl.constexpr = False,
37
+ ):
38
+ """2D-grid tile scheduling shared by the kernels below: grouped-swizzle the
39
+ ``(pid_m, pid_n)`` program ids for L2 locality on B, then build the operand offset
40
+ vectors. Returns ``(pid_m, pid_n, offs_am, offs_bn, offs_k)`` — the swizzled ids
41
+ (reused by the output store) and the ``%``-wrapped row/col offsets plus the K range.
42
+
43
+ ``offs_bn`` is the WEIGHT-side row range: under ``GATE`` the doubled span
44
+ ``[2*pid_n*BN, 2*(pid_n+1)*BN)`` wrapped at ``2N``. The grid is sized by the OUTPUT tiles.
45
+
46
+ The swizzle keeps the B (weight) column-tile L2-hot while the co-scheduled rows reuse it,
47
+ so the depth cap is set by the WEIGHT footprint, capped at ``min(num_pid_m, .)``. With
48
+ 1-byte weights the reuse thrashes past ~512KB (a hard cliff): the growing rival is the
49
+ rows' A sub-tile per K-step (``depth * BM * BK`` bytes), so ``SWIZZLE_GROUP_A_BYTES //
50
+ (BM * BK)`` — MEASURED on B200 (2026-07-16), bd BK128->32 (cliffs at 64), MX BK256->16,
51
+ BM64->64 (g*BM*BK ~512KB across BM and BK; BN-independent). Packed-fp4 weights
52
+ (``WEIGHT_VALUES_PER_BYTE==2``) halve that hot set, so it never thrashes and full grouping
53
+ wins outright (monotone, no cliff — measured for both W4A4 and W4A8, i.e. weight-driven not
54
+ activation-driven). Uses an EXPLICIT grouped swizzle (linearize the 2D program ids, then
55
+ group), NOT ``tl.swizzle2d`` (which degrades with group depth, -3pp+ MFU at depth 32). Same
56
+ grid launch, same result set — only the program-id -> tile mapping changes."""
57
+ num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
58
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
59
+ pid = tl.program_id(axis=1) * num_pid_m + tl.program_id(axis=0)
60
+ if WEIGHT_VALUES_PER_BYTE == 2:
61
+ max_group = num_pid_m
62
+ else:
63
+ max_group = SWIZZLE_GROUP_A_BYTES // (BLOCK_SIZE_M * BLOCK_SIZE_K)
64
+ num_pid_in_group = max_group * num_pid_n
65
+ group_id = pid // num_pid_in_group
66
+ first_pid_m = group_id * max_group
67
+ group_size_m = min(num_pid_m - first_pid_m, max_group)
68
+ pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
69
+ pid_n = (pid % num_pid_in_group) // group_size_m
70
+ WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
71
+ offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
72
+ offs_bn = (pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)) % (2 * N if GATE else N)
73
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
74
+ return pid_m, pid_n, offs_am, offs_bn, offs_k
75
+
76
+
77
+ @triton.jit
78
+ def operand_tile_descriptor(
79
+ HostDescriptor,
80
+ W,
81
+ N,
82
+ K,
83
+ stride_n,
84
+ stride_k,
85
+ BLOCK_SIZE_N: tl.constexpr,
86
+ BLOCK_SIZE_K: tl.constexpr,
87
+ B_MEMORY_MODE: tl.constexpr,
88
+ GATE: tl.constexpr = False,
89
+ ):
90
+ """Resolve one operand's tile descriptor once per program (weight OR activation — the bd 2D
91
+ kernel calls it for both): the host-built TMA descriptor as passed, a device-built in-kernel
92
+ tensormap, or 0 under "pointer" (never read — the constexpr branch folds it out of
93
+ ``load_weight_tile``). Single return — only the taken branch compiles."""
94
+ if B_MEMORY_MODE == "host_descriptor":
95
+ descriptor = HostDescriptor
96
+ elif B_MEMORY_MODE == "device_descriptor":
97
+ # under GATE the operand is the (2N, K) gate|up weight and a tile is a contiguous 2*BN
98
+ # row span of it, so both the tensor extent and the box double
99
+ descriptor = tl.make_tensor_descriptor(
100
+ W,
101
+ shape=((2 * N) if GATE else N, K),
102
+ strides=(stride_n, stride_k),
103
+ block_shape=((2 * BLOCK_SIZE_N) if GATE else BLOCK_SIZE_N, BLOCK_SIZE_K),
104
+ )
105
+ else: # pointer
106
+ descriptor = 0
107
+ return descriptor
108
+
109
+
110
+ @triton.jit
111
+ def load_grouped_act_tile(
112
+ a_ptrs,
113
+ a_descriptor,
114
+ m_start,
115
+ ka_off,
116
+ row_mask,
117
+ gather_rows,
118
+ A_MEMORY_MODE: tl.constexpr,
119
+ A_GATHER: tl.constexpr = False,
120
+ ):
121
+ """A plain (scale-less) grouped activation K-tile: the masked pointer load, or —
122
+ under the descriptor arm — sm_100 tma gather4 over the tile's ARBITRARY source rows
123
+ (gathered launches; padded rows read row 0 and are store-masked) or the ``[BM, BK]``
124
+ box at the tile's contiguous sorted-row start (no-gather launches; tail rows past
125
+ the tensor clamp to zero). ``row_mask`` None loads the pointer tile maskless (the
126
+ %-wrapped 2D fast path). Single return — only the taken arm compiles."""
127
+ if A_MEMORY_MODE == "pointer":
128
+ if row_mask is None:
129
+ a = tl.load(a_ptrs)
130
+ else:
131
+ a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
132
+ elif A_GATHER:
133
+ a = a_descriptor.gather(gather_rows, ka_off)
134
+ else:
135
+ a = a_descriptor.load([m_start, ka_off])
136
+ return a
137
+
138
+
139
+ @triton.jit
140
+ def load_grouped_weight_tile(
141
+ w_ptrs,
142
+ w_descriptor,
143
+ row0,
144
+ n_off,
145
+ kb_off,
146
+ BLOCK_SIZE_N: tl.constexpr,
147
+ KB: tl.constexpr,
148
+ GATE: tl.constexpr,
149
+ B_MEMORY_MODE: tl.constexpr,
150
+ SWAP_AB: tl.constexpr = False,
151
+ ):
152
+ """One K-major (optionally gate|up-stacked) MX weight K-tile for the grouped / batched loop:
153
+ the explicit-pointer tile flattened to the ``[KB, (2|1)*BN]`` rhs (or, under ``SWAP_AB``, the
154
+ ``[(2|1)*BN, KB]`` rows-major lhs — the batched-decode orientation), or the ``[(2|1), BN, KB]``
155
+ descriptor box over the ``(E, 2N|N, K_bytes)`` weight view, reshaped and transposed to the same
156
+ form (the fused-era TMA arm: natural orientation + per-iteration trans; grouped/2D never swap).
157
+ Single return — only the taken arm compiles; the caller advances ``w_ptrs`` and passes the box
158
+ offsets either way."""
159
+ if B_MEMORY_MODE == "pointer":
160
+ w = tl.load(w_ptrs)
161
+ else:
162
+ w = tl.trans(
163
+ tl.reshape(
164
+ w_descriptor.load([row0, n_off, kb_off]),
165
+ ((2 if GATE else 1) * BLOCK_SIZE_N, KB),
166
+ )
167
+ )
168
+ return w
169
+
170
+
171
+ @triton.jit
172
+ def load_weight_tile(
173
+ w_ptrs, w_descriptor, row_off, k_off, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr = False
174
+ ):
175
+ """One weight K-tile. Descriptor modes load the ``(BN, BK)`` box at ``(row_off, k_off)``
176
+ and, in the natural orientation, transpose it once to the ``(BK, BN)`` K-major rhs the
177
+ pointer arm builds directly (``SWAP_AB`` keeps the box as-is: the weight rows then sit in
178
+ the MMA M dim). Pointer mode loads the explicit tile in whatever orientation ``w_ptrs``
179
+ was built with. Single return — only the taken branch compiles."""
180
+ if B_MEMORY_MODE == "pointer":
181
+ w = tl.load(w_ptrs)
182
+ else:
183
+ w = w_descriptor.load([row_off, k_off])
184
+ if not SWAP_AB:
185
+ w = tl.trans(w)
186
+ return w
187
+
188
+
189
+ @triton.jit
190
+ def matmul_weight_ptrs(
191
+ B,
192
+ offs_n,
193
+ offs_k,
194
+ stride_b_n,
195
+ stride_b_k,
196
+ B_MEMORY_MODE: tl.constexpr,
197
+ SWAP_AB: tl.constexpr = False,
198
+ ):
199
+ """Prologue weight-tile pointers via ``operand_tile_ptrs`` (which folds the
200
+ descriptor-vs-pointer arm); ``offs_n`` carries the gate|up span."""
201
+ return operand_tile_ptrs(B, offs_n, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB)
202
+
203
+
204
+ @triton.jit
205
+ def load_act_mx(
206
+ a_ptrs, as_ptrs, as_global, as_global_row, value_mask, scale_mask, a_descriptor, m_off, k_off,
207
+ as_descriptor, as_ptr, gather_rows, stride_as_m, pid_m, k, M, K,
208
+ A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr, GROUPED: tl.constexpr,
209
+ SWIZZLED_SCALES: tl.constexpr, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_K: tl.constexpr,
210
+ SCALE_GROUP_K: tl.constexpr, ACTIVATION_FORMAT: tl.constexpr,
211
+ ):
212
+ """MX activation tile + scale. Offline-quantized A under a swizzled weight -> pre-swizzled
213
+ SWIZZLE_32_4_4 scale (tcgen05 fast path); grouped -> gathered per-(row, K-group) affine scale off
214
+ the source-order ``As``; else (2D / decode raw A) -> ``load_mx_act_tile`` inline-quant / in-register.
215
+ Raw bf16 A stays in-register affine even under a swizzled weight (the dot_scaled mix is fine)."""
216
+ SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
217
+ A_OFFLINE: tl.constexpr = (
218
+ a_ptrs.dtype.element_ty == tl.float8e4nv or a_ptrs.dtype.element_ty == tl.uint8
219
+ )
220
+ if SWIZZLED_SCALES and A_OFFLINE: # pre-swizzled SWIZZLE_32_4_4 scale (tcgen05 fast path)
221
+ a = load_grouped_act_tile(
222
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
223
+ )
224
+ a_s = load_swizzled_scale_tile(
225
+ as_descriptor, as_ptr, 0, pid_m, k, M, K, BLOCK_SIZE_M, SCALE_COLS, SCALE_GROUP_K
226
+ )
227
+ elif GROUPED: # gathered per-(row, K-group) affine scale off the row-major source-order As
228
+ a = load_grouped_act_tile(
229
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
230
+ )
231
+ offs_sf = k * SCALE_COLS + tl.arange(0, SCALE_COLS)
232
+ a_s = tl.load(
233
+ as_ptrs + gather_rows[:, None] * stride_as_m + offs_sf[None, :],
234
+ mask=scale_mask[:, None],
235
+ other=0.0,
236
+ )
237
+ else: # 2D / decode: inline-quant affine or in-register scale
238
+ a, a_s = load_mx_act_tile(
239
+ a_ptrs, as_ptrs, as_global, as_global_row, scale_mask, a_descriptor, m_off, k_off, 0,
240
+ BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, A_MEMORY_MODE,
241
+ FORMAT=ACTIVATION_FORMAT,
242
+ )
243
+ return a, a_s
244
+
245
+
246
+ @triton.jit
247
+ def load_act_block_dynamic(
248
+ a_ptrs, as_ptrs, value_mask, scale_mask, a_descriptor, m_off, k_off, gather_rows, k,
249
+ A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr, GROUPED: tl.constexpr,
250
+ SWAP_AB: tl.constexpr,
251
+ ):
252
+ """block_dynamic activation tile + per-row per-K-block scale. Grouped: gathered value + the scale
253
+ read contiguously at ``As + k``; else ``load_block_fp8_act_tile`` (swap-aware, inline-quant 2D)."""
254
+ if GROUPED: # gathered value + per-row per-K-block scale read contiguously at As + k
255
+ a = load_grouped_act_tile(
256
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
257
+ )
258
+ a_s = tl.load(as_ptrs + k, mask=scale_mask, other=0.0)
259
+ else:
260
+ a, a_s = load_block_fp8_act_tile(
261
+ a_ptrs, as_ptrs, a_descriptor, m_off, k_off, A_MEMORY_MODE, scale_mask, SWAP_AB
262
+ )
263
+ return a, a_s
264
+
265
+
266
+ @triton.jit
267
+ def load_act_static(
268
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, a_s_static,
269
+ A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr,
270
+ ):
271
+ """static activation tile. Pre-quantized fp8 A loads as the rows-major MMA lhs; raw bf16/fp16
272
+ (inline arm, small M, pointer-only) is quantized against the scalar ``a_s_static``. ``a_s`` = the
273
+ values (the static scale is a scalar folded post-loop)."""
274
+ if a_ptrs.dtype.element_ty == tl.float8e4nv: # pre-quantized fp8 A (MMA lhs, rows-major)
275
+ a = load_grouped_act_tile(
276
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
277
+ )
278
+ else: # raw bf16/fp16 (inline arm, M<threshold, pointer-only) — quantize vs the static scale
279
+ a = (tl.load(a_ptrs).to(tl.float32) / a_s_static).to(tl.float8e4nv)
280
+ a_s = a
281
+ return a, a_s
282
+
283
+
284
+ @triton.jit
285
+ def load_act_plain(
286
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows,
287
+ A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr,
288
+ ):
289
+ """tensor / full_precision activation: plain value tile, no scale (returns ``(a, a)`` — the
290
+ second slot is dead, kept for the uniform (value, scale) shape)."""
291
+ a = load_grouped_act_tile(
292
+ a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
293
+ )
294
+ return a, a
295
+
296
+
297
+ @triton.jit
298
+ def _weight_scale_mx(
299
+ bs_ptrs, bs_mask, bs_descriptor, bs_ptr, blk_idx, expert_id, pid_n, k, N, K,
300
+ stride_bs_e, stride_bs_n, stride_bs_k,
301
+ GROUPED: tl.constexpr, GATE: tl.constexpr, PER_EXPERT: tl.constexpr,
302
+ SWIZZLED_SCALES: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
303
+ SCALE_COLS: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
304
+ k_col_off=0,
305
+ ):
306
+ """MX weight-scale K-tile ``(rows, SCALE_COLS)``. Grouped: pre-swizzled SWIZZLE_32_4_4 via the
307
+ descriptor (``load_swizzled_scale`` — gate|up is one 2*BN tile off the block-interleaved buffer,
308
+ GATE folding into REP/width), else an inlined affine per-group read off the un-swizzled 3D ``Bs``
309
+ (inlined, not ``load_weight_scale_tile``: the nested leaf breaks warp-specialization partitioning
310
+ in the grouped loop). 2D-GATE / batched decode: the per-(expert, N, K) 3D leaf. 2D dense: the
311
+ descriptor bulk-load (BN=128) else the bounds-masked affine pointer."""
312
+ if GROUPED:
313
+ if SWIZZLED_SCALES:
314
+ NW: tl.constexpr = (2 if GATE else 1) * BLOCK_SIZE_N
315
+ # REP counts the tile's whole 128-row blocks, so it derives from the DOUBLED extent:
316
+ # BN=64 under GATE is one 128-row block, not 2 * (64 // 128) == 0
317
+ NREP: tl.constexpr = max(1, NW // 128)
318
+ b_s = load_swizzled_scale(
319
+ bs_descriptor, blk_idx, k, NREP, SCALE_COLS // 4, NW, SCALE_COLS
320
+ )
321
+ else: # affine per-group read off the un-swizzled 3D Bs (num_experts, n_rows, K//g)
322
+ NW: tl.constexpr = (2 if GATE else 1) * BLOCK_SIZE_N
323
+ base = bs_ptrs + expert_id * stride_bs_e
324
+ # clamp rather than mask (a masked load's other=0.0 blocks WS lowering): a partial
325
+ # last N-tile reads a clamped scale the epilogue masks off via N_COLS
326
+ offs_bn = tl.minimum(
327
+ pid_n * NW + tl.arange(0, NW), ((2 if GATE else 1) * N) - 1
328
+ )
329
+ offs_sf = k * SCALE_COLS + k_col_off + tl.arange(0, SCALE_COLS)
330
+ b_s = tl.load(base + offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k)
331
+ elif GATE or PER_EXPERT: # 2D-GATE (expert 0) / batched decode: per-(expert, N, K) scale leaf
332
+ b_s = load_weight_scale_tile(
333
+ SWIZZLED_SCALES, bs_descriptor, bs_ptr, expert_id, pid_n, k, N, K,
334
+ stride_bs_e, stride_bs_n, stride_bs_k, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K, GATE,
335
+ k_col_off=k_col_off,
336
+ )
337
+ elif SWIZZLED_SCALES: # pre-swizzled SWIZZLE_32_4_4 scale — descriptor at BN=128, gather below
338
+ b_s = load_swizzled_scale_tile(
339
+ bs_descriptor, bs_ptr, 0, pid_n, k, N, K, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K,
340
+ )
341
+ elif bs_mask is None: # weight-only 2D: N-clamped in-bounds ptrs, UNMASKED (no other=0.0 constant
342
+ b_s = tl.load(bs_ptrs) # -> the K-loop warp-specialization-lowers; tail N masked in epilogue)
343
+ else:
344
+ b_s = tl.load(bs_ptrs, mask=bs_mask[:, None], other=0.0) # 0.0 casts to fp8/uint8
345
+ return b_s
346
+
347
+
348
+ @triton.jit
349
+ def _weight_scale_block_dynamic(
350
+ bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k,
351
+ GROUPED: tl.constexpr, GATE: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
352
+ ):
353
+ """block_dynamic per-K-block weight scale. Grouped reads it at ``bs_ptrs + k*stride`` (GATE folds
354
+ gate on the first ``BN`` columns, up on the rest via ``up_s_ptr``); batched reads the pre-offset
355
+ maskless pointer; 2D reads the bounds-masked advanced pointer."""
356
+ if GROUPED:
357
+ if GATE: # gate scale on the first BN columns, up on the rest
358
+ b_s = tl.where(
359
+ tl.arange(0, 2 * BLOCK_SIZE_N) < BLOCK_SIZE_N,
360
+ tl.load(bs_ptrs + k * stride_bs_k),
361
+ tl.load(up_s_ptr + k * stride_bs_k),
362
+ )
363
+ else:
364
+ # One 128-block scale covers the whole BN tile, so the pointer is scalar and the load
365
+ # is rank-0. Broadcast to the tile's N extent so BOTH grouped arms hand back a rank-1
366
+ # [.., N] scale: `block_dynamic_dot`'s dot_scaled arm indexes `b_s.shape[0]`, which a
367
+ # rank-0 value cannot answer, and that killed every BLOCK_SIZE_M>=128 config (the only
368
+ # ones that take the tcgen05 arm) for the ungated grouped GEMM -- the MoE down proj.
369
+ # broadcast_to keeps the dtype, so decode_group_scale still sees UE8M0 as uint8.
370
+ b_s = tl.broadcast_to(tl.load(bs_ptrs + k * stride_bs_k)[None], [BLOCK_SIZE_N])
371
+ elif bs_mask is None: # batched: bs_ptrs pre-offset (gate/up folded), maskless decode tile
372
+ b_s = tl.load(bs_ptrs)
373
+ else:
374
+ b_s = tl.load(bs_ptrs, mask=bs_mask, other=0.0)
375
+ return b_s
376
+
377
+
378
+ @triton.jit
379
+ def _weight_scale_static(
380
+ bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k,
381
+ GROUPED: tl.constexpr, GATE: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
382
+ ):
383
+ """static weight scale — as block_dynamic, but the grouped non-GATE scale broadcasts to a per-N
384
+ vector so ``accumulate("static")`` applies it in N (the static act scale is a scalar folded
385
+ post-loop)."""
386
+ if GROUPED:
387
+ if GATE: # gate on the first BN, up on the rest
388
+ b_s = tl.where(
389
+ tl.arange(0, 2 * BLOCK_SIZE_N) < BLOCK_SIZE_N,
390
+ tl.load(bs_ptrs + k * stride_bs_k),
391
+ tl.load(up_s_ptr + k * stride_bs_k),
392
+ )
393
+ else:
394
+ b_s = tl.load(bs_ptrs + k * stride_bs_k) + tl.zeros(
395
+ (BLOCK_SIZE_N,), bs_ptrs.dtype.element_ty
396
+ )
397
+ elif bs_mask is None: # batched: bs_ptrs pre-offset (gate/up folded), maskless decode tile
398
+ b_s = tl.load(bs_ptrs)
399
+ else:
400
+ b_s = tl.load(bs_ptrs, mask=bs_mask, other=0.0) # affine col index -> mask OOB last tile
401
+ return b_s
402
+
403
+
404
+ @triton.jit
405
+ def _weight_value(
406
+ b_ptrs, b_descriptor, row0, n_off, k_off,
407
+ GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr,
408
+ SWAP_AB: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, KB: tl.constexpr,
409
+ ):
410
+ """Format-agnostic weight value tile: the per-expert 3D box (GROUPED) or the plain swap-aware
411
+ tile. Shared by every ``load_weight_<format>`` — the value load never depends on the scale
412
+ format. A 2D gate|up weight takes the PLAIN arm: its rows are interleaved, so the tile is a
413
+ contiguous ``2*BN`` span of a 2D ``(2N, K)`` tensor and needs no 3D box — routing it through
414
+ the grouped loader issued a 3-offset load against a 2D descriptor ("expected 2 offsets, but
415
+ got 3"), unreachable only while gate was fenced to pointer mode. Single-exit if/else (an early
416
+ return would type-check the untaken arm)."""
417
+ if GROUPED:
418
+ w = load_grouped_weight_tile(
419
+ b_ptrs, b_descriptor, row0, n_off, k_off, BLOCK_SIZE_N, KB, GATE, B_MEMORY_MODE, SWAP_AB
420
+ )
421
+ else:
422
+ w = load_weight_tile(b_ptrs, b_descriptor, n_off, k_off, B_MEMORY_MODE, SWAP_AB)
423
+ return w
424
+
425
+
426
+ @triton.jit
427
+ def load_weight_mx(
428
+ b_ptrs, b_descriptor, bs_ptrs, bs_mask, bs_descriptor, bs_ptr, row0, n_off, k_off,
429
+ blk_idx, expert_id, pid_n, k, N, K, stride_bs_e, stride_bs_n, stride_bs_k,
430
+ GATE: tl.constexpr, GROUPED: tl.constexpr, PER_EXPERT: tl.constexpr,
431
+ B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr, SWIZZLED_SCALES: tl.constexpr,
432
+ BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
433
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
434
+ k_col_off=0,
435
+ ):
436
+ """The MX weight path: value tile + pre-swizzled/affine group scale. ``k_col_off`` shifts the
437
+ affine scale read for a K-tile the caller clamped back in bounds (0 for aligned callers)."""
438
+ KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
439
+ SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
440
+ w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
441
+ w_s = _weight_scale_mx(
442
+ bs_ptrs, bs_mask, bs_descriptor, bs_ptr, blk_idx, expert_id, pid_n, k, N, K,
443
+ stride_bs_e, stride_bs_n, stride_bs_k,
444
+ GROUPED, GATE, PER_EXPERT, SWIZZLED_SCALES, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K, k_col_off=k_col_off,
445
+ )
446
+ return w, w_s
447
+
448
+
449
+ @triton.jit
450
+ def load_weight_block_dynamic(
451
+ b_ptrs, b_descriptor, bs_ptrs, bs_mask, up_s_ptr, row0, n_off, k_off, k, stride_bs_k,
452
+ GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
453
+ BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
454
+ ):
455
+ """The block_dynamic weight path: value tile + per-K-block scale (gate on the first BN, up on the
456
+ rest via ``up_s_ptr``)."""
457
+ KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
458
+ w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
459
+ w_s = _weight_scale_block_dynamic(bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k, GROUPED, GATE, BLOCK_SIZE_N)
460
+ return w, w_s
461
+
462
+
463
+ @triton.jit
464
+ def load_weight_static(
465
+ b_ptrs, b_descriptor, bs_ptrs, bs_mask, up_s_ptr, row0, n_off, k_off, k, stride_bs_k,
466
+ GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
467
+ BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
468
+ ):
469
+ """The static weight path: value tile + per-K-block scale broadcast to a per-N vector (the static
470
+ act scale is a scalar folded post-loop)."""
471
+ KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
472
+ w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
473
+ w_s = _weight_scale_static(bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k, GROUPED, GATE, BLOCK_SIZE_N)
474
+ return w, w_s
475
+
476
+
477
+ @triton.jit
478
+ def load_weight_plain(
479
+ b_ptrs, b_descriptor, row0, n_off, k_off,
480
+ GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
481
+ BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
482
+ ):
483
+ """The tensor / full_precision weight path: plain value tile, no block scale (the per-tensor
484
+ scale, if any, is applied post-loop). Returns ``(w, w)`` so callers keep the uniform (value,
485
+ scale) shape; the second slot is dead."""
486
+ KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
487
+ w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
488
+ return w, w
489
+
490
+
491
+ @triton.jit
492
+ def advance_ptrs(
493
+ a_ptrs,
494
+ as_ptrs,
495
+ w_ptrs,
496
+ ws_ptrs,
497
+ w_up_ptrs,
498
+ ws_up_ptrs,
499
+ a_step,
500
+ as_step,
501
+ w_step,
502
+ ws_step,
503
+ A_MEMORY_MODE: tl.constexpr,
504
+ W_MEMORY_MODE: tl.constexpr,
505
+ ADVANCE_AS: tl.constexpr,
506
+ ADVANCE_WS: tl.constexpr,
507
+ GATE_STREAMS: tl.constexpr = False,
508
+ ):
509
+ """Advance the shared GEMM operand pointers one K-step, folding the memory-mode / scale-layout /
510
+ gate|up-stream conditionals out of every loop. The operand set is uniform across the kernels:
511
+ activation (``a_ptrs`` + affine scale ``as_ptrs``) and weight (``w_ptrs`` + affine scale
512
+ ``ws_ptrs``), the weight either a single stream or — under ``GATE_STREAMS`` — the gate|up pair
513
+ (``w_up_ptrs`` + ``ws_up_ptrs``, bumped by the same steps). Value pointers advance only on the
514
+ pointer arm (a descriptor arm re-derives the box K offset from ``k``); scale pointers advance
515
+ only when read affine (``ADVANCE_AS`` / ``ADVANCE_WS`` — swizzled / in-leaf / per-tensor scales
516
+ don't). Pass a dead pointer + step 0 (flag off) for any stream a kernel doesn't carry. Returns
517
+ the six pointers in argument order."""
518
+ if A_MEMORY_MODE == "pointer":
519
+ a_ptrs += a_step
520
+ if W_MEMORY_MODE == "pointer":
521
+ w_ptrs += w_step
522
+ if GATE_STREAMS:
523
+ w_up_ptrs += w_step
524
+ if ADVANCE_AS:
525
+ as_ptrs += as_step
526
+ if ADVANCE_WS:
527
+ ws_ptrs += ws_step
528
+ if GATE_STREAMS:
529
+ ws_up_ptrs += ws_step
530
+ return a_ptrs, as_ptrs, w_ptrs, ws_ptrs, w_up_ptrs, ws_up_ptrs
531
+
532
+
533
+ @triton.jit
534
+ def weight_tile_ptrs(
535
+ base,
536
+ offs_n,
537
+ offs_k,
538
+ stride_n,
539
+ stride_k,
540
+ SWAP_AB: tl.constexpr,
541
+ ):
542
+ """Weight-tile pointers oriented by ``SWAP_AB``. A gate|up tile is a ``2*BN`` row span
543
+ (``offs_n`` carries it); the epilogue splits the projections."""
544
+ return oriented_tile_ptrs(base, offs_n, offs_k, stride_n, stride_k, SWAP_AB)
545
+
546
+
547
+ @triton.jit
548
+ def oriented_tile_ptrs(
549
+ base, offs_rows, offs_k, stride_rows, stride_k, SWAP_AB: tl.constexpr
550
+ ):
551
+ """Operand-tile pointers oriented by whether these rows sit in the MMA M dim
552
+ (``SWAP_AB``, from the weight's viewpoint — activation callers pass the flag inverted):
553
+ rows-major ``[rows, K]`` when they do, else K-major ``[K, rows]``. Only the taken
554
+ constexpr branch compiles, so the divergent shapes never meet. The per-step K-advance
555
+ is identical for both layouts, so the caller advances the returned pointer the same
556
+ way regardless of orientation."""
557
+ if SWAP_AB:
558
+ ptrs = base + (offs_rows[:, None] * stride_rows + offs_k[None, :] * stride_k)
559
+ else:
560
+ ptrs = base + (offs_k[:, None] * stride_k + offs_rows[None, :] * stride_rows)
561
+ return ptrs
562
+
563
+
564
+ @triton.jit
565
+ def operand_tile_ptrs(
566
+ base,
567
+ offs_rows,
568
+ offs_k,
569
+ stride_rows,
570
+ stride_k,
571
+ MEMORY_MODE: tl.constexpr,
572
+ SWAP_AB: tl.constexpr,
573
+ ):
574
+ """Prologue operand-tile pointer, folding the per-operand memory-mode branch: the explicit
575
+ oriented ``[rows,K]``/``[K,rows]`` tile on the pointer arm, or ``base`` as a scalar
576
+ placeholder on a descriptor arm (which reads its box via the descriptor — building the index
577
+ tensor there would only stay live across the K-loop and spill registers). ``SWAP_AB`` is the
578
+ orientation from the weight's viewpoint (activation callers pass it inverted). Single
579
+ return — the arms have divergent types (a ``[rows,K]`` tile vs the scalar base), so an early
580
+ return can't unify them; the constexpr selects one."""
581
+ if MEMORY_MODE == "pointer":
582
+ ptrs = oriented_tile_ptrs(base, offs_rows, offs_k, stride_rows, stride_k, SWAP_AB)
583
+ else:
584
+ ptrs = base
585
+ return ptrs
build/torch-rocm/matmul.py ADDED
@@ -0,0 +1,1970 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import os
17
+
18
+ import torch
19
+ import triton
20
+ import triton.language as tl
21
+ from triton.tools.tensor_descriptor import TensorDescriptor
22
+
23
+ from .bayesian_autotuner import bayesian_autotune
24
+ from .compat import add_op_namespace_prefix, FP8_DTYPE, is_sm10x, NIBBLES_PER_BYTE, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, get_accelerator_autotuning_configs, tl_dtype
25
+ from .descriptors import maybe_descriptor, rebind_bd_descriptors, rebind_mx_descriptors, rebind_weight_only_descriptors
26
+ from .formats import check_activation_format, global_scale_stride, normalize_global_scale, e2m1_as_uint8, is_mx, mx_scale_family, resolve_activation_format, resolve_output_dtype, ue8m0_as_uint8, validate_dense_2d_operands, weight_format
27
+ from .swizzle import swizzle_mx_scales, swizzled_scale_descriptor
28
+ from .quant import MX_ACT_QUANT, fp8_act_quant_block_dynamic, fp8_act_quant_tensor_wide, maybe_act_quant, mxfp8_act_quant, nvfp4_act_quant
29
+ from .loading.scales import apply_global_scale, mx_2d_scale_ptrs
30
+ from .mma import block_dynamic_dot, fp8_dot, mx_compute, mx_weight_only_compute, static_dot
31
+ from .loading.tiles import (
32
+ advance_ptrs,
33
+ load_act_block_dynamic,
34
+ load_act_mx,
35
+ load_act_plain,
36
+ load_act_static,
37
+ load_weight_block_dynamic,
38
+ load_weight_mx,
39
+ load_weight_plain,
40
+ load_weight_static,
41
+ matmul_weight_ptrs,
42
+ operand_tile_descriptor,
43
+ operand_tile_ptrs,
44
+ swizzle_offsets,
45
+ )
46
+ from .epilogue import acc_init, add_bias, bias_strides, gemm_epilogue, store_masked, store_masked_oriented
47
+ from .pruners import PATH_ANCHOR_AXES, fp8_dot_warp_pruner, gate_stacked_tmem_trap_pruner, block_dynamic_2d_warp_spec_pruner, block_dynamic_mma_width_pruner, block_within_dim_pruner, compose_pruners, descriptor_box_pruner, descriptor_needs_prequant_pruner, matched_memory_modes_pruner, mx_2d_swap_scope_pruner, mx_config_pruner, scalar_max_m_pruner, smem_pruner, swizzled_out_bm_pruner, swizzled_scale_config_pruner, warp_spec_compile_guard_pruner
48
+
49
+ # The 2D-grid kernels' L2-locality swizzle depth is derived per-tile inside
50
+ # ``swizzle_offsets`` (see SWIZZLE_GROUP_A_BYTES there) — no per-kernel constant here.
51
+
52
+ # maybe_act_quant crossovers (min rows for offline pre-quant). MX: MEASURED — B200
53
+ # 2D-matmul sweep, graph-timed, H=6144 MXFP8 / H=4096 MXFP4: inline wins only at M=1
54
+ # (33 vs 44us / 20 vs 30us), offline from M=16 for MXFP8 (22 vs 33us), 2-3x by M>=64
55
+ # (MXFP4's M=16 cell marginally favors inline — outweighed); NVFP4 inherits the
56
+ # family gate (not swept — same pack/amax structure as MXFP4). STATIC: inherited
57
+ # estimate, not swept — its inline arm is cheaper elementwise work, so the true
58
+ # crossover is at or above the MX one; the M=1 decode case (the one that matters) is
59
+ # inline either way.
60
+ MX_MATMUL_ACT_PREQUANT_MIN_M = 16
61
+ STATIC_MATMUL_ACT_PREQUANT_MIN_M = 16
62
+ # Block-dynamic has NO gate: offline wins at EVERY M incl. M=1 (24.7 vs 31.0us, isolated
63
+ # arm A/B, graph-timed, H=6144 b128) — the inline arm pays a per-tile fp32 amax+div. The
64
+ # always-offline paths (bd 2D/batched/grouped, fused gate_ups, MX grouped) quantize
65
+ # unconditionally; only the two gates above have a real M=1 inline win.
66
+
67
+
68
+ @bayesian_autotune(
69
+ # tune_block_m: BLOCK_SIZE_M is a config axis. tune_block_n: the N tile is DECOUPLED
70
+ # from the caller's scale granularity (block_n) — a BN=256 tile over 128-wide scale
71
+ # columns halves activation re-reads; any BN is numerically fine (the offs_bn //
72
+ # block_n gather spans or splits scale columns), while BK stays pinned to block_k
73
+ # (activation scale groups are per block_k).
74
+ #
75
+ # Two memory regimes, tuner-routed per (N, K, M) key:
76
+ # - pointer: wins decode / short-K (big tiles starve here — the loop is load-bound).
77
+ # - host-TMA on BOTH operands (A_MEMORY_MODE / B_MEMORY_MODE = descriptor) in the NATURAL
78
+ # orientation (no SWAP_AB — the descriptor box transposes once, in load_weight_tile, to the
79
+ # pointer arm's K-major rhs): wins wide-N compute prefill, feeding the big tiles the pointer
80
+ # arm cannot. WS is load-bearing here: the per-iteration transpose races without it.
81
+ # Emitting both axes lets the tuner route per regime. Verdicts B200 (sm_100) — re-chart on H100.
82
+ get_accelerator_autotuning_configs(
83
+ warp_spec=True,
84
+ tune_block_m=True,
85
+ tune_block_n=True,
86
+ a_memory_modes=("descriptor", "pointer"),
87
+ b_memory_modes=("descriptor", "pointer"),
88
+ pre_hook=rebind_bd_descriptors,
89
+ ),
90
+ # m_bit_length (log2 M bucket) keys the M tile, mirroring mx_dynamic_matmul_kernel; GATE keys the
91
+ # gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space). block_n/block_k
92
+ # (the launch-pinned quant block) gate descriptor-box legality and the K tile — two checkpoints
93
+ # at one shape with different blocks must not share a winner.
94
+ ["N", "K", "m_bit_length", "GATE", "block_n", "block_k"],
95
+ n_trials=100,
96
+ path_anchor_axes=PATH_ANCHOR_AXES,
97
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
98
+ finite_check_args=("C", "Cs"),
99
+ # WS compile guard + descriptor TMA-box limits (256/dim) + shared-memory fit (the big
100
+ # TMA prefill tiles reach the smem ceiling; BM256 BN256 OOMs). The gate|up stack reads via the
101
+ # pointer arm (a contiguous descriptor box can't span the N-apart gate/up rows), so prune the
102
+ # B-descriptor configs under GATE. The implicit dot_scaled arm (UE8M0 As, native-M tile)
103
+ # carries the sm_10x scaled-MMA width cap — GATE at BN=256 would emit a width-512 trap.
104
+ prune_configs_by={
105
+ "early_config_prune": compose_pruners(
106
+ fp8_dot_warp_pruner(),
107
+ warp_spec_compile_guard_pruner(),
108
+ matched_memory_modes_pruner(),
109
+ descriptor_box_pruner("block_k"),
110
+ smem_pruner("block_k"),
111
+ block_dynamic_mma_width_pruner(),
112
+ block_dynamic_2d_warp_spec_pruner(),
113
+ )
114
+ },
115
+ )
116
+ @triton.jit
117
+ def w8a8_block_dynamic_fp8_matmul_kernel(
118
+ A, # (M, K) E4M3 activations (pre-quantized once by the wrapper)
119
+ ADescriptor, # host TMA descriptor over A (M, K), box (BLOCK_SIZE_M, block_k); read iff A_MEMORY_MODE != "pointer"
120
+ As, # (M, K // block_k) per-row, per-K-block activation scales (fp32 or uint8/UE8M0)
121
+ B, # (N, K) FP8 weights
122
+ BDescriptor, # host TMA descriptor over B (N, K), box (BLOCK_SIZE_N, block_k); read iff B_MEMORY_MODE != "pointer"
123
+ Bs, # (N // block_n, K // block_k) weight scales (fp32 or uint8/UE8M0)
124
+ C, # (M, N) output
125
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
126
+ # Shape
127
+ M,
128
+ N,
129
+ K,
130
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
131
+ # Strides
132
+ stride_a_m,
133
+ stride_a_k,
134
+ stride_as_m,
135
+ stride_as_k, # activation-scale group-axis stride: 1 for a row-major (M, K//block_k) grid,
136
+ # but a real stride when the caller hands in a transposed view (the weight side has carried
137
+ # both strides all along; this is the activation side catching up)
138
+ stride_b_k,
139
+ stride_b_n,
140
+ stride_bs_k,
141
+ stride_bs_n,
142
+ stride_c_m,
143
+ stride_c_n,
144
+ stride_bias_e,
145
+ stride_bias_n,
146
+ # Weight-quantization blocks (the caller's block_size); block_k is also the K tile
147
+ # (the activation scale groups are per block_k)
148
+ block_n: tl.constexpr,
149
+ block_k: tl.constexpr,
150
+ # Meta-parameters
151
+ BLOCK_SIZE_M: tl.constexpr,
152
+ BLOCK_SIZE_N: tl.constexpr,
153
+ WARP_SPEC: tl.constexpr = False,
154
+ B_MEMORY_MODE: tl.constexpr = "pointer",
155
+ A_MEMORY_MODE: tl.constexpr = "pointer",
156
+ SWAP_AB: tl.constexpr = False,
157
+ # Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output (bf16 — the
158
+ # requant output formats live on the MX kernel). Every arm folds out at compile time when
159
+ # GATE=False (the plain dense GEMM, unchanged).
160
+ GATE: tl.constexpr = False,
161
+ ACT_FN: tl.constexpr = "silu",
162
+ SWIGLU_ALPHA: tl.constexpr = None,
163
+ SWIGLU_LIMIT: tl.constexpr = None,
164
+ SIMULATE_UNFUSED: tl.constexpr = False,
165
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
166
+ ):
167
+ """Block-scale FP8 GEMM kernel.
168
+
169
+ Computes ``C = A @ B.T``. Activations arrive pre-quantized (one pass in the
170
+ wrapper — the inline per-N-tile quant would repeat N//BN times per element; see the
171
+ grouped kernels). 2D grid with swizzle for L2 cache locality on B.
172
+
173
+ Two operand-feed regimes, tuner-routed (see the decorator): explicit pointers, or
174
+ host-TMA descriptors on both operands. Under the descriptor modes the natural
175
+ orientation loads the ``(BN, BK)`` weight box and transposes it once (in
176
+ ``load_weight_tile``) to the same K-major rhs the pointer arm builds — no ``SWAP_AB``
177
+ (that trans is race-free only under ``WARP_SPEC``). ``SWAP_AB`` remains an
178
+ independent pointer-arm orientation knob (weight tile in the MMA M dim, activation
179
+ loaded transposed via strides); it is implemented but not currently emitted.
180
+
181
+ UE8M0 scales (activations quantized to power-of-two, weights UE8M0) on a native-M tile
182
+ (the MMA M operand is 128-wide: BLOCK_SIZE_M no-swap, BLOCK_SIZE_N swapped) fold the
183
+ 128-group scales into a tcgen05 ``dot_scaled`` MMA — see ``block_dynamic_dot``. fp32
184
+ scales, or a narrow M tile (decode), keep the plain ``tl.dot`` + software rescale.
185
+ """
186
+ USE_DOT_SCALED: tl.constexpr = (As.dtype.element_ty == tl.uint8) and (
187
+ (BLOCK_SIZE_N if SWAP_AB else BLOCK_SIZE_M) >= 128
188
+ )
189
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
190
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, block_k, GATE=GATE
191
+ )
192
+ # Scale pointers index off AFFINE row/col offsets + a bounds mask; the %-wrapped forms
193
+ # (offs_am/offs_bn from swizzle_offsets) drive ONLY the pointer arm's operand tiles
194
+ # (token replication at decode). Keeping scales affine avoids the non-affine gather
195
+ # the wrap induces (~460us / 18pp at prefill).
196
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
197
+ n_rows = 2 * N if GATE else N
198
+ offs_am_lin = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
199
+ offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
200
+ as_ptrs = As + offs_am_lin * stride_as_m
201
+ as_mask = offs_am_lin < M
202
+ bs_mask = offs_bn_lin < n_rows
203
+ a_descriptor = operand_tile_descriptor(
204
+ ADescriptor, A, M, K, stride_a_m, stride_a_k, BLOCK_SIZE_M, block_k, A_MEMORY_MODE
205
+ )
206
+ b_descriptor = operand_tile_descriptor(
207
+ BDescriptor,
208
+ B,
209
+ N,
210
+ K,
211
+ stride_b_n,
212
+ stride_b_k,
213
+ BLOCK_SIZE_N,
214
+ block_k,
215
+ B_MEMORY_MODE, GATE,
216
+ )
217
+ # Explicit operand-tile pointers on the pointer arm; a scalar placeholder on a descriptor
218
+ # arm (which reads its box via the descriptor — the [BM,BK]/[BK,BN] index tensor would stay
219
+ # live across the K-loop for nothing and spill registers, ~460us / 20pp MFU at wide-N prefill).
220
+ # Under GATE the weight is the stacked (2N, K) gate|up tile (matmul_weight_ptrs; pointer-only, the
221
+ # descriptor box can't span the N-apart rows), and its per-weight-row block scale is a 2*BN gather
222
+ # (gate rows [0,N), up rows offset by N//block_n scale blocks) — the block_dynamic_dot broadcast
223
+ # then folds it per row exactly as in the dense case.
224
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, not SWAP_AB)
225
+ b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB)
226
+ # the scale-index gather decouples the tile from the scale grid: a wide tile spans several
227
+ # scale columns, a narrow one shares a column
228
+ bs_ptrs = Bs + (offs_bn_lin // block_n) * stride_bs_n
229
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
230
+
231
+ for k in tl.range(0, tl.cdiv(K, block_k), warp_specialize=WARP_SPEC):
232
+ a, a_s = load_act_block_dynamic(
233
+ a_ptrs, as_ptrs, None, as_mask, a_descriptor, pid_m * BLOCK_SIZE_M, k * block_k,
234
+ 0, 0, A_MEMORY_MODE, False, False, SWAP_AB,
235
+ )
236
+ b, b_s = load_weight_block_dynamic(
237
+ b_ptrs, b_descriptor, bs_ptrs, bs_mask, 0, 0, pid_n * n_width, k * block_k, 0, 0,
238
+ GATE, False, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, block_k,
239
+ )
240
+ accumulator = block_dynamic_dot(
241
+ accumulator, a, a_s, b, b_s, block_k, SWAP_AB, USE_DOT_SCALED, False
242
+ )
243
+ a_ptrs, as_ptrs, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
244
+ a_ptrs, as_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
245
+ block_k * stride_a_k, stride_as_k, block_k * stride_b_k, stride_bs_k,
246
+ A_MEMORY_MODE, B_MEMORY_MODE, True, True, False,
247
+ )
248
+
249
+ if GATE:
250
+ gemm_epilogue(
251
+ C, None, accumulator, offs_am_lin, pid_n, pid_m, as_mask,
252
+ stride_c_m, stride_c_n, 0, 0,
253
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, block_k,
254
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
255
+ COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, N_COLS=N,
256
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
257
+ )
258
+ else:
259
+ accumulator = add_bias(
260
+ accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
261
+ BLOCK_SIZE_N, N, SWAP_AB,
262
+ )
263
+ store_masked_oriented(
264
+ C,
265
+ accumulator,
266
+ pid_m,
267
+ pid_n,
268
+ M,
269
+ N,
270
+ stride_c_m,
271
+ stride_c_n,
272
+ BLOCK_SIZE_M,
273
+ BLOCK_SIZE_N,
274
+ SWAP_AB,
275
+ )
276
+
277
+
278
+ @bayesian_autotune(
279
+ # tune_block_m mirrors the block-dynamic kernel (adaptive BM=128 cost it 17% at prefill).
280
+ get_accelerator_autotuning_configs(
281
+ tune_block_nk=True, warp_spec=True, tune_block_m=True
282
+ ),
283
+ # GATE keys the gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space).
284
+ ["N", "K", "m_bit_length", "GATE"],
285
+ n_trials=100,
286
+ path_anchor_axes=PATH_ANCHOR_AXES,
287
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
288
+ finite_check_args=("C", "Cs"),
289
+ # block_k is a tuned axis and the loop below is maskless — veto non-dividing BKs;
290
+ # WS is a pure perf axis here (non-WS is the validated state), compile-guarded.
291
+ prune_configs_by={
292
+ "early_config_prune": compose_pruners(
293
+ fp8_dot_warp_pruner(),
294
+ block_within_dim_pruner("K"),
295
+ warp_spec_compile_guard_pruner(),
296
+ )
297
+ },
298
+ )
299
+ @triton.jit
300
+ def w8a8_tensor_dynamic_fp8_matmul_kernel(
301
+ A, # (M, K) pre-quantized FP8 activations
302
+ As, # (M,) per-token activation scales
303
+ B, # (N, K) FP8 weights
304
+ Bs, # scalar/(1,) per-tensor weight scale
305
+ C, # (M, N) output
306
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
307
+ # Shape
308
+ M,
309
+ N,
310
+ K,
311
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
312
+ # Strides
313
+ stride_a_m,
314
+ stride_a_k,
315
+ stride_as_m,
316
+ stride_b_k,
317
+ stride_b_n,
318
+ stride_c_m,
319
+ stride_c_n,
320
+ stride_bias_e,
321
+ stride_bias_n,
322
+ # Meta-parameters
323
+ BLOCK_SIZE_M: tl.constexpr,
324
+ BLOCK_SIZE_N: tl.constexpr,
325
+ BLOCK_SIZE_K: tl.constexpr,
326
+ WARP_SPEC: tl.constexpr = False,
327
+ # Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
328
+ # folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
329
+ GATE: tl.constexpr = False,
330
+ ACT_FN: tl.constexpr = "silu",
331
+ SWIGLU_ALPHA: tl.constexpr = None,
332
+ SWIGLU_LIMIT: tl.constexpr = None,
333
+ SIMULATE_UNFUSED: tl.constexpr = False,
334
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
335
+ ):
336
+ """Tensor-scale FP8 GEMM kernel.
337
+
338
+ Computes ``C = A @ B.T`` with one activation scale per row and one
339
+ weight scale for the full matrix.
340
+ Uses a 2D grid with swizzle for L2 cache locality on B tiles.
341
+ """
342
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
343
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
344
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GATE=GATE
345
+ )
346
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, "pointer", True)
347
+ # Under GATE the weight is the stacked (2N, K) gate|up tile; one per-tensor weight scale
348
+ # covers both projections, applied (with the per-row act scale) before the GLU split.
349
+ b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, "pointer")
350
+
351
+ a_s = tl.load(As + offs_am * stride_as_m)
352
+ b_s = tl.load(Bs)
353
+
354
+ # Accumulate raw dot products, apply scales once after the loop.
355
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
356
+ for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
357
+ a, _as = load_act_plain(a_ptrs, a_ptrs, 0, 0, None, 0, "pointer", False)
358
+ b, _bs = load_weight_plain(
359
+ b_ptrs, b_ptrs, 0, pid_n * n_width, k * BLOCK_SIZE_K,
360
+ GATE, False, "pointer", False, BLOCK_SIZE_N, BLOCK_SIZE_K,
361
+ )
362
+ accumulator = accumulator + fp8_dot(a, b, False, BLOCK_SIZE_K)
363
+ a_ptrs, _asp, b_ptrs, _bsp, _, _ = advance_ptrs(
364
+ a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
365
+ BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
366
+ "pointer", "pointer", False, False, False,
367
+ )
368
+
369
+ accumulator = accumulator * a_s[:, None] * b_s
370
+
371
+ if GATE:
372
+ out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
373
+ gemm_epilogue(
374
+ C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
375
+ stride_c_m, stride_c_n, 0, 0,
376
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
377
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
378
+ COMPUTE_MODE="dot", N_COLS=N,
379
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
380
+ )
381
+ else:
382
+ accumulator = add_bias(
383
+ accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
384
+ BLOCK_SIZE_N, N, False,
385
+ )
386
+ store_masked(
387
+ C,
388
+ accumulator,
389
+ pid_m,
390
+ pid_n,
391
+ M,
392
+ N,
393
+ stride_c_m,
394
+ stride_c_n,
395
+ BLOCK_SIZE_M,
396
+ BLOCK_SIZE_N,
397
+ )
398
+
399
+
400
+ @bayesian_autotune(
401
+ # tune_block_m + tune_block_n + WARP_SPEC + host-TMA memory modes mirror the block-dynamic
402
+ # kernel above (same 2D swizzle loop physics). Both A/B memory axes are emitted and the tuner
403
+ # routes descriptor vs pointer per (N, K, M) key; the A descriptor is read only on the offline
404
+ # (pre-quantized fp8 A) arm — descriptor_needs_prequant_pruner keeps the M<threshold inline arm
405
+ # pointer-only. The N tile is decoupled from the scale granularity.
406
+ get_accelerator_autotuning_configs(
407
+ warp_spec=True,
408
+ tune_block_m=True,
409
+ tune_block_n=True,
410
+ a_memory_modes=("descriptor", "pointer"),
411
+ b_memory_modes=("descriptor", "pointer"),
412
+ pre_hook=rebind_bd_descriptors,
413
+ ),
414
+ # GATE keys the gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space);
415
+ # block_n/block_k (the launch-pinned quant block) gate descriptor-box legality and the K tile.
416
+ ["N", "K", "m_bit_length", "GATE", "block_n", "block_k"],
417
+ n_trials=100,
418
+ path_anchor_axes=PATH_ANCHOR_AXES,
419
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
420
+ finite_check_args=("C", "Cs"),
421
+ # The gate|up stack reads via the pointer arm (a contiguous descriptor box can't span the
422
+ # N-apart gate/up rows), so prune the B-descriptor configs under GATE.
423
+ prune_configs_by={
424
+ "early_config_prune": compose_pruners(
425
+ fp8_dot_warp_pruner(),
426
+ warp_spec_compile_guard_pruner(),
427
+ descriptor_needs_prequant_pruner(),
428
+ matched_memory_modes_pruner(),
429
+ descriptor_box_pruner("block_k"),
430
+ smem_pruner("block_k"),
431
+ )
432
+ },
433
+ )
434
+ @triton.jit
435
+ def w8a8_block_static_fp8_matmul_kernel(
436
+ A, # (M, K) E4M3 activations (pre-quantized against the static scale by the wrapper)
437
+ ADescriptor, # host TMA descriptor over A (M, K), box (BM, block_k); read iff A_MEMORY_MODE != "pointer"
438
+ As, # scalar — static per-tensor activation scale (calibration-time)
439
+ B, # (N, K) FP8 weights
440
+ BDescriptor, # host TMA descriptor over B (N, K), box (BN, block_k); read iff B_MEMORY_MODE != "pointer"
441
+ Bs, # (N // block_n, K // block_k) weight scales (fp32 or uint8/UE8M0)
442
+ C, # (M, N) output
443
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
444
+ # Shape
445
+ M,
446
+ N,
447
+ K,
448
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
449
+ # Strides
450
+ stride_a_m,
451
+ stride_a_k,
452
+ stride_b_k,
453
+ stride_b_n,
454
+ stride_bs_k,
455
+ stride_bs_n,
456
+ stride_c_m,
457
+ stride_c_n,
458
+ stride_bias_e,
459
+ stride_bias_n,
460
+ # Weight-quantization blocks (see the block-dynamic kernel)
461
+ block_n: tl.constexpr,
462
+ block_k: tl.constexpr,
463
+ # Meta-parameters
464
+ BLOCK_SIZE_M: tl.constexpr,
465
+ BLOCK_SIZE_N: tl.constexpr,
466
+ WARP_SPEC: tl.constexpr = False,
467
+ A_MEMORY_MODE: tl.constexpr = "pointer",
468
+ B_MEMORY_MODE: tl.constexpr = "pointer",
469
+ # Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
470
+ # folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
471
+ GATE: tl.constexpr = False,
472
+ ACT_FN: tl.constexpr = "silu",
473
+ SWIGLU_ALPHA: tl.constexpr = None,
474
+ SWIGLU_LIMIT: tl.constexpr = None,
475
+ SIMULATE_UNFUSED: tl.constexpr = False,
476
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
477
+ ):
478
+ """Block-scale FP8 GEMM with static (per-tensor) activation scale.
479
+
480
+ ``A`` arrives pre-quantized (one elementwise ``(A / As).to(fp8)`` pass in the
481
+ wrapper — an inline division would repeat per N-tile). Per-block weight scales apply
482
+ per-K-tile during accumulation; the scalar activation scale is applied once at
483
+ the end.
484
+ """
485
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
486
+ n_rows = 2 * N if GATE else N
487
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
488
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, block_k, GATE=GATE
489
+ )
490
+ a_descriptor = operand_tile_descriptor(
491
+ ADescriptor, A, M, K, stride_a_m, stride_a_k, BLOCK_SIZE_M, block_k, A_MEMORY_MODE
492
+ )
493
+ b_descriptor = operand_tile_descriptor(
494
+ BDescriptor, B, N, K, stride_b_n, stride_b_k, BLOCK_SIZE_N, block_k, B_MEMORY_MODE, GATE,
495
+ )
496
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
497
+ # Weight-scale index off the AFFINE column offset + a bounds mask (the %-wrapped offs_bn
498
+ # from swizzle_offsets drives only the pointer operand tile); an affine gather avoids the
499
+ # non-affine scale read the wrap induces, matching the block-dynamic kernel above.
500
+ b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE)
501
+ offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
502
+ bs_ptrs = Bs + (offs_bn_lin // block_n) * stride_bs_n
503
+ bs_mask = offs_bn_lin < n_rows
504
+ a_s_static = tl.load(As)
505
+
506
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
507
+ for k in tl.range(0, tl.cdiv(K, block_k), warp_specialize=WARP_SPEC):
508
+ a, _as = load_act_static(
509
+ a_ptrs, a_descriptor, pid_m * BLOCK_SIZE_M, k * block_k, None, 0, a_s_static,
510
+ A_MEMORY_MODE, False,
511
+ )
512
+ b, b_s = load_weight_static(
513
+ b_ptrs, b_descriptor, bs_ptrs, bs_mask, 0, 0, pid_n * n_width, k * block_k, 0, 0,
514
+ GATE, False, B_MEMORY_MODE, False, BLOCK_SIZE_N, block_k,
515
+ )
516
+ accumulator = static_dot(accumulator, a, b, b_s, False, block_k, False)
517
+ a_ptrs, _asp, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
518
+ a_ptrs, a_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
519
+ block_k * stride_a_k, 0, block_k * stride_b_k, stride_bs_k,
520
+ A_MEMORY_MODE, B_MEMORY_MODE, False, True, False,
521
+ )
522
+
523
+ accumulator = accumulator * a_s_static
524
+ if GATE:
525
+ out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
526
+ gemm_epilogue(
527
+ C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
528
+ stride_c_m, stride_c_n, 0, 0,
529
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, block_k,
530
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
531
+ COMPUTE_MODE="dot", N_COLS=N,
532
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
533
+ )
534
+ else:
535
+ accumulator = add_bias(
536
+ accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
537
+ BLOCK_SIZE_N, N, False,
538
+ )
539
+ store_masked(
540
+ C,
541
+ accumulator,
542
+ pid_m,
543
+ pid_n,
544
+ M,
545
+ N,
546
+ stride_c_m,
547
+ stride_c_n,
548
+ BLOCK_SIZE_M,
549
+ BLOCK_SIZE_N,
550
+ )
551
+
552
+
553
+ @bayesian_autotune(
554
+ # tune_block_m: BLOCK_SIZE_M becomes a config axis, so the tuner sizes the M tile
555
+ # per workload — small at decode, large at prefill.
556
+ # scalar is in the mode set for M=1 decode (attn projection): it avoids the MMA M->16 pad
557
+ # that held MXFP8 attn decode 35% over block-dynamic at identical weight bytes; fp4-scalar
558
+ # is dropped by the pruner (ALU-bound unpack). swap_ab emits the BM=1 swap decode rows,
559
+ # scoped by mx_2d_swap_scope_pruner to E4M3-scale (NVFP4) decode — its ONLY native M=1
560
+ # arm (dot_scaled has no sub-M=128 E4M3 form; scalar is unpack-bound: 60-142µs vs swap's
561
+ # 22-50µs on the GLM attn shape, 2026-08-05). UE8M0 formats keep swap OFF: an 18-cell
562
+ # forced-swap sweep (cudagraph, M3+dsv4 decode) showed swap losing there (M3 attn −38%).
563
+ get_accelerator_autotuning_configs(
564
+ mx=True,
565
+ tune_block_nk=True,
566
+ compute_modes=("dot_scaled", "dot", "scalar"),
567
+ tune_block_m=True,
568
+ swap_ab=True,
569
+ # host-TMA on both operands, tuner-picked vs pointers (measured +14-17pp: mxfp8
570
+ # 54->71%, W4A8 50->67% via mixed A=ptr/B=desc, W4A4 +14%). Mixed A/B modes KEPT
571
+ # (unlike dense bd): A=pointer + B=descriptor wins the mid-M band.
572
+ a_memory_modes=("descriptor", "pointer"),
573
+ b_memory_modes=("descriptor", "pointer"),
574
+ pre_hook=rebind_mx_descriptors,
575
+ # WS tuner axis: dot_scaled + WS + TMA compiles + wins at prefill (1918 -> ~1995 @M=4k);
576
+ # warp_spec_compile_guard_pruner keeps it to num_warps%4==0, BM>=64.
577
+ warp_spec=True,
578
+ ),
579
+ # the MXFP4/MXFP8 split keys itself — the tuner appends every tensor arg's dtype to
580
+ # its cache key (memory and disk);
581
+ # m_bit_length (log2 M bucket) keys the M tile — the winner keeps shifting with M well past the
582
+ # BM ceiling and it is NOT noise: cross-applying configs (N=K=4096) costs +62% at M=128 and +245%
583
+ # at M=4096 (the thin M=128 tile can't saturate the wide GEMM), so don't collapse the buckets.
584
+ # ACTIVATION_FORMAT keys the inline act-quant grid: A stays raw bf16 under every
585
+ # format below the pre-quant M threshold, so the dtype-appended key can't
586
+ # split W4A8 from W4A4 itself. OUTPUT_FORMAT/SWIZZLED_OUT key the requant epilogue
587
+ # explicitly (today the C/Cs dtypes split them incidentally; a descriptor Cs has no
588
+ # dtype token, so don't lean on the append).
589
+ ["N", "K", "m_bit_length", "ACTIVATION_FORMAT", "SWIZZLED_SCALES", "GATE", "OUTPUT_FORMAT", "SWIZZLED_OUT"],
590
+ n_trials=100,
591
+ path_anchor_axes=PATH_ANCHOR_AXES,
592
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
593
+ finite_check_args=("C", "Cs"),
594
+ # BK-within-K veto (the loop loads are unmasked) + the sm_10x dot_scaled shape guards
595
+ # + scalar restricted to decode-sized M (a BM=1 GEVM at prefill is TPE poison) + the
596
+ # gate|up stack reads via the pointer arm (the descriptor's one contiguous box can't span
597
+ # the N-apart gate/up rows — a (2,N,K) box is a follow-up), so prune descriptor under GATE.
598
+ prune_configs_by={
599
+ "early_config_prune": compose_pruners(
600
+ gate_stacked_tmem_trap_pruner(),
601
+ swizzled_out_bm_pruner(),
602
+ mx_config_pruner("K"),
603
+ mx_2d_swap_scope_pruner(),
604
+ scalar_max_m_pruner("M"),
605
+ smem_pruner(),
606
+ descriptor_box_pruner("BLOCK_SIZE_K"),
607
+ swizzled_scale_config_pruner(),
608
+ warp_spec_compile_guard_pruner(),
609
+ )
610
+ },
611
+ )
612
+ @triton.jit
613
+ def mx_dynamic_matmul_kernel(
614
+ A, # (M, K) activations: E4M3 (pre-quantized) or raw bf16/fp16 (quantized inline)
615
+ ADescriptor, # host TMA descriptor over A (M, K_bytes), box (BM, BK_bytes); read iff A_MEMORY_MODE != "pointer"
616
+ As, # activation scales: SWIZZLE_32_4_4 (swizzled arm — bulk via ASDescriptor at BM=128, or this pointer for the BM<128 gather) or (M, K // 32) UE8M0 affine
617
+ ASDescriptor, # host TMA descriptor over the SWIZZLE_32_4_4 A scales (BM=128 bulk load); read iff SWIZZLED_SCALES
618
+ B, # (N, K) E4M3 (MXFP8) or (N, K // 2) packed E2M1 (MXFP4) weights
619
+ BDescriptor, # host TMA descriptor over B (N, K_bytes), box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
620
+ Bs, # weight scales: SWIZZLE_32_4_4 (swizzled arm — bulk via BSDescriptor at BN=128, or this pointer for the BN<128 gather) or (N, K // SCALE_GROUP_K) affine
621
+ BSDescriptor, # host TMA descriptor over the SWIZZLE_32_4_4 B scales (BN=128 bulk load); read iff SWIZZLED_SCALES
622
+ C, # (M, N) output (the GLU intermediate under GATE)
623
+ Cs, # (M, N // group) row-major requant output scale — written iff OUTPUT_FORMAT and not SWIZZLED_OUT
624
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
625
+ CSDescriptor, # SWIZZLE_32_4_4 requant-scale descriptor — written iff SWIZZLED_OUT (dummy else), like AS/BS
626
+ AsGlobal, # (1,) fp32 NVFP4 activation global g_a — SOLELY normalizes the inline raw-A quant (A/g_a); read iff not None
627
+ AsBsGlobal, # (1,) fp32 NVFP4 WEIGHT global g_b — recovers on the accumulator together with AsGlobal (in-register product); read iff not None
628
+ CsGlobal, # (1,) fp32 NVFP4 output global (next proj's provided input_scale); normalizes the requant; read iff not None
629
+ # Shape
630
+ M,
631
+ N,
632
+ K,
633
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
634
+ # Strides
635
+ stride_a_m,
636
+ stride_a_k,
637
+ stride_as_m,
638
+ stride_as_k, # activation-scale group-axis stride, in GROUPS (see the affine advance below):
639
+ # 1 for the row-major (M, K // SCALE_GROUP_K) grid, a real stride under a transposed view.
640
+ # Dead on the swizzled arm, which re-derives its block offsets instead of walking pointers.
641
+ stride_b_k,
642
+ stride_b_n,
643
+ stride_bs_k,
644
+ stride_bs_n,
645
+ stride_c_m,
646
+ stride_c_n,
647
+ stride_cs_m, # requant output-scale strides (dead unless OUTPUT_FORMAT)
648
+ stride_cs_n,
649
+ stride_bias_e,
650
+ stride_bias_n,
651
+ stride_as_global, # expert stride of AsGlobal (0 broadcasts a shared scalar)
652
+ stride_bs_global, # expert stride of AsBsGlobal (``global_scale_stride``)
653
+ stride_cs_global, # expert stride of CsGlobal (0 broadcasts a shared scalar)
654
+ # Meta-parameters
655
+ BLOCK_SIZE_M: tl.constexpr,
656
+ BLOCK_SIZE_N: tl.constexpr,
657
+ BLOCK_SIZE_K: tl.constexpr,
658
+ SCALE_GROUP_K: tl.constexpr,
659
+ COMPUTE_MODE: tl.constexpr,
660
+ A_MEMORY_MODE: tl.constexpr = "pointer",
661
+ B_MEMORY_MODE: tl.constexpr = "pointer",
662
+ ACTIVATION_FORMAT: tl.constexpr = "mxfp8",
663
+ SWIZZLED_SCALES: tl.constexpr = False, # scales pre-swizzled (5D weight + matching acts); else affine/inline
664
+ SWIZZLED_OUT: tl.constexpr = False, # requant emits Cs in SWIZZLE_32_4_4 (CSDescriptor); single source: wrapper
665
+ # Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
666
+ # folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
667
+ GATE: tl.constexpr = False,
668
+ ACT_FN: tl.constexpr = "silu",
669
+ SWIGLU_ALPHA: tl.constexpr = None,
670
+ SWIGLU_LIMIT: tl.constexpr = None,
671
+ OUTPUT_FORMAT: tl.constexpr = None,
672
+ SIMULATE_UNFUSED: tl.constexpr = False,
673
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
674
+ WARP_SPEC: tl.constexpr = False, # tuner axis; +4% at prefill (guard: num_warps%4, BM>=64)
675
+ SWAP_AB: tl.constexpr = False, # swapped decode orientation (BM=1); scoped by mx_2d_swap_scope_pruner
676
+ ):
677
+ """Unified MXFP4/MXFP8 (W4A8/W8A8; a ``uint8`` ``A`` is packed E2M1 — W4A4) GEMM.
678
+
679
+ ``C = A @ B.T``. ``A``'s dtype picks the activation form (compile-time folded):
680
+ pre-quantized E4M3 + UE8M0 group-32 scales (one wrapper pass — the inline
681
+ per-N-tile quant re-ran N//BN times per element, ~2x at prefill) vs raw bf16/fp16
682
+ quantized inline (small M: the GEMM is weight-bandwidth-bound with idle ALU, so
683
+ inline is free and a separate quant kernel only adds latency; both forms are
684
+ bit-exact, same group-32 boundaries). Each operand's format is its dtype (``uint8``
685
+ = packed E2M1, two values per byte; else E4M3). ``COMPUTE_MODE`` picks the MMA:
686
+ ``tl.dot_scaled`` (native M=128 scaled MMA) vs fp8 ``tl.dot`` + per-group software
687
+ rescale (wins at decode; FP4 unpacks E2M1->E4M3 first — lossless). 2D grid with
688
+ swizzle for L2 reuse.
689
+ """
690
+ # uint8 A = packed-E2M1 activations (W4A4 — the dtype IS the activation format)
691
+ ACT_VALUES_PER_BYTE: tl.constexpr = 2 if A.dtype.element_ty == tl.uint8 else 1
692
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr = 2 if B.dtype.element_ty == tl.uint8 else 1
693
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
694
+ SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
695
+ offs_ka = tl.arange(0, BLOCK_SIZE_K // ACT_VALUES_PER_BYTE)
696
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
697
+ # packed-fp4 weights (WEIGHT_VALUES_PER_BYTE==2) route swizzle_offsets to full L2 grouping
698
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
699
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, WEIGHT_VALUES_PER_BYTE, GATE=GATE
700
+ )
701
+ # Scales read affine off row/col offsets + bounds masks (the %-wrapped operand offsets would
702
+ # make the scale load a non-affine gather); the SWIZZLED arm reads them via the SA/BS
703
+ # descriptors in the loop instead, so its pointer tiles are dead (base scalars + null masks).
704
+ as_ptrs, bs_ptrs, as_mask, bs_mask = mx_2d_scale_ptrs(
705
+ As, Bs, pid_m, pid_n, M, N, stride_as_m, stride_bs_n, stride_bs_k,
706
+ BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_COLS, SWIZZLED_SCALES, GATE,
707
+ )
708
+ # Operand tiles: activation + the (GATE-stacked-aware) weight, both single leaf calls (the
709
+ # descriptor-vs-pointer and gate|up-stack branches fold inside the leaves). The swap arm
710
+ # loads both naturally — mx_compute reorients at the dot under SWAP_AB, flattening the token tile.
711
+ if SWAP_AB:
712
+ tl.static_assert(
713
+ BLOCK_SIZE_M == 1, "the mx swap arm flattens the single-token tile"
714
+ )
715
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_ka, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
716
+ b_ptrs = matmul_weight_ptrs(
717
+ B, offs_bn, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB
718
+ )
719
+
720
+ accumulator = acc_init(COMPUTE_MODE if SWAP_AB else "dot", BLOCK_SIZE_M, n_width, SWAP_AB)
721
+ for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
722
+ a, a_s = load_act_mx(
723
+ a_ptrs, as_ptrs, AsGlobal, 0, None, as_mask, ADescriptor, pid_m * BLOCK_SIZE_M,
724
+ k * (BLOCK_SIZE_K // ACT_VALUES_PER_BYTE), ASDescriptor, As, 0, 0, pid_m, k, M, K,
725
+ A_MEMORY_MODE, False, False, SWIZZLED_SCALES,
726
+ BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, ACTIVATION_FORMAT,
727
+ )
728
+ b, b_s = load_weight_mx(
729
+ b_ptrs, BDescriptor, bs_ptrs, bs_mask, BSDescriptor, Bs, 0, pid_n * n_width,
730
+ k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE), 0, 0, pid_n, k, N, K,
731
+ 0, stride_bs_n, stride_bs_k,
732
+ GATE, False, False, B_MEMORY_MODE, SWAP_AB, SWIZZLED_SCALES,
733
+ BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
734
+ )
735
+ accumulator = mx_compute(
736
+ accumulator, a, a_s, b, b_s, COMPUTE_MODE,
737
+ n_width, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB,
738
+ )
739
+ a_ptrs, as_ptrs, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
740
+ a_ptrs, as_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
741
+ (BLOCK_SIZE_K // ACT_VALUES_PER_BYTE) * stride_a_k,
742
+ (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_as_k,
743
+ (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k,
744
+ (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_bs_k,
745
+ A_MEMORY_MODE, B_MEMORY_MODE,
746
+ not SWIZZLED_SCALES, (not SWIZZLED_SCALES) and not GATE, False,
747
+ )
748
+
749
+ # NVFP4 two-level: block e4m3 scales rode through dot_scaled; recover the combined per-tensor
750
+ # one epilogue for both arms: GATE splits+SwiGLUs (+ optional requant), plain casts+stores.
751
+ # affine output rows (swizzle's offs_am is %-wrapped — the store scatters to real rows);
752
+ # N_COLS masks the 2D-dense column tail.
753
+ out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
754
+ gemm_epilogue(
755
+ C, Cs, accumulator, out_row, pid_n, pid_m, out_row < M,
756
+ stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
757
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, SCALE_GROUP_K,
758
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
759
+ COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, N_COLS=N, SWIZZLED_OUT=SWIZZLED_OUT,
760
+ CSDescriptor=CSDescriptor, CsGlobal=CsGlobal, stride_cs_global=stride_cs_global,
761
+ GlobalScale=AsBsGlobal,
762
+ stride_global_e=stride_bs_global,
763
+ GlobalScaleA=AsGlobal,
764
+ stride_global_a=stride_as_global,
765
+ global_row=0,
766
+ Bias=Bias,
767
+ stride_bias_e=stride_bias_e,
768
+ stride_bias_n=stride_bias_n,
769
+ )
770
+
771
+
772
+ @bayesian_autotune(
773
+ # weight-only is a plain bf16 dot (the fp4/fp8 weight is upcast per-group in-loop), tuned like the
774
+ # full-precision kernel — tile + WS — PLUS a host-TMA memory-mode axis on both operands
775
+ # (tuner-routed vs pointers, like mx_dynamic): the descriptor box feeds the wide-N prefill tiles
776
+ # the pointer arm starves; pointer wins decode / short-K. Scales stay affine (pointer) — weight-only
777
+ # never swizzles — so the descriptor covers only the operand values. BK spans any number of
778
+ # groups (the dequant is in-tile), so no BK==group constraint.
779
+ get_accelerator_autotuning_configs(
780
+ tune_block_nk=True,
781
+ tune_block_m=True,
782
+ warp_spec=True,
783
+ compute_modes=("dot", "dot_scaled"),
784
+ a_memory_modes=("descriptor", "pointer"),
785
+ b_memory_modes=("descriptor", "pointer"),
786
+ pre_hook=rebind_weight_only_descriptors,
787
+ ),
788
+ ["N", "K", "m_bit_length", "GATE"],
789
+ n_trials=100,
790
+ path_anchor_axes=PATH_ANCHOR_AXES,
791
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
792
+ finite_check_args=("C", "Cs"),
793
+ # WS compile guard + descriptor TMA-box limits (256/dim) + smem fit; the gate|up stack reads via
794
+ # the pointer arm (a contiguous box can't span the N-apart gate/up rows), so prune B-descriptor
795
+ # under GATE.
796
+ prune_configs_by={
797
+ "early_config_prune": compose_pruners(
798
+ block_within_dim_pruner("K"),
799
+ mx_config_pruner("K", "N"), # dot_scaled shape gates
800
+ warp_spec_compile_guard_pruner(),
801
+ descriptor_box_pruner("BLOCK_SIZE_K"),
802
+ smem_pruner("BLOCK_SIZE_K"),
803
+ )
804
+ },
805
+ )
806
+ @triton.jit
807
+ def mx_weight_only_matmul_2d_kernel(
808
+ A, # (M, K) raw BF16/FP16 activations — NOT quantized
809
+ ADescriptor, # host TMA descriptor over A (M, K), box (BM, BK); read iff A_MEMORY_MODE != "pointer"
810
+ B, # (N, K[/2]) MXFP4/NVFP4 (packed E2M1) / MXFP8 (E4M3) weights; under GATE the (2N, ...) gate|up stack
811
+ BDescriptor, # host TMA descriptor over B (N, K_bytes), box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
812
+ Bs, # (N, K // SCALE_GROUP_K) group scales — UE8M0 (MX) or E4M3 (NVFP4)
813
+ BsGlobal, # scalar fp32 NVFP4 per-tensor global — recovers on the accumulator; read iff not None
814
+ C, # (M, N) output (GLU intermediate under GATE)
815
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
816
+ M,
817
+ N,
818
+ K,
819
+ m_bit_length, # autotune key only (log2 M bucket)
820
+ stride_a_m,
821
+ stride_a_k,
822
+ stride_b_n,
823
+ stride_b_k,
824
+ stride_bs_n,
825
+ stride_bs_k,
826
+ stride_c_m,
827
+ stride_c_n,
828
+ stride_bias_e,
829
+ stride_bias_n,
830
+ stride_bs_global, # expert stride of BsGlobal (``global_scale_stride``)
831
+ BLOCK_SIZE_M: tl.constexpr,
832
+ BLOCK_SIZE_N: tl.constexpr,
833
+ BLOCK_SIZE_K: tl.constexpr,
834
+ SCALE_GROUP_K: tl.constexpr,
835
+ WEIGHT_VALUES_PER_BYTE: tl.constexpr,
836
+ A_MEMORY_MODE: tl.constexpr = "pointer",
837
+ B_MEMORY_MODE: tl.constexpr = "pointer",
838
+ COMPUTE_MODE: tl.constexpr = "dot",
839
+ WARP_SPEC: tl.constexpr = False,
840
+ GATE: tl.constexpr = False,
841
+ ACT_FN: tl.constexpr = "silu",
842
+ SWIGLU_ALPHA: tl.constexpr = None,
843
+ SWIGLU_LIMIT: tl.constexpr = None,
844
+ SIMULATE_UNFUSED: tl.constexpr = False,
845
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
846
+ ):
847
+ """weight-only dense matmul: ``C = A @ B.T`` with a RAW bf16 activation and an MXFP4/MXFP8 weight
848
+ upcast to bf16 in-loop (unpack E2M1->E4M3, per-group group-scale multiply), then a plain bf16
849
+ ``tl.dot``. This is the ``matmul_ogs`` format (fp4/fp8 weight bytes kept in memory, bf16 acts,
850
+ weight upcast in-MMA) — no activation quant, no ``dot_scaled``. Pointer/affine scales only.
851
+ ``GATE`` loads the stacked (2N, K) gate|up weight as one ``[BK, 2*BN]`` dot + applies the GLU."""
852
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
853
+ n_rows = 2 * N if GATE else N
854
+ SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
855
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
856
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
857
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, WEIGHT_VALUES_PER_BYTE, GATE=GATE
858
+ )
859
+ # Only the weight scale is needed (bf16 acts carry no scale). Build it N-clamped + UNMASKED
860
+ # (bs_mask=None): valid columns (offs_bn_lin < N) read their real scale; the tail reads a clamped
861
+ # in-bounds scale that the epilogue masks off (N_COLS / store_masked). Dropping the other=0.0
862
+ # bounds mask is what lets the K-loop warp-specialization-lower (the masked load's fill constant
863
+ # is the WS partitioner blocker). GATE routes its stacked gate|up scale through the per-expert
864
+ # leaf, not these ptrs, so this only governs the plain dense arm.
865
+ offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
866
+ bs_ptrs = Bs + (
867
+ tl.minimum(offs_bn_lin, n_rows - 1)[:, None] * stride_bs_n
868
+ + tl.arange(0, SCALE_COLS)[None, :] * stride_bs_k
869
+ )
870
+ bs_mask = None
871
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
872
+ b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE)
873
+
874
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
875
+ for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
876
+ # raw bf16 [BM, BK]: pointer tile or the host-TMA box (no scale — the second slot is dead;
877
+ # a distinct name, not `_`, which is already bound pre-loop by mx_2d_scale_ptrs)
878
+ a, a_scale_dead = load_act_plain(
879
+ a_ptrs, ADescriptor, pid_m * BLOCK_SIZE_M, k * BLOCK_SIZE_K, None, 0, A_MEMORY_MODE, False
880
+ )
881
+ b, b_s = load_weight_mx(
882
+ b_ptrs, BDescriptor, bs_ptrs, bs_mask, None, Bs, 0, pid_n * n_width,
883
+ k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE), 0, 0, pid_n, k, N, K,
884
+ 0, stride_bs_n, stride_bs_k,
885
+ GATE, False, False, B_MEMORY_MODE, False, False,
886
+ BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
887
+ )
888
+ accumulator = mx_weight_only_compute(
889
+ accumulator, a, b, b_s, COMPUTE_MODE, BLOCK_SIZE_K, n_width, SCALE_GROUP_K
890
+ )
891
+ # descriptor arms read by absolute box offset (m/k above); the pointer arms advance here
892
+ a_ptrs += BLOCK_SIZE_K * stride_a_k
893
+ b_ptrs += (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k
894
+ bs_ptrs += SCALE_COLS * stride_bs_k
895
+
896
+ if GATE:
897
+ out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
898
+ gemm_epilogue(
899
+ C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
900
+ stride_c_m, stride_c_n, 0, 0,
901
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
902
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
903
+ COMPUTE_MODE="dot", N_COLS=N,
904
+ GlobalScale=BsGlobal, stride_global_e=stride_bs_global,
905
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
906
+ )
907
+ else:
908
+ # explicit (not the epilogue fold): this arm exits through store_masked, which has no
909
+ # GlobalScale slot
910
+ accumulator = apply_global_scale(
911
+ accumulator, BsGlobal, stride_bs_global, None, 0, 0
912
+ )
913
+ accumulator = add_bias(
914
+ accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
915
+ BLOCK_SIZE_N, N, False,
916
+ )
917
+ store_masked(
918
+ C, accumulator, pid_m, pid_n, M, N, stride_c_m, stride_c_n,
919
+ BLOCK_SIZE_M, BLOCK_SIZE_N,
920
+ )
921
+
922
+
923
+ @bayesian_autotune(
924
+ # Same 2D-swizzle loop physics as the tensor kernel — tune the tile, WS a pure perf axis
925
+ # (compile-guarded), no memory-mode/scale axes (unquantized, no scales anywhere).
926
+ get_accelerator_autotuning_configs(tune_block_nk=True, warp_spec=True, tune_block_m=True),
927
+ # GATE keys the gate|up arm separately (its stacked dot is 2*BN wide).
928
+ ["N", "K", "m_bit_length", "GATE"],
929
+ n_trials=100,
930
+ path_anchor_axes=PATH_ANCHOR_AXES,
931
+ # 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
932
+ finite_check_args=("C", "Cs"),
933
+ prune_configs_by={
934
+ "early_config_prune": compose_pruners(
935
+ block_within_dim_pruner("K"),
936
+ warp_spec_compile_guard_pruner(),
937
+ )
938
+ },
939
+ )
940
+ @triton.jit
941
+ def full_precision_matmul_2d_kernel(
942
+ A, # (M, K) BF16/FP16 activations
943
+ B, # (N, K) weights in A's dtype; under GATE the (2N, K) gate|up stack
944
+ C, # (M, N) output; under GATE the GLU intermediate
945
+ Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
946
+ # Shape
947
+ M,
948
+ N,
949
+ K,
950
+ m_bit_length, # autotune key only (log2 M bucket); unused in body
951
+ # Strides
952
+ stride_a_m,
953
+ stride_a_k,
954
+ stride_b_k,
955
+ stride_b_n,
956
+ stride_c_m,
957
+ stride_c_n,
958
+ stride_bias_e,
959
+ stride_bias_n,
960
+ # Meta-parameters
961
+ BLOCK_SIZE_M: tl.constexpr,
962
+ BLOCK_SIZE_N: tl.constexpr,
963
+ BLOCK_SIZE_K: tl.constexpr,
964
+ WARP_SPEC: tl.constexpr = False,
965
+ # Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Folds out at
966
+ # compile time when GATE=False (the plain dense GEMM).
967
+ GATE: tl.constexpr = False,
968
+ ACT_FN: tl.constexpr = "silu",
969
+ SWIGLU_ALPHA: tl.constexpr = None,
970
+ SWIGLU_LIMIT: tl.constexpr = None,
971
+ SIMULATE_UNFUSED: tl.constexpr = False,
972
+ INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
973
+ ):
974
+ """Full-precision (BF16/FP16) 2D matmul — the single-GEMM sibling of
975
+ ``full_precision_matmul_grouped/batched``: plain ``tl.dot``, fp32 accumulation, no scales.
976
+ ``GATE`` loads the stacked (2N, K) gate|up weight as one ``[BK, 2*BN]`` dot and applies the
977
+ ``ACT_FN``/SwiGLU GLU; ``GATE=False`` is the plain dense GEMM. 2D grid with swizzle for L2 reuse."""
978
+ n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
979
+ pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
980
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GATE=GATE
981
+ )
982
+ a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, "pointer", True)
983
+ b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, "pointer")
984
+
985
+ accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
986
+ for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
987
+ a = tl.load(a_ptrs)
988
+ w, _ = load_weight_plain(
989
+ b_ptrs, b_ptrs, 0, pid_n * n_width, k * BLOCK_SIZE_K,
990
+ GATE, False, "pointer", False, BLOCK_SIZE_N, BLOCK_SIZE_K,
991
+ )
992
+ accumulator = accumulator + fp8_dot(a, w, False, BLOCK_SIZE_K)
993
+ a_ptrs += BLOCK_SIZE_K * stride_a_k
994
+ b_ptrs += BLOCK_SIZE_K * stride_b_k
995
+
996
+ if GATE:
997
+ out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
998
+ gemm_epilogue(
999
+ C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
1000
+ stride_c_m, stride_c_n, 0, 0,
1001
+ BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
1002
+ ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
1003
+ COMPUTE_MODE="dot", N_COLS=N,
1004
+ Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
1005
+ )
1006
+ else:
1007
+ accumulator = add_bias(
1008
+ accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
1009
+ BLOCK_SIZE_N, N, False,
1010
+ )
1011
+ store_masked(
1012
+ C, accumulator, pid_m, pid_n, M, N, stride_c_m, stride_c_n,
1013
+ BLOCK_SIZE_M, BLOCK_SIZE_N,
1014
+ )
1015
+
1016
+
1017
+ @compile_time_only_triton_op(
1018
+ add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul"),
1019
+ mutates_args=(),
1020
+ opaque=True,
1021
+ )
1022
+ def w8a8_block_dynamic_fp8_matmul(
1023
+ A: torch.Tensor,
1024
+ B: torch.Tensor,
1025
+ Bs: torch.Tensor,
1026
+ block_size: list[int],
1027
+ output_dtype: torch.dtype | None = None,
1028
+ gate: bool = False,
1029
+ act_fn: str = "silu",
1030
+ swiglu_alpha: float | None = None,
1031
+ swiglu_limit: float | None = None,
1032
+ simulate_unfused: bool = False,
1033
+ bias: torch.Tensor | None = None,
1034
+ ) -> list[torch.Tensor]:
1035
+ """Block-scale FP8 matmul: ``C = A @ B.T``; activations quantized offline in one pass.
1036
+
1037
+ A: (..., K) raw activations, bf16/fp16/fp32 (quantized to FP8 in one wrapper pass)
1038
+ B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack (gate rows [0,N), up [N,2N))
1039
+ Bs: (N // block_n, K // block_k) per-block weight scales (2N rows under ``gate``)
1040
+
1041
+ ``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
1042
+ ``[..., N]`` GLU intermediate (``output_dtype``); the fused-requant output formats stay on
1043
+ the MX path. Returns a one-element list (mirrors the MX/grouped/batched op signature).
1044
+ """
1045
+ assert len(block_size) == 2, (
1046
+ f"block_size must be [block_n, block_k], got {block_size}"
1047
+ )
1048
+ block_n, block_k = block_size[0], block_size[1]
1049
+
1050
+ validate_dense_2d_operands(A, B)
1051
+
1052
+ rows, K = B.shape
1053
+ # Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
1054
+ N = rows // 2 if gate else rows
1055
+ M = A.numel() // A.shape[-1]
1056
+ assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
1057
+ assert not gate or N % block_n == 0, (
1058
+ f"gate|up fusion needs N ({N}) divisible by block_n ({block_n}) — the up-proj scale block"
1059
+ )
1060
+
1061
+ assert Bs.ndim == 2, f"Bs must be 2D (rows//block_n, K//block_k), got ndim={Bs.ndim}"
1062
+ assert Bs.shape == (triton.cdiv(rows, block_n), K // block_k), (
1063
+ f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(rows, block_n)}, {K // block_k})"
1064
+ )
1065
+
1066
+ bs_u8 = ue8m0_as_uint8(Bs)
1067
+ # UE8M0 weight scales are the DeepGEMM-Blackwell format — quantize activations to UE8M0
1068
+ # too so the kernel folds both group scales into the tcgen05 dot_scaled MMA (else fp32).
1069
+ A_q, A_s = fp8_act_quant_block_dynamic(
1070
+ A.view(M, K), block_k, use_ue8m0=bs_u8.dtype == torch.uint8
1071
+ )
1072
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1073
+ # Host TMA descriptors over the (M, K) / (N, K) matrices — the placeholder box is
1074
+ # re-bound per tuned config by rebind_bd_descriptors. Read only by the descriptor
1075
+ # (host-TMA) configs the tuner picks for wide-N prefill; pointer configs never touch
1076
+ # them (the constexpr arm folds out in the load helpers).
1077
+ a_descriptor = TensorDescriptor.from_tensor(A_q, [1, block_k])
1078
+ b_descriptor = maybe_descriptor(B, [1, block_k])
1079
+
1080
+ def grid(META):
1081
+ return (
1082
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1083
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1084
+ )
1085
+
1086
+ with device_context(A.device):
1087
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1088
+ compile_time_only_triton_wrap(w8a8_block_dynamic_fp8_matmul_kernel)[grid](
1089
+ A_q,
1090
+ a_descriptor,
1091
+ A_s,
1092
+ B,
1093
+ b_descriptor,
1094
+ bs_u8,
1095
+ C,
1096
+ bias,
1097
+ M,
1098
+ N,
1099
+ K,
1100
+ int(M).bit_length(),
1101
+ A_q.stride(0),
1102
+ A_q.stride(1),
1103
+ A_s.stride(0),
1104
+ A_s.stride(1),
1105
+ B.stride(1),
1106
+ B.stride(0),
1107
+ bs_u8.stride(1),
1108
+ bs_u8.stride(0),
1109
+ C.stride(-2),
1110
+ C.stride(-1),
1111
+ bias_stride_e,
1112
+ bias_stride_n,
1113
+ block_n=block_n,
1114
+ block_k=block_k,
1115
+ GATE=gate,
1116
+ ACT_FN=act_fn,
1117
+ SWIGLU_ALPHA=swiglu_alpha,
1118
+ SWIGLU_LIMIT=swiglu_limit,
1119
+ SIMULATE_UNFUSED=simulate_unfused,
1120
+ INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
1121
+ )
1122
+
1123
+ return [C]
1124
+
1125
+
1126
+ @compile_time_only_triton_op(
1127
+ add_op_namespace_prefix("w8a8_block_static_fp8_matmul"),
1128
+ mutates_args=(),
1129
+ opaque=True,
1130
+ )
1131
+ def w8a8_block_static_fp8_matmul(
1132
+ A: torch.Tensor,
1133
+ B: torch.Tensor,
1134
+ As: torch.Tensor,
1135
+ Bs: torch.Tensor,
1136
+ block_size: list[int],
1137
+ output_dtype: torch.dtype | None = None,
1138
+ gate: bool = False,
1139
+ act_fn: str = "silu",
1140
+ swiglu_alpha: float | None = None,
1141
+ swiglu_limit: float | None = None,
1142
+ simulate_unfused: bool = False,
1143
+ bias: torch.Tensor | None = None,
1144
+ ) -> list[torch.Tensor]:
1145
+ """Block-scale FP8 matmul with static (per-tensor) activation quantization.
1146
+
1147
+ A: (..., K) raw bf16/fp16 activations — pre-quantized against ``As`` in the wrapper
1148
+ B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack
1149
+ As: scalar / (1,) — per-tensor static activation scale
1150
+ Bs: (N // block_n, K // block_k) per-block weight scales (2N rows under ``gate``)
1151
+
1152
+ ``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
1153
+ ``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the MX/grouped op).
1154
+ """
1155
+ assert len(block_size) == 2, (
1156
+ f"block_size must be [block_n, block_k], got {block_size}"
1157
+ )
1158
+ block_n, block_k = block_size[0], block_size[1]
1159
+
1160
+ assert B.dtype != torch.int8, (
1161
+ "static activation quant is not supported on the FP4 path"
1162
+ )
1163
+ assert not (block_n == B.size(0) and block_k == B.size(1)), (
1164
+ "static activation quant requires block-wise weights, not tensor-mode"
1165
+ )
1166
+ validate_dense_2d_operands(A, B)
1167
+ assert As.numel() == 1, f"As must be scalar or (1,), got {tuple(As.shape)}"
1168
+
1169
+ rows, K = B.shape
1170
+ # Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
1171
+ N = rows // 2 if gate else rows
1172
+ M = A.numel() // A.shape[-1]
1173
+
1174
+ assert Bs.ndim == 2, f"Bs must be 2D (rows//block_n, K//block_k), got ndim={Bs.ndim}"
1175
+ assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
1176
+ assert not gate or N % block_n == 0, (
1177
+ f"gate|up fusion needs N ({N}) divisible by block_n ({block_n}) — the up-proj scale block"
1178
+ )
1179
+ assert Bs.shape == (triton.cdiv(rows, block_n), K // block_k), (
1180
+ f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(rows, block_n)}, {K // block_k})"
1181
+ )
1182
+
1183
+ def grid(META):
1184
+ return (
1185
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1186
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1187
+ )
1188
+
1189
+ bs_u8 = ue8m0_as_uint8(Bs)
1190
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1191
+ As = As.reshape(1).to(torch.float32)
1192
+ # M-gated static pre-quant (bit-exact with the inline arm: same scalar, same cast);
1193
+ # like MX, the inline form is cheap elementwise work — at M=1 a separate kernel is
1194
+ # pure added latency. The kernel picks its arm off A's dtype.
1195
+ A_q, _ = maybe_act_quant(
1196
+ A.view(M, K),
1197
+ lambda x: ((x.to(torch.float32) / As).to(FP8_DTYPE), As),
1198
+ STATIC_MATMUL_ACT_PREQUANT_MIN_M,
1199
+ )
1200
+ # Host-TMA descriptors over (M, K)/(N, K); placeholder boxes re-bound per config by
1201
+ # rebind_bd_descriptors. Read only by the descriptor configs the tuner picks for wide-N
1202
+ # prefill (offline fp8 A); pointer/inline configs never touch them.
1203
+ a_descriptor = TensorDescriptor.from_tensor(A_q, [1, block_k])
1204
+ b_descriptor = maybe_descriptor(B, [1, block_k])
1205
+
1206
+ with device_context(A.device):
1207
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1208
+ compile_time_only_triton_wrap(w8a8_block_static_fp8_matmul_kernel)[grid](
1209
+ A_q,
1210
+ a_descriptor,
1211
+ As,
1212
+ B,
1213
+ b_descriptor,
1214
+ bs_u8,
1215
+ C,
1216
+ bias,
1217
+ M,
1218
+ N,
1219
+ K,
1220
+ int(M).bit_length(), # m_bit_length key bucket
1221
+ A_q.stride(0),
1222
+ A_q.stride(1),
1223
+ B.stride(1),
1224
+ B.stride(0),
1225
+ bs_u8.stride(1),
1226
+ bs_u8.stride(0),
1227
+ C.stride(-2),
1228
+ C.stride(-1),
1229
+ bias_stride_e,
1230
+ bias_stride_n,
1231
+ # Meta-parameters (BM and BN come from the config; BK is the caller's
1232
+ # block_k — see the block-dynamic kernel)
1233
+ block_n=block_n,
1234
+ block_k=block_k,
1235
+ GATE=gate,
1236
+ ACT_FN=act_fn,
1237
+ SWIGLU_ALPHA=swiglu_alpha,
1238
+ SWIGLU_LIMIT=swiglu_limit,
1239
+ SIMULATE_UNFUSED=simulate_unfused,
1240
+ INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
1241
+ )
1242
+
1243
+ return [C]
1244
+
1245
+
1246
+ @compile_time_only_triton_op(
1247
+ add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul"),
1248
+ mutates_args=(),
1249
+ opaque=True,
1250
+ )
1251
+ def w8a8_tensor_dynamic_fp8_matmul(
1252
+ A: torch.Tensor,
1253
+ B: torch.Tensor,
1254
+ Bs: torch.Tensor,
1255
+ output_dtype: torch.dtype | None = None,
1256
+ gate: bool = False,
1257
+ act_fn: str = "silu",
1258
+ swiglu_alpha: float | None = None,
1259
+ swiglu_limit: float | None = None,
1260
+ simulate_unfused: bool = False,
1261
+ bias: torch.Tensor | None = None,
1262
+ ) -> list[torch.Tensor]:
1263
+ """Tensor-scale FP8 matmul: ``C = A @ B.T``; activations quantized offline per row.
1264
+
1265
+ A: (..., K) raw activations, bf16/fp16/fp32 (flattened to (M, K)
1266
+ internally) — per-row scales computed via ``fp8_act_quant_tensor_wide(A, K)``.
1267
+ B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack (one per-tensor scale).
1268
+ Bs: scalar, (1,), or (1, 1) — single tensor-scale weight scale.
1269
+
1270
+ ``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
1271
+ ``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the MX/grouped op).
1272
+ """
1273
+ validate_dense_2d_operands(A, B)
1274
+
1275
+ rows, K = B.shape
1276
+ # Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
1277
+ N = rows // 2 if gate else rows
1278
+ M = A.numel() // A.shape[-1]
1279
+
1280
+ assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
1281
+
1282
+ # Per-row scalar activation scale (one per token).
1283
+ qA, As = fp8_act_quant_tensor_wide(A, K)
1284
+ As = As.reshape(M)
1285
+ Bs = Bs.reshape(1)
1286
+
1287
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1288
+
1289
+ def grid(META):
1290
+ return (
1291
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1292
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1293
+ )
1294
+
1295
+ with device_context(A.device):
1296
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1297
+ compile_time_only_triton_wrap(w8a8_tensor_dynamic_fp8_matmul_kernel)[grid](
1298
+ qA,
1299
+ As,
1300
+ B,
1301
+ Bs,
1302
+ C,
1303
+ bias,
1304
+ M,
1305
+ N,
1306
+ K,
1307
+ int(M).bit_length(), # m_bit_length key bucket
1308
+ qA.stride(-2),
1309
+ qA.stride(-1),
1310
+ As.stride(0),
1311
+ B.stride(1),
1312
+ B.stride(0),
1313
+ C.stride(-2),
1314
+ C.stride(-1),
1315
+ bias_stride_e,
1316
+ bias_stride_n,
1317
+ GATE=gate,
1318
+ ACT_FN=act_fn,
1319
+ SWIGLU_ALPHA=swiglu_alpha,
1320
+ SWIGLU_LIMIT=swiglu_limit,
1321
+ SIMULATE_UNFUSED=simulate_unfused,
1322
+ INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
1323
+ )
1324
+
1325
+ return [C]
1326
+
1327
+
1328
+ @compile_time_only_triton_op(
1329
+ add_op_namespace_prefix("mx_dynamic_matmul"), mutates_args=(), opaque=True
1330
+ )
1331
+ def mx_dynamic_matmul(
1332
+ A: torch.Tensor,
1333
+ B: torch.Tensor,
1334
+ As: torch.Tensor | None,
1335
+ Bs: torch.Tensor,
1336
+ output_dtype: torch.dtype | None = None,
1337
+ activation_format: str | None = None,
1338
+ gate: bool = False,
1339
+ act_fn: str = "silu",
1340
+ swiglu_alpha: float | None = None,
1341
+ swiglu_limit: float | None = None,
1342
+ simulate_unfused: bool = False,
1343
+ quantize_output: bool = False,
1344
+ a_global_scale: torch.Tensor | None = None,
1345
+ b_global_scale: torch.Tensor | None = None,
1346
+ output_global_scale: torch.Tensor | None = None,
1347
+ bias: torch.Tensor | None = None,
1348
+ ) -> list[torch.Tensor]:
1349
+ """MX/NVFP4 matmul ``C = A @ B.T``; activations quantized offline above the
1350
+ ``maybe_act_quant`` M threshold, inline below it. ``activation_format`` sets the
1351
+ activation grid — E4M3 (``"mxfp8"``, the default) or packed E2M1 (``"mxfp4"``,
1352
+ W4A4); NVFP4 weights (E4M3 scales) pin ``"nvfp4"``. Weight format detected from
1353
+ ``B.dtype``: ``int8`` → packed E2M1 (``B`` is ``(N, K//2)``); ``float8_e4m3fn`` →
1354
+ unpacked E4M3. The scale dtype carries the family: UE8M0 = MX group-32, E4M3 =
1355
+ NVFP4 group-16 (``Bs`` is ``(N, K//group)``); tile + dot path are autotuned.
1356
+
1357
+ A: (..., K) raw activations, bf16/fp16/fp32;
1358
+ leading dims are flattened to (M, K) and restored on the output
1359
+ """
1360
+ assert B.ndim == 2 and Bs.ndim in (2, 5, 6) # 5D = pre-swizzled; 6D = the gate|up artifact
1361
+ assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
1362
+ f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
1363
+ )
1364
+ assert A.is_contiguous(), "A must be contiguous"
1365
+ # B may be row-major (the forward weight) or a transposed view of one: the kernel reads
1366
+ # both of B's strides, and `descriptor_box_pruner` keeps the TMA arms — which need a
1367
+ # unit-stride innermost dim — off the transposed form. This is what lets a backward pass
1368
+ # contract over N by handing in `B.t()` instead of materializing a transposed copy.
1369
+ assert B.is_contiguous() or B.t().is_contiguous(), (
1370
+ "B must be contiguous or a transposed view of a contiguous tensor, got strides "
1371
+ f"{tuple(B.stride())} for shape {tuple(B.shape)}"
1372
+ )
1373
+ # cuBLAS block-scaled fast path — INSIDE the op so every caller gets it (dispatchers,
1374
+ # direct op calls, prequantized-As callers) and so autograd keeps working: register_autograd
1375
+ # hangs on this op and saves INPUTS, so the registered dgrad serves a scaled_mm forward
1376
+ # unchanged. Measured 1.4-4.8x over the Triton kernel at every M (B200, 2026-08-27); a None
1377
+ # falls through to the Triton launch exactly as before. Skipped under fake/meta propagation
1378
+ # (the opaque op's fake impl runs this body with launches disabled — the shape-correct C
1379
+ # allocation below is all fake mode needs).
1380
+ from . import compat as _compat
1381
+
1382
+ if (
1383
+ not gate and not quantize_output and bias is None and not simulate_unfused
1384
+ and not _compat._SKIP_LAUNCHES_MIRROR
1385
+ ):
1386
+ _smm = _torch_scaled_mm_2d(
1387
+ A, B, As, Bs, activation_format, a_global_scale, b_global_scale,
1388
+ resolve_output_dtype(output_dtype, A, None),
1389
+ )
1390
+ if _smm is not None:
1391
+ return [_smm]
1392
+
1393
+ WEIGHT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
1394
+
1395
+ rows, K_b = B.shape
1396
+ K = A.shape[-1]
1397
+ M = A.numel() // K
1398
+ # Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
1399
+ N = rows // 2 if gate else rows
1400
+ assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
1401
+ f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[1] (={K_b})"
1402
+ )
1403
+ # Weight scales arrive row-major (rows, K // scale_group) — read affine, no gain swizzling only
1404
+ # one operand — or already SWIZZLE_32_4_4 (5D), swizzled once at load: the deployment contract,
1405
+ # the op never swizzles in the hot path. The format is the scale dtype (E4M3 = NVFP4 group-16,
1406
+ # UE8M0 = MX group-32). SWIZZLED_SCALES governs both operands (a swizzled weight is paired with
1407
+ # swizzled acts). Callers wanting the tcgen05 fast path pre-swizzle the weight (5D).
1408
+ swizzled_scales = Bs.ndim == 5
1409
+ scale_group = mx_scale_family(Bs, K)
1410
+ if not swizzled_scales:
1411
+ assert Bs.shape == (rows, K // scale_group), (
1412
+ f"Bs shape {tuple(Bs.shape)} != ({rows}, {K // scale_group})"
1413
+ )
1414
+ else:
1415
+ # the artifact's row blocks must match THIS weight (one matrix: leading dim 1) — a
1416
+ # wrong-layer artifact with a matching K would otherwise dequantize with garbage scales silently
1417
+ assert Bs.shape[:2] == (1, triton.cdiv(rows, 128)), (
1418
+ f"swizzled Bs carries {tuple(Bs.shape[:2])} (matrices, 128-row blocks), expected "
1419
+ f"{(1, triton.cdiv(rows, 128))} for this ({rows}, K) weight — wrong artifact"
1420
+ )
1421
+ b_u8 = e2m1_as_uint8(B)
1422
+ bs_u8 = ue8m0_as_uint8(Bs) # caller's layout (5D swizzled / 2D affine); the op never swizzles
1423
+ activation_format = resolve_activation_format(activation_format, B, Bs)
1424
+ # resolve BEFORE the C alloc: with a pre-quantized A (As given) the default output is
1425
+ # bf16, not A's storage dtype (fp8 / packed int8 are not valid output element types)
1426
+ output_dtype = resolve_output_dtype(output_dtype, A, As)
1427
+ # Activation quant is always maybe_act_quant — offline above the M threshold, inline in the
1428
+ # kernel below it (fp4 packs in-register). The offline arm writes swizzled scales directly when
1429
+ # the weight is swizzled (fused, no post-quant pass), else affine. Inline acts (small M) stay
1430
+ # affine in-register even under a swizzled weight — the dot_scaled reads the mixed layout fine.
1431
+ # NVFP4 two-level: a_global_scale (the calibrated activation global) normalizes the quant on both
1432
+ # arms — the offline kernel divides before the block quant, the inline arm via AsGlobal.
1433
+ act_global_scale = normalize_global_scale(a_global_scale, 1) # g_a; folds in-kernel
1434
+ if act_global_scale is not None:
1435
+ assert activation_format == "nvfp4", "an activation global is NVFP4-only"
1436
+ act_quant = lambda a: MX_ACT_QUANT[activation_format]( # noqa: E731
1437
+ a, swizzled=swizzled_scales, global_scale=act_global_scale
1438
+ )
1439
+ elif swizzled_scales:
1440
+ act_quant = lambda a: MX_ACT_QUANT[activation_format](a, swizzled=True) # noqa: E731
1441
+ else:
1442
+ act_quant = MX_ACT_QUANT[activation_format]
1443
+ # NVFP4 accumulator correction: the g_a·g_b product folded onto the fp32 accumulator
1444
+ input_global_scale = normalize_global_scale(b_global_scale, 1) # g_b
1445
+ out_global_scale = normalize_global_scale(output_global_scale, 1)
1446
+ # As given ⇒ A is already quantized (the routed-op parity: a pre-quantized activation + its
1447
+ # scales); else quantize raw A (offline above the M threshold, inline in the kernel below it).
1448
+ if As is not None:
1449
+ assert (As.dtype == torch.float8_e4m3fn) == (Bs.dtype == torch.float8_e4m3fn), (
1450
+ f"activation scales ({As.dtype}) must match the weight scale family ({Bs.dtype})"
1451
+ )
1452
+ assert swizzled_scales or As.ndim != 5, (
1453
+ "un-swizzled weights pair with affine (row-major) activation scales, got 5D As"
1454
+ )
1455
+ a_vals, as_scales = A.view(M, K), As
1456
+ # one SWIZZLED_SCALES flag governs both operands in-kernel: row-major activation
1457
+ # scales under a swizzled weight are swizzled here (the grouped wrapper does the same)
1458
+ if swizzled_scales and As.ndim == 2:
1459
+ as_scales = swizzle_mx_scales(As)
1460
+ else:
1461
+ a_vals, as_scales = maybe_act_quant(A.view(M, K), act_quant, MX_MATMUL_ACT_PREQUANT_MIN_M)
1462
+ A_q = e2m1_as_uint8(a_vals)
1463
+ as_u8 = ue8m0_as_uint8(as_scales)
1464
+ # the act-scale layout is carried by the buffer itself: 5D = swizzled (the offline arm under
1465
+ # a swizzled weight, or pre-swizzled As); the inline arm's raw activation stays 2D (affine)
1466
+ act_swizzled = as_u8.ndim == 5
1467
+ # gate|up output: the GLU intermediate [M, N] (output_dtype), or — under ``quantize_output`` —
1468
+ # requantized (mxfp8: fp8 + (M, N//32) uint8 Cs; fp4: packed (M, N//2) + (M, N//out_g) scale).
1469
+ output_format = activation_format if quantize_output and gate else None
1470
+ requant = output_format is not None
1471
+ out_g = 16 if output_format == "nvfp4" else 32
1472
+ cs_dtype = torch.float8_e4m3fn if output_format == "nvfp4" else torch.uint8
1473
+ # Swizzled in -> swizzled out: a swizzled block requants Cs straight into the down's
1474
+ # SWIZZLE_32_4_4 layout (CSDescriptor), so the fused down reads it on the fast path. 2D dense
1475
+ # output rows are contiguous (no scatter), so it's always swizzle-able. Format-general — the
1476
+ # swizzle is a byte-tiling over the N // out_g scale grid (mxfp8/mxfp4 group-32, nvfp4 group-16).
1477
+ swizzled_out = requant and swizzled_scales
1478
+ if output_format in ("mxfp4", "nvfp4"):
1479
+ C = A.new_empty(A.shape[:-1] + (N // 2,), dtype=torch.int8)
1480
+ elif output_format == "mxfp8":
1481
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=FP8_DTYPE)
1482
+ else:
1483
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1484
+ if swizzled_out:
1485
+ cb_cs = triton.cdiv(N // out_g, 4)
1486
+ cs_ret = torch.empty(1, triton.cdiv(M, 128), cb_cs, 2, 256, device=A.device, dtype=cs_dtype)
1487
+ CSDescriptor = TensorDescriptor.from_tensor(cs_ret, [1, 1, 1, 2, 256])
1488
+ Cs = None # row-major pointer unread here; CSDescriptor does the store
1489
+ elif requant:
1490
+ cs_ret = torch.empty(M, N // out_g, device=A.device, dtype=cs_dtype)
1491
+ Cs, CSDescriptor = cs_ret, None
1492
+ else:
1493
+ cs_ret, Cs, CSDescriptor = None, None, None # unread (no OUTPUT_FORMAT)
1494
+ stride_cs_m = cs_ret.stride(0) if (requant and not swizzled_out) else 0
1495
+ stride_cs_n = cs_ret.stride(1) if (requant and not swizzled_out) else 0
1496
+ # Host-TMA descriptors over the packed (M, K_bytes) / (N, K_bytes) matrices — placeholder
1497
+ # box rebound per tuned config by rebind_mx_descriptors. Read only by the descriptor
1498
+ # configs the tuner picks; pointer configs never touch them.
1499
+ a_descriptor = TensorDescriptor.from_tensor(A_q, [1, 32])
1500
+ b_descriptor = TensorDescriptor.from_tensor(b_u8, [1, 32])
1501
+ # One scale pointer per operand (as_u8/bs_u8) — the swizzled buffer on the fast path (also the
1502
+ # base for the BM<128 / BN<128 scalar gather), the affine scale otherwise — plus its descriptor,
1503
+ # built only on the swizzled path (dummy off the operand descriptor when un-swizzled, unread).
1504
+ as_descriptor = swizzled_scale_descriptor(as_u8) if act_swizzled else a_descriptor
1505
+ bs_descriptor = swizzled_scale_descriptor(bs_u8) if swizzled_scales else b_descriptor
1506
+
1507
+ def grid(META):
1508
+ return (
1509
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1510
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1511
+ )
1512
+
1513
+ with device_context(A.device):
1514
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1515
+ compile_time_only_triton_wrap(mx_dynamic_matmul_kernel)[grid](
1516
+ A_q,
1517
+ a_descriptor,
1518
+ as_u8,
1519
+ as_descriptor,
1520
+ b_u8,
1521
+ b_descriptor,
1522
+ bs_u8,
1523
+ bs_descriptor,
1524
+ C,
1525
+ Cs,
1526
+ bias,
1527
+ CSDescriptor,
1528
+ act_global_scale, # AsGlobal: g_a for the inline-quant arm (A/g_a)
1529
+ input_global_scale, # AsBsGlobal = g_b (acc; g_a folds in-kernel via AsGlobal)
1530
+ out_global_scale, # CsGlobal: requant output normalization (next proj's provided input_scale); None folds out
1531
+ M,
1532
+ N,
1533
+ K,
1534
+ int(
1535
+ M
1536
+ ).bit_length(), # m_bit_length key bucket; int() concretizes M (a SymInt under torch.compile has no .bit_length)
1537
+ A_q.stride(0),
1538
+ A_q.stride(1),
1539
+ as_u8.stride(0), # As row stride (affine act-scale read; dead on the swizzled arm)
1540
+ as_u8.stride(-1), # As group-axis stride (1 when row-major; see stride_as_k)
1541
+ b_u8.stride(1),
1542
+ b_u8.stride(0),
1543
+ bs_u8.stride(1),
1544
+ bs_u8.stride(0),
1545
+ C.stride(-2),
1546
+ C.stride(-1),
1547
+ stride_cs_m,
1548
+ stride_cs_n,
1549
+ bias_stride_e,
1550
+ bias_stride_n,
1551
+ global_scale_stride(act_global_scale),
1552
+ global_scale_stride(input_global_scale),
1553
+ global_scale_stride(out_global_scale),
1554
+ SCALE_GROUP_K=scale_group,
1555
+ ACTIVATION_FORMAT=activation_format,
1556
+ SWIZZLED_SCALES=swizzled_scales,
1557
+ SWIZZLED_OUT=swizzled_out,
1558
+ GATE=gate,
1559
+ ACT_FN=act_fn,
1560
+ SWIGLU_ALPHA=swiglu_alpha,
1561
+ SWIGLU_LIMIT=swiglu_limit,
1562
+ OUTPUT_FORMAT=output_format,
1563
+ SIMULATE_UNFUSED=simulate_unfused,
1564
+ INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
1565
+ )
1566
+ # NVFP4's block scales are already normalized by the provided output global (CsGlobal =
1567
+ # output_global_scale) at requant, so the caller pairs cs_ret with that global as the down's As.
1568
+ return [C, cs_ret] if cs_ret is not None else [C]
1569
+
1570
+
1571
+ @compile_time_only_triton_op(
1572
+ add_op_namespace_prefix("full_precision_matmul_2d"), mutates_args=(), opaque=True
1573
+ )
1574
+ def full_precision_matmul_2d(
1575
+ A: torch.Tensor,
1576
+ B: torch.Tensor,
1577
+ output_dtype: torch.dtype | None = None,
1578
+ gate: bool = False,
1579
+ act_fn: str = "silu",
1580
+ swiglu_alpha: float | None = None,
1581
+ swiglu_limit: float | None = None,
1582
+ simulate_unfused: bool = False,
1583
+ bias: torch.Tensor | None = None,
1584
+ ) -> list[torch.Tensor]:
1585
+ """Full-precision (BF16/FP16) 2D matmul ``C = A @ B.T``; no quantization anywhere. ``gate``
1586
+ fuses the ``(2N, K)`` gate|up projection into one stacked GEMM + SwiGLU, returning the
1587
+ ``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the quantized 2D ops)."""
1588
+ validate_dense_2d_operands(A, B)
1589
+ assert A.dtype == B.dtype and A.dtype in (torch.bfloat16, torch.float16), (
1590
+ f"full-precision path needs matching BF16/FP16 A and B, got {A.dtype} / {B.dtype}"
1591
+ )
1592
+ rows, K = B.shape
1593
+ # Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
1594
+ N = rows // 2 if gate else rows
1595
+ M = A.numel() // A.shape[-1]
1596
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1597
+
1598
+ def grid(META):
1599
+ return (
1600
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1601
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1602
+ )
1603
+
1604
+ with device_context(A.device):
1605
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1606
+ compile_time_only_triton_wrap(full_precision_matmul_2d_kernel)[grid](
1607
+ A,
1608
+ B,
1609
+ C,
1610
+ bias,
1611
+ M,
1612
+ N,
1613
+ K,
1614
+ int(M).bit_length(), # m_bit_length key bucket
1615
+ A.stride(-2),
1616
+ A.stride(-1),
1617
+ B.stride(1),
1618
+ B.stride(0),
1619
+ C.stride(-2),
1620
+ C.stride(-1),
1621
+ bias_stride_e,
1622
+ bias_stride_n,
1623
+ GATE=gate,
1624
+ ACT_FN=act_fn,
1625
+ SWIGLU_ALPHA=swiglu_alpha,
1626
+ SWIGLU_LIMIT=swiglu_limit,
1627
+ SIMULATE_UNFUSED=simulate_unfused,
1628
+ INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
1629
+ )
1630
+
1631
+ return [C]
1632
+
1633
+
1634
+ @compile_time_only_triton_op(
1635
+ add_op_namespace_prefix("mx_weight_only_matmul_2d"),
1636
+ mutates_args=(),
1637
+ opaque=True,
1638
+ )
1639
+ def mx_weight_only_matmul_2d(
1640
+ A: torch.Tensor,
1641
+ B: torch.Tensor,
1642
+ Bs: torch.Tensor,
1643
+ output_dtype: torch.dtype | None = None,
1644
+ gate: bool = False,
1645
+ act_fn: str = "silu",
1646
+ swiglu_alpha: float | None = None,
1647
+ swiglu_limit: float | None = None,
1648
+ simulate_unfused: bool = False,
1649
+ b_global_scale: torch.Tensor | None = None,
1650
+ bias: torch.Tensor | None = None,
1651
+ ) -> list[torch.Tensor]:
1652
+ """weight-only dense matmul ``C = A @ B.T``: a RAW bf16/fp16 activation (no quant) against an
1653
+ MXFP4/NVFP4/MXFP8 weight upcast to bf16 in-loop (NVFP4's per-tensor fp32 ``b_global_scale``
1654
+ recovers on the accumulator; ``None`` = single-level) — the ``matmul_ogs`` format (fp4/fp8 weight bytes in
1655
+ memory, bf16 acts, weight upcast in-MMA). Affine (2D) weight scales only. ``gate`` fuses the
1656
+ ``(2N, K)`` gate|up projection + SwiGLU, returning the ``[..., N]`` GLU intermediate. Returns a
1657
+ one-element list (mirrors the other 2D ops). Packed-E2M1 K (B is (rows, K//2)) is handled
1658
+ below, so this op does its own shape checks (not ``validate_dense_2d_operands``)."""
1659
+ assert A.dtype in (torch.bfloat16, torch.float16, torch.float32), (
1660
+ f"weight-only takes a raw bf16/fp16/fp32 activation, got {A.dtype}"
1661
+ )
1662
+ assert Bs.ndim == 2, f"weight-only takes affine (2D) weight scales, got ndim={Bs.ndim}"
1663
+ WEIGHT_VALUES_PER_BYTE = 2 if B.dtype == torch.int8 else 1
1664
+ rows, K_b = B.shape
1665
+ K = A.shape[-1]
1666
+ M = A.numel() // K
1667
+ N = rows // 2 if gate else rows
1668
+ assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
1669
+ f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[1] (={K_b})"
1670
+ )
1671
+ scale_group = mx_scale_family(Bs, K)
1672
+ assert Bs.shape == (rows, K // scale_group), (
1673
+ f"Bs shape {tuple(Bs.shape)} != ({rows}, {K // scale_group})"
1674
+ )
1675
+ assert A.is_contiguous(), "A must be contiguous (the host-TMA descriptor arm reads its box)"
1676
+ b_u8 = e2m1_as_uint8(B)
1677
+ bs_u8 = ue8m0_as_uint8(Bs)
1678
+ b_global_scale = normalize_global_scale(b_global_scale, 1)
1679
+ C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
1680
+ # Host-TMA descriptors over the (M, K) bf16 A and packed (N, K_bytes) weight — placeholder box
1681
+ # rebound per tuned config by rebind_weight_only_descriptors, read only by the descriptor configs the
1682
+ # tuner picks (pointer configs never touch them). Scales stay affine (no scale descriptor).
1683
+ a_descriptor = TensorDescriptor.from_tensor(A.view(M, K), [1, 32])
1684
+ b_descriptor = TensorDescriptor.from_tensor(b_u8, [1, 32])
1685
+
1686
+ def grid(META):
1687
+ return (
1688
+ triton.cdiv(M, META["BLOCK_SIZE_M"]),
1689
+ triton.cdiv(N, META["BLOCK_SIZE_N"]),
1690
+ )
1691
+
1692
+ with device_context(A.device):
1693
+ bias_stride_e, bias_stride_n = bias_strides(bias)
1694
+ compile_time_only_triton_wrap(mx_weight_only_matmul_2d_kernel)[grid](
1695
+ A,
1696
+ a_descriptor,
1697
+ b_u8,
1698
+ b_descriptor,
1699
+ bs_u8,
1700
+ b_global_scale,
1701
+ C,
1702
+ bias,
1703
+ M,
1704
+ N,
1705
+ K,
1706
+ int(M).bit_length(),
1707
+ A.stride(-2),
1708
+ A.stride(-1),
1709
+ b_u8.stride(0),
1710
+ b_u8.stride(1),
1711
+ bs_u8.stride(0),
1712
+ bs_u8.stride(1),
1713
+ C.stride(-2),
1714
+ C.stride(-1),
1715
+ bias_stride_e,
1716
+ bias_stride_n,
1717
+ global_scale_stride(b_global_scale),
1718
+ SCALE_GROUP_K=scale_group,
1719
+ WEIGHT_VALUES_PER_BYTE=WEIGHT_VALUES_PER_BYTE,
1720
+ GATE=gate,
1721
+ ACT_FN=act_fn,
1722
+ SWIGLU_ALPHA=swiglu_alpha,
1723
+ SWIGLU_LIMIT=swiglu_limit,
1724
+ SIMULATE_UNFUSED=simulate_unfused,
1725
+ INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
1726
+ )
1727
+
1728
+ return [C]
1729
+
1730
+
1731
+ # ── torch scaled_mm fast path (2D dense MX, Blackwell) ────────────────────────
1732
+ #
1733
+ # cuBLAS's block-scaled GEMM beats our Triton 2D kernel at EVERY measured M on the
1734
+ # quantized-activation MX formats (B200, N=18432 K=6144, 200-trial tunes, 2026-08-27):
1735
+ # mxfp8 M=16 3.42x M=256 3.17x M=1024 1.57x M=4096 1.55x M=8192 1.42x
1736
+ # nvfp4 M=16 4.83x M=256 4.15x M=1024 2.48x M=4096 1.81x M=8192 1.80x
1737
+ # and its bf16 output is bit-identical to ours at every checked shape (same offline act
1738
+ # quant feeds both, fp32 accumulate). Only the M=1 swap-AB decode dispatch stays ahead
1739
+ # (18.4us vs ~33), so the route floor is M >= 2.
1740
+ #
1741
+ # Inductor refuses to lower scaled_mm with SWIZZLE_32_4_4 scales ("does not yet support
1742
+ # non-trivial swizzles" — repros/scaled_mm_swizzle_compile.py), so the call lives behind an
1743
+ # OPAQUE custom op: dynamo captures the op as a leaf and the real scaled_mm runs at runtime,
1744
+ # which serves compiled callers too. F.scaled_mm has no autograd formula while our ops carry
1745
+ # registered dgrads, so differentiating callers fall through to the Triton ops. CUDA-graph
1746
+ # capture is fine (measured under do_bench_cudagraph).
1747
+
1748
+ _SCALED_MM_MIN_M = int(os.environ.get("FINEGRAINED_SCALED_MM_MIN_M", "2"))
1749
+
1750
+
1751
+ @torch.library.custom_op(add_op_namespace_prefix("scaled_mm_2d_mx"), mutates_args=())
1752
+ def _scaled_mm_2d_op(
1753
+ Aq: torch.Tensor,
1754
+ As: torch.Tensor,
1755
+ B: torch.Tensor,
1756
+ Bs: torch.Tensor,
1757
+ g_a: torch.Tensor | None,
1758
+ g_b: torch.Tensor | None,
1759
+ fmt: str,
1760
+ ) -> torch.Tensor:
1761
+ """``F.scaled_mm`` behind an OPAQUE custom op. Inductor cannot lower scaled_mm with
1762
+ SWIZZLE_32_4_4 scales (repros/scaled_mm_swizzle_compile.py), but it never has to: dynamo
1763
+ captures this op as a leaf and the real call runs at runtime — so compiled callers get the
1764
+ cuBLAS fast path too, not just eager ones."""
1765
+ F = torch.nn.functional
1766
+ ST, SW = F.ScalingType, F.SwizzleType
1767
+ if fmt == "mxfp8":
1768
+ return F.scaled_mm(
1769
+ Aq, B.t(),
1770
+ As.view(torch.float8_e8m0fnu), ST.BlockWise1x32,
1771
+ Bs.view(torch.float8_e8m0fnu), ST.BlockWise1x32,
1772
+ swizzle_a=SW.SWIZZLE_32_4_4, swizzle_b=SW.SWIZZLE_32_4_4,
1773
+ output_dtype=torch.bfloat16,
1774
+ )
1775
+ one = torch.ones((), device=Aq.device, dtype=torch.float32)
1776
+ FP4 = torch.float4_e2m1fn_x2
1777
+ return F.scaled_mm(
1778
+ Aq.view(FP4), B.view(FP4).t(),
1779
+ [As.view(torch.float8_e4m3fn), one if g_a is None else g_a.reshape(()).float()],
1780
+ [ST.BlockWise1x16, ST.TensorWise],
1781
+ [Bs.view(torch.float8_e4m3fn), one if g_b is None else g_b.reshape(()).float()],
1782
+ [ST.BlockWise1x16, ST.TensorWise],
1783
+ swizzle_a=[SW.SWIZZLE_32_4_4, SW.NO_SWIZZLE],
1784
+ swizzle_b=[SW.SWIZZLE_32_4_4, SW.NO_SWIZZLE],
1785
+ output_dtype=torch.bfloat16,
1786
+ )
1787
+
1788
+
1789
+ @_scaled_mm_2d_op.register_fake
1790
+ def _(Aq, As, B, Bs, g_a, g_b, fmt):
1791
+ return Aq.new_empty(Aq.shape[0], B.shape[0], dtype=torch.bfloat16)
1792
+
1793
+
1794
+ def _smm_reject(reason):
1795
+ if os.environ.get("FINEGRAINED_SCALED_MM_DEBUG"):
1796
+ print(f"[scaled_mm route] rejected: {reason}", flush=True)
1797
+ return None
1798
+
1799
+
1800
+ def _torch_scaled_mm_2d(A, B, As, Bs, activation_format, a_global_scale, b_global_scale, out_dtype):
1801
+ """The scaled_mm route, or ``None`` to take the Triton ops. Fires only where measured to
1802
+ win AND where the semantics are identical: plain ungated GEMM, quantized activations in
1803
+ the WEIGHT's fmt, SWIZZLE_32_4_4 weight scales (Blackwell scaled_mm rejects row-major
1804
+ outright), bf16 out, eager, no grad."""
1805
+ if os.environ.get("FINEGRAINED_DISABLE_SCALED_MM"):
1806
+ return _smm_reject("env-disabled")
1807
+ if not is_sm10x():
1808
+ return _smm_reject("not-sm10x")
1809
+ if not (hasattr(torch.nn.functional, "scaled_mm") and hasattr(torch.nn.functional, "ScalingType")):
1810
+ return _smm_reject("no-F.scaled_mm")
1811
+ fmt = weight_format(B, Bs)
1812
+ if fmt not in ("mxfp8", "nvfp4") or activation_format == "bf16":
1813
+ return _smm_reject("format/activation_format")
1814
+ if (a_global_scale is not None and a_global_scale.numel() > 1) or (
1815
+ b_global_scale is not None and b_global_scale.numel() > 1
1816
+ ):
1817
+ # scaled_mm's second level is one TensorWise value per operand, so a gate|up stack whose
1818
+ # halves carry their own global has no form here — the Triton kernels fold it by column
1819
+ return _smm_reject("multi-valued global")
1820
+ # None follows the weights; resolve it before comparing (the bench and the integrations pass it)
1821
+ if resolve_activation_format(activation_format, B, Bs) != fmt:
1822
+ return _smm_reject("format-mismatch-after-resolve")
1823
+ if Bs.ndim != 5: # affine (row-major) scales: scaled_mm rejects them on Blackwell
1824
+ return _smm_reject("affine-Bs")
1825
+ if out_dtype is not torch.bfloat16:
1826
+ return _smm_reject("out-dtype")
1827
+ M = A.numel() // A.shape[-1]
1828
+ group = 32 if fmt == "mxfp8" else 16
1829
+ K = A.shape[-1] if As is None else (A.shape[-1] * (2 if fmt == "nvfp4" else 1))
1830
+ if M < _SCALED_MM_MIN_M or K % group or B.shape[0] % 16:
1831
+ return _smm_reject("shape/minM")
1832
+ if As is not None and As.ndim != 5:
1833
+ return _smm_reject("affine-As") # pre-quantized activations must arrive with swizzled scales too
1834
+ if fmt == "mxfp8":
1835
+ Aq = A.reshape(M, K) if As is not None else None
1836
+ if Aq is None:
1837
+ Aq, As = mxfp8_act_quant(A.reshape(M, K), swizzled=True)
1838
+ else:
1839
+ Aq = A.reshape(M, K // 2) if As is not None else None
1840
+ if Aq is None:
1841
+ Aq, As = nvfp4_act_quant(A.reshape(M, K), swizzled=True, global_scale=a_global_scale)[:2]
1842
+ out = _scaled_mm_2d_op(Aq, As, B, Bs, a_global_scale, b_global_scale, fmt)
1843
+ return out.reshape(*A.shape[:-1], B.shape[0])
1844
+
1845
+
1846
+ def matmul_2d(
1847
+ A: torch.Tensor,
1848
+ B: torch.Tensor,
1849
+ As: torch.Tensor | None = None,
1850
+ Bs: torch.Tensor | None = None,
1851
+ *,
1852
+ bias: torch.Tensor | None = None, # (E, N_out) per-expert output bias; (N_out,) for 2D
1853
+ activation_format: str | None = None,
1854
+ gate: bool = False,
1855
+ act_fn: str = "silu",
1856
+ swiglu_alpha: float | None = None,
1857
+ swiglu_limit: float | None = None,
1858
+ quantize_output: bool = False,
1859
+ simulate_unfused: bool = False,
1860
+ output_dtype: torch.dtype | None = None,
1861
+ a_global_scale: torch.Tensor | None = None,
1862
+ b_global_scale: torch.Tensor | None = None,
1863
+ output_global_scale: torch.Tensor | None = None,
1864
+ ) -> torch.Tensor | list[torch.Tensor]:
1865
+ """Dense (2D) quantized matmul dispatcher (W8A8 FP8, W4A8/W4A4 FP4) — the single-GEMM sibling of
1866
+ ``matmul_grouped``/``matmul_batched``, taking the same ``As``/``Bs`` scale spec,
1867
+ ``activation_format`` / gate|up fusion kwargs, and inferred quant block (no ``block_size`` argument — it
1868
+ falls out of the weight-scale shape, so the data can't disagree with a parameter).
1869
+
1870
+ ``As`` is the activation block-scale spec, same as the routed ops: ``None`` → the op quantizes raw
1871
+ ``A`` dynamically; a per-tensor scalar → static (calibrated) FP8 activation scale; block scales →
1872
+ pre-quantized ``A``. ``Bs`` is the weight block scale. ``a_global_scale``/``b_global_scale`` are the
1873
+ two-level NVFP4 per-tensor second-level scales (calibrated ``input_scale`` ``g_a`` and
1874
+ ``weight_scale_2`` ``g_b``) — provided, never computed; the op folds ``g_a·g_b`` onto the
1875
+ accumulator (``a_global_scale`` rides raw ``A`` or a pre-quantized ``As`` alike).
1876
+ ``output_global_scale`` (NVFP4 requant only) is the NEXT proj's provided ``input_scale``:
1877
+ the fused requant normalizes the GLU intermediate by it, returning ``(C, Cs)`` the down-proj
1878
+ consumes as ``As=Cs, a_global_scale=output_global_scale``.
1879
+
1880
+ ``activation_format`` is the activations' quantization: ``None`` (default) = the weights' own
1881
+ format, ``"bf16"`` = weight-only (no activation quant), or an explicit ``"fp8"`` / ``"mxfp8"`` /
1882
+ ``"mxfp4"`` / ``"nvfp4"`` (``"mxfp8"`` on MXFP4 weights = the W4A8 chain; ``check_activation_format``
1883
+ has the per-family matrix). ``gate=True`` reads ``B`` as the ``(2N, K)`` gate|up stack and fuses
1884
+ the ``act_fn`` GLU (``swiglu_alpha``/``swiglu_limit`` its clamped variant), returning the
1885
+ ``[..., N]`` intermediate; ``quantize_output=True`` (MX weights only) stores that intermediate in
1886
+ ``activation_format`` and returns ``(C, Cs)`` for the next op.
1887
+
1888
+ Routes by weight dtype + inferred block: ``Bs`` None → full-precision; MX/NVFP4 (int8/E4M3 weights
1889
+ with UE8M0 group-32 or E4M3 group-16 ``Bs``) → ``mx_dynamic_matmul``; tensor-wide FP8 (scalar
1890
+ ``Bs``) → ``w8a8_tensor_dynamic_fp8_matmul``; block FP8 → the static variant when ``As`` is a
1891
+ per-tensor scalar, else dynamic.
1892
+ """
1893
+ check_activation_format(activation_format, quantize_output)
1894
+ assert (a_global_scale is None and b_global_scale is None) or (
1895
+ Bs is not None and weight_format(B, Bs) == "nvfp4"
1896
+ ), "two-level globals (a_global_scale / b_global_scale) are NVFP4-only"
1897
+ assert output_global_scale is None or (
1898
+ Bs is not None and quantize_output and resolve_activation_format(activation_format, B, Bs) == "nvfp4"
1899
+ ), (
1900
+ "output_global_scale is the NVFP4 requant second level — it requires quantize_output=True on NVFP4 "
1901
+ "(the epilogue would otherwise normalize by it with nothing downstream to compensate)"
1902
+ )
1903
+
1904
+ def _unwrap(ret: list[torch.Tensor]) -> torch.Tensor | tuple[torch.Tensor, ...]:
1905
+ # tuple, matching the routed siblings — callers branch on isinstance(out, tuple)
1906
+ return ret[0] if len(ret) == 1 else tuple(ret)
1907
+
1908
+ if Bs is None: # unquantized BF16/FP16 weights — plain dot, no scales
1909
+ assert As is None and a_global_scale is None and activation_format in (None, "bf16") and not quantize_output, (
1910
+ "the full-precision path (Bs=None) takes no activation scale or quantized activation format"
1911
+ )
1912
+ return _unwrap(
1913
+ full_precision_matmul_2d(
1914
+ A, B, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
1915
+ )
1916
+
1917
+ if is_mx(B, Bs):
1918
+ if activation_format == "bf16": # weight-only: raw bf16 activation, MX weight upcast in-MMA
1919
+ assert As is None and a_global_scale is None and not quantize_output, (
1920
+ "weight-only (activation_format='bf16') takes a raw bf16 activation, no As/global/requant"
1921
+ )
1922
+ assert output_global_scale is None, (
1923
+ "weight-only has no requant epilogue for output_global_scale to normalize"
1924
+ )
1925
+ return _unwrap(
1926
+ mx_weight_only_matmul_2d(
1927
+ A, B, Bs, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit,
1928
+ simulate_unfused, b_global_scale=b_global_scale, bias=bias)
1929
+ )
1930
+ return _unwrap(
1931
+ mx_dynamic_matmul(
1932
+ A, B, As, Bs, output_dtype, activation_format,
1933
+ gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, quantize_output,
1934
+ a_global_scale, b_global_scale, output_global_scale, bias=bias)
1935
+ )
1936
+ # FP8 activations are always E4M3 with the weight-implied scale granularity, so "fp8" is a
1937
+ # no-op format name (accepted for symmetry with the MoE ops); no other name applies here.
1938
+ assert activation_format in (None, "fp8"), (
1939
+ f"FP8-weight activations are E4M3 ('fp8'), got {activation_format!r}"
1940
+ )
1941
+ assert not quantize_output, "quantize_output (fused-requant gate|up output) is MX-only"
1942
+ # Infer the FP8 quant block from the weight-scale shape (scalar Bs = tensor-wide). The contraction
1943
+ # dim K tiles evenly, so block_k is exact; N may be non-aligned (a partial last block, so
1944
+ # N % Bs.shape[0] != 0 and plain division would under-count) — recover block_n from the even dim,
1945
+ # the square-block convention for these FP8 weights.
1946
+ if Bs.numel() == 1:
1947
+ block_size = None
1948
+ else:
1949
+ block_k = B.shape[1] // Bs.shape[1]
1950
+ block_n = B.shape[0] // Bs.shape[0] if B.shape[0] % Bs.shape[0] == 0 else block_k
1951
+ block_size = [block_n, block_k]
1952
+ if block_size is None: # tensor-wide (per-tensor) scale
1953
+ assert As is None, "tensor-wide FP8 quantizes A dynamically — no As"
1954
+ return _unwrap(
1955
+ w8a8_tensor_dynamic_fp8_matmul(
1956
+ A, B, Bs, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
1957
+ )
1958
+ # Block-wise FP8: a per-tensor scalar As is the static (calibrated) activation scale; else dynamic.
1959
+ if As is not None:
1960
+ assert As.numel() == 1, "block-wise FP8 As is a per-tensor static scale (scalar)"
1961
+ return _unwrap(
1962
+ w8a8_block_static_fp8_matmul(
1963
+ A, B, As, Bs, block_size, output_dtype,
1964
+ gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
1965
+ )
1966
+ return _unwrap(
1967
+ w8a8_block_dynamic_fp8_matmul(
1968
+ A, B, Bs, block_size, output_dtype,
1969
+ gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
1970
+ )
build/torch-rocm/metadata.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "finegrained-kernels",
3
+ "id": "_finegrained_kernels_rocm_1348465",
4
+ "version": 0,
5
+ "license": "Apache-2.0",
6
+ "python-depends": [],
7
+ "backend": {
8
+ "type": "rocm"
9
+ },
10
+ "digest": {
11
+ "algorithm": "sha256",
12
+ "files": {
13
+ "__init__.py": "43nBPHxybam5ChWZmsz1Ji+pjJ6K6zF5lzwbqPcMwE4=",
14
+ "_ops.py": "Ssu+B/vlZZOXRh+hKJNWiv3V59Z7J6Wf0w/+9c7uT+M=",
15
+ "backward.py": "ri9lKPhb9SmkYBvUS8KvuORiW5PcKViNamFNVWxb69g=",
16
+ "batched.py": "cSkV7Mnv6kK+k5MANm8f6ZFu3Jdbh0tAz9u9t4e+R3c=",
17
+ "bayesian_autotuner.py": "DPpIATTls8aY7n12oKggpuAOvC3RlugYl1Wid1t5J+U=",
18
+ "compat.py": "yj7b7kCAOrq3j1Y3WxSb/YKoVcPRFfkeXmMj7dkqQoc=",
19
+ "descriptors.py": "4acRMazGPFlbydNt1Hbo8RpAqb3fOtnOQBv5yb4onqw=",
20
+ "epilogue.py": "tGALTI/VMgO3Mz0sQ6H6wuRZpO4UKqM1/4h7d81SnVM=",
21
+ "finegrained_kernels/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY=",
22
+ "formats.py": "V6F0PXdNhXQn/UBz0/frbvcp5Lgterh053XDrvQ/Cpw=",
23
+ "grouped.py": "PXhZU9Rn6UIIiQtR6IUmI060Mank5GMuooTWapRvya8=",
24
+ "loading/__init__.py": "so7kYmlVoiASfoOzemdU5fBTiP8yA85gVPNQExIq/KU=",
25
+ "loading/scales.py": "8HJpflmImEXYJquA6hVLStUUEecvExAxBEYVb3qPPWE=",
26
+ "loading/tiles.py": "hvFIDikI1k0+vV1QOjqdl1bovI/JEElfNwLWg7VC3ZM=",
27
+ "matmul.py": "qLV1KsPyCeAq3O0swnk38GUOT+lS12WxUM8bQZ9DF9A=",
28
+ "mma.py": "1fg2an7Dc8Vd6qpSmq+Q0781A6sNilOJHU0PqEbouT8=",
29
+ "moe.py": "rpiYMM+kzTtNuxXFUrQARLO9kLHcEgKR8rs/dZ0wn/g=",
30
+ "norm.py": "VvE6GUE0Oztf7PDcviLCcB4fhnXt7/zgdE+aApv6DBM=",
31
+ "pruners.py": "v03bXbvsZp1znVIj/eiR7LAWctU66DJlJr51wn8Pheg=",
32
+ "quant.py": "PZhzgOHSDH8lLV6zvrv040B4g9SgFE6Eq6bc8XxfPY8=",
33
+ "scheduling.py": "UGgQ5uVdm4cWPnH+4w6Ffou/PSX3iUarMeRo2E04BhY=",
34
+ "swizzle.py": "ueIJ9FsO8QZhVt3nMFlA+8b+U9/ZHabHFlhufsONV04="
35
+ }
36
+ },
37
+ "provenance": {
38
+ "kernel-builder": {
39
+ "version": "0.17.0-dev0",
40
+ "sha": "81580bb92577f2f7228661ca2a221fc052375709",
41
+ "dirty": false
42
+ },
43
+ "kernel": {
44
+ "sha": "1348465af00f2d4c596d37bdb262ae0875dc1d77",
45
+ "dirty": false
46
+ }
47
+ }
48
+ }
build/torch-rocm/metadata.json.sigstore ADDED
@@ -0,0 +1 @@
 
 
1
+ {"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"certificate":{"rawBytes":"MIIHTDCCBtKgAwIBAgIUKwy7DRLjNl12oimua1LRYxRBXBQwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwOTE2MTU0MjA3WhcNMjYwOTE2MTU1MjA3WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkX51E0716VRnV+N7XqZPQSuT+8+WFqVZ8jyLnA6PxNlNCIgr4O1c3VI4np8PMZ+9k0EozfVf3XbIiPsqY9vU/aOCBfEwggXtMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUo1O1wLYeWm7XlvpitaZXgdwuluUwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wawYDVR0RAQH/BGEwX4ZdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzATBgorBgEEAYO/MAEEBAVCdWlsZDArBgorBgEEAYO/MAEFBB1odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eTAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMG0GCisGAQQBg78wAQkEXwxdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzAbBgorBgEEAYO/MAELBA0MC3NlbGYtaG9zdGVkMEAGCisGAQQBg78wAQwEMgwwaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5MDgGCisGAQQBg78wAQ0EKgwoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAaBgorBgEEAYO/MAEPBAwMCjEwNzE0NzU1MjkwLgYKKwYBBAGDvzABEAQgDB5odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2UwGAYKKwYBBAGDvzABEQQKDAgyNTcyMDc0MzBtBgorBgEEAYO/MAESBF8MXWh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS8uZ2l0aHViL3dvcmtmbG93cy9idWlsZC55YW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKDEzNDg0NjVhZjAwZjJkNGM1OTZkMzdiZGIyNjJhZTA4NzVkYzFkNzcwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBkBgorBgEEAYO/MAEVBFYMVGh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS9hY3Rpb25zL3J1bnMvMzUxMTY2NDkyOTcvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBGBgorBgEEAYO/MAEYBDgMNnJlcG86aHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHk6cmVmOnJlZnMvaGVhZHMvbWFpbjCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABoKrh6swAAAQDAEgwRgIhANX/7k2eKwwKjy8ZxKOpmVkg1xAd5OZZtFP+JoYLm5xdAiEA541PEWCxlJ8vBCctUblYJ8CwahUQrQHAB1az4ZQBRvAwCgYIKoZIzj0EAwMDaAAwZQIxAOrFZmrON23FouSKXOvH3VhA3MDrJ0zVLduijdnHKbEB6XAwuqVcNzLd8e+SystARQIwOdpWPy0GsRlCrzjovhZ/iOd3XOZtYhCHbwuhADV5U8LyW0FHy/Jl3g8mCcNkM+hY"}, "tlogEntries":[{"logIndex":"2863678766", "logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion":{"kind":"hashedrekord", "version":"0.0.1"}, "integratedTime":"1789573327", "inclusionPromise":{"signedEntryTimestamp":"MEUCIQDOUMA7NduNwOul83R0X3Ocp2ybE707LUZ6Z7MxIDEDHwIgY3TKJTPvt660IBYW6CxkGv0W7iyJWsMQ6DE9TAVfu2A="}, "inclusionProof":{"logIndex":"2741774504", "rootHash":"h7JnsSI02oktCuUf2hPr37WQeR1av+o8FGUwr34KmHo=", "treeSize":"2741774526", "hashes":["1O4OieGYomUIdapwcW0fU0+2+02pFm1aEo0ti4sg08s=", "mcj/XMUVpOVz+S5LUKHconnoT5H4349XaBWB7RFeOkI=", "Wdf+QLOD4triMw+JPnM0auon4nMf4ww9lucCEay94fU=", "6OX+tq7oFddP7/kDlt87XDsQ0WlCJnt1SdJUYyCdR+8=", "B5h5p0vseS2PGxobaWllh1w2JdneoJtoZSCxox8DQMc=", "66RI6MJezQM4018f5MMtQln23ah4XGjWHARyTP4OGH4=", "Ojxn+u3l9okV6fn1qhDIlj0xB1NlpdMwwfQZrYDTTDA=", "dNQ0Q6e05QOPvF+9aMH7hKeNMC+sdDn/+tAhAdOJnpQ=", "cuse2fF4Cl9QdYLPPxCWd9gSEI01WyuvnM3KB1Ah78Y=", "g+kZXC8JlqtkZn+3BtMPZOIjPv/1QgiVvlH7tpuF5rU=", "z7hfW7IyWq58tcWTZuli3Brmb7xA7392X7mpl47fnCc=", "/DBUF30Sdk4LDt4XUgHCGSr10fT7nDkSitYbygYwfNg=", "vC+yZlSpf9tVD+3MdNtHCe71eNNHHi6gplgltL/tmDc=", "gGfwuhE97xnhmxu6exffPtS05ABS4/ldyg66sFcxSRE=", "jhevWFOJ2cWU6SvzMnAe08zHGcVquRlP7gAed39+wI4=", "qxzHanAzz57SDdmJe0B7bJK72NTIBbwEMGBBKvDOROw=", "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY="], "checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n2741774526\nh7JnsSI02oktCuUf2hPr37WQeR1av+o8FGUwr34KmHo=\n\n— rekor.sigstore.dev wNI9ajBGAiEA1L58wqYgcdSBeA6sGCi5zk9nSn7U/bA/v0obbkuIovICIQDm2hhhrVGvjLnHrfJ4aGYqS45uNAgpXJMtOVd90NuZzQ==\n"}}, "canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkZmI0MDlkYzcyN2RjMWM1M2I1MGJjZGZjMzU0NmE4NmZjNWNjOWVlY2UzOTVhZmJkZmEzZWJhOWIwNmRjZDY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNjRFZZNFd2Ylhob3pHcDFFSTZSdWxNUTJOZ295M2w1OWpxUWxtcVpXem93SWdKaUYxQ0RhWUl0Tm5icC84WDdMd05kQW03bVVycW93ZWJaL2RMall1dzRNPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaFVSRU5EUW5STFowRjNTVUpCWjBsVlMzZDVOMFJTVEdwT2JERXliMmx0ZFdFeFRGSlplRkpDV0VKUmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFpkMDlVUlRKTlZGVXdUV3BCTTFkb1kwNU5hbGwzVDFSRk1rMVVWVEZOYWtFelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZyV0RVeFJUQTNNVFpXVW01V0swNDNXSEZhVUZGVGRWUXJPQ3RYUm5GV1dqaHFlVXdLYmtFMlVIaE9iRTVEU1dkeU5FOHhZek5XU1RSdWNEaFFUVm9yT1dzd1JXOTZabFptTTFoaVNXbFFjM0ZaT1haVkwyRlBRMEptUlhkbloxaDBUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZ2TVU4eENuZE1XV1ZYYlRkWWJIWndhWFJoV2xoblpIZDFiSFZWZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJGM1dVUldVakJTUVZGSUwwSkhSWGRZTkZwa1lVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKb01Wb3laSEJpYldSdFdWZE9iQXBNTW5Sc1kyMDFiR0pJVFhSWk1qbDBZbGhXZFdGWVVqVk1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1rb3hZVmQ0YTB4dWJHaGlWM2hCQ21OdFZtMWplVGx2V2xkR2EyTjVPWFJaVjJ4MVRVUnJSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkxNbWd3WkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpBS1lWYzVkV041Tlc1aFdGSnZaRmRLTVdNeVZubFpNamwxWkVkV2RXUkROV3BpTWpCM1NIZFpTMHQzV1VKQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWN3cGlNMlJtV2tkc2VtTkhSakJaTW1kM1RtZFpTMHQzV1VKQ1FVZEVkbnBCUWtGM1VXOU5WRTB3VDBSUk1rNVhSbTFOUkVKdFRXMVJNRmw2VlRWT2JWRjZDazR5U210WmFra3lUVzFHYkUxRVp6Tk9WMUpxVFZkUk0wNTZRVlJDWjI5eVFtZEZSVUZaVHk5TlFVVkZRa0ZXUTJSWGJITmFSRUZ5UW1kdmNrSm5SVVVLUVZsUEwwMUJSVVpDUWpGdlpGZGtibUZYTlc1YWJVWnFXbE01Y2xwWVNuVmFWM2g2VEZkT2RtSlhNVEZpYld3d1pWUkJaRUpuYjNKQ1owVkZRVmxQTHdwTlFVVkhRa0U1ZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QzZFpTMHQzV1VKQ1FVZEVkbnBCUWtOQlVYUkVRM1J2WkVoU2QyTjZiM1pNTTFKMkNtRXlWblZNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVY3dSME5wYzBkQlVWRkNaemM0ZDBGUmEwVUtXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5YURGYU1tUndZbTFrYlZsWFRteE1NblJzWTIwMWJHSklUWFJaTWpsMFlsaFdkUXBoV0ZJMVRIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTmlNMlI2VERKS01XRlhlR3RNYm14b1lsZDRRV050Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxQ2sxRVowZERhWE5IUVZGUlFtYzNPSGRCVVc5RlMyZDNiMDFVVFRCUFJGRXlUbGRHYlUxRVFtMU5iVkV3V1hwVk5VNXRVWHBPTWtwcldXcEpNazF0Um13S1RVUm5NMDVYVW1wTlYxRXpUbnBCWWtKbmIzSkNaMFZGUVZsUEwwMUJSVXhDUVRCTlF6Tk9iR0pIV1hSaFJ6bDZaRWRXYTAxRlFVZERhWE5IUVZGUlFncG5OemgzUVZGM1JVMW5kM2RoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbWd4V2pKa2NHSnRaRzFaVjA1c1RESjBiR050Tld4aVNFMTBDbGt5T1hSaVdGWjFZVmhTTlUxRVowZERhWE5IUVZGUlFtYzNPSGRCVVRCRlMyZDNiMDFVVFRCUFJGRXlUbGRHYlUxRVFtMU5iVkV3V1hwVk5VNXRVWG9LVGpKS2ExbHFTVEpOYlVac1RVUm5NMDVYVW1wTlYxRXpUbnBCWmtKbmIzSkNaMFZGUVZsUEwwMUJSVTlDUWtWTlJETktiRnB1VFhaaFIxWm9Xa2hOZGdwaVYwWndZbXBCWVVKbmIzSkNaMFZGUVZsUEwwMUJSVkJDUVhkTlEycEZkMDU2UlRCT2VsVXhUV3ByZDB4bldVdExkMWxDUWtGSFJIWjZRVUpGUVZGbkNrUkNOVzlrU0ZKM1kzcHZka3d5WkhCa1IyZ3hXV2sxYW1JeU1IWmhTRlp1V2pKc2RWb3lXbWhaTWxWM1IwRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVc0tSRUZuZVU1VVkzbE5SR013VFhwQ2RFSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1JqaE5XRmRvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVid3BrVjJSdVlWYzFibHB0Um1wYVV6bHlXbGhLZFZwWGVIcE1WMDUyWWxjeE1XSnRiREJsVXpoMVdqSnNNR0ZJVm1sTU0yUjJZMjEwYldKSE9UTmplVGxwQ21SWGJITmFRelUxV1ZjeGMxRklTbXhhYmsxMllVZFdhRnBJVFhaaVYwWndZbXBCTkVKbmIzSkNaMFZGUVZsUEwwMUJSVlJDUTI5TlMwUkZlazVFWnpBS1RtcFdhRnBxUVhkYWFrcHJUa2ROTVU5VVdtdE5lbVJwV2tkSmVVNXFTbWhhVkVFMFRucFdhMWw2Um10T2VtTjNTVkZaUzB0M1dVSkNRVWRFZG5wQlFncEdRVkZVUkVKR00ySXpTbkphYlhoMlpERTVhMkZZVG5kWldGSnFZVVJDYTBKbmIzSkNaMFZGUVZsUEwwMUJSVlpDUmxsTlZrZG9NR1JJUW5wUGFUaDJDbG95YkRCaFNGWnBURzFPZG1KVE9XOWtWMlJ1WVZjMWJscHRSbXBhVXpseVdsaEtkVnBYZUhwTVYwNTJZbGN4TVdKdGJEQmxVemxvV1ROU2NHSXlOWG9LVEROS01XSnVUWFpOZWxWNFRWUlpNazVFYTNsUFZHTjJXVmhTTUZwWE1YZGtTRTEyVFZSQlYwSm5iM0pDWjBWRlFWbFBMMDFCUlZkQ1FXZE5RbTVDTVFwWmJYaHdXWHBDUjBKbmIzSkNaMFZGUVZsUEwwMUJSVmxDUkdkTlRtNUtiR05IT0RaaFNGWnVXakpzZFZveVdtaFpNbFYyWVRKV2VXSnRWbk5qZVRGcUNtSXlNWFJrVnpWd1pFaHJObU50Vm0xUGJrcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRMEpwZDFsTFMzZFpRa0pCU0ZkbFVVbEZRV2RTT1VKSWMwRUtaVkZDTTBGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFtOUxjbWcyYzNkQlFVRlJSQXBCUldkM1VtZEphRUZPV0M4M2F6SmxTM2QzUzJwNU9GcDRTMDl3YlZaclp6RjRRV1ExVDFwYWRFWlFLMHB2V1V4dE5YaGtRV2xGUVRVME1WQkZWME40Q214S09IWkNRMk4wVldKc1dVbzRRM2RoYUZWUmNsRklRVUl4WVhvMFdsRkNVblpCZDBObldVbExiMXBKZW1vd1JVRjNUVVJoUVVGM1dsRkplRUZQY2tZS1dtMXlUMDR5TTBadmRWTkxXRTkyU0ROV2FFRXpUVVJ5U2pCNlZreGtkV2xxWkc1SVMySkZRalpZUVhkMWNWWmpUbnBNWkRobEsxTjVjM1JCVWxGSmR3cFBaSEJYVUhrd1IzTlNiRU55ZW1wdmRtaGFMMmxQWkROWVQxcDBXV2hEU0dKM2RXaEJSRlkxVlRoTWVWY3dSa2g1TDBwc00yYzRiVU5qVG10TksyaFpDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}], "timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyDADAgEAMIICvwYJKoZIhvcNAQcCoIICsDCCAqwCAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgEEWXb1aAU9uWYKBG9k5yRgiCru6MaWJqEJ7eotv6QLsCFAVQ7WjJb8AfpOlPrlg4vundjMeXGA8yMDI2MDkxNjE1NDIwN1owAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdowggHWAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwOTE2MTU0MjA3WjAvBgkqhkiG9w0BCQQxIgQgnhWF5DW3tegD61eDeAX0KIpasZoCwslhIsaRZtJyxHwwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGYwZAIwQ8JSNadEl6jXNYx0SryoCfLxVtpQES91e45sgot7605M9yXk+nFgOfZ5qRSu1fJdAjB3nmagQ7GQmID30bAxY5/nRpO4VFWhFXqvRv5R0UcT17jG0LE4x4TOt9vkd1x9JHE="}]}}, "messageSignature":{"messageDigest":{"algorithm":"SHA2_256", "digest":"37QJ3HJ9wcU7ULzfw1Rqhvxcye7OOVr736PrqbBtzWk="}, "signature":"MEUCIQCcDVY4WvbXhozGp1EI6RulMQ2Ngoy3l59jqQlmqZWzowIgJiF1CDaYItNnbp/8X7LwNdAm7mUrqowebZ/dLjYuw4M="}}
build/torch-rocm/mma.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import triton
16
+ import triton.language as tl
17
+
18
+ from .quant import e2m1_cols_to_e4m3, e2m1_cols_to_f32, e2m1_to_bf16, e2m1_to_e4m3, e2m1_to_f32
19
+ from .loading.scales import decode_group_scale
20
+
21
+
22
+ @triton.jit
23
+ def mx_dot_scaled(
24
+ acc,
25
+ a,
26
+ a_scale,
27
+ w,
28
+ w_scale,
29
+ SWAP_AB: tl.constexpr = False,
30
+ BLOCK_SIZE_K: tl.constexpr = 0,
31
+ SCALE_GROUP_K: tl.constexpr = 0,
32
+ ):
33
+ """MX 'dot_scaled' step: the tcgen05 scaled MMA folds the group scales into the tensor core;
34
+ each operand's format is its tile's dtype (``uint8`` = packed E2M1, E4M3, or bf16 for the
35
+ weight-only unscaled lhs). fp4 on BOTH operands lowers to the native ``kind::mxf4`` MMA (2x the
36
+ fp8 rate; native iff the M operand is 128 — same gate as mxf8f6f4). Plain: ``a`` [BM, BK] x
37
+ ``w`` [BK, BN] (the caller pre-shapes ``w``/``w_scale``, e.g. ``tl.trans(gu)``). ``SWAP_AB``
38
+ (decode): the weight ``w`` [BN, BK] is the lhs (output rows in M) and the single [BK] token is
39
+ padded to the N=16 rhs (col 0 real — 16 is Triton's tcgen05-selection gate, N=8 drops to the
40
+ bf16-upcast fallback, bare-1 was 1.83x); ``acc`` is the persistent ``[BN, MMA_N_ATOM]``
41
+ accumulator the caller takes column 0 of (never a fresh per-step init, which trips the sm_100
42
+ accumulator-init pass). A packed ``a`` stays packed (the E4M3-scaled mxf4nvf4 kind is fp4 x fp4
43
+ only) and the token's group scale broadcasts to the rhs columns."""
44
+ a_fmt: tl.constexpr = (
45
+ "e2m1" if a.dtype == tl.uint8
46
+ else ("e4m3" if a.dtype == tl.float8e4nv else "bf16")
47
+ )
48
+ w_fmt: tl.constexpr = "e2m1" if w.dtype == tl.uint8 else "e4m3"
49
+ if SWAP_AB:
50
+ rhs = swap_pad_rhs(a, BLOCK_SIZE_K // 2 if a.dtype == tl.uint8 else BLOCK_SIZE_K)
51
+ if a_scale.dtype == tl.uint8: # UE8M0 broadcast via the zero-add idiom
52
+ asc = tl.trans(
53
+ a_scale[:, None]
54
+ + tl.zeros((BLOCK_SIZE_K // SCALE_GROUP_K, MMA_N_ATOM), tl.uint8)
55
+ )
56
+ else: # E4M3 (NVFP4) — no fp8 arithmetic; materialize the broadcast directly
57
+ asc = tl.trans(
58
+ tl.broadcast_to(
59
+ a_scale[:, None], (BLOCK_SIZE_K // SCALE_GROUP_K, MMA_N_ATOM)
60
+ )
61
+ )
62
+ acc = tl.dot_scaled(w, w_scale, w_fmt, rhs, asc, a_fmt, acc)
63
+ else:
64
+ acc = tl.dot_scaled(a, a_scale, a_fmt, w, w_scale, w_fmt, acc)
65
+ return acc
66
+
67
+
68
+ @triton.jit
69
+ def mx_dot_rescale(
70
+ acc,
71
+ a,
72
+ a_scale,
73
+ w,
74
+ w_scale,
75
+ SWAP_AB: tl.constexpr = False,
76
+ BLOCK_SIZE_K: tl.constexpr = 0,
77
+ ):
78
+ """MX 'dot' step (BK == one scale group): fp8 ``tl.dot`` on E4M3-decoded operands + per-group
79
+ software rescale (both UE8M0 scales decoded here), accumulating into ``acc``. Plain: ``a``
80
+ [BM, BK] x ``w`` [BK, BN] with the weight's per-column scale transposed onto the product (the
81
+ batched gate_up kernel passes the stacked 2*BN tile — per-column independence keeps that
82
+ bit-exact). ``SWAP_AB`` (decode): the weight ``w`` [ROWS, BK] is the lhs (E2M1 column-unpacked,
83
+ K order low nibble first) and the [BK] token padded to the N=16 atom is the rhs — the
84
+ well-shaped fp8 MMA at M=1 (M quantizes to 64/128, N only to 8, so weight rows fill the big
85
+ atom); the weight's per-output-row scale broadcasts down the acc columns and the token's single
86
+ group scale is a scalar; ``acc`` is the persistent ``[ROWS, MMA_N_ATOM]`` accumulator."""
87
+ aq = e2m1_cols_to_e4m3(a) if a.dtype == tl.uint8 else a
88
+ a_s = decode_group_scale(a_scale)
89
+ w_s = decode_group_scale(w_scale)
90
+ if SWAP_AB:
91
+ wq = e2m1_cols_to_e4m3(w) if w.dtype == tl.uint8 else w
92
+ acc = acc + tl.dot(wq, swap_pad_rhs(aq, BLOCK_SIZE_K)) * w_s * a_s
93
+ else:
94
+ wq = e2m1_to_e4m3(w) if w.dtype == tl.uint8 else w
95
+ acc = acc + tl.dot(aq, wq) * a_s * tl.trans(w_s)
96
+ return acc
97
+
98
+
99
+ @triton.jit
100
+ def mx_weight_upcast(w, w_scale, BLOCK_SIZE_K: tl.constexpr, N: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
101
+ OUT_DTYPE: tl.constexpr = tl.bfloat16):
102
+ """Upcast one MXFP4/MXFP8 weight K-tile ``[BK, N]`` to ``OUT_DTYPE`` for the weight-only path: unpack
103
+ E2M1 -> bf16 directly (fp8 passes through a cast) and apply each ``SCALE_GROUP_K``-row K-group's
104
+ UE8M0 group scale. ``w_scale`` is ``[N, BK // SCALE_GROUP_K]`` (per-N-row, per-group); transposed
105
+ and broadcast across each group's rows to ``[BK, N]``. Unlike the ``dot`` arm (BK == group, scale
106
+ folded onto the [M,N] product) this dequantizes IN the tile, so BK spans any number of groups — a
107
+ full-BK bf16 ``tl.dot`` (the matmul_ogs format: fp4 weight, bf16 acts).
108
+
109
+ The scale is applied as a plain bf16 multiply, NOT a hand-rolled exponent-add: the UE8M0 scale is
110
+ a power of two so a ``bf16 * 2^k`` is already an exact exponent shift (a cheap FMA), and folding it
111
+ into the bits as an int add + zero-mask ``tl.where`` measured ~13% SLOWER (gate_up 4.68->5.29ms) —
112
+ the mask + uint16<->int32 bitcast chain costs more than the multiply it replaces."""
113
+ ng: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
114
+ wq = e2m1_to_bf16(w) if w.dtype == tl.uint8 else w.to(tl.bfloat16) # [BK, N] bf16 (direct, no E4M3)
115
+ # Reshape the (real, contiguous) weight to [ng, g, N] and broadcast the group scale [ng, 1, N]
116
+ # across each group's g rows in the multiply, then reshape back to [BK, N]. (Reshaping a
117
+ # broadcasted stride-0 tensor doesn't lower, so broadcast happens IN the op, not before it.)
118
+ ws = tl.trans(decode_group_scale(w_scale)).to(tl.bfloat16)[:, None, :] # [ng, 1, N]
119
+ w3 = wq.reshape(ng, SCALE_GROUP_K, N) * ws
120
+ # dequant + scale happen in bf16 (exact: E2M1/E4M3 codes and power-of-two UE8M0 scales all fit),
121
+ # then widen if the activation is wider — bf16->fp32 is lossless and keeps the multiply cheap.
122
+ return w3.reshape(BLOCK_SIZE_K, N).to(OUT_DTYPE)
123
+
124
+
125
+ @triton.jit
126
+ def mx_weight_only_compute(
127
+ acc,
128
+ a,
129
+ w,
130
+ w_scale,
131
+ COMPUTE_MODE: tl.constexpr,
132
+ BLOCK_SIZE_K: tl.constexpr,
133
+ N_WIDTH: tl.constexpr,
134
+ SCALE_GROUP_K: tl.constexpr,
135
+ SWAP_AB: tl.constexpr = False,
136
+ ):
137
+ """Weight-only (W4A16/W8A16) MMA step: raw bf16/fp16 activations against an MXFP4/MXFP8 weight
138
+ tile, dispatched on ``COMPUTE_MODE``. ``"dot_scaled"`` hands the packed weight and its UE8M0
139
+ group scales straight to the tcgen05 scaled MMA with an UNSCALED activation operand (the
140
+ ``lhs_scale=None`` / bf16-format form), so the tensor core does the fp4 decode and the group
141
+ rescale; ``"dot"`` upcasts the tile to the activation dtype in-loop and runs a plain
142
+ ``tl.dot`` (the matmul_ogs Hopper format). Both are correct everywhere and the tuner picks per
143
+ workload — the two differ by arm: measured on gpt-oss K=2880, ``dot_scaled`` wins the down
144
+ projection by 18.5% while the stacked gate|up tile prefers ``dot`` by 7.3``%``. ``SWAP_AB``
145
+ is the decode form: weight output rows lead the tile and the FMA reduce replaces the MMA
146
+ (both dot arms keep the token in M). Single return — only the taken branch compiles."""
147
+ if SWAP_AB:
148
+ acc = mx_weight_only_scalar_swapped(
149
+ acc, tl.reshape(a, (BLOCK_SIZE_K,)), w, w_scale,
150
+ N_WIDTH, BLOCK_SIZE_K, SCALE_GROUP_K,
151
+ )
152
+ elif COMPUTE_MODE == "dot_scaled":
153
+ acc = mx_dot_scaled(acc, a, None, w, w_scale)
154
+ else:
155
+ acc = acc + tl.dot(
156
+ a, mx_weight_upcast(w, w_scale, BLOCK_SIZE_K, N_WIDTH, SCALE_GROUP_K, a.dtype)
157
+ )
158
+ return acc
159
+
160
+
161
+ @triton.jit
162
+ def mx_weight_only_scalar_swapped(
163
+ acc,
164
+ a,
165
+ w,
166
+ w_scale,
167
+ ROWS_W: tl.constexpr,
168
+ BLOCK_SIZE_K: tl.constexpr,
169
+ SCALE_GROUP_K: tl.constexpr,
170
+ ):
171
+ """Swapped weight-only reduce: CUDA-core FMA GEVM against RAW bf16 activations (no act
172
+ scale — the weight-only counterpart of ``mx_scalar_reduce_swapped``). Weight
173
+ ``w`` is output-rows-major ``[ROWS_W, BK]``, ``a`` the ``[BK]`` token; the UE8M0 group
174
+ scale factors out of the inner sum, so it costs one multiply per group instead of per
175
+ element. Returns ``acc + [1, ROWS_W]``.
176
+
177
+ This is the decode structure for W4A16: with no MMA there is no M->16 pad to waste, and
178
+ the loop runs at the dequantized weight stream's own rate (gpt-oss gate_up 32.0 -> 13.7us,
179
+ down 19.8 -> 9.1us, against a 12.3us/6us dequant-inclusive load floor)."""
180
+ NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
181
+ wq = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [ROWS_W, BK]
182
+ prod = wq * a.to(tl.float32)[None, :]
183
+ grp = tl.sum(tl.reshape(prod, (ROWS_W, NG, SCALE_GROUP_K)), axis=2) # [ROWS_W, NG]
184
+ return acc + tl.reshape(
185
+ tl.sum(grp * decode_group_scale(w_scale), axis=1), (1, ROWS_W)
186
+ )
187
+
188
+
189
+ @triton.jit
190
+ def mx_scalar_reduce(
191
+ acc,
192
+ a,
193
+ a_scale,
194
+ w,
195
+ w_scale,
196
+ ROWS_W: tl.constexpr,
197
+ BLOCK_SIZE_K: tl.constexpr,
198
+ SCALE_GROUP_K: tl.constexpr,
199
+ SWAP_AB: tl.constexpr = False,
200
+ ):
201
+ """MX 'scalar' step: CUDA-core FMA GEMV, unpacking MXFP4 to fp32 and dequantizing activation +
202
+ weight by their group scales, reducing over K into ``acc``. No tensor core (so no M->16 MMA
203
+ pad) — wins the memory-bound decode GEMV (M=1). The gate_up kernels pass the stacked gate|up
204
+ tile (ROWS_W = 2*BN). The UE8M0 scale is constant within each ``SCALE_GROUP_K`` group, so it
205
+ factors out of the inner sum: reduce the raw products per group, then apply ONE combined
206
+ (act x weight) scale per group — ``SCALE_GROUP_K``x fewer scale-muls (~18% faster, bit-identical
207
+ to the expanded form). Plain: ``a`` [BM, BK] against ``w`` [BK, ROWS_W]. ``SWAP_AB`` (decode):
208
+ ``w`` output-rows-major [ROWS_W, BK] against the [BK] token, MXFP4 unpacked along columns (K),
209
+ no transposes. Both return ``acc + [1, ROWS_W]``."""
210
+ NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
211
+ aq = e2m1_cols_to_f32(a) if a.dtype == tl.uint8 else a.to(tl.float32)
212
+ if SWAP_AB:
213
+ wq = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [ROWS_W, BK]
214
+ grp = tl.sum(tl.reshape(aq[None, :] * wq, (ROWS_W, NG, SCALE_GROUP_K)), axis=2)
215
+ scale = decode_group_scale(a_scale)[None, :] * decode_group_scale(w_scale) # [ROWS_W, NG]
216
+ acc = acc + tl.reshape(tl.sum(grp * scale, axis=1), (1, ROWS_W))
217
+ else:
218
+ wq = e2m1_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [BK, ROWS_W]
219
+ grp = tl.sum(tl.reshape(tl.trans(aq) * wq, (NG, SCALE_GROUP_K, ROWS_W)), axis=1)
220
+ scale = tl.trans(decode_group_scale(a_scale)) * tl.trans(decode_group_scale(w_scale))
221
+ acc = acc + tl.sum(grp * scale, axis=0)[None, :]
222
+ return acc
223
+
224
+
225
+ @triton.jit
226
+ def mx_compute(
227
+ acc,
228
+ a,
229
+ a_scale,
230
+ w,
231
+ w_scale,
232
+ COMPUTE_MODE: tl.constexpr,
233
+ BLOCK_SIZE_N: tl.constexpr,
234
+ BLOCK_SIZE_K: tl.constexpr,
235
+ SCALE_GROUP_K: tl.constexpr,
236
+ SWAP_AB: tl.constexpr = False,
237
+ ):
238
+ """Single-projection MMA step, dispatched on ``COMPUTE_MODE`` (scaled-MMA on the raw weight,
239
+ fp8 ``tl.dot`` + per-group rescale, or the scalar reduce); ``SWAP_AB`` selects each leaf's
240
+ decode form — weight output rows in the MMA M dim, the single token flattened to the [BK] rhs
241
+ (a packed-E2M1 token flattens to its BYTE length; dot_scaled consumes it packed, dot/scalar
242
+ column-unpack it losslessly). The acc shapes diverge across modes and orientations, but only
243
+ the taken constexpr branch compiles, so the single return never has to unify them.
244
+ ``BLOCK_SIZE_N`` is the weight tile's row count — the gate_up kernels pass ``2*BN`` with the
245
+ stacked gate|up tile (split back via ``split_gate_up``): one load and one MMA for both
246
+ projections keeps the native microscaled-MMA M=128 operand at BN=64, doubling the CTAs on the
247
+ parallelism-starved decode grid (dsv4 gate_up 1.34x, bit-exact)."""
248
+ if SWAP_AB:
249
+ if a.dtype == tl.uint8:
250
+ a1 = tl.reshape(a, (BLOCK_SIZE_K // 2,))
251
+ else:
252
+ a1 = tl.reshape(a, (BLOCK_SIZE_K,))
253
+ as1 = tl.reshape(a_scale, (BLOCK_SIZE_K // SCALE_GROUP_K,))
254
+ else:
255
+ a1 = a
256
+ as1 = a_scale
257
+ if COMPUTE_MODE == "dot_scaled":
258
+ acc = mx_dot_scaled(acc, a1, as1, w, w_scale, SWAP_AB, BLOCK_SIZE_K, SCALE_GROUP_K)
259
+ elif COMPUTE_MODE == "dot":
260
+ acc = mx_dot_rescale(acc, a1, as1, w, w_scale, SWAP_AB, BLOCK_SIZE_K)
261
+ else: # scalar
262
+ acc = mx_scalar_reduce(acc, a1, as1, w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB)
263
+ return acc
264
+
265
+
266
+ # ── swap-AB decode compute: M=1 batched GEMV with output rows in the MMA M dim ───────
267
+ #
268
+ # The batched (decode) kernels are structurally M=1, where the sm_100 scaled MMA pads M→128.
269
+ # Putting the WEIGHT's output rows in M (fully used) and the single token in N cuts that pad to
270
+ # the N-atom (16) — a ~1.5× decode win for fp4 (dot_scaled), neutral-to-worse for fp8 (scalar
271
+ # stays memory-bound and wins, so the tuner keeps it). Weight is loaded output-rows-major
272
+ # ``[BN, BK]`` for every mode, so the kernel does ONE load; each helper returns ``[1, BN]``.
273
+
274
+
275
+ # The sm_100 MMA's minimum N tile (16). In the swap path the single decode token sits in the MMA's
276
+ # N dim, so it must be padded up to this width (col 0 = the token, cols 1..15 = zero). It is NOT a
277
+ # block size — BLOCK_SIZE_M stays 1 under swap; this is the token's *padded N extent*, fixed by the
278
+ # hardware. The plain int is the source of truth: host-side consumers (the autotune pruners, which
279
+ # filter configs in ordinary Python) compare against it directly, while kernels read the
280
+ # tl.constexpr view derived from it — the only module-global form a @triton.jit fn can read.
281
+ MMA_N_ATOM_WIDTH = 16
282
+ MMA_N_ATOM = tl.constexpr(MMA_N_ATOM_WIDTH)
283
+
284
+
285
+ @triton.jit
286
+ def swap_pad_rhs(a, BLOCK_SIZE_K: tl.constexpr):
287
+ """Pad the ``[BLOCK_SIZE_K]`` M=1 token to the ``[BLOCK_SIZE_K, MMA_N_ATOM]`` swap-AB MMA rhs —
288
+ only column 0 is the real token (16 is measured, not a hardware floor — see acc_init). Used
289
+ by the M=1 batched / fused-MoE fp8 ``tl.dot`` swap paths (weight output rows in the MMA M dim);
290
+ the caller takes column 0 of the ``[BN, MMA_N_ATOM]`` result after the K-loop."""
291
+ return tl.where(
292
+ tl.arange(0, MMA_N_ATOM)[None, :] == 0,
293
+ a[:, None],
294
+ tl.zeros((BLOCK_SIZE_K, MMA_N_ATOM), a.dtype),
295
+ )
296
+
297
+
298
+ @triton.jit
299
+ def fp8_dot(a, b, SWAP_AB: tl.constexpr, BLOCK_SIZE_K: tl.constexpr):
300
+ """Swap-aware plain ``tl.dot`` for the fp8 (block/tensor-dynamic) paths — no scaling; the caller
301
+ applies its own per-block/per-tensor scales. Swap: weight ``b`` ``[N, BK]`` × the single token
302
+ padded to the N=16 atom → ``[N, MMA_N_ATOM]`` (col 0 real). No-swap: token ``a`` ``[M, BK]`` ×
303
+ weight ``b`` ``[BK, N]`` → ``[M, N]``. ``BLOCK_SIZE_K`` is the contraction tile (the down
304
+ projection passes its intermediate tile). Single return: only the taken branch compiles."""
305
+ if SWAP_AB:
306
+ out = tl.dot(b, swap_pad_rhs(tl.reshape(a, (BLOCK_SIZE_K,)), BLOCK_SIZE_K))
307
+ else:
308
+ out = tl.dot(a, b)
309
+ return out
310
+
311
+
312
+ @triton.jit
313
+ def block_dynamic_dot(
314
+ acc, a, a_s, b, b_s,
315
+ block_k: tl.constexpr, SWAP_AB: tl.constexpr, USE_DOT_SCALED: tl.constexpr,
316
+ FAKE_BATCH: tl.constexpr = False,
317
+ ):
318
+ """Accumulate one block-dynamic (1x128/128x128) fp8 K-tile into ``acc``, oriented by
319
+ ``SWAP_AB``. ``USE_DOT_SCALED`` (UE8M0 scales on a native-M tile): fold the group scales
320
+ into the tcgen05 MMA — the tile's single 128-group scale broadcasts in-register to the
321
+ ``block_k // 32`` group-32 columns ``dot_scaled`` consumes, identical to a 128-group
322
+ rescale but with no 4x scale memory and no software multiply. Else: plain fp8 ``tl.dot``
323
+ + per-group software rescale (``decode_group_scale`` is a no-op on fp32 scales, decodes
324
+ UE8M0). ``FAKE_BATCH`` (single-token decode): ``fp8_dot`` pads the lone token to the MMA N
325
+ atom and both scales broadcast down the weight-row (M) dim. Single-exit if/else so only the
326
+ taken arm type-checks (a trailing fall-through arm would be checked even when an earlier
327
+ branch is taken)."""
328
+ if USE_DOT_SCALED:
329
+ reps: tl.constexpr = block_k // 32
330
+ a_sg = a_s[:, None].broadcast_to(a_s.shape[0], reps)
331
+ b_sg = b_s[:, None].broadcast_to(b_s.shape[0], reps)
332
+ if SWAP_AB:
333
+ acc = tl.dot_scaled(b, b_sg, "e4m3", a, a_sg, "e4m3", acc)
334
+ else:
335
+ acc = tl.dot_scaled(a, a_sg, "e4m3", b, b_sg, "e4m3", acc)
336
+ else:
337
+ # plain fp8 tl.dot + per-group decoded scales, oriented by SWAP_AB (weight rows in the MMA
338
+ # M dim under swap). decode_group_scale: fp32 passthrough, UE8M0 -> 2^(e-127).
339
+ a_sd = decode_group_scale(a_s)
340
+ b_sd = decode_group_scale(b_s)
341
+ if FAKE_BATCH:
342
+ d = fp8_dot(a, b, SWAP_AB, block_k)
343
+ if SWAP_AB: # [BN, N-atom]: weight-row scale down the M=BN dim, act scalar broadcasts
344
+ acc = acc + d * a_sd[:, None] * b_sd[:, None]
345
+ else: # no-swap [BM, BN] GEVM: act scale down M, weight-N scale across N (upstream form)
346
+ acc = acc + d * a_sd[:, None] * b_sd[None, :]
347
+ elif SWAP_AB:
348
+ acc = acc + tl.dot(b, a) * b_sd[:, None] * a_sd[None, :]
349
+ else:
350
+ acc = acc + tl.dot(a, b) * a_sd[:, None] * b_sd[None, :]
351
+ return acc
352
+
353
+
354
+ @triton.jit
355
+ def static_dot(acc, a, b, b_s, SWAP_AB: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, FAKE_BATCH: tl.constexpr):
356
+ """static format K-step: plain (swap-aware) fp8 dot + per-K-block weight rescale. FAKE_BATCH
357
+ (single-token decode) routes the rescale down the weight-row (M) dim; else it broadcasts across
358
+ the N columns. The per-tensor activation scale is applied post-loop."""
359
+ b_sd = decode_group_scale(b_s)
360
+ d = fp8_dot(a, b, SWAP_AB, BLOCK_SIZE_K)
361
+ if FAKE_BATCH:
362
+ acc = acc + d * b_sd[:, None]
363
+ else:
364
+ acc = acc + d * b_sd[None, :]
365
+ return acc
366
+
367
+
build/torch-rocm/moe.py ADDED
@@ -0,0 +1,864 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """MoE forwards — thin orchestrations over the base ``matmul_grouped`` / ``matmul_batched`` ops.
16
+
17
+ The base ops carry the gate|up fusion (SwiGLU + FP8/MX requant) and the gather/scatter row
18
+ maps, so both the fused and unfused MoE forwards are pure sequencing here — no MoE-specific
19
+ kernels live in this module:
20
+
21
+ fused: gate_up (``gate=True`` + ``quantize_output=True``) -> down -> ``weighted_reduce``. The
22
+ SwiGLU + intermediate requant happen inside the gate_up kernel epilogue. A model's
23
+ ``post_expert_norm`` runs on the down's routed rows, before the reduce.
24
+ unfused: gate_up (plain GEMM) -> host ``apply_glu`` -> down (plain GEMM) -> ``weighted_reduce``.
25
+ The activation + requant happen between two plain GEMMs; the GEMMs self-quantize their
26
+ raw inputs (``As=None``). Same math as the fused path, split across kernels.
27
+
28
+ grouped (prefill) shares one on-device routing pass (``compute_grouped_scheduling``): gate_up
29
+ gathers hidden by routed row and leaves its output expert-ordered; down reads it in place and
30
+ scatters to routed rows. batched (decode) dispatches per token: ``gather_idx`` reads each routed
31
+ row from the unexpanded hidden in-kernel (no copy), and EP-sentinel rows (``id >= num_experts``)
32
+ are left uninit by the GEMM and skipped in ``weighted_reduce``. ``moe_fused_*`` / ``moe_unfused_*`` are format-neutral:
33
+ the base ops dispatch on the weight dtypes / scale layout (block-dynamic FP8, MXFP4/MXFP8,
34
+ NVFP4), and every forward takes ``activation_format`` naming the block's activation quantization
35
+ (``None`` = the weights' own format, ``"bf16"`` = weight-only, or an explicit format such as
36
+ ``"mxfp8"`` on MXFP4 weights for W4A8)."""
37
+
38
+ import functools
39
+ from collections.abc import Callable
40
+
41
+ import torch
42
+ import torch.nn.functional as F
43
+ import triton
44
+ import triton.language as tl
45
+ from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
46
+
47
+ from .grouped import matmul_grouped
48
+ from .batched import GATE_UNSTACK_MAX_S, matmul_batched
49
+ from .bayesian_autotuner import bayesian_autotune
50
+ from .compat import MX_SCALE_GROUP_K, NVFP4_SCALE_GROUP_K, compile_time_only_triton_wrap, decode_pdl, device_context
51
+ from .formats import get_supported_act_fns, is_mx, is_mxfp4, weight_format
52
+ from .norm import norm_column_factor, rms_inv_rows, rms_norm_rows
53
+ from .quant import _launch_act_quant
54
+ from .scheduling import compute_grouped_scheduling
55
+ from .epilogue import fused_glu
56
+
57
+
58
+ @bayesian_autotune(
59
+ [
60
+ triton.Config({"BLOCK_H": block_h}, num_warps=warps)
61
+ for block_h in (256, 512, 1024, 2048)
62
+ for warps in (4, 8)
63
+ ],
64
+ # the H tile width trades off against grid occupancy: at few groups (decode) narrow
65
+ # tiles spread more H-blocks across SMs, at many groups (prefill) wide tiles amortize
66
+ # the per-row weight load — so key on H and the group-count bucket.
67
+ ["H", "num_groups_bit_length", "NORM"],
68
+ n_trials=8,
69
+ )
70
+ @triton.jit
71
+ def weighted_reduce_kernel(
72
+ Rows, # (num_groups * NUM_TOP_K, H) — rows to reduce, group-major
73
+ Out, # (num_groups, H) — one reduced row per group
74
+ Ids, # (num_groups, NUM_TOP_K) — per-row id; a row is skipped when its id >= NUM_EXPERTS
75
+ Weights, # (num_groups * NUM_TOP_K,) — per-row scale
76
+ NormWeight, # (H,) fused post-expert norm weight; None (with NormInv) folds the arm out
77
+ NormInv, # (num_groups * NUM_TOP_K,) fp32 per-row rsqrt from rms_inv_rows
78
+ H,
79
+ stride_rows_m,
80
+ stride_rows_h,
81
+ stride_o_m,
82
+ stride_o_h,
83
+ stride_ids_m,
84
+ stride_ids_k,
85
+ num_groups_bit_length, # autotune key only (log2 group-count bucket); unused in body
86
+ NUM_TOP_K: tl.constexpr,
87
+ NUM_EXPERTS: tl.constexpr,
88
+ BLOCK_H: tl.constexpr,
89
+ NORM: tl.constexpr = None, # a get_supported_norms() name folded into the reduce
90
+ SIMULATE_UNFUSED: tl.constexpr = False,
91
+ PDL: tl.constexpr = False,
92
+ ):
93
+ """Per group ``g``, the weighted sum of its ``NUM_TOP_K`` rows into ``Out[g]``:
94
+ ``sum_k Weights[g*NUM_TOP_K + k] * Rows[g*NUM_TOP_K + k]``, skipping rows whose id is
95
+ ``>= NUM_EXPERTS`` (out-of-range rows are never written upstream and contribute 0).
96
+ fp32 accumulate; ~2.8x a generic ``view(g, k, H).sum(1)``. ``SIMULATE_UNFUSED`` rounds
97
+ each weighted row to ``Out``'s dtype before summing, matching a reference that weights
98
+ in that dtype; production leaves the accumulation in fp32.
99
+
100
+ ``NORM`` folds a per-expert output norm into the pass: each row is scaled by its
101
+ ``NormInv`` (``rms_inv_rows``, one pass ahead of this one) and the norm's column factor
102
+ before the routing weight, so the normalized rows are never written or re-read."""
103
+ if PDL:
104
+ gdc_wait()
105
+ g = tl.program_id(0)
106
+ offs_h = tl.program_id(1) * BLOCK_H + tl.arange(0, BLOCK_H)
107
+ mask = offs_h < H
108
+ acc = tl.zeros((BLOCK_H,), tl.float32)
109
+ if NORM is not None: # the tile's column factor is row-independent — load it once
110
+ factor = norm_column_factor(NormWeight, offs_h, mask, NORM)
111
+ for k in tl.static_range(NUM_TOP_K):
112
+ flat = g * NUM_TOP_K + k
113
+ valid = tl.load(Ids + g * stride_ids_m + k * stride_ids_k) < NUM_EXPERTS
114
+ weight = tl.load(Weights + flat)
115
+ row = tl.load(
116
+ Rows + flat * stride_rows_m + offs_h * stride_rows_h,
117
+ mask=mask & valid,
118
+ other=0.0,
119
+ ).to(tl.float32)
120
+ if NORM is not None:
121
+ # masked like the value load: a sentinel row's rsqrt comes off uninitialized memory,
122
+ # and 0 * NaN would poison the sum
123
+ row = row * factor * tl.load(NormInv + flat, mask=valid, other=0.0)
124
+ if SIMULATE_UNFUSED: # the reference materializes the normalized row in Out's dtype
125
+ row = row.to(Out.dtype.element_ty).to(tl.float32)
126
+ contrib = weight * row
127
+ if SIMULATE_UNFUSED:
128
+ contrib = contrib.to(Out.dtype.element_ty).to(tl.float32)
129
+ acc += contrib
130
+ if PDL:
131
+ gdc_launch_dependents()
132
+ tl.store(
133
+ Out + g * stride_o_m + offs_h * stride_o_h,
134
+ acc.to(Out.dtype.element_ty),
135
+ mask=mask,
136
+ )
137
+
138
+
139
+
140
+ def weighted_reduce(
141
+ rows: torch.Tensor,
142
+ top_k_index: torch.Tensor,
143
+ top_k_weights: torch.Tensor,
144
+ num_experts: int,
145
+ simulate_unfused: bool = False,
146
+ norm: str | None = None,
147
+ norm_weight: torch.Tensor | None = None,
148
+ norm_inv: torch.Tensor | None = None,
149
+ ) -> torch.Tensor:
150
+ """Routing-weighted top-k reduce — the bookend of the fused-MoE chain. Folds each token's
151
+ ``num_top_k`` expert-output rows (``rows``, group-major, scaled by ``top_k_weights``, with
152
+ EP-sentinel rows ``id >= num_experts`` skipped) from the routed-row layout back to
153
+ ``(num_tokens, H)``. See ``weighted_reduce_kernel``. ``norm`` (a ``get_supported_norms()``
154
+ name, with ``norm_weight`` and the ``rms_inv_rows`` ``norm_inv``) folds a per-expert output
155
+ norm into this pass rather than normalizing the rows in one of their own."""
156
+ assert (norm is None) == (norm_inv is None), "a fused norm needs its per-row rsqrt"
157
+ num_tokens, num_top_k = top_k_index.shape
158
+ H = rows.size(1)
159
+ reduced = torch.empty(num_tokens, H, device=rows.device, dtype=rows.dtype)
160
+ with device_context(rows.device):
161
+ compile_time_only_triton_wrap(weighted_reduce_kernel)[
162
+ lambda meta: (num_tokens, triton.cdiv(H, meta["BLOCK_H"]))
163
+ ](
164
+ rows,
165
+ reduced,
166
+ top_k_index,
167
+ top_k_weights,
168
+ norm_weight,
169
+ norm_inv,
170
+ H,
171
+ rows.stride(0),
172
+ rows.stride(1),
173
+ reduced.stride(0),
174
+ reduced.stride(1),
175
+ top_k_index.stride(0),
176
+ top_k_index.stride(1),
177
+ num_groups_bit_length=int(num_tokens).bit_length(),
178
+ NUM_TOP_K=num_top_k,
179
+ NUM_EXPERTS=num_experts,
180
+ NORM=norm,
181
+ SIMULATE_UNFUSED=simulate_unfused,
182
+ PDL=decode_pdl(),
183
+ launch_pdl=decode_pdl(),
184
+ )
185
+ return reduced
186
+
187
+
188
+ def _validate_moe(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale):
189
+ """gate_up and down must share the format (both MX or both block-dynamic FP8 — the
190
+ intermediate handed between them carries one quant format). Returns whether the format
191
+ is MX (the fused dispatchers branch on it); the fp8 quantization block is derived from
192
+ the scale shapes (``weight_block_size``), never passed. Scales are the pure block scales
193
+ (per the decoupled API — per-tensor globals ride as separate ``*_global_scale`` args);
194
+ the format predicates read the block scale's dtype/grouping."""
195
+ gate_up_is_mx = is_mx(gate_up_proj, gate_up_proj_scale)
196
+ if gate_up_is_mx != is_mx(down_proj, down_proj_scale):
197
+ raise ValueError(
198
+ "gate_up_proj and down_proj must use the same format (both MX or both block-dynamic FP8)."
199
+ )
200
+ if is_mxfp4(gate_up_proj, gate_up_proj_scale) != is_mxfp4(down_proj, down_proj_scale):
201
+ raise ValueError("gate_up_proj and down_proj must use the same MX format (both MXFP4 or both MXFP8).")
202
+ return gate_up_is_mx
203
+
204
+
205
+ def _gather_idx(top_k_index: torch.Tensor) -> torch.Tensor:
206
+ """The batched routed-row gather: routed row ``s`` (``= t*K + k``) reads token ``s // num_top_k``
207
+ of the unexpanded hidden. ``matmul_batched`` applies it in-kernel, so no ``(S, H)`` copy.
208
+ The map depends only on the SHAPE, so it is cached per (tokens, top_k, device) — its two
209
+ elementwise launches (~3µs) were the largest non-GEMM cost of the fp8 decode chain."""
210
+ return _gather_idx_cached(
211
+ top_k_index.shape[0], top_k_index.shape[1], top_k_index.device
212
+ )
213
+
214
+
215
+ @functools.lru_cache(maxsize=64)
216
+ def _gather_idx_cached(
217
+ num_tokens: int, num_top_k: int, device: torch.device
218
+ ) -> torch.Tensor:
219
+ return (
220
+ torch.arange(num_tokens * num_top_k, device=device, dtype=torch.int32)
221
+ // num_top_k
222
+ )
223
+
224
+
225
+ def _torch_weighted_reduce(down_out, top_k_index, top_k_weights, num_experts):
226
+ """Naive (unfused) routing-weighted top-k reduce in plain torch — NOT the fused
227
+ ``weighted_reduce`` kernel. Materializes the (bf16) weighted contribs, masks EP-sentinel rows
228
+ (``id >= num_experts``, left uninit in ``down_out``) to 0, and torch-sums to ``(num_tokens, H)``
229
+ (fp32 accumulate, activation-dtype out). This is the independent reference the fused
230
+ ``weighted_reduce`` is checked against; the fused path's ``simulate_unfused`` reproduces its
231
+ bf16-contrib rounding."""
232
+ num_tokens, num_top_k = top_k_index.shape
233
+ dropped = (top_k_index.reshape(-1) >= num_experts).reshape(-1, 1)
234
+ # masked in place on the PRODUCT: a sentinel row's down_out is uninitialized, so zeroing the
235
+ # weight instead would leave 0 * NaN == NaN.
236
+ contrib = down_out * top_k_weights.reshape(-1, 1)
237
+ contrib.masked_fill_(dropped, 0)
238
+ return contrib.view(num_tokens, num_top_k, down_out.size(1)).sum(dim=1)
239
+
240
+
241
+ def _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate):
242
+ """The activation on the host, for the arms the epilogue does not fuse: a callable takes the
243
+ raw GEMM output (interleaved gate|up columns when ``gate``) and returns the intermediate —
244
+ the caller owns its semantics, so any activation runs without a kernel change; a name from
245
+ ``get_supported_act_fns()`` runs the torch GLU (``gate``) or the bare activation."""
246
+ if callable(act_fn):
247
+ return act_fn(gate_up_out)
248
+ if act_fn not in get_supported_act_fns():
249
+ raise ValueError(f"act_fn must be one of {get_supported_act_fns()} or a callable, got {act_fn!r}")
250
+ if gate:
251
+ return fused_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit)
252
+ return {"silu": F.silu, "gelu": F.gelu, "relu": F.relu}[act_fn](gate_up_out)
253
+
254
+
255
+ def _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate) -> dict:
256
+ """The gate_up GEMM's fused-GLU kwargs when the activation is a fusable name; ``{}`` (a plain
257
+ GEMM) sends a callable — or an unfusable name, rejected in ``_host_glu`` — to the host."""
258
+ if not isinstance(act_fn, str) or act_fn not in get_supported_act_fns():
259
+ return {}
260
+ return dict(
261
+ gate=gate,
262
+ act_fn=act_fn,
263
+ swiglu_alpha=swiglu_alpha,
264
+ swiglu_limit=swiglu_limit,
265
+ simulate_unfused=simulate_unfused,
266
+ )
267
+
268
+
269
+ # ── Fused (gate_up epilogue owns SwiGLU + intermediate requant) ──────────────
270
+
271
+
272
+ def _block_format(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale, activation_format):
273
+ """The MoE block's activation format, resolved to a name: validates the weight pairing; an
274
+ explicit ``activation_format`` (``"bf16"`` weight-only, or a format) is respected as-is,
275
+ ``None`` follows the weights (fp8 / mxfp8 / mxfp4 / nvfp4 — mxfp4 weights default to mxfp4
276
+ activations, the all-fp4 W4A4 chain; unquantized BF16/FP16 weights carry no scales, so their
277
+ activations stay ``"bf16"``, the full-precision path)."""
278
+ _validate_moe(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale)
279
+ if activation_format is not None:
280
+ return activation_format
281
+ if gate_up_proj_scale is None:
282
+ return "bf16"
283
+ return weight_format(gate_up_proj, gate_up_proj_scale)
284
+
285
+
286
+ def _post_expert_norm(down_out, post_expert_norm, weight, eps):
287
+ """A model's per-expert output norm applied to the down projection's routed rows — one row
288
+ per (token, expert) application, which is what such a norm is defined over. ``None`` passes
289
+ through; a ``get_supported_norms()`` name runs ``rms_norm_rows`` against ``weight``; anything
290
+ else is a host callable, like ``act_fn``'s."""
291
+ if post_expert_norm is None:
292
+ return down_out
293
+ if isinstance(post_expert_norm, str):
294
+ return rms_norm_rows(down_out, weight, eps, post_expert_norm)
295
+ return post_expert_norm(down_out)
296
+
297
+
298
+ def _fused_post_expert_norm(down_out, post_expert_norm, weight, eps):
299
+ """``(rows, weighted_reduce kwargs)`` for a fused forward: a named norm rides INTO the reduce
300
+ — one pass for the row's ``rsqrt``, then the reduce it already runs applies that and the
301
+ column factor — so the normalized rows are never materialized. A callable has to run first."""
302
+ if isinstance(post_expert_norm, str):
303
+ return down_out, {
304
+ "norm": post_expert_norm,
305
+ "norm_weight": weight,
306
+ "norm_inv": rms_inv_rows(down_out, weight, eps, post_expert_norm),
307
+ }
308
+ return _post_expert_norm(down_out, post_expert_norm, weight, eps), {}
309
+
310
+
311
+ def moe_fused_grouped(
312
+ hidden_states: torch.Tensor, # (T, H)
313
+ top_k_index: torch.Tensor, # (T, K) int
314
+ top_k_weights: torch.Tensor, # (T, K)
315
+ gate_up_proj: torch.Tensor, # (E, 2I, H)
316
+ down_proj: torch.Tensor, # (E, H, I)
317
+ gate_up_proj_scale_inv: torch.Tensor,
318
+ down_proj_scale_inv: torch.Tensor,
319
+ gate_up_proj_weight_global_scale: torch.Tensor | None = None,
320
+ down_proj_weight_global_scale: torch.Tensor | None = None,
321
+ gate_up_proj_input_global_scale: torch.Tensor | None = None,
322
+ down_proj_input_global_scale: torch.Tensor | None = None,
323
+ post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
324
+ # get_supported_norms() name (fused into the reduce) or a host callable
325
+ post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
326
+ post_expert_norm_eps: float = 1e-6,
327
+ gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
328
+ down_proj_bias: torch.Tensor | None = None, # (E, H)
329
+ act_fn: str | Callable = "silu",
330
+ swiglu_alpha: float | None = None,
331
+ swiglu_limit: float | None = None,
332
+ simulate_unfused: bool = False,
333
+ activation_format: str | None = None,
334
+ gate: bool = True,
335
+ ) -> torch.Tensor:
336
+ """Fused grouped MoE (prefill): gather gate_up + SiLU + requant epilogue → quantized
337
+ expert-ordered intermediate → grouped down → routing-weighted top-k reduce. Returns
338
+ ``(num_tokens, hidden_dim)``. The base ops dispatch on the weight dtypes / scale
339
+ layout (block-dynamic FP8, MXFP8/MXFP4, NVFP4); ``activation_format`` names the activation
340
+ quantization for the whole block — activations and the fused intermediate requant
341
+ carry it (``"mxfp4"``/``"nvfp4"`` run all-fp4 W4A4 chains, ``"bf16"`` is weight-only);
342
+ ``None`` follows the weight format, and the ops validate the pairing. The
343
+ ``*_proj_input_global_scale`` pair is the NVFP4 activation second level (a checkpoint's
344
+ calibrated per-projection ``input_scale``): the gate_up quantizes hidden against its own,
345
+ requants the intermediate against the down's, and the down consumes that as its activation
346
+ global. ``None`` is dynamic quant everywhere else. ``simulate_unfused`` (testing) rounds each step through
347
+ the activation dtype so the output matches the unfused reference to reduce order. ``act_fn`` is a
348
+ ``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
349
+ callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
350
+ projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
351
+ — the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
352
+ fmt = _block_format(
353
+ gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
354
+ )
355
+ num_top_k = top_k_index.size(-1)
356
+ glu = _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate)
357
+ NUM_EXPERTS = gate_up_proj.size(0)
358
+ expert_start, gather_idx, scatter_idx = compute_grouped_scheduling(
359
+ top_k_index, NUM_EXPERTS, num_top_k
360
+ )
361
+
362
+ # Phase 1: gate_up + SiLU + requant in the block format -> expert-ordered quantized
363
+ # intermediate (the op quantizes the raw hidden itself and owns the expand-vs-gather
364
+ # regime policy — this forward is pure sequencing). scatter_idx=None: the down reads
365
+ # the intermediate in place. (C, Cs) under a requant format; a bare Tensor otherwise.
366
+ gate_up_out = matmul_grouped(
367
+ hidden_states,
368
+ gate_up_proj,
369
+ Bs=gate_up_proj_scale_inv,
370
+ a_global_scale=gate_up_proj_input_global_scale,
371
+ b_global_scale=gate_up_proj_weight_global_scale,
372
+ # the intermediate requant normalizes against the DOWN's calibrated input global,
373
+ # which the down then consumes as its activation global — the two-level handoff
374
+ output_global_scale=down_proj_input_global_scale,
375
+ expert_start=expert_start,
376
+ **glu,
377
+ bias=gate_up_proj_bias,
378
+ # fmt is the resolved format; "bf16" (weight-only) leaves the GLU intermediate bf16,
379
+ # no requant.
380
+ activation_format=fmt,
381
+ quantize_output=bool(glu) and fmt != "bf16",
382
+ output_dtype=hidden_states.dtype,
383
+ gather_idx=gather_idx,
384
+ )
385
+ inter, inter_scale = (
386
+ gate_up_out if isinstance(gate_up_out, tuple) else (gate_up_out, None)
387
+ )
388
+ if not glu:
389
+ inter = _host_glu(inter, act_fn, swiglu_alpha, swiglu_limit, gate)
390
+ # Phase 2: grouped down over the expert-ordered pre-quantized intermediate (its dtypes
391
+ # carry the format; gather_idx=None), scattering to routed rows (scatter_idx).
392
+ down_out = matmul_grouped(
393
+ inter,
394
+ down_proj,
395
+ As=inter_scale,
396
+ Bs=down_proj_scale_inv,
397
+ a_global_scale=down_proj_input_global_scale,
398
+ b_global_scale=down_proj_weight_global_scale,
399
+ expert_start=expert_start,
400
+ bias=down_proj_bias,
401
+ # weight-only: the intermediate is bf16 (As None) — the down goes weight-only too; a
402
+ # quantized intermediate carries its format in its own dtypes.
403
+ activation_format=fmt if inter_scale is None else None,
404
+ output_dtype=hidden_states.dtype,
405
+ scatter_idx=scatter_idx,
406
+ )
407
+
408
+ # Phase 3: routing-weighted top-k reduce -> (num_tokens, hidden_dim). simulate_unfused
409
+ # rounds each weighted contrib to the activation dtype before summing, matching the
410
+ # unfused path's torch reduce (which materializes bf16 contribs); production
411
+ # accumulates in fp32.
412
+ rows, norm_kwargs = _fused_post_expert_norm(
413
+ down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
414
+ )
415
+ return weighted_reduce(
416
+ rows, top_k_index, top_k_weights, NUM_EXPERTS, simulate_unfused, **norm_kwargs
417
+ )
418
+
419
+
420
+ def moe_fused_batched(
421
+ hidden_states: torch.Tensor, # (T, H)
422
+ top_k_index: torch.Tensor, # (T, K) int
423
+ top_k_weights: torch.Tensor, # (T, K)
424
+ gate_up_proj: torch.Tensor, # (E, 2I, H)
425
+ down_proj: torch.Tensor, # (E, H, I)
426
+ gate_up_proj_scale_inv: torch.Tensor,
427
+ down_proj_scale_inv: torch.Tensor,
428
+ gate_up_proj_weight_global_scale: torch.Tensor | None = None,
429
+ down_proj_weight_global_scale: torch.Tensor | None = None,
430
+ gate_up_proj_input_global_scale: torch.Tensor | None = None,
431
+ down_proj_input_global_scale: torch.Tensor | None = None,
432
+ post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
433
+ # get_supported_norms() name (fused into the reduce) or a host callable
434
+ post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
435
+ post_expert_norm_eps: float = 1e-6,
436
+ gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
437
+ down_proj_bias: torch.Tensor | None = None, # (E, H)
438
+ act_fn: str | Callable = "silu",
439
+ swiglu_alpha: float | None = None,
440
+ swiglu_limit: float | None = None,
441
+ simulate_unfused: bool = False,
442
+ activation_format: str | None = None,
443
+ gate: bool = True,
444
+ ) -> torch.Tensor:
445
+ """Fused batched MoE (decode): gate_up + SiLU + requant epilogue → per-row quantized
446
+ intermediate → batched down → routing-weighted top-k reduce. Returns
447
+ ``(num_tokens, hidden_dim)``. The base ops dispatch on the weight dtypes / scale
448
+ layout (block-dynamic FP8, MXFP8/MXFP4, NVFP4 — decode runs the software/swap arms
449
+ below the native mxf4nvf4 M=128 staging); ``activation_format`` names the activation
450
+ quantization for the whole block — activations and the fused intermediate requant
451
+ carry it (``"mxfp4"`` runs the all-fp4 W4A4 chain, ``"bf16"`` is weight-only); ``None``
452
+ follows the weight format, and the ops validate the pairing. The
453
+ ``*_proj_input_global_scale`` pair is the NVFP4 activation second level (a checkpoint's
454
+ calibrated per-projection ``input_scale``): the gate_up quantizes hidden against its own,
455
+ requants the intermediate against the down's, and the down consumes that as its activation
456
+ global. ``None`` is dynamic quant everywhere else. ``simulate_unfused`` (testing) rounds each
457
+ step through the activation dtype so the output matches the unfused reference to
458
+ reduce order. ``act_fn`` is a
459
+ ``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
460
+ callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
461
+ projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
462
+ — the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
463
+ fmt = _block_format(
464
+ gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
465
+ )
466
+ glu = _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate)
467
+ NUM_EXPERTS = gate_up_proj.size(0)
468
+ expert_ids = top_k_index.reshape(-1)
469
+ gather_idx = _gather_idx(top_k_index)
470
+
471
+ # Phase 1: gate_up + SiLU + requant in the block format -> per-row quantized
472
+ # intermediate (the op quantizes the raw activations). gather_idx reads each routed
473
+ # row from the unexpanded hidden in-kernel (no copy).
474
+ # (C, Cs) under a requant format; a bare Tensor on the full-precision path
475
+ gate_up_out = matmul_batched(
476
+ hidden_states,
477
+ gate_up_proj,
478
+ Bs=gate_up_proj_scale_inv,
479
+ a_global_scale=gate_up_proj_input_global_scale,
480
+ b_global_scale=gate_up_proj_weight_global_scale,
481
+ # the two-level handoff, as in the grouped sibling
482
+ output_global_scale=down_proj_input_global_scale,
483
+ expert_ids=expert_ids,
484
+ **glu,
485
+ bias=gate_up_proj_bias,
486
+ # Decode (batched): "bf16" (weight-only) leaves the intermediate bf16, no requant.
487
+ # Block-FP8: INSIDE the unstacked decode band the requant fuses into the GLU kernel
488
+ # (``fused_glu(quant_group=...)`` — one launch, hands the down a ready fp8+scales intermediate and
489
+ # kills its offline act quant); ABOVE the band the stacked epilogue's requant pins
490
+ # the gate|up tile to the whole block scale and halves the grid, so the bf16 handoff
491
+ # (down inline-quants) stays the win there.
492
+ activation_format=fmt,
493
+ quantize_output=(
494
+ bool(glu) and fmt != "bf16" and (fmt != "fp8" or expert_ids.numel() <= GATE_UNSTACK_MAX_S)
495
+ ),
496
+ output_dtype=hidden_states.dtype,
497
+ gather_idx=gather_idx,
498
+ )
499
+ inter, inter_scale = (
500
+ gate_up_out if isinstance(gate_up_out, tuple) else (gate_up_out, None)
501
+ )
502
+ if not glu:
503
+ inter = _host_glu(inter, act_fn, swiglu_alpha, swiglu_limit, gate)
504
+ # Phase 2: batched down over the intermediate (its dtypes carry the format; already
505
+ # routed-order, no gather).
506
+ down_out = matmul_batched(
507
+ inter,
508
+ down_proj,
509
+ As=inter_scale,
510
+ Bs=down_proj_scale_inv,
511
+ a_global_scale=down_proj_input_global_scale,
512
+ b_global_scale=down_proj_weight_global_scale,
513
+ expert_ids=expert_ids,
514
+ bias=down_proj_bias,
515
+ # weight-only / block-FP8: the intermediate is bf16 (As is None), so the down carries the
516
+ # format and quantizes it, mirroring the unfused sibling; a quantized MX intermediate
517
+ # carries its format in its own dtypes.
518
+ activation_format=fmt if inter_scale is None or fmt == "fp8" else None,
519
+ output_dtype=hidden_states.dtype,
520
+ )
521
+ # Phase 3: routing-weighted top-k reduce -> (num_tokens, hidden_dim). simulate_unfused
522
+ # rounds each weighted contrib to the activation dtype before summing, matching the
523
+ # unfused path's torch reduce (which materializes bf16 contribs); production
524
+ # accumulates in fp32.
525
+ rows, norm_kwargs = _fused_post_expert_norm(
526
+ down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
527
+ )
528
+ return weighted_reduce(
529
+ rows, top_k_index, top_k_weights, NUM_EXPERTS, simulate_unfused, **norm_kwargs
530
+ )
531
+
532
+
533
+ # ── Unfused (plain GEMMs + host GLU) ──────────────────────────────────────────
534
+
535
+
536
+ def moe_unfused_grouped(
537
+ hidden_states: torch.Tensor,
538
+ top_k_index: torch.Tensor,
539
+ top_k_weights: torch.Tensor,
540
+ gate_up_proj: torch.Tensor,
541
+ down_proj: torch.Tensor,
542
+ gate_up_proj_scale_inv: torch.Tensor,
543
+ down_proj_scale_inv: torch.Tensor,
544
+ gate_up_proj_weight_global_scale: torch.Tensor | None = None,
545
+ down_proj_weight_global_scale: torch.Tensor | None = None,
546
+ gate_up_proj_input_global_scale: torch.Tensor | None = None,
547
+ down_proj_input_global_scale: torch.Tensor | None = None,
548
+ post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
549
+ # get_supported_norms() name (fused into the reduce) or a host callable
550
+ post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
551
+ post_expert_norm_eps: float = 1e-6,
552
+ gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
553
+ down_proj_bias: torch.Tensor | None = None, # (E, H)
554
+ act_fn: str | Callable = "silu",
555
+ swiglu_alpha: float | None = None,
556
+ swiglu_limit: float | None = None,
557
+ activation_format: str | None = None,
558
+ gate: bool = True,
559
+ ) -> torch.Tensor:
560
+ """Unfused grouped MoE: gate_up (plain grouped GEMM, gather hidden) → host ``apply_glu`` →
561
+ down (plain grouped GEMM, scatter to routed rows) → routing-weighted reduce. Same math as
562
+ ``moe_fused_grouped`` but the SwiGLU + intermediate quant happen between two plain GEMMs
563
+ rather than inside the gate_up epilogue; each GEMM quantizes its raw input in
564
+ ``activation_format`` (``None`` follows the weight format, mirroring the fused forward — mxfp4
565
+ weights run the all-fp4 W4A4 chain). All formats route through the shared ``matmul_grouped``. The NVFP4 activation globals thread the same way as the fused sibling:
566
+ each GEMM quantizes its raw input against its ``*_input_global_scale``. ``act_fn`` is a
567
+ ``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
568
+ callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
569
+ projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
570
+ — the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
571
+ fmt = _block_format(
572
+ gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
573
+ )
574
+
575
+ num_top_k = top_k_index.size(-1)
576
+ NUM_EXPERTS = gate_up_proj.size(0)
577
+ expert_start, gather_idx, scatter_idx = compute_grouped_scheduling(
578
+ top_k_index, NUM_EXPERTS, num_top_k
579
+ )
580
+
581
+ # gate_up as a plain GEMM (no gate epilogue) over gathered hidden -> expert-ordered (S, 2I).
582
+ gate_up_out = matmul_grouped(
583
+ hidden_states,
584
+ gate_up_proj,
585
+ Bs=gate_up_proj_scale_inv,
586
+ a_global_scale=gate_up_proj_input_global_scale,
587
+ b_global_scale=gate_up_proj_weight_global_scale,
588
+ expert_start=expert_start,
589
+ bias=gate_up_proj_bias,
590
+ activation_format=fmt,
591
+ output_dtype=hidden_states.dtype,
592
+ gather_idx=gather_idx,
593
+ )
594
+ inter = _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate)
595
+ # down over the expert-ordered intermediate (quantized in the same format), scattering
596
+ # to routed rows.
597
+ down_out = matmul_grouped(
598
+ inter,
599
+ down_proj,
600
+ Bs=down_proj_scale_inv,
601
+ a_global_scale=down_proj_input_global_scale,
602
+ b_global_scale=down_proj_weight_global_scale,
603
+ expert_start=expert_start,
604
+ bias=down_proj_bias,
605
+ activation_format=fmt,
606
+ output_dtype=hidden_states.dtype,
607
+ scatter_idx=scatter_idx,
608
+ )
609
+ return _torch_weighted_reduce(
610
+ _post_expert_norm(down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps),
611
+ top_k_index, top_k_weights, NUM_EXPERTS,
612
+ )
613
+
614
+
615
+ def moe_torch_grouped(
616
+ hidden_states: torch.Tensor, # (T, H)
617
+ top_k_index: torch.Tensor, # (T, K) int
618
+ top_k_weights: torch.Tensor, # (T, K)
619
+ gate_up_proj: torch.Tensor, # (E, 2I, H) E4M3
620
+ down_proj: torch.Tensor, # (E, H, I) E4M3
621
+ gate_up_proj_scale_inv: torch.Tensor, # gate_up scale through torchao's triton_mx_block_rearrange_per_group_3d (NOT swizzle_mx_scales)
622
+ down_proj_scale_inv: torch.Tensor, # down scale through the same torchao rearrange
623
+ gate_up_proj_weight_global_scale: torch.Tensor | None = None,
624
+ down_proj_weight_global_scale: torch.Tensor | None = None,
625
+ gate_up_proj_input_global_scale: torch.Tensor | None = None,
626
+ down_proj_input_global_scale: torch.Tensor | None = None,
627
+ post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
628
+ # get_supported_norms() name (fused into the reduce) or a host callable
629
+ post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
630
+ post_expert_norm_eps: float = 1e-6,
631
+ gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
632
+ down_proj_bias: torch.Tensor | None = None, # (E, H)
633
+ act_fn: str = "silu",
634
+ swiglu_alpha: float | None = None,
635
+ swiglu_limit: float | None = None,
636
+ activation_format: str | None = None,
637
+ ) -> torch.Tensor:
638
+ """Torch-only MX grouped MoE — the fair cuBLAS baseline for ``moe_fused_grouped`` /
639
+ ``moe_unfused_grouped`` on the PUBLIC ``torch.nn.functional.scaled_grouped_mm``. Same weights,
640
+ scales, and routing as our forwards; the only difference is the machinery torch forces:
641
+
642
+ - Routing by **sort**, not our on-device gather/scatter: stable-argsort the ``T*K`` routed slots
643
+ by expert into contiguous groups (cumulative ``offs``).
644
+ - Two ``scaled_grouped_mm`` calls (per-format ``ScalingType``: group-32 ``BlockWise1x32`` for
645
+ mxfp8/mxfp4, group-16 ``BlockWise1x16`` for nvfp4; fp4 operands viewed as ``e2m1_x2``).
646
+ - Our Triton MX act-quant (so torch is timed on the same fast quant), the shared host ``apply_glu``,
647
+ and the shared ``_torch_weighted_reduce``. All three MX formats.
648
+
649
+ WEIGHT scales arrive already SWIZZLE_32_4_4-blocked by **torchao's**
650
+ ``triton_mx_block_rearrange_per_group_3d`` (done once offline — a real deployment doesn't
651
+ reblock a fixed weight every forward); this is scaled_grouped_mm's own layout, NOT the
652
+ ``swizzle_mx_scales`` artifact the other four forwards consume. The timed loop only blocks the
653
+ ACTIVATION scale (which changes each call). The format is read off the dtypes (the block
654
+ preserves them: E4M3 scale = NVFP4, uint8 = MX; packed-E2M1 weight = int8) since the blocked
655
+ shape no longer matches the group-shape detectors."""
656
+ assert gate_up_proj.dtype in (torch.int8, torch.float8_e4m3fn), (
657
+ "torch grouped baseline is MX-only (packed E2M1 or E4M3 weights)"
658
+ )
659
+ assert gate_up_proj_scale_inv.ndim not in (5, 6) and down_proj_scale_inv.ndim not in (5, 6), (
660
+ "the torch baseline consumes torchao's triton_mx_block_rearrange_per_group_3d layout, "
661
+ "not the swizzle_mx_scales artifact — a byte-compatible wrong layout would misread silently"
662
+ )
663
+ assert activation_format != "bf16", (
664
+ "the torch baseline always quantizes activations (scaled_grouped_mm has no bf16-act x "
665
+ "MX-weight form) — activation_format='bf16' (W4A16/W8A16) is not representable here"
666
+ )
667
+
668
+ import torch.nn.functional as F
669
+ from torch.nn.functional import ScalingType, SwizzleType
670
+
671
+ # torchao >= 0.18 required with cutlass-dsl >= 4.6 (0.17 imports a helper path 4.6
672
+ # removed; fixed upstream in pytorch/ao).
673
+ # torchao's per-group blocked-scale builder for the per-forward ACTIVATION scale (graph-capturable
674
+ # @triton_op, same S+128·E static padding + SWIZZLE_32_4_4 layout scaled_grouped_mm consumes). The
675
+ # weight scale is already blocked (offline); only the act scale is blocked here.
676
+ from torchao.prototype.moe_training.kernels.mxfp8 import (
677
+ triton_mx_block_rearrange_2d_M_groups,
678
+ )
679
+
680
+ nvfp4 = gate_up_proj_scale_inv.dtype == torch.float8_e4m3fn
681
+ packed = gate_up_proj.dtype == torch.int8 # fp4 formats pack e2m1
682
+ family = "nvfp4" if nvfp4 else "mxfp4" if packed else "mxfp8"
683
+ act_format = family if activation_format is None else activation_format
684
+ scale_group = NVFP4_SCALE_GROUP_K if nvfp4 else MX_SCALE_GROUP_K
685
+ scale_dtype = (
686
+ torch.float8_e4m3fn if nvfp4 else torch.uint8
687
+ ) # our act-quant/swizzle carry uint8
688
+ # scaled_grouped_mm dispatches on the scale dtype — view the uint8 MX scales as e8m0 for it
689
+ f_dtype = torch.float8_e4m3fn if nvfp4 else torch.float8_e8m0fnu
690
+ SWZ = SwizzleType.SWIZZLE_32_4_4
691
+ BW = ScalingType.BlockWise1x16 if nvfp4 else ScalingType.BlockWise1x32
692
+ FP4 = getattr(torch, "float4_e2m1fn_x2", None)
693
+ E = gate_up_proj.shape[0]
694
+ # NVFP4's tcgen05 MMA kind requires TWO-level scaling: the e4m3 per-16 block scale AND a
695
+ # per-tensor global fp32 scale — unlike our kernels there is no "no global" form, so a
696
+ # missing calibrated global rides as identity 1.0 (dynamic quant). Weight globals arrive
697
+ # as the separate *_global_scale args; a calibrated *_input_global_scale normalizes that
698
+ # GEMM's activation quant and rides as its TensorWise scale. MX formats are single-level.
699
+ tensorwise = ScalingType.TensorWise
700
+
701
+ def _tensorwise_global(g, n): # (n,) fp32 TensorWise operand, identity when uncalibrated
702
+ if g is None:
703
+ return torch.ones(n, device=hidden_states.device, dtype=torch.float32)
704
+ if g.numel() == 1:
705
+ return g.reshape(-1).float().expand(n).contiguous()
706
+ assert g.shape == (n,), (
707
+ f"torch's scaled_grouped_mm takes ONE TensorWise global per expert, got "
708
+ f"{tuple(g.shape)} — a per-expert activation global has no slot in this baseline "
709
+ "(the Triton ops take one)"
710
+ )
711
+ return g.float()
712
+
713
+ top_k = top_k_index.shape[1]
714
+ out_dtype = hidden_states.dtype
715
+
716
+ # route: stable-sort routed slots by expert into contiguous groups (torch has no gather/scatter fuse)
717
+ flat_e = top_k_index.reshape(-1)
718
+ order = torch.argsort(flat_e, stable=True)
719
+ counts = torch.histc(flat_e.float(), bins=E, min=0, max=E - 1).to(torch.int32)
720
+ offs = counts.cumsum(0).to(torch.int32)
721
+ tok = (order // top_k).to(torch.long) # source token of each sorted slot
722
+ slot_e = flat_e[order].to(torch.long) # expert of each sorted slot, for the per-expert biases
723
+
724
+ def pk(t): # view a packed-e2m1 operand as torch's fp4 dtype for scaled_grouped_mm
725
+ return t.view(FP4) if packed else t
726
+
727
+ def aswz(a_s): # (S, K//G) -> per-group blocked layout, one launch
728
+ return triton_mx_block_rearrange_2d_M_groups(a_s.view(torch.uint8), offs).view(
729
+ f_dtype
730
+ )
731
+
732
+ def wswz(
733
+ w_s,
734
+ ): # weight scale is pre-blocked offline (SWIZZLE_32_4_4) — pass through, no per-call kernel
735
+ return w_s.view(f_dtype)
736
+
737
+ def grouped_mm(a, w_q, w_s, w_g=None, a_g=None):
738
+ assert a_g is None or nvfp4, "an activation global is NVFP4-only" # match the ops
739
+ assert a_g is None or a_g.numel() == 1, (
740
+ "this baseline quantizes the routed rows in one pass, so the activation global is "
741
+ "per tensor; per-expert activation globals are a Triton-op path"
742
+ )
743
+ # our Triton MX act-quant (format-taking launcher) — torch is timed on the same fast quant
744
+ aq, a_s = _launch_act_quant(
745
+ a, act_format, scale_group, scale_dtype, global_scale=a_g
746
+ )
747
+ sa, ra = aswz(a_s), BW
748
+ sb, rb = wswz(w_s), BW
749
+ if nvfp4: # two-level: block e4m3 + the per-tensor/per-expert fp32 globals
750
+ sa, ra = [sa, _tensorwise_global(a_g, 1)], [BW, tensorwise]
751
+ sb, rb = [sb, _tensorwise_global(w_g, E)], [BW, tensorwise]
752
+ return F.scaled_grouped_mm(
753
+ pk(aq),
754
+ pk(w_q).transpose(-2, -1),
755
+ sa,
756
+ ra,
757
+ sb,
758
+ rb,
759
+ swizzle_a=SWZ,
760
+ swizzle_b=SWZ,
761
+ offs=offs,
762
+ output_dtype=out_dtype,
763
+ )
764
+
765
+ gate_up = grouped_mm(
766
+ hidden_states[tok],
767
+ gate_up_proj,
768
+ gate_up_proj_scale_inv,
769
+ gate_up_proj_weight_global_scale,
770
+ gate_up_proj_input_global_scale,
771
+ )
772
+ # torch has no fused bias, so this baseline adds both host-side; rows are expert-sorted here,
773
+ # so the per-expert bias indexes by the sorted expert ids
774
+ if gate_up_proj_bias is not None:
775
+ gate_up = gate_up + gate_up_proj_bias[slot_e]
776
+ inter = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit)
777
+ down_out = grouped_mm(
778
+ inter, down_proj, down_proj_scale_inv,
779
+ down_proj_weight_global_scale, down_proj_input_global_scale,
780
+ )
781
+
782
+ # One weighted scatter-reduce: down_out is expert-sorted, so index_add_ over the source-token
783
+ # map fuses unroute + routing-weight + top-k sum into (T, H) directly — no separate unsort pass.
784
+ out = torch.zeros_like(hidden_states)
785
+ w = top_k_weights.reshape(-1)[order].unsqueeze(-1).to(out.dtype)
786
+ if down_proj_bias is not None:
787
+ down_out = down_out + down_proj_bias[slot_e]
788
+ down_out = _post_expert_norm(
789
+ down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
790
+ )
791
+ return out.index_add_(0, tok, down_out * w)
792
+
793
+
794
+ def moe_unfused_batched(
795
+ hidden_states: torch.Tensor,
796
+ top_k_index: torch.Tensor,
797
+ top_k_weights: torch.Tensor,
798
+ gate_up_proj: torch.Tensor,
799
+ down_proj: torch.Tensor,
800
+ gate_up_proj_scale_inv: torch.Tensor,
801
+ down_proj_scale_inv: torch.Tensor,
802
+ gate_up_proj_weight_global_scale: torch.Tensor | None = None,
803
+ down_proj_weight_global_scale: torch.Tensor | None = None,
804
+ gate_up_proj_input_global_scale: torch.Tensor | None = None,
805
+ down_proj_input_global_scale: torch.Tensor | None = None,
806
+ post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
807
+ # get_supported_norms() name (fused into the reduce) or a host callable
808
+ post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
809
+ post_expert_norm_eps: float = 1e-6,
810
+ gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
811
+ down_proj_bias: torch.Tensor | None = None, # (E, H)
812
+ act_fn: str | Callable = "silu",
813
+ swiglu_alpha: float | None = None,
814
+ swiglu_limit: float | None = None,
815
+ activation_format: str | None = None,
816
+ gate: bool = True,
817
+ ) -> torch.Tensor:
818
+ """Unfused batched MoE: gate_up (plain batched GEMM, gather hidden) → host ``apply_glu`` →
819
+ down (plain batched GEMM) → routing-weighted reduce. Same math as ``moe_fused_batched`` but
820
+ the SwiGLU + intermediate quant happen between two plain GEMMs; each GEMM quantizes its raw
821
+ input in ``activation_format`` (``None`` follows the weight format, ``"bf16"`` is weight-only). All
822
+ formats route through the shared ``matmul_batched``. The NVFP4 activation globals thread the same way as the fused sibling:
823
+ each GEMM quantizes its raw input against its ``*_input_global_scale``. ``act_fn`` is a
824
+ ``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
825
+ callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
826
+ projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
827
+ — the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
828
+ fmt = _block_format(
829
+ gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
830
+ )
831
+ NUM_EXPERTS = gate_up_proj.size(0)
832
+ expert_ids = top_k_index.reshape(-1)
833
+ gather_idx = _gather_idx(top_k_index)
834
+
835
+ # gate_up as a plain GEMM (no gate epilogue) over gathered hidden -> (S, 2I).
836
+ gate_up_out = matmul_batched(
837
+ hidden_states,
838
+ gate_up_proj,
839
+ Bs=gate_up_proj_scale_inv,
840
+ a_global_scale=gate_up_proj_input_global_scale,
841
+ b_global_scale=gate_up_proj_weight_global_scale,
842
+ expert_ids=expert_ids,
843
+ bias=gate_up_proj_bias,
844
+ activation_format=fmt,
845
+ output_dtype=hidden_states.dtype,
846
+ gather_idx=gather_idx,
847
+ )
848
+ inter = _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate)
849
+ # down over the intermediate (quantized in the same format), routed-order output.
850
+ down_out = matmul_batched(
851
+ inter,
852
+ down_proj,
853
+ Bs=down_proj_scale_inv,
854
+ a_global_scale=down_proj_input_global_scale,
855
+ b_global_scale=down_proj_weight_global_scale,
856
+ expert_ids=expert_ids,
857
+ bias=down_proj_bias,
858
+ activation_format=fmt,
859
+ output_dtype=hidden_states.dtype,
860
+ )
861
+ return _torch_weighted_reduce(
862
+ _post_expert_norm(down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps),
863
+ top_k_index, top_k_weights, NUM_EXPERTS,
864
+ )
build/torch-rocm/norm.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Row-wise RMS normalization, in the three forms models write it.
16
+
17
+ Every form is ``x * factor(h) * rsqrt(mean(y**2) + eps)`` over the row: the per-column
18
+ ``factor`` is the norm weight (``"rms_norm"``) or ``1 + weight`` (the zero-centered
19
+ parameterization), and ``y`` is the raw row except under ``"input_scaled_rms_norm"``, which
20
+ scales BEFORE normalizing so the row's mean square is taken on the scaled values.
21
+
22
+ The MoE chain never materializes the normalized rows: ``rms_inv_rows`` computes the per-row
23
+ ``rsqrt`` in one pass and ``weighted_reduce`` folds it — with the column factor — into the
24
+ reduce it already runs. ``rms_norm_rows`` is the standalone form for the paths that have no
25
+ reduce to fold into."""
26
+
27
+ import torch
28
+ import triton
29
+ import triton.language as tl
30
+
31
+ from .bayesian_autotuner import bayesian_autotune
32
+ from .compat import compile_time_only_triton_wrap, device_context
33
+
34
+ # the fused names, by how a model composes the weight and the normalization
35
+ RMS_NORMS = ("rms_norm", "centered_rms_norm", "input_scaled_rms_norm")
36
+
37
+
38
+ def get_supported_norms() -> tuple[str, ...]:
39
+ """The norm names the kernels fuse (``post_expert_norm``), each ``x * factor * rsqrt(mean +
40
+ eps)`` over the row: ``"rms_norm"`` scales the normalized row by ``weight``,
41
+ ``"centered_rms_norm"`` by ``1 + weight`` (the zero-centered parameterization), and
42
+ ``"input_scaled_rms_norm"`` scales by ``1 + weight`` BEFORE normalizing, so the mean square
43
+ is taken on the scaled row. Anything else stays a host callable."""
44
+ return RMS_NORMS
45
+
46
+
47
+ @triton.jit
48
+ def norm_column_factor(W, offs_h, mask, NORM: tl.constexpr):
49
+ """The per-column multiplier of a fused norm: the weight, or ``1 + weight`` under the
50
+ zero-centered parameterizations."""
51
+ w = tl.load(W + offs_h, mask=mask, other=0.0).to(tl.float32)
52
+ if NORM == "rms_norm":
53
+ factor = w
54
+ else:
55
+ factor = 1.0 + w
56
+ return factor
57
+
58
+
59
+ @bayesian_autotune(
60
+ [
61
+ triton.Config({"BLOCK_H": block_h}, num_warps=warps)
62
+ for block_h in (256, 512, 1024, 2048)
63
+ for warps in (4, 8)
64
+ ],
65
+ # one program per row, so the tile width trades the row's loop length against occupancy
66
+ ["H", "NORM", "WRITE_ROWS"],
67
+ n_trials=8,
68
+ )
69
+ @triton.jit
70
+ def _rms_norm_kernel(
71
+ X, # (S, H) rows to normalize
72
+ W, # (H,) norm weight
73
+ Out, # (S, H) normalized rows; None writes only Inv (the fused chain's one-pass arm)
74
+ Inv, # (S,) fp32 per-row rsqrt(mean + eps); None skips it
75
+ H,
76
+ eps,
77
+ stride_x_m,
78
+ stride_x_h,
79
+ stride_o_m,
80
+ stride_o_h,
81
+ NORM: tl.constexpr,
82
+ WRITE_ROWS: tl.constexpr, # Out is written (else the pass stops at Inv)
83
+ BLOCK_H: tl.constexpr,
84
+ ):
85
+ """One program per row: accumulate the row's mean square in fp32 over ``BLOCK_H`` tiles, then
86
+ ``Inv[row] = rsqrt(mean + eps)`` and, under ``WRITE_ROWS``, a second pass writing
87
+ ``x * factor * inv`` (the row is L2-resident by then). ``input_scaled_rms_norm`` squares the
88
+ SCALED values, the other forms the raw ones — the one place the three differ."""
89
+ row = tl.program_id(0)
90
+ x_row = X + row * stride_x_m
91
+ square_sum = tl.zeros((), tl.float32)
92
+ for h in range(0, tl.cdiv(H, BLOCK_H)):
93
+ offs_h = h * BLOCK_H + tl.arange(0, BLOCK_H)
94
+ mask = offs_h < H
95
+ x = tl.load(x_row + offs_h * stride_x_h, mask=mask, other=0.0).to(tl.float32)
96
+ if NORM == "input_scaled_rms_norm":
97
+ x = x * norm_column_factor(W, offs_h, mask, NORM)
98
+ square_sum += tl.sum(x * x, 0)
99
+ inv = tl.rsqrt(square_sum / H + eps)
100
+ if Inv is not None:
101
+ tl.store(Inv + row, inv)
102
+ if WRITE_ROWS:
103
+ out_row = Out + row * stride_o_m
104
+ for h in range(0, tl.cdiv(H, BLOCK_H)):
105
+ offs_h = h * BLOCK_H + tl.arange(0, BLOCK_H)
106
+ mask = offs_h < H
107
+ x = tl.load(x_row + offs_h * stride_x_h, mask=mask, other=0.0).to(tl.float32)
108
+ out = x * norm_column_factor(W, offs_h, mask, NORM) * inv
109
+ tl.store(out_row + offs_h * stride_o_h, out.to(Out.dtype.element_ty), mask=mask)
110
+
111
+
112
+ def _launch_rms_norm(x, weight, eps, norm, write_rows):
113
+ """One launch of ``_rms_norm_kernel`` over ``x``'s rows, returning the normalized rows or the
114
+ per-row ``rsqrt`` — whichever was asked for is the only one allocated."""
115
+ assert norm in RMS_NORMS, f"norm must be one of {RMS_NORMS} or a callable, got {norm!r}"
116
+ assert x.ndim == 2, f"rows must be 2D (rows, H), got ndim={x.ndim}"
117
+ assert weight.ndim == 1 and weight.shape[0] == x.shape[1], (
118
+ f"norm weight {tuple(weight.shape)} does not match the row width ({x.shape[1]},)"
119
+ )
120
+ S, H = x.shape
121
+ out = torch.empty_like(x) if write_rows else None
122
+ inv = None if write_rows else torch.empty(S, device=x.device, dtype=torch.float32)
123
+ with device_context(x.device):
124
+ compile_time_only_triton_wrap(_rms_norm_kernel)[(S,)](
125
+ x,
126
+ weight,
127
+ out,
128
+ inv,
129
+ H,
130
+ eps,
131
+ x.stride(0),
132
+ x.stride(1),
133
+ out.stride(0) if write_rows else 0,
134
+ out.stride(1) if write_rows else 0,
135
+ NORM=norm,
136
+ WRITE_ROWS=write_rows,
137
+ )
138
+ return out if write_rows else inv
139
+
140
+
141
+ def rms_norm_rows(
142
+ x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6, norm: str = "rms_norm"
143
+ ) -> torch.Tensor:
144
+ """RMS-normalize ``(S, H)`` rows in one launch, in the ``get_supported_norms()`` form named by
145
+ ``norm`` (fp32 accumulate, input dtype out). The MoE chain folds this into
146
+ ``weighted_reduce`` instead; this is the standalone form."""
147
+ return _launch_rms_norm(x, weight, eps, norm, write_rows=True)
148
+
149
+
150
+ def rms_inv_rows(
151
+ x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6, norm: str = "rms_norm"
152
+ ) -> torch.Tensor:
153
+ """The ``(S,)`` fp32 ``rsqrt(mean + eps)`` of each row under ``norm`` — the half of
154
+ ``rms_norm_rows`` a consumer that already reads the rows needs, so the rows are never
155
+ written back."""
156
+ return _launch_rms_norm(x, weight, eps, norm, write_rows=False)
build/torch-rocm/pruners.py ADDED
@@ -0,0 +1,1213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ import triton
17
+
18
+ from .compat import get_active_device_type, is_sm10x, is_sm90, sm_count, sm_shared_memory_limit
19
+ from .mma import MMA_N_ATOM_WIDTH
20
+
21
+ # ── config pruners ────────────────────────────────────────────────────────────
22
+ # Every guard exists for one of four reasons; the map (pruner -> rule -> attached to):
23
+ #
24
+ # SILENTLY-WRONG configs (must be pruned, the tuner would time and pick them):
25
+ # block_within_dim_pruner BK not dividing K over-reads rows (maskless K-loops) —
26
+ # tensor/weight-only/full-precision 2D+batched+grouped, and the
27
+ # first stage of mx_config_pruner, whose BN|N stage is scoped
28
+ # to the mx-dynamic AFFINE arm (its grouped scale gather has no
29
+ # wrap; swizzled masks its N tail). The weight-only batched and
30
+ # grouped kernels carry their own ungated BN|N stages.
31
+ # scalar "scalar" -> BM=1 the scalar GEVM broadcasts wrong at BM>1 (config builder)
32
+ # descriptor_needs_prequant_pruner raw-A inline-quant arm reads a_ptrs, dead under a
33
+ # descriptor mode — static 2D
34
+ # weight_only_swap_scope_pruner SWAP_AB iff COMPUTE_MODE == "scalar" — the swapped dot
35
+ # arms are unimplemented and a non-swapped scalar has no
36
+ # accumulator shape — weight-only batched
37
+ # mma_trap_ok swapped dot_scaled BK<128 (sticky 716), single-trip
38
+ # accumulator-init miscompile, width > 256 (sticky trap),
39
+ # packed-fp4 GATE BN<64 silent zeros — mx 2D/batched/grouped;
40
+ # raises when empty (never falls back to a miscompile)
41
+ # gate_stacked_tmem_trap_pruner GATE 2*BN=512 acc overflows tensor memory undetected
42
+ # at exactly BN=256/BM=64 (checker gap -> sticky 716) —
43
+ # weight-only grouped; raises when empty
44
+ # block_dynamic_mma_width_pruner the same width trap on the bd implicit dot_scaled
45
+ # arm (GATE 2*BN) — bd 2D only (grouped/batched bd pin their
46
+ # N tile to the quant block, within the cap); UE8M0-As-gated
47
+ # scale_subblock_pruner (whole-block clause) a tile wider than the quant block reads
48
+ # one scale across two blocks; raises when the grid has no
49
+ # fitting tile — bd/static batched
50
+ # COMPILER BUGS / unsupported combos on this triton+arch (benign inf, pruned to save
51
+ # compiles and to keep can't-win configs from poisoning the TPE densities):
52
+ # warp_spec_compile_guard_pruner WS compiles iff (BN if swapped else BM)>=64 &
53
+ # num_warps%4==0 — bd/tensor/mx/weight-only/fp 2D+grouped
54
+ # gated_pointer_weight_warp_spec_pruner GATE+pointer-weight WS lowering failures —
55
+ # weight-only grouped
56
+ # affine_scale_warp_spec_pruner affine scale gather's other=0.0 can't take
57
+ # ttg.partition under WS — mx-dynamic grouped
58
+ # global_scale_warp_spec_pruner the NVFP4 global load can't take ttg.partition
59
+ # under WS — weight-only grouped, BsGlobal-gated
60
+ # nvfp4_native_ok (mx_config_pruner) E4M3-scale dot_scaled PassManager-fails off the
61
+ # native staging: M operand < 128, num_warps outside {4, 8},
62
+ # or a weight-only kernel (no As — never native)
63
+ # descriptor_box_pruner TMA box > 256/dim is illegal — all descriptor-axis kernels
64
+ # smem_pruner empirical smem model (B200-sampled, keep-side error) —
65
+ # bd/static/mx/weight-only 2D; tensor/mx/weight-only/fp grouped;
66
+ # mx batched
67
+ # RACE guards (Triton 3.7.1 pipeliner, per-loop-structure flake maps):
68
+ # block_dynamic_grouped_matmul_pruner WS-only at BM>=64, non-WS below (disjoint) —
69
+ # bd + static grouped (plain and GATE arms, same loop)
70
+ # block_dynamic_2d_warp_spec_pruner bd 2D WS compile-fail laws: w16+WS at any tile,
71
+ # and the implicit dot_scaled arm (UE8M0 As, native-M tile)
72
+ # + WS — bd 2D only (COMPILER BUG class, listed here next to
73
+ # its sibling)
74
+ # TPE-POISON fences (valid configs that can't win in a regime):
75
+ # scalar_max_m_pruner scalar above M=64 (prefill GEVM) — mx 2D
76
+ # mx_config_pruner poison fences the dot arm where the
77
+ # native MMA exists (A/B'd 2026-07-14; weight-only kernels
78
+ # keep dot), mma_poison_ok: no-swap BK<128 where 128 divides K
79
+ # (basin claim) + swapped rows<128 (bf16 fallback never wins)
80
+ # scale_subblock_pruner sub-quant-block BN only when the grid starves — bd/static
81
+ # batched
82
+ # mx_2d_swap_scope_pruner 2D mx SWAP_AB rows only for E4M3-scale decode (BM=1,
83
+ # M<=16) — swap loses everywhere else (18-cell sweep)
84
+ # packed_schedule_scope_pruner PACKED_SCHEDULE rows only at BM>=128 (register relief
85
+ # is the whole win) — full-precision grouped
86
+ # swizzled_scales_bm_pruner grouped mx swizzled arm pinned to BM=BN=128 (slab layout);
87
+ # affine arm keeps every BM/BN
88
+ # swizzled_scale_config_pruner swizzled-arm BK%128, BN<=128, GATE block pairs —
89
+ # mx 2D/batched
90
+ # matched_memory_modes_pruner mixed pointer/descriptor never wins a DENSE 2D GEMM —
91
+ # bd/static 2D (grouped keeps mixed: it wins there)
92
+ # Every pruner is a ``config_filter``: a per-config ``ok(config, args)`` predicate, an
93
+ # optional ``when(args)`` regime gate, and an empty-survivor policy (advisory guards fall
94
+ # back to the untouched grid; the BK-within-K correctness veto raises via ``on_empty`` —
95
+ # silence there means wrong results). Ordered below: shared infrastructure, then the four
96
+ # categories above.
97
+
98
+ # sm_10x caps a single scaled MMA (dot_scaled) at N=256; Triton miscompiles wider ones
99
+ # into a sticky illegal-address device trap. Plain-dot MMAs have NO such cap: a forced
100
+ # width-512 GATE sweep on the tensor grouped kernel (2026-07-14) ran bit-exact wherever
101
+ # it fit shared memory and failed only as benign launch-time smem overflows.
102
+ SM10X_SCALED_MMA_MAX_N = 256
103
+
104
+ # Branch axes that RELOCATE the tile optimum (compute unit / operand orientation): the
105
+ # tuner's guaranteed max-tile anchors group by these (``path_anchor_axes`` — a declaration,
106
+ # so the tuner itself stays independent of configuration details). Scheduling axes
107
+ # (WARP_SPEC, PACKED_SCHEDULE, memory modes) share the tile basin shape and stay out —
108
+ # per-basin anchors over the full branch cross starved a 100-trial budget (see
109
+ # _basin_anchor_indices).
110
+ PATH_ANCHOR_AXES = ("COMPUTE_MODE", "SWAP_AB")
111
+
112
+
113
+ def config_dim(c, all_args, name):
114
+ """A tile dimension for config ``c`` — from its tuned meta, else the launch args.
115
+ No default: a pruner reasoning about a dimension the kernel doesn't have is a wiring
116
+ bug, and a made-up value would silently mis-prune."""
117
+ v = c.kwargs.get(name, all_args.get(name))
118
+ if v is None:
119
+ raise ValueError(
120
+ f"pruner needs {name} (autotune config meta or launch arg); none found"
121
+ )
122
+ return v
123
+
124
+
125
+ def config_filter(ok, when=None, on_empty=None):
126
+ """Wrap a per-config predicate into an ``early_config_prune``: keeps the configs where
127
+ ``ok(config, all_args)`` (``all_args`` = launch args + autotune kwargs). Every pruner
128
+ below is one of these — the predicate is always named ``ok``, so the factories differ
129
+ only in their rule, not their plumbing. ``when(all_args)`` gates the whole filter —
130
+ hardware/launch-regime checks live there, per-config logic in ``ok``. When nothing
131
+ survives, the untouched grid is returned (guards are advisory) unless ``on_empty(configs,
132
+ all_args)`` is given — correctness vetoes pass one that raises, because falling back to
133
+ configs that compute wrong results must be loud."""
134
+
135
+ def prune(configs, named_args, **kwargs):
136
+ all_args = {**named_args, **kwargs}
137
+ if when is not None and not when(all_args):
138
+ return configs
139
+ kept = [c for c in configs if ok(c, all_args)]
140
+ if kept:
141
+ return kept
142
+ return configs if on_empty is None else on_empty(configs, all_args)
143
+
144
+ return prune
145
+
146
+
147
+ def compose_pruners(*pruners):
148
+ """Chain ``early_config_prune`` callbacks left to right (each sees the previous
149
+ survivors)."""
150
+
151
+ def prune(configs, named_args, **kwargs):
152
+ for p in pruners:
153
+ configs = p(configs, named_args, **kwargs)
154
+ return configs
155
+
156
+ return prune
157
+
158
+
159
+ def block_within_dim_pruner(dim_arg: str, block_key: str = "BLOCK_SIZE_K", when=None):
160
+ """``early_config_prune`` dropping configs whose ``block_key`` tile does not divide the
161
+ launch dim named by ``dim_arg``: the K-loops load unmasked and the batched/grouped
162
+ N-tiles store row-masked only, so a non-dividing tile's last trip reads or writes past
163
+ the row — silently wrong results the tuner would happily time and pick (a BN=256
164
+ winner at N=128 corrupted 40/64 rows before the N veto existed). A dim smaller than
165
+ every grid tile is a hard error. ``when`` gates the veto to a launch regime (e.g. the
166
+ grouped swizzled arm masks its N-tail — %-wrapped value load + ``N_COLS`` store mask — so
167
+ BN needn't divide N there). Used standalone by the tensor-dynamic kernels and as the first
168
+ stages of ``mx_config_pruner``."""
169
+
170
+ def ok(c, args):
171
+ block = c.kwargs.get(block_key, 0)
172
+ return block == 0 or args[dim_arg] % block == 0
173
+
174
+ def raise_no_dividing_block(configs, args):
175
+ min_block = min(c.kwargs.get(block_key, 0) for c in configs)
176
+ raise ValueError(
177
+ f"{dim_arg}={args[dim_arg]} is not a multiple of any {block_key} in the "
178
+ f"autotune grid; the unmasked tile would run past the row. Pad the problem "
179
+ f"along {dim_arg} (smallest grid tile: {min_block})."
180
+ )
181
+
182
+ return config_filter(ok, when=when, on_empty=raise_no_dividing_block)
183
+
184
+
185
+ def block_fits_dim_pruner(dim_arg: str, block_key: str = "BLOCK_SIZE_K"):
186
+ """``early_config_prune`` for a loop whose tail tile SLIDES back in bounds instead of
187
+ running past the row (the weight-only batched K-loop): the tile need not divide the dim,
188
+ it only has to fit inside it, since the clamp would otherwise walk backwards off the
189
+ buffer. Everywhere the tail is unmasked, use ``block_within_dim_pruner`` — this one keeps
190
+ non-dividing tiles, and on a maskless loop those read past the row."""
191
+
192
+ def ok(c, args):
193
+ block = c.kwargs.get(block_key, 0)
194
+ return block == 0 or block <= args[dim_arg]
195
+
196
+ def raise_no_fitting_block(configs, args):
197
+ min_block = min(c.kwargs.get(block_key, 0) for c in configs)
198
+ raise ValueError(
199
+ f"{dim_arg}={args[dim_arg]} is smaller than every {block_key} in the autotune "
200
+ f"grid (smallest: {min_block}); the sliding tail tile has nowhere to sit."
201
+ )
202
+
203
+ return config_filter(ok, on_empty=raise_no_fitting_block)
204
+
205
+
206
+ def require_moe_dims_aligned(N: int, K: int, block_n: int, block_k: int) -> None:
207
+ """Routed MoE (grouped/batched) GEMMs tile expert dims by the quant block — N-tiles store
208
+ column-unmasked and K loads unmasked — so ``N`` and ``K`` must each be a multiple of the block.
209
+ Non-aligned dims are a dense capability: use ``matmul_2d`` (its single GEMM masks the N/K tail).
210
+ Raised early with a clear message rather than letting a tile run past the row."""
211
+ if N % block_n != 0 or K % block_k != 0:
212
+ raise ValueError(
213
+ f"routed MoE GEMM needs N ({N}) and K ({K}) each a multiple of the quant block "
214
+ f"(block_n={block_n}, block_k={block_k}); non-aligned dims are supported only by the "
215
+ f"dense matmul_2d op."
216
+ )
217
+
218
+
219
+ def warp_spec_compile_guard_pruner():
220
+ """``early_config_prune`` dropping ``warp_specialize`` configs that can never compile: WS needs
221
+ ``num_warps % 4 == 0`` (its async producer/consumer partitions) and enough M work — the MMA's M
222
+ operand, which ``SWAP_AB`` moves to ``BLOCK_SIZE_N`` (so BM=1 decode can WS when swapped), so
223
+ ``(BN if swapped else BM) >= 64``. ``dot_scaled`` + WS + TMA + gate all compile and win here
224
+ (measured 1995 TFLOPS on a clean context) — the earlier "dot_scaled/gate never WS-compiles"
225
+ verdicts were a ``num_warps=2`` / trap-contaminated-GPU misdiagnosis. Non-WS configs all stay —
226
+ WS is purely a perf axis. CUDA-only."""
227
+
228
+ def ok(c, args):
229
+ if not c.kwargs.get("WARP_SPEC"):
230
+ return True
231
+ # WS needs num_warps a multiple of 4 (its async producer/consumer partitions) and enough M
232
+ # work — the MMA's M operand, which SWAP_AB moves to BLOCK_SIZE_N (so BM=1 decode can still
233
+ # WS when swapped). dot_scaled + WS + TMA DOES compile here (measured 1995 on a clean
234
+ # context) — the old "dot_scaled never WS-compiles" was a num_warps=2 misdiagnosis on a
235
+ # trap-contaminated GPU. Callers needing dot_scaled+WS off (block-dynamic swap arm) fence it.
236
+ mma_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
237
+ return config_dim(c, args, mma_m) >= 64 and c.num_warps % 4 == 0
238
+
239
+ return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
240
+
241
+
242
+ def gated_pointer_weight_warp_spec_pruner():
243
+ """``early_config_prune`` dropping the ``warp_specialize`` configs that cannot lower on the
244
+ weight-only grouped MX kernel's GATE arm, where the stacked gate|up tile doubles the weight
245
+ extent to ``2 * BLOCK_SIZE_N``. With a pointer-mode weight (its masked load carries the
246
+ ``other=0.0`` constant the WS partitioner cannot tag) the automatic-warp-specialization pass
247
+ fails in ``TritonGPUOptimizePartitionWarps`` with ``RuntimeError: PassManager::run failed``,
248
+ but only where the partitioner also has to split a descriptor-mode A, 16 warps, or a
249
+ sub-128 M tile — plain pointer/pointer at BM=128 and w4/w8 compiles and is often the winner.
250
+
251
+ Region measured by tuning the GPT-OSS W4A16 problem (N=K=2880) with a fresh Triton cache and
252
+ ``FINEGRAINED_AUTOTUNE_LOG``: of 129 WS configs, the 12 that failed are exactly the ones this
253
+ predicate names, and it drops none of the 117 that compiled. Every failure sat in the GATE
254
+ shape — the ungated shape's 63 WS configs all compiled. Unfenced these are wasted compiles per
255
+ new shape (the tuner memoizes them only after paying once). The ``BM < 128`` clause rests on a
256
+ single observation, so it is the clause to revisit if a later probe contradicts it. CUDA-only;
257
+ a no-op for ungated launches.
258
+
259
+ NOT covered: the later ``dot_scaled`` arm adds 4 UNGATED failures (descriptor-A +
260
+ pointer-B at num_warps>=8), outside this predicate's GATE scope. Left unfenced on
261
+ purpose — the region separates only on a GATE x COMPUTE_MODE x A-mode x warps
262
+ interaction, and every predicate wide enough to catch it also dropped working configs
263
+ across the five probes. The tuner memoizes the 4 after paying once per shape, and this
264
+ fence was re-checked against the two-arm grid: 0/207 working configs dropped."""
265
+
266
+ def ok(c, args):
267
+ if not c.kwargs.get("WARP_SPEC"):
268
+ return True
269
+ if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer":
270
+ return True
271
+ return not (
272
+ c.kwargs.get("A_MEMORY_MODE", "pointer") != "pointer"
273
+ or c.num_warps >= 16
274
+ or config_dim(c, args, "BLOCK_SIZE_M") < 128
275
+ )
276
+
277
+ return config_filter(
278
+ ok, when=lambda args: args.get("GATE", False) and get_active_device_type() == "cuda"
279
+ )
280
+
281
+
282
+ def affine_scale_warp_spec_pruner():
283
+ """``early_config_prune`` dropping ``warp_specialize`` on the grouped MX AFFINE (row-major,
284
+ non-``SWIZZLED_SCALES``) scale path. Its per-(gathered-row, K-group) 2D pointer-gather scale
285
+ load carries an ``other=0.0`` constant the WS partitioner cannot tag — the automatic-warp-
286
+ specialization pass fails with ``'arith.constant' op does not have expected attribute
287
+ ttg.partition``. The swizzled arm (gate + non-gate) loads the scale through a TMA descriptor
288
+ (no such constant) and WS-compiles + wins, so the fused / deployment path keeps WS. CUDA-only;
289
+ a no-op where ``SWIZZLED_SCALES`` isn't a kernel arg (defaults to allowing WS)."""
290
+
291
+ def ok(c, args):
292
+ if not c.kwargs.get("WARP_SPEC"):
293
+ return True
294
+ return args.get("SWIZZLED_SCALES", True)
295
+
296
+ return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
297
+
298
+
299
+ def mx_config_pruner(k_arg: str, n_arg: str | None = None, block_within_k: bool = True):
300
+ """``early_config_prune`` for the MX kernels (2D, batched, grouped — their tile is always
301
+ tuned): a BK-within-K veto plus sm_10x MMA-shape guards (no-ops elsewhere and for scalar
302
+ configs). GATE-aware: under the GATE constexpr (read from the launch args) the kernel
303
+ computes gate|up as one stacked 2*BN extent, so the swapped dot_scaled lhs has ``2*BN``
304
+ rows and the no-swap combined dot is ``2*BN`` wide; a plain launch's counts are just
305
+ ``BN``.
306
+
307
+ ``block_within_k=False`` drops the BK veto for a caller whose K-loop masks or slides its
308
+ tail (the weight-only batched kernel); the MMA-shape guards still apply.
309
+
310
+ - ``BLOCK_SIZE_K`` not dividing the launch's contraction dim (``k_arg`` names it) →
311
+ dropped: the K-loop loads are unmasked, so any non-dividing BK's last trip reads
312
+ past the row — silently wrong results the tuner would happily time and pick (bit us when
313
+ BK=512 met a K=256 test problem).
314
+ - MXFP4 ``scalar`` configs are KEPT. They used to be dropped on sm_10x: fp4 scalar decode
315
+ was ALU-bound in the E2M1 unpack and measured 1.8x slower than dot_scaled. That verdict
316
+ was against the INTEGER unpack. Decoding E2M1 through the sm_100 CVT unit
317
+ (``_e2m1_pair_to_e4m3``) inverts it — on the dsv4 W4A8 decode, scalar+swap beats the
318
+ dot_scaled crown 20.7 vs 41.8us (gate_up) and 11.3 vs 22.0us (down), bit-identical —
319
+ because at BM=1 dot_scaled cannot reach the native microscaled MMA either (it needs an
320
+ M=128 operand) and emulates through the same upcast, while scalar skips the MMA
321
+ entirely. Re-fence only against a fresh measurement.
322
+ - Swapped ``dot_scaled`` rows < 128 → dropped: the native microscaled MMA gates on the M
323
+ operand being exactly 128, so smaller rows run the bf16-upcast fallback and never win —
324
+ the same poison mechanism (an earlier dsv4 gate_up tune shipped 63µs missing the 43µs
325
+ swap winner).
326
+ - No-swap ``dot_scaled`` width > ``SM10X_SCALED_MMA_MAX_N`` → dropped: Triton
327
+ miscompiles wider scaled MMAs into a sticky illegal-address device trap that poisons
328
+ the context mid-autotune.
329
+
330
+ Packed-E2M1 activations (a ``uint8`` activation tile, W4A4 — caller-packed or
331
+ quantized inline from a raw tile) are consumed by EVERY arm: dot_scaled natively
332
+ (the ``kind::mxf4`` MMA was probed bit-exact: BK down to 64, single-trip, and
333
+ BN=256 all clean; BM<128 falls back to the correct-but-slow f16 kind), and the
334
+ dot/scalar/swap arms column-unpack them to E4M3 (lossless) first — no arm is
335
+ structurally packed-incompatible, so W4A4 needs no shape gate of its own.
336
+
337
+ The shape gates above are scoped to ``dot_scaled`` — they are native scaled-MMA bug
338
+ gates. The ``dot`` arm (BK structurally the UE8M0 group, 32) is CORRECT everywhere
339
+ probed (forced-config sweep 2026-07-14, GATE and plain, MXFP4/MXFP8, incl. width-512
340
+ stacked dots) but is fenced on sm_10x as TPE POISON: an A/B of fresh 100-trial tunes
341
+ (mx grouped, S=4096 E=32 N=1024 K=2048 MXFP8) shipped dot winners 2-3x SLOWER with the
342
+ dot rows in the grid (gate 0.297ms vs 0.095ms, plain 0.147ms vs 0.079ms) — the ~2x
343
+ larger grid dilutes the trial budget and the early-sampled dot basin hijacks the TPE
344
+ densities, so the native scaled-MMA winner is never found. dot never wins on sm_10x
345
+ (native dot_scaled owns prefill, scalar owns M=1 decode); other targets keep the arm.
346
+ Revisit if a dot win materializes (e.g. the batched decode BM=16 observation).
347
+
348
+ The fence applies only where the kernel carries an ``As`` activation-scale operand, so
349
+ the weight-only (W4A16/W8A16) kernels keep their dot rows while still getting the shape
350
+ gates. Their ``dot`` is a weight upcast + plain ``tl.dot`` over a free BK — with raw
351
+ activations there is no native microscaled MMA to lose to, so both arms upcast and this
352
+ one WINS the stacked gate|up tile (gpt-oss K=2880: dot 4645.7us vs dot_scaled 5011.3us,
353
+ while the down projection goes the other way, 2348.1us vs 2882.6us).
354
+
355
+ The gates are two classes with opposite empty-survivor policies, composed trap-first:
356
+ ``mma_trap_ok`` (miscompiles: swapped BK<128, single-trip, width, fp4 GATE zeros) raises
357
+ when nothing survives — falling back to configs that compute wrong results must be loud —
358
+ while the poison fences (dot/scalar arms, ``mma_poison_ok``) stay advisory, so emptying
359
+ one of them re-admits CORRECT-but-slow configs, never a miscompile. A contraction dim
360
+ smaller than every grid BK is a hard error: any config would over-read past the row and
361
+ return silently wrong results."""
362
+
363
+ def scaled_mma_available(args):
364
+ # dot_scaled rows exist for this launch iff the grid's smallest BK (64)
365
+ # divides the contraction dim with at least two trips; off the 64 grid no
366
+ # row survives the shape gates, so the software arms are the only correct
367
+ # ones and their poison fences lift.
368
+ return args[k_arg] % 64 == 0 and args[k_arg] > 64
369
+
370
+ def mma_trap_ok(c, args):
371
+ # MISCOMPILE gates only — every rule here names a config that runs and returns
372
+ # wrong results or traps the device; the composed filter raises when they empty.
373
+ if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
374
+ return True
375
+ # Swapped dot_scaled below BK=128 is a Triton sm_10x lowering bug — a STICKY
376
+ # misaligned-address device trap — on the UE8M0 (group-32) scale family. Probed
377
+ # per-arm 2026-07-15 (fresh processes): batched W4A4 swap BK=64 traps at K=2880
378
+ # AND K=2944 alike (K-alignment is NOT the variable), swap BK=128 runs clean and
379
+ # correct, no-swap BK=64 is compute-sanitizer-clean, and the grouped kernel (no
380
+ # swap axis) is clean at BK=64 in every memory mode. The E4M3 (NVFP4 group-16)
381
+ # variant does NOT trap: 40 raw-grid sweep configs + 6 forced fresh-process runs
382
+ # at K∈{256,2048,2944} all bit-correct (2026-08-05) — and swap BN=64 GATE is the
383
+ # native M=128 staging for gate decode, so it stays admitted. All probing on
384
+ # B200 — re-chart on H100 or the target device before trusting the fence there.
385
+ if (
386
+ c.kwargs.get("SWAP_AB")
387
+ and config_dim(c, args, "BLOCK_SIZE_K") < 128
388
+ and getattr(args.get("Bs"), "dtype", None) != torch.float8_e4m3fn
389
+ ):
390
+ return False
391
+ # Single-trip dot_scaled (BK >= contraction dim) trips the sm_10x
392
+ # accumulator-init miscompile (uninitialized TMEM alloc must be mutable).
393
+ # Bites only small K (e.g. a K=512 gate_up with BK=512): silently wrong results
394
+ # the tuner would happily time and pick (surfaced as a 35% MXFP4 fused-MoE error).
395
+ if config_dim(c, args, "BLOCK_SIZE_K") >= args[k_arg]:
396
+ return False
397
+ # tcgen05 microscaled-fp4 MMA silently no-ops at the GATE stacked N=2*BN=64 shape (BN<64):
398
+ # the IR emits a correct tc_gen5_mma_scaled (128x64 acc, 64x2 scale) but it never writes the
399
+ # accumulator, which reads back its zero-init → EXACTLY-ZERO output. N-independent (probed
400
+ # zero at both non-aligned N=320 and aligned N=512; BN=64 / stacked N=128 is correct
401
+ # everywhere), so this is a LATENT poison config: any packed-fp4 gate|up whose tuner crowned
402
+ # BN=32 (favoured at small N) would silently return zeros — larger shapes pass only because
403
+ # BN>=64 wins. (BN=32 is recoverable only by emitting gate/up as two separate BN dots
404
+ # instead of one stacked 2*BN dot; not worth it — a sub-64 gated tile rarely wins over BN=64.)
405
+ if (
406
+ args.get("GATE")
407
+ and config_dim(c, args, "BLOCK_SIZE_N") < 64
408
+ and getattr(args.get("B"), "dtype", None) == torch.uint8
409
+ ):
410
+ return False
411
+ # no-swap width cap: Triton miscompiles a scaled MMA wider than 256 into a sticky
412
+ # illegal-address device trap; the stacked 2*BN extent under GATE counts in full
413
+ rows = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
414
+ return c.kwargs.get("SWAP_AB") or rows <= SM10X_SCALED_MMA_MAX_N
415
+
416
+ def raise_all_mma_trapped(configs, args):
417
+ raise ValueError(
418
+ f"every autotune config hits an sm_10x scaled-MMA miscompile for "
419
+ f"{k_arg}={args[k_arg]} (GATE={bool(args.get('GATE'))}); no correct config "
420
+ f"exists in this grid — extend the grid (a no-swap BK strictly inside the "
421
+ f"contraction dim) rather than lifting the fence: the pruned configs return "
422
+ f"wrong results or trap the device."
423
+ )
424
+
425
+ def mma_poison_ok(c, args):
426
+ # CORRECT-but-can't-win fences (advisory: emptying re-admits correct configs)
427
+ if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
428
+ return True
429
+ # no-swap BK<128 rows are CORRECT but admitted only where BK=128 can't divide the
430
+ # contraction dim (e.g. gpt-oss K=2880). "The BK>=128 basin owns the shape" is a
431
+ # basin claim, not an A/B — the fence only trims trial-budget dilution, so lifting
432
+ # it costs tune time, not results.
433
+ if (
434
+ not c.kwargs.get("SWAP_AB")
435
+ and config_dim(c, args, "BLOCK_SIZE_K") < 128
436
+ and args[k_arg] % 128 == 0
437
+ ):
438
+ return False
439
+ # swapped rows < 128 run the bf16-upcast fallback (the native microscaled MMA
440
+ # gates on the M operand being exactly 128) — correct, never wins, TPE poison
441
+ rows = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
442
+ return not c.kwargs.get("SWAP_AB") or rows >= 128
443
+
444
+ def dot_arm_ok(c, args):
445
+ return c.kwargs.get("COMPUTE_MODE") != "dot"
446
+
447
+ def acts_are_scaled(args):
448
+ # The dot-arm poison verdict holds only where dot_scaled can reach the NATIVE
449
+ # microscaled MMA, which needs both operands microscaled — i.e. the kernel carries an
450
+ # activation-scale operand. The weight-only (W4A16/W8A16) kernels have no ``As``: their
451
+ # activations are raw bf16/fp16, so no native low-precision MMA exists, both arms upcast,
452
+ # and their "dot" (weight upcast + plain tl.dot) genuinely competes. Keyed on the operand,
453
+ # not A's dtype — the MX-dynamic inline-quant arm takes a raw bf16 A and still reaches the
454
+ # native MMA.
455
+ return "As" in args
456
+
457
+ def nvfp4_native_ok(c, args):
458
+ # mxf4nvf4 (E4M3 scales) has NO fallback below the native staging — the decompose
459
+ # path materializes the scale through an integer-only DenseElementsAttr and
460
+ # PassManager fails outright (charted BM {16,32,64} x BK {128,256}, 2026-07-15;
461
+ # warps/operand law 2026-08-12). Native staging needs the M operand >= 128 AND
462
+ # num_warps in {4, 8} (2 and 16 both fail across every BN/BK/stages cell) AND
463
+ # both operands microscaled — the weight-only kernels (no ``As``: raw bf16 acts,
464
+ # in-register inline quant) never stage it, so their E4M3-scale dot_scaled fails
465
+ # at every tile (their dot/scalar arms compete instead). dot_scaled-only
466
+ # (dot/scalar decode E4M3 scales in software at any M);
467
+ # the MMA's M operand is BLOCK_SIZE_M, or the weight rows when SWAP_AB puts the
468
+ # token in N.
469
+ if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
470
+ return True
471
+ if not acts_are_scaled(args) or not 4 <= c.num_warps <= 8:
472
+ return False
473
+ if c.kwargs.get("SWAP_AB"):
474
+ return (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N") >= 128
475
+ return config_dim(c, args, "BLOCK_SIZE_M") >= 128
476
+
477
+ def scales_are_e4m3(args):
478
+ return getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn
479
+
480
+ stages = [block_within_dim_pruner(k_arg)] if block_within_k else []
481
+ if n_arg is not None:
482
+ # BN | N is needed only on the mx-dynamic AFFINE arm: its weight-scale gather has no
483
+ # column wrap. The swizzled arm masks its N-tail (grouped/batched: %-wrapped value load
484
+ # + N_COLS store mask; 2D: swizzle-offset wrap), and the weight-only kernels (no
485
+ # SWIZZLED_SCALES axis) clamp their scale pointers in-bounds and mask the tail in the
486
+ # epilogue (N_COLS), so both keep non-dividing BN.
487
+ stages.append(
488
+ block_within_dim_pruner(
489
+ n_arg, "BLOCK_SIZE_N", when=lambda args: not args.get("SWIZZLED_SCALES", True)
490
+ )
491
+ )
492
+ # trap gates run FIRST (on the full grid, raising when empty); the poison fences after
493
+ # them are advisory, so an emptying poison fence falls back to trap-clean survivors —
494
+ # never to a miscompile (the old single mixed filter fell back to the untouched grid,
495
+ # re-admitting the single-trip/width traps it existed to remove).
496
+ return compose_pruners(
497
+ *stages,
498
+ config_filter(nvfp4_native_ok, when=scales_are_e4m3),
499
+ config_filter(
500
+ mma_trap_ok, when=lambda args: is_sm10x(), on_empty=raise_all_mma_trapped
501
+ ),
502
+ config_filter(
503
+ dot_arm_ok,
504
+ when=lambda args: is_sm10x()
505
+ and scaled_mma_available(args)
506
+ and acts_are_scaled(args),
507
+ ),
508
+ config_filter(mma_poison_ok, when=lambda args: is_sm10x()),
509
+ )
510
+
511
+
512
+ def dot_scaled_staging_pruner():
513
+ """``early_config_prune`` dropping ``dot_scaled`` where the MMA's M operand degenerates to a
514
+ single row: ``BLOCK_SIZE_M == 1`` with no ``SWAP_AB`` to move the operand to ``BLOCK_SIZE_N``.
515
+ The scaled MMA has no 1-row staging and the config dies in ``PassManager::run failed`` at every
516
+ warps/stages cell — the decode arms that DO serve BM=1 are scalar, or dot_scaled swapped (which
517
+ puts the weight rows in M). Measured on the batched mx kernel: the last 6 dead compiles per
518
+ tune after the swapped-scalar act fence.
519
+
520
+ Two staging laws, both of which ``mx_config_pruner`` already enforces for NVFP4/E4M3 and
521
+ neither of which was applied to the UE8M0 half:
522
+
523
+ - ``num_warps`` outside {4, 8}: the scaled MMA has no staging for it (w16 s6 measured failing).
524
+ - a 1-row M operand, per above.
525
+
526
+ Measured on the batched mx kernel: 128 dead compiles per tune on HEAD, 2 after both clauses."""
527
+
528
+ def ok(c, args):
529
+ if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
530
+ return True
531
+ if c.num_warps not in (4, 8):
532
+ return False
533
+ if c.kwargs.get("SWAP_AB"):
534
+ return True
535
+ return config_dim(c, args, "BLOCK_SIZE_M") > 1
536
+
537
+ return config_filter(ok)
538
+
539
+
540
+ def block_dynamic_2d_warp_spec_pruner():
541
+ """``early_config_prune`` for the block-dynamic 2D kernel's two WARP_SPEC compile-fail
542
+ laws (Triton 3.7.1 PassManager, TritonGPUPipeline; charted 2026-08-13 at the
543
+ DeepSeek-V4-Flash ue8m0 checkpoint shapes, forced-config matrices):
544
+ - WS + num_warps=16 fails at EVERY tile/scale-dtype (w8 compiles everywhere). The mx
545
+ 2D kernel WS-compiles w16 fine; the bd GROUPED kernel does NOT on its pointer arm
546
+ (same law, see block_dynamic_grouped_matmul_pruner) — so this tracks the loop's
547
+ structure and memory mode, not a global WS law.
548
+ - the implicit ``dot_scaled`` arm (``USE_DOT_SCALED``: UE8M0/uint8 ``As`` on a
549
+ native-M tile — (BN if SWAP_AB else BM) >= 128) + WS fails at every warps/stages/
550
+ memory-mode cell; BM=64 and non-WS all compile. fp32-scale launches never emit the
551
+ scaled MMA and keep the (w<=8) WS grid.
552
+ Mostly 2D-only: the grouped bd loop WS-compiles from its descriptor arm and its own
553
+ race guard REQUIRES WS at BM >= 64, but it shares the dot_scaled+WS failure on the
554
+ POINTER arm (see block_dynamic_grouped_matmul_pruner). Compile soundness is
555
+ per-loop-structure AND per-memory-mode — chart both before reusing a law."""
556
+
557
+ def ok(c, args):
558
+ if not c.kwargs.get("WARP_SPEC"):
559
+ return True
560
+ if c.num_warps > 8:
561
+ return False
562
+ if getattr(args.get("As"), "dtype", None) == torch.uint8:
563
+ native_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
564
+ return config_dim(c, args, native_m) < 128
565
+ return True
566
+
567
+ return config_filter(ok, when=lambda args: is_sm10x())
568
+
569
+
570
+ def block_dynamic_mma_width_pruner():
571
+ """``early_config_prune`` for the block-dynamic kernels' implicit ``dot_scaled`` arm
572
+ (UE8M0 scales on a native-M tile — ``USE_DOT_SCALED`` in the kernel): the sm_10x
573
+ width cap ``mx_config_pruner``'s trap gate applies to the MX kernels, which the bd
574
+ family never got. Under GATE the stacked gate|up dot is ``2 * BLOCK_SIZE_N`` wide, so
575
+ a BN=256 GATE config emits a width-512 scaled MMA — Triton miscompiles those into a
576
+ sticky illegal-address device trap the tuner trips mid-tune. Scoped to launches whose
577
+ ``As`` is UE8M0 (uint8) on sm_10x; fp32-scale launches never emit the scaled MMA.
578
+ Configs below the native-M tile keep the plain ``tl.dot`` + software rescale and pass
579
+ untouched; the swapped N side is the token tile (within the cap on every grid)."""
580
+
581
+ def ok(c, args):
582
+ native_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
583
+ if config_dim(c, args, native_m) < 128 or c.kwargs.get("SWAP_AB"):
584
+ return True
585
+ width = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
586
+ return width <= SM10X_SCALED_MMA_MAX_N
587
+
588
+ def raise_all_too_wide(configs, args):
589
+ raise ValueError(
590
+ "every autotune config emits a scaled MMA wider than "
591
+ f"{SM10X_SCALED_MMA_MAX_N} (GATE={bool(args.get('GATE'))}) — sm_10x "
592
+ "miscompiles those into a sticky device trap; extend the grid below the cap."
593
+ )
594
+
595
+ return config_filter(
596
+ ok,
597
+ when=lambda args: is_sm10x()
598
+ and getattr(args.get("As"), "dtype", None) == torch.uint8,
599
+ on_empty=raise_all_too_wide,
600
+ )
601
+
602
+
603
+ def fp8_dot_warp_pruner():
604
+ """SILENTLY-WRONG fence: the fp8 ``tl.dot`` paths lose accuracy above ``num_warps=2`` at both
605
+ ends of the M-tile range, on sm_90. Two clauses, one phenomenon.
606
+
607
+ (1) The single-token swapped GEVM. ``swap_pad_rhs`` pads the lone token to the ``MMA_N_ATOM``
608
+ rhs and the epilogue takes column 0 after the K-loop (``acc_init``'s ``[BN, MMA_N_ATOM]``
609
+ accumulator). Above 2 warps that column comes back wrong.
610
+
611
+ (2) Large M tiles, ``BLOCK_SIZE_M >= 64``, in the grouped and 2D kernels.
612
+
613
+ Measured on H100 (sm_90), triton 3.6 AND 3.8, by forcing every admitted config against the
614
+ torch oracle and scoring error in bf16-ULP units (the floor — what perfect bf16 output
615
+ storage costs — is 0.49 mean / 1.00 max; the torch reference itself sits exactly there)::
616
+
617
+ BLOCK_SIZE_M 16 / 32, any num_warps 0.49 mean / 1.0 max ULP bit-optimal
618
+ BLOCK_SIZE_M 64 / 128, num_warps 8 0.58 mean / 52.1 max ULP WRONG (grouped)
619
+ BLOCK_SIZE_M 64 / 128, num_warps >2 280 of 578 configs WRONG (2D)
620
+ SWAP_AB + BLOCK_SIZE_M=1, num_warps >2 WRONG (batched)
621
+
622
+ The tuner benches by SPEED, so these get crowned silently: block-static crowned a wrong one
623
+ outright, while block-dynamic / tensor-dynamic (batched) and the grouped kernel carried
624
+ 30-90 wrong configs each behind a GREEN test, because the winner happened to be correct.
625
+
626
+ In the grouped kernel ``BLOCK_SIZE_M >= 64`` is admitted only at ``num_warps=8``, so those two
627
+ axes are confounded there and the data cannot separate them; the 2D kernel does separate them
628
+ (BM=64 at num_warps=2 is clean), which is why the rule is written on num_warps.
629
+
630
+ Scoped by a POSITIVE ``is_sm90()`` test, not by ``not is_sm10x()``: the latter is also true on
631
+ XPU, ROCm and driverless build boxes, which do not run the NVIDIA codegen this was probed
632
+ against, and restricting them on sm_90 evidence would be a guess. sm_10x is likewise untouched
633
+ — it is where this repo's tuning and validation were done and its tcgen05 MMA lowers these
634
+ paths differently. Other CUDA arches (sm_89, sm_120) are unmeasured and so unfenced; each
635
+ needs its own probe. The sm_90 cost is real — the grouped kernel is the prefill path, where
636
+ the large-M tiles are the throughput — and is accepted: the alternative is a crowned config
637
+ that returns wrong results."""
638
+
639
+ def ok(c, args):
640
+ bm = c.kwargs.get("BLOCK_SIZE_M", 1)
641
+ if c.num_warps <= 2:
642
+ return True
643
+ if bm >= 64: # (2) large-M tile
644
+ return False
645
+ # (1) swapped single-token GEVM; native-M tiles need no pad/column-extract
646
+ return not (c.kwargs.get("SWAP_AB") and bm < MMA_N_ATOM_WIDTH)
647
+
648
+ def raise_all_wrong(configs, args):
649
+ raise ValueError(
650
+ "every autotune config is an fp8 tl.dot at num_warps>2 with a BLOCK_SIZE_M that "
651
+ "returns WRONG results off sm_10x; no correct config exists in this grid — extend it "
652
+ "with a num_warps=2 or a 16/32-row M tile rather than lifting the fence."
653
+ )
654
+
655
+ return config_filter(ok, when=lambda args: is_sm90(), on_empty=raise_all_wrong)
656
+
657
+
658
+ def scale_subblock_pruner(min_ctas_per_sm: int = 4):
659
+ """``early_config_prune`` for kernels whose compute tile (``BLOCK_SIZE_N``) may subdivide the
660
+ quant block (``BLOCK_N``). One scale covers the whole block, so a narrower tile just reads its
661
+ own block — and it multiplies the N grid, the lever while the whole-block grid is too thin to
662
+ hide the loop's latency (a gate|up fusion halves that grid again by folding both into one program).
663
+
664
+ Sub-block tiles are admitted ONLY there: below ``min_ctas_per_sm`` whole-block programs per SM.
665
+ "Fills the device" (one program per SM) is NOT enough at decode — the loop is latency-bound, so
666
+ a 192-448-program grid still gains from more, narrower tiles: forced BN=64 vs the BN=128 crown,
667
+ bit-identical, DSV4 fp8 decode gate 24.9 -> 23.4us and down 13.4 -> 12.2, DSV3 down 22.3 -> 21.7
668
+ but DSV3 gate 0.88x (2026-08-29) — a tuner call, so the fence only trims prefill, where a
669
+ thousands-of-programs grid never wants the 3x wider config space (measurably worse winners
670
+ when it carried it).
671
+
672
+ A fused requant always pins the tile to the whole block: its output scale is one amax per
673
+ (row, ``BLOCK_N``) group, which a partial tile can't compute. Raises when nothing
674
+ survives: a tile wider than the quant block reads ONE scale across two blocks — silently
675
+ wrong results — so a checkpoint whose ``BLOCK_N`` is off the tile grid must fail loudly,
676
+ not fall back to the full grid."""
677
+
678
+ def ok(c, args):
679
+ block_n = args.get("BLOCK_N")
680
+ if block_n is None:
681
+ return True
682
+ bn = config_dim(c, args, "BLOCK_SIZE_N")
683
+ starved = args["S"] * triton.cdiv(args["N"], block_n) < min_ctas_per_sm * sm_count(
684
+ args["B"].device.index
685
+ )
686
+ if args.get("OUTPUT_FORMAT") is not None or not starved:
687
+ return bn == block_n
688
+ return bn <= block_n and args["N"] % bn == 0
689
+
690
+ def raise_no_block_tile(configs, args):
691
+ raise ValueError(
692
+ f"no BLOCK_SIZE_N in the autotune grid fits the weight quant block "
693
+ f"(BLOCK_N={args.get('BLOCK_N')}); a wider tile would read one scale across "
694
+ f"two quant blocks and return wrong results — extend the grid or requantize "
695
+ f"the checkpoint to a gridded block size."
696
+ )
697
+
698
+ return config_filter(ok, on_empty=raise_no_block_tile)
699
+
700
+
701
+ def packed_schedule_scope_pruner(min_bm: int = 128):
702
+ """``early_config_prune`` scoping ``PACKED_SCHEDULE`` rows to big-BM configs — the only
703
+ regime the packed resolve wins (register relief unlocking pressure-walled tiles: DSV3
704
+ bf16 forced pairs +10.7% at BM128 vs −12.5% at BM64/BN256 and a wash at BM32,
705
+ 2026-08-06; both tuned crowns chose BM128). Everywhere else the inline resolve's free
706
+ ALU beats the packed arm's per-tile dependent loads, so those rows are tune-budget
707
+ dilution (the axis would double the grid; this keeps it +25%)."""
708
+
709
+ def ok(c, args):
710
+ if not c.kwargs.get("PACKED_SCHEDULE"):
711
+ return True
712
+ return config_dim(c, args, "BLOCK_SIZE_M") >= min_bm
713
+
714
+ return config_filter(ok)
715
+
716
+
717
+ def weight_only_warp_spec_matched_mode_pruner():
718
+ """``early_config_prune`` for the weight-only grouped kernel's WARP_SPEC lowering wall
719
+ (Triton 3.7.1 ``TritonGPUOptimizePartitionWarps`` -> PassManager::run failed; the pass
720
+ cannot partition this loop when BOTH operand loads share a memory mode). Charted
721
+ 2026-08-26 (48-cell forced-config matrix, BM 64 and 128 IDENTICAL — BM-independent):
722
+
723
+ CM A/B modes w4 w8 w16
724
+ dot ptr/ptr ok ok FAIL
725
+ dot desc/desc ok ok ok (was charted FAIL/FAIL — refuted, see _TRAPPING_NUM_WARPS)
726
+ dot_scaled ptr/ptr FAIL FAIL FAIL
727
+ dot_scaled desc/desc ok ok ok (w8 was charted FAIL — refuted)
728
+ MIXED modes (ptr/desc, desc/ptr): 24/24 ok at every warp count.
729
+ BN=32 + WS: FAIL in every cell (the tile law, fenced here).
730
+
731
+ The non-monotone warp dependence (w16 rescues desc/desc dot but breaks ptr/ptr dot) marks
732
+ this as the pass's internal partition feasibility, not kernel source — the source is
733
+ identical across passing and failing cells. Two root-cause attempts on the sibling family
734
+ are refuted and recorded (block_dynamic_grouped_matmul_pruner); the kernel-side remedy is
735
+ to stop emitting configs the pass cannot lower. Cost in the wild: 19 dead compiles per
736
+ weight-only tune, and under inductor ONE failing config kills the whole torch.compile
737
+ cell instead of scoring inf — this fence is what recovers those cells."""
738
+
739
+ # desc/desc rows REMOVED 2026-08-29: forced through the tuner's own Config objects (pre_hook
740
+ # intact) at the charted GPT-OSS shape, (dot, desc/desc) w4 AND w8 and (dot_scaled, desc/desc) w8
741
+ # all compile and run bit-identical — dot+WS+BK=128 is the gate_up's best config (2128 vs 2235us).
742
+ # The original matrix was charted with hand-built Configs, which skip the descriptor pre_hooks
743
+ # and fail every descriptor cell for the wrong reason. The pointer rows stand unrefuted.
744
+ _TRAPPING_NUM_WARPS = {
745
+ ("dot", "pointer"): {16},
746
+ ("dot_scaled", "pointer"): {4, 8, 16},
747
+ }
748
+
749
+ def ok(c, args):
750
+ if not c.kwargs.get("WARP_SPEC"):
751
+ return True
752
+ # BN=32 + WS fails to lower in every memory mode / warp count / compute mode probed
753
+ # (9/9 cells, GPT-OSS N=K=2880, 2026-08-29) — the tile law the matrix never encoded
754
+ if config_dim(c, args, "BLOCK_SIZE_N") == 32:
755
+ return False
756
+ a, b = c.kwargs.get("A_MEMORY_MODE"), c.kwargs.get("B_MEMORY_MODE")
757
+ if a != b: # mixed modes lower everywhere
758
+ return True
759
+ return c.num_warps not in _TRAPPING_NUM_WARPS.get((c.kwargs.get("COMPUTE_MODE"), a), set())
760
+
761
+ return config_filter(ok, when=lambda args: is_sm10x())
762
+
763
+
764
+ def weight_only_swap_scope_pruner():
765
+ """``early_config_prune`` binding the weight-only kernel's ``SWAP_AB`` axis to its one
766
+ implemented arm: the CUDA-core reduce. Swap exists here to drop the MMA entirely — with
767
+ raw bf16 activations there is no native fp4 x bf16 MMA, so both dot arms pay an M->16 pad
768
+ and a full-tile upcast, and a swapped ``dot_scaled`` measured no better than the row-major
769
+ one (35.2 vs 38.0us, gpt-oss gate_up decode). So ``SWAP_AB`` iff ``COMPUTE_MODE`` is
770
+ ``"scalar"``: the swapped-dot rows would compile into an arm the compute helper does not
771
+ implement, and a non-swapped scalar row has no accumulator shape."""
772
+
773
+ def ok(c, args):
774
+ return bool(c.kwargs.get("SWAP_AB")) == (c.kwargs.get("COMPUTE_MODE") == "scalar")
775
+
776
+ return config_filter(ok)
777
+
778
+
779
+ def mx_2d_swap_scope_pruner(max_m: int = 16):
780
+ """``early_config_prune`` scoping the 2D mx kernel's ``SWAP_AB`` rows to the ONE regime
781
+ they exist for: E4M3-scale (NVFP4) single-token decode. NVFP4 has no other native M=1
782
+ arm — ``dot_scaled`` has no sub-M=128 form for E4M3 scales (PassManager) and scalar is
783
+ E2M1-unpack-bound (measured 60-142µs vs swap's 22-50µs on the GLM attn shape,
784
+ 2026-08-05). Everywhere else swap is a proven loser (an 18-cell forced-swap sweep:
785
+ UE8M0 formats' scalar/dot arms win, M3 attn swap −38%), so its rows are dropped:
786
+ UE8M0-scale launches, M above the decode band, and any non-BM=1 row (the mx swap
787
+ compute flattens the token tile — BM=1 is structural, enforced by a static_assert).
788
+
789
+ Also drops UNSWAPPED ``scalar``: the arm exists only in its swapped form here, so those
790
+ configs cannot build an accumulator and fail to compile."""
791
+
792
+ def ok(c, args):
793
+ swap = bool(c.kwargs.get("SWAP_AB"))
794
+ # Under GATE, scalar exists only as the swapped arm: unswapped it has no accumulator
795
+ # shape (``acc_init`` is handed "dot" when not swapped) and every such config dies in
796
+ # CompilationError (3 dead compiles per gated mxfp4 tune). NOT true ungated — unswapped
797
+ # scalar is a legitimate (and sometimes winning) W4A4 arm there, and fencing it globally
798
+ # crowned a wrong config at N=320.
799
+ if c.kwargs.get("COMPUTE_MODE") == "scalar" and not swap and args.get("GATE"):
800
+ return False
801
+ if not swap:
802
+ return True
803
+ return (
804
+ config_dim(c, args, "BLOCK_SIZE_M") == 1
805
+ and getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn
806
+ and args["M"] <= max_m
807
+ )
808
+
809
+ return config_filter(ok)
810
+
811
+
812
+ def swizzled_out_bm_pruner():
813
+ """``early_config_prune`` pinning the SWIZZLE_32_4_4 requant OUTPUT arm (``SWIZZLED_OUT``) to
814
+ ``BLOCK_SIZE_M == BLOCK_SIZE_N == 128`` — the output-side twin of
815
+ ``swizzled_scales_bm_pruner``.
816
+
817
+ ``_epilogue_requant_mx`` writes the group scales straight into the down proj's swizzled
818
+ layout as ``q_s.reshape(1, 4, 32, REP_K_CS, 4)`` with ``REP_K_CS = (BN // group) // 4``.
819
+ ``q_s`` is ``[BM, BN // group]``, so the reshape balances only at ``BM == 128``
820
+ (``BM * (BN//group) == 128 * (BN//group)``); every other BM raises "reshape() cannot change
821
+ total number of elements". BN is pinned with it because the descriptor's block shape is the
822
+ tuned ``REP_K_CS``. The code comment there already said "BM/BN pinned 128" — this makes the
823
+ config space agree, instead of handing the tuner configs that cannot compile.
824
+
825
+ Measured 2026-08-26 on the gated fp4-requant cells: 288 dead compiles per tune, i.e. every
826
+ non-128 config of the swizzled-out key (``SWIZZLED_OUT`` is in the tune key, so the key is
827
+ entirely swizzled). Un-swizzled requant keeps every BM/BN — it stores Cs row-major."""
828
+
829
+ def ok(c, args):
830
+ return (
831
+ config_dim(c, args, "BLOCK_SIZE_M") == 128
832
+ and config_dim(c, args, "BLOCK_SIZE_N") == 128
833
+ )
834
+
835
+ return config_filter(ok, when=lambda args: args.get("SWIZZLED_OUT"))
836
+
837
+
838
+ def swizzled_scales_bm_pruner():
839
+ """``early_config_prune`` pinning the grouped MX pre-swizzled (``SWIZZLED_SCALES``) arm to
840
+ ``BLOCK_SIZE_M == 128`` and a whole-128-block ``BLOCK_SIZE_N``. BM: the offline act-quant lays each expert's scale
841
+ slab out 128-padded, and the kernel's per-tile scale-block index (``pid_m``) only lines up
842
+ when the M tile is exactly 128 (``build_tile_layout`` pads experts on the same granularity).
843
+ BN: the scale is read as whole 128-row SWIZZLE_32_4_4 blocks off the descriptor (under GATE,
844
+ the gate 128-block + the up 128-block stacked into the 2*BN tile); a sub-128 BN would need a
845
+ partial-block read the descriptor can't express, while any whole multiple is a multi-block bulk
846
+ load the pre_hook sizes per config — BN=256 wins the down GEMM (665 -> 576us at DSV4 prefill,
847
+ bit-identical; GATE at 256 is the scaled-MMA width cap's veto, ``mx_config_pruner``).
848
+
849
+ Both pins are swizzled-slab properties, so the un-swizzled (affine) arm keeps every BM and
850
+ BN: ``build_tile_layout`` tiles any BM with a row mask, and the affine act-scale read is
851
+ indexed by the gathered row, not ``pid_m``."""
852
+
853
+ def ok(c, args):
854
+ return (
855
+ config_dim(c, args, "BLOCK_SIZE_M") == 128
856
+ and config_dim(c, args, "BLOCK_SIZE_N") % 128 == 0
857
+ )
858
+
859
+ return config_filter(ok, when=lambda args: args.get("SWIZZLED_SCALES"))
860
+
861
+
862
+ def swizzled_scale_config_pruner(allow_gate_subblock=False):
863
+ """Drop pre-swizzled-scale configs the SWIZZLE_32_4_4 descriptor load can't serve, gated on
864
+ ``SWIZZLED_SCALES`` (the un-swizzled arm loads scales per group and takes any tile):
865
+
866
+ - ``BLOCK_SIZE_K < 4 * SCALE_GROUP_K``: the un-swizzle reads whole 4-group K bands
867
+ (``REP_K = (BK // SCALE_GROUP_K) // 4``), so a smaller BK collapses ``REP_K`` to 0 and the
868
+ reshape traps — 128 for group-32 (MX), 64 for group-16 (NVFP4). BK must also tile whole
869
+ bands (``BK % (4 * group) == 0``).
870
+ - ``BLOCK_SIZE_N > 128`` not a whole-block multiple: the descriptor arm bulk-loads
871
+ ``BN // 128`` swizzled blocks (each kernel's pre_hook sizes the box per config), a sub-128 BN
872
+ reads a scalar slice of one block; a BN between is neither, and a multi-block BN whose
873
+ last tile passes the 128-padded N extent zero-fills (see the fit bound below). BN=256 is
874
+ a legal two-block load where it fits (the GATE tile has always read one) and wins at
875
+ prefill — grouped down 665 -> 576us, dense 2D 703 -> 552us at M=49152, bit-identical; the
876
+ scaled-MMA width cap is ``mx_config_pruner``'s.
877
+ - under ``GATE``, ``BLOCK_SIZE_N != 128``: the gate|up scale is interleaved as whole 128-row
878
+ block pairs [g0,u0,g1,u1,...], read as one 2*BN tile; a sub-128 BN can't index a block pair
879
+ off the descriptor. ``allow_gate_subblock`` (batched decode) admits BN in (32, 64): its
880
+ scale leaf pointer-GATHERs the sub-128 gate|up tile out of the interleaved buffer, which is
881
+ the raw path's BN=32 gate|up tile without un-swizzling (the whole swizzled-vs-raw decode
882
+ gap). Packed fp4 is admitted too: the historical sub-tile fp4->bf16 lowering trap
883
+ (``xVals.size() % 4``) no longer reproduces — mxfp4 AND nvfp4 forced at BN∈{32,64},
884
+ BK=128, scalar, K=2048 all run bit-correct (2026-08-05); a resurfacing trap is a loud
885
+ compile failure (forgiven inf), never silent."""
886
+
887
+ def ok(c, args):
888
+ band = 4 * args["SCALE_GROUP_K"] # one whole swizzled K band (4 scale groups)
889
+ if config_dim(c, args, "BLOCK_SIZE_K") % band != 0:
890
+ return False
891
+ bn = config_dim(c, args, "BLOCK_SIZE_N")
892
+ if args.get("GATE"):
893
+ return bn == 128 or (allow_gate_subblock and bn in (32, 64))
894
+ if bn > 128:
895
+ # BN=256 pipelines OOM smem at num_stages >= 5 (253 KB at s5, 304 KB at s6 for the
896
+ # fp8 x fp4 and fp8 x fp8 dot_scaled tiles, BK=128; s4 fits) — below smem_pruner's
897
+ # keep-side bound, so without this the TPE burns whole 200-trial budgets on dead
898
+ # compiles (observed 2026-08-29).
899
+ if c.num_stages >= 5:
900
+ return False
901
+ # A multi-block tile must FIT the swizzled grid: the bulk load reads BN // 128 whole
902
+ # blocks with no row mask, and the grid is padded to 128 rows — a tile whose blocks
903
+ # pass the padded extent TMA-zero-fills the scales: silent garbage, never a trap
904
+ # (mxfp4_swizzled N=128 forced at BN=256 fails parity on triton 3.7.1 and 3.8 alike;
905
+ # the BN=256 prefill wins were measured at N % 256 == 0 shapes).
906
+ n = args.get("N")
907
+ if n is not None and -(-n // bn) * bn > -(-n // 128) * 128:
908
+ return False
909
+ return bn <= 128 or bn % 128 == 0
910
+
911
+ def raise_no_swizzled_tile(configs, args):
912
+ raise ValueError(
913
+ "no autotune config can serve pre-swizzled scales for this launch (the "
914
+ "SWIZZLE_32_4_4 read needs BLOCK_SIZE_K % 128 == 0 and a <=128-row or 128-multiple N tile; "
915
+ f"GATE={bool(args.get('GATE'))}) — the contraction dim likely has no "
916
+ "128-dividing tile; pass affine (row-major) scales for this shape."
917
+ )
918
+
919
+ return config_filter(
920
+ ok, when=lambda args: args.get("SWIZZLED_SCALES"), on_empty=raise_no_swizzled_tile
921
+ )
922
+
923
+
924
+ def block_dynamic_grouped_matmul_pruner():
925
+ """``early_config_prune`` for the block-dynamic grouped kernel: the Triton 3.7.1
926
+ pipeliner-race guard, sized to its four-load-stream single-dot K-loop. The GATE arm
927
+ shares the guard: post-activation-prequant it dots the stacked gate|up as the same
928
+ single-dot loop (one wider weight tile, not a second dot/load stream) and was
929
+ flake-verified race-free cross-process. Measured on sm_100, 15-fresh-process flake
930
+ runs per cell, big and tiny shapes:
931
+
932
+ - ``warp_specialize`` compiles iff ``num_warps % 4 == 0`` (its async partitions) and
933
+ ``BLOCK_SIZE_M >= 64``, and is race-free everywhere it compiles — but on the POINTER
934
+ arm it hits a lowering wall: "'arith.constant' op does not have expected attribute
935
+ ttg.partition" -> PassManager::run failed (TritonGPUPipeline). The descriptor arm
936
+ always compiles. The obvious suspect — the masked activation load's ``other=0.0``,
937
+ which is the known cause of the analogous mx-grouped fence — was PROBED and is NOT
938
+ it: dropping the mask entirely leaves every pointer cell below still failing, so the
939
+ trigger is structural to the pointer arm, not that constant. Do not re-try the
940
+ clamp-not-mask rewrite on this basis. Two INDEPENDENT legs, forced-config matrices,
941
+ 2026-08-21 B200:
942
+ * ``num_warps >= 16`` fails at BM 64 AND 128, with fp32 scales as well as UE8M0 —
943
+ a property of this loop under WS, not of the scaled MMA.
944
+ * uint8/UE8M0 ``As`` on a native-M ``BLOCK_SIZE_M >= 128`` tile (the implicit
945
+ ``dot_scaled`` arm) fails at every warps/stages cell: pointer 0/6, descriptor
946
+ 4/6 + 2 smem.
947
+ Combined with the race guard below, the descriptor arm is the only BM=128 route for
948
+ UE8M0 grouped — which is what makes the native tcgen05 tile reachable at all here.
949
+ - The default pipeliner RACES at ``BLOCK_SIZE_M >= 64`` (3/15 wrong at BM64/w8) and is
950
+ clean at BM16/32.
951
+
952
+ The regions are disjoint, so per ``BLOCK_SIZE_M``: BM >= 64 keeps only the compilable
953
+ WS configs, BM < 64 keeps only non-WS. CUDA-only — the race is a CUDA pipeliner
954
+ artifact and the WS axis isn't emitted elsewhere.
955
+
956
+ Also attached to the STATIC grouped kernel as an INHERITED guard: its three-load-stream
957
+ K-loop (scalar act scale preloaded, no per-row ``as`` stream) was never flake-mapped —
958
+ race soundness is per-loop-structure, so this map may over-fence it (perf cost) or, if
959
+ the 3-stream loop races elsewhere, under-fence it. Re-map with multi-process flake runs
960
+ before trusting the static attachment."""
961
+
962
+ def ok(c, args):
963
+ bm = config_dim(c, args, "BLOCK_SIZE_M")
964
+ if bm < 64:
965
+ return not c.kwargs.get("WARP_SPEC")
966
+ if not (c.kwargs.get("WARP_SPEC") and c.num_warps % 4 == 0):
967
+ return False
968
+ if c.kwargs.get("A_MEMORY_MODE") == "pointer":
969
+ if c.num_warps >= 16:
970
+ return False
971
+ if getattr(args.get("As"), "dtype", None) == torch.uint8 and bm >= 128:
972
+ return False
973
+ return True
974
+
975
+ return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
976
+
977
+
978
+ def scalar_max_m_pruner(m_arg: str, max_m: int = 64):
979
+ """``early_config_prune`` dropping ``scalar`` configs when the launch's row count
980
+ (``m_arg``) exceeds ``max_m``: scalar is a BM=1 GEVM — sensible for decode-sized M,
981
+ hopeless at prefill, and hopeless-but-benched configs poison the TPE's per-dimension
982
+ densities (measured: with scalar in the M=8192 grid the 2D MX attn prefill tune
983
+ landed 0.48x vs hub; without it, 2.06x)."""
984
+
985
+ def ok(c, args):
986
+ return c.kwargs.get("COMPUTE_MODE") != "scalar"
987
+
988
+ return config_filter(ok, when=lambda args: args[m_arg] > max_m)
989
+
990
+ def matched_memory_modes_pruner():
991
+ """Drop configs that mix pointer and descriptor operand loads — for a DENSE 2D GEMM
992
+ only (contiguous A, whole M in one tile). Measured across M=1..8192 on the block-scale
993
+ 2D matmul: mixed only ever TIES the matched combos (<1us at small M) and loses at large
994
+ M, so pruning it costs no win and halves the descriptor search. Do NOT use this on the
995
+ GROUPED kernels: there M is distributed across experts (S/E small), A is token-GATHERED,
996
+ and A=pointer + B=descriptor (mixed) is the dominant winner — those keep both axes free.
997
+ Non-empty (the matched configs remain)."""
998
+
999
+ def ok(c, args):
1000
+ a_ptr = c.kwargs.get("A_MEMORY_MODE", "pointer") == "pointer"
1001
+ b_ptr = c.kwargs.get("B_MEMORY_MODE", "pointer") == "pointer"
1002
+ return a_ptr == b_ptr
1003
+
1004
+ return config_filter(ok)
1005
+
1006
+
1007
+ def descriptor_needs_prequant_pruner():
1008
+ """Keep only pointer-mode configs when ``A`` is raw (bf16/fp16 — the ``maybe_act_quant``
1009
+ M<threshold inline-quant arm). A descriptor operand load needs a pre-quantized fp8 ``A``: the
1010
+ inline arm reads ``a_ptrs``, which ``operand_tile_ptrs`` leaves a dead placeholder under a
1011
+ descriptor mode, so a descriptor config there would compute garbage yet bench fast (a broken
1012
+ config the tuner would crown). Gated on ``A``'s dtype so the offline (fp8) path keeps both
1013
+ memory axes; a no-op for callers that always pre-quantize (``A`` already fp8)."""
1014
+
1015
+ def ok(c, args):
1016
+ return (
1017
+ c.kwargs.get("A_MEMORY_MODE", "pointer") == "pointer"
1018
+ and c.kwargs.get("B_MEMORY_MODE", "pointer") == "pointer"
1019
+ )
1020
+
1021
+ return config_filter(
1022
+ ok, when=lambda args: args["A"].dtype not in (torch.float8_e4m3fn, torch.int8, torch.uint8)
1023
+ )
1024
+
1025
+
1026
+ def descriptor_box_pruner(k_dim="BLOCK_SIZE_K"):
1027
+ """Drop descriptor configs whose TMA box exceeds the tensormap's 256-per-dim limit:
1028
+ the weight inner dim is the K tile in BYTES over values-per-byte (packed E2M1
1029
+ halves it; E4M3 is one byte per value), so BK=512 e4m3 boxes are illegal while
1030
+ BK=512 packed ones fit. Pointer configs pass untouched. ``k_dim`` names the K-tile
1031
+ axis — ``BLOCK_SIZE_K`` for the tune_block_nk kernels, ``block_k`` for the block-scale
1032
+ kernels whose K tile is the caller's fixed scale block."""
1033
+
1034
+ def ok(c, args):
1035
+ # tiles may be tuned (config kwargs) or pinned launch args — config_dim reads both
1036
+ bk = config_dim(c, args, k_dim)
1037
+ if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer":
1038
+ weight_vpb = (
1039
+ 2 if getattr(args.get("B"), "dtype", None) == torch.uint8 else 1
1040
+ )
1041
+ if bk // weight_vpb > 256 or config_dim(c, args, "BLOCK_SIZE_N") > 256:
1042
+ return False
1043
+ # A TMA descriptor needs a unit-stride innermost dim; a transposed operand
1044
+ # (column-major view, as a backward pass hands in) has none, so only the
1045
+ # pointer arm can serve it. The kernel itself is stride-general.
1046
+ b = args.get("B")
1047
+ if b is not None and getattr(b, "ndim", 0) == 2 and b.stride(-1) != 1:
1048
+ return False
1049
+ if c.kwargs.get("A_MEMORY_MODE", "pointer") != "pointer":
1050
+ # descriptor A, both arms tuner-routed per the tokens-per-expert key:
1051
+ # contiguous boxes (no gather) win at dense routing (-7.1/-7.4%) and lose at
1052
+ # skew; tma gather4 (gathered rows, 1-row box, sm_100 only) is MONOTONIC in
1053
+ # tokens/expert — 2x loss at 32/expert, parity at 1024, -5.3/-4.7% at 4096
1054
+ # (32k tokens, 2026-07-16). Only the arch is fenced.
1055
+ act_vpb = 2 if getattr(args.get("A"), "dtype", None) == torch.uint8 else 1
1056
+ if args.get("GatherIdx") is not None and not is_sm10x():
1057
+ return False
1058
+ if bk // act_vpb > 256:
1059
+ return False
1060
+ return True
1061
+
1062
+ return config_filter(ok)
1063
+
1064
+
1065
+ def global_scale_warp_spec_pruner():
1066
+ """Drop WARP_SPEC configs when the launch carries an NVFP4 global (``BsGlobal`` not None):
1067
+ the per-expert global load cannot take ``ttg.partition`` under warp specialization — the
1068
+ same Triton lowering gap as the affine scale gather's ``other=0.0`` — so every such config
1069
+ is a benign compile-failure inf. Fenced to save the compiles; single-level launches
1070
+ (``BsGlobal`` None) keep their WS rows."""
1071
+
1072
+ def ok(c, args):
1073
+ return not (c.kwargs.get("WARP_SPEC") and args.get("BsGlobal") is not None)
1074
+
1075
+ return config_filter(ok)
1076
+
1077
+
1078
+ def gate_stacked_tmem_trap_pruner():
1079
+ """Miscompile gate: drop GATE configs at exactly ``BLOCK_SIZE_N == 256, BLOCK_SIZE_M == 64``
1080
+ — the stacked gate|up accumulator (``2*BN = 512`` columns) exceeds sm_100 tensor memory, and
1081
+ at BM=64 Triton's resource checker misses the overflow, so the config runs and TRAPS the
1082
+ device (sticky CUDA 716 "misaligned address"; one such trial poisons the whole tune and
1083
+ every launch after it). Probed per-arm in fresh processes (2026-08-10, B200, weight-only
1084
+ grouped, K=2880 and K=2048 alike — N-divisibility is NOT the variable): BM=64 traps in every
1085
+ memory mode, WS on and off, BK 64 and 128; BM=128 fails CLEANLY (OutOfResources: tensor
1086
+ memory 544 > 512) and self-prunes; BM=32 and BN<=128 run correct. The checker gap is the
1087
+ only reason this needs a fence at all — drop precisely the probed trap point and let every
1088
+ neighbor keep self-reporting.
1089
+
1090
+ ON sm_90 the whole ``2*BN = 512`` stacked tile goes, not just the BM=64 point. The
1091
+ self-pruning above is a TENSOR-MEMORY overflow, and sm_90 has no tensor memory to overflow:
1092
+ nothing raises OutOfResources, so the config runs and returns garbage instead. Measured on
1093
+ H100, triton 3.8, nvfp4 gate|silu with fused requant through the 2D kernel — sampling the
1094
+ admitted grid, ``BLOCK_SIZE_N`` splits cleanly::
1095
+
1096
+ BN 32 / 64 / 128 60 correct, 0 wrong
1097
+ BN 256 0 correct, 9 wrong (BM=128; the crowned config varies per tune,
1098
+ so the scenario test passes or fails by luck)
1099
+
1100
+ The mechanism is the stacked accumulator width, which is format-independent, so this fences
1101
+ every GATE config at BN=256 rather than only the nvfp4 cell that exposed it. The BM=64 trap
1102
+ point stays fenced on every arch (it was probed on B200); the wider sm_90 clause is scoped by
1103
+ a positive arch test so XPU/ROCm, which never ran this codegen, keep their full grid."""
1104
+
1105
+ def ok(c, args):
1106
+ if not args.get("GATE") or config_dim(c, args, "BLOCK_SIZE_N") != 256:
1107
+ return True
1108
+ if config_dim(c, args, "BLOCK_SIZE_M") == 64:
1109
+ return False # the probed sm_100 trap point — fenced on every arch
1110
+ return not is_sm90() # sm_90 has no tensor memory, so nothing self-reports the overflow
1111
+
1112
+ def raise_all_trap(configs, args):
1113
+ raise ValueError(
1114
+ "every config in the grid is the (GATE, BN=256, BM=64) tensor-memory trap point; "
1115
+ "falling back would run configs that trap the device (sticky CUDA 716)"
1116
+ )
1117
+
1118
+ return config_filter(ok, on_empty=raise_all_trap)
1119
+
1120
+
1121
+ def smem_pruner(k_dim="BLOCK_SIZE_K", min_sets: float = 0.5):
1122
+ """``early_config_prune`` dropping configs whose shared memory certainly cannot fit,
1123
+ with the bound picked by the operand dtypes (sampled from ``metadata.shared`` across
1124
+ every kernel family, 22 cells):
1125
+
1126
+ - both operands >= 2-byte floats (the unquantized loops): ``num_stages`` full
1127
+ buffers of both tiles (exact) plus the fixed barrier terms at their sampled
1128
+ MINIMA (+16 base; +28 warp-spec, observed up to 48 varying with warps; +32
1129
+ host-descriptor) — minima keep the bound below the true usage, so a config is
1130
+ only ever dropped when its operand buffers alone cannot fit.
1131
+ - quantized ``dot_scaled`` (the only quantized arm whose tiles can reach the limit):
1132
+ the per-allocation law, every term named by its TTGIR ``local_alloc`` and exact to
1133
+ the allocator's alignment/packing slack (<= 16 B observed; barriers pack into
1134
+ padding, so no fixed constant is added — the slack is keep-side) —
1135
+ ``num_stages - 1`` buffers of operands+scales at the native M=128 staging (weights
1136
+ full-width even when fp4-packed; GATE shifts the A tile to a full ``num_stages``),
1137
+ or the bf16-upcast fallback below the gate (one single-buffered upcast tile,
1138
+ packed weights, no A-scale buffers).
1139
+ - quantized ``dot``/``scalar``: the raw-operand floor (their 32-wide K tiles cannot
1140
+ reach the limit).
1141
+
1142
+ ``min_sets`` scales the quantized ``dot_scaled`` bound: the default HALF a set is the
1143
+ family-wide keep-side envelope (the 2D nvfp4 arm sub-tiles a request into half a set);
1144
+ the GROUPED kernel never compiles below TWO full sets — fresh-tune logs at the DSV4 W4A8
1145
+ gate_up key (244 trials, 2026-08-29): 0 of 82 compiling configs had 2*set > smem, while
1146
+ 120 of 162 dead ones did (every BK=512 request, 59/59), so ``min_sets=2`` there halves the
1147
+ dead compiles a 100-trial tune pays without dropping a single compiling config.
1148
+
1149
+ Both arms are EMPIRICAL models of this Triton's allocator, fit to and verified
1150
+ against ``metadata.shared`` samples (not derived from a spec) — chosen so the error
1151
+ direction under the sampled behavior is always toward KEEPING a config, which then
1152
+ merely compiles and self-prunes as inf. All samples taken on B200 (sm_100) —
1153
+ re-verify on a Triton bump AND on H100 or the target device (smem capacity and
1154
+ allocator behavior are per-arch). Non-empty fallback."""
1155
+
1156
+ def ok(c, args):
1157
+ a, b = args["A"], args["B"]
1158
+ tiles = 2 if args.get("GATE") else 1
1159
+ bk = config_dim(c, args, k_dim)
1160
+ unquantized = a.element_size() >= 2 and b.element_size() >= 2
1161
+ stages = c.num_stages if unquantized else c.num_stages - 1
1162
+ packed = 2 if b.dtype == torch.uint8 else 1
1163
+ bm = config_dim(c, args, "BLOCK_SIZE_M")
1164
+ bn = config_dim(c, args, "BLOCK_SIZE_N")
1165
+ if unquantized:
1166
+ # sampled minima of the fixed allocations beside the operand buffers
1167
+ # (the barrier terms vary by tens of bytes with warps; minima only ever
1168
+ # err toward keeping a config)
1169
+ fixed = (
1170
+ 16
1171
+ + (28 if c.kwargs.get("WARP_SPEC") else 0)
1172
+ + (32 if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer" else 0)
1173
+ )
1174
+ need = fixed + stages * bk * (
1175
+ a.element_size() * bm + b.element_size() * tiles * bn
1176
+ )
1177
+ elif c.kwargs.get("COMPUTE_MODE") == "dot_scaled":
1178
+ # KEEP-SIDE bound = HALF of one buffer set: operands stored PACKED (fp4 = 2
1179
+ # values/byte) plus both group scale tiles (BK/group cols; group 32 UE8M0, 16
1180
+ # E4M3). Requested-stages models over-estimate — the pipeliner CLAMPS stages
1181
+ # (an s3 request can compile with an s2 footprint) and the 2D nvfp4 arm can
1182
+ # compile half a set — so any tighter bound drops configs the compiler would
1183
+ # have rescued. Proven est<=actual on 1,099 metadata.shared samples across the
1184
+ # family (see OPTIMIZATION_LOG: smem-keep-side-refit).
1185
+ group = 16 if getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn else 32
1186
+ # A's smem bytes follow its LAUNCH dtype: packed E2M1 (int8/uint8) = 2 values/byte,
1187
+ # E4M3 = 1 B/value, raw bf16/fp16 (weight-only) = element_size B/value
1188
+ a_dtype = a.dtype
1189
+ if a_dtype in (torch.int8, torch.uint8):
1190
+ a_bytes = (bk // 2) * bm
1191
+ else:
1192
+ a_bytes = bk * bm * a.element_size()
1193
+ # HALF a set: the sampled lower envelope. The 2D kernel's nvfp4 arm can compile an
1194
+ # s2 request into half a buffer set (sub-tile pipelining: BN=64/128/256 x BK=512
1195
+ # all landed at set/2 + <=8 B), so the full set is not a lower bound; the half set
1196
+ # is, over all 1099 samples (largest remaining margin 68 B on a 4 KB decode tile,
1197
+ # covered by the halving). A config whose HALF set exceeds the limit cannot compile
1198
+ # in any observed form — everything else is kept and self-reports as a benign inf.
1199
+ # The family-wide keep-side model counts fp4 weights PACKED; the grouped pipeline stages
1200
+ # them at full E4M3 width (BN=256, BK=128, s5 OOMs at 253 KB = 5 x 50 KB sets, which only
1201
+ # a full-width B tile adds up to), so the whole-set bound (min_sets >= 1) uses full width.
1202
+ b_bytes = bk * tiles * bn if min_sets >= 1 else (bk // packed) * tiles * bn
1203
+ need = int(
1204
+ (a_bytes + b_bytes + (bk // group) * bm + (bk // group) * tiles * bn) * min_sets
1205
+ )
1206
+ else:
1207
+ # dot (BK = the 32-group) / scalar: tiles too small to reach the limit —
1208
+ # the raw-operand floor suffices
1209
+ need = stages * (bk * bm + (bk // packed) * tiles * bn)
1210
+ return need <= sm_shared_memory_limit()
1211
+
1212
+ return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
1213
+
build/torch-rocm/quant.py ADDED
@@ -0,0 +1,955 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ import triton
17
+ import triton.language as tl
18
+ from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
19
+
20
+ from .bayesian_autotuner import bayesian_autotune
21
+ from .formats import global_scale_stride, is_per_expert_global
22
+ from .compat import add_op_namespace_prefix, FP8_DTYPE, MX_SCALE_GROUP_K, NVFP4_SCALE_GROUP_K, compile_time_only_triton_op, compile_time_only_triton_wrap, decode_pdl, device_context, is_sm10x
23
+ from .swizzle import swizzle_store_block
24
+ from .scheduling import build_tile_layout, resolve_tile_inline
25
+
26
+ # ── Triton-side helpers (inlined by ``@triton.jit`` callers) ──────────────────
27
+
28
+
29
+ @triton.jit
30
+ def fp8_act_quant_inline(
31
+ a_raw, TRANSPOSED: tl.constexpr = False, UE8M0: tl.constexpr = False
32
+ ):
33
+ """Inline FP8 (E4M3) activation quant for the W8A8 block-scale path.
34
+
35
+ Per-token amax → fp32 scale ``amax/448`` (floored at 1e-12 against zero rows)
36
+ → cast values to FP8. Returns ``(a_fp8, a_s)`` with ``a_s`` shaped ``(M,)``;
37
+ ``TRANSPOSED`` marks a ``(K, M)`` tile (the swapped descriptor arm), where the
38
+ token axis is 0 instead of 1.
39
+
40
+ ``UE8M0`` ceils each scale up to a power of two and returns the E8M0 exponent
41
+ byte (uint8) instead of the fp32 scale — the group format the tcgen05
42
+ ``dot_scaled`` MMA consumes. The ceil mirrors DeepGEMM's ``ceil_to_ue8m0``
43
+ (add ``0x7FFFFF``, clear the mantissa) so our scales match its checkpoints.
44
+ """
45
+ if TRANSPOSED:
46
+ a_s = tl.max(tl.abs(a_raw), axis=0) / 448.0
47
+ else:
48
+ a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
49
+ denom = tl.maximum(a_s, 1e-12)
50
+ if UE8M0:
51
+ bits = (denom.to(tl.int32, bitcast=True) + 0x7FFFFF) & ~0x7FFFFF
52
+ denom = bits.to(tl.float32, bitcast=True)
53
+ if TRANSPOSED:
54
+ a_fp8 = (a_raw / denom[None, :]).to(tl.float8e4nv)
55
+ else:
56
+ a_fp8 = (a_raw / denom[:, None]).to(tl.float8e4nv)
57
+ if UE8M0:
58
+ a_scale = ((bits >> 23) & 0xFF).to(tl.uint8)
59
+ else:
60
+ a_scale = a_s
61
+ return a_fp8, a_scale
62
+
63
+
64
+ # cvt.e2m1x2.f32 (hardware FP4 pack) exists only on sm_100 (Blackwell). Resolved once at
65
+ # import as a compile-time constexpr for the jit helper below; the ALU fallback compiles
66
+ # everywhere else. ``is_sm10x`` is driverless-safe, so no import-time guard is needed.
67
+ _E2M1_HW_CVT = tl.constexpr(is_sm10x())
68
+
69
+
70
+ @triton.jit
71
+ def _quant_e2m1_packed(v, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_K: tl.constexpr):
72
+ """Pack signed, pre-scaled values on the E2M1 grid to ``(M, K//2)`` uint8 (first value
73
+ of each pair in the low nibble). On sm_100 the Blackwell hardware convert
74
+ ``cvt.rn.satfinite.e2m1x2.f32`` (two f32 → one packed byte, first operand → HIGH nibble)
75
+ does it in one instruction; elsewhere a ``>=``-threshold bucketize builds the code. The
76
+ two agree except at exact E2M1 midpoints (0.25, 0.75, …), where the hardware rounds to
77
+ nearest-even and the ALU form rounds half-up. Only the taken arm compiles."""
78
+ if _E2M1_HW_CVT:
79
+ lo, hi = tl.split(tl.reshape(v, (BLOCK_SIZE_M, BLOCK_SIZE_K // 2, 2)))
80
+ packed = tl.inline_asm_elementwise(
81
+ "{ .reg .b8 t8; cvt.rn.satfinite.e2m1x2.f32 t8, $1, $2; cvt.u16.u8 $0, t8; }",
82
+ "=h,f,f",
83
+ [hi, lo],
84
+ dtype=tl.uint16,
85
+ is_pure=True,
86
+ pack=1,
87
+ )
88
+ values = (packed & 0xFF).to(tl.uint8)
89
+ else:
90
+ av = tl.abs(v)
91
+ code = (
92
+ (av >= 0.25).to(tl.int32)
93
+ + (av >= 0.75).to(tl.int32)
94
+ + (av >= 1.25).to(tl.int32)
95
+ + (av >= 1.75).to(tl.int32)
96
+ + (av >= 2.5).to(tl.int32)
97
+ + (av >= 3.5).to(tl.int32)
98
+ + (av >= 5.0).to(tl.int32)
99
+ ) | ((v < 0).to(tl.int32) << 3)
100
+ lo, hi = tl.split(tl.reshape(code, (BLOCK_SIZE_M, BLOCK_SIZE_K // 2, 2)))
101
+ values = (lo | (hi << 4)).to(tl.uint8)
102
+ return values
103
+
104
+
105
+ @triton.jit
106
+ def mx_act_quant_inline(
107
+ a_raw,
108
+ BLOCK_SIZE_M: tl.constexpr,
109
+ BLOCK_SIZE_K: tl.constexpr,
110
+ SCALE_GROUP_K: tl.constexpr,
111
+ FORMAT: tl.constexpr = "mxfp8",
112
+ ):
113
+ """Inline MX activation quant, one helper for both value grids. Per-row, per-K-group
114
+ amax → UE8M0 scale (ceil to the next power of two via the exponent-bump trick, the
115
+ divisor being the grid's largest magnitude) → values onto the format's grid:
116
+
117
+ - ``"mxfp8"``: cast to E4M3 — returns ``((M, K) fp8, (M, K // SCALE_GROUP_K) uint8)``.
118
+ - ``"mxfp4"``: round to E2M1 and pack nibble pairs (``_quant_e2m1_packed`` — hardware
119
+ ``cvt.e2m1x2`` on sm_100, else a ``>=``-threshold bucketize; they agree off exact
120
+ midpoints) — returns ``((M, K//2) uint8, (M, K // SCALE_GROUP_K) uint8)``.
121
+ - ``"nvfp4"``: E4M3 scale (amax/6, not a power of two), values divide by the DECODED
122
+ scale before the E2M1 grid — returns ``((M, K//2) uint8, (M, K // SCALE_GROUP_K) E4M3)``.
123
+
124
+ Only the taken format arm compiles."""
125
+ a_groups = tl.reshape(
126
+ a_raw, (BLOCK_SIZE_M, BLOCK_SIZE_K // SCALE_GROUP_K, SCALE_GROUP_K)
127
+ )
128
+ amax = tl.max(tl.abs(a_groups), axis=2)
129
+ if FORMAT == "nvfp4":
130
+ # E4M3 scale (amax/6 rounded to E4M3, NOT a power of two); values divide by the
131
+ # DECODED scale before hitting the E2M1 grid — the standard NVFP4 two-step
132
+ scales = (amax / 6.0).to(tl.float8e4nv)
133
+ decoded = tl.maximum(scales.to(tl.float32), 1.1754944e-38)
134
+ v = tl.reshape(a_groups / decoded[:, :, None], (BLOCK_SIZE_M, BLOCK_SIZE_K))
135
+ values = _quant_e2m1_packed(v, BLOCK_SIZE_M, BLOCK_SIZE_K)
136
+ elif FORMAT == "mxfp4":
137
+ bits = (amax / 6.0).to(tl.int32, bitcast=True)
138
+ # ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
139
+ exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
140
+ exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
141
+ exp_ceil = tl.where(amax == 0, 127, exp_ceil)
142
+ scales = exp_ceil.to(tl.uint8)
143
+ a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
144
+ v = tl.reshape(a_groups / a_s_pow2[:, :, None], (BLOCK_SIZE_M, BLOCK_SIZE_K))
145
+ values = _quant_e2m1_packed(v, BLOCK_SIZE_M, BLOCK_SIZE_K)
146
+ else:
147
+ bits = (amax / 448.0).to(tl.int32, bitcast=True)
148
+ # ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
149
+ exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
150
+ exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
151
+ scales = exp_ceil.to(tl.uint8)
152
+ a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
153
+ values = tl.reshape(
154
+ a_groups / tl.maximum(a_s_pow2[:, :, None], 1e-12),
155
+ (BLOCK_SIZE_M, BLOCK_SIZE_K),
156
+ ).to(tl.float8e4nv)
157
+ return values, scales
158
+
159
+
160
+ @triton.jit
161
+ def _e2m1_code_to_e4m3_bits(code):
162
+ """One E2M1 4-bit code -> the E4M3 byte holding the same value, in pure integer
163
+ ops. Every E2M1 magnitude ``{0, .5, 1, 1.5, 2, 3, 4, 6}`` is exact in E4M3, and
164
+ above the 0.5 subnormal the mapping is affine in the code: ``bits = (mag + 12) << 2``
165
+ (exponent re-bias +6, mantissa bit lands at bit 2). No float math, no converts —
166
+ callers bitcast the byte to ``float8e4nv``."""
167
+ code = code.to(tl.int32)
168
+ mag = code & 7
169
+ bits = tl.where(mag == 0, 0, tl.where(mag == 1, 0x30, (mag + 12) << 2))
170
+ return bits | ((code >> 3) << 7)
171
+
172
+
173
+ @triton.jit
174
+ def _e2m1_code_to_bf16_bits(code):
175
+ """One E2M1 4-bit code -> the bf16 bit pattern (uint16) holding the same value, pure int ops.
176
+ The 8 magnitudes ``{0,.5,1,1.5,2,3,4,6}`` are exact in bf16; above the 0.5 subnormal the mapping
177
+ is affine in the code: ``bits = 0x3F80 + (mag - 2) * 0x40`` (each step doubles/1.5x's cleanly on
178
+ the bf16 exponent/mantissa). Callers bitcast the uint16 to bf16 — no float convert, and no E4M3
179
+ intermediate. bf16 is the exact intermediate for every activation dtype, not the final one: the
180
+ caller widens the dequantized tile if the activation is wider."""
181
+ code = code.to(tl.int32)
182
+ mag = code & 7
183
+ bits = tl.where(mag == 0, 0, tl.where(mag == 1, 0x3F00, 0x3F80 + (mag - 2) * 0x40))
184
+ return bits | ((code >> 3) << 15) # sign at bit 15
185
+
186
+
187
+ @triton.jit
188
+ def _e2m1_pair_to_f16(packed):
189
+ """One packed E2M1 byte -> the two fp16 values it holds, as a uint32 (low half = low nibble),
190
+ through the sm_100 CVT unit: ONE instruction per pair against the bit-twiddle form's ~10
191
+ integer ops. Every E2M1 magnitude is exact in fp16, so this is a value-identical decode, and
192
+ the direction of the unpack is irrelevant to the cost — the arithmetic is the whole bill
193
+ (measured on the gpt-oss W4A16 decode weight stream: 39.6us bit-twiddle, either axis, vs
194
+ 11.6us here, against an 8.0us load-only floor). PTX has no 8-bit asm operand, so the byte
195
+ rides in a 16-bit register and is narrowed inside the block."""
196
+ return tl.inline_asm_elementwise(
197
+ "{ .reg .b8 t; cvt.u8.u16 t, $1; cvt.rn.f16x2.e2m1x2 $0, t; }",
198
+ "=r,h",
199
+ [packed.to(tl.uint16)],
200
+ dtype=tl.uint32,
201
+ is_pure=True,
202
+ pack=1,
203
+ )
204
+
205
+
206
+ @triton.jit
207
+ def e2m1_to_bf16(b_packed):
208
+ """Unpack packed MXFP4 (E2M1, two nibbles/byte along K) straight to bf16, doubling the K (row)
209
+ dim: ``(R, C) uint8 -> (2R, C) bf16``. Direct (no E4M3 round-trip), and the low/high nibbles
210
+ interleave along K with a SINGLE 3D axis-swap (``join`` -> ``[R, C, 2]`` -> swap last two ->
211
+ reshape) instead of the three 2D transposes ``e2m1_to_e4m3`` needs — the weight-only unpack hot path
212
+ (fp4 weight upcast in-loop, the caller widening to the activation dtype). K order low-nibble-first, matching
213
+ ``e2m1_to_e4m3``: ``[byte0_lo, byte0_hi, byte1_lo, ...]``. On sm_100 the codes decode through
214
+ the hardware CVT (``_e2m1_pair_to_f16``, fp16 being exact for the grid); elsewhere the integer
215
+ bit-build. Only the taken arm compiles."""
216
+ if _E2M1_HW_CVT:
217
+ pair = _e2m1_pair_to_f16(b_packed)
218
+ lo = (pair & 0xFFFF).to(tl.uint16)
219
+ hi = (pair >> 16).to(tl.uint16)
220
+ paired = tl.trans(tl.join(lo, hi), 0, 2, 1)
221
+ f16 = tl.reshape(
222
+ paired, (2 * b_packed.shape[0], b_packed.shape[1])
223
+ ).to(tl.float16, bitcast=True)
224
+ return f16.to(tl.bfloat16)
225
+ lo = _e2m1_code_to_bf16_bits(b_packed & 0xF) # [R, C] int32 (bf16 bits)
226
+ hi = _e2m1_code_to_bf16_bits(b_packed >> 4) # [R, C]
227
+ paired = tl.join(lo, hi) # [R, C, 2] (pair in last dim)
228
+ paired = tl.trans(paired, 0, 2, 1) # [R, 2, C] (pair adjacent in row dim)
229
+ out = tl.reshape(paired, (2 * b_packed.shape[0], b_packed.shape[1])) # [2R, C]
230
+ return out.to(tl.uint16).to(tl.bfloat16, bitcast=True)
231
+
232
+
233
+ @triton.jit
234
+ def _e2m1_pair_to_e4m3(packed):
235
+ """One packed E2M1 byte -> its two values as an E4M3 pair (uint16, low byte = low nibble),
236
+ through the CVT unit in two instructions — fp4 to fp16 to fp8 — against the bit-build's ~8
237
+ integer ops per value. Both hops are exact: every E2M1 magnitude is representable in fp16
238
+ and in E4M3, so the pair is value-identical to ``_e2m1_code_to_e4m3_bits``."""
239
+ return tl.inline_asm_elementwise(
240
+ "{ .reg .b8 t; .reg .b32 f; cvt.u8.u16 t, $1; cvt.rn.f16x2.e2m1x2 f, t; "
241
+ "cvt.rn.satfinite.e4m3x2.f16x2 $0, f; }",
242
+ "=h,h",
243
+ [packed.to(tl.uint16)],
244
+ dtype=tl.uint16,
245
+ is_pure=True,
246
+ pack=1,
247
+ )
248
+
249
+
250
+ @triton.jit
251
+ def e2m1_cols_to_bf16(packed):
252
+ """Column-unpack packed E2M1 to bf16: ``(..., C) uint8 -> (..., 2C)``, low nibble first — the
253
+ column-axis counterpart of the row-doubling ``e2m1_to_bf16``, for the swapped weight-only
254
+ tile (output rows leading, K contiguous). Same sm_100 hardware decode; the pair lands in
255
+ adjacent columns, so ``tl.interleave`` places it (no axis swap)."""
256
+ if _E2M1_HW_CVT:
257
+ pair = _e2m1_pair_to_f16(packed)
258
+ lo = (pair & 0xFFFF).to(tl.uint16)
259
+ hi = (pair >> 16).to(tl.uint16)
260
+ f16 = tl.interleave(lo, hi).to(tl.float16, bitcast=True)
261
+ return f16.to(tl.bfloat16)
262
+ bits = tl.interleave(
263
+ _e2m1_code_to_bf16_bits(packed & 0xF), _e2m1_code_to_bf16_bits(packed >> 4)
264
+ )
265
+ return bits.to(tl.uint16).to(tl.bfloat16, bitcast=True)
266
+
267
+
268
+ @triton.jit
269
+ def e2m1_cols_to_f32(packed):
270
+ """Column-unpack packed E2M1 straight to fp32, for the CUDA-core reduces: ``(..., C) uint8 ->
271
+ (..., 2C)``. The narrow-float hop its siblings owe their MMA callers (bf16 for ``tl.dot``, E4M3
272
+ for the scaled MMA) is pure work for a reduce that widens to fp32 anyway — measured 12.6 ->
273
+ 15.4us on the gpt-oss decode tile for the bf16 round trip alone. Bit-identical to going via
274
+ either: every E2M1 value is exact in f16, bf16, E4M3 and fp32."""
275
+ if _E2M1_HW_CVT:
276
+ pair = _e2m1_pair_to_f16(packed)
277
+ lo = (pair & 0xFFFF).to(tl.uint16)
278
+ hi = (pair >> 16).to(tl.uint16)
279
+ f16 = tl.interleave(lo, hi).to(tl.float16, bitcast=True)
280
+ return f16.to(tl.float32)
281
+ bits = tl.interleave(
282
+ _e2m1_code_to_bf16_bits(packed & 0xF), _e2m1_code_to_bf16_bits(packed >> 4)
283
+ )
284
+ return bits.to(tl.uint16).to(tl.bfloat16, bitcast=True).to(tl.float32)
285
+
286
+
287
+ @triton.jit
288
+ def e2m1_cols_to_e4m3(packed):
289
+ """Column-unpack packed E2M1 (two nibbles per byte along the last dim, low nibble
290
+ first) to E4M3: ``(..., C) uint8 -> (..., 2C)`` — the column-axis counterpart of the
291
+ row-doubling ``e2m1_to_e4m3``; lossless (every E2M1 value is exact in E4M3). sm_100
292
+ decodes through the CVT unit (``_e2m1_pair_to_e4m3``), elsewhere the integer bit-build;
293
+ either way the bytes are bitcast once. Only the taken arm compiles."""
294
+ if _E2M1_HW_CVT:
295
+ pair = _e2m1_pair_to_e4m3(packed)
296
+ bits = tl.interleave(pair & 0xFF, pair >> 8)
297
+ else:
298
+ bits = tl.interleave(
299
+ _e2m1_code_to_e4m3_bits(packed & 0xF), _e2m1_code_to_e4m3_bits(packed >> 4)
300
+ )
301
+ return bits.to(tl.uint8).to(tl.float8e4nv, bitcast=True)
302
+
303
+
304
+ @triton.jit
305
+ def e2m1_to_f32(b_packed):
306
+ """Row-doubling counterpart of ``e2m1_cols_to_f32``: ``(R, C) uint8 -> (2R, C)`` fp32, for the
307
+ non-swapped CUDA-core reduce. Skips the E4M3 hop ``e2m1_to_e4m3`` owes its ``tl.dot`` callers;
308
+ bit-identical, since every E2M1 value is exact in E4M3, f16 and fp32."""
309
+ if _E2M1_HW_CVT:
310
+ pair = _e2m1_pair_to_f16(b_packed)
311
+ lo = (pair & 0xFFFF).to(tl.uint16)
312
+ hi = (pair >> 16).to(tl.uint16)
313
+ unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
314
+ return unpacked.to(tl.uint16).to(tl.float16, bitcast=True).to(tl.float32)
315
+ lo = _e2m1_code_to_bf16_bits(b_packed & 0xF)
316
+ hi = _e2m1_code_to_bf16_bits(b_packed >> 4)
317
+ unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
318
+ return unpacked.to(tl.uint16).to(tl.bfloat16, bitcast=True).to(tl.float32)
319
+
320
+
321
+ @triton.jit
322
+ def e2m1_to_e4m3(b_packed):
323
+ """Unpack packed MXFP4 (E2M1, two nibbles/byte along K) to E4M3, doubling the K
324
+ (row) dim: ``(R, C) uint8 -> (2R, C) E4M3``. E2M1's 8 magnitudes are all exact in
325
+ E4M3, so this is lossless — it lets the FP8 ``tl.dot`` path stand in for
326
+ ``tl.dot_scaled`` at decode (avoiding its M->128 pad). K order is the low nibble
327
+ first: ``[byte0_lo, byte0_hi, byte1_lo, ...]``. sm_100 decodes through the CVT unit
328
+ (``_e2m1_pair_to_e4m3``); elsewhere the integer bit-build. Only the taken arm compiles."""
329
+ if _E2M1_HW_CVT:
330
+ pair = _e2m1_pair_to_e4m3(b_packed)
331
+ lo, hi = pair & 0xFF, pair >> 8
332
+ else:
333
+ lo = _e2m1_code_to_e4m3_bits(b_packed & 0xF)
334
+ hi = _e2m1_code_to_e4m3_bits(b_packed >> 4)
335
+ # interleave along the K (row) dim via trans -> interleave-last-dim -> trans back
336
+ unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
337
+ return unpacked.to(tl.uint8).to(tl.float8e4nv, bitcast=True)
338
+
339
+
340
+ def _quant_block_k_pruner(configs, named_args, **kwargs):
341
+ """Keep configs whose BLOCK_K divides K (the quant grid is K // BLOCK_K programs per row;
342
+ K is always a multiple of 32, so the BLOCK_K=32 configs guarantee a non-empty list). On the
343
+ SWIZZLED path, additionally require BLOCK_K a multiple of ``4 * SCALE_GROUP_K`` so a whole
344
+ SWIZZLE_32_4_4 col-block (4 scale groups) lands inside one K-tile. BLOCK_T is free on the dense
345
+ grid (the per-element store handles any tile height) but pinned to 128 on the GROUPED grid,
346
+ where one program == one 128-row expert-sorted tile (``build_tile_layout``'s pad granularity)."""
347
+ args = {**named_args, **kwargs}
348
+ k, g = args["K"], args["SCALE_GROUP_K"]
349
+ swizzled, grouped = args.get("SWIZZLED", False), args.get("GROUPED", True)
350
+ return [
351
+ c
352
+ for c in configs
353
+ if k % c.kwargs["BLOCK_K"] == 0
354
+ and (not swizzled or c.kwargs["BLOCK_K"] % (4 * g) == 0)
355
+ and (not grouped or c.kwargs["BLOCK_T"] == 128)
356
+ ]
357
+
358
+
359
+ @triton.jit
360
+ def store_mx_act_scales(
361
+ S,
362
+ SOut,
363
+ out_row, # per-lane output row (any int vector: contiguous grid rows or scattered dests)
364
+ s,
365
+ row_mask,
366
+ kb,
367
+ T,
368
+ K: tl.constexpr,
369
+ SCALE_GROUP_K: tl.constexpr,
370
+ BLOCK_K: tl.constexpr,
371
+ SWIZZLED: tl.constexpr,
372
+ MASK: tl.constexpr, # "rows" (per-lane), "blocks" (whole 128-row blocks past T), "none"
373
+ ):
374
+ """Store one act-quant scale tile. Row-major (``S``, per ``out_row``) or straight into the
375
+ SWIZZLE_32_4_4 buffer (``SOut``) as PER-ELEMENT ptr arithmetic (no 128-row reshape): s[t, g]
376
+ lands at (block*cb_total + kb*REP_K + rep)*512 + r32*16 + outer4*4 + c4, where block =
377
+ out_row//128, r32 = out_row%32, outer4 = (out_row%128)//32, and the col-block splits g into
378
+ rep = g//4, c4 = g%4. Byte-identical to the old reshape form at BLOCK_T=128 (grouped grid)
379
+ but valid for any out_row vector, so the dense grid, the grouped-sorted grid, and the
380
+ scatter (quantize-once) stores all share it. Masking is the caller\'s store policy: the
381
+ grouped-sorted grid writes its padding rows too ("none" — quantized zeros the GEMM never
382
+ reads), the dense grid masks whole tiles past the real row-blocks ("blocks"), scattered
383
+ and row-major stores mask per lane ("rows")."""
384
+ groups: tl.constexpr = BLOCK_K // SCALE_GROUP_K
385
+ if SWIZZLED:
386
+ cb_total = K // SCALE_GROUP_K // 4
387
+ lg = tl.arange(0, groups)
388
+ rep = lg // 4
389
+ block = out_row // 128
390
+ off = (
391
+ (block[:, None] * cb_total + kb * (groups // 4) + rep[None, :]) * 512
392
+ + (out_row % 32)[:, None] * 16
393
+ + ((out_row % 128) // 32)[:, None] * 4
394
+ + (lg % 4)[None, :]
395
+ )
396
+ if MASK == "rows":
397
+ tl.store(SOut + off, s, mask=row_mask[:, None])
398
+ elif MASK == "blocks":
399
+ tl.store(SOut + off, s, mask=block[:, None] < tl.cdiv(T, 128))
400
+ else:
401
+ tl.store(SOut + off, s)
402
+ else:
403
+ sg = kb * groups + tl.arange(0, groups)
404
+ tl.store(
405
+ S + out_row.to(tl.int64)[:, None] * (K // SCALE_GROUP_K) + sg[None, :],
406
+ s,
407
+ mask=row_mask[:, None],
408
+ )
409
+
410
+
411
+ @bayesian_autotune(
412
+ [
413
+ triton.Config({"BLOCK_K": bk, "BLOCK_T": bt}, num_warps=w)
414
+ for bk in (32, 64, 128, 256)
415
+ for bt in (8, 16, 32, 64, 128)
416
+ for w in (2, 4, 8)
417
+ ],
418
+ # t_bucket (log2 of the token count) is in the key: at small T the tile is the only
419
+ # parallelism lever while at prefill scale it isn't — same bucketing as the grouped
420
+ # kernels (raw T would retune per unique token count). SWIZZLED keys the swizzled-scale
421
+ # store separately (a disjoint config basin), GROUPED the grid — its 128-row expert tiles
422
+ # pin BLOCK_T, so its crowns cannot be shared with the dense grid's. FORMAT keys the value
423
+ # dtype/packing (E4M3 vs packed E2M1, and SCALE_GROUP_K 32 vs 16) — a dtype-blind key hands
424
+ # packed MXFP4 the E4M3 config and mistunes it.
425
+ ["K", "t_bucket", "SWIZZLED", "GROUPED", "FORMAT"],
426
+ n_trials=100,
427
+ prune_configs_by={"early_config_prune": _quant_block_k_pruner},
428
+ )
429
+ @triton.jit
430
+ def _mx_act_quant_kernel(
431
+ X,
432
+ Y,
433
+ S, # (T, K // SCALE_GROUP_K) row-major scales; None on the swizzled path
434
+ SOut, # flat SWIZZLE_32_4_4 scale buffer (1, n_tiles, cb, 2, 256); None on the plain path
435
+ GatherIdx, # (S,) int32 sorted position -> source row of X; read only when GROUPED and not None
436
+ ExpertStart, # (NUM_EXPERTS_POW2 + 1,) int32 cumulative sorted-row starts; read iff GROUPED
437
+ GlobalScale, # fp32 NVFP4 second-level global, per tensor or per expert; None ⇒ single-level (arm folds out)
438
+ GlobalIdx, # (T,) row -> expert map, for a per-expert global on the DENSE grid (the batched op's own expert_ids); None on the grouped grid, whose tiles carry an expert
439
+ stride_global, # expert stride of GlobalScale (0 broadcasts a scalar)
440
+ last_global, # index of GlobalScale's last entry — clamps GlobalIdx, whose EP sentinel rows name an out-of-range expert (they are masked out of the GEMM, so any in-range global serves them)
441
+ stride_x_t,
442
+ stride_x_k,
443
+ T,
444
+ t_bucket, # autotune key only (log2 token-count bucket); unused in body
445
+ K: tl.constexpr,
446
+ SCALE_GROUP_K: tl.constexpr,
447
+ # dynamo's triton wrapper appends the tuner's config kwargs after the call kwargs
448
+ # and requires signature order — the tuned axes stay LAST
449
+ FORMAT: tl.constexpr = "mxfp8",
450
+ SWIZZLED: tl.constexpr = False,
451
+ GROUPED: tl.constexpr = True, # grid: expert-sorted tiles (True) vs plain dense rows (False)
452
+ NUM_EXPERTS_POW2: tl.constexpr = 1, # always passed explicitly; see the dense launch
453
+ BLOCK_K: tl.constexpr = 32,
454
+ BLOCK_T: tl.constexpr = 32,
455
+ PDL: tl.constexpr = False,
456
+ ):
457
+ """One-pass activation quant, one launch per format (``mx_act_quant_inline`` does
458
+ the math, so the offline and inline forms are bit-identical by construction): E4M3 +
459
+ UE8M0 ("mxfp8"), packed E2M1 + UE8M0 ("mxfp4"), or packed E2M1 + E4M3 group-16
460
+ ("nvfp4"). Group boundaries are identical across forms (SCALE_GROUP_K | BLOCK_K | K).
461
+ Arbitrary input strides.
462
+
463
+ Two independent axes. ``GROUPED`` picks the GRID: dense ``(cdiv(T, BLOCK_T), K // BLOCK_K)``
464
+ over plain row tiles (the one-row-per-program form starved memory at 1.5-1.8 TB/s on the
465
+ packed formats; the row tile coalesces), or ``(n_m_tiles, K // BLOCK_K)`` over the
466
+ expert-sorted, 128-padded tile layout (``build_tile_layout``, so BLOCK_T is pinned there).
467
+ A grouped program gathers its tile's source rows through ``GatherIdx`` (padding masked) and
468
+ scatters the VALUES back to source row order — the GEMM still TMA-gathers them, and duplicate
469
+ sorted->source writes store identical bytes — and its tile carries the expert a per-expert
470
+ NVFP4 global is indexed by, which is why only the dense grid needs a row map for that.
471
+
472
+ ``SWIZZLED`` picks the SCALE STORE: straight into the SWIZZLE_32_4_4 layout the grouped GEMM
473
+ reads affine (the inverse of ``load_swizzled_scale``), so no post-quant gather/swizzle pass,
474
+ or row-major ``S``. Padding-row scales are quantized zeros (harmless: the GEMM masks those
475
+ rows' values to 0)."""
476
+ if PDL:
477
+ gdc_wait()
478
+ kb = tl.program_id(1)
479
+ if SWIZZLED:
480
+ pid_m = tl.program_id(0)
481
+ # scale output-row position (== source row on the dense path); the swizzled block index is
482
+ # so // 128, valid for ANY BLOCK_T (no 128-row pin) so the dense grid autotunes BLOCK_T.
483
+ so = pid_m * BLOCK_T + tl.arange(0, BLOCK_T)
484
+ if GROUPED:
485
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
486
+ ExpertStart, NUM_EXPERTS_POW2, BLOCK_T
487
+ )
488
+ expert_id, sorted_idx, row_mask = resolve_tile_inline(
489
+ tl.program_id(0), exp_start, freqs, tile_start_excl, e_offs, BLOCK_T
490
+ )
491
+ if GatherIdx is not None:
492
+ in_row = tl.load(GatherIdx + sorted_idx, mask=row_mask, other=0).to(tl.int64)
493
+ else:
494
+ in_row = sorted_idx.to(tl.int64)
495
+ else:
496
+ expert_id = 0
497
+ rows = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
498
+ in_row = rows.to(tl.int64)
499
+ row_mask = rows < T
500
+ offs = kb * BLOCK_K + tl.arange(0, BLOCK_K)
501
+ x = tl.load(
502
+ X + in_row[:, None] * stride_x_t + offs[None, :] * stride_x_k,
503
+ mask=row_mask[:, None],
504
+ other=0.0,
505
+ ).to(tl.float32)
506
+ if GlobalScale is not None:
507
+ # NVFP4 two-level: normalize by the calibrated global before the block quant — block
508
+ # scales are then amax/6 of x/g (the canonical two-step); the GEMM folds g back onto the
509
+ # accumulator (g_a·g_b). A per-expert global is indexed by the tile's OWN expert, which
510
+ # the grouped grid resolved for free — the rows of a tile belong to one expert; the dense
511
+ # grid has no such layout and takes the caller's row map. None folds the arm out.
512
+ if GlobalIdx is not None:
513
+ e = tl.minimum(tl.load(GlobalIdx + in_row, mask=row_mask, other=0), last_global)
514
+ x = x / tl.load(GlobalScale + e * stride_global).to(tl.float32)[:, None]
515
+ else:
516
+ x = x / tl.load(GlobalScale + expert_id * stride_global).to(tl.float32)
517
+ y, s = mx_act_quant_inline(x, BLOCK_T, BLOCK_K, SCALE_GROUP_K, FORMAT)
518
+ width: tl.constexpr = BLOCK_K // 2 if FORMAT != "mxfp8" else BLOCK_K
519
+ y_row: tl.constexpr = K // (BLOCK_K // width) # per-row element count of Y
520
+ yo = kb * width + tl.arange(0, width)
521
+ # values -> source row order (the swizzled grid scatters via the gathered in_row)
522
+ if PDL:
523
+ gdc_launch_dependents()
524
+ tl.store(Y + in_row[:, None] * y_row + yo[None, :], y, mask=row_mask[:, None])
525
+ if SWIZZLED:
526
+ store_mx_act_scales(
527
+ S, SOut, so, s, row_mask, kb, T, K, SCALE_GROUP_K, BLOCK_K, True,
528
+ "none" if GROUPED else "blocks",
529
+ )
530
+ else:
531
+ store_mx_act_scales(
532
+ S, SOut, in_row, s, row_mask, kb, T, K, SCALE_GROUP_K, BLOCK_K, False, "rows"
533
+ )
534
+
535
+
536
+ def mx_act_quant_grouped(
537
+ x: torch.Tensor,
538
+ activation_format: str,
539
+ scale_group: int,
540
+ scale_dtype: torch.dtype,
541
+ gather_idx: torch.Tensor | None,
542
+ expert_start: torch.Tensor,
543
+ global_scale: torch.Tensor | None = None,
544
+ swizzled: bool = True,
545
+ ) -> tuple[torch.Tensor, torch.Tensor, int]:
546
+ """Offline MX act-quant on the GROUPED grid of ``_mx_act_quant_kernel``: one program per
547
+ expert-sorted tile, so each tile knows its own expert and a per-expert NVFP4 global needs no
548
+ per-row index. Returns ``(values, scales, num_m_tiles)`` — the values in source order (E4M3,
549
+ or packed E2M1 uint8 for the fp4 formats) and the scales either as the ``(1, num_m_tiles, cb,
550
+ 2, 256)`` SWIZZLE_32_4_4 tensor the GEMM reads through a descriptor (``swizzled``) or
551
+ row-major ``(T, K // scale_group)`` for the affine arm. Only the ``expert_start[-1]``
552
+ scheduled rows are laid out; expert padding is per 128 (the ``BLOCK_T`` pin).
553
+
554
+ ``n_m_tiles`` is a STATIC host-side upper bound (``S//128 + E``) on the padded tile count —
555
+ never ``.item()`` (a CPU-GPU sync / cudagraph break). The buffer/grid over-allocate to it; the
556
+ kernel writes only the real tiles (``build_tile_layout`` in-kernel), extra tile-programs mask
557
+ out, and the GEMM reads only ``pid_m < total_m_tiles``. ``sum(ceil(freq/128)) <= S//128 + E``."""
558
+ T, K = x.shape
559
+ E = expert_start.numel() - 1
560
+ S = gather_idx.numel() if gather_idx is not None else T
561
+ n_m_tiles = S // 128 + E
562
+ packed = activation_format != "mxfp8"
563
+ y = torch.empty(
564
+ T, K // 2 if packed else K, device=x.device, dtype=torch.uint8 if packed else FP8_DTYPE
565
+ )
566
+ if swizzled:
567
+ cb = triton.cdiv(K // scale_group, 4)
568
+ scales = torch.empty(1, n_m_tiles, cb, 2, 256, device=x.device, dtype=scale_dtype)
569
+ else:
570
+ scales = torch.empty(T, K // scale_group, device=x.device, dtype=scale_dtype)
571
+ with device_context(x.device):
572
+ compile_time_only_triton_wrap(_mx_act_quant_kernel)[
573
+ lambda META: (n_m_tiles, K // META["BLOCK_K"])
574
+ ](
575
+ x,
576
+ y,
577
+ None if swizzled else scales, # S: row-major scales, affine arm only
578
+ scales if swizzled else None, # SOut: SWIZZLE_32_4_4 buffer, swizzled arm only
579
+ gather_idx, # None = no gather (the is-not-None guard folds the load out)
580
+ expert_start,
581
+ global_scale, # fp32 NVFP4 two-level global; None ⇒ single-level (arm folds out)
582
+ None, # GlobalIdx: the grouped tiles carry their own expert
583
+ global_scale_stride(global_scale),
584
+ 0, # last_global: unread without a row map
585
+ x.stride(0),
586
+ x.stride(1),
587
+ T,
588
+ T.bit_length(),
589
+ K=K,
590
+ SCALE_GROUP_K=scale_group,
591
+ FORMAT=activation_format,
592
+ SWIZZLED=swizzled,
593
+ GROUPED=True,
594
+ NUM_EXPERTS_POW2=E,
595
+ PDL=decode_pdl(),
596
+ launch_pdl=decode_pdl(),
597
+ )
598
+ return y, scales, n_m_tiles
599
+
600
+
601
+ @triton.jit
602
+ def _swizzle_grouped_scales_kernel(
603
+ SRC, # (rows, cols) row-major pre-quantized group scales (uint8 / e8m0 / e4m3, 1 byte)
604
+ DST, # flat SWIZZLE_32_4_4 output buffer (1, n_tiles, NCB, 2, 256)
605
+ GatherIdx, # (S,) int sorted position -> source row of SRC; read only when not None
606
+ ExpertStart, # (NUM_EXPERTS_POW2 + 1,) cumulative sorted-row starts, S sentinel
607
+ COLS,
608
+ NCB, # number of 4-wide column blocks (cols // 4)
609
+ stride_src_m,
610
+ NUM_EXPERTS_POW2: tl.constexpr,
611
+ ):
612
+ """Gather + expert-sorted 128-pad + swizzle a PRE-QUANTIZED grouped scale into the
613
+ SWIZZLE_32_4_4 layout, in one launch — the padded tile layout is derived in-kernel from
614
+ ``ExpertStart`` (``build_tile_layout``), so no torch index build. One 128x4 block per
615
+ (M-tile, col-block): inverse of ``load_swizzled_scale``. Padding rows write quantized-zero
616
+ scales (the GEMM masks those rows)."""
617
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
618
+ ExpertStart, NUM_EXPERTS_POW2, 128
619
+ )
620
+ pid_m = tl.program_id(0)
621
+ cb = tl.program_id(1)
622
+ _, sorted_idx, row_mask = resolve_tile_inline(
623
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, 128
624
+ )
625
+ if GatherIdx is not None:
626
+ src = tl.load(GatherIdx + sorted_idx, mask=row_mask, other=0)
627
+ else:
628
+ src = sorted_idx
629
+ cj = cb * 4 + tl.arange(0, 4)
630
+ s = tl.load(
631
+ SRC + src[:, None] * stride_src_m + cj[None, :],
632
+ mask=row_mask[:, None] & (cj[None, :] < COLS),
633
+ other=0,
634
+ )
635
+ swizzle_store_block(DST, s, pid_m, cb, NCB)
636
+
637
+
638
+ def swizzle_grouped_mx_scales(
639
+ scale: torch.Tensor,
640
+ expert_start: torch.Tensor,
641
+ gather_idx: torch.Tensor | None = None,
642
+ pad_bm: int = 128,
643
+ ) -> tuple[torch.Tensor, int]:
644
+ """Fused gather + expert-sorted 128-pad + swizzle of a pre-quantized grouped scale (a
645
+ down projection's externally supplied ``As``), one triton launch — the layout comes from
646
+ ``expert_start`` in-kernel (no torch index build). Returns ``(swizzled_scale, num_m_tiles)``
647
+ as the ``(1, num_m_tiles, cols // 4, 2, 256)`` tensor; the caller builds the GEMM's scale
648
+ descriptor from it. ``gather_idx`` None = activations already expert-sorted.
649
+
650
+ ``n_m_tiles`` is a STATIC host-side upper bound (``S//pad_bm + E``) — never ``.item()`` (a
651
+ CPU-GPU sync / cudagraph break); the buffer/grid over-allocate and the extra tile-programs
652
+ mask out (``sum(ceil(freq/pad_bm)) <= S//pad_bm + E``)."""
653
+ E = expert_start.numel() - 1
654
+ S = gather_idx.numel() if gather_idx is not None else scale.shape[0]
655
+ n_m_tiles = S // pad_bm + E
656
+ cols = scale.shape[1]
657
+ cb = triton.cdiv(cols, 4)
658
+ src = scale.view(torch.uint8) # byte-level; the binder rejects e8m0/e4m3
659
+ out = torch.empty(1, n_m_tiles, cb, 2, 256, device=scale.device, dtype=torch.uint8)
660
+ with device_context(scale.device):
661
+ compile_time_only_triton_wrap(_swizzle_grouped_scales_kernel)[(n_m_tiles, cb)](
662
+ src,
663
+ out, # flat SWIZZLE_32_4_4 buffer (pointer store; no descriptor)
664
+ gather_idx, # None = no gather (the is-not-None guard folds the load out)
665
+ expert_start,
666
+ cols,
667
+ cb,
668
+ src.stride(0),
669
+ NUM_EXPERTS_POW2=E,
670
+ )
671
+ return out.view(scale.dtype), n_m_tiles
672
+
673
+
674
+ def maybe_act_quant(x, act_quant, min_m):
675
+ """Row-count-gated offline activation pre-quant. Apply ``act_quant`` (a one-pass
676
+ quant kernel, e.g. ``mxfp8_act_quant``) when the GEMM consuming ``x`` is
677
+ compute-bound (``rows >= min_m``) — the inline form re-quantizes per N-tile there.
678
+ ``min_m`` is the consumer kernel's crossover, defined next to its wrapper with its
679
+ provenance (measured sweep or inherited estimate). Below the threshold return ``x``
680
+ raw: the weight-bandwidth-bound GEMM quantizes its
681
+ one thin tile inline for free (the UE8M0 inline quant is exponent-only), and a
682
+ separate quant kernel is pure added latency (M=1 graph attn decode measured
683
+ 0.66-0.85x offline). Bit-exact either way. Returns ``(a, a_scale)``; the consumer
684
+ kernel picks its arm off ``a``'s dtype at compile time (fp8 = pre-quantized, raw
685
+ bf16/fp16 = quantize inline), so in the inline arm ``a_scale`` is a constexpr-dead
686
+ placeholder."""
687
+ if x.shape[0] >= min_m:
688
+ return act_quant(x)
689
+ return x, x
690
+
691
+
692
+ def mxfp8_act_quant(x: torch.Tensor, swizzled: bool = False) -> tuple[torch.Tensor, torch.Tensor]:
693
+ """Quantize ``(T, K)`` activations to MX once (E4M3 values + UE8M0 group-32 uint8 scales)
694
+ instead of inline per weight-tile — the fused gate_up re-ran the inline quant per N-tile
695
+ (16x redundant amax/convert ALU + 2x act bytes), which held it at ~380 TFLOPS while the
696
+ pre-quantized down kernel ran at ~1080. One pass costs ~50µs at 8k tokens. Bit-exact with
697
+ the inline form (same group boundaries). ``swizzled=True`` emits the scale directly in
698
+ SWIZZLE_32_4_4 for the tcgen05 fast path (same dense grid, per-element store)."""
699
+ return _launch_act_quant(x, "mxfp8", MX_SCALE_GROUP_K, torch.uint8, swizzled)
700
+
701
+
702
+ def mxfp4_act_quant(x: torch.Tensor, swizzled: bool = False) -> tuple[torch.Tensor, torch.Tensor]:
703
+ """Quantize ``(T, K)`` activations to MXFP4 in one kernel pass: packed-E2M1 values
704
+ (``(T, K//2)`` int8, two codes per byte, first value in the low nibble) + UE8M0
705
+ group-32 uint8 scales (``(T, K//32)``, amax/6 ceil'd to a power of two). Bit-identical
706
+ to the fused epilogues' inline form (shared ``mx_act_quant_inline`` arm). Feeds the
707
+ W4A4 arm of the MX matmul ops; quantizing activations to fp4 at runtime is an
708
+ accuracy call the caller owns — the ops never do it implicitly. ``swizzled=True`` emits the
709
+ scale directly in SWIZZLE_32_4_4 for the tcgen05 fast path."""
710
+ return _launch_act_quant(x, "mxfp4", MX_SCALE_GROUP_K, torch.uint8, swizzled)
711
+
712
+
713
+ def nvfp4_act_quant(
714
+ x: torch.Tensor,
715
+ swizzled: bool = False,
716
+ global_scale: torch.Tensor | None = None,
717
+ expert_index: torch.Tensor | None = None,
718
+ ) -> tuple[torch.Tensor, torch.Tensor]:
719
+ """Quantize ``(T, K)`` activations to NVFP4 in one kernel pass: packed-E2M1 values
720
+ (``(T, K//2)`` int8) + E4M3 group-16 block scales (``(T, K//16)`` — ``amax/6`` rounded to
721
+ E4M3, NOT a power of two; values divide by the DECODED scale before the E2M1 grid,
722
+ the standard two-step). ``global_scale`` is the CALIBRATED per-tensor second level
723
+ (fp32, the checkpoint's ``input_scale``): values are normalized by it before
724
+ the block quant, so block scales stay in e4m3 range regardless of the activation's
725
+ dynamic range — the canonical two-level format. The GEMM folds ``g_a·g_b`` back onto
726
+ the accumulator (pass ``As=scales`` with ``a_global_scale=global_scale``). Per-expert globals
727
+ need each row's expert, which ``expert_index`` gives where the caller already holds that map
728
+ (the batched op's ``expert_ids``, sentinels and all) — it is read only when the globals are
729
+ per expert, and ``mx_act_quant_grouped`` needs none at all, its tiles carrying an expert.
730
+ ``None`` = single-level (``g_a = 1``). ``swizzled=True`` emits the scale directly in
731
+ SWIZZLE_32_4_4 for the tcgen05 fast path."""
732
+ return _launch_act_quant(
733
+ x, "nvfp4", NVFP4_SCALE_GROUP_K, torch.float8_e4m3fn, swizzled, global_scale, expert_index
734
+ )
735
+
736
+
737
+ def nvfp4_quantize_two_level(
738
+ weight: torch.Tensor, swizzled: bool = False
739
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
740
+ """Canonical two-level NVFP4 quant of a ``(N, K)`` (or ``(K,)``-last) tensor. Returns
741
+ ``(packed_e2m1 int8, e4m3 group-16 block scales, fp32 per-tensor global)`` — the block scale
742
+ is the op's ``Bs`` and the global its ``b_global_scale`` (the decoupled API keeps the two levels
743
+ as separate arguments).
744
+
745
+ The second level is a per-tensor fp32 global = ``amax / (6 · 448)`` — the smallest global that
746
+ keeps every e4m3 block scale in range (block ``amax/6`` after dividing by the global stays
747
+ ``≤ 448``). Two-level quant IS single-level quant of the globally-normalized tensor, so the block
748
+ values + scales come straight from ``nvfp4_act_quant(weight / global)``. The kernels multiply
749
+ the folded ``g_a · g_b`` onto the accumulator; the e4m3 block scales ride ``dot_scaled`` as
750
+ usual (activations are single-level ⇒ ``g_a = 1``)."""
751
+ global_scale = (weight.abs().amax() / (6.0 * 448.0)).clamp(min=1e-30).float()
752
+ # normalize in fp32: bf16 / 0-dim-fp32 stays bf16 under type promotion, and the extra
753
+ # rounding before the block quant flips E2M1 codes vs the exact normalized tensor
754
+ packed, block = nvfp4_act_quant((weight.float() / global_scale).contiguous(), swizzled)
755
+ return packed.view(torch.int8), block, global_scale.reshape(1)
756
+
757
+
758
+ # offline act-quant pass per activation format (keys = ``resolve_activation_format`` results)
759
+ MX_ACT_QUANT = {
760
+ "mxfp8": mxfp8_act_quant,
761
+ "mxfp4": mxfp4_act_quant,
762
+ "nvfp4": nvfp4_act_quant,
763
+ }
764
+
765
+
766
+ def _launch_act_quant(
767
+ x, activation_format, scale_group, scale_dtype, swizzled=False, global_scale=None,
768
+ expert_index=None,
769
+ ):
770
+ """One-pass activation quant for every format (``mxfp8`` = E4M3 values, else packed E2M1) and
771
+ both scale layouts. ``swizzled=True`` writes the scale straight into the SWIZZLE_32_4_4 buffer
772
+ ``(1, cdiv(T, 128), cb, 2, 256)`` (per-element ptr store, dense autotuned ``BLOCK_T`` — same grid
773
+ as the affine path, just the store address flips); ``swizzled=False`` writes row-major
774
+ ``(T, K // scale_group)``. ``global_scale`` (NVFP4 two-level, ``(1,)`` fp32) normalizes the
775
+ values before the block quant. Returns ``(values, scales)``."""
776
+ T, K = x.shape
777
+ packed = activation_format != "mxfp8"
778
+ per_expert = is_per_expert_global(global_scale)
779
+ if packed:
780
+ assert K % (2 * scale_group) == 0, (
781
+ f"K (={K}) must be a multiple of {2 * scale_group} to pack E2M1 pairs"
782
+ )
783
+ values = torch.empty(
784
+ T, K // 2 if packed else K, device=x.device,
785
+ dtype=torch.uint8 if packed else FP8_DTYPE,
786
+ )
787
+ if swizzled:
788
+ cb = triton.cdiv(K // scale_group, 4)
789
+ scales = torch.empty(
790
+ 1, triton.cdiv(T, 128), cb, 2, 256, device=x.device, dtype=scale_dtype
791
+ )
792
+ else:
793
+ scales = torch.empty(T, K // scale_group, device=x.device, dtype=scale_dtype)
794
+ with device_context(x.device):
795
+ compile_time_only_triton_wrap(_mx_act_quant_kernel)[
796
+ lambda META: (triton.cdiv(T, META["BLOCK_T"]), K // META["BLOCK_K"])
797
+ ](
798
+ x,
799
+ values,
800
+ None if swizzled else scales, # S: row-major scales, plain arm only
801
+ scales if swizzled else None, # SOut: SWIZZLE_32_4_4 buffer, swizzled arm only
802
+ None, # GatherIdx: the dense grid reads rows directly
803
+ None, # ExpertStart: no expert-sorted tiles on the dense grid
804
+ global_scale, # fp32 NVFP4 two-level global; None ⇒ single-level (arm folds out)
805
+ expert_index if per_expert else None, # row -> expert map, read iff the globals are per expert
806
+ global_scale_stride(global_scale),
807
+ (global_scale.numel() - 1) if per_expert else 0,
808
+ x.stride(0),
809
+ x.stride(1),
810
+ T,
811
+ T.bit_length(),
812
+ K=K,
813
+ SCALE_GROUP_K=scale_group,
814
+ FORMAT=activation_format,
815
+ SWIZZLED=swizzled,
816
+ GROUPED=False,
817
+ # explicit even though the dense grid never reads it: inductor's wrap_triton path
818
+ # drops constexprs left to their default, and the arity mismatch that produces kills
819
+ # the whole torch.compile launch ("launcher() missing 1 required positional argument")
820
+ NUM_EXPERTS_POW2=1,
821
+ PDL=decode_pdl(),
822
+ launch_pdl=decode_pdl(),
823
+ )
824
+ return (values.view(torch.int8) if packed else values), scales
825
+
826
+
827
+ @bayesian_autotune(
828
+ [
829
+ triton.Config({"BLOCK_T": bt}, num_warps=w)
830
+ for bt in (16, 32, 64, 128)
831
+ for w in (1, 2, 4, 8)
832
+ ],
833
+ ["K", "BLOCK_K", "t_bucket"],
834
+ n_trials=100,
835
+ )
836
+ @triton.jit
837
+ def _fp8_act_quant_block_dynamic_kernel(
838
+ X,
839
+ Y,
840
+ S,
841
+ stride_x_t,
842
+ stride_x_k,
843
+ T,
844
+ t_bucket, # autotune key only (log2 token-count bucket); unused in body
845
+ K: tl.constexpr,
846
+ BLOCK_K: tl.constexpr,
847
+ BLOCK_T: tl.constexpr,
848
+ UE8M0: tl.constexpr = False,
849
+ PDL: tl.constexpr = False,
850
+ ):
851
+ """One-pass block-FP8 activation quant: rows → E4M3 + one ``amax/448`` scale per
852
+ ``BLOCK_K`` span (fp32, or a UE8M0 exponent byte under ``UE8M0``). Grid
853
+ ``(cdiv(T, BLOCK_T), K // BLOCK_K)`` — each program quantizes a ``[BLOCK_T, BLOCK_K]``
854
+ tile (``BLOCK_T`` rows over one K-block, each row its own scale). The one-row-per-program
855
+ form starved memory (128-byte transactions across ~T*K/BK tiny programs); the row tile
856
+ gives the loads something to coalesce. The span equals the consumer's ``BLOCK_SIZE_K``,
857
+ so results are bit-exact with the inline quant. Arbitrary input strides (no host-side
858
+ copy); ``BLOCK_K`` is fixed by the scale layout, ``BLOCK_T`` and warps are tuned."""
859
+ if PDL:
860
+ gdc_wait()
861
+ pid_t = tl.program_id(0)
862
+ kb = tl.program_id(1)
863
+ rows = (pid_t * BLOCK_T + tl.arange(0, BLOCK_T)).to(tl.int64)
864
+ offs = kb * BLOCK_K + tl.arange(0, BLOCK_K)
865
+ row_mask = rows < T
866
+ x = tl.load(
867
+ X + rows[:, None] * stride_x_t + offs[None, :] * stride_x_k,
868
+ mask=row_mask[:, None],
869
+ other=0.0,
870
+ ).to(tl.float32)
871
+ y, s = fp8_act_quant_inline(x, UE8M0=UE8M0)
872
+ tl.store(Y + rows[:, None] * K + offs[None, :], y, mask=row_mask[:, None])
873
+ if PDL:
874
+ gdc_launch_dependents()
875
+ tl.store(S + rows * (K // BLOCK_K) + kb, s, mask=row_mask)
876
+
877
+
878
+ def fp8_act_quant_block_dynamic(
879
+ x: torch.Tensor, block_k: int, use_ue8m0: bool = False
880
+ ) -> tuple[torch.Tensor, torch.Tensor]:
881
+ """Quantize ``(T, K)`` activations to block-FP8 once (E4M3 + per-``block_k`` scales)
882
+ instead of inline per weight-tile — same rationale and layout as ``mxfp8_act_quant`` (a
883
+ GEMM re-reads its activation once per N-tile). Bit-exact with the inline form. Scales are
884
+ fp32 (``amax/448``) or, under ``use_ue8m0``, UE8M0 exponent bytes (power-of-two scales)
885
+ for the tcgen05 ``dot_scaled`` path — the DeepGEMM-Blackwell format."""
886
+ T, K = x.shape
887
+ y = torch.empty(T, K, device=x.device, dtype=FP8_DTYPE)
888
+ s_dtype = torch.uint8 if use_ue8m0 else torch.float32
889
+ s = torch.empty(T, K // block_k, device=x.device, dtype=s_dtype)
890
+
891
+ def grid(META):
892
+ return (triton.cdiv(T, META["BLOCK_T"]), K // block_k)
893
+
894
+ with device_context(x.device):
895
+ compile_time_only_triton_wrap(_fp8_act_quant_block_dynamic_kernel)[grid](
896
+ x, y, s, x.stride(0), x.stride(1), T, T.bit_length(),
897
+ K=K, BLOCK_K=block_k, UE8M0=use_ue8m0,
898
+ PDL=decode_pdl(),
899
+ launch_pdl=decode_pdl(),
900
+ )
901
+ return y, s
902
+
903
+
904
+ # ── fp8_act_quant_tensor_wide kernel (used by tensor-mode FP8 wrappers) ───────────────────
905
+
906
+
907
+ @triton.jit
908
+ def _fp8_act_quant_kernel(
909
+ x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr, PADDED_BLOCK: tl.constexpr,
910
+ PDL: tl.constexpr = False,
911
+ ):
912
+ if PDL:
913
+ gdc_wait()
914
+ # ``tl.arange`` needs a power-of-2 length, so iterate over PADDED_BLOCK (the next
915
+ # power of 2) and mask the tail — lets block_size be non-power-of-2 (e.g. a full
916
+ # row K=14336 in tensor-mode). Masked lanes load 0, which can't affect ``amax``.
917
+ pid = tl.program_id(axis=0)
918
+ cols = tl.arange(0, PADDED_BLOCK)
919
+ mask = cols < BLOCK_SIZE
920
+ offs = pid * BLOCK_SIZE + cols
921
+ x = tl.load(x_ptr + offs, mask=mask, other=0.0).to(tl.float32)
922
+ s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
923
+ y = (x / tl.maximum(s, 1e-12)).to(y_ptr.dtype.element_ty)
924
+ tl.store(y_ptr + offs, y, mask=mask)
925
+ if PDL:
926
+ gdc_launch_dependents()
927
+ tl.store(s_ptr + pid, s)
928
+
929
+
930
+ @compile_time_only_triton_op(
931
+ add_op_namespace_prefix("fp8_act_quant_tensor_wide"), mutates_args=(), opaque=True
932
+ )
933
+ def fp8_act_quant_tensor_wide(
934
+ x: torch.Tensor, block_size: int = 128
935
+ ) -> tuple[torch.Tensor, torch.Tensor]:
936
+ assert x.is_contiguous()
937
+ assert x.shape[-1] % block_size == 0
938
+ y = torch.empty_like(x, dtype=FP8_DTYPE)
939
+ grid = (triton.cdiv(x.numel(), block_size),)
940
+ s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
941
+
942
+ with device_context(x.device):
943
+ compile_time_only_triton_wrap(_fp8_act_quant_kernel)[grid](
944
+ x,
945
+ y,
946
+ s,
947
+ BLOCK_SIZE=block_size,
948
+ PADDED_BLOCK=triton.next_power_of_2(block_size),
949
+ PDL=decode_pdl(),
950
+ launch_pdl=decode_pdl(),
951
+ )
952
+
953
+ return y, s
954
+
955
+
build/torch-rocm/scheduling.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+
17
+ import torch
18
+ import triton
19
+ import triton.language as tl
20
+
21
+ from .compat import add_op_namespace_prefix, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, is_sm10x
22
+
23
+
24
+ @triton.jit
25
+ def build_tile_layout(
26
+ ExpertStart, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE_M: tl.constexpr
27
+ ):
28
+ """Load ``expert_start`` once and derive the per-BM tile layout vectors (kept in
29
+ registers for the whole persistent loop): per-expert first sorted row, token count,
30
+ exclusive tile-start cumsum, and the total M-tile count. ``ExpertStart`` is
31
+ ``(NUM_EXPERTS + 1,)`` with a trailing ``S`` sentinel (``expert_start[E] == S``)."""
32
+ e_offs = tl.arange(0, NUM_EXPERTS)
33
+ exp_start = tl.load(ExpertStart + e_offs)
34
+ exp_end = tl.load(ExpertStart + e_offs + 1)
35
+ freqs = exp_end - exp_start
36
+ tiles_per_e = (freqs + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
37
+ tile_start_excl = (
38
+ tl.cumsum(tiles_per_e, 0) - tiles_per_e
39
+ ) # first tile index of expert e
40
+ total_m_tiles = tl.sum(tiles_per_e, 0)
41
+ return exp_start, freqs, tile_start_excl, total_m_tiles, e_offs
42
+
43
+
44
+ @triton.jit
45
+ def resolve_tile_inline(
46
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M: tl.constexpr
47
+ ):
48
+ """Map an M-tile id to its owning expert + the tile's sorted row range, from the
49
+ register-resident layout (no global loads). Returns ``(expert_id, sorted_indices,
50
+ row_mask)``."""
51
+ # Bucketize via the exclusive tile cumsum: #experts whose tile-start <= pid_m, minus 1.
52
+ expert_id = tl.sum((tile_start_excl <= pid_m).to(tl.int32), 0) - 1
53
+ sel = (
54
+ e_offs == expert_id
55
+ ) # scalar-index the E-vectors via mask-sum (no dynamic index)
56
+ e_start = tl.sum(tl.where(sel, exp_start, 0), 0)
57
+ e_tile_start = tl.sum(tl.where(sel, tile_start_excl, 0), 0)
58
+ freq = tl.sum(tl.where(sel, freqs, 0), 0)
59
+ within = pid_m - e_tile_start
60
+ m_start = e_start + within * BLOCK_SIZE_M
61
+ offs = tl.arange(0, BLOCK_SIZE_M)
62
+ row_mask = offs < freq - within * BLOCK_SIZE_M
63
+ sorted_indices = tl.max_contiguous(m_start + offs, BLOCK_SIZE_M)
64
+ return expert_id, sorted_indices, row_mask
65
+
66
+ # Flat-slot tile per program for the O(S) routing kernels (count + scatter). These are small
67
+ # latency-bound atomic kernels that want many programs: a sweep over {256..4096} x prefill shapes
68
+ # put 256 best (or within ~1%) for both, with 1024 up to ~1.5x slower. The grid derives from it
69
+ # so the two can't drift. Power of 2.
70
+ _ROUTING_BLOCK_SIZE = 256
71
+
72
+
73
+ @triton.jit
74
+ def _exclusive_offsets_kernel(
75
+ ExpertFreq, ExpertStart, Counters, NUM_EXPERTS: tl.constexpr
76
+ ):
77
+ """Exclusive cumsum of per-expert token counts → ``expert_start`` (leading 0, trailing
78
+ S), and zero the scatter counters — one launch."""
79
+ offs = tl.arange(0, NUM_EXPERTS)
80
+ incl = tl.cumsum(tl.load(ExpertFreq + offs), 0)
81
+ tl.store(ExpertStart, 0)
82
+ tl.store(ExpertStart + 1 + offs, incl)
83
+ tl.store(Counters + offs, tl.zeros([NUM_EXPERTS], tl.int32))
84
+
85
+
86
+ @triton.jit
87
+ def _scatter_kernel(
88
+ ExpertIds,
89
+ Perm,
90
+ PermToken,
91
+ ExpertStart,
92
+ Counters,
93
+ S,
94
+ NUM_TOP_K: tl.constexpr,
95
+ NUM_EXPERTS: tl.constexpr,
96
+ BLOCK_SIZE: tl.constexpr,
97
+ ):
98
+ """Counting-sort scatter: each flat slot atomically claims the next slot of its expert
99
+ (``expert_start[e] + counter[e]++``). O(S), replaces an O(S·logS) argsort. Within-expert
100
+ order is arbitrary (atomic race) — fine, the per-token reduce is order-invariant. Slots whose
101
+ expert is non-local (EP sentinel id ``>= NUM_EXPERTS``) are skipped — matches ``_count_kernel``,
102
+ and avoids the atomic/store landing at an out-of-range (invalid) global address."""
103
+ offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
104
+ expert_id = tl.load(ExpertIds + offs, mask=offs < S, other=NUM_EXPERTS)
105
+ valid = expert_id < NUM_EXPERTS
106
+ dest = tl.load(ExpertStart + expert_id, mask=valid, other=0) + tl.atomic_add(
107
+ Counters + expert_id, 1, mask=valid, sem="relaxed"
108
+ )
109
+ tl.store(Perm + dest, offs, mask=valid)
110
+ tl.store(PermToken + dest, offs // NUM_TOP_K, mask=valid)
111
+
112
+
113
+ @triton.jit
114
+ def _count_kernel(
115
+ ExpertIds, ExpertFreq, S, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE: tl.constexpr
116
+ ):
117
+ """Per-expert token count via atomics — replaces ``torch.histc`` (no float cast), fixed
118
+ ``(NUM_EXPERTS,)`` output stays CUDA-graph friendly. ``ExpertFreq`` is pre-zeroed."""
119
+ offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
120
+ mask = offs < S
121
+ expert_id = tl.load(ExpertIds + offs, mask=mask, other=NUM_EXPERTS)
122
+ tl.atomic_add(
123
+ ExpertFreq + expert_id, 1, mask=mask & (expert_id < NUM_EXPERTS), sem="relaxed"
124
+ )
125
+
126
+
127
+ def compute_grouped_scheduling(
128
+ expert_ids: torch.Tensor, num_experts: int, num_top_k: int
129
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
130
+ """On-device routing: expert-sorted index (no copy of the activations) via two Triton
131
+ launches — exclusive offsets + an atomic counting-sort scatter (replaces host ``argsort``).
132
+ Run it once per layer and pass the results to every grouped GEMM of that layer. Returns
133
+ ``(expert_start, gather_idx, scatter_idx)``:
134
+
135
+ - ``expert_start`` — ``(E+1,)`` cumulative sorted-row starts padded with S; the tiling
136
+ schedule the kernels build their register-resident tile layout from.
137
+ - ``gather_idx`` — each sorted position's source row of hidden (``perm // num_top_k``,
138
+ many-to-one for top_k > 1: the gather that reads hidden without replication). Pass as the
139
+ GEMM's input map (``None`` = ``A`` already expert-sorted, e.g. the down projection).
140
+ - ``scatter_idx`` — each sorted position's token-major routed destination row ``(t*K + j)``,
141
+ the ``perm = torch.sort(expert_ids)`` indices (kernels un-permute by SCATTERING at store
142
+ time, never materializing ``inv_perm``). Pass as the output map (``None`` = leave the output
143
+ expert-sorted, e.g. the gate_up projection's intermediate).
144
+
145
+ E must be a power of 2 (the scheduling kernels hold the per-expert vectors in one
146
+ ``tl.arange`` block)."""
147
+ # the scheduling kernels hold the (E,) frequency/offset vectors in one tl.arange
148
+ # block, which requires a power of 2 — fail here with a clear message instead of a
149
+ # Triton compile error from an internal kernel
150
+ assert num_experts & (num_experts - 1) == 0, (
151
+ f"num_experts ({num_experts}) must be a power of 2"
152
+ )
153
+ gather_idx, scatter_idx, expert_start = _compute_grouped_scheduling(
154
+ expert_ids, num_experts, num_top_k
155
+ )
156
+ return expert_start, gather_idx, scatter_idx
157
+
158
+
159
+ @compile_time_only_triton_op(
160
+ add_op_namespace_prefix("compute_grouped_scheduling"), mutates_args=(), opaque=True
161
+ )
162
+ def _compute_grouped_scheduling(
163
+ expert_ids: torch.Tensor, num_experts: int, num_top_k: int
164
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
165
+ device = expert_ids.device
166
+ expert_ids = expert_ids.int().contiguous() # routing kernels index with unit stride
167
+ num_routed_tokens = expert_ids.numel() # S = num_tokens * num_top_k
168
+ expert_freq = torch.zeros(num_experts, dtype=torch.int32, device=device)
169
+ expert_start = torch.empty(num_experts + 1, dtype=torch.int32, device=device)
170
+ counters = torch.empty(num_experts, dtype=torch.int32, device=device)
171
+ perm = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
172
+ perm_token = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
173
+ with device_context(device):
174
+ compile_time_only_triton_wrap(_count_kernel)[
175
+ (triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)
176
+ ](
177
+ expert_ids,
178
+ expert_freq,
179
+ num_routed_tokens,
180
+ NUM_EXPERTS=num_experts,
181
+ BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
182
+ )
183
+ compile_time_only_triton_wrap(_exclusive_offsets_kernel)[(1,)](
184
+ expert_freq,
185
+ expert_start,
186
+ counters,
187
+ NUM_EXPERTS=num_experts,
188
+ )
189
+ compile_time_only_triton_wrap(_scatter_kernel)[
190
+ (triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)
191
+ ](
192
+ expert_ids,
193
+ perm,
194
+ perm_token,
195
+ expert_start,
196
+ counters,
197
+ num_routed_tokens,
198
+ NUM_TOP_K=num_top_k,
199
+ NUM_EXPERTS=num_experts,
200
+ BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
201
+ )
202
+ return perm_token, perm, expert_start
203
+
204
+
205
+ # The packed schedule exists for ONE tile size: BM=128, the only region the resolve trade
206
+ # wins (packed_schedule_scope_pruner) — so the table is a single section: header slot 0 =
207
+ # total_m_tiles, then (expert_id, m_start, row_end) per tile.
208
+ PACKED_SCHEDULE_BM = 128
209
+
210
+
211
+ def packed_schedule_capacity(num_routed_tokens: int, num_experts: int) -> int:
212
+ """Worst-case M-tile count: every expert pads its own last tile, so
213
+ ``cdiv(S, BM) + E`` bounds ``sum_e(cdiv(freq_e, BM))``. Fixed shape — graph-capturable."""
214
+ return (num_routed_tokens + PACKED_SCHEDULE_BM - 1) // PACKED_SCHEDULE_BM + num_experts
215
+
216
+
217
+ def build_packed_schedule(
218
+ expert_start: torch.Tensor, num_routed_tokens: int, num_experts: int
219
+ ) -> torch.Tensor:
220
+ """Allocate + fill the packed tile schedule (one launch, fixed shape — graph-safe)."""
221
+ schedule = torch.empty(
222
+ 1 + 3 * packed_schedule_capacity(num_routed_tokens, num_experts),
223
+ dtype=torch.int32, device=expert_start.device,
224
+ )
225
+ pow2 = triton.next_power_of_2(num_experts)
226
+ with device_context(expert_start.device):
227
+ compile_time_only_triton_wrap(_packed_schedule_kernel)[(pow2,)](
228
+ expert_start, schedule, NUM_EXPERTS=pow2
229
+ )
230
+ return schedule
231
+
232
+
233
+ @triton.jit
234
+ def _packed_schedule_kernel(ExpertStart, Schedule, NUM_EXPERTS: tl.constexpr):
235
+ """Build the packed tile schedule in one launch: one program per expert writes its
236
+ tiles' ``(expert_id, m_start, row_end)`` triples (header slot 0 = total tile count,
237
+ written by expert 0 from the full cumsum). Replaces the kernels' O(E)
238
+ register-resident tile layout with O(1) loads — the packed-schedule tuner axis
239
+ (register relief; wins at the pressure-walled BM=128 configs)."""
240
+ BM: tl.constexpr = 128 # PACKED_SCHEDULE_BM
241
+ e = tl.program_id(0)
242
+ e_start = tl.load(ExpertStart + e)
243
+ e_end = tl.load(ExpertStart + e + 1)
244
+ offs = tl.arange(0, NUM_EXPERTS)
245
+ starts = tl.load(ExpertStart + offs)
246
+ ends = tl.load(ExpertStart + offs + 1)
247
+ tiles_per = (ends - starts + BM - 1) // BM
248
+ before = tl.sum(tl.where(offs < e, tiles_per, 0), 0)
249
+ if e == 0:
250
+ tl.store(Schedule, tl.sum(tiles_per, 0)) # header: total_m_tiles
251
+ n_tiles = (e_end - e_start + BM - 1) // BM
252
+ for t in range(n_tiles):
253
+ base = 1 + (before + t) * 3
254
+ tl.store(Schedule + base, e)
255
+ tl.store(Schedule + base + 1, e_start + t * BM)
256
+ tl.store(Schedule + base + 2, e_end)
257
+
258
+
259
+ @triton.jit
260
+ def load_packed_schedule(Schedule, BLOCK_SIZE_M: tl.constexpr):
261
+ """The packed counterpart of ``build_tile_layout``: read the header (total_m_tiles).
262
+ The schedule is built for BM=128 only — the sole region the packed resolve wins (see
263
+ ``packed_schedule_scope_pruner``); the static_assert is the self-fence."""
264
+ tl.static_assert(BLOCK_SIZE_M == 128, "the packed schedule is built for BM=128")
265
+ return tl.load(Schedule)
266
+
267
+
268
+ @triton.jit
269
+ def prefetch_packed_entry(Schedule, pid_m, total_m_tiles):
270
+ """Issue a future M-tile's ``(expert_id, m_start, row_end)`` loads (masked past the
271
+ schedule end). Consumed by ``resolve_grouped_tile_packed`` one persistent iteration
272
+ later, so the entry's dependent-load latency hides under the current tile's K-loop
273
+ instead of serializing into every tile (~300ns/tile, the packed arm's whole overhead)."""
274
+ m = pid_m < total_m_tiles
275
+ entry = Schedule + 1 + pid_m * 3
276
+ return (tl.load(entry, mask=m, other=0),
277
+ tl.load(entry + 1, mask=m, other=0),
278
+ tl.load(entry + 2, mask=m, other=0))
279
+
280
+
281
+ @triton.jit
282
+ def resolve_grouped_tile_packed(
283
+ tile_id,
284
+ num_n_tiles,
285
+ prefetched,
286
+ GatherIdx,
287
+ ScatterIdx,
288
+ BLOCK_SIZE_N: tl.constexpr,
289
+ BLOCK_SIZE_M: tl.constexpr,
290
+ GATE: tl.constexpr = False,
291
+ ):
292
+ """``resolve_grouped_tile`` with the M-tile resolved from the packed schedule —
293
+ consuming the triple ``prefetch_packed_entry`` issued last iteration (zero dependent
294
+ loads on this tile's critical path).
295
+ Same return contract; bit-identical tiles."""
296
+ pid_n = tile_id % num_n_tiles
297
+ expert_id, tile_row0, row_end = prefetched
298
+ offs_global_m = tl.max_contiguous(tile_row0 + tl.arange(0, BLOCK_SIZE_M), BLOCK_SIZE_M)
299
+ row_mask = offs_global_m < row_end
300
+ if GatherIdx is not None:
301
+ in_row = tl.load(GatherIdx + offs_global_m, mask=row_mask, other=0)
302
+ else:
303
+ in_row = offs_global_m
304
+ if ScatterIdx is not None:
305
+ out_row = tl.load(ScatterIdx + offs_global_m, mask=row_mask, other=0)
306
+ else:
307
+ out_row = offs_global_m
308
+ expert_id64 = expert_id.to(tl.int64)
309
+ WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
310
+ offs_bn = pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)
311
+ row0 = expert_id64.to(tl.int32)
312
+ n_off = pid_n * WEIGHT_ROWS
313
+ m_start = tl.min(in_row).to(tl.int32)
314
+ return pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off, m_start
315
+
316
+
317
+ @triton.jit
318
+ def resolve_grouped_tile(
319
+ tile_id,
320
+ num_n_tiles,
321
+ exp_start,
322
+ freqs,
323
+ tile_start_excl,
324
+ e_offs,
325
+ GatherIdx,
326
+ ScatterIdx,
327
+ BLOCK_SIZE_N: tl.constexpr,
328
+ BLOCK_SIZE_M: tl.constexpr,
329
+ GATE: tl.constexpr = False,
330
+ ):
331
+ """One persistent grouped tile: split the flat ``tile_id`` into (M-tile, N-tile), map
332
+ the M-tile to its expert + rows via ``resolve_tile_inline`` (on the register-resident
333
+ layout ``build_tile_layout`` builds once per program, passed in), and apply the virtual
334
+ sort — rows load from ``in_row`` and store to ``out_row``, mapped by ``GatherIdx`` /
335
+ ``ScatterIdx`` when present (``None`` = expert-sorted, the position itself; the ``None``
336
+ check folds at trace time, so no separate has-gather/has-scatter flag is needed).
337
+
338
+ Returns ``(pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off,
339
+ m_start)`` — both expert-id widths (``expert_id`` int32 for TMA descriptor row indices,
340
+ ``expert_id64`` int64 for byte-offset pointer arithmetic that overflows int32 at full expert
341
+ counts), plus the tile's derived load coordinates every grouped K-loop needs: ``row0`` (the
342
+ weight descriptor's expert-slab row base, ×2 under GATE for the gate|up stack), ``n_off`` (the
343
+ N-tile byte offset), and ``m_start`` (the contiguous sorted-row box start — ``min(in_row)``,
344
+ exact because a no-gather tile's rows are contiguous). Shared by the base grouped GEMMs and the
345
+ fused kernels; callers ``_``-ignore whichever fields they don't use."""
346
+ pid_m = tile_id // num_n_tiles
347
+ pid_n = tile_id % num_n_tiles
348
+ expert_id, offs_global_m, row_mask = resolve_tile_inline(
349
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
350
+ )
351
+ if GatherIdx is not None:
352
+ in_row = tl.load(GatherIdx + offs_global_m, mask=row_mask, other=0)
353
+ else:
354
+ in_row = offs_global_m
355
+ if ScatterIdx is not None:
356
+ out_row = tl.load(ScatterIdx + offs_global_m, mask=row_mask, other=0)
357
+ else:
358
+ out_row = offs_global_m
359
+ expert_id64 = expert_id.to(tl.int64)
360
+ WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
361
+ offs_bn = pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)
362
+ row0 = expert_id64.to(tl.int32)
363
+ n_off = pid_n * WEIGHT_ROWS
364
+ m_start = tl.min(in_row).to(tl.int32)
365
+ return pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off, m_start
366
+
367
+
368
+ def expand_gather_below_parity(vals, scales, gather_idx, num_experts):
369
+ """Trade an in-kernel activation gather for one contiguous copy of the (packed) rows +
370
+ scales, when the copy beats what gathering can offer: on sm_10x the gathered descriptor
371
+ arm is tma gather4, MONOTONIC in tokens/expert (2x loss at 32/expert, parity ~1024 — the
372
+ measured law in ``smem_pruner``'s gather clause), so expand below parity; every other
373
+ arch has no gather TMA and forfeits descriptor-A entirely under a gather, so any large-S
374
+ gather expands. Small S (decode) keeps the gather — nothing to amortize, pointer-A wins.
375
+ Call AFTER the activation quant: expanding packed rows costs a fraction of raw bf16 and
376
+ leaves the quant at (num_tokens, K) instead of top_k times that.
377
+ Returns ``(vals, scales, gather_idx)`` with ``gather_idx=None`` when expanded."""
378
+ if gather_idx is None:
379
+ return vals, scales, gather_idx
380
+ if expand_regime(gather_idx.shape[0], num_experts):
381
+ g = gather_idx.to(torch.long)
382
+ vals = vals[g].contiguous()
383
+ if scales is not None and scales.ndim == 2: # row-major per-row scales follow the rows
384
+ scales = scales[g].contiguous()
385
+ gather_idx = None
386
+ return vals, scales, gather_idx
387
+
388
+
389
+ def expand_regime(S_rows: int, num_experts: int) -> bool:
390
+ """Where one contiguous copy of the routed rows beats in-kernel gathering: prefill row
391
+ counts (S >= 4096 amortizes the copy) below the sm_10x tma-gather4 parity (~1024
392
+ tokens/expert — gather4's 1-row box is MONOTONIC in tokens/expert; every other arch has
393
+ no gather TMA at all, so any large-S gather expands).
394
+
395
+ Raw bf16 activations (the full-precision grouped path) were A/B'd against keeping the
396
+ in-kernel gather, 200-trial tunes each side, 2026-08-28: DeepSeek-V4 3455 -> 3344us (-3.2%),
397
+ DeepSeek-V3 7267 -> 7290 (wash), GLM-5.2 6039 -> 6439 (+6.6%). The copy (150-370us) is
398
+ offset by what the gather costs the GEMM, and which side wins is shape-specific within the
399
+ tune's roll variance — so the law is NOT split on ``act_itemsize``; it stays as measured.
400
+ ``FINEGRAINED_FORCE_GATHER=1`` is the A/B knob (never expand)."""
401
+ if os.environ.get("FINEGRAINED_FORCE_GATHER"):
402
+ return False
403
+ return S_rows >= 4096 and (not is_sm10x() or S_rows < 1024 * num_experts)
build/torch-rocm/swizzle.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ import triton
17
+ import triton.language as tl
18
+ from triton.tools.tensor_descriptor import TensorDescriptor
19
+
20
+ from .compat import compile_time_only_triton_wrap, device_context
21
+
22
+
23
+ @triton.jit
24
+ def swizzle_store_block(DST, s, blk, cb, NCB):
25
+ """Pack one row-major ``(128, 4)`` scale block ``s`` into its SWIZZLE_32_4_4 ``(32, 16)`` block
26
+ and store it at flat offset ``(blk * NCB + cb) * 512`` — the inverse of the un-swizzle in
27
+ ``load_swizzled_scale``. Shared by every scale-swizzle kernel below."""
28
+ sw = s.reshape(4, 32, 4).trans(1, 0, 2).reshape(32, 16)
29
+ r = tl.arange(0, 32)
30
+ c = tl.arange(0, 16)
31
+ tl.store(DST + (blk * NCB + cb) * 512 + r[:, None] * 16 + c[None, :], sw)
32
+
33
+
34
+ @triton.jit
35
+ def _swizzle_scales_kernel(
36
+ SRC, # (rows, cols) row-major block scales (uint8 / e8m0 / e4m3 — 1 byte)
37
+ DST, # flat SWIZZLE_32_4_4 output, (n_row_blocks * n_col_blocks * 512,)
38
+ GatherIdx, # (padded_rows,) output-row -> source row (-1 = pad); read only when not None
39
+ ROWS,
40
+ COLS,
41
+ NCB, # number of 4-wide column blocks
42
+ stride_src_m,
43
+ ):
44
+ """One 128x4 SWIZZLE_32_4_4 block per (row-block, col-block) program: gather+pad+swizzle in
45
+ a single launch (replaces the torch view/permute/transpose chain — several kernels + two
46
+ full-tensor copies). Per block: ``(128, 4) -> (4, 32, 4) -> trans(1,0,2) -> (32, 16)`` — the
47
+ exact reorder the torch packer did, so bit-identical."""
48
+ rb = tl.program_id(0)
49
+ cb = tl.program_id(1)
50
+ ri = rb * 128 + tl.arange(0, 128)
51
+ if GatherIdx is not None:
52
+ src = tl.load(GatherIdx + ri, mask=ri < ROWS, other=-1)
53
+ valid = src >= 0
54
+ src = tl.where(valid, src, 0)
55
+ else:
56
+ src = ri
57
+ valid = ri < ROWS
58
+ cj = cb * 4 + tl.arange(0, 4)
59
+ s = tl.load(
60
+ SRC + src[:, None] * stride_src_m + cj[None, :],
61
+ mask=valid[:, None] & (cj[None, :] < COLS),
62
+ other=0,
63
+ )
64
+ swizzle_store_block(DST, s, rb, cb, NCB)
65
+
66
+
67
+ def unswizzle_mx_scales(
68
+ swizzled: torch.Tensor, rows: int, cols: int, *, num_experts: int | None = None
69
+ ) -> torch.Tensor:
70
+ """Inverse of ``swizzle_mx_scales``: a SWIZZLE_32_4_4 artifact back to the plain row-major
71
+ ``(rows, cols)`` grid — or ``(num_experts, rows, cols)`` for an expert stack — with the
72
+ (128, 4) padding trimmed. Pure reshape/permute on the byte view (no kernel); it runs at
73
+ save/dequantize time, never on a forward, so the torch-level cost is irrelevant.
74
+
75
+ ``rows``/``cols`` are the LOGICAL dims of ONE matrix (for a gate|up slab, the full ``2N``);
76
+ both are trimmed back out of the (128, 4) padding. ``num_experts`` selects the stacked
77
+ ``(E, rows, cols)`` result and must match the artifact's leading dim."""
78
+ nrb, ncb = -(-rows // 128), -(-cols // 4)
79
+ n_matrices = num_experts if num_experts is not None else 1
80
+ assert swizzled.shape == (n_matrices, nrb, ncb, 2, 256), (
81
+ f"swizzled artifact is {tuple(swizzled.shape)}, expected {(n_matrices, nrb, ncb, 2, 256)} "
82
+ f"for {n_matrices}x({rows}, {cols})"
83
+ )
84
+ plain = (
85
+ swizzled.reshape(n_matrices, nrb * ncb, 32, 4, 4)
86
+ .transpose(2, 3)
87
+ .reshape(n_matrices, nrb, ncb, 128, 4)
88
+ .permute(0, 1, 3, 2, 4)
89
+ .reshape(n_matrices, nrb * 128, ncb * 4)[:, :rows, :cols]
90
+ )
91
+ return (plain if num_experts is not None else plain[0]).contiguous()
92
+
93
+
94
+ def swizzled_scale_descriptor(scale_u8: torch.Tensor) -> TensorDescriptor:
95
+ """TMA descriptor over a SWIZZLE_32_4_4 artifact ``(G, row_blocks, cols // 4, 2, 256)`` addressed
96
+ as ``(1, G * row_blocks, cols // 4, 2, 256)``: the kernels index 128-row blocks flat
97
+ (``group * row_blocks + block``), so the expert axis folds into the block axis."""
98
+ assert scale_u8.ndim == 5 and scale_u8.is_contiguous(), (
99
+ f"swizzled scale must be a contiguous 5D artifact, got {tuple(scale_u8.shape)}"
100
+ )
101
+ groups, row_blocks, cols4 = scale_u8.shape[:3]
102
+ blocks = groups * row_blocks
103
+ return TensorDescriptor(
104
+ scale_u8, [1, blocks, cols4, 2, 256], [blocks * cols4 * 512, cols4 * 512, 512, 256, 1], [1, 1, 1, 2, 256]
105
+ )
106
+
107
+
108
+ def _swizzle_to_blocks(
109
+ scale: torch.Tensor, gather_idx: torch.Tensor | None
110
+ ) -> torch.Tensor:
111
+ """One ``(rows, cols)`` scale matrix -> its ``(row_blocks, cols//4, 2, 256)``
112
+ SWIZZLE_32_4_4 block stack (single kernel launch)."""
113
+ cols = scale.shape[1]
114
+ rows = gather_idx.shape[0] if gather_idx is not None else scale.shape[0]
115
+ nrb = triton.cdiv(rows, 128)
116
+ ncb = triton.cdiv(cols, 4)
117
+ # the reorder is byte-level; view as uint8 so the triton binder accepts e8m0/e4m3 scales
118
+ src = scale.view(torch.uint8)
119
+ out = torch.empty(nrb * ncb * 512, device=scale.device, dtype=torch.uint8)
120
+ with device_context(scale.device):
121
+ compile_time_only_triton_wrap(_swizzle_scales_kernel)[(nrb, ncb)](
122
+ src,
123
+ out,
124
+ gather_idx, # None = no gather (the is-not-None guard folds the load out)
125
+ rows,
126
+ cols,
127
+ ncb,
128
+ src.stride(0),
129
+ )
130
+ return out.view(scale.dtype).reshape(nrb, ncb, 2, 256)
131
+
132
+
133
+ def swizzle_mx_scales(
134
+ scale: torch.Tensor, gather_idx: torch.Tensor | None = None
135
+ ) -> torch.Tensor:
136
+ """Reorder a block-scale tensor into the ``SWIZZLE_32_4_4`` layout the Blackwell tcgen05
137
+ scaled-MMA consumes, one triton launch per matrix (``_swizzle_scales_kernel`` — no torch
138
+ permute/transpose copies). The scale values are unchanged — this only rearranges them from
139
+ plain row-major into the swizzled order the tensor core reads directly (the same layout
140
+ cuBLAS/CUTLASS require for MXFP8/NVFP4 ``scaled_mm``). Plain row-major forces a gather that
141
+ caps the scaled dot below the fp8/fp4 peak; the swizzle removes it. Apply to weight scales
142
+ once at model load; the ops accept the result directly.
143
+
144
+ ``scale`` is the per-block scale grid — UE8M0 (``float8_e8m0fnu``, group-32 MXFP8/MXFP4) or
145
+ E4M3 (``float8_e4m3fn``, group-16 NVFP4); the reorder is dtype-agnostic (moves bytes):
146
+
147
+ - 2D ``(rows, K // group)``: one matrix (a dense weight's scale).
148
+ - 3D ``(E, rows, K // group)``: an expert stack, each expert swizzled independently so its
149
+ blocks stay ``ceil(rows/128)``-aligned (the reader indexes expert ``e`` at
150
+ ``e * ceil(rows/128)``; byte-identical to a flat swizzle when ``rows % 128 == 0``).
151
+
152
+ ``gather_idx`` (2D only): a 1-D ``(padded_rows,)`` index mapping each output (sorted) row to
153
+ its source row in ``scale`` (``-1`` = padding → zero row), folded into the kernel's load — the
154
+ routed/expert-sorted, per-tile-padded layout a grouped GEMM reads affine per BM=128 tile.
155
+ ``padded_rows`` must be a multiple of 128.
156
+
157
+ ``rows``/``cols`` are zero-padded to (128, 4) multiples; returns the 5D
158
+ ``(G, row_blocks, ceil(cols/4), 2, 256)`` artifact the ops read — ``G`` the expert count (1 for
159
+ a matrix), so an expert stack shards/gathers on its leading dim. Bit-identical to CUTLASS's
160
+ packer (verified)."""
161
+ assert gather_idx is None or gather_idx.shape[0] % 128 == 0, (
162
+ f"gather_idx rows must be 128-padded, got {None if gather_idx is None else gather_idx.shape[0]}"
163
+ )
164
+ if scale.ndim == 3:
165
+ assert gather_idx is None, "gather_idx applies to a single 2D scale, not an expert stack"
166
+ return torch.stack([_swizzle_to_blocks(e, None) for e in scale])
167
+ assert scale.ndim == 2, (
168
+ f"expected a 2D (rows, K//group) or 3D (E, rows, K//group) scale, got {tuple(scale.shape)}"
169
+ )
170
+ return _swizzle_to_blocks(scale, gather_idx).unsqueeze(0)