kernels-bot commited on
Commit
1a264fc
·
verified ·
1 Parent(s): 14efb56

Uploaded using `kernel-builder`.

Browse files
build/torch-cuda/__init__.py CHANGED
@@ -1,32 +1,20 @@
1
- from .act_quant import fp8_act_quant
2
- from .batched import (
3
- w8a8_fp8_matmul_batched,
4
- w8a8_block_fp8_matmul_batched,
5
- w8a8_tensor_fp8_matmul_batched,
6
- )
7
- from .grouped import (
8
- w8a8_fp8_matmul_grouped,
9
- w8a8_block_fp8_matmul_grouped,
10
- w8a8_tensor_fp8_matmul_grouped,
11
- )
12
- from .matmul import (
13
- w8a8_fp8_matmul,
14
- w8a8_block_fp8_matmul,
15
- w8a8_tensor_fp8_matmul,
16
- )
17
 
18
  __all__ = [
19
  "fp8_act_quant",
20
- # Single matmul
21
- "w8a8_fp8_matmul",
22
- "w8a8_block_fp8_matmul",
23
- "w8a8_tensor_fp8_matmul",
24
  # Batched matmul
25
- "w8a8_fp8_matmul_batched",
26
- "w8a8_block_fp8_matmul_batched",
27
- "w8a8_tensor_fp8_matmul_batched",
28
  # Grouped matmul
29
- "w8a8_fp8_matmul_grouped",
30
- "w8a8_block_fp8_matmul_grouped",
31
- "w8a8_tensor_fp8_matmul_grouped",
 
 
32
  ]
 
1
+ from .utils import fp8_act_quant
2
+ from .matmul import matmul_2d
3
+ from .batched import matmul_batched
4
+ from .grouped import matmul_grouped
5
+ from .fused_batched import moe_fused_batched
6
+ from .fused_grouped import moe_fused_grouped
 
 
 
 
 
 
 
 
 
 
7
 
8
  __all__ = [
9
  "fp8_act_quant",
10
+ # 2D matmul
11
+ "matmul_2d",
 
 
12
  # Batched matmul
13
+ "matmul_batched",
 
 
14
  # Grouped matmul
15
+ "matmul_grouped",
16
+ # Fused batched matmul
17
+ "moe_fused_batched",
18
+ # Fused grouped matmul
19
+ "moe_fused_grouped",
20
  ]
build/torch-cuda/_ops.py CHANGED
@@ -22,7 +22,7 @@ def get_backend() -> str:
22
 
23
  def _find_ops_name() -> str:
24
  kernel_name = "finegrained_fp8"
25
- unique_id = "7c5619e"
26
  backend = get_backend()
27
  return f"_{kernel_name}_{backend}_{unique_id}"
28
 
 
22
 
23
  def _find_ops_name() -> str:
24
  kernel_name = "finegrained_fp8"
25
+ unique_id = "93d7641"
26
  backend = get_backend()
27
  return f"_{kernel_name}_{backend}_{unique_id}"
28
 
build/torch-cuda/act_quant.py DELETED
@@ -1,73 +0,0 @@
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 torch.library import triton_op, wrap_triton
19
-
20
- from .utils import device_context
21
-
22
-
23
- _FP8_DTYPE = torch.float8_e4m3fn
24
-
25
-
26
- # Copied from https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/inference/kernel.py
27
- @triton.jit
28
- def _fp8_act_quant_kernel(x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr):
29
- pid = tl.program_id(axis=0)
30
- offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
31
- x = tl.load(x_ptr + offs).to(tl.float32)
32
- s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
33
- y = (x / s).to(y_ptr.dtype.element_ty)
34
- tl.store(y_ptr + offs, y)
35
- tl.store(s_ptr + pid, s)
36
-
37
-
38
- @triton_op("finegrained_fp8::fp8_act_quant", mutates_args=())
39
- def _fp8_act_quant(
40
- x: torch.Tensor, block_size: int = 128
41
- ) -> tuple[torch.Tensor, torch.Tensor]:
42
- assert x.is_contiguous()
43
- assert x.shape[-1] % block_size == 0
44
- y = torch.empty_like(x, dtype=_FP8_DTYPE)
45
- grid = (triton.cdiv(x.numel(), block_size),)
46
- s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
47
-
48
- with device_context(x.device):
49
- wrap_triton(_fp8_act_quant_kernel)[grid](x, y, s, BLOCK_SIZE=block_size)
50
-
51
- return y, s
52
-
53
-
54
- def fp8_act_quant(
55
- x: torch.Tensor, block_size: int = 128
56
- ) -> tuple[torch.Tensor, torch.Tensor]:
57
- """Quantize activations to FP8 with per-block dynamic scaling.
58
-
59
- Splits the last dimension of ``x`` into blocks of ``block_size`` elements,
60
- computes ``scale = max(|x_block|) / 448`` per block, and quantizes to
61
- ``float8_e4m3fn``.
62
-
63
- Args:
64
- x: Input tensor in bf16/fp16/fp32. Last dimension must be divisible by
65
- ``block_size`` and the tensor must be contiguous.
66
- block_size: Number of elements per quantization block (default: 128).
67
-
68
- Returns:
69
- A tuple ``(quantized, scales)`` where ``quantized`` has dtype
70
- ``float8_e4m3fn`` with the same shape as ``x``, and ``scales`` has
71
- shape ``(*x.shape[:-1], x.shape[-1] // block_size)`` in float32.
72
- """
73
- return torch.ops.finegrained_fp8.fp8_act_quant(x, block_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/batched.py CHANGED
@@ -15,31 +15,101 @@
15
  import torch
16
  import triton
17
  import triton.language as tl
18
- from .act_quant import fp8_act_quant
19
  from torch.library import triton_op, wrap_triton
20
 
21
- from .utils import device_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
- key=["N", "K"],
31
  )
32
  @triton.jit
33
- def w8a8_block_fp8_matmul_batched_kernel(
34
- A, # (S, K) raw BF16/FP16 activations
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
  ExpertIds, # (S,) — which expert each batch element routes to
39
  # Shape
40
  S,
41
  N,
42
  K,
 
43
  stride_am,
44
  stride_ak,
45
  stride_be,
@@ -50,28 +120,24 @@ def w8a8_block_fp8_matmul_batched_kernel(
50
  stride_bs_e,
51
  stride_bs_k,
52
  stride_bs_n,
 
 
53
  # Meta-parameters
 
54
  BLOCK_SIZE_N: tl.constexpr,
55
  BLOCK_SIZE_K: tl.constexpr,
56
- BLOCK_SIZE_M: tl.constexpr,
57
  ):
58
  """Block-scale batched FP8 expert matmul kernel.
59
 
60
  Each program handles one routed token row and one N-tile, looks up the
61
  owning expert from ``ExpertIds``, and applies fused activation quantization.
62
  """
63
- batch_id = tl.program_id(axis=0)
64
- pid_n = tl.program_id(axis=1)
65
-
66
- # Cast expert_id to int64 to prevent int32 overflow when computing
67
- # expert_id * stride_Eb (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
68
- # 3072×3072 FP8 weights).
69
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
70
-
71
- A = A + batch_id * stride_am
72
- B = B + expert_id * stride_be
73
- C = C + batch_id * stride_cm
74
- Bs = Bs + expert_id * stride_bs_e
75
 
76
  offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
77
  offs_k = tl.arange(0, BLOCK_SIZE_K)
@@ -82,55 +148,35 @@ def w8a8_block_fp8_matmul_batched_kernel(
82
 
83
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
84
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
85
- # ---- fused fp8_act_quant ----
86
  a_raw = tl.load(a_ptrs).to(tl.float32)
87
- a_s = tl.max(tl.abs(a_raw)) / 448.0
88
- a = (a_raw / tl.maximum(a_s, 1e-12)).to(tl.float8e4nv)
89
- # ---- matmul ----
90
  b = tl.load(b_ptrs)
91
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
92
- accumulator += tl.dot(a, b) * a_s * b_s[None, :]
93
  a_ptrs += BLOCK_SIZE_K * stride_ak
94
  b_ptrs += BLOCK_SIZE_K * stride_bk
 
95
 
96
- if C.dtype.element_ty == tl.bfloat16:
97
- c = accumulator.to(tl.bfloat16)
98
- elif C.dtype.element_ty == tl.float16:
99
- c = accumulator.to(tl.float16)
100
- else:
101
- c = accumulator.to(tl.float32)
102
-
103
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
104
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
105
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
106
- # Fake-batch trick aliases all BLOCK_SIZE_M lanes to the same C row, so emitting
107
- # `tl.store(c_ptrs, c)` issues BLOCK_SIZE_M duplicate-address stores. On NVIDIA
108
- # WGMMA this is usually benign (last-write-wins of identical bytes), but on Intel
109
- # XPU the duplicate-address store has hardware-undefined behavior and corrupts the
110
- # output. Mask so only lane 0 stores — the (M, N) accumulator rows are
111
- # mathematically identical (same A row × same B), so lane 0 holds the right value.
112
- tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
113
 
114
 
115
  @triton.autotune(
116
- configs=[
117
- triton.Config({}, num_warps=w, num_stages=s)
118
- for w in [2, 4, 8, 16]
119
- for s in [2, 3, 4, 5]
120
- ],
121
  key=["N", "K"],
122
  )
123
  @triton.jit
124
- def w8a8_tensor_fp8_matmul_batched_kernel(
125
  A, # (S, K) pre-quantized FP8 activations
126
- B, # (E, N, K) FP8 weight matrices
127
  C, # (S, N) output
128
- As, # (S, 1) per-tensor activation scales
129
- Bs, # (E, 1, 1) per-tensor weight scales
130
- ExpertIds,
 
131
  S,
132
  N,
133
  K,
 
134
  stride_am,
135
  stride_ak,
136
  stride_be,
@@ -140,24 +186,24 @@ def w8a8_tensor_fp8_matmul_batched_kernel(
140
  stride_cn,
141
  stride_as_m,
142
  stride_bs_e,
 
 
 
 
143
  BLOCK_SIZE_N: tl.constexpr,
144
  BLOCK_SIZE_K: tl.constexpr,
145
- BLOCK_SIZE_M: tl.constexpr,
146
  ):
147
  """Tensor-scale batched FP8 expert matmul kernel.
148
 
149
  Activations are already quantized; the kernel applies per-token activation
150
  scales and per-expert tensor weight scales.
151
  """
152
- batch_id = tl.program_id(axis=0)
153
- pid_n = tl.program_id(axis=1)
154
-
155
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
156
-
157
- A = A + batch_id * stride_am
158
- B = B + expert_id * stride_be
159
- C = C + batch_id * stride_cm
160
- Bs = Bs + expert_id * stride_bs_e
161
 
162
  offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
163
  offs_k = tl.arange(0, BLOCK_SIZE_K)
@@ -177,45 +223,126 @@ def w8a8_tensor_fp8_matmul_batched_kernel(
177
 
178
  accumulator = accumulator * a_s * b_s
179
 
180
- if C.dtype.element_ty == tl.bfloat16:
181
- c = accumulator.to(tl.bfloat16)
182
- elif C.dtype.element_ty == tl.float16:
183
- c = accumulator.to(tl.float16)
184
- else:
185
- c = accumulator.to(tl.float32)
186
 
187
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
188
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
189
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
190
- # See block-FP8 kernel above: BLOCK_SIZE_M lanes alias the same C row;
191
- # mask so only lane 0 stores to avoid hardware-undefined duplicate writes on XPU.
192
- tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_batched", mutates_args=())
196
- def _w8a8_block_fp8_matmul_batched(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  A: torch.Tensor,
198
  B: torch.Tensor,
199
  Bs: torch.Tensor,
200
  expert_ids: torch.Tensor,
201
  block_size: list[int],
 
202
  ) -> torch.Tensor:
203
  """Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
204
 
205
  A: (S, K) raw bf16/fp16 activations
206
- B: (E, N, K) FP8 expert weights
207
- Bs: (E, N // block_n, K // block_k) per-block weight scales
208
  """
209
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
210
  assert A.is_contiguous(), "A must be contiguous"
211
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
212
  assert B.is_contiguous(), "B must be contiguous"
213
  assert A.shape[1] == B.shape[2], (
214
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
215
  )
216
 
217
  S, K = A.shape
218
- E, N, _ = B.shape
219
 
220
  assert len(block_size) == 2, (
221
  f"block_size must be [block_n, block_k], got {block_size}"
@@ -225,21 +352,18 @@ def _w8a8_block_fp8_matmul_batched(
225
  assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
226
  assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
227
  assert Bs.ndim == 3, (
228
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
229
  )
230
- assert Bs.shape == (E, N // block_n, K // block_k), (
231
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
232
  )
233
 
234
- C = A.new_empty(S, N)
235
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
236
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
237
- # register pressure and a better-matched FP8 WGMMA instruction, improving
238
- # both accuracy and performance for small M (decode).
239
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
240
  grid = (S, triton.cdiv(N, block_n))
 
 
241
  with device_context(A.device):
242
- wrap_triton(w8a8_block_fp8_matmul_batched_kernel)[grid](
243
  A,
244
  B,
245
  C,
@@ -258,60 +382,63 @@ def _w8a8_block_fp8_matmul_batched(
258
  Bs.stride(0),
259
  Bs.stride(2),
260
  Bs.stride(1),
 
261
  BLOCK_SIZE_N=block_n,
262
  BLOCK_SIZE_K=block_k,
263
- BLOCK_SIZE_M=BLOCK_SIZE_M,
 
264
  )
265
 
266
  return C
267
 
268
 
269
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_batched", mutates_args=())
270
- def _w8a8_tensor_fp8_matmul_batched(
 
 
271
  A: torch.Tensor,
272
  B: torch.Tensor,
273
  Bs: torch.Tensor,
274
  expert_ids: torch.Tensor,
 
275
  ) -> torch.Tensor:
276
  """Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
277
 
278
  A: (S, K) raw bf16/fp16 activations
279
- B: (E, N, K) FP8 expert weights
280
- Bs: (E,) or (E, 1, 1) per-expert weight scales
281
  """
282
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
283
  assert A.is_contiguous(), "A must be contiguous"
284
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
285
  assert B.is_contiguous(), "B must be contiguous"
286
  assert A.shape[1] == B.shape[2], (
287
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
288
  )
289
 
290
  S, K = A.shape
291
- E, N, _ = B.shape
292
 
293
- # Normalize Bs to (E, 1, 1)
294
  if Bs.ndim == 1:
295
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
296
- Bs = Bs.reshape(E, 1, 1)
 
 
297
  else:
298
- assert Bs.shape == (E, 1, 1), (
299
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
300
  )
301
 
302
- BLOCK_SIZE_N = 128
303
- BLOCK_SIZE_K = 128
304
- C = A.new_empty(S, N)
305
  qA, As = fp8_act_quant(A, K)
306
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
307
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
308
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
309
- # register pressure and a better-matched FP8 WGMMA instruction, improving
310
- # both accuracy and performance for small M (decode).
311
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
312
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
313
  with device_context(A.device):
314
- wrap_triton(w8a8_tensor_fp8_matmul_batched_kernel)[grid](
315
  qA,
316
  B,
317
  C,
@@ -330,69 +457,170 @@ def _w8a8_tensor_fp8_matmul_batched(
330
  C.stride(1),
331
  As.stride(0),
332
  Bs.stride(0),
333
- BLOCK_SIZE_N=BLOCK_SIZE_N,
334
- BLOCK_SIZE_K=BLOCK_SIZE_K,
335
- BLOCK_SIZE_M=BLOCK_SIZE_M,
336
  )
337
 
338
  return C
339
 
340
 
341
- def w8a8_block_fp8_matmul_batched(
342
  A: torch.Tensor,
343
  B: torch.Tensor,
344
  Bs: torch.Tensor,
345
  expert_ids: torch.Tensor,
346
  block_size: list[int],
 
347
  ) -> torch.Tensor:
348
  """Block-scale batched FP8 matmul with fused activation quantization.
349
 
350
  A: (S, K) raw activations, bf16/fp16/fp32
351
- B: (E, N, K) FP8 expert weights
352
- Bs: (E, N // block_n, K // block_k) per-block weight scales
 
 
353
  """
354
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_batched(
355
- A, B, Bs, expert_ids, block_size
356
  )
357
 
358
 
359
- def w8a8_tensor_fp8_matmul_batched(
360
  A: torch.Tensor,
361
  B: torch.Tensor,
362
  Bs: torch.Tensor,
363
  expert_ids: torch.Tensor,
 
364
  ) -> torch.Tensor:
365
  """Tensor-scale batched FP8 matmul with fused activation quantization.
366
 
367
  A: (S, K) raw activations, bf16/fp16/fp32
368
- B: (E, N, K) FP8 expert weights
369
- Bs: (E,) or (E, 1, 1) per-expert weight scales
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  """
371
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_batched(
372
- A, B, Bs, expert_ids
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  )
 
 
 
 
 
 
 
374
 
 
 
375
 
376
- def w8a8_fp8_matmul_batched(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  A: torch.Tensor,
378
  B: torch.Tensor,
379
  Bs: torch.Tensor,
380
  expert_ids: torch.Tensor,
381
  block_size: list[int] | None,
 
382
  ) -> torch.Tensor:
383
- """Unified batched W8A8 FP8 matmul dispatcher.
 
384
 
385
- Dispatch rules:
386
- - tensor mode when ``block_size is None``
387
- - tensor mode when ``block_size == [N, K]``
388
- - otherwise block mode
389
 
390
- Returns:
391
- Output tensor ``[S, N]`` in the same dtype as ``A``.
 
 
 
 
392
  """
393
- if block_size is None or (
394
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
395
- ):
396
- return w8a8_tensor_fp8_matmul_batched(A, B, Bs, expert_ids)
 
 
 
397
 
398
- return w8a8_block_fp8_matmul_batched(A, B, Bs, expert_ids, block_size)
 
 
 
15
  import torch
16
  import triton
17
  import triton.language as tl
 
18
  from torch.library import triton_op, wrap_triton
19
 
20
+ from ._ops import add_op_namespace_prefix, ops
21
+ from .bayesian_autotuner import bayesian_autotune
22
+ from .utils import (
23
+ MX_SCALE_GROUP_K,
24
+ NIBBLES_PER_BYTE,
25
+ DECODE_BLOCK_SIZE_M,
26
+ decode_ue8m0_scale,
27
+ device_context,
28
+ mx_dot_rescale,
29
+ mx_dot_scaled,
30
+ mx_scalar_reduce,
31
+ mxfp_act_quant_inline,
32
+ mxfp4_e2m1_to_e4m3,
33
+ fp8_act_quant,
34
+ fp8_act_quant_inline,
35
+ get_accelerator_autotuning_configs,
36
+ get_mxfp_autotuning_configs,
37
+ is_mxfp,
38
+ is_tensor_wide,
39
+ e2m1_as_uint8,
40
+ ue8m0_as_uint8,
41
+ )
42
+
43
+
44
+ @triton.jit
45
+ def expert_setup(
46
+ A,
47
+ B,
48
+ C,
49
+ Bs,
50
+ ExpertIds,
51
+ stride_am,
52
+ stride_be,
53
+ stride_cm,
54
+ stride_bs_e,
55
+ stride_eid,
56
+ ):
57
+ """Per-(row, expert) prologue shared by the batched kernels: read the program
58
+ ids, look up the routed expert, and advance the A/B/C/Bs base pointers to this
59
+ row's slice. Returns ``(batch_id, pid_n, expert_id, A, B, C, Bs)``.
60
+
61
+ The caller must early-return on the EP sentinel (``expert_id >= num_experts``)
62
+ before any load — the pointer arithmetic itself is harmless, only the loads on a
63
+ non-local expert would be out of bounds."""
64
+ batch_id = tl.program_id(axis=0)
65
+ pid_n = tl.program_id(axis=1)
66
+ # Cast to int64 to prevent overflow on expert_id * stride_be.
67
+ expert_id = tl.load(ExpertIds + batch_id * stride_eid).to(tl.int64)
68
+ A = A + batch_id * stride_am
69
+ B = B + expert_id * stride_be
70
+ C = C + batch_id * stride_cm
71
+ Bs = Bs + expert_id * stride_bs_e
72
+ return batch_id, pid_n, expert_id, A, B, C, Bs
73
+
74
+
75
+ @triton.jit
76
+ def store_row(
77
+ C,
78
+ accumulator,
79
+ pid_n,
80
+ stride_cn,
81
+ BLOCK_SIZE_M: tl.constexpr,
82
+ BLOCK_SIZE_N: tl.constexpr,
83
+ ):
84
+ """Output epilogue shared by the batched kernels (``C`` already advanced to the
85
+ row). The fake-batch trick aliases all ``BLOCK_SIZE_M`` lanes to the same C row,
86
+ so a plain store would issue ``BLOCK_SIZE_M`` duplicate-address writes — benign on
87
+ NVIDIA WGMMA (last-write-wins of identical bytes) but hardware-undefined on Intel
88
+ XPU, where it corrupts the output. Mask so only lane 0 stores; the accumulator
89
+ rows are mathematically identical (same A row × same B), so lane 0 is correct."""
90
+ c = accumulator.to(C.dtype.element_ty)
91
+ offs_cm = tl.arange(0, BLOCK_SIZE_M)
92
+ offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
93
+ c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
94
+ tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
95
 
96
 
97
  @triton.autotune(
98
+ configs=get_accelerator_autotuning_configs(),
99
+ key=["N", "K", "S"],
 
 
 
 
100
  )
101
  @triton.jit
102
+ def w8a8_block_dynamic_fp8_matmul_batched_kernel(
103
+ A, # (S, K) raw BF16/FP16 activations
104
+ B, # (num_experts, N, K) FP8 weight matrices
105
+ C, # (S, N) output
106
+ Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
107
  ExpertIds, # (S,) — which expert each batch element routes to
108
  # Shape
109
  S,
110
  N,
111
  K,
112
+ # Strides
113
  stride_am,
114
  stride_ak,
115
  stride_be,
 
120
  stride_bs_e,
121
  stride_bs_k,
122
  stride_bs_n,
123
+ stride_eid,
124
+ num_experts,
125
  # Meta-parameters
126
+ BLOCK_SIZE_M: tl.constexpr,
127
  BLOCK_SIZE_N: tl.constexpr,
128
  BLOCK_SIZE_K: tl.constexpr,
 
129
  ):
130
  """Block-scale batched FP8 expert matmul kernel.
131
 
132
  Each program handles one routed token row and one N-tile, looks up the
133
  owning expert from ``ExpertIds``, and applies fused activation quantization.
134
  """
135
+ _, pid_n, expert_id, A, B, C, Bs = expert_setup(
136
+ A, B, C, Bs, ExpertIds, stride_am, stride_be, stride_cm, stride_bs_e, stride_eid
137
+ )
138
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
139
+ if expert_id >= num_experts:
140
+ return
 
 
 
 
 
 
141
 
142
  offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
143
  offs_k = tl.arange(0, BLOCK_SIZE_K)
 
148
 
149
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
150
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
 
151
  a_raw = tl.load(a_ptrs).to(tl.float32)
152
+ a, a_s = fp8_act_quant_inline(a_raw)
 
 
153
  b = tl.load(b_ptrs)
154
+ b_s = decode_ue8m0_scale(tl.load(bs_ptrs))
155
+ accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
156
  a_ptrs += BLOCK_SIZE_K * stride_ak
157
  b_ptrs += BLOCK_SIZE_K * stride_bk
158
+ bs_ptrs += stride_bs_k
159
 
160
+ store_row(C, accumulator, pid_n, stride_cn, BLOCK_SIZE_M, BLOCK_SIZE_N)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
 
163
  @triton.autotune(
164
+ configs=get_accelerator_autotuning_configs(tune_block_nk=True),
 
 
 
 
165
  key=["N", "K"],
166
  )
167
  @triton.jit
168
+ def w8a8_tensor_dynamic_fp8_matmul_batched_kernel(
169
  A, # (S, K) pre-quantized FP8 activations
170
+ B, # (num_experts, N, K) FP8 weight matrices
171
  C, # (S, N) output
172
+ As, # (S,) per-token activation scales
173
+ Bs, # (num_experts, 1, 1) per-tensor weight scales
174
+ ExpertIds, # (S,) — which expert each batch element routes to
175
+ # Shape
176
  S,
177
  N,
178
  K,
179
+ # Strides
180
  stride_am,
181
  stride_ak,
182
  stride_be,
 
186
  stride_cn,
187
  stride_as_m,
188
  stride_bs_e,
189
+ stride_eid,
190
+ num_experts,
191
+ # Meta-parameters
192
+ BLOCK_SIZE_M: tl.constexpr,
193
  BLOCK_SIZE_N: tl.constexpr,
194
  BLOCK_SIZE_K: tl.constexpr,
 
195
  ):
196
  """Tensor-scale batched FP8 expert matmul kernel.
197
 
198
  Activations are already quantized; the kernel applies per-token activation
199
  scales and per-expert tensor weight scales.
200
  """
201
+ batch_id, pid_n, expert_id, A, B, C, Bs = expert_setup(
202
+ A, B, C, Bs, ExpertIds, stride_am, stride_be, stride_cm, stride_bs_e, stride_eid
203
+ )
204
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
205
+ if expert_id >= num_experts:
206
+ return
 
 
 
207
 
208
  offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
209
  offs_k = tl.arange(0, BLOCK_SIZE_K)
 
223
 
224
  accumulator = accumulator * a_s * b_s
225
 
226
+ store_row(C, accumulator, pid_n, stride_cn, BLOCK_SIZE_M, BLOCK_SIZE_N)
 
 
 
 
 
227
 
 
 
 
 
 
 
228
 
229
+ @bayesian_autotune(get_mxfp_autotuning_configs(), ["N", "K", "S"], n_trials=60)
230
+ @triton.jit
231
+ def mxfp_dynamic_matmul_batched_kernel(
232
+ A, # (S, K) raw BF16/FP16 activations
233
+ B, # (num_experts, N, K) E4M3 (MXFP8) or (num_experts, N, K // 2) packed E2M1 (MXFP4) expert weights
234
+ C, # (S, N) output
235
+ Bs, # (num_experts, N, K // SCALE_GROUP_K) UE8M0 weight scales
236
+ ExpertIds, # (S,) — which expert each routed row uses
237
+ # Shape
238
+ S,
239
+ N,
240
+ K,
241
+ # Strides
242
+ stride_am,
243
+ stride_ak,
244
+ stride_be,
245
+ stride_bk,
246
+ stride_bn,
247
+ stride_cm,
248
+ stride_cn,
249
+ stride_bs_e,
250
+ stride_bs_k,
251
+ stride_bs_n,
252
+ stride_eid,
253
+ num_experts,
254
+ # Meta-parameters
255
+ BLOCK_SIZE_M: tl.constexpr,
256
+ BLOCK_SIZE_N: tl.constexpr,
257
+ BLOCK_SIZE_K: tl.constexpr,
258
+ VALUES_PER_BYTE: tl.constexpr,
259
+ SCALE_GROUP_K: tl.constexpr,
260
+ COMPUTE_MODE: tl.constexpr,
261
+ ):
262
+ """Unified batched MXFP4/MXFP8 (W4A8/W8A8) expert matmul with fused act quant.
263
+
264
+ One routed row + one N-tile per program; expert looked up from ``ExpertIds``. ``A`` is
265
+ quantized to E4M3 per K-group inline (UE8M0 scale). ``VALUES_PER_BYTE`` picks the
266
+ weight format (2 = packed E2M1 / MXFP4, 1 = unpacked E4M3 / MXFP8); ``COMPUTE_MODE``
267
+ picks ``tl.dot_scaled`` (native M=128) vs fp8 ``tl.dot`` + per-group rescale (wins at
268
+ decode where the scaled MMA's M→128 pad is waste; FP4 unpacks E2M1->E4M3, lossless).
269
+ """
270
+ _, pid_n, expert_id, A, B, C, Bs = expert_setup(
271
+ A, B, C, Bs, ExpertIds, stride_am, stride_be, stride_cm, stride_bs_e, stride_eid
272
+ )
273
+ # EP sentinel: row routed to a non-local expert; output is left uninit.
274
+ if expert_id >= num_experts:
275
+ return
276
+
277
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
278
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
279
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)
280
+ offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
281
+ a_ptrs = A + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0 + offs_k[None, :] * stride_ak
282
+ b_ptrs = B + (offs_kb[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
283
+ bs_ptrs = Bs + offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k
284
 
285
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
286
+ for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
287
+ a_raw = tl.load(a_ptrs).to(tl.float32)
288
+ a, a_scale = mxfp_act_quant_inline(
289
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K
290
+ )
291
+ b = tl.load(b_ptrs)
292
+ b_s = tl.load(bs_ptrs).to(tl.uint8)
293
+ bq = mxfp4_e2m1_to_e4m3(b) if VALUES_PER_BYTE == 2 else b
294
+ if COMPUTE_MODE == "dot_scaled":
295
+ accumulator = mx_dot_scaled(
296
+ a, a_scale, b, b_s, accumulator, VALUES_PER_BYTE
297
+ )
298
+ elif COMPUTE_MODE == "dot":
299
+ accumulator = mx_dot_rescale(accumulator, a, bq, a_scale, b_s)
300
+ else: # scalar
301
+ accumulator = mx_scalar_reduce(
302
+ accumulator,
303
+ a,
304
+ a_scale,
305
+ bq,
306
+ b_s,
307
+ BLOCK_SIZE_M,
308
+ BLOCK_SIZE_N,
309
+ BLOCK_SIZE_K,
310
+ SCALE_GROUP_K,
311
+ )
312
+ a_ptrs += BLOCK_SIZE_K * stride_ak
313
+ b_ptrs += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_bk
314
+ bs_ptrs += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_bs_k
315
+
316
+ store_row(C, accumulator, pid_n, stride_cn, BLOCK_SIZE_M, BLOCK_SIZE_N)
317
+
318
+
319
+ @triton_op(
320
+ add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul_batched"), mutates_args=()
321
+ )
322
+ def _w8a8_block_dynamic_fp8_matmul_batched(
323
  A: torch.Tensor,
324
  B: torch.Tensor,
325
  Bs: torch.Tensor,
326
  expert_ids: torch.Tensor,
327
  block_size: list[int],
328
+ output_dtype: torch.dtype | None = None,
329
  ) -> torch.Tensor:
330
  """Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
331
 
332
  A: (S, K) raw bf16/fp16 activations
333
+ B: (num_experts, N, K) FP8 expert weights
334
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales
335
  """
336
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
337
  assert A.is_contiguous(), "A must be contiguous"
338
+ assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
339
  assert B.is_contiguous(), "B must be contiguous"
340
  assert A.shape[1] == B.shape[2], (
341
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
342
  )
343
 
344
  S, K = A.shape
345
+ num_experts, N, _ = B.shape
346
 
347
  assert len(block_size) == 2, (
348
  f"block_size must be [block_n, block_k], got {block_size}"
 
352
  assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
353
  assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
354
  assert Bs.ndim == 3, (
355
+ f"Bs must be 3D (num_experts, N//block_n, K//block_k), got ndim={Bs.ndim}"
356
  )
357
+ assert Bs.shape == (num_experts, N // block_n, K // block_k), (
358
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {N // block_n}, {K // block_k})"
359
  )
360
 
361
+ Bs = ue8m0_as_uint8(Bs)
 
 
 
 
 
362
  grid = (S, triton.cdiv(N, block_n))
363
+ C = A.new_empty(S, N, dtype=output_dtype)
364
+
365
  with device_context(A.device):
366
+ wrap_triton(w8a8_block_dynamic_fp8_matmul_batched_kernel)[grid](
367
  A,
368
  B,
369
  C,
 
382
  Bs.stride(0),
383
  Bs.stride(2),
384
  Bs.stride(1),
385
+ expert_ids.stride(0),
386
  BLOCK_SIZE_N=block_n,
387
  BLOCK_SIZE_K=block_k,
388
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
389
+ num_experts=num_experts,
390
  )
391
 
392
  return C
393
 
394
 
395
+ @triton_op(
396
+ add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul_batched"), mutates_args=()
397
+ )
398
+ def _w8a8_tensor_dynamic_fp8_matmul_batched(
399
  A: torch.Tensor,
400
  B: torch.Tensor,
401
  Bs: torch.Tensor,
402
  expert_ids: torch.Tensor,
403
+ output_dtype: torch.dtype | None = None,
404
  ) -> torch.Tensor:
405
  """Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
406
 
407
  A: (S, K) raw bf16/fp16 activations
408
+ B: (num_experts, N, K) FP8 expert weights
409
+ Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
410
  """
411
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
412
  assert A.is_contiguous(), "A must be contiguous"
413
+ assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
414
  assert B.is_contiguous(), "B must be contiguous"
415
  assert A.shape[1] == B.shape[2], (
416
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
417
  )
418
 
419
  S, K = A.shape
420
+ num_experts, N, _ = B.shape
421
 
422
+ # Normalize Bs to (num_experts, 1, 1)
423
  if Bs.ndim == 1:
424
+ assert Bs.shape[0] == num_experts, (
425
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts},)"
426
+ )
427
+ Bs = Bs.reshape(num_experts, 1, 1)
428
  else:
429
+ assert Bs.shape == (num_experts, 1, 1), (
430
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, 1, 1)"
431
  )
432
 
433
+ Bs = ue8m0_as_uint8(Bs)
 
 
434
  qA, As = fp8_act_quant(A, K)
435
+ C = A.new_empty(S, N, dtype=output_dtype)
436
+
437
+ def grid(META):
438
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
439
+
 
 
440
  with device_context(A.device):
441
+ wrap_triton(w8a8_tensor_dynamic_fp8_matmul_batched_kernel)[grid](
442
  qA,
443
  B,
444
  C,
 
457
  C.stride(1),
458
  As.stride(0),
459
  Bs.stride(0),
460
+ expert_ids.stride(0),
461
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
462
+ num_experts=num_experts,
463
  )
464
 
465
  return C
466
 
467
 
468
+ def w8a8_block_dynamic_fp8_matmul_batched(
469
  A: torch.Tensor,
470
  B: torch.Tensor,
471
  Bs: torch.Tensor,
472
  expert_ids: torch.Tensor,
473
  block_size: list[int],
474
+ output_dtype: torch.dtype | None = None,
475
  ) -> torch.Tensor:
476
  """Block-scale batched FP8 matmul with fused activation quantization.
477
 
478
  A: (S, K) raw activations, bf16/fp16/fp32
479
+ B: (num_experts, N, K) FP8 expert weights
480
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales
481
+ expert_ids: (S,) — kernel loads stride-aware, any int dtype works
482
+ output_dtype: defaults to ``A.dtype``
483
  """
484
+ return ops.w8a8_block_dynamic_fp8_matmul_batched(
485
+ A, B, Bs, expert_ids, block_size, output_dtype
486
  )
487
 
488
 
489
+ def w8a8_tensor_dynamic_fp8_matmul_batched(
490
  A: torch.Tensor,
491
  B: torch.Tensor,
492
  Bs: torch.Tensor,
493
  expert_ids: torch.Tensor,
494
+ output_dtype: torch.dtype | None = None,
495
  ) -> torch.Tensor:
496
  """Tensor-scale batched FP8 matmul with fused activation quantization.
497
 
498
  A: (S, K) raw activations, bf16/fp16/fp32
499
+ B: (num_experts, N, K) FP8 expert weights
500
+ Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
501
+ output_dtype: defaults to ``A.dtype``
502
+ """
503
+ return ops.w8a8_tensor_dynamic_fp8_matmul_batched(
504
+ A, B, Bs, expert_ids, output_dtype
505
+ )
506
+
507
+
508
+ @triton_op(add_op_namespace_prefix("mxfp_dynamic_matmul_batched"), mutates_args=())
509
+ def _mxfp_dynamic_matmul_batched(
510
+ A: torch.Tensor,
511
+ B: torch.Tensor,
512
+ Bs: torch.Tensor,
513
+ expert_ids: torch.Tensor,
514
+ output_dtype: torch.dtype | None = None,
515
+ ) -> torch.Tensor:
516
+ """Batched MX matmul ``C[s] = A[s] @ B[expert_ids[s]].T`` with fused act quant.
517
+ Weight format is detected from ``B.dtype``: ``int8`` → packed E2M1 (MXFP4, ``B`` is
518
+ ``(num_experts, N, K//2)``); ``float8_e4m3fn`` → unpacked E4M3 (MXFP8, ``(num_experts, N, K)``). Both use
519
+ UE8M0 group-32 scales ``(num_experts, N, K//32)``; tile + dot path are autotuned.
520
+
521
+ A: (S, K) raw activations, bf16/fp16/fp32 (quantized inline to E4M3)
522
+ expert_ids: (S,) which expert each routed row uses
523
  """
524
+ assert A.ndim == 2 and B.ndim == 3 and Bs.ndim == 3
525
+ assert expert_ids.ndim == 1
526
+ assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
527
+ f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
528
+ )
529
+ assert Bs.dtype == torch.float8_e8m0fnu, (
530
+ f"Bs must be float8_e8m0fnu, got {Bs.dtype}"
531
+ )
532
+ VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
533
+
534
+ S, K = A.shape
535
+ num_experts, N, K_b = B.shape
536
+ assert expert_ids.shape[0] == S
537
+ assert K == VALUES_PER_BYTE * K_b, (
538
+ f"K (={K}) must equal {VALUES_PER_BYTE} * B.shape[2] (={K_b})"
539
+ )
540
+ assert K % MX_SCALE_GROUP_K == 0, (
541
+ f"K (={K}) must be a multiple of {MX_SCALE_GROUP_K}"
542
  )
543
+ assert Bs.shape == (num_experts, N, K // MX_SCALE_GROUP_K), (
544
+ f"Bs shape {tuple(Bs.shape)} != ({num_experts}, {N}, {K // MX_SCALE_GROUP_K})"
545
+ )
546
+
547
+ B = e2m1_as_uint8(B)
548
+ bs_u8 = ue8m0_as_uint8(Bs)
549
+ C = A.new_empty((S, N), dtype=output_dtype)
550
 
551
+ def grid(META):
552
+ return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
553
 
554
+ with device_context(A.device):
555
+ wrap_triton(mxfp_dynamic_matmul_batched_kernel)[grid](
556
+ A,
557
+ B,
558
+ C,
559
+ bs_u8,
560
+ expert_ids,
561
+ S,
562
+ N,
563
+ K,
564
+ A.stride(0),
565
+ A.stride(1),
566
+ B.stride(0),
567
+ B.stride(2),
568
+ B.stride(1),
569
+ C.stride(0),
570
+ C.stride(1),
571
+ bs_u8.stride(0),
572
+ bs_u8.stride(2),
573
+ bs_u8.stride(1),
574
+ expert_ids.stride(0),
575
+ # Meta-parameters (BLOCK_SIZE_N, BLOCK_SIZE_K come from autotune Config)
576
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
577
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
578
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
579
+ num_experts=num_experts,
580
+ )
581
+ return C
582
+
583
+
584
+ def mxfp_dynamic_matmul_batched(
585
+ A: torch.Tensor,
586
+ B: torch.Tensor,
587
+ Bs: torch.Tensor,
588
+ expert_ids: torch.Tensor,
589
+ output_dtype: torch.dtype | None = None,
590
+ ) -> torch.Tensor:
591
+ """Batched MX (MXFP4/MXFP8) matmul with fused act quant; the weight format
592
+ (packed E2M1 vs E4M3) is detected from ``B.dtype``. Tile + dot path autotuned."""
593
+ return ops.mxfp_dynamic_matmul_batched(A, B, Bs, expert_ids, output_dtype)
594
+
595
+
596
+ def matmul_batched(
597
  A: torch.Tensor,
598
  B: torch.Tensor,
599
  Bs: torch.Tensor,
600
  expert_ids: torch.Tensor,
601
  block_size: list[int] | None,
602
+ output_dtype: torch.dtype | None = None,
603
  ) -> torch.Tensor:
604
+ """Batched quantized matmul dispatcher (W8A8 FP8 or W4A8 FP4). Routes one
605
+ routed row per program to ``B[expert_ids[s]]``.
606
 
607
+ ``output_dtype`` defaults to ``A.dtype``.
 
 
 
608
 
609
+ Routes by weight dtype and ``block_size``:
610
+ - MX weights — ``int8`` (packed E2M1) or ``float8_e4m3fn`` (E4M3) with UE8M0
611
+ group-32 ``Bs`` (shape ``[num_experts, N, K//32]``) → ``mxfp_dynamic_matmul_batched``
612
+ (``block_size`` ignored; tile + dot path autotuned).
613
+ - ``block_size`` None or full ``[N, K]`` → ``w8a8_tensor_dynamic_fp8_matmul_batched``.
614
+ - otherwise → ``w8a8_block_dynamic_fp8_matmul_batched``.
615
  """
616
+ if is_mxfp(B, Bs):
617
+ return mxfp_dynamic_matmul_batched(A, B, Bs, expert_ids, output_dtype)
618
+
619
+ if is_tensor_wide(block_size, B):
620
+ return w8a8_tensor_dynamic_fp8_matmul_batched(
621
+ A, B, Bs, expert_ids, output_dtype
622
+ )
623
 
624
+ return w8a8_block_dynamic_fp8_matmul_batched(
625
+ A, B, Bs, expert_ids, block_size, output_dtype
626
+ )
build/torch-cuda/bayesian_autotuner.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 json
32
+ import math
33
+ import os
34
+ import random
35
+ import time
36
+ from collections import defaultdict
37
+ from typing import Dict, List
38
+
39
+ from triton.runtime.autotuner import (
40
+ Autotuner,
41
+ Config,
42
+ JITFunction,
43
+ driver,
44
+ get_cache_invalidating_env_vars,
45
+ get_cache_manager,
46
+ knobs,
47
+ triton_key,
48
+ )
49
+
50
+
51
+ class BayesianAutotuner(Autotuner):
52
+ """Drop-in replacement for ``triton.runtime.autotuner.Autotuner`` that
53
+ benches ~``n_trials`` configs per key via TPE Bayesian optimization +
54
+ coordinate-descent refinement, instead of the full grid."""
55
+
56
+ def __init__(
57
+ self,
58
+ *args,
59
+ n_trials: int = 80,
60
+ n_startup_trials: int = 12,
61
+ gamma: float = 0.25,
62
+ refine: bool = True,
63
+ max_refine_iters: int = 5,
64
+ log_path: str | None = None,
65
+ **kwargs,
66
+ ):
67
+ super().__init__(*args, **kwargs)
68
+ # Bayesian trial budget — the per-decorator default, overridable via the
69
+ # FINEGRAINED_AUTOTUNE_TRIALS env var (quick sweeps / exhaustive runs without touching
70
+ # the decorators; set it >= grid size to fall back to stock exhaustive bench-all).
71
+ self.n_trials = int(os.environ.get("FINEGRAINED_AUTOTUNE_TRIALS") or n_trials)
72
+ self.n_startup_trials = n_startup_trials
73
+ # top fraction of measured configs the TPE treats as "good"
74
+ self.gamma = gamma
75
+ self.refine = refine
76
+ self.max_refine_iters = max_refine_iters
77
+ # JSONL log of every benched (key, config, ms) — set here or via the
78
+ # FINEGRAINED_AUTOTUNE_LOG env var. Analyse offline to prune bad configs.
79
+ self.log_path = log_path or os.environ.get("FINEGRAINED_AUTOTUNE_LOG")
80
+
81
+ def run(self, *args, **kwargs):
82
+ # Small grid → defer to parent (stock exhaustive bench-all).
83
+ if len(self.configs) <= 1 or self.n_trials >= len(self.configs):
84
+ return super().run(*args, **kwargs)
85
+
86
+ self.nargs = dict(zip(self.arg_names, args))
87
+ all_args = {**self.nargs, **kwargs}
88
+ _args = {k: v for k, v in all_args.items() if k in self.arg_names}
89
+ key = [_args[k] for k in self.keys if k in _args]
90
+ key.extend(str(v.dtype) for v in _args.values() if hasattr(v, "dtype"))
91
+ key = tuple(key)
92
+
93
+ if key not in self.cache:
94
+ pruned = self.prune_configs(kwargs)
95
+
96
+ def benchmark():
97
+ t0 = time.time()
98
+ self.cache[key] = self._bayesian_search(pruned, args, kwargs, key)
99
+ self.bench_time = time.time() - t0
100
+ if knobs.autotuning.print:
101
+ fn_name = getattr(self.fn, "__name__", str(self.fn))
102
+ print(
103
+ f"[bayesian-autotune] {fn_name} tuned "
104
+ f"{len(self.configs_timings)} configs in {self.bench_time:.1f}s — "
105
+ f"key={key}, best={self.cache[key].all_kwargs()}"
106
+ )
107
+ full_nargs = {**self.nargs, **kwargs, **self.cache[key].all_kwargs()}
108
+ self.pre_hook(full_nargs, reset_only=True)
109
+
110
+ if self.cache_results:
111
+ self.check_disk_cache(key, pruned, benchmark)
112
+ else:
113
+ benchmark()
114
+
115
+ config = self.cache[key]
116
+ self.best_config = config
117
+ if config.pre_hook is not None:
118
+ config.pre_hook({**self.nargs, **kwargs, **config.all_kwargs()})
119
+ ret = self.fn.run(*args, **kwargs, **config.all_kwargs())
120
+ self.nargs = None
121
+ return ret
122
+
123
+ def _bayesian_search(self, configs: List[Config], args, kwargs, key) -> Config:
124
+ timings: Dict[int, float] = {}
125
+ sigs = [tuple(sorted(c.all_kwargs().items())) for c in configs]
126
+
127
+ def bench_idx(idx: int) -> float:
128
+ if idx in timings:
129
+ return timings[idx]
130
+ try:
131
+ ms = self._bench(*args, config=configs[idx], **kwargs)
132
+ if isinstance(ms, (tuple, list)):
133
+ ms = ms[0]
134
+ timings[idx] = float(ms)
135
+ except Exception:
136
+ timings[idx] = float("inf")
137
+ self._log_result(key, configs[idx], timings[idx])
138
+ return timings[idx]
139
+
140
+ # Distinct values per config dimension, for the TPE's Laplace smoothing.
141
+ dim_vals: Dict = defaultdict(set)
142
+ for sig in sigs:
143
+ for d, v in sig:
144
+ dim_vals[d].add(v)
145
+
146
+ # Seed phase: a few random configs (seeded → deterministic), warm-started from the
147
+ # most recent cached key's best, to give the TPE an initial good/bad split.
148
+ n_startup = max(2, min(self.n_startup_trials, self.n_trials))
149
+ order = list(range(len(configs)))
150
+ random.Random(0).shuffle(order)
151
+ warm_idx = self._warm_start_index(configs)
152
+ if warm_idx is not None:
153
+ order = [warm_idx] + [i for i in order if i != warm_idx]
154
+ for idx in order[:n_startup]:
155
+ bench_idx(idx)
156
+
157
+ # TPE: split measured configs into good (top-gamma) / bad, build per-dimension value
158
+ # densities for each, and bench the unmeasured config maximizing log l(x) - log g(x)
159
+ # (Expected-Improvement proxy), updating the model after each measurement.
160
+ while len(timings) < self.n_trials:
161
+ ranked = sorted(timings, key=timings.get)
162
+ n_good = max(1, round(self.gamma * len(ranked)))
163
+ good_c: Dict = defaultdict(lambda: defaultdict(int))
164
+ bad_c: Dict = defaultdict(lambda: defaultdict(int))
165
+ for j, i in enumerate(ranked):
166
+ tgt = good_c if j < n_good else bad_c
167
+ for d, v in sigs[i]:
168
+ tgt[d][v] += 1
169
+ n_bad = len(ranked) - n_good
170
+ best_i, best_score = None, -math.inf
171
+ for i in range(len(configs)):
172
+ if i in timings:
173
+ continue
174
+ score = 0.0
175
+ for d, v in sigs[i]:
176
+ V = len(dim_vals[d])
177
+ lp = (good_c[d][v] + 1.0) / (
178
+ n_good + V
179
+ ) # P(v | good), Laplace-smoothed
180
+ gp = (bad_c[d][v] + 1.0) / (n_bad + V) # P(v | bad)
181
+ score += math.log(lp) - math.log(gp)
182
+ if score > best_score:
183
+ best_score, best_i = score, i
184
+ if best_i is None:
185
+ break
186
+ bench_idx(best_i)
187
+
188
+ # Coordinate-descent refinement: try single-dim perturbations around
189
+ # the current best until no neighbor improves.
190
+ if self.refine:
191
+ for _ in range(self.max_refine_iters):
192
+ best_idx = min(timings, key=timings.get)
193
+ best_sig = sigs[best_idx]
194
+ best_ms = timings[best_idx]
195
+ improved = False
196
+ for i, s in enumerate(sigs):
197
+ if i in timings:
198
+ continue
199
+ diff = sum(1 for (_, a), (_, b) in zip(best_sig, s) if a != b)
200
+ if diff != 1:
201
+ continue
202
+ if bench_idx(i) < best_ms:
203
+ improved = True
204
+ if not improved:
205
+ break
206
+
207
+ self.configs_timings = {configs[i]: t for i, t in timings.items()}
208
+ return configs[min(timings, key=timings.get)]
209
+
210
+ def _log_result(self, key, config: Config, ms: float):
211
+ """Append one ``(key, config, ms)`` record as JSONL for offline analysis —
212
+ e.g. pruning configs that are consistently far off the per-key best. ``inf`` ms
213
+ marks a config that failed to compile/run (out of resources, etc.)."""
214
+ if not self.log_path:
215
+ return
216
+ try:
217
+ rec = {
218
+ "timestamp": time.time(),
219
+ "fn_name": getattr(self.fn, "__name__", str(self.fn)),
220
+ "key": list(key),
221
+ "kwargs": config.kwargs,
222
+ "num_warps": config.num_warps,
223
+ "num_stages": config.num_stages,
224
+ "ms": ms,
225
+ }
226
+ with open(self.log_path, "a") as f:
227
+ f.write(json.dumps(rec, default=str) + "\n")
228
+ except Exception:
229
+ pass
230
+
231
+ def _warm_start_index(self, configs: List[Config]):
232
+ """Return the index in ``configs`` matching the most recently cached
233
+ key's best config (or ``None`` if no prior tune or no match in the
234
+ current pruned list)."""
235
+ if not self.cache:
236
+ return None
237
+ # Python 3.7+ dicts preserve insertion order; last entry = most recent tune.
238
+ prev_best = next(reversed(self.cache.values()))
239
+ prev_kwargs = prev_best.all_kwargs()
240
+ for i, c in enumerate(configs):
241
+ if c.all_kwargs() == prev_kwargs:
242
+ return i
243
+ return None
244
+
245
+ def check_disk_cache(self, tuning_key, configs, bench_fn):
246
+ """Persist the tuned best config to Triton's on-disk cache so a later run (or process)
247
+ skips the whole minutes-long search+compile — most of which is Triton codegen, not
248
+ benching. Keyed exactly like Triton's own autotune cache (triton version + backend +
249
+ kernel source hash + invalidating env + key + the full config grid), so any of those
250
+ changing re-tunes.
251
+
252
+ Unlike Triton's stock version we do NOT bail when configs carry a ``pre_hook`` (ours
253
+ always do): we persist each config's ``all_kwargs()`` — serializable and pre_hook-free —
254
+ and on a hit re-match it to the live ``Config`` object, so the pre_hook is preserved from
255
+ code rather than lost in (de)serialization. A hit launches only the winning config."""
256
+ if not tuning_key:
257
+ bench_fn()
258
+ return False
259
+
260
+ from triton.compiler.compiler import make_backend
261
+
262
+ fn = self.fn
263
+ while not isinstance(fn, JITFunction):
264
+ fn = fn.fn
265
+ env_vars = get_cache_invalidating_env_vars()
266
+ cache_key = [
267
+ triton_key(),
268
+ make_backend(driver.active.get_current_target()).hash(),
269
+ fn.cache_key,
270
+ str(sorted(env_vars.items())),
271
+ str(tuning_key),
272
+ ] + [str(c) for c in configs] # str(Config) is pre_hook-free and process-stable
273
+ cache_key = hashlib.sha256("-".join(cache_key).encode("utf-8")).hexdigest()
274
+ cache = get_cache_manager(cache_key)
275
+ file_name = f"{fn.__name__[:150]}.bayes_autotune.json"
276
+
277
+ # signature -> live Config (carries the pre_hook); used to re-match the cached winner
278
+ by_sig = {tuple(sorted(c.all_kwargs().items())): c for c in configs}
279
+ path = cache.get_file(file_name)
280
+ if path:
281
+ try:
282
+ with open(path) as f:
283
+ data = json.load(f)
284
+ best = by_sig.get(tuple(sorted(data["best"].items())))
285
+ if best is not None:
286
+ self.cache[tuning_key] = best
287
+ self.configs_timings = {
288
+ by_sig[s]: t
289
+ for kw, t in data["timings"]
290
+ for s in (tuple(sorted(kw.items())),)
291
+ if s in by_sig
292
+ }
293
+ return True
294
+ except Exception:
295
+ pass # corrupt/stale cache file → fall through and re-tune
296
+
297
+ bench_fn()
298
+ try:
299
+ cache.put(
300
+ json.dumps(
301
+ {
302
+ "best": self.cache[tuning_key].all_kwargs(),
303
+ "timings": [
304
+ (c.all_kwargs(), t) for c, t in self.configs_timings.items()
305
+ ],
306
+ }
307
+ ),
308
+ file_name,
309
+ binary=False,
310
+ )
311
+ except Exception:
312
+ pass
313
+ return False
314
+
315
+
316
+ def bayesian_autotune(
317
+ configs,
318
+ key,
319
+ *,
320
+ n_trials: int = 80,
321
+ n_startup_trials: int = 12,
322
+ gamma: float = 0.25,
323
+ refine: bool = True,
324
+ max_refine_iters: int = 5,
325
+ log_path: str | None = None,
326
+ cache_results: bool = True,
327
+ reset_to_zero=None,
328
+ restore_value=None,
329
+ **kwargs,
330
+ ):
331
+ """Decorator mirroring ``@triton.autotune``. Extra kwargs:
332
+ n_trials: total configs benched per key (TPE budget)
333
+ n_startup_trials: random seed configs before the TPE model kicks in
334
+ gamma: top fraction of measured configs treated as "good"
335
+ refine, max_refine_iters: coordinate-descent refinement after the TPE
336
+ log_path: JSONL of benched configs (or FINEGRAINED_AUTOTUNE_LOG)
337
+ cache_results: persist the tuned best config to disk (on by default) so later
338
+ runs skip the search+compile — see BayesianAutotuner.check_disk_cache"""
339
+
340
+ def decorator(fn):
341
+ return BayesianAutotuner(
342
+ fn,
343
+ fn.arg_names,
344
+ configs,
345
+ key,
346
+ reset_to_zero,
347
+ restore_value,
348
+ n_trials=n_trials,
349
+ n_startup_trials=n_startup_trials,
350
+ gamma=gamma,
351
+ refine=refine,
352
+ max_refine_iters=max_refine_iters,
353
+ log_path=log_path,
354
+ cache_results=cache_results,
355
+ **kwargs,
356
+ )
357
+
358
+ return decorator
build/torch-cuda/fused_batched.py ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Fused batched MoE: two deterministic kernels (no atomics, no sort), per routed token.
16
+
17
+ Kernel 1 (S x N-tiles): gather -> gate_up GEMM -> SiLU -> FP8 requant -> fp8 intermediate
18
+ Kernel 2 (S x H-tiles): fp8 intermediate -> down GEMM -> routing weight -> output
19
+
20
+ Recipe-named to mirror ``batched.py``; ``moe_fused_batched`` is the neutral dispatcher.
21
+ """
22
+
23
+ import torch
24
+ import triton
25
+ import triton.language as tl
26
+ from torch.library import triton_op, wrap_triton
27
+
28
+ from ._ops import add_op_namespace_prefix, ops
29
+ from .utils import (
30
+ FP8_DTYPE,
31
+ MX_SCALE_GROUP_K,
32
+ NIBBLES_PER_BYTE,
33
+ DECODE_BLOCK_SIZE_M,
34
+ device_context,
35
+ fp8_act_quant_inline,
36
+ get_mxfp_autotuning_configs,
37
+ is_mxfp,
38
+ is_mxfp4,
39
+ mxfp_act_quant_inline,
40
+ mx_compute,
41
+ mx_compute_gate_up,
42
+ glu,
43
+ topk_reduce_kernel,
44
+ TOPK_REDUCE_BLOCK_H,
45
+ e2m1_as_uint8,
46
+ ue8m0_as_uint8,
47
+ )
48
+ from .batched import store_row
49
+ from .bayesian_autotuner import bayesian_autotune
50
+
51
+
52
+ # ── Batched fused: two-kernel approach (no sorting, no atomics) ──────────────
53
+ #
54
+ # Same two-kernel architecture as grouped fused but with per-token dispatch:
55
+ # Kernel 1: (S, N-tiles) — gate_up + SiLU + FP8 quant → intermediate buffer
56
+ # Kernel 2: (S, H-tiles) — fp8 intermediate → down proj → output
57
+ # No sorting needed — expert lookup is per-token via ExpertIds.
58
+
59
+
60
+ @triton.autotune(
61
+ configs=[
62
+ triton.Config({}, num_warps=w, num_stages=s)
63
+ for w in [2, 4, 8, 16]
64
+ for s in [2, 3, 4, 5, 6]
65
+ ],
66
+ # num_routed_tokens (= grid axis-0, runtime) is in the key so decode re-tunes per
67
+ # batch size — a config tuned at S=8 is wrong at S=256 (GPU unsaturated at S=8). The
68
+ # constexpr dims auto-partition the cache, so they don't need to be in the key.
69
+ key=["num_routed_tokens"],
70
+ )
71
+ @triton.jit
72
+ def w8a8_block_dynamic_fp8_moe_batched_gate_up_kernel(
73
+ HiddenStates,
74
+ GateUp,
75
+ GateUpScale,
76
+ Intermediate,
77
+ IntermediateScale,
78
+ ExpertIds,
79
+ stride_a_m,
80
+ stride_a_k,
81
+ stride_gu_e,
82
+ stride_gu_n,
83
+ stride_gu_k,
84
+ stride_gus_e,
85
+ stride_gus_n,
86
+ stride_gus_k,
87
+ num_routed_tokens, # tuning knob: in the autotune key so decode re-tunes per batch
88
+ NUM_EXPERTS: tl.constexpr,
89
+ NUM_TOP_K: tl.constexpr,
90
+ HIDDEN_DIM: tl.constexpr,
91
+ NUM_N_TILES: tl.constexpr,
92
+ INTERMEDIATE_DIM: tl.constexpr,
93
+ BLOCK_SIZE_N: tl.constexpr,
94
+ BLOCK_SIZE_K: tl.constexpr,
95
+ BLOCK_SIZE_M: tl.constexpr,
96
+ SIMULATE_UNFUSED: tl.constexpr,
97
+ ACT_FN: tl.constexpr,
98
+ ):
99
+ """Batched kernel 1: per-token gate_up + SiLU + FP8 quant. Grid: (S, N-tiles)."""
100
+ batch_id = tl.program_id(axis=0)
101
+ pid_n = tl.program_id(axis=1)
102
+
103
+ # Each token is replicated NUM_TOP_K times in the routed (S,) layout, so the
104
+ # source row is batch_id // NUM_TOP_K — gathered from the unexpanded activations
105
+ # (no token-index tensor, no top_k-replicated copy). int64 against offset overflow.
106
+ token = (batch_id // NUM_TOP_K).to(tl.int64)
107
+ expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
108
+ # EP sentinel: row routed to a non-local expert — skip. The intermediate row is left
109
+ # uninit; the down kernel writes zeros for this row, so the output stays well-defined.
110
+ if expert_id >= NUM_EXPERTS:
111
+ return
112
+ a_ptr = (
113
+ HiddenStates
114
+ + (token + tl.arange(0, BLOCK_SIZE_M))[:, None] * stride_a_m
115
+ + tl.arange(0, BLOCK_SIZE_K)[None, :] * stride_a_k
116
+ )
117
+ b_gate_ptr = (
118
+ GateUp
119
+ + expert_id * stride_gu_e
120
+ + tl.arange(0, BLOCK_SIZE_K)[:, None] * stride_gu_k
121
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[None, :] * stride_gu_n
122
+ )
123
+ b_up_ptr = (
124
+ GateUp
125
+ + expert_id * stride_gu_e
126
+ + tl.arange(0, BLOCK_SIZE_K)[:, None] * stride_gu_k
127
+ + (INTERMEDIATE_DIM + pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[
128
+ None, :
129
+ ]
130
+ * stride_gu_n
131
+ )
132
+ bs_gate_ptr = GateUpScale + expert_id * stride_gus_e + pid_n * stride_gus_n
133
+ bs_up_ptr = (
134
+ GateUpScale + expert_id * stride_gus_e + (NUM_N_TILES + pid_n) * stride_gus_n
135
+ )
136
+
137
+ acc_gate = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
138
+ acc_up = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
139
+
140
+ for _ in range(0, tl.cdiv(HIDDEN_DIM, BLOCK_SIZE_K)):
141
+ a_raw = tl.load(a_ptr).to(tl.float32)
142
+ a, a_s = fp8_act_quant_inline(a_raw)
143
+
144
+ w_gate = tl.load(b_gate_ptr)
145
+ w_up = tl.load(b_up_ptr)
146
+ w_s_gate = tl.load(bs_gate_ptr)
147
+ w_s_up = tl.load(bs_up_ptr)
148
+
149
+ acc_gate += tl.dot(a, w_gate) * a_s[:, None] * w_s_gate
150
+ acc_up += tl.dot(a, w_up) * a_s[:, None] * w_s_up
151
+
152
+ a_ptr += BLOCK_SIZE_K * stride_a_k
153
+ b_gate_ptr += BLOCK_SIZE_K * stride_gu_k
154
+ b_up_ptr += BLOCK_SIZE_K * stride_gu_k
155
+ bs_gate_ptr += stride_gus_k
156
+ bs_up_ptr += stride_gus_k
157
+
158
+ intermediate = glu(
159
+ acc_gate, acc_up, ACT_FN, SIMULATE_UNFUSED, HiddenStates.dtype.element_ty
160
+ )
161
+
162
+ # Requant the intermediate to FP8 — the same inline per-row act quant as the inputs;
163
+ # with BLOCK_SIZE_M=1 the per-row scale is the single per-tile scalar we store.
164
+ inter, inter_s = fp8_act_quant_inline(intermediate)
165
+ store_row(
166
+ Intermediate + batch_id * INTERMEDIATE_DIM,
167
+ inter,
168
+ pid_n,
169
+ 1,
170
+ BLOCK_SIZE_M,
171
+ BLOCK_SIZE_N,
172
+ )
173
+ tl.store(IntermediateScale + batch_id * NUM_N_TILES + pid_n, tl.max(inter_s))
174
+
175
+
176
+ @triton.autotune(
177
+ configs=[
178
+ triton.Config({}, num_warps=w, num_stages=s)
179
+ for w in [2, 4, 8, 16]
180
+ for s in [2, 3, 4, 5, 6]
181
+ ],
182
+ key=["num_routed_tokens"],
183
+ )
184
+ @triton.jit
185
+ def w8a8_block_dynamic_fp8_moe_batched_down_kernel(
186
+ Intermediate,
187
+ IntermediateScale,
188
+ Down,
189
+ DownScale,
190
+ ExpertIds,
191
+ SampleWeights,
192
+ Out,
193
+ stride_down_e,
194
+ stride_down_n,
195
+ stride_down_k,
196
+ stride_downs_e,
197
+ stride_downs_n,
198
+ stride_downs_k,
199
+ num_routed_tokens, # tuning knob: in the autotune key so decode re-tunes per batch
200
+ NUM_EXPERTS: tl.constexpr,
201
+ HIDDEN_DIM: tl.constexpr,
202
+ INTERMEDIATE_DIM: tl.constexpr,
203
+ BLOCK_SIZE_N: tl.constexpr,
204
+ BLOCK_SIZE_H: tl.constexpr,
205
+ BLOCK_SIZE_M: tl.constexpr,
206
+ NUM_N_TILES: tl.constexpr,
207
+ SIMULATE_UNFUSED: tl.constexpr,
208
+ ):
209
+ """Batched kernel 2: fp8 intermediate → down proj → output. Grid: (S, H-tiles)."""
210
+ batch_id = tl.program_id(axis=0)
211
+ pid_h = tl.program_id(axis=1)
212
+
213
+ expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
214
+ # EP sentinel: row routed to a non-local expert. The program is already launched, so
215
+ # write its zero tile here (skipping the weight load) — cheaper than a host-side mask
216
+ # pass, and it leaves the output fully defined for a plain top-k sum.
217
+ if expert_id >= NUM_EXPERTS:
218
+ z = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_H), dtype=tl.float32)
219
+ store_row(Out + batch_id * HIDDEN_DIM, z, pid_h, 1, BLOCK_SIZE_M, BLOCK_SIZE_H)
220
+ return
221
+
222
+ w_down_ptr = (
223
+ Down
224
+ + expert_id * stride_down_e
225
+ + tl.arange(0, BLOCK_SIZE_N)[:, None] * stride_down_k
226
+ + (pid_h * BLOCK_SIZE_H + tl.arange(0, BLOCK_SIZE_H))[None, :] * stride_down_n
227
+ )
228
+ inter_ptr = (
229
+ Intermediate
230
+ + (batch_id + tl.arange(0, BLOCK_SIZE_M))[:, None] * INTERMEDIATE_DIM
231
+ + tl.arange(0, BLOCK_SIZE_N)[None, :]
232
+ )
233
+ inter_s_ptr = (
234
+ IntermediateScale
235
+ + (batch_id + tl.arange(0, BLOCK_SIZE_M))[:, None] * NUM_N_TILES
236
+ )
237
+ ws_down_ptr = DownScale + expert_id * stride_downs_e + pid_h * stride_downs_n
238
+
239
+ acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_H), dtype=tl.float32)
240
+
241
+ for _ in range(0, NUM_N_TILES):
242
+ inter = tl.load(inter_ptr)
243
+ inter_s = tl.load(inter_s_ptr)
244
+ w_s_down = tl.load(ws_down_ptr)
245
+ w_down = tl.load(w_down_ptr)
246
+ acc += tl.dot(inter, w_down) * inter_s * w_s_down
247
+ inter_ptr += BLOCK_SIZE_N
248
+ inter_s_ptr += 1
249
+ ws_down_ptr += stride_downs_k
250
+ w_down_ptr += BLOCK_SIZE_N * stride_down_k
251
+
252
+ if SIMULATE_UNFUSED:
253
+ acc = acc.to(Out.dtype.element_ty).to(tl.float32)
254
+
255
+ acc = acc * tl.load(SampleWeights + batch_id)
256
+ store_row(Out + batch_id * HIDDEN_DIM, acc, pid_h, 1, BLOCK_SIZE_M, BLOCK_SIZE_H)
257
+
258
+
259
+ @triton_op(
260
+ add_op_namespace_prefix("w8a8_block_dynamic_fp8_moe_batched"), mutates_args=()
261
+ )
262
+ def _w8a8_block_dynamic_fp8_moe_batched(
263
+ hidden_states: torch.Tensor,
264
+ gate_up_proj: torch.Tensor,
265
+ gate_up_proj_scale: torch.Tensor,
266
+ down_proj: torch.Tensor,
267
+ down_proj_scale: torch.Tensor,
268
+ top_k_index: torch.Tensor,
269
+ top_k_weights: torch.Tensor,
270
+ block_size: list[int],
271
+ output_dtype: torch.dtype,
272
+ simulate_unfused: bool = False,
273
+ act_fn: str = "silu",
274
+ ) -> torch.Tensor:
275
+ """Block-dynamic FP8 batched fused MoE in ONE op: gate_up + SiLU + FP8 requant →
276
+ grouped down → routing-weighted per-(token, expert) output. gate_up gathers each routed
277
+ row directly from the unexpanded ``hidden_states`` (source row ``s // NUM_TOP_K``, no
278
+ replicated copy). ``inter``/``inter_scales`` are internal; the caller reduces over top-k."""
279
+ device = hidden_states.device
280
+ HIDDEN_DIM = hidden_states.size(1)
281
+ NUM_EXPERTS = gate_up_proj.size(0)
282
+ num_tokens = hidden_states.size(0)
283
+ num_routed_tokens = top_k_index.numel()
284
+ INTERMEDIATE_DIM = gate_up_proj.size(1) // 2
285
+ NUM_TOP_K = num_routed_tokens // hidden_states.size(0)
286
+ BLOCK_SIZE_N, BLOCK_SIZE_K = block_size
287
+ NUM_N_TILES = triton.cdiv(INTERMEDIATE_DIM, BLOCK_SIZE_N)
288
+ NUM_H_TILES = triton.cdiv(HIDDEN_DIM, BLOCK_SIZE_N)
289
+
290
+ inter = torch.empty(
291
+ num_routed_tokens, INTERMEDIATE_DIM, device=device, dtype=FP8_DTYPE
292
+ )
293
+ inter_scales = torch.empty(
294
+ num_routed_tokens, NUM_N_TILES, device=device, dtype=torch.float32
295
+ )
296
+
297
+ out = torch.empty(num_routed_tokens, HIDDEN_DIM, device=device, dtype=output_dtype)
298
+ reduced = torch.empty(num_tokens, HIDDEN_DIM, device=device, dtype=output_dtype)
299
+ with device_context(device):
300
+ wrap_triton(w8a8_block_dynamic_fp8_moe_batched_gate_up_kernel)[
301
+ (num_routed_tokens, NUM_N_TILES)
302
+ ](
303
+ hidden_states,
304
+ gate_up_proj,
305
+ gate_up_proj_scale,
306
+ inter,
307
+ inter_scales,
308
+ top_k_index,
309
+ hidden_states.stride(0),
310
+ hidden_states.stride(1),
311
+ gate_up_proj.stride(0),
312
+ gate_up_proj.stride(1),
313
+ gate_up_proj.stride(2),
314
+ gate_up_proj_scale.stride(0),
315
+ gate_up_proj_scale.stride(1),
316
+ gate_up_proj_scale.stride(2),
317
+ num_routed_tokens=num_routed_tokens,
318
+ NUM_TOP_K=NUM_TOP_K,
319
+ NUM_EXPERTS=NUM_EXPERTS,
320
+ HIDDEN_DIM=HIDDEN_DIM,
321
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
322
+ BLOCK_SIZE_N=BLOCK_SIZE_N,
323
+ BLOCK_SIZE_K=BLOCK_SIZE_K,
324
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
325
+ NUM_N_TILES=NUM_N_TILES,
326
+ SIMULATE_UNFUSED=simulate_unfused,
327
+ ACT_FN=act_fn,
328
+ )
329
+ wrap_triton(w8a8_block_dynamic_fp8_moe_batched_down_kernel)[
330
+ (num_routed_tokens, NUM_H_TILES)
331
+ ](
332
+ inter,
333
+ inter_scales,
334
+ down_proj,
335
+ down_proj_scale,
336
+ top_k_index,
337
+ top_k_weights,
338
+ out,
339
+ down_proj.stride(0),
340
+ down_proj.stride(1),
341
+ down_proj.stride(2),
342
+ down_proj_scale.stride(0),
343
+ down_proj_scale.stride(1),
344
+ down_proj_scale.stride(2),
345
+ num_routed_tokens=num_routed_tokens,
346
+ NUM_EXPERTS=NUM_EXPERTS,
347
+ HIDDEN_DIM=HIDDEN_DIM,
348
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
349
+ BLOCK_SIZE_N=BLOCK_SIZE_N,
350
+ BLOCK_SIZE_H=BLOCK_SIZE_N,
351
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
352
+ NUM_N_TILES=NUM_N_TILES,
353
+ SIMULATE_UNFUSED=simulate_unfused,
354
+ )
355
+ wrap_triton(topk_reduce_kernel)[
356
+ (num_tokens, triton.cdiv(HIDDEN_DIM, TOPK_REDUCE_BLOCK_H))
357
+ ](
358
+ out,
359
+ reduced,
360
+ HIDDEN_DIM,
361
+ out.stride(0),
362
+ out.stride(1),
363
+ reduced.stride(0),
364
+ reduced.stride(1),
365
+ NUM_TOP_K=NUM_TOP_K,
366
+ BLOCK_H=TOPK_REDUCE_BLOCK_H,
367
+ )
368
+
369
+ return reduced
370
+
371
+
372
+ def w8a8_block_dynamic_fp8_moe_batched(
373
+ hidden_states: torch.Tensor,
374
+ top_k_index: torch.Tensor,
375
+ top_k_weights: torch.Tensor,
376
+ gate_up_proj: torch.Tensor,
377
+ down_proj: torch.Tensor,
378
+ gate_up_proj_scale: torch.Tensor,
379
+ down_proj_scale: torch.Tensor,
380
+ block_size: list[int],
381
+ simulate_unfused: bool = False,
382
+ act_fn: str = "silu",
383
+ ) -> torch.Tensor:
384
+ """Batched fused MoE (deterministic, no sorting, no atomics): a single ``triton_op``
385
+ runs gate_up + down (gathering each routed row from the unexpanded ``hidden_states``,
386
+ source row ``s // num_top_k``, no replicated copy). The top-k reduce stays plain torch
387
+ so ``torch.compile`` can fuse it with the surrounding model graph."""
388
+
389
+ out = ops.w8a8_block_dynamic_fp8_moe_batched(
390
+ hidden_states,
391
+ gate_up_proj,
392
+ gate_up_proj_scale,
393
+ down_proj,
394
+ down_proj_scale,
395
+ top_k_index,
396
+ top_k_weights,
397
+ block_size,
398
+ hidden_states.dtype,
399
+ simulate_unfused,
400
+ act_fn,
401
+ )
402
+ return out
403
+
404
+
405
+ # ── MXFP8 fused (tl.dot_scaled, UE8M0 group-32 scales, tunable tiles) ─────────
406
+ #
407
+ # Mirrors the block-dynamic kernels but: activations/intermediate quantize via the MX
408
+ # group-32 inline quant, weights/scales feed tl.dot_scaled, and the tile (BLOCK_SIZE_N/K)
409
+ # is free to autotune — the group-32 scale runs along K independently of the compute
410
+ # tile (the whole reason MXFP8 can tune the tile that the block recipe must lock to 128).
411
+
412
+
413
+ @bayesian_autotune(
414
+ get_mxfp_autotuning_configs(),
415
+ ["num_routed_tokens"],
416
+ n_trials=60,
417
+ )
418
+ @triton.jit
419
+ def mxfp_dynamic_moe_batched_gate_up_kernel(
420
+ HiddenStates,
421
+ GateUp,
422
+ GateUpScale,
423
+ Intermediate,
424
+ IntermediateScale,
425
+ ExpertIds,
426
+ stride_a_m,
427
+ stride_a_k,
428
+ stride_gu_e,
429
+ stride_gu_n,
430
+ stride_gu_k,
431
+ stride_gus_e,
432
+ stride_gus_n,
433
+ stride_gus_k,
434
+ num_routed_tokens, # tuning knob: in the autotune key so decode re-tunes per batch
435
+ NUM_EXPERTS: tl.constexpr,
436
+ NUM_TOP_K: tl.constexpr,
437
+ HIDDEN_DIM: tl.constexpr,
438
+ INTERMEDIATE_DIM: tl.constexpr,
439
+ BLOCK_SIZE_N: tl.constexpr,
440
+ BLOCK_SIZE_K: tl.constexpr,
441
+ BLOCK_SIZE_M: tl.constexpr,
442
+ VALUES_PER_BYTE: tl.constexpr,
443
+ SCALE_GROUP_K: tl.constexpr,
444
+ SIMULATE_UNFUSED: tl.constexpr,
445
+ COMPUTE_MODE: tl.constexpr,
446
+ ACT_FN: tl.constexpr,
447
+ ):
448
+ """MXFP4/MXFP8 kernel 1: gate_up + SiLU + MXFP8 requant. N = intermediate
449
+ (output) tile, K = hidden (contraction) tile — both tunable. Grid: (S, N-tiles)."""
450
+ batch_id = tl.program_id(axis=0)
451
+ pid_n = tl.program_id(axis=1)
452
+ token = (batch_id // NUM_TOP_K).to(tl.int64)
453
+ expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
454
+ if expert_id >= NUM_EXPERTS: # EP sentinel; down kernel zeros the output row
455
+ return
456
+
457
+ a_ptr = (
458
+ HiddenStates
459
+ + (token + tl.arange(0, BLOCK_SIZE_M))[:, None] * stride_a_m
460
+ + tl.arange(0, BLOCK_SIZE_K)[None, :] * stride_a_k
461
+ )
462
+ b_gate_ptr = (
463
+ GateUp
464
+ + expert_id * stride_gu_e
465
+ + tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)[:, None] * stride_gu_k
466
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[None, :] * stride_gu_n
467
+ )
468
+ b_up_ptr = (
469
+ GateUp
470
+ + expert_id * stride_gu_e
471
+ + tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)[:, None] * stride_gu_k
472
+ + (INTERMEDIATE_DIM + pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[
473
+ None, :
474
+ ]
475
+ * stride_gu_n
476
+ )
477
+ bs_gate_ptr = (
478
+ GateUpScale
479
+ + expert_id * stride_gus_e
480
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[:, None] * stride_gus_n
481
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, :] * stride_gus_k
482
+ )
483
+ bs_up_ptr = (
484
+ GateUpScale
485
+ + expert_id * stride_gus_e
486
+ + (INTERMEDIATE_DIM + pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[
487
+ :, None
488
+ ]
489
+ * stride_gus_n
490
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, :] * stride_gus_k
491
+ )
492
+
493
+ acc_gate = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
494
+ acc_up = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
495
+ for _ in range(0, tl.cdiv(HIDDEN_DIM, BLOCK_SIZE_K)):
496
+ a_raw = tl.load(a_ptr).to(tl.float32)
497
+ a, a_scale = mxfp_act_quant_inline(
498
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K
499
+ )
500
+ b_gate = tl.load(b_gate_ptr)
501
+ b_up = tl.load(b_up_ptr)
502
+ bs_gate = tl.load(bs_gate_ptr)
503
+ bs_up = tl.load(bs_up_ptr)
504
+ acc_gate, acc_up = mx_compute_gate_up(
505
+ acc_gate,
506
+ acc_up,
507
+ a,
508
+ a_scale,
509
+ b_gate,
510
+ b_up,
511
+ bs_gate,
512
+ bs_up,
513
+ COMPUTE_MODE,
514
+ VALUES_PER_BYTE,
515
+ BLOCK_SIZE_M,
516
+ BLOCK_SIZE_N,
517
+ BLOCK_SIZE_K,
518
+ SCALE_GROUP_K,
519
+ )
520
+ a_ptr += BLOCK_SIZE_K * stride_a_k
521
+ b_gate_ptr += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_gu_k
522
+ b_up_ptr += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_gu_k
523
+ bs_gate_ptr += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_gus_k
524
+ bs_up_ptr += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_gus_k
525
+
526
+ intermediate = glu(
527
+ acc_gate, acc_up, ACT_FN, SIMULATE_UNFUSED, HiddenStates.dtype.element_ty
528
+ )
529
+
530
+ # MXFP8 requant of the intermediate (E4M3 + UE8M0 group-32 along this N-tile).
531
+ inter, inter_scale = mxfp_act_quant_inline(
532
+ intermediate, BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K
533
+ )
534
+ store_row(
535
+ Intermediate + batch_id * INTERMEDIATE_DIM,
536
+ inter,
537
+ pid_n,
538
+ 1,
539
+ BLOCK_SIZE_M,
540
+ BLOCK_SIZE_N,
541
+ )
542
+ offs_sc = pid_n * (BLOCK_SIZE_N // SCALE_GROUP_K) + tl.arange(
543
+ 0, BLOCK_SIZE_N // SCALE_GROUP_K
544
+ )
545
+ sc_ptrs = (
546
+ IntermediateScale
547
+ + batch_id * (INTERMEDIATE_DIM // SCALE_GROUP_K)
548
+ + offs_sc[None, :]
549
+ )
550
+ tl.store(sc_ptrs, inter_scale)
551
+
552
+
553
+ @bayesian_autotune(
554
+ get_mxfp_autotuning_configs(),
555
+ ["num_routed_tokens"],
556
+ n_trials=60,
557
+ )
558
+ @triton.jit
559
+ def mxfp_dynamic_moe_batched_down_kernel(
560
+ Intermediate,
561
+ IntermediateScale,
562
+ Down,
563
+ DownScale,
564
+ ExpertIds,
565
+ SampleWeights,
566
+ Out,
567
+ stride_down_e,
568
+ stride_down_n,
569
+ stride_down_k,
570
+ stride_downs_e,
571
+ stride_downs_n,
572
+ stride_downs_k,
573
+ num_routed_tokens, # tuning knob: in the autotune key so decode re-tunes per batch
574
+ NUM_EXPERTS: tl.constexpr,
575
+ HIDDEN_DIM: tl.constexpr,
576
+ INTERMEDIATE_DIM: tl.constexpr,
577
+ BLOCK_SIZE_N: tl.constexpr,
578
+ BLOCK_SIZE_K: tl.constexpr,
579
+ BLOCK_SIZE_M: tl.constexpr,
580
+ VALUES_PER_BYTE: tl.constexpr,
581
+ SCALE_GROUP_K: tl.constexpr,
582
+ SIMULATE_UNFUSED: tl.constexpr,
583
+ COMPUTE_MODE: tl.constexpr,
584
+ ):
585
+ """MXFP4/MXFP8 kernel 2: MXFP8 intermediate ��� down proj. N = hidden
586
+ (output) tile, K = intermediate (contraction) tile. Grid: (S, H-tiles)."""
587
+ batch_id = tl.program_id(axis=0)
588
+ pid_n = tl.program_id(axis=1)
589
+ expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
590
+ if expert_id >= NUM_EXPERTS: # EP sentinel: zero the output row for the top-k sum
591
+ z = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
592
+ store_row(Out + batch_id * HIDDEN_DIM, z, pid_n, 1, BLOCK_SIZE_M, BLOCK_SIZE_N)
593
+ return
594
+
595
+ a_ptr = (
596
+ Intermediate
597
+ + (batch_id + tl.arange(0, BLOCK_SIZE_M))[:, None] * INTERMEDIATE_DIM
598
+ + tl.arange(0, BLOCK_SIZE_K)[None, :]
599
+ )
600
+ as_ptr = (
601
+ IntermediateScale
602
+ + (batch_id + tl.arange(0, BLOCK_SIZE_M))[:, None]
603
+ * (INTERMEDIATE_DIM // SCALE_GROUP_K)
604
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, :]
605
+ )
606
+ w_down_ptr = (
607
+ Down
608
+ + expert_id * stride_down_e
609
+ + tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)[:, None] * stride_down_k
610
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[None, :] * stride_down_n
611
+ )
612
+ ws_down_ptr = (
613
+ DownScale
614
+ + expert_id * stride_downs_e
615
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[:, None] * stride_downs_n
616
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, :] * stride_downs_k
617
+ )
618
+
619
+ acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
620
+ for _ in range(0, tl.cdiv(INTERMEDIATE_DIM, BLOCK_SIZE_K)):
621
+ a = tl.load(a_ptr)
622
+ a_scale = tl.load(as_ptr)
623
+ w = tl.load(w_down_ptr)
624
+ w_scale = tl.load(ws_down_ptr)
625
+ acc = mx_compute(
626
+ acc,
627
+ a,
628
+ a_scale,
629
+ w,
630
+ w_scale,
631
+ COMPUTE_MODE,
632
+ VALUES_PER_BYTE,
633
+ BLOCK_SIZE_M,
634
+ BLOCK_SIZE_N,
635
+ BLOCK_SIZE_K,
636
+ SCALE_GROUP_K,
637
+ )
638
+ a_ptr += BLOCK_SIZE_K
639
+ as_ptr += BLOCK_SIZE_K // SCALE_GROUP_K
640
+ w_down_ptr += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_down_k
641
+ ws_down_ptr += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_downs_k
642
+
643
+ if SIMULATE_UNFUSED:
644
+ acc = acc.to(Out.dtype.element_ty).to(tl.float32)
645
+ acc = acc * tl.load(SampleWeights + batch_id)
646
+ store_row(Out + batch_id * HIDDEN_DIM, acc, pid_n, 1, BLOCK_SIZE_M, BLOCK_SIZE_N)
647
+
648
+
649
+ @triton_op(add_op_namespace_prefix("mxfp_dynamic_moe_batched"), mutates_args=())
650
+ def _mxfp_dynamic_moe_batched(
651
+ hidden_states: torch.Tensor,
652
+ gate_up_proj: torch.Tensor,
653
+ gate_up_proj_scale: torch.Tensor,
654
+ down_proj: torch.Tensor,
655
+ down_proj_scale: torch.Tensor,
656
+ top_k_index: torch.Tensor,
657
+ top_k_weights: torch.Tensor,
658
+ output_dtype: torch.dtype,
659
+ simulate_unfused: bool = False,
660
+ act_fn: str = "silu",
661
+ ) -> torch.Tensor:
662
+ """MXFP4/MXFP8 batched fused MoE in ONE op: gate_up + SiLU + MXFP8 requant → grouped
663
+ down → routing-weighted per-(token, expert) output. gate_up and down must share the same MX
664
+ format. ``inter``/``inter_scales`` are internal; caller reduces over top-k."""
665
+ gate_up_is_fp4 = is_mxfp4(gate_up_proj, gate_up_proj_scale)
666
+ down_is_fp4 = is_mxfp4(down_proj, down_proj_scale)
667
+ if gate_up_is_fp4 != down_is_fp4:
668
+ raise ValueError(
669
+ "gate_up_proj and down_proj must use the same MX format (both MXFP4 or both MXFP8)."
670
+ )
671
+
672
+ device = hidden_states.device
673
+ HIDDEN_DIM = hidden_states.size(1)
674
+ NUM_EXPERTS = gate_up_proj.size(0)
675
+ num_tokens = hidden_states.size(0)
676
+ num_routed_tokens = top_k_index.numel()
677
+ INTERMEDIATE_DIM = gate_up_proj.size(1) // 2
678
+ NUM_TOP_K = num_routed_tokens // hidden_states.size(0)
679
+ VALUES_PER_BYTE = NIBBLES_PER_BYTE if gate_up_is_fp4 else 1
680
+ gate_up_proj_u8 = e2m1_as_uint8(gate_up_proj)
681
+ gate_up_proj_scale_u8 = ue8m0_as_uint8(gate_up_proj_scale)
682
+ down_proj_u8 = e2m1_as_uint8(down_proj)
683
+ down_proj_scale_u8 = ue8m0_as_uint8(down_proj_scale)
684
+
685
+ inter = torch.empty(
686
+ num_routed_tokens, INTERMEDIATE_DIM, device=device, dtype=FP8_DTYPE
687
+ )
688
+ inter_scales = torch.empty(
689
+ num_routed_tokens,
690
+ INTERMEDIATE_DIM // MX_SCALE_GROUP_K,
691
+ device=device,
692
+ dtype=torch.uint8,
693
+ )
694
+ out = torch.empty(num_routed_tokens, HIDDEN_DIM, device=device, dtype=output_dtype)
695
+ reduced = torch.empty(num_tokens, HIDDEN_DIM, device=device, dtype=output_dtype)
696
+
697
+ def gate_up_grid(META):
698
+ return (num_routed_tokens, triton.cdiv(INTERMEDIATE_DIM, META["BLOCK_SIZE_N"]))
699
+
700
+ def down_grid(META):
701
+ return (num_routed_tokens, triton.cdiv(HIDDEN_DIM, META["BLOCK_SIZE_N"]))
702
+
703
+ with device_context(device):
704
+ wrap_triton(mxfp_dynamic_moe_batched_gate_up_kernel)[gate_up_grid](
705
+ hidden_states,
706
+ gate_up_proj_u8,
707
+ gate_up_proj_scale_u8,
708
+ inter,
709
+ inter_scales,
710
+ top_k_index,
711
+ hidden_states.stride(0),
712
+ hidden_states.stride(1),
713
+ gate_up_proj_u8.stride(0),
714
+ gate_up_proj_u8.stride(1),
715
+ gate_up_proj_u8.stride(2),
716
+ gate_up_proj_scale_u8.stride(0),
717
+ gate_up_proj_scale_u8.stride(1),
718
+ gate_up_proj_scale_u8.stride(2),
719
+ num_routed_tokens=num_routed_tokens,
720
+ NUM_TOP_K=NUM_TOP_K,
721
+ NUM_EXPERTS=NUM_EXPERTS,
722
+ HIDDEN_DIM=HIDDEN_DIM,
723
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
724
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
725
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
726
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
727
+ SIMULATE_UNFUSED=simulate_unfused,
728
+ ACT_FN=act_fn,
729
+ )
730
+ wrap_triton(mxfp_dynamic_moe_batched_down_kernel)[down_grid](
731
+ inter,
732
+ inter_scales,
733
+ down_proj_u8,
734
+ down_proj_scale_u8,
735
+ top_k_index,
736
+ top_k_weights,
737
+ out,
738
+ down_proj_u8.stride(0),
739
+ down_proj_u8.stride(1),
740
+ down_proj_u8.stride(2),
741
+ down_proj_scale_u8.stride(0),
742
+ down_proj_scale_u8.stride(1),
743
+ down_proj_scale_u8.stride(2),
744
+ num_routed_tokens=num_routed_tokens,
745
+ NUM_EXPERTS=NUM_EXPERTS,
746
+ HIDDEN_DIM=HIDDEN_DIM,
747
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
748
+ BLOCK_SIZE_M=DECODE_BLOCK_SIZE_M,
749
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
750
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
751
+ SIMULATE_UNFUSED=simulate_unfused,
752
+ )
753
+ wrap_triton(topk_reduce_kernel)[
754
+ (num_tokens, triton.cdiv(HIDDEN_DIM, TOPK_REDUCE_BLOCK_H))
755
+ ](
756
+ out,
757
+ reduced,
758
+ HIDDEN_DIM,
759
+ out.stride(0),
760
+ out.stride(1),
761
+ reduced.stride(0),
762
+ reduced.stride(1),
763
+ NUM_TOP_K=NUM_TOP_K,
764
+ BLOCK_H=TOPK_REDUCE_BLOCK_H,
765
+ )
766
+
767
+ return reduced
768
+
769
+
770
+ def mxfp_dynamic_moe_batched(
771
+ hidden_states: torch.Tensor,
772
+ top_k_index: torch.Tensor,
773
+ top_k_weights: torch.Tensor,
774
+ gate_up_proj: torch.Tensor,
775
+ down_proj: torch.Tensor,
776
+ gate_up_proj_scale: torch.Tensor,
777
+ down_proj_scale: torch.Tensor,
778
+ simulate_unfused: bool = False,
779
+ act_fn: str = "silu",
780
+ ) -> torch.Tensor:
781
+ """Two-kernel batched fused MX MoE — MXFP4 or MXFP8 weights (UE8M0 group-32), the
782
+ format picked per-weight by the ops. Same structure as the block-dynamic path but
783
+ with a tunable tile and an MXFP8 group-32 intermediate; ``block_size`` is unused."""
784
+ out = ops.mxfp_dynamic_moe_batched(
785
+ hidden_states,
786
+ gate_up_proj,
787
+ gate_up_proj_scale,
788
+ down_proj,
789
+ down_proj_scale,
790
+ top_k_index,
791
+ top_k_weights,
792
+ hidden_states.dtype,
793
+ simulate_unfused,
794
+ act_fn,
795
+ )
796
+ return out
797
+
798
+
799
+ # ── Dispatcher ────────────────────────────────────────────────────────────────
800
+
801
+
802
+ def moe_fused_batched(
803
+ hidden_states: torch.Tensor,
804
+ top_k_index: torch.Tensor,
805
+ top_k_weights: torch.Tensor,
806
+ gate_up_proj: torch.Tensor,
807
+ down_proj: torch.Tensor,
808
+ gate_up_proj_scale_inv: torch.Tensor,
809
+ down_proj_scale_inv: torch.Tensor,
810
+ block_size: list[int] | None,
811
+ simulate_unfused: bool = False,
812
+ act_fn: str = "silu",
813
+ ) -> torch.Tensor:
814
+ """Fused batched-MoE dispatcher — routes to the recipe matching the weight dtype /
815
+ scale layout, mirroring ``matmul_batched``. Implemented: block-dynamic FP8 and MXFP8
816
+ (tensor-dynamic) and MXFP4. ``simulate_unfused`` (testing) rounds each step through the
817
+ activation dtype so the output matches the unfused reference to reduce order."""
818
+
819
+ gate_up_is_mx = is_mxfp(gate_up_proj, gate_up_proj_scale_inv)
820
+ down_is_mx = is_mxfp(down_proj, down_proj_scale_inv)
821
+ if gate_up_is_mx != down_is_mx:
822
+ raise ValueError(
823
+ "gate_up_proj and down_proj must use the same recipe (both MX or both block-dynamic FP8)."
824
+ )
825
+
826
+ if gate_up_is_mx:
827
+ return mxfp_dynamic_moe_batched(
828
+ hidden_states,
829
+ top_k_index,
830
+ top_k_weights,
831
+ gate_up_proj,
832
+ down_proj,
833
+ gate_up_proj_scale_inv,
834
+ down_proj_scale_inv,
835
+ simulate_unfused,
836
+ act_fn,
837
+ )
838
+
839
+ if block_size is None:
840
+ raise ValueError("block_size is required for block-dynamic FP8 weights.")
841
+
842
+ return w8a8_block_dynamic_fp8_moe_batched(
843
+ hidden_states,
844
+ top_k_index,
845
+ top_k_weights,
846
+ gate_up_proj,
847
+ down_proj,
848
+ gate_up_proj_scale_inv,
849
+ down_proj_scale_inv,
850
+ block_size,
851
+ simulate_unfused,
852
+ act_fn,
853
+ )
build/torch-cuda/fused_grouped.py ADDED
@@ -0,0 +1,1110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
6
+ """Fused grouped MoE (prefill counterpart to the fused batched/decode path).
7
+
8
+ Two **persistent** kernels over expert-grouped M-tiles, no ``A_sorted`` materialization:
9
+ 1. Routing (on-device): an atomic counting-sort scatter (``_scatter_kernel``, O(S)) →
10
+ ``perm`` / ``perm_token`` + an ``expert_start`` exclusive offset vector — the sort is
11
+ just an index; activations stay unsorted.
12
+ 2. Phase 1 — gate_up, **gathering** each tile's hidden rows via ``perm_token``, + SiLU,
13
+ then **FP8 requant** of the intermediate — same trick as ``fused_batched``: the down
14
+ kernel reads FP8 directly instead of a bf16 round-trip.
15
+ 3. Phase 2 — grouped down over the FP8 intermediate, then **fuses the routing-weight ×
16
+ reorder** (no atomics): each row is scaled by its weight and **scattered** to its flat
17
+ ``(token, slot)`` row via ``perm``, leaving the host only a top-k sum back to (T, H).
18
+
19
+ Scheduling is persistent: a fixed ``NUM_SMS`` programs grid-stride over all tiles. The
20
+ expert-tile layout is built once into registers from ``expert_start`` (``_build_tile_layout``)
21
+ and each tile's owner is resolved inline (``_resolve_tile_inline``) — no padded grid, no
22
+ per-tile binary search, no host-side tile-offset precompute.
23
+
24
+ Recipe-named to mirror ``fused_batched``: ``w8a8_block_dynamic_fp8_moe_grouped`` (128x128
25
+ block scales, FP8 intermediate) and ``mxfp_dynamic_moe_grouped`` (MXFP4/MXFP8, UE8M0
26
+ group-32 scales, MXFP8 intermediate); ``moe_fused_grouped`` is the neutral dispatcher.
27
+ """
28
+
29
+ import torch
30
+ import triton
31
+ import triton.language as tl
32
+ from triton.tools.tensor_descriptor import TensorDescriptor
33
+
34
+ from .bayesian_autotuner import bayesian_autotune
35
+ from .grouped import store_tile
36
+ from .utils import (
37
+ FP8_DTYPE,
38
+ MX_SCALE_GROUP_K,
39
+ NIBBLES_PER_BYTE,
40
+ decode_ue8m0_scale,
41
+ device_context,
42
+ sm_count,
43
+ fp8_act_quant_inline,
44
+ topk_reduce_kernel,
45
+ TOPK_REDUCE_BLOCK_H,
46
+ get_accelerator_autotuning_configs,
47
+ get_mxfp_autotuning_configs,
48
+ is_mxfp,
49
+ is_mxfp4,
50
+ mxfp_act_quant_inline,
51
+ mx_compute,
52
+ smem_config_pruner,
53
+ glu,
54
+ e2m1_as_uint8,
55
+ ue8m0_as_uint8,
56
+ )
57
+
58
+
59
+ # ── Persistent tile scheduling (register-resident expert-tile layout) ─────────
60
+
61
+ # Flat-slot tile per program for the O(S) routing kernels (count + scatter). These are small
62
+ # latency-bound atomic kernels that want many programs: a sweep over {256..4096} x prefill shapes
63
+ # put 256 best (or within ~1%) for both, with 1024 up to ~1.5x slower. The grid derives from it
64
+ # so the two can't drift. Power of 2.
65
+ _ROUTING_BLOCK_SIZE = 256
66
+
67
+
68
+ @triton.jit
69
+ def _build_tile_layout(
70
+ ExpertStart, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE_M: tl.constexpr
71
+ ):
72
+ """Load ``expert_start`` once and derive the per-BM tile layout vectors (kept in
73
+ registers for the whole persistent loop): per-expert first sorted row, token count,
74
+ exclusive tile-start cumsum, and the total M-tile count. ``ExpertStart`` is
75
+ ``(NUM_EXPERTS + 1,)`` with a trailing ``S`` sentinel (``expert_start[E] == S``)."""
76
+ e_offs = tl.arange(0, NUM_EXPERTS)
77
+ exp_start = tl.load(ExpertStart + e_offs)
78
+ exp_end = tl.load(ExpertStart + e_offs + 1)
79
+ freqs = exp_end - exp_start
80
+ tiles_per_e = (freqs + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
81
+ tile_start_excl = (
82
+ tl.cumsum(tiles_per_e, 0) - tiles_per_e
83
+ ) # first tile index of expert e
84
+ total_m_tiles = tl.sum(tiles_per_e, 0)
85
+ return exp_start, freqs, tile_start_excl, total_m_tiles, e_offs
86
+
87
+
88
+ @triton.jit
89
+ def _resolve_tile_inline(
90
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M: tl.constexpr
91
+ ):
92
+ """Map an M-tile id to its owning expert + the tile's sorted row range, from the
93
+ register-resident layout (no global loads). Returns ``(expert_id, sorted_indices,
94
+ row_mask)``."""
95
+ # Bucketize via the exclusive tile cumsum: #experts whose tile-start <= pid_m, minus 1.
96
+ expert_id = tl.sum((tile_start_excl <= pid_m).to(tl.int32), 0) - 1
97
+ sel = (
98
+ e_offs == expert_id
99
+ ) # scalar-index the E-vectors via mask-sum (no dynamic index)
100
+ e_start = tl.sum(tl.where(sel, exp_start, 0), 0)
101
+ e_tile_start = tl.sum(tl.where(sel, tile_start_excl, 0), 0)
102
+ freq = tl.sum(tl.where(sel, freqs, 0), 0)
103
+ within = pid_m - e_tile_start
104
+ m_start = e_start + within * BLOCK_SIZE_M
105
+ offs = tl.arange(0, BLOCK_SIZE_M)
106
+ row_mask = offs < freq - within * BLOCK_SIZE_M
107
+ sorted_indices = tl.max_contiguous(m_start + offs, BLOCK_SIZE_M)
108
+ return expert_id, sorted_indices, row_mask
109
+
110
+
111
+ @triton.jit
112
+ def _exclusive_offsets_kernel(
113
+ ExpertFreq, ExpertStart, Counters, NUM_EXPERTS: tl.constexpr
114
+ ):
115
+ """Exclusive cumsum of per-expert token counts → ``expert_start`` (leading 0, trailing
116
+ S), and zero the scatter counters — one launch."""
117
+ offs = tl.arange(0, NUM_EXPERTS)
118
+ incl = tl.cumsum(tl.load(ExpertFreq + offs), 0)
119
+ tl.store(ExpertStart, 0)
120
+ tl.store(ExpertStart + 1 + offs, incl)
121
+ tl.store(Counters + offs, tl.zeros([NUM_EXPERTS], tl.int32))
122
+
123
+
124
+ @triton.jit
125
+ def _scatter_kernel(
126
+ ExpertIds,
127
+ Perm,
128
+ PermToken,
129
+ ExpertStart,
130
+ Counters,
131
+ S,
132
+ NUM_TOP_K: tl.constexpr,
133
+ BLOCK_SIZE: tl.constexpr,
134
+ ):
135
+ """Counting-sort scatter: each flat slot atomically claims the next slot of its expert
136
+ (``expert_start[e] + counter[e]++``). O(S), replaces an O(S·logS) argsort. Within-expert
137
+ order is arbitrary (atomic race) — fine, the per-token reduce is order-invariant."""
138
+ offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
139
+ mask = offs < S
140
+ expert_id = tl.load(ExpertIds + offs, mask=mask, other=0)
141
+ dest = tl.load(ExpertStart + expert_id, mask=mask, other=0) + tl.atomic_add(
142
+ Counters + expert_id, 1, mask=mask
143
+ )
144
+ tl.store(Perm + dest, offs, mask=mask)
145
+ tl.store(PermToken + dest, offs // NUM_TOP_K, mask=mask)
146
+
147
+
148
+ @triton.jit
149
+ def _count_kernel(
150
+ ExpertIds, ExpertFreq, S, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE: tl.constexpr
151
+ ):
152
+ """Per-expert token count via atomics — replaces ``torch.histc`` (no float cast), fixed
153
+ ``(NUM_EXPERTS,)`` output stays CUDA-graph friendly. ``ExpertFreq`` is pre-zeroed."""
154
+ offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
155
+ mask = offs < S
156
+ expert_id = tl.load(ExpertIds + offs, mask=mask, other=NUM_EXPERTS)
157
+ tl.atomic_add(ExpertFreq + expert_id, 1, mask=mask & (expert_id < NUM_EXPERTS))
158
+
159
+
160
+ def _grouped_routing(expert_ids: torch.Tensor, num_experts: int, num_top_k: int):
161
+ """On-device routing: expert-sorted index (no copy of the activations) via two Triton
162
+ launches — exclusive offsets + an atomic counting-sort scatter (replaces host
163
+ ``argsort``). Returns ``(perm_token, perm, expert_start, num_experts, num_routed_tokens)``.
164
+ ``perm`` is the sorted-position → flat
165
+ ``(t*K + j)`` map (gate_up gathers via ``perm_token = perm // K``, down scatters via
166
+ ``perm``); ``expert_start`` is ``(E+1,)`` padded with S so the kernels build the tile
167
+ layout in-register (E is a power of 2)."""
168
+ device = expert_ids.device
169
+ expert_ids = expert_ids.int()
170
+ num_routed_tokens = expert_ids.numel() # S = num_tokens * num_top_k
171
+ expert_freq = torch.zeros(num_experts, dtype=torch.int32, device=device)
172
+ expert_start = torch.empty(num_experts + 1, dtype=torch.int32, device=device)
173
+ counters = torch.empty(num_experts, dtype=torch.int32, device=device)
174
+ perm = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
175
+ perm_token = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
176
+ with device_context(device):
177
+ _count_kernel[(triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)](
178
+ expert_ids,
179
+ expert_freq,
180
+ num_routed_tokens,
181
+ NUM_EXPERTS=num_experts,
182
+ BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
183
+ )
184
+ _exclusive_offsets_kernel[(1,)](
185
+ expert_freq,
186
+ expert_start,
187
+ counters,
188
+ NUM_EXPERTS=num_experts,
189
+ )
190
+ _scatter_kernel[(triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)](
191
+ expert_ids,
192
+ perm,
193
+ perm_token,
194
+ expert_start,
195
+ counters,
196
+ num_routed_tokens,
197
+ NUM_TOP_K=num_top_k,
198
+ BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
199
+ )
200
+ return perm_token, perm, expert_start, num_experts, num_routed_tokens
201
+
202
+
203
+ # MX MoE autotune axes: the MMA flavor and the weight-load mechanism. The kernels implement all
204
+ # three memory modes; get_mxfp_autotuning_configs drops the device-inapplicable descriptor
205
+ # flavor (host on XPU, device on CUDA).
206
+ _MX_COMPUTE_MODES = ("dot_scaled", "dot")
207
+ _MX_MEMORY_MODES = ("host_descriptor", "device_descriptor", "pointer")
208
+
209
+
210
+ # ── Block-dynamic FP8 ────────────────────────────────────────────────────────
211
+
212
+
213
+ @bayesian_autotune(
214
+ get_accelerator_autotuning_configs(tune_block_m=True),
215
+ ["INTERMEDIATE_DIM", "HIDDEN_DIM", "tokens_per_sm_bit_length"],
216
+ n_trials=60,
217
+ )
218
+ @triton.jit
219
+ def w8a8_block_dynamic_fp8_moe_grouped_gate_up_kernel(
220
+ Hidden, # (T, H) raw activations, UNSORTED
221
+ PermToken, # (S,) int32 — sorted position -> source token id
222
+ GateUp, # (E, 2I, H) FP8
223
+ GateUpScale, # (E, 2I//bn, H//bk) UE8M0 block scales
224
+ ExpertStart, # (NUM_EXPERTS+1,) int32 — exclusive sorted-row start per expert (pad S)
225
+ Inter, # (S, I) FP8 — output (expert-sorted)
226
+ InterScale, # (S, NUM_I_TILES) fp32 — per-row, per-I-tile activation scale
227
+ stride_h_t,
228
+ stride_h_k,
229
+ stride_gu_e,
230
+ stride_gu_n,
231
+ stride_gu_k,
232
+ stride_gus_e,
233
+ stride_gus_n,
234
+ stride_gus_k,
235
+ stride_int_m,
236
+ stride_int_n,
237
+ stride_is_m,
238
+ stride_is_n,
239
+ stride_pt,
240
+ tokens_per_sm_bit_length, # autotune key only (log2 tokens-per-SM bucket); unused in body
241
+ HIDDEN_DIM: tl.constexpr,
242
+ INTERMEDIATE_DIM: tl.constexpr,
243
+ BLOCK_SIZE_M: tl.constexpr,
244
+ BLOCK_SIZE_N: tl.constexpr,
245
+ BLOCK_SIZE_K: tl.constexpr,
246
+ NUM_EXPERTS: tl.constexpr,
247
+ NUM_SMS: tl.constexpr,
248
+ SIMULATE_UNFUSED: tl.constexpr,
249
+ ACT_FN: tl.constexpr,
250
+ ):
251
+ """Phase 1: persistent grid-stride over (M-tile, I-tile). Gather hidden rows per expert
252
+ M-tile, gate + up block-FP8 matmuls, SiLU-combine, FP8-requant the intermediate."""
253
+ start_pid = tl.program_id(axis=0)
254
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = _build_tile_layout(
255
+ ExpertStart, NUM_EXPERTS, BLOCK_SIZE_M
256
+ )
257
+ num_n_tiles = tl.cdiv(INTERMEDIATE_DIM, BLOCK_SIZE_N)
258
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
259
+ for tile_id in tl.range(start_pid, total_m_tiles * num_n_tiles, NUM_SMS):
260
+ pid_m = tile_id // num_n_tiles
261
+ pid_n = tile_id % num_n_tiles
262
+ expert_id, offs_global_m, row_mask = _resolve_tile_inline(
263
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
264
+ )
265
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
266
+
267
+ token = tl.load(PermToken + offs_global_m * stride_pt, mask=row_mask, other=0)
268
+ a_ptrs = Hidden + token[:, None] * stride_h_t + offs_k[None, :] * stride_h_k
269
+ gate_ptr = (
270
+ GateUp
271
+ + expert_id * stride_gu_e
272
+ + tl.arange(0, BLOCK_SIZE_K)[:, None] * stride_gu_k
273
+ + (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[None, :] * stride_gu_n
274
+ )
275
+ up_ptr = (
276
+ GateUp
277
+ + expert_id * stride_gu_e
278
+ + tl.arange(0, BLOCK_SIZE_K)[:, None] * stride_gu_k
279
+ + (INTERMEDIATE_DIM + pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N))[
280
+ None, :
281
+ ]
282
+ * stride_gu_n
283
+ )
284
+ gate_s_ptr = GateUpScale + expert_id * stride_gus_e + pid_n * stride_gus_n
285
+ up_s_ptr = (
286
+ GateUpScale
287
+ + expert_id * stride_gus_e
288
+ + (num_n_tiles + pid_n) * stride_gus_n
289
+ )
290
+
291
+ acc_gate = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
292
+ acc_up = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
293
+ for _ in range(0, tl.cdiv(HIDDEN_DIM, BLOCK_SIZE_K)):
294
+ a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
295
+ a, a_s = fp8_act_quant_inline(a_raw)
296
+ w_gate = tl.load(gate_ptr)
297
+ w_up = tl.load(up_ptr)
298
+ w_s_gate = decode_ue8m0_scale(tl.load(gate_s_ptr))
299
+ w_s_up = decode_ue8m0_scale(tl.load(up_s_ptr))
300
+ acc_gate += tl.dot(a, w_gate) * a_s[:, None] * w_s_gate
301
+ acc_up += tl.dot(a, w_up) * a_s[:, None] * w_s_up
302
+ a_ptrs += BLOCK_SIZE_K * stride_h_k
303
+ gate_ptr += BLOCK_SIZE_K * stride_gu_k
304
+ up_ptr += BLOCK_SIZE_K * stride_gu_k
305
+ gate_s_ptr += stride_gus_k
306
+ up_s_ptr += stride_gus_k
307
+
308
+ intermediate = glu(
309
+ acc_gate, acc_up, ACT_FN, SIMULATE_UNFUSED, Hidden.dtype.element_ty
310
+ )
311
+ inter, inter_s = fp8_act_quant_inline(
312
+ intermediate
313
+ ) # FP8 requant, per-row scale
314
+ int_ptrs = (
315
+ Inter
316
+ + offs_global_m[:, None] * stride_int_m
317
+ + offs_bn[None, :] * stride_int_n
318
+ )
319
+ tl.store(int_ptrs, inter, mask=row_mask[:, None])
320
+ tl.store(
321
+ InterScale + offs_global_m * stride_is_m + pid_n * stride_is_n,
322
+ inter_s,
323
+ mask=row_mask,
324
+ )
325
+
326
+
327
+ @bayesian_autotune(
328
+ get_accelerator_autotuning_configs(tune_block_m=True),
329
+ ["INTERMEDIATE_DIM", "HIDDEN_DIM", "tokens_per_sm_bit_length"],
330
+ n_trials=60,
331
+ )
332
+ @triton.jit
333
+ def w8a8_block_dynamic_fp8_moe_grouped_down_kernel(
334
+ Inter, # (S, I) FP8 — expert-sorted intermediate
335
+ InterScale, # (S, NUM_I_TILES) fp32 — per-row, per-I-tile scale
336
+ Down, # (E, H, I) FP8
337
+ DownScale, # (E, H//bn, I//bk) UE8M0 block scales
338
+ ExpertStart, # (NUM_EXPERTS+1,) int32 — exclusive sorted-row start per expert (pad S)
339
+ Perm, # (S,) int32 — sorted position -> flat (token, slot) row
340
+ SampleWeights, # (S,) routing weight per flat row
341
+ ProjOut, # (S, H) bf16, flat (token, slot) order
342
+ stride_int_m,
343
+ stride_int_n,
344
+ stride_is_m,
345
+ stride_is_n,
346
+ stride_down_e,
347
+ stride_down_h,
348
+ stride_down_i,
349
+ stride_downs_e,
350
+ stride_downs_h,
351
+ stride_downs_i,
352
+ stride_po_m,
353
+ stride_po_n,
354
+ stride_perm,
355
+ tokens_per_sm_bit_length, # autotune key only (log2 tokens-per-SM bucket); unused in body
356
+ HIDDEN_DIM: tl.constexpr,
357
+ INTERMEDIATE_DIM: tl.constexpr,
358
+ NUM_I_TILES: tl.constexpr,
359
+ BLOCK_SIZE_M: tl.constexpr,
360
+ BLOCK_SIZE_H: tl.constexpr,
361
+ BLOCK_SIZE_I: tl.constexpr,
362
+ NUM_EXPERTS: tl.constexpr,
363
+ NUM_SMS: tl.constexpr,
364
+ SIMULATE_UNFUSED: tl.constexpr,
365
+ ):
366
+ """Phase 2: persistent grid-stride over (M-tile, H-tile). Grouped down over the FP8
367
+ intermediate, then routing-weight × scatter to the flat (token, slot) row."""
368
+ start_pid = tl.program_id(axis=0)
369
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = _build_tile_layout(
370
+ ExpertStart, NUM_EXPERTS, BLOCK_SIZE_M
371
+ )
372
+ num_h_tiles = tl.cdiv(HIDDEN_DIM, BLOCK_SIZE_H)
373
+ offs_i = tl.arange(0, BLOCK_SIZE_I)
374
+ for tile_id in tl.range(start_pid, total_m_tiles * num_h_tiles, NUM_SMS):
375
+ pid_m = tile_id // num_h_tiles
376
+ pid_h = tile_id % num_h_tiles
377
+ expert_id, offs_global_m, row_mask = _resolve_tile_inline(
378
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
379
+ )
380
+ offs_h = pid_h * BLOCK_SIZE_H + tl.arange(0, BLOCK_SIZE_H)
381
+
382
+ w_down_ptr = (
383
+ Down
384
+ + expert_id * stride_down_e
385
+ + tl.arange(0, BLOCK_SIZE_I)[:, None] * stride_down_i
386
+ + (pid_h * BLOCK_SIZE_H + tl.arange(0, BLOCK_SIZE_H))[None, :]
387
+ * stride_down_h
388
+ )
389
+ ws_down_ptr = DownScale + expert_id * stride_downs_e + pid_h * stride_downs_h
390
+
391
+ acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_H), dtype=tl.float32)
392
+ for i_tile in range(0, NUM_I_TILES):
393
+ i_off = i_tile * BLOCK_SIZE_I
394
+ inter = tl.load(
395
+ Inter
396
+ + offs_global_m[:, None] * stride_int_m
397
+ + (i_off + offs_i)[None, :] * stride_int_n,
398
+ mask=row_mask[:, None],
399
+ other=0.0,
400
+ )
401
+ inter_s = tl.load(
402
+ InterScale + offs_global_m * stride_is_m + i_tile * stride_is_n,
403
+ mask=row_mask,
404
+ other=0.0,
405
+ )
406
+ w_s_down = decode_ue8m0_scale(tl.load(ws_down_ptr))
407
+ w_down = tl.load(w_down_ptr)
408
+ acc += tl.dot(inter, w_down) * inter_s[:, None] * w_s_down
409
+ w_down_ptr += BLOCK_SIZE_I * stride_down_i
410
+ ws_down_ptr += stride_downs_i
411
+
412
+ if SIMULATE_UNFUSED:
413
+ acc = acc.to(ProjOut.dtype.element_ty).to(tl.float32)
414
+ # Fused routing-weight × top-k reorder (no atomics): scale each row by its weight
415
+ # and scatter to its flat (token, slot) row; the host then just sums over slots.
416
+ flat = tl.load(Perm + offs_global_m * stride_perm, mask=row_mask, other=0)
417
+ weight = tl.load(SampleWeights + flat, mask=row_mask, other=0.0)
418
+ acc = acc * weight[:, None]
419
+ store_tile(ProjOut, acc, flat, offs_h, row_mask, stride_po_m, stride_po_n)
420
+
421
+
422
+ def w8a8_block_dynamic_fp8_moe_grouped(
423
+ hidden_states: torch.Tensor, # (T, H)
424
+ top_k_index: torch.Tensor, # (T, K) int
425
+ top_k_weights: torch.Tensor, # (T, K)
426
+ gate_up_proj: torch.Tensor, # (E, 2I, H) FP8
427
+ down_proj: torch.Tensor, # (E, H, I) FP8
428
+ gate_up_proj_scale: torch.Tensor,
429
+ down_proj_scale: torch.Tensor,
430
+ block_size: list[int],
431
+ simulate_unfused: bool = False,
432
+ act_fn: str = "silu",
433
+ ) -> torch.Tensor:
434
+ """Block-dynamic FP8 fused grouped MoE: gather gate_up+SiLU → FP8 intermediate →
435
+ grouped down → routing-weighted top-k reduce. Returns ``(num_tokens, hidden_dim)``."""
436
+ device = hidden_states.device
437
+ num_tokens = hidden_states.size(0)
438
+ num_top_k = top_k_index.size(-1)
439
+ HIDDEN_DIM = hidden_states.size(1)
440
+ INTERMEDIATE_DIM = down_proj.size(2)
441
+ BLOCK_SIZE_N, BLOCK_SIZE_K = block_size
442
+ NUM_I_TILES = INTERMEDIATE_DIM // BLOCK_SIZE_N
443
+
444
+ perm_token, perm, expert_start, NUM_EXPERTS, num_routed_tokens = _grouped_routing(
445
+ top_k_index, gate_up_proj.size(0), num_top_k
446
+ )
447
+ num_sms = sm_count(device.index)
448
+ tokens_per_sm_bit_length = (num_routed_tokens // num_sms).bit_length()
449
+
450
+ gate_up_scale_u8 = ue8m0_as_uint8(gate_up_proj_scale)
451
+ down_scale_u8 = ue8m0_as_uint8(down_proj_scale)
452
+
453
+ inter = torch.empty(
454
+ num_routed_tokens, INTERMEDIATE_DIM, device=device, dtype=FP8_DTYPE
455
+ )
456
+ inter_scale = torch.empty(
457
+ num_routed_tokens, NUM_I_TILES, device=device, dtype=torch.float32
458
+ )
459
+ out = torch.empty(
460
+ num_routed_tokens, HIDDEN_DIM, device=device, dtype=hidden_states.dtype
461
+ )
462
+ reduced = torch.empty(
463
+ num_tokens, HIDDEN_DIM, device=device, dtype=hidden_states.dtype
464
+ )
465
+ with device_context(device):
466
+ w8a8_block_dynamic_fp8_moe_grouped_gate_up_kernel[(num_sms,)](
467
+ hidden_states,
468
+ perm_token,
469
+ gate_up_proj,
470
+ gate_up_scale_u8,
471
+ expert_start,
472
+ inter,
473
+ inter_scale,
474
+ hidden_states.stride(0),
475
+ hidden_states.stride(1),
476
+ gate_up_proj.stride(0),
477
+ gate_up_proj.stride(1),
478
+ gate_up_proj.stride(2),
479
+ gate_up_scale_u8.stride(0),
480
+ gate_up_scale_u8.stride(1),
481
+ gate_up_scale_u8.stride(2),
482
+ inter.stride(0),
483
+ inter.stride(1),
484
+ inter_scale.stride(0),
485
+ inter_scale.stride(1),
486
+ perm_token.stride(0),
487
+ tokens_per_sm_bit_length=tokens_per_sm_bit_length,
488
+ HIDDEN_DIM=HIDDEN_DIM,
489
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
490
+ BLOCK_SIZE_N=BLOCK_SIZE_N,
491
+ BLOCK_SIZE_K=BLOCK_SIZE_K,
492
+ NUM_EXPERTS=NUM_EXPERTS,
493
+ NUM_SMS=num_sms,
494
+ SIMULATE_UNFUSED=simulate_unfused,
495
+ ACT_FN=act_fn,
496
+ )
497
+ w8a8_block_dynamic_fp8_moe_grouped_down_kernel[(num_sms,)](
498
+ inter,
499
+ inter_scale,
500
+ down_proj,
501
+ down_scale_u8,
502
+ expert_start,
503
+ perm,
504
+ top_k_weights,
505
+ out,
506
+ inter.stride(0),
507
+ inter.stride(1),
508
+ inter_scale.stride(0),
509
+ inter_scale.stride(1),
510
+ down_proj.stride(0),
511
+ down_proj.stride(1),
512
+ down_proj.stride(2),
513
+ down_scale_u8.stride(0),
514
+ down_scale_u8.stride(1),
515
+ down_scale_u8.stride(2),
516
+ out.stride(0),
517
+ out.stride(1),
518
+ perm.stride(0),
519
+ tokens_per_sm_bit_length=tokens_per_sm_bit_length,
520
+ HIDDEN_DIM=HIDDEN_DIM,
521
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
522
+ NUM_I_TILES=NUM_I_TILES,
523
+ BLOCK_SIZE_H=BLOCK_SIZE_N,
524
+ BLOCK_SIZE_I=BLOCK_SIZE_K,
525
+ NUM_EXPERTS=NUM_EXPERTS,
526
+ NUM_SMS=num_sms,
527
+ SIMULATE_UNFUSED=simulate_unfused,
528
+ )
529
+ topk_reduce_kernel[(num_tokens, triton.cdiv(HIDDEN_DIM, TOPK_REDUCE_BLOCK_H))](
530
+ out,
531
+ reduced,
532
+ HIDDEN_DIM,
533
+ out.stride(0),
534
+ out.stride(1),
535
+ reduced.stride(0),
536
+ reduced.stride(1),
537
+ NUM_TOP_K=num_top_k,
538
+ BLOCK_H=TOPK_REDUCE_BLOCK_H,
539
+ )
540
+
541
+ return reduced
542
+
543
+
544
+ # ── MXFP4/MXFP8 (UE8M0 group-32, tunable tile, MXFP8 intermediate) ────────────
545
+
546
+
547
+ def _set_gate_up_descriptor(nargs):
548
+ """Per-config pre_hook: set the gate_up TMA descriptor box to the autotuned
549
+ [2 (gate|up), BLOCK_SIZE_N, BK//VALUES_PER_BYTE] over the (2E, I, H/vpb) weight view.
550
+
551
+ MUST mutate ``block_shape`` in place, not rebind ``nargs[...]`` to a fresh descriptor:
552
+ the autotuner launches with the original ``*args`` object, so a rebind never reaches the
553
+ kernel (the placeholder box survives → all but BN=32,BK=32 configs fail the reshape). An
554
+ in-place edit of the passed object propagates to both compile-time specialization and the
555
+ launch-time tensor-map fill. ``shape``/``strides`` are already correct from the wrapper's
556
+ ``from_tensor`` placeholder; only the per-config box changes. No-op unless MEMORY_MODE is host_descriptor (pointer /
557
+ device_descriptor don't use the host-built descriptor)."""
558
+ if nargs["MEMORY_MODE"] != "host_descriptor":
559
+ return
560
+ nargs["GateUpDescriptor"].block_shape = [
561
+ 2,
562
+ nargs["BLOCK_SIZE_N"],
563
+ nargs["BLOCK_SIZE_K"] // nargs["VALUES_PER_BYTE"],
564
+ ]
565
+
566
+
567
+ @bayesian_autotune(
568
+ get_mxfp_autotuning_configs(
569
+ pre_hook=_set_gate_up_descriptor,
570
+ compute_modes=_MX_COMPUTE_MODES,
571
+ memory_modes=_MX_MEMORY_MODES,
572
+ tune_block_m=True,
573
+ ), # prefill: no scalar; combined gate∪up dot ([2*BN,K] reshape) — TMA vs block-ptr load
574
+ ["INTERMEDIATE_DIM", "HIDDEN_DIM", "tokens_per_sm_bit_length"],
575
+ n_trials=60,
576
+ # bf16 activation tile + fused gate|up weight tiles
577
+ prune_configs_by={
578
+ "early_config_prune": smem_config_pruner(act_bytes=2, n_weight_tiles=2)
579
+ },
580
+ )
581
+ @triton.jit
582
+ def mxfp_dynamic_moe_grouped_gate_up_kernel(
583
+ Hidden, # (T, H) raw activations, UNSORTED
584
+ PermToken, # (S,) int32 — sorted position -> source token id
585
+ GateUp, # (E, 2I, H//VALUES_PER_BYTE) MXFP4/MXFP8
586
+ GateUpScale, # (E, 2I, H//32) UE8M0 group-32 scales
587
+ GateUpDescriptor, # TMA descriptor over the (2E, I, H//VALUES_PER_BYTE) weight view
588
+ ExpertStart, # (NUM_EXPERTS+1,) int32 — exclusive sorted-row start per expert (pad S)
589
+ Inter, # (S, I) E4M3 — MXFP8 intermediate (expert-sorted)
590
+ InterScale, # (S, I//32) UE8M0 group-32 scales
591
+ stride_h_t,
592
+ stride_h_k,
593
+ stride_gu_e,
594
+ stride_gu_n,
595
+ stride_gu_k,
596
+ stride_gus_e,
597
+ stride_gus_n,
598
+ stride_gus_k,
599
+ stride_int_m,
600
+ stride_int_n,
601
+ stride_is_m,
602
+ stride_is_n,
603
+ stride_pt,
604
+ tokens_per_sm_bit_length, # autotune key only (log2 tokens-per-SM bucket); unused in body
605
+ HIDDEN_DIM: tl.constexpr,
606
+ INTERMEDIATE_DIM: tl.constexpr,
607
+ BLOCK_SIZE_M: tl.constexpr,
608
+ BLOCK_SIZE_N: tl.constexpr,
609
+ BLOCK_SIZE_K: tl.constexpr,
610
+ VALUES_PER_BYTE: tl.constexpr,
611
+ SCALE_GROUP_K: tl.constexpr,
612
+ NUM_EXPERTS: tl.constexpr,
613
+ NUM_SMS: tl.constexpr,
614
+ SIMULATE_UNFUSED: tl.constexpr,
615
+ COMPUTE_MODE: tl.constexpr,
616
+ MEMORY_MODE: tl.constexpr,
617
+ ACT_FN: tl.constexpr,
618
+ ):
619
+ """MXFP4/MXFP8 phase 1 (persistent): gather hidden rows per expert M-tile, gate + up MX
620
+ matmuls (``tl.dot_scaled`` or fp8 ``tl.dot`` + per-group rescale), SiLU, MXFP8-requant."""
621
+ start_pid = tl.program_id(axis=0)
622
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = _build_tile_layout(
623
+ ExpertStart, NUM_EXPERTS, BLOCK_SIZE_M
624
+ )
625
+ num_n_tiles = tl.cdiv(INTERMEDIATE_DIM, BLOCK_SIZE_N)
626
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
627
+ for tile_id in tl.range(start_pid, total_m_tiles * num_n_tiles, NUM_SMS):
628
+ pid_m = tile_id // num_n_tiles
629
+ pid_n = tile_id % num_n_tiles
630
+ expert_id, offs_global_m, row_mask = _resolve_tile_inline(
631
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
632
+ )
633
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
634
+
635
+ token = tl.load(PermToken + offs_global_m * stride_pt, mask=row_mask, other=0)
636
+ a_ptrs = Hidden + token[:, None] * stride_h_t + offs_k[None, :] * stride_h_k
637
+ n_off = pid_n * BLOCK_SIZE_N
638
+
639
+ # Load gate (row 2e) + up (row 2e+1) of the (2E, I, H) view as ONE combined tile, then
640
+ # split — this is the shared loop. MEMORY_MODE picks the LOAD only (decoupled from COMPUTE_MODE):
641
+ # a host/device descriptor (TMA on NVIDIA) vs explicit rank-3 pointers. COMPUTE_MODE picks the compute on the
642
+ # loaded tile: scaled-MMA (dot_scaled) or fp8 dot + per-group software rescale (dot).
643
+ gu_row = expert_id * 2
644
+ # (E, 2I, H//vpb) reinterpreted as (2E, I, H//vpb); host_descriptor uses the passed
645
+ # GateUpDescriptor, device_descriptor builds one in-kernel, pointer indexes it directly.
646
+ if MEMORY_MODE == "pointer":
647
+ gu_ptr = (
648
+ GateUp
649
+ + (gu_row + tl.arange(0, 2))[:, None, None]
650
+ * (INTERMEDIATE_DIM * stride_gu_n)
651
+ + (n_off + tl.arange(0, BLOCK_SIZE_N))[None, :, None] * stride_gu_n
652
+ + tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)[None, None, :]
653
+ * stride_gu_k
654
+ )
655
+ elif MEMORY_MODE == "device_descriptor":
656
+ gu_desc = tl.make_tensor_descriptor(
657
+ GateUp,
658
+ shape=(
659
+ 2 * NUM_EXPERTS,
660
+ INTERMEDIATE_DIM,
661
+ HIDDEN_DIM // VALUES_PER_BYTE,
662
+ ),
663
+ strides=(INTERMEDIATE_DIM * stride_gu_n, stride_gu_n, stride_gu_k),
664
+ block_shape=(2, BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE),
665
+ )
666
+ gu_scale_ptr = (
667
+ GateUpScale
668
+ + expert_id * stride_gus_e
669
+ + tl.arange(0, 2)[:, None, None] * (INTERMEDIATE_DIM * stride_gus_n)
670
+ + (n_off + tl.arange(0, BLOCK_SIZE_N))[None, :, None] * stride_gus_n
671
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, None, :] * stride_gus_k
672
+ )
673
+ acc = tl.zeros((BLOCK_SIZE_M, 2 * BLOCK_SIZE_N), dtype=tl.float32)
674
+ for k_off in tl.range(0, HIDDEN_DIM, BLOCK_SIZE_K):
675
+ a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
676
+ a, a_scale = mxfp_act_quant_inline(
677
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K
678
+ )
679
+ if MEMORY_MODE == "host_descriptor":
680
+ gu = tl.reshape(
681
+ GateUpDescriptor.load([gu_row, n_off, k_off // VALUES_PER_BYTE]),
682
+ [2 * BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE],
683
+ )
684
+ elif MEMORY_MODE == "device_descriptor":
685
+ gu = tl.reshape(
686
+ gu_desc.load([gu_row, n_off, k_off // VALUES_PER_BYTE]),
687
+ [2 * BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE],
688
+ )
689
+ else:
690
+ gu = tl.reshape(
691
+ tl.load(gu_ptr),
692
+ [2 * BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE],
693
+ )
694
+ gu_ptr += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_gu_k
695
+ gu_scale = tl.reshape(
696
+ tl.load(gu_scale_ptr + (k_off // SCALE_GROUP_K) * stride_gus_k),
697
+ [2 * BLOCK_SIZE_N, BLOCK_SIZE_K // SCALE_GROUP_K],
698
+ )
699
+ gu_t = tl.trans(
700
+ gu
701
+ ) # [BK, 2*BN] gate∪up weight; mx_compute decodes it for the fp8 path
702
+ acc = mx_compute(
703
+ acc,
704
+ a,
705
+ a_scale,
706
+ gu_t,
707
+ gu_scale,
708
+ COMPUTE_MODE,
709
+ VALUES_PER_BYTE,
710
+ BLOCK_SIZE_M,
711
+ BLOCK_SIZE_N,
712
+ BLOCK_SIZE_K,
713
+ SCALE_GROUP_K,
714
+ )
715
+ a_ptrs += BLOCK_SIZE_K * stride_h_k
716
+ acc_3d = tl.permute(tl.reshape(acc, [BLOCK_SIZE_M, 2, BLOCK_SIZE_N]), (0, 2, 1))
717
+ acc_gate, acc_up = tl.split(acc_3d)
718
+
719
+ intermediate = glu(
720
+ acc_gate, acc_up, ACT_FN, SIMULATE_UNFUSED, Hidden.dtype.element_ty
721
+ )
722
+
723
+ # MXFP8 requant of the intermediate (E4M3 + UE8M0 group-32 along this N-tile).
724
+ inter, inter_scale = mxfp_act_quant_inline(
725
+ intermediate, BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K
726
+ )
727
+ int_ptrs = (
728
+ Inter
729
+ + offs_global_m[:, None] * stride_int_m
730
+ + offs_bn[None, :] * stride_int_n
731
+ )
732
+ tl.store(int_ptrs, inter, mask=row_mask[:, None])
733
+ offs_sc = pid_n * (BLOCK_SIZE_N // SCALE_GROUP_K) + tl.arange(
734
+ 0, BLOCK_SIZE_N // SCALE_GROUP_K
735
+ )
736
+ sc_ptrs = (
737
+ InterScale
738
+ + offs_global_m[:, None] * stride_is_m
739
+ + offs_sc[None, :] * stride_is_n
740
+ )
741
+ tl.store(sc_ptrs, inter_scale, mask=row_mask[:, None])
742
+
743
+
744
+ def _set_down_descriptor(nargs):
745
+ """Per-config pre_hook: in-place set the down TMA descriptor box to
746
+ [BLOCK_SIZE_N, BK//VALUES_PER_BYTE] over the (E*H, I/vpb) weight view. In-place, not
747
+ rebind — see _set_gate_up_descriptor. No-op unless MEMORY_MODE is host_descriptor."""
748
+ if nargs["MEMORY_MODE"] != "host_descriptor":
749
+ return
750
+ nargs["DownDescriptor"].block_shape = [
751
+ nargs["BLOCK_SIZE_N"],
752
+ nargs["BLOCK_SIZE_K"] // nargs["VALUES_PER_BYTE"],
753
+ ]
754
+
755
+
756
+ @bayesian_autotune(
757
+ get_mxfp_autotuning_configs(
758
+ pre_hook=_set_down_descriptor,
759
+ compute_modes=_MX_COMPUTE_MODES,
760
+ memory_modes=_MX_MEMORY_MODES,
761
+ tune_block_m=True,
762
+ ), # prefill: no scalar; TMA vs block-ptr load
763
+ ["INTERMEDIATE_DIM", "HIDDEN_DIM", "tokens_per_sm_bit_length"],
764
+ n_trials=60,
765
+ # fp8 intermediate activation tile + single down weight tile
766
+ prune_configs_by={
767
+ "early_config_prune": smem_config_pruner(act_bytes=1, n_weight_tiles=1)
768
+ },
769
+ )
770
+ @triton.jit
771
+ def mxfp_dynamic_moe_grouped_down_kernel(
772
+ Inter, # (S, I) E4M3 — MXFP8 intermediate (expert-sorted)
773
+ InterScale, # (S, I//32) UE8M0 group-32 scales
774
+ Down, # (E, H, I//VALUES_PER_BYTE) MXFP4/MXFP8
775
+ DownScale, # (E, H, I//32) UE8M0 group-32 scales
776
+ DownDescriptor, # TMA descriptor over the (E*H, I//VALUES_PER_BYTE) weight view
777
+ ExpertStart, # (NUM_EXPERTS+1,) int32 — exclusive sorted-row start per expert (pad S)
778
+ Perm, # (S,) int32 — sorted position -> flat (token, slot) row
779
+ SampleWeights, # (S,) routing weight per flat row
780
+ ProjOut, # (S, H) bf16, flat (token, slot) order
781
+ stride_int_m,
782
+ stride_int_n,
783
+ stride_is_m,
784
+ stride_is_n,
785
+ stride_down_e,
786
+ stride_down_n,
787
+ stride_down_k,
788
+ stride_downs_e,
789
+ stride_downs_n,
790
+ stride_downs_k,
791
+ stride_po_m,
792
+ stride_po_n,
793
+ stride_perm,
794
+ tokens_per_sm_bit_length, # autotune key only (log2 tokens-per-SM bucket); unused in body
795
+ HIDDEN_DIM: tl.constexpr,
796
+ INTERMEDIATE_DIM: tl.constexpr,
797
+ BLOCK_SIZE_M: tl.constexpr,
798
+ BLOCK_SIZE_N: tl.constexpr,
799
+ BLOCK_SIZE_K: tl.constexpr,
800
+ VALUES_PER_BYTE: tl.constexpr,
801
+ SCALE_GROUP_K: tl.constexpr,
802
+ NUM_EXPERTS: tl.constexpr,
803
+ NUM_SMS: tl.constexpr,
804
+ SIMULATE_UNFUSED: tl.constexpr,
805
+ COMPUTE_MODE: tl.constexpr,
806
+ MEMORY_MODE: tl.constexpr,
807
+ ):
808
+ """MXFP4/MXFP8 phase 2 (persistent): MXFP8 intermediate → down proj, then routing-weight
809
+ × scatter to the flat (token, slot) row. N = hidden tile, K = intermediate tile."""
810
+ start_pid = tl.program_id(axis=0)
811
+ exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = _build_tile_layout(
812
+ ExpertStart, NUM_EXPERTS, BLOCK_SIZE_M
813
+ )
814
+ num_n_tiles = tl.cdiv(HIDDEN_DIM, BLOCK_SIZE_N)
815
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
816
+ offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
817
+ for tile_id in tl.range(start_pid, total_m_tiles * num_n_tiles, NUM_SMS):
818
+ pid_m = tile_id // num_n_tiles
819
+ pid_n = tile_id % num_n_tiles
820
+ expert_id, offs_global_m, row_mask = _resolve_tile_inline(
821
+ pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
822
+ )
823
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
824
+
825
+ a_ptrs = (
826
+ Inter
827
+ + offs_global_m[:, None] * stride_int_m
828
+ + offs_k[None, :] * stride_int_n
829
+ )
830
+ as_ptrs = (
831
+ InterScale
832
+ + offs_global_m[:, None] * stride_is_m
833
+ + offs_sf[None, :] * stride_is_n
834
+ )
835
+ n_off = pid_n * BLOCK_SIZE_N
836
+ ws_down_ptr = (
837
+ DownScale
838
+ + expert_id * stride_downs_e
839
+ + (n_off + tl.arange(0, BLOCK_SIZE_N))[:, None] * stride_downs_n
840
+ + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)[None, :] * stride_downs_k
841
+ )
842
+
843
+ # Down weight tile [BK//vpb, BN] loaded once per K-chunk: MEMORY_MODE picks the LOAD (a
844
+ # host/device descriptor over the (E*H, I//vpb) view vs explicit pointers), COMPUTE_MODE
845
+ # picks the compute. Scales stay a pointer load (a descriptor needs >=16B inner; the
846
+ # BK//32 row is too narrow).
847
+ if MEMORY_MODE == "pointer":
848
+ w_down_ptr = (
849
+ Down
850
+ + expert_id * stride_down_e
851
+ + tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)[:, None] * stride_down_k
852
+ + (n_off + tl.arange(0, BLOCK_SIZE_N))[None, :] * stride_down_n
853
+ )
854
+ elif MEMORY_MODE == "device_descriptor":
855
+ down_desc = tl.make_tensor_descriptor(
856
+ Down, # (E, H, I//vpb) flattened to the (E*H, I//vpb) view
857
+ shape=(NUM_EXPERTS * HIDDEN_DIM, INTERMEDIATE_DIM // VALUES_PER_BYTE),
858
+ strides=(stride_down_n, stride_down_k),
859
+ block_shape=(BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE),
860
+ )
861
+ acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
862
+ for k_off in tl.range(0, INTERMEDIATE_DIM, BLOCK_SIZE_K):
863
+ a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
864
+ a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0)
865
+ if MEMORY_MODE == "host_descriptor":
866
+ w = tl.trans(
867
+ tl.reshape(
868
+ DownDescriptor.load(
869
+ [expert_id * HIDDEN_DIM + n_off, k_off // VALUES_PER_BYTE]
870
+ ),
871
+ [BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE],
872
+ )
873
+ )
874
+ elif MEMORY_MODE == "device_descriptor":
875
+ w = tl.trans(
876
+ tl.reshape(
877
+ down_desc.load(
878
+ [expert_id * HIDDEN_DIM + n_off, k_off // VALUES_PER_BYTE]
879
+ ),
880
+ [BLOCK_SIZE_N, BLOCK_SIZE_K // VALUES_PER_BYTE],
881
+ )
882
+ )
883
+ else:
884
+ w = tl.load(w_down_ptr)
885
+ w_down_ptr += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_down_k
886
+ w_scale = tl.load(ws_down_ptr + (k_off // SCALE_GROUP_K) * stride_downs_k)
887
+ acc = mx_compute(
888
+ acc,
889
+ a,
890
+ a_scale,
891
+ w,
892
+ w_scale,
893
+ COMPUTE_MODE,
894
+ VALUES_PER_BYTE,
895
+ BLOCK_SIZE_M,
896
+ BLOCK_SIZE_N,
897
+ BLOCK_SIZE_K,
898
+ SCALE_GROUP_K,
899
+ )
900
+ a_ptrs += BLOCK_SIZE_K * stride_int_n
901
+ as_ptrs += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_is_n
902
+
903
+ if SIMULATE_UNFUSED:
904
+ acc = acc.to(ProjOut.dtype.element_ty).to(tl.float32)
905
+ # Fused routing-weight × top-k reorder (no atomics): scale each row by its weight
906
+ # and scatter to its flat (token, slot) row; the host then just sums over slots.
907
+ flat = tl.load(Perm + offs_global_m * stride_perm, mask=row_mask, other=0)
908
+ weight = tl.load(SampleWeights + flat, mask=row_mask, other=0.0)
909
+ acc = acc * weight[:, None]
910
+ store_tile(ProjOut, acc, flat, offs_bn, row_mask, stride_po_m, stride_po_n)
911
+
912
+
913
+ def mxfp_dynamic_moe_grouped(
914
+ hidden_states: torch.Tensor,
915
+ top_k_index: torch.Tensor,
916
+ top_k_weights: torch.Tensor,
917
+ gate_up_proj: torch.Tensor,
918
+ down_proj: torch.Tensor,
919
+ gate_up_proj_scale: torch.Tensor,
920
+ down_proj_scale: torch.Tensor,
921
+ simulate_unfused: bool = False,
922
+ act_fn: str = "silu",
923
+ ) -> torch.Tensor:
924
+ """MXFP4/MXFP8 fused grouped MoE (UE8M0 group-32); gate_up and down must share the same MX
925
+ format. Same structure as the block-dynamic path but with a tunable tile and MXFP8 intermediate."""
926
+ gate_up_is_fp4 = is_mxfp4(gate_up_proj, gate_up_proj_scale)
927
+ down_is_fp4 = is_mxfp4(down_proj, down_proj_scale)
928
+ if gate_up_is_fp4 != down_is_fp4:
929
+ raise ValueError(
930
+ "gate_up_proj and down_proj must use the same MX format (both MXFP4 or both MXFP8)."
931
+ )
932
+
933
+ device = hidden_states.device
934
+ num_sms = sm_count(device.index)
935
+ num_top_k = top_k_index.size(-1)
936
+ num_tokens = hidden_states.size(0)
937
+ HIDDEN_DIM = hidden_states.size(1)
938
+ INTERMEDIATE_DIM = gate_up_proj.size(1) // 2
939
+ perm_token, perm, expert_start, NUM_EXPERTS, num_routed_tokens = _grouped_routing(
940
+ top_k_index, gate_up_proj.size(0), num_top_k
941
+ )
942
+ tokens_per_sm_bit_length = (num_routed_tokens // num_sms).bit_length()
943
+ VALUES_PER_BYTE = NIBBLES_PER_BYTE if gate_up_is_fp4 else 1
944
+ gate_up_proj_u8 = e2m1_as_uint8(gate_up_proj)
945
+ down_proj_u8 = e2m1_as_uint8(down_proj)
946
+ gate_up_scale_u8 = ue8m0_as_uint8(gate_up_proj_scale)
947
+ down_scale_u8 = ue8m0_as_uint8(down_proj_scale)
948
+
949
+ inter = torch.empty(
950
+ num_routed_tokens, INTERMEDIATE_DIM, device=device, dtype=FP8_DTYPE
951
+ )
952
+ inter_scale = torch.empty(
953
+ num_routed_tokens,
954
+ INTERMEDIATE_DIM // MX_SCALE_GROUP_K,
955
+ device=device,
956
+ dtype=torch.uint8,
957
+ )
958
+ out = torch.empty(
959
+ num_routed_tokens, HIDDEN_DIM, device=device, dtype=hidden_states.dtype
960
+ )
961
+ reduced = torch.empty(
962
+ num_tokens, HIDDEN_DIM, device=device, dtype=hidden_states.dtype
963
+ )
964
+
965
+ # Host-built descriptors for host_descriptor mode: views (not reshapes) of the weight
966
+ # buffers — gate_up (E, 2I, H/vpb) → (2E, I, H/vpb) so one [2, BN, BK/vpb] box loads gate
967
+ # (row 2e) + up (2e+1); down (E, H, I/vpb) → (E*H, I/vpb), one [BN, BK/vpb] box per (expert,
968
+ # hidden-tile). Cheap to build, so always created (device_descriptor/pointer ignore them).
969
+ gate_up_2e = gate_up_proj_u8.view(
970
+ 2 * gate_up_proj_u8.size(0), INTERMEDIATE_DIM, gate_up_proj_u8.size(2)
971
+ )
972
+ gate_up_descriptor = TensorDescriptor.from_tensor(
973
+ gate_up_2e, [2, 32, 32 // VALUES_PER_BYTE]
974
+ )
975
+ down_eh = down_proj_u8.view(down_proj_u8.size(0) * HIDDEN_DIM, down_proj_u8.size(2))
976
+ down_descriptor = TensorDescriptor.from_tensor(down_eh, [32, 32 // VALUES_PER_BYTE])
977
+ with device_context(device):
978
+ mxfp_dynamic_moe_grouped_gate_up_kernel[(num_sms,)](
979
+ hidden_states,
980
+ perm_token,
981
+ gate_up_proj_u8,
982
+ gate_up_scale_u8,
983
+ gate_up_descriptor,
984
+ expert_start,
985
+ inter,
986
+ inter_scale,
987
+ hidden_states.stride(0),
988
+ hidden_states.stride(1),
989
+ gate_up_proj_u8.stride(0),
990
+ gate_up_proj_u8.stride(1),
991
+ gate_up_proj_u8.stride(2),
992
+ gate_up_scale_u8.stride(0),
993
+ gate_up_scale_u8.stride(1),
994
+ gate_up_scale_u8.stride(2),
995
+ inter.stride(0),
996
+ inter.stride(1),
997
+ inter_scale.stride(0),
998
+ inter_scale.stride(1),
999
+ perm_token.stride(0),
1000
+ tokens_per_sm_bit_length=tokens_per_sm_bit_length,
1001
+ HIDDEN_DIM=HIDDEN_DIM,
1002
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
1003
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
1004
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
1005
+ NUM_EXPERTS=NUM_EXPERTS,
1006
+ NUM_SMS=num_sms,
1007
+ SIMULATE_UNFUSED=simulate_unfused,
1008
+ ACT_FN=act_fn,
1009
+ )
1010
+ mxfp_dynamic_moe_grouped_down_kernel[(num_sms,)](
1011
+ inter,
1012
+ inter_scale,
1013
+ down_proj_u8,
1014
+ down_scale_u8,
1015
+ down_descriptor,
1016
+ expert_start,
1017
+ perm,
1018
+ top_k_weights,
1019
+ out,
1020
+ inter.stride(0),
1021
+ inter.stride(1),
1022
+ inter_scale.stride(0),
1023
+ inter_scale.stride(1),
1024
+ down_proj_u8.stride(0),
1025
+ down_proj_u8.stride(1),
1026
+ down_proj_u8.stride(2),
1027
+ down_scale_u8.stride(0),
1028
+ down_scale_u8.stride(1),
1029
+ down_scale_u8.stride(2),
1030
+ out.stride(0),
1031
+ out.stride(1),
1032
+ perm.stride(0),
1033
+ tokens_per_sm_bit_length=tokens_per_sm_bit_length,
1034
+ HIDDEN_DIM=HIDDEN_DIM,
1035
+ INTERMEDIATE_DIM=INTERMEDIATE_DIM,
1036
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
1037
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
1038
+ NUM_EXPERTS=NUM_EXPERTS,
1039
+ NUM_SMS=num_sms,
1040
+ SIMULATE_UNFUSED=simulate_unfused,
1041
+ )
1042
+ topk_reduce_kernel[(num_tokens, triton.cdiv(HIDDEN_DIM, TOPK_REDUCE_BLOCK_H))](
1043
+ out,
1044
+ reduced,
1045
+ HIDDEN_DIM,
1046
+ out.stride(0),
1047
+ out.stride(1),
1048
+ reduced.stride(0),
1049
+ reduced.stride(1),
1050
+ NUM_TOP_K=num_top_k,
1051
+ BLOCK_H=TOPK_REDUCE_BLOCK_H,
1052
+ )
1053
+
1054
+ return reduced
1055
+
1056
+
1057
+ # ── Dispatcher ────────────────────────────────────────────────────────────────
1058
+
1059
+
1060
+ def moe_fused_grouped(
1061
+ hidden_states: torch.Tensor,
1062
+ top_k_index: torch.Tensor,
1063
+ top_k_weights: torch.Tensor,
1064
+ gate_up_proj: torch.Tensor,
1065
+ down_proj: torch.Tensor,
1066
+ gate_up_proj_scale_inv: torch.Tensor,
1067
+ down_proj_scale_inv: torch.Tensor,
1068
+ block_size: list[int] | None,
1069
+ simulate_unfused: bool = False,
1070
+ act_fn: str = "silu",
1071
+ ) -> torch.Tensor:
1072
+ """Fused grouped-MoE dispatcher — routes to the recipe matching the weight dtype /
1073
+ scale layout, mirroring ``moe_fused_batched``. Implemented: block-dynamic FP8 and MXFP8 /
1074
+ MXFP4 (UE8M0 group-32). ``simulate_unfused`` (testing) rounds each step through the
1075
+ activation dtype so the output matches the unfused reference to reduce order."""
1076
+ gate_up_is_mx = is_mxfp(gate_up_proj, gate_up_proj_scale_inv)
1077
+ down_is_mx = is_mxfp(down_proj, down_proj_scale_inv)
1078
+ if gate_up_is_mx != down_is_mx:
1079
+ raise ValueError(
1080
+ "gate_up_proj and down_proj must use the same recipe (both MX or both block-dynamic FP8)."
1081
+ )
1082
+
1083
+ if gate_up_is_mx:
1084
+ return mxfp_dynamic_moe_grouped(
1085
+ hidden_states,
1086
+ top_k_index,
1087
+ top_k_weights,
1088
+ gate_up_proj,
1089
+ down_proj,
1090
+ gate_up_proj_scale_inv,
1091
+ down_proj_scale_inv,
1092
+ simulate_unfused,
1093
+ act_fn,
1094
+ )
1095
+
1096
+ if block_size is None:
1097
+ raise ValueError("block_size is required for block-dynamic FP8 weights.")
1098
+
1099
+ return w8a8_block_dynamic_fp8_moe_grouped(
1100
+ hidden_states,
1101
+ top_k_index,
1102
+ top_k_weights,
1103
+ gate_up_proj,
1104
+ down_proj,
1105
+ gate_up_proj_scale_inv,
1106
+ down_proj_scale_inv,
1107
+ block_size,
1108
+ simulate_unfused,
1109
+ act_fn,
1110
+ )
build/torch-cuda/grouped.py CHANGED
@@ -15,28 +15,138 @@
15
  import torch
16
  import triton
17
  import triton.language as tl
18
- from .act_quant import fp8_act_quant
19
  from torch.library import triton_op, wrap_triton
20
 
21
- from .utils import device_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
  key=["N", "K", "BLOCK_SIZE_M"],
31
  )
32
  @triton.jit
33
- def w8a8_block_fp8_matmul_grouped_kernel(
34
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert id
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
- Offsets, # (E,) int32 — cumulative row-end per expert
39
- TileOffsets, # (E,) int32 — cumulative tile-end per expert
40
  # Shape
41
  S,
42
  N,
@@ -52,11 +162,13 @@ def w8a8_block_fp8_matmul_grouped_kernel(
52
  stride_bs_e,
53
  stride_bs_k,
54
  stride_bs_n,
 
 
 
55
  # Meta-parameters
56
- NUM_EXPERTS: tl.constexpr,
57
  BLOCK_SIZE_N: tl.constexpr,
58
  BLOCK_SIZE_K: tl.constexpr,
59
- BLOCK_SIZE_M: tl.constexpr,
60
  NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
61
  ):
62
  """Block-scale grouped FP8 expert matmul kernel.
@@ -67,47 +179,23 @@ def w8a8_block_fp8_matmul_grouped_kernel(
67
  pid_m = tl.program_id(axis=0)
68
  pid_n = tl.program_id(axis=1)
69
 
70
- # Exit early for programs beyond the actual tile count.
71
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
72
  if pid_m >= total_tiles:
73
  return
74
 
75
- # Binary search in TileOffsets to find the owning expert.
76
- # Finds the smallest e such that TileOffsets[e] > pid_m (upper_bound semantics),
77
- # which is the expert whose tile range contains pid_m.
78
- # O(log2(NUM_EXPERTS)) loads instead of the O(NUM_EXPERTS) linear scan.
79
- # NUM_EXPERTS_BIT_LENGTH is ceil(log2(E))+1 for powers-of-two, giving one
80
- # harmless extra iteration when lo==hi; it's a compile-time constant so the
81
- # loop is fully unrolled by the compiler.
82
- lo = 0
83
- hi = NUM_EXPERTS
84
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
85
- mid = (lo + hi) >> 1
86
- mid_val = tl.load(TileOffsets + mid)
87
- is_left = mid_val <= pid_m
88
- lo = tl.where(is_left, mid + 1, lo)
89
- hi = tl.where(is_left, hi, mid)
90
-
91
- # Cast expert_id to int64 to prevent int32 overflow when computing
92
- # expert_id * stride_be (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
93
- # 3072×3072 FP8 weights).
94
- expert_id = lo.to(tl.int64)
95
-
96
- prev_eid = tl.maximum(expert_id - 1, 0)
97
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
98
- expert_end = tl.load(Offsets + expert_id)
99
- M_expert = expert_end - expert_start
100
-
101
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
102
- local_tile = pid_m - expert_tile_start
103
- m_off = local_tile * BLOCK_SIZE_M
104
-
105
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
106
- row_mask = offs_am < M_expert
107
- offs_global_m = expert_start + offs_am
108
-
109
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
110
- offs_k = tl.arange(0, BLOCK_SIZE_K)
111
 
112
  a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
113
  b_ptrs = (
@@ -120,49 +208,37 @@ def w8a8_block_fp8_matmul_grouped_kernel(
120
 
121
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
122
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
123
- # ---- fused fp8_act_quant ----
124
  a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
125
- a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
126
- a = (a_raw / tl.maximum(a_s[:, None], 1e-12)).to(tl.float8e4nv)
127
- # ---- matmul ----
128
  b = tl.load(b_ptrs)
129
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
130
  accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
131
  a_ptrs += BLOCK_SIZE_K * stride_ak
132
  b_ptrs += BLOCK_SIZE_K * stride_bk
 
133
 
134
- if C.dtype.element_ty == tl.bfloat16:
135
- c = accumulator.to(tl.bfloat16)
136
- elif C.dtype.element_ty == tl.float16:
137
- c = accumulator.to(tl.float16)
138
- else:
139
- c = accumulator.to(tl.float32)
140
-
141
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
142
- c_mask = row_mask[:, None]
143
- tl.store(c_ptrs, c, mask=c_mask)
144
 
145
 
146
- @triton.autotune(
147
- configs=[
148
- triton.Config({}, num_warps=w, num_stages=s)
149
- for w in [2, 4, 8, 16]
150
- for s in [2, 3, 4, 5]
151
- ],
152
- key=["N", "K", "BLOCK_SIZE_M"],
153
  )
154
  @triton.jit
155
- def w8a8_tensor_fp8_matmul_grouped_kernel(
156
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert idc
157
- B, # (E, N, K) FP8 weight matrices
158
  C, # (S, N) output
159
- As, # (S, 1) activation scales
160
- Bs, # (E, 1, 1) per-tensor weight scales
161
- Offsets,
162
- TileOffsets,
 
163
  S,
164
  N,
165
  K,
 
166
  stride_am,
167
  stride_ak,
168
  stride_be,
@@ -172,10 +248,13 @@ def w8a8_tensor_fp8_matmul_grouped_kernel(
172
  stride_cn,
173
  stride_as_m,
174
  stride_bs_e,
175
- NUM_EXPERTS: tl.constexpr,
 
 
 
 
176
  BLOCK_SIZE_N: tl.constexpr,
177
  BLOCK_SIZE_K: tl.constexpr,
178
- BLOCK_SIZE_M: tl.constexpr,
179
  NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
180
  ):
181
  """Tensor-scale grouped FP8 expert matmul kernel.
@@ -186,35 +265,23 @@ def w8a8_tensor_fp8_matmul_grouped_kernel(
186
  pid_m = tl.program_id(axis=0)
187
  pid_n = tl.program_id(axis=1)
188
 
189
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
190
  if pid_m >= total_tiles:
191
  return
192
 
193
- lo = 0
194
- hi = NUM_EXPERTS
195
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
196
- mid = (lo + hi) >> 1
197
- mid_val = tl.load(TileOffsets + mid)
198
- is_left = mid_val <= pid_m
199
- lo = tl.where(is_left, mid + 1, lo)
200
- hi = tl.where(is_left, hi, mid)
201
- expert_id = lo.to(tl.int64)
202
-
203
- prev_eid = tl.maximum(expert_id - 1, 0)
204
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
205
- expert_end = tl.load(Offsets + expert_id)
206
- M_expert = expert_end - expert_start
207
-
208
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
209
- local_tile = pid_m - expert_tile_start
210
- m_off = local_tile * BLOCK_SIZE_M
211
-
212
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
213
- row_mask = offs_am < M_expert
214
- offs_global_m = expert_start + offs_am
215
-
216
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
217
- offs_k = tl.arange(0, BLOCK_SIZE_K)
218
 
219
  a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
220
  b_ptrs = (
@@ -231,50 +298,151 @@ def w8a8_tensor_fp8_matmul_grouped_kernel(
231
  for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
232
  a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
233
  b = tl.load(b_ptrs)
234
-
235
  accumulator += tl.dot(a, b)
236
  a_ptrs += BLOCK_SIZE_K * stride_ak
237
  b_ptrs += BLOCK_SIZE_K * stride_bk
238
-
239
  accumulator = accumulator * a_s[:, None] * b_s
240
 
241
- if C.dtype.element_ty == tl.bfloat16:
242
- c = accumulator.to(tl.bfloat16)
243
- elif C.dtype.element_ty == tl.float16:
244
- c = accumulator.to(tl.float16)
245
- else:
246
- c = accumulator.to(tl.float32)
247
 
248
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
249
- c_mask = row_mask[:, None]
250
- tl.store(c_ptrs, c, mask=c_mask)
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_grouped", mutates_args=())
254
- def _w8a8_block_fp8_matmul_grouped(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  A: torch.Tensor,
256
  B: torch.Tensor,
257
  Bs: torch.Tensor,
258
  offsets: torch.Tensor,
259
  tokens_per_expert: torch.Tensor,
260
  block_size: list[int],
 
261
  ) -> torch.Tensor:
262
  """Block-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
263
 
264
  A: (S, K) raw bf16/fp16 activations, sorted by expert
265
- B: (E, N, K) FP8 expert weights
266
- Bs: (E, N // block_n, K // block_k) per-block weight scales
267
  """
268
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
269
  assert A.is_contiguous(), "A must be contiguous"
270
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
271
  assert B.is_contiguous(), "B must be contiguous"
272
  assert A.shape[1] == B.shape[2], (
273
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
274
  )
275
 
276
  S, K = A.shape
277
- E, N, _ = B.shape
278
 
279
  assert len(block_size) == 2, (
280
  f"block_size must be [block_n, block_k], got {block_size}"
@@ -284,25 +452,22 @@ def _w8a8_block_fp8_matmul_grouped(
284
  assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
285
  assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
286
  assert Bs.ndim == 3, (
287
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
288
  )
289
- assert Bs.shape == (E, N // block_n, K // block_k), (
290
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
291
  )
292
 
293
- C = A.new_empty(S, N)
294
- # Adaptive BLOCK_SIZE_M: match tile to average tokens per expert.
295
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
296
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
297
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
298
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
299
- # Programs beyond the real tile count exit immediately via the early-return
300
- # guard inside the kernel. This is faster than syncing for the exact count
301
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
302
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
303
- grid = (max_M_tiles, triton.cdiv(N, block_n))
304
  with device_context(A.device):
305
- wrap_triton(w8a8_block_fp8_matmul_grouped_kernel)[grid](
306
  A,
307
  B,
308
  C,
@@ -322,66 +487,70 @@ def _w8a8_block_fp8_matmul_grouped(
322
  Bs.stride(0),
323
  Bs.stride(2),
324
  Bs.stride(1),
 
 
325
  # Meta-parameters
326
- NUM_EXPERTS=E,
327
  BLOCK_SIZE_N=block_n,
328
  BLOCK_SIZE_K=block_k,
329
  BLOCK_SIZE_M=BLOCK_SIZE_M,
330
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
331
  )
332
 
333
  return C
334
 
335
 
336
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_grouped", mutates_args=())
337
- def _w8a8_tensor_fp8_matmul_grouped(
 
 
338
  A: torch.Tensor,
339
  B: torch.Tensor,
340
  Bs: torch.Tensor,
341
  offsets: torch.Tensor,
342
  tokens_per_expert: torch.Tensor,
 
343
  ) -> torch.Tensor:
344
  """Tensor-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
345
 
346
  A: (S, K) raw bf16/fp16 activations, sorted by expert
347
- B: (E, N, K) FP8 expert weights
348
- Bs: (E,) or (E, 1, 1) per-expert weight scales
349
  """
350
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
351
  assert A.is_contiguous(), "A must be contiguous"
352
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
353
  assert B.is_contiguous(), "B must be contiguous"
354
  assert A.shape[1] == B.shape[2], (
355
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
356
  )
357
 
358
  S, K = A.shape
359
- E, N, _ = B.shape
360
 
361
- # Normalize Bs to (E, 1, 1)
362
  if Bs.ndim == 1:
363
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
364
- Bs = Bs.reshape(E, 1, 1)
 
 
365
  else:
366
- assert Bs.shape == (E, 1, 1), (
367
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
368
  )
369
 
370
- BLOCK_SIZE_N = 128
371
- BLOCK_SIZE_K = 128
372
- C = A.new_empty(S, N)
373
  qA, As = fp8_act_quant(A, K)
374
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
375
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
376
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
377
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
378
- # Programs beyond the real tile count exit immediately via the early-return
379
- # guard inside the kernel. This is faster than syncing for the exact count
380
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
381
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
382
- grid = (max_M_tiles, triton.cdiv(N, BLOCK_SIZE_N))
383
  with device_context(A.device):
384
- wrap_triton(w8a8_tensor_fp8_matmul_grouped_kernel)[grid](
385
  qA,
386
  B,
387
  C,
@@ -401,77 +570,190 @@ def _w8a8_tensor_fp8_matmul_grouped(
401
  C.stride(1),
402
  As.stride(0),
403
  Bs.stride(0),
404
- # Meta-parameters
405
- NUM_EXPERTS=E,
406
- BLOCK_SIZE_N=BLOCK_SIZE_N,
407
- BLOCK_SIZE_K=BLOCK_SIZE_K,
408
  BLOCK_SIZE_M=BLOCK_SIZE_M,
409
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
410
  )
411
 
412
  return C
413
 
414
 
415
- def w8a8_block_fp8_matmul_grouped(
416
  A: torch.Tensor,
417
  B: torch.Tensor,
418
  Bs: torch.Tensor,
419
  offsets: torch.Tensor,
420
  tokens_per_expert: torch.Tensor,
421
  block_size: list[int],
 
422
  ) -> torch.Tensor:
423
  """Block-scale grouped FP8 matmul with fused activation quantization.
424
 
425
  A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
426
- B: (E, N, K) FP8 expert weights
427
- Bs: (E, N // block_n, K // block_k) per-block weight scales
 
428
  """
429
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_grouped(
430
- A, B, Bs, offsets, tokens_per_expert, block_size
431
  )
432
 
433
 
434
- def w8a8_tensor_fp8_matmul_grouped(
435
  A: torch.Tensor,
436
  B: torch.Tensor,
437
  Bs: torch.Tensor,
438
  offsets: torch.Tensor,
439
  tokens_per_expert: torch.Tensor,
 
440
  ) -> torch.Tensor:
441
  """Tensor-scale grouped FP8 matmul with fused activation quantization.
442
 
443
  A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
444
- B: (E, N, K) FP8 expert weights
445
- Bs: (E,) or (E, 1, 1) per-expert weight scales
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  """
447
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_grouped(
448
- A, B, Bs, offsets, tokens_per_expert
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  )
450
 
451
 
452
- def w8a8_fp8_matmul_grouped(
453
  A: torch.Tensor,
454
  B: torch.Tensor,
455
  Bs: torch.Tensor,
456
  offsets: torch.Tensor,
457
  tokens_per_expert: torch.Tensor,
458
  block_size: list[int] | None,
 
459
  ) -> torch.Tensor:
460
- """Unified grouped W8A8 FP8 matmul dispatcher.
 
461
 
462
- Dispatch rules:
463
- - tensor mode when ``block_size is None``
464
- - tensor mode when ``block_size == [N, K]``
465
- - otherwise block mode
466
 
467
- Returns:
468
- Output tensor ``[S, N]`` in the same dtype as ``A``, in expert-sorted order.
 
 
 
 
469
  """
470
- if block_size is None or (
471
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
472
- ):
473
- return w8a8_tensor_fp8_matmul_grouped(A, B, Bs, offsets, tokens_per_expert)
 
 
 
 
 
474
 
475
- return w8a8_block_fp8_matmul_grouped(
476
- A, B, Bs, offsets, tokens_per_expert, block_size
477
  )
 
15
  import torch
16
  import triton
17
  import triton.language as tl
 
18
  from torch.library import triton_op, wrap_triton
19
 
20
+ from ._ops import add_op_namespace_prefix, ops
21
+ from .bayesian_autotuner import bayesian_autotune
22
+ from .utils import (
23
+ MX_SCALE_GROUP_K,
24
+ NIBBLES_PER_BYTE,
25
+ adaptive_block_size_m,
26
+ device_context,
27
+ mxfp_act_quant_inline,
28
+ mxfp4_e2m1_to_e4m3,
29
+ fp8_act_quant,
30
+ fp8_act_quant_inline,
31
+ get_accelerator_autotuning_configs,
32
+ get_mxfp_autotuning_configs,
33
+ is_mxfp,
34
+ is_tensor_wide,
35
+ e2m1_as_uint8,
36
+ ue8m0_as_uint8,
37
+ decode_ue8m0_scale,
38
+ mx_dot_rescale,
39
+ mx_dot_scaled,
40
+ )
41
+
42
+
43
+ def _grouped_tile_layout(
44
+ tokens_per_expert: torch.Tensor,
45
+ block_size_m: int,
46
+ S: int,
47
+ num_experts: int,
48
+ ) -> tuple[torch.Tensor, int]:
49
+ """Compute the M-tile layout for the grouped kernels.
50
+
51
+ Returns ``(tile_offsets, max_m_tiles)``:
52
+ - ``tile_offsets``: int32 (num_experts,) cumulative tile-end per expert, used by
53
+ ``_grouped_tile_setup`` to locate an M-tile's owning expert.
54
+ - ``max_m_tiles``: upper bound on total M-tiles, used as the grid axis-0
55
+ size. Real tile count <= this; surplus programs early-return inside the
56
+ kernel. Keeps the grid data-independent (cuda-graph / torch.compile safe).
57
+ """
58
+ tiles_per_expert = (tokens_per_expert + block_size_m - 1) // block_size_m
59
+ tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
60
+ max_m_tiles = triton.cdiv(S, block_size_m) + num_experts
61
+ return tile_offsets, max_m_tiles
62
+
63
+
64
+ @triton.jit
65
+ def _grouped_tile_setup(
66
+ pid_m,
67
+ pid_n,
68
+ Offsets,
69
+ TileOffsets,
70
+ stride_offs,
71
+ stride_tile,
72
+ num_experts,
73
+ NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
74
+ BLOCK_SIZE_M: tl.constexpr,
75
+ BLOCK_SIZE_N: tl.constexpr,
76
+ BLOCK_SIZE_K: tl.constexpr,
77
+ ):
78
+ """Map a grouped M-tile to its expert and build the per-tile offset vectors —
79
+ the prologue shared by every grouped kernel.
80
+
81
+ Returns ``(expert_id, offs_global_m, row_mask, offs_bn, offs_k)``:
82
+ - ``expert_id``: int64 owning expert
83
+ - ``offs_global_m``: ``(BLOCK_SIZE_M,)`` global row indices into A
84
+ - ``row_mask``: ``(BLOCK_SIZE_M,)`` validity mask within the expert's M
85
+ - ``offs_bn``: ``(BLOCK_SIZE_N,)`` output column offsets
86
+ - ``offs_k``: ``(BLOCK_SIZE_K,)`` K range
87
+
88
+ Caller must have early-returned if ``pid_m`` exceeds total_tiles
89
+ (``TileOffsets[(num_experts - 1) * stride_tile]``) — the ``Offsets`` load below
90
+ is out of bounds for an out-of-range tile otherwise.
91
+ """
92
+ # Binary search: upper_bound(TileOffsets, pid_m). NUM_EXPERTS_BIT_LENGTH is
93
+ # ceil(log2(num_experts))+1, giving one harmless extra iteration; constexpr so the
94
+ # loop unrolls.
95
+ lo = 0
96
+ hi = num_experts
97
+ for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
98
+ mid = (lo + hi) >> 1
99
+ mid_val = tl.load(TileOffsets + mid * stride_tile)
100
+ is_left = mid_val <= pid_m
101
+ lo = tl.where(is_left, mid + 1, lo)
102
+ hi = tl.where(is_left, hi, mid)
103
+ # Cast to int64 so ``expert_id * stride_be`` doesn't overflow for large num_experts
104
+ # × large weight matrices (e.g. 255 * 9_437_184 > 2^31).
105
+ expert_id = lo.to(tl.int64)
106
+
107
+ prev_eid = tl.maximum(expert_id - 1, 0)
108
+ expert_start = tl.where(
109
+ expert_id == 0, 0, tl.load(Offsets + prev_eid * stride_offs)
110
+ )
111
+ expert_end = tl.load(Offsets + expert_id * stride_offs)
112
+ M_expert = expert_end - expert_start
113
+
114
+ expert_tile_start = tl.where(
115
+ expert_id == 0, 0, tl.load(TileOffsets + prev_eid * stride_tile)
116
+ )
117
+ local_tile = pid_m - expert_tile_start
118
+ m_off = local_tile * BLOCK_SIZE_M
119
+
120
+ offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
121
+ row_mask = offs_am < M_expert
122
+ offs_global_m = expert_start + offs_am
123
+ offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
124
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
125
+ return expert_id, offs_global_m, row_mask, offs_bn, offs_k
126
+
127
+
128
+ @triton.jit
129
+ def store_tile(C, accumulator, offs_global_m, offs_bn, row_mask, stride_cm, stride_cn):
130
+ """Output epilogue shared by the grouped kernels: cast the fp32 accumulator to
131
+ ``C``'s dtype and store the tile at expert-sorted global rows ``offs_global_m`` ×
132
+ columns ``offs_bn``, masked to the expert's valid rows (``row_mask``)."""
133
+ c = accumulator.to(C.dtype.element_ty)
134
+ c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
135
+ tl.store(c_ptrs, c, mask=row_mask[:, None])
136
 
137
 
138
  @triton.autotune(
139
+ configs=get_accelerator_autotuning_configs(),
 
 
 
 
140
  key=["N", "K", "BLOCK_SIZE_M"],
141
  )
142
  @triton.jit
143
+ def w8a8_block_dynamic_fp8_matmul_grouped_kernel(
144
+ A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert id
145
+ B, # (num_experts, N, K) FP8 weight matrices
146
+ C, # (S, N) output
147
+ Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
148
+ Offsets, # (num_experts,) int32 — cumulative row-end per expert
149
+ TileOffsets, # (num_experts,) int32 — cumulative tile-end per expert
150
  # Shape
151
  S,
152
  N,
 
162
  stride_bs_e,
163
  stride_bs_k,
164
  stride_bs_n,
165
+ stride_offs,
166
+ stride_tile,
167
+ num_experts,
168
  # Meta-parameters
169
+ BLOCK_SIZE_M: tl.constexpr,
170
  BLOCK_SIZE_N: tl.constexpr,
171
  BLOCK_SIZE_K: tl.constexpr,
 
172
  NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
173
  ):
174
  """Block-scale grouped FP8 expert matmul kernel.
 
179
  pid_m = tl.program_id(axis=0)
180
  pid_n = tl.program_id(axis=1)
181
 
182
+ total_tiles = tl.load(TileOffsets + (num_experts - 1) * stride_tile)
 
183
  if pid_m >= total_tiles:
184
  return
185
 
186
+ expert_id, offs_global_m, row_mask, offs_bn, offs_k = _grouped_tile_setup(
187
+ pid_m,
188
+ pid_n,
189
+ Offsets,
190
+ TileOffsets,
191
+ stride_offs,
192
+ stride_tile,
193
+ num_experts,
194
+ NUM_EXPERTS_BIT_LENGTH,
195
+ BLOCK_SIZE_M,
196
+ BLOCK_SIZE_N,
197
+ BLOCK_SIZE_K,
198
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
201
  b_ptrs = (
 
208
 
209
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
210
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
 
211
  a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
212
+ a, a_s = fp8_act_quant_inline(a_raw)
 
 
213
  b = tl.load(b_ptrs)
214
+ b_s = decode_ue8m0_scale(tl.load(bs_ptrs))
215
  accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
216
  a_ptrs += BLOCK_SIZE_K * stride_ak
217
  b_ptrs += BLOCK_SIZE_K * stride_bk
218
+ bs_ptrs += stride_bs_k
219
 
220
+ store_tile(C, accumulator, offs_global_m, offs_bn, row_mask, stride_cm, stride_cn)
 
 
 
 
 
 
 
 
 
221
 
222
 
223
+ @bayesian_autotune(
224
+ get_accelerator_autotuning_configs(tune_block_nk=True),
225
+ ["N", "K", "BLOCK_SIZE_M"],
226
+ n_trials=60,
 
 
 
227
  )
228
  @triton.jit
229
+ def w8a8_tensor_dynamic_fp8_matmul_grouped_kernel(
230
+ A, # (S, K) pre-quantized FP8 activations, sorted/grouped by expert id
231
+ B, # (num_experts, N, K) FP8 weight matrices
232
  C, # (S, N) output
233
+ As, # (S,) per-token activation scales
234
+ Bs, # (num_experts, 1, 1) per-tensor weight scales
235
+ Offsets, # (num_experts,) int32 — cumulative row-end per expert
236
+ TileOffsets, # (num_experts,) int32 — cumulative tile-end per expert
237
+ # Shape
238
  S,
239
  N,
240
  K,
241
+ # Strides
242
  stride_am,
243
  stride_ak,
244
  stride_be,
 
248
  stride_cn,
249
  stride_as_m,
250
  stride_bs_e,
251
+ stride_offs,
252
+ stride_tile,
253
+ num_experts,
254
+ # Meta-parameters
255
+ BLOCK_SIZE_M: tl.constexpr,
256
  BLOCK_SIZE_N: tl.constexpr,
257
  BLOCK_SIZE_K: tl.constexpr,
 
258
  NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
259
  ):
260
  """Tensor-scale grouped FP8 expert matmul kernel.
 
265
  pid_m = tl.program_id(axis=0)
266
  pid_n = tl.program_id(axis=1)
267
 
268
+ total_tiles = tl.load(TileOffsets + (num_experts - 1) * stride_tile)
269
  if pid_m >= total_tiles:
270
  return
271
 
272
+ expert_id, offs_global_m, row_mask, offs_bn, offs_k = _grouped_tile_setup(
273
+ pid_m,
274
+ pid_n,
275
+ Offsets,
276
+ TileOffsets,
277
+ stride_offs,
278
+ stride_tile,
279
+ num_experts,
280
+ NUM_EXPERTS_BIT_LENGTH,
281
+ BLOCK_SIZE_M,
282
+ BLOCK_SIZE_N,
283
+ BLOCK_SIZE_K,
284
+ )
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
287
  b_ptrs = (
 
298
  for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
299
  a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
300
  b = tl.load(b_ptrs)
 
301
  accumulator += tl.dot(a, b)
302
  a_ptrs += BLOCK_SIZE_K * stride_ak
303
  b_ptrs += BLOCK_SIZE_K * stride_bk
 
304
  accumulator = accumulator * a_s[:, None] * b_s
305
 
306
+ store_tile(C, accumulator, offs_global_m, offs_bn, row_mask, stride_cm, stride_cn)
 
 
 
 
 
307
 
 
 
 
308
 
309
+ @bayesian_autotune(
310
+ get_mxfp_autotuning_configs(
311
+ compute_modes=("dot_scaled", "dot")
312
+ ), # prefill: no scalar branch
313
+ ["N", "K", "BLOCK_SIZE_M"],
314
+ n_trials=60,
315
+ )
316
+ @triton.jit
317
+ def mxfp_dynamic_matmul_grouped_kernel(
318
+ A, # (S, K) raw BF16/FP16 activations, sorted by expert id
319
+ B, # (num_experts, N, K) E4M3 (MXFP8) or (num_experts, N, K // 2) packed E2M1 (MXFP4) expert weights
320
+ C, # (S, N) output
321
+ Bs, # (num_experts, N, K // SCALE_GROUP_K) UE8M0 weight scales
322
+ Offsets, # (num_experts,) int32 — cumulative row-end per expert
323
+ TileOffsets, # (num_experts,) int32 — cumulative tile-end per expert
324
+ # Shape
325
+ S,
326
+ N,
327
+ K,
328
+ # Strides
329
+ stride_am,
330
+ stride_ak,
331
+ stride_be,
332
+ stride_bk,
333
+ stride_bn,
334
+ stride_cm,
335
+ stride_cn,
336
+ stride_bs_e,
337
+ stride_bs_k,
338
+ stride_bs_n,
339
+ stride_offs,
340
+ stride_tile,
341
+ num_experts,
342
+ # Meta-parameters
343
+ BLOCK_SIZE_M: tl.constexpr,
344
+ BLOCK_SIZE_N: tl.constexpr,
345
+ BLOCK_SIZE_K: tl.constexpr,
346
+ NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
347
+ VALUES_PER_BYTE: tl.constexpr,
348
+ SCALE_GROUP_K: tl.constexpr,
349
+ COMPUTE_MODE: tl.constexpr,
350
+ ):
351
+ """Unified grouped MXFP4/MXFP8 (W4A8/W8A8) expert matmul with fused act quant.
352
+
353
+ Tokens sorted by expert; each M-tile maps to its expert via ``TileOffsets``. ``A`` is
354
+ quantized to E4M3 per K-group inline (UE8M0 scale). ``VALUES_PER_BYTE`` picks the
355
+ weight format (2 = packed E2M1 / MXFP4, 1 = unpacked E4M3 / MXFP8); ``COMPUTE_MODE``
356
+ picks ``tl.dot_scaled`` vs fp8 ``tl.dot`` + per-group rescale (decode; FP4 unpacks
357
+ E2M1->E4M3 first, lossless).
358
+ """
359
+ pid_m = tl.program_id(axis=0)
360
+ pid_n = tl.program_id(axis=1)
361
+
362
+ total_tiles = tl.load(TileOffsets + (num_experts - 1) * stride_tile)
363
+ if pid_m >= total_tiles:
364
+ return
365
+
366
+ expert_id, offs_global_m, row_mask, offs_bn, offs_k = _grouped_tile_setup(
367
+ pid_m,
368
+ pid_n,
369
+ Offsets,
370
+ TileOffsets,
371
+ stride_offs,
372
+ stride_tile,
373
+ num_experts,
374
+ NUM_EXPERTS_BIT_LENGTH,
375
+ BLOCK_SIZE_M,
376
+ BLOCK_SIZE_N,
377
+ BLOCK_SIZE_K,
378
+ )
379
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)
380
+ offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
381
+
382
+ a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
383
+ b_ptrs = (
384
+ B
385
+ + expert_id * stride_be
386
+ + offs_kb[:, None] * stride_bk
387
+ + offs_bn[None, :] * stride_bn
388
+ )
389
+ bs_ptrs = (
390
+ Bs
391
+ + expert_id * stride_bs_e
392
+ + offs_bn[:, None] * stride_bs_n
393
+ + offs_sf[None, :] * stride_bs_k
394
+ )
395
 
396
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
397
+ for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
398
+ a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
399
+ a, a_scale = mxfp_act_quant_inline(
400
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K
401
+ )
402
+ b = tl.load(b_ptrs)
403
+ b_s = tl.load(bs_ptrs).to(tl.uint8)
404
+ bq = mxfp4_e2m1_to_e4m3(b) if VALUES_PER_BYTE == 2 else b
405
+ if COMPUTE_MODE == "dot_scaled":
406
+ accumulator = mx_dot_scaled(
407
+ a, a_scale, b, b_s, accumulator, VALUES_PER_BYTE
408
+ )
409
+ else: # dot
410
+ accumulator = mx_dot_rescale(accumulator, a, bq, a_scale, b_s)
411
+ a_ptrs += BLOCK_SIZE_K * stride_ak
412
+ b_ptrs += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_bk
413
+ bs_ptrs += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_bs_k
414
+
415
+ store_tile(C, accumulator, offs_global_m, offs_bn, row_mask, stride_cm, stride_cn)
416
+
417
+
418
+ @triton_op(
419
+ add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul_grouped"), mutates_args=()
420
+ )
421
+ def _w8a8_block_dynamic_fp8_matmul_grouped(
422
  A: torch.Tensor,
423
  B: torch.Tensor,
424
  Bs: torch.Tensor,
425
  offsets: torch.Tensor,
426
  tokens_per_expert: torch.Tensor,
427
  block_size: list[int],
428
+ output_dtype: torch.dtype | None = None,
429
  ) -> torch.Tensor:
430
  """Block-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
431
 
432
  A: (S, K) raw bf16/fp16 activations, sorted by expert
433
+ B: (num_experts, N, K) FP8 expert weights
434
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales
435
  """
436
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
437
  assert A.is_contiguous(), "A must be contiguous"
438
+ assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
439
  assert B.is_contiguous(), "B must be contiguous"
440
  assert A.shape[1] == B.shape[2], (
441
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
442
  )
443
 
444
  S, K = A.shape
445
+ num_experts, N, _ = B.shape
446
 
447
  assert len(block_size) == 2, (
448
  f"block_size must be [block_n, block_k], got {block_size}"
 
452
  assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
453
  assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
454
  assert Bs.ndim == 3, (
455
+ f"Bs must be 3D (num_experts, N//block_n, K//block_k), got ndim={Bs.ndim}"
456
  )
457
+ assert Bs.shape == (num_experts, N // block_n, K // block_k), (
458
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {N // block_n}, {K // block_k})"
459
  )
460
 
461
+ Bs = ue8m0_as_uint8(Bs)
462
+ C = A.new_empty(S, N, dtype=output_dtype)
463
+ BLOCK_SIZE_M = adaptive_block_size_m((S + num_experts - 1) // num_experts)
464
+ tile_offsets, max_m_tiles = _grouped_tile_layout(
465
+ tokens_per_expert, BLOCK_SIZE_M, S, num_experts
466
+ )
467
+ grid = (max_m_tiles, triton.cdiv(N, block_n))
468
+
 
 
 
469
  with device_context(A.device):
470
+ wrap_triton(w8a8_block_dynamic_fp8_matmul_grouped_kernel)[grid](
471
  A,
472
  B,
473
  C,
 
487
  Bs.stride(0),
488
  Bs.stride(2),
489
  Bs.stride(1),
490
+ offsets.stride(0),
491
+ tile_offsets.stride(0),
492
  # Meta-parameters
493
+ num_experts=num_experts,
494
  BLOCK_SIZE_N=block_n,
495
  BLOCK_SIZE_K=block_k,
496
  BLOCK_SIZE_M=BLOCK_SIZE_M,
497
+ NUM_EXPERTS_BIT_LENGTH=num_experts.bit_length(),
498
  )
499
 
500
  return C
501
 
502
 
503
+ @triton_op(
504
+ add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul_grouped"), mutates_args=()
505
+ )
506
+ def _w8a8_tensor_dynamic_fp8_matmul_grouped(
507
  A: torch.Tensor,
508
  B: torch.Tensor,
509
  Bs: torch.Tensor,
510
  offsets: torch.Tensor,
511
  tokens_per_expert: torch.Tensor,
512
+ output_dtype: torch.dtype | None = None,
513
  ) -> torch.Tensor:
514
  """Tensor-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
515
 
516
  A: (S, K) raw bf16/fp16 activations, sorted by expert
517
+ B: (num_experts, N, K) FP8 expert weights
518
+ Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
519
  """
520
  assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
521
  assert A.is_contiguous(), "A must be contiguous"
522
+ assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
523
  assert B.is_contiguous(), "B must be contiguous"
524
  assert A.shape[1] == B.shape[2], (
525
  f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
526
  )
527
 
528
  S, K = A.shape
529
+ num_experts, N, _ = B.shape
530
 
531
+ # Normalize Bs to (num_experts, 1, 1)
532
  if Bs.ndim == 1:
533
+ assert Bs.shape[0] == num_experts, (
534
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts},)"
535
+ )
536
+ Bs = Bs.reshape(num_experts, 1, 1)
537
  else:
538
+ assert Bs.shape == (num_experts, 1, 1), (
539
+ f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, 1, 1)"
540
  )
541
 
 
 
 
542
  qA, As = fp8_act_quant(A, K)
543
+ C = A.new_empty(S, N, dtype=output_dtype)
544
+ BLOCK_SIZE_M = adaptive_block_size_m((S + num_experts - 1) // num_experts)
545
+ tile_offsets, max_m_tiles = _grouped_tile_layout(
546
+ tokens_per_expert, BLOCK_SIZE_M, S, num_experts
547
+ )
548
+
549
+ def grid(META):
550
+ return (max_m_tiles, triton.cdiv(N, META["BLOCK_SIZE_N"]))
551
+
552
  with device_context(A.device):
553
+ wrap_triton(w8a8_tensor_dynamic_fp8_matmul_grouped_kernel)[grid](
554
  qA,
555
  B,
556
  C,
 
570
  C.stride(1),
571
  As.stride(0),
572
  Bs.stride(0),
573
+ offsets.stride(0),
574
+ tile_offsets.stride(0),
575
+ # Meta-parameters (BLOCK_SIZE_N, BLOCK_SIZE_K come from autotune Config)
576
+ num_experts=num_experts,
577
  BLOCK_SIZE_M=BLOCK_SIZE_M,
578
+ NUM_EXPERTS_BIT_LENGTH=num_experts.bit_length(),
579
  )
580
 
581
  return C
582
 
583
 
584
+ def w8a8_block_dynamic_fp8_matmul_grouped(
585
  A: torch.Tensor,
586
  B: torch.Tensor,
587
  Bs: torch.Tensor,
588
  offsets: torch.Tensor,
589
  tokens_per_expert: torch.Tensor,
590
  block_size: list[int],
591
+ output_dtype: torch.dtype | None = None,
592
  ) -> torch.Tensor:
593
  """Block-scale grouped FP8 matmul with fused activation quantization.
594
 
595
  A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
596
+ B: (num_experts, N, K) FP8 expert weights
597
+ Bs: (num_experts, N // block_n, K // block_k) per-block weight scales
598
+ output_dtype: defaults to ``A.dtype``
599
  """
600
+ return ops.w8a8_block_dynamic_fp8_matmul_grouped(
601
+ A, B, Bs, offsets, tokens_per_expert, block_size, output_dtype
602
  )
603
 
604
 
605
+ def w8a8_tensor_dynamic_fp8_matmul_grouped(
606
  A: torch.Tensor,
607
  B: torch.Tensor,
608
  Bs: torch.Tensor,
609
  offsets: torch.Tensor,
610
  tokens_per_expert: torch.Tensor,
611
+ output_dtype: torch.dtype | None = None,
612
  ) -> torch.Tensor:
613
  """Tensor-scale grouped FP8 matmul with fused activation quantization.
614
 
615
  A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
616
+ B: (num_experts, N, K) FP8 expert weights
617
+ Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
618
+ output_dtype: defaults to ``A.dtype``
619
+ """
620
+ return ops.w8a8_tensor_dynamic_fp8_matmul_grouped(
621
+ A, B, Bs, offsets, tokens_per_expert, output_dtype
622
+ )
623
+
624
+
625
+ @triton_op(add_op_namespace_prefix("mxfp_dynamic_matmul_grouped"), mutates_args=())
626
+ def _mxfp_dynamic_matmul_grouped(
627
+ A: torch.Tensor,
628
+ B: torch.Tensor,
629
+ Bs: torch.Tensor,
630
+ offsets: torch.Tensor,
631
+ tokens_per_expert: torch.Tensor,
632
+ output_dtype: torch.dtype | None = None,
633
+ ) -> torch.Tensor:
634
+ """Grouped MX matmul with fused act quant — per-expert ``C[s] = A[s] @ B[e].T`` over
635
+ contiguous, expert-sorted rows. Weight format detected from ``B.dtype``: ``int8`` →
636
+ packed E2M1 (MXFP4, ``B`` is ``(num_experts, N, K//2)``); ``float8_e4m3fn`` → unpacked E4M3
637
+ (MXFP8, ``(num_experts, N, K)``). UE8M0 group-32 scales ``(num_experts, N, K//32)``; tile + dot autotuned.
638
+
639
+ A: (S, K) raw activations, bf16/fp16/fp32, expert-sorted (quantized inline to E4M3)
640
+ offsets: (num_experts,) — exclusive prefix of expert token counts (cumsum)
641
+ tokens_per_expert: (num_experts,) — per-expert row count
642
  """
643
+ assert A.ndim == 2 and B.ndim == 3 and Bs.ndim == 3
644
+ assert offsets.ndim == 1 and tokens_per_expert.ndim == 1
645
+ assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
646
+ f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
647
+ )
648
+ assert Bs.dtype == torch.float8_e8m0fnu, (
649
+ f"Bs must be float8_e8m0fnu, got {Bs.dtype}"
650
+ )
651
+ VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
652
+
653
+ S, K = A.shape
654
+ num_experts, N, K_b = B.shape
655
+ assert offsets.shape[0] == num_experts and tokens_per_expert.shape[0] == num_experts
656
+ assert K == VALUES_PER_BYTE * K_b, (
657
+ f"K (={K}) must equal {VALUES_PER_BYTE} * B.shape[2] (={K_b})"
658
+ )
659
+ assert K % MX_SCALE_GROUP_K == 0, (
660
+ f"K (={K}) must be a multiple of {MX_SCALE_GROUP_K}"
661
+ )
662
+ assert Bs.shape == (num_experts, N, K // MX_SCALE_GROUP_K), (
663
+ f"Bs shape {tuple(Bs.shape)} != ({num_experts}, {N}, {K // MX_SCALE_GROUP_K})"
664
+ )
665
+
666
+ B = e2m1_as_uint8(B)
667
+ bs_u8 = ue8m0_as_uint8(Bs)
668
+ C = A.new_empty((S, N), dtype=output_dtype)
669
+ BLOCK_SIZE_M = adaptive_block_size_m((S + num_experts - 1) // num_experts)
670
+ tile_offsets, max_m_tiles = _grouped_tile_layout(
671
+ tokens_per_expert, BLOCK_SIZE_M, S, num_experts
672
+ )
673
+
674
+ def grid(META):
675
+ return (max_m_tiles, triton.cdiv(N, META["BLOCK_SIZE_N"]))
676
+
677
+ with device_context(A.device):
678
+ wrap_triton(mxfp_dynamic_matmul_grouped_kernel)[grid](
679
+ A,
680
+ B,
681
+ C,
682
+ bs_u8,
683
+ offsets,
684
+ tile_offsets,
685
+ S,
686
+ N,
687
+ K,
688
+ A.stride(0),
689
+ A.stride(1),
690
+ B.stride(0),
691
+ B.stride(2),
692
+ B.stride(1),
693
+ C.stride(0),
694
+ C.stride(1),
695
+ bs_u8.stride(0),
696
+ bs_u8.stride(2),
697
+ bs_u8.stride(1),
698
+ offsets.stride(0),
699
+ tile_offsets.stride(0),
700
+ # Meta-parameters (BLOCK_SIZE_N, BLOCK_SIZE_K come from autotune Config)
701
+ num_experts=num_experts,
702
+ BLOCK_SIZE_M=BLOCK_SIZE_M,
703
+ NUM_EXPERTS_BIT_LENGTH=num_experts.bit_length(),
704
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
705
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
706
+ )
707
+ return C
708
+
709
+
710
+ def mxfp_dynamic_matmul_grouped(
711
+ A: torch.Tensor,
712
+ B: torch.Tensor,
713
+ Bs: torch.Tensor,
714
+ offsets: torch.Tensor,
715
+ tokens_per_expert: torch.Tensor,
716
+ output_dtype: torch.dtype | None = None,
717
+ ) -> torch.Tensor:
718
+ """Grouped MX (MXFP4/MXFP8) matmul with fused act quant — per-expert
719
+ ``C[s] = A[s] @ B[e].T`` over expert-sorted rows; weight format detected from
720
+ ``B.dtype``. Tile + dot path autotuned."""
721
+ return ops.mxfp_dynamic_matmul_grouped(
722
+ A, B, Bs, offsets, tokens_per_expert, output_dtype
723
  )
724
 
725
 
726
+ def matmul_grouped(
727
  A: torch.Tensor,
728
  B: torch.Tensor,
729
  Bs: torch.Tensor,
730
  offsets: torch.Tensor,
731
  tokens_per_expert: torch.Tensor,
732
  block_size: list[int] | None,
733
+ output_dtype: torch.dtype | None = None,
734
  ) -> torch.Tensor:
735
+ """Grouped quantized matmul dispatcher (W8A8 FP8 or W4A8 FP4). Tokens must
736
+ be sorted by expert; M-tiles are mapped to experts via ``offsets``.
737
 
738
+ ``output_dtype`` defaults to ``A.dtype``.
 
 
 
739
 
740
+ Routes by weight dtype and ``block_size``:
741
+ - MX weights — ``int8`` (packed E2M1) or ``float8_e4m3fn`` (E4M3) with UE8M0
742
+ group-32 ``Bs`` (shape ``[num_experts, N, K//32]``) → ``mxfp_dynamic_matmul_grouped``
743
+ (``block_size`` ignored; tile + dot path autotuned).
744
+ - ``block_size`` None or full ``[N, K]`` → ``w8a8_tensor_dynamic_fp8_matmul_grouped``.
745
+ - otherwise → ``w8a8_block_dynamic_fp8_matmul_grouped``.
746
  """
747
+ if is_mxfp(B, Bs):
748
+ return mxfp_dynamic_matmul_grouped(
749
+ A, B, Bs, offsets, tokens_per_expert, output_dtype
750
+ )
751
+
752
+ if is_tensor_wide(block_size, B):
753
+ return w8a8_tensor_dynamic_fp8_matmul_grouped(
754
+ A, B, Bs, offsets, tokens_per_expert, output_dtype
755
+ )
756
 
757
+ return w8a8_block_dynamic_fp8_matmul_grouped(
758
+ A, B, Bs, offsets, tokens_per_expert, block_size, output_dtype
759
  )
build/torch-cuda/matmul.py CHANGED
@@ -17,38 +17,103 @@ import triton
17
  import triton.language as tl
18
  from torch.library import triton_op, wrap_triton
19
 
20
- from .utils import device_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
- # Adapted from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/layers/quantization/fp8_kernel.py
24
  @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4]
29
- ],
30
  key=["N", "K", "BLOCK_SIZE_M"],
31
  )
32
  @triton.jit
33
- def w8a8_block_fp8_matmul_kernel(
34
- # Pointers to inputs and output
35
- A,
36
- B,
37
- C,
38
- As,
39
- Bs,
40
- # Shape for matmul
41
  M,
42
  N,
43
  K,
 
44
  stride_am,
45
  stride_ak,
46
  stride_bk,
47
  stride_bn,
48
  stride_cm,
49
  stride_cn,
50
- stride_as_m,
51
- stride_as_k,
52
  stride_bs_k,
53
  stride_bs_n,
54
  # Meta-parameters
@@ -57,72 +122,65 @@ def w8a8_block_fp8_matmul_kernel(
57
  BLOCK_SIZE_K: tl.constexpr,
58
  GROUP_SIZE_M: tl.constexpr,
59
  ):
60
- """Block-scale FP8 GEMM kernel.
61
 
62
- Computes ``C = A @ B.T`` with block-wise activation/weight scales.
63
- Uses a 2D grid with swizzle for L2 cache locality on B tiles.
 
64
  """
65
- pid_m = tl.program_id(axis=0)
66
- pid_n = tl.program_id(axis=1)
67
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
68
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
69
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
70
-
71
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
72
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
73
- offs_k = tl.arange(0, BLOCK_SIZE_K)
74
  a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
75
  b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
76
 
77
- as_ptrs = As + offs_am * stride_as_m
78
  offs_bsn = offs_bn // BLOCK_SIZE_N
79
  bs_ptrs = Bs + offs_bsn * stride_bs_n
80
 
81
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
82
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
83
  k_remaining = K - k * BLOCK_SIZE_K
84
- a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0)
 
 
 
85
  b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
86
-
87
- a_s = tl.load(as_ptrs + k * stride_as_k)
88
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
89
-
90
  accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
91
  a_ptrs += BLOCK_SIZE_K * stride_ak
92
  b_ptrs += BLOCK_SIZE_K * stride_bk
93
-
94
- if C.dtype.element_ty == tl.bfloat16:
95
- c = accumulator.to(tl.bfloat16)
96
- elif C.dtype.element_ty == tl.float16:
97
- c = accumulator.to(tl.float16)
98
- else:
99
- c = accumulator.to(tl.float32)
100
-
101
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
102
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
103
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
104
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
105
- tl.store(c_ptrs, c, mask=c_mask)
 
106
 
107
 
108
  @triton.autotune(
109
- configs=[
110
- triton.Config({}, num_warps=w, num_stages=s)
111
- for w in [2, 4, 8, 16]
112
- for s in [2, 3, 4]
113
- ],
114
  key=["N", "K", "BLOCK_SIZE_M"],
115
  )
116
  @triton.jit
117
- def w8a8_tensor_fp8_matmul_kernel(
118
- A,
119
- B,
120
- C,
121
- As,
122
- Bs,
 
123
  M,
124
  N,
125
  K,
 
126
  stride_am,
127
  stride_ak,
128
  stride_bk,
@@ -130,6 +188,7 @@ def w8a8_tensor_fp8_matmul_kernel(
130
  stride_cm,
131
  stride_cn,
132
  stride_as_m,
 
133
  BLOCK_SIZE_M: tl.constexpr,
134
  BLOCK_SIZE_N: tl.constexpr,
135
  BLOCK_SIZE_K: tl.constexpr,
@@ -141,16 +200,9 @@ def w8a8_tensor_fp8_matmul_kernel(
141
  weight scale for the full matrix.
142
  Uses a 2D grid with swizzle for L2 cache locality on B tiles.
143
  """
144
- pid_m = tl.program_id(axis=0)
145
- pid_n = tl.program_id(axis=1)
146
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
147
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
148
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
149
-
150
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
151
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
152
- offs_k = tl.arange(0, BLOCK_SIZE_K)
153
-
154
  a_ptrs = A + offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak
155
  b_ptrs = B + offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn
156
 
@@ -169,33 +221,201 @@ def w8a8_tensor_fp8_matmul_kernel(
169
 
170
  accumulator = accumulator * a_s[:, None] * b_s
171
 
172
- if C.dtype.element_ty == tl.bfloat16:
173
- c = accumulator.to(tl.bfloat16)
174
- elif C.dtype.element_ty == tl.float16:
175
- c = accumulator.to(tl.float16)
176
- else:
177
- c = accumulator.to(tl.float32)
 
 
 
 
 
 
178
 
179
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
180
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
181
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
182
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
183
- tl.store(c_ptrs, c, mask=c_mask)
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul", mutates_args=())
187
- def _w8a8_block_fp8_matmul(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  A: torch.Tensor,
189
  B: torch.Tensor,
190
- As: torch.Tensor,
191
  Bs: torch.Tensor,
192
  block_size: list[int],
193
- output_dtype: torch.dtype = torch.float32,
194
  ) -> torch.Tensor:
195
- """Block-scale FP8 matmul: C = A @ B.T with per-block scales.
196
 
197
- As: (M, K // block_k) — per-token-group activation scales
198
- Bs: (N // block_n, K // block_k) — per-block weight scales
 
199
  """
200
  assert len(block_size) == 2, (
201
  f"block_size must be [block_n, block_k], got {block_size}"
@@ -212,10 +432,6 @@ def _w8a8_block_fp8_matmul(
212
  N, K = B.shape
213
  M = A.numel() // A.shape[-1]
214
 
215
- assert As.ndim >= 2, f"As must be at least 2D, got ndim={As.ndim}"
216
- assert As.shape[-1] == triton.cdiv(K, block_k), (
217
- f"As last dim {As.shape[-1]} != expected {triton.cdiv(K, block_k)} (cdiv(K={K}, block_k={block_k}))"
218
- )
219
  assert Bs.ndim == 2, f"Bs must be 2D (N//block_n, K//block_k), got ndim={Bs.ndim}"
220
  assert Bs.shape == (triton.cdiv(N, block_n), triton.cdiv(K, block_k)), (
221
  f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(N, block_n)}, {triton.cdiv(K, block_k)})"
@@ -223,20 +439,17 @@ def _w8a8_block_fp8_matmul(
223
 
224
  BLOCK_SIZE_K = block_k
225
  BLOCK_SIZE_N = block_n
 
226
  C_shape = A.shape[:-1] + (N,)
 
227
  C = A.new_empty(C_shape, dtype=output_dtype)
228
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
229
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
230
- # register pressure and a better-matched FP8 WGMMA instruction, improving
231
- # both accuracy and performance for small M (decode).
232
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
233
  grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
 
234
  with device_context(A.device):
235
- wrap_triton(w8a8_block_fp8_matmul_kernel)[grid](
236
  A,
237
  B,
238
  C,
239
- As,
240
  Bs,
241
  M,
242
  N,
@@ -247,64 +460,73 @@ def _w8a8_block_fp8_matmul(
247
  B.stride(0),
248
  C.stride(-2),
249
  C.stride(-1),
250
- As.stride(-2),
251
- As.stride(-1),
252
  Bs.stride(1),
253
  Bs.stride(0),
254
  # Meta-parameters
255
  BLOCK_SIZE_M=BLOCK_SIZE_M,
256
  BLOCK_SIZE_N=BLOCK_SIZE_N,
257
  BLOCK_SIZE_K=BLOCK_SIZE_K,
258
- GROUP_SIZE_M=8,
259
  )
260
 
261
  return C
262
 
263
 
264
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul", mutates_args=())
265
- def _w8a8_tensor_fp8_matmul(
266
  A: torch.Tensor,
267
  B: torch.Tensor,
268
- As: torch.Tensor,
269
  Bs: torch.Tensor,
270
- output_dtype: torch.dtype = torch.float32,
 
 
271
  ) -> torch.Tensor:
272
- """Tensor-scale FP8 matmul: C = A @ B.T with per-row / per-tensor scales.
273
 
274
- As: scalar, (M,), or (M, 1) — per-row activation scales
275
- Bs: scalar, (1,), or (1, 1) — single weight scale
 
 
276
  """
 
 
 
 
 
 
 
 
 
 
 
277
  assert A.shape[-1] == B.shape[-1], (
278
  f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
279
  )
280
  assert A.is_contiguous(), "A must be contiguous"
281
  assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
282
  assert B.is_contiguous(), "B must be contiguous"
 
283
 
284
  N, K = B.shape
285
  M = A.numel() // A.shape[-1]
286
 
287
- # Normalize As to (M,)
288
- if As.numel() == 1:
289
- As = As.reshape(1).expand(M).contiguous()
290
- elif As.ndim == 2:
291
- As = As.reshape(M)
292
- assert As.ndim == 1 and As.shape[0] == M, (
293
- f"As must be scalar, (M,), or (M,1) with M={M}, got {tuple(As.shape)}"
294
  )
295
 
296
- # Normalize Bs to (1,)
297
- assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
298
- Bs = Bs.reshape(1)
 
299
 
300
- BLOCK_SIZE_N = 128
301
- BLOCK_SIZE_K = 128
302
  C_shape = A.shape[:-1] + (N,)
303
  C = A.new_empty(C_shape, dtype=output_dtype)
304
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
305
- grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
306
  with device_context(A.device):
307
- wrap_triton(w8a8_tensor_fp8_matmul_kernel)[grid](
308
  A,
309
  B,
310
  C,
@@ -319,93 +541,277 @@ def _w8a8_tensor_fp8_matmul(
319
  B.stride(0),
320
  C.stride(-2),
321
  C.stride(-1),
322
- As.stride(0),
 
323
  # Meta-parameters
324
  BLOCK_SIZE_M=BLOCK_SIZE_M,
325
  BLOCK_SIZE_N=BLOCK_SIZE_N,
326
  BLOCK_SIZE_K=BLOCK_SIZE_K,
327
- GROUP_SIZE_M=8,
328
  )
329
 
330
  return C
331
 
332
 
333
- def w8a8_block_fp8_matmul(
 
334
  A: torch.Tensor,
335
  B: torch.Tensor,
336
- As: torch.Tensor,
337
  Bs: torch.Tensor,
338
- block_size: list[int],
339
- output_dtype: torch.dtype = torch.float32,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  ) -> torch.Tensor:
341
- """Block-wise W8A8 FP8 matrix multiplication.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
- Computes ``C = A @ B.T`` where both operands are pre-quantized to
344
- ``float8_e4m3fn`` with per-block scales, and accumulates in float32
345
- before casting to ``output_dtype``.
 
 
 
 
 
 
346
 
347
  Args:
348
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
349
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
350
- As: Per-token-group activation scales ``[M, K // block_size[1]]``.
351
  Bs: Per-block weight scales ``[N // block_size[0], K // block_size[1]]``.
352
- block_size: ``[block_n, block_k]`` quantization block dimensions, e.g. ``[128, 128]``.
353
  output_dtype: dtype of the returned tensor (default: ``torch.float32``).
354
 
355
  Returns:
356
- Output tensor ``[M, N]`` in ``output_dtype``.
357
  """
358
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul(
359
- A, B, As, Bs, block_size, output_dtype
360
- )
361
 
362
 
363
- def w8a8_tensor_fp8_matmul(
364
  A: torch.Tensor,
365
  B: torch.Tensor,
 
366
  As: torch.Tensor,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  Bs: torch.Tensor,
368
- output_dtype: torch.dtype = torch.float32,
369
  ) -> torch.Tensor:
370
- """Tensor-scale W8A8 FP8 matrix multiplication.
371
 
372
- Computes ``C = A @ B.T`` in tensor-scale mode using pre-quantized FP8
373
- activations/weights and tensor scales.
374
 
375
  Args:
376
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
377
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
378
- As: Per-row activation scales ``[M]``.
379
  Bs: Single weight scale, scalar or ``[1]``.
380
  output_dtype: dtype of the returned tensor.
 
 
381
 
382
- Returns:
383
- Output tensor ``[M, N]`` in ``output_dtype``.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  """
385
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
386
 
387
 
388
- def w8a8_fp8_matmul(
389
  A: torch.Tensor,
390
  B: torch.Tensor,
391
- As: torch.Tensor,
392
  Bs: torch.Tensor,
393
  block_size: list[int] | None,
394
- output_dtype: torch.dtype = torch.float32,
 
395
  ) -> torch.Tensor:
396
- """Unified W8A8 FP8 matmul dispatcher.
397
 
398
- Dispatch rules:
399
- - tensor mode when ``block_size is None``
400
- - tensor mode when ``block_size == [N, K]``
401
- - otherwise block mode
402
 
403
- Returns:
404
- Output tensor ``[M, N]`` in ``output_dtype``.
405
- """
406
- if block_size is None or (
407
- block_size[0] == B.size(0) and block_size[1] == B.size(1)
408
- ):
409
- return w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
410
 
411
- return w8a8_block_fp8_matmul(A, B, As, Bs, block_size, output_dtype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  import triton.language as tl
18
  from torch.library import triton_op, wrap_triton
19
 
20
+ from ._ops import add_op_namespace_prefix, ops
21
+ from .bayesian_autotuner import bayesian_autotune
22
+ from .utils import (
23
+ NIBBLES_PER_BYTE,
24
+ MX_SCALE_GROUP_K,
25
+ adaptive_block_size_m,
26
+ decode_ue8m0_scale,
27
+ mx_dot_rescale,
28
+ mx_dot_scaled,
29
+ device_context,
30
+ fp8_act_quant,
31
+ fp8_act_quant_inline,
32
+ get_accelerator_autotuning_configs,
33
+ get_mxfp_autotuning_configs,
34
+ is_mxfp,
35
+ is_tensor_wide,
36
+ mxfp_act_quant_inline,
37
+ mxfp4_e2m1_to_e4m3,
38
+ e2m1_as_uint8,
39
+ ue8m0_as_uint8,
40
+ )
41
+
42
+ # Swizzle group size for the 2D-grid kernels' L2-locality tiling (``_swizzle_offsets``) —
43
+ # a perf knob passed as the ``GROUP_SIZE_M`` constexpr, not a correctness parameter.
44
+ GROUP_SIZE_M = 8
45
+
46
+
47
+ @triton.jit
48
+ def _swizzle_offsets(
49
+ M,
50
+ N,
51
+ BLOCK_SIZE_M: tl.constexpr,
52
+ BLOCK_SIZE_N: tl.constexpr,
53
+ BLOCK_SIZE_K: tl.constexpr,
54
+ GROUP_SIZE_M: tl.constexpr,
55
+ ):
56
+ """2D-grid tile scheduling shared by the kernels below: swizzle the
57
+ ``(pid_m, pid_n)`` program ids for L2 locality on B, then build the operand
58
+ offset vectors. Returns ``(pid_m, pid_n, offs_am, offs_bn, offs_k)`` — the
59
+ swizzled ids (reused by the output store) and the ``%``-wrapped row/col offsets
60
+ plus the K range."""
61
+ pid_m = tl.program_id(axis=0)
62
+ pid_n = tl.program_id(axis=1)
63
+ num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
64
+ num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
65
+ pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
66
+ offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
67
+ offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
68
+ offs_k = tl.arange(0, BLOCK_SIZE_K)
69
+ return pid_m, pid_n, offs_am, offs_bn, offs_k
70
+
71
+
72
+ @triton.jit
73
+ def _store_masked(
74
+ C,
75
+ accumulator,
76
+ pid_m,
77
+ pid_n,
78
+ M,
79
+ N,
80
+ stride_cm,
81
+ stride_cn,
82
+ BLOCK_SIZE_M: tl.constexpr,
83
+ BLOCK_SIZE_N: tl.constexpr,
84
+ ):
85
+ """Shared output epilogue of the kernels below: cast the fp32 accumulator to
86
+ ``C``'s dtype and store the ``(BLOCK_SIZE_M, BLOCK_SIZE_N)`` tile at the swizzled
87
+ ``(pid_m, pid_n)``, masked to the ``(M, N)`` bounds."""
88
+ c = accumulator.to(C.dtype.element_ty)
89
+ offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
90
+ offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
91
+ c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
92
+ c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
93
+ tl.store(c_ptrs, c, mask=c_mask)
94
 
95
 
 
96
  @triton.autotune(
97
+ configs=get_accelerator_autotuning_configs(),
 
 
 
 
98
  key=["N", "K", "BLOCK_SIZE_M"],
99
  )
100
  @triton.jit
101
+ def w8a8_block_dynamic_fp8_matmul_kernel(
102
+ A, # (M, K) raw BF16/FP16 activations
103
+ B, # (N, K) FP8 weights
104
+ C, # (M, N) output
105
+ Bs, # (N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (fp32 or uint8/UE8M0)
106
+ # Shape
 
 
107
  M,
108
  N,
109
  K,
110
+ # Strides
111
  stride_am,
112
  stride_ak,
113
  stride_bk,
114
  stride_bn,
115
  stride_cm,
116
  stride_cn,
 
 
117
  stride_bs_k,
118
  stride_bs_n,
119
  # Meta-parameters
 
122
  BLOCK_SIZE_K: tl.constexpr,
123
  GROUP_SIZE_M: tl.constexpr,
124
  ):
125
+ """Block-scale FP8 GEMM kernel with fused activation quantization.
126
 
127
+ Computes ``C = A @ B.T`` with bf16/fp16 ``A`` quantized to FP8 per-K-tile
128
+ inline (one scale per M-row per BLOCK_SIZE_K) and pre-quantized FP8 weights
129
+ with per-block scales. 2D grid with swizzle for L2 cache locality on B.
130
  """
131
+ pid_m, pid_n, offs_am, offs_bn, offs_k = _swizzle_offsets(
132
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GROUP_SIZE_M
133
+ )
 
 
 
 
 
 
134
  a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
135
  b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
136
 
 
137
  offs_bsn = offs_bn // BLOCK_SIZE_N
138
  bs_ptrs = Bs + offs_bsn * stride_bs_n
139
 
140
  accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
141
  for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
142
  k_remaining = K - k * BLOCK_SIZE_K
143
+ a_raw = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0).to(
144
+ tl.float32
145
+ )
146
+ a, a_s = fp8_act_quant_inline(a_raw)
147
  b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
148
+ b_s = decode_ue8m0_scale(tl.load(bs_ptrs))
 
 
 
149
  accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
150
  a_ptrs += BLOCK_SIZE_K * stride_ak
151
  b_ptrs += BLOCK_SIZE_K * stride_bk
152
+ bs_ptrs += stride_bs_k
153
+
154
+ _store_masked(
155
+ C,
156
+ accumulator,
157
+ pid_m,
158
+ pid_n,
159
+ M,
160
+ N,
161
+ stride_cm,
162
+ stride_cn,
163
+ BLOCK_SIZE_M,
164
+ BLOCK_SIZE_N,
165
+ )
166
 
167
 
168
  @triton.autotune(
169
+ configs=get_accelerator_autotuning_configs(tune_block_nk=True),
 
 
 
 
170
  key=["N", "K", "BLOCK_SIZE_M"],
171
  )
172
  @triton.jit
173
+ def w8a8_tensor_dynamic_fp8_matmul_kernel(
174
+ A, # (M, K) pre-quantized FP8 activations
175
+ B, # (N, K) FP8 weights
176
+ C, # (M, N) output
177
+ As, # (M,) per-token activation scales
178
+ Bs, # scalar/(1,) per-tensor weight scale
179
+ # Shape
180
  M,
181
  N,
182
  K,
183
+ # Strides
184
  stride_am,
185
  stride_ak,
186
  stride_bk,
 
188
  stride_cm,
189
  stride_cn,
190
  stride_as_m,
191
+ # Meta-parameters
192
  BLOCK_SIZE_M: tl.constexpr,
193
  BLOCK_SIZE_N: tl.constexpr,
194
  BLOCK_SIZE_K: tl.constexpr,
 
200
  weight scale for the full matrix.
201
  Uses a 2D grid with swizzle for L2 cache locality on B tiles.
202
  """
203
+ pid_m, pid_n, offs_am, offs_bn, offs_k = _swizzle_offsets(
204
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GROUP_SIZE_M
205
+ )
 
 
 
 
 
 
 
206
  a_ptrs = A + offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak
207
  b_ptrs = B + offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn
208
 
 
221
 
222
  accumulator = accumulator * a_s[:, None] * b_s
223
 
224
+ _store_masked(
225
+ C,
226
+ accumulator,
227
+ pid_m,
228
+ pid_n,
229
+ M,
230
+ N,
231
+ stride_cm,
232
+ stride_cn,
233
+ BLOCK_SIZE_M,
234
+ BLOCK_SIZE_N,
235
+ )
236
 
 
 
 
 
 
237
 
238
+ @triton.autotune(
239
+ configs=get_accelerator_autotuning_configs(),
240
+ key=["N", "K", "BLOCK_SIZE_M"],
241
+ )
242
+ @triton.jit
243
+ def w8a8_block_static_fp8_matmul_kernel(
244
+ A, # (M, K) raw BF16/FP16 activations
245
+ B, # (N, K) FP8 weights
246
+ C, # (M, N) output
247
+ As, # scalar — static per-tensor activation scale (calibration-time)
248
+ Bs, # (N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (fp32 or uint8/UE8M0)
249
+ # Shape
250
+ M,
251
+ N,
252
+ K,
253
+ # Strides
254
+ stride_am,
255
+ stride_ak,
256
+ stride_bk,
257
+ stride_bn,
258
+ stride_cm,
259
+ stride_cn,
260
+ stride_bs_k,
261
+ stride_bs_n,
262
+ # Meta-parameters
263
+ BLOCK_SIZE_M: tl.constexpr,
264
+ BLOCK_SIZE_N: tl.constexpr,
265
+ BLOCK_SIZE_K: tl.constexpr,
266
+ GROUP_SIZE_M: tl.constexpr,
267
+ ):
268
+ """Block-scale FP8 GEMM with static (per-tensor) activation scale.
269
+
270
+ ``A`` is raw bf16/fp16; the kernel divides by the scalar ``As`` and casts
271
+ to FP8 inline. Per-block weight scales apply per-K-tile during
272
+ accumulation; the scalar activation scale factors out of the loop and
273
+ is applied once at the end.
274
+ """
275
+ pid_m, pid_n, offs_am, offs_bn, offs_k = _swizzle_offsets(
276
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GROUP_SIZE_M
277
+ )
278
+ a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
279
+ b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
280
+
281
+ offs_bsn = offs_bn // BLOCK_SIZE_N
282
+ bs_ptrs = Bs + offs_bsn * stride_bs_n
283
+ a_s_static = tl.load(As)
284
+
285
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
286
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
287
+ k_remaining = K - k * BLOCK_SIZE_K
288
+ a_raw = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0).to(
289
+ tl.float32
290
+ )
291
+ a = (a_raw / a_s_static).to(tl.float8e4nv)
292
+ b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
293
+ b_s = decode_ue8m0_scale(tl.load(bs_ptrs))
294
+ accumulator += tl.dot(a, b) * b_s[None, :]
295
+ a_ptrs += BLOCK_SIZE_K * stride_ak
296
+ b_ptrs += BLOCK_SIZE_K * stride_bk
297
+ bs_ptrs += stride_bs_k
298
+
299
+ accumulator = accumulator * a_s_static
300
+ _store_masked(
301
+ C,
302
+ accumulator,
303
+ pid_m,
304
+ pid_n,
305
+ M,
306
+ N,
307
+ stride_cm,
308
+ stride_cn,
309
+ BLOCK_SIZE_M,
310
+ BLOCK_SIZE_N,
311
+ )
312
+
313
+
314
+ @bayesian_autotune(
315
+ get_mxfp_autotuning_configs(
316
+ compute_modes=("dot_scaled", "dot")
317
+ ), # no scalar branch here
318
+ ["N", "K", "BLOCK_SIZE_M"],
319
+ n_trials=60,
320
+ )
321
+ @triton.jit
322
+ def mxfp_dynamic_matmul_kernel(
323
+ A, # (M, K) raw BF16/FP16 activations
324
+ B, # (N, K) E4M3 (MXFP8) or (N, K // 2) packed E2M1 (MXFP4) weights
325
+ C, # (M, N) output
326
+ Bs, # (N, K // SCALE_GROUP_K) UE8M0 weight scales
327
+ # Shape
328
+ M,
329
+ N,
330
+ K,
331
+ # Strides
332
+ stride_am,
333
+ stride_ak,
334
+ stride_bk,
335
+ stride_bn,
336
+ stride_cm,
337
+ stride_cn,
338
+ stride_bs_k,
339
+ stride_bs_n,
340
+ # Meta-parameters
341
+ BLOCK_SIZE_M: tl.constexpr,
342
+ BLOCK_SIZE_N: tl.constexpr,
343
+ BLOCK_SIZE_K: tl.constexpr,
344
+ GROUP_SIZE_M: tl.constexpr,
345
+ VALUES_PER_BYTE: tl.constexpr,
346
+ SCALE_GROUP_K: tl.constexpr,
347
+ COMPUTE_MODE: tl.constexpr,
348
+ ):
349
+ """Unified MXFP4/MXFP8 (W4A8/W8A8) GEMM with fused activation quantization.
350
+
351
+ ``C = A @ B.T`` with bf16/fp16 ``A`` quantized to E4M3 per K-group inline (UE8M0
352
+ scale). ``VALUES_PER_BYTE`` picks the weight format: 2 = packed E2M1 (MXFP4),
353
+ 1 = unpacked E4M3 (MXFP8). ``COMPUTE_MODE`` picks the MMA: ``tl.dot_scaled``
354
+ (native M=128 scaled MMA) vs fp8 ``tl.dot`` + per-group software rescale (wins at
355
+ decode; FP4 unpacks E2M1->E4M3 first — lossless). 2D grid with swizzle for L2 reuse.
356
+ """
357
+ pid_m, pid_n, offs_am, offs_bn, offs_k = _swizzle_offsets(
358
+ M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GROUP_SIZE_M
359
+ )
360
+ offs_kb = tl.arange(0, BLOCK_SIZE_K // VALUES_PER_BYTE)
361
+ offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
362
+ a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
363
+ b_ptrs = B + (offs_kb[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
364
+ bs_ptrs = Bs + (offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k)
365
 
366
+ accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
367
+ for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
368
+ k_remaining = K - k * BLOCK_SIZE_K
369
+ a_raw = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0).to(
370
+ tl.float32
371
+ )
372
+ a, a_scale = mxfp_act_quant_inline(
373
+ a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K
374
+ )
375
+ b = tl.load(
376
+ b_ptrs, mask=offs_kb[:, None] < k_remaining // VALUES_PER_BYTE, other=0.0
377
+ )
378
+ b_s = tl.load(
379
+ bs_ptrs, mask=offs_sf[None, :] < k_remaining // SCALE_GROUP_K, other=0
380
+ ).to(tl.uint8)
381
+ bq = mxfp4_e2m1_to_e4m3(b) if VALUES_PER_BYTE == 2 else b
382
+ if COMPUTE_MODE == "dot_scaled":
383
+ accumulator = mx_dot_scaled(
384
+ a, a_scale, b, b_s, accumulator, VALUES_PER_BYTE
385
+ )
386
+ else: # dot
387
+ accumulator = mx_dot_rescale(accumulator, a, bq, a_scale, b_s)
388
+ a_ptrs += BLOCK_SIZE_K * stride_ak
389
+ b_ptrs += (BLOCK_SIZE_K // VALUES_PER_BYTE) * stride_bk
390
+ bs_ptrs += (BLOCK_SIZE_K // SCALE_GROUP_K) * stride_bs_k
391
+
392
+ _store_masked(
393
+ C,
394
+ accumulator,
395
+ pid_m,
396
+ pid_n,
397
+ M,
398
+ N,
399
+ stride_cm,
400
+ stride_cn,
401
+ BLOCK_SIZE_M,
402
+ BLOCK_SIZE_N,
403
+ )
404
+
405
+
406
+ @triton_op(add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul"), mutates_args=())
407
+ def _w8a8_block_dynamic_fp8_matmul(
408
  A: torch.Tensor,
409
  B: torch.Tensor,
 
410
  Bs: torch.Tensor,
411
  block_size: list[int],
412
+ output_dtype: torch.dtype | None = None,
413
  ) -> torch.Tensor:
414
+ """Block-scale FP8 matmul: ``C = A @ B.T`` with fused activation quantization.
415
 
416
+ A: (..., K) raw activations, bf16/fp16/fp32 (quantized inline to FP8)
417
+ B: (N, K) FP8 weights
418
+ Bs: (N // block_n, K // block_k) per-block weight scales
419
  """
420
  assert len(block_size) == 2, (
421
  f"block_size must be [block_n, block_k], got {block_size}"
 
432
  N, K = B.shape
433
  M = A.numel() // A.shape[-1]
434
 
 
 
 
 
435
  assert Bs.ndim == 2, f"Bs must be 2D (N//block_n, K//block_k), got ndim={Bs.ndim}"
436
  assert Bs.shape == (triton.cdiv(N, block_n), triton.cdiv(K, block_k)), (
437
  f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(N, block_n)}, {triton.cdiv(K, block_k)})"
 
439
 
440
  BLOCK_SIZE_K = block_k
441
  BLOCK_SIZE_N = block_n
442
+ Bs = ue8m0_as_uint8(Bs)
443
  C_shape = A.shape[:-1] + (N,)
444
+ BLOCK_SIZE_M = adaptive_block_size_m(M)
445
  C = A.new_empty(C_shape, dtype=output_dtype)
 
 
 
 
 
446
  grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
447
+
448
  with device_context(A.device):
449
+ wrap_triton(w8a8_block_dynamic_fp8_matmul_kernel)[grid](
450
  A,
451
  B,
452
  C,
 
453
  Bs,
454
  M,
455
  N,
 
460
  B.stride(0),
461
  C.stride(-2),
462
  C.stride(-1),
 
 
463
  Bs.stride(1),
464
  Bs.stride(0),
465
  # Meta-parameters
466
  BLOCK_SIZE_M=BLOCK_SIZE_M,
467
  BLOCK_SIZE_N=BLOCK_SIZE_N,
468
  BLOCK_SIZE_K=BLOCK_SIZE_K,
469
+ GROUP_SIZE_M=GROUP_SIZE_M,
470
  )
471
 
472
  return C
473
 
474
 
475
+ @triton_op(add_op_namespace_prefix("w8a8_block_static_fp8_matmul"), mutates_args=())
476
+ def _w8a8_block_static_fp8_matmul(
477
  A: torch.Tensor,
478
  B: torch.Tensor,
 
479
  Bs: torch.Tensor,
480
+ As: torch.Tensor,
481
+ block_size: list[int],
482
+ output_dtype: torch.dtype | None = None,
483
  ) -> torch.Tensor:
484
+ """Block-scale FP8 matmul with static (per-tensor) activation quantization.
485
 
486
+ A: (..., K) raw bf16/fp16 activations — quantized to FP8 inline against ``As``
487
+ B: (N, K) FP8 weights
488
+ Bs: (N // block_n, K // block_k) per-block weight scales
489
+ As: scalar / (1,) — per-tensor static activation scale
490
  """
491
+ assert len(block_size) == 2, (
492
+ f"block_size must be [block_n, block_k], got {block_size}"
493
+ )
494
+ block_n, block_k = block_size[0], block_size[1]
495
+
496
+ assert B.dtype != torch.int8, (
497
+ "static activation quant is not supported on the FP4 path"
498
+ )
499
+ assert not (block_n == B.size(0) and block_k == B.size(1)), (
500
+ "static activation quant requires block-wise weights, not tensor-mode"
501
+ )
502
  assert A.shape[-1] == B.shape[-1], (
503
  f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
504
  )
505
  assert A.is_contiguous(), "A must be contiguous"
506
  assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
507
  assert B.is_contiguous(), "B must be contiguous"
508
+ assert As.numel() == 1, f"As must be scalar or (1,), got {tuple(As.shape)}"
509
 
510
  N, K = B.shape
511
  M = A.numel() // A.shape[-1]
512
 
513
+ assert Bs.ndim == 2, f"Bs must be 2D (N//block_n, K//block_k), got ndim={Bs.ndim}"
514
+ assert Bs.shape == (triton.cdiv(N, block_n), triton.cdiv(K, block_k)), (
515
+ f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(N, block_n)}, {triton.cdiv(K, block_k)})"
 
 
 
 
516
  )
517
 
518
+ BLOCK_SIZE_K = block_k
519
+ BLOCK_SIZE_N = block_n
520
+ BLOCK_SIZE_M = adaptive_block_size_m(M)
521
+ grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
522
 
523
+ Bs = ue8m0_as_uint8(Bs)
 
524
  C_shape = A.shape[:-1] + (N,)
525
  C = A.new_empty(C_shape, dtype=output_dtype)
526
+ As = As.reshape(1).to(torch.float32)
527
+
528
  with device_context(A.device):
529
+ wrap_triton(w8a8_block_static_fp8_matmul_kernel)[grid](
530
  A,
531
  B,
532
  C,
 
541
  B.stride(0),
542
  C.stride(-2),
543
  C.stride(-1),
544
+ Bs.stride(1),
545
+ Bs.stride(0),
546
  # Meta-parameters
547
  BLOCK_SIZE_M=BLOCK_SIZE_M,
548
  BLOCK_SIZE_N=BLOCK_SIZE_N,
549
  BLOCK_SIZE_K=BLOCK_SIZE_K,
550
+ GROUP_SIZE_M=GROUP_SIZE_M,
551
  )
552
 
553
  return C
554
 
555
 
556
+ @triton_op(add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul"), mutates_args=())
557
+ def _w8a8_tensor_dynamic_fp8_matmul(
558
  A: torch.Tensor,
559
  B: torch.Tensor,
 
560
  Bs: torch.Tensor,
561
+ output_dtype: torch.dtype | None = None,
562
+ ) -> torch.Tensor:
563
+ """Tensor-scale FP8 matmul: ``C = A @ B.T`` with fused activation quantization.
564
+
565
+ A: (..., K) raw activations, bf16/fp16/fp32 (flattened to (M, K)
566
+ internally) — per-row scales computed via ``fp8_act_quant(A, K)``.
567
+ B: (N, K) FP8 weights.
568
+ Bs: scalar, (1,), or (1, 1) — single tensor-scale weight scale.
569
+ """
570
+ assert A.shape[-1] == B.shape[-1], (
571
+ f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
572
+ )
573
+ assert A.is_contiguous(), "A must be contiguous"
574
+ assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
575
+ assert B.is_contiguous(), "B must be contiguous"
576
+
577
+ N, K = B.shape
578
+ M = A.numel() // A.shape[-1]
579
+
580
+ assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
581
+
582
+ # Per-row scalar activation scale (one per token).
583
+ qA, As = fp8_act_quant(A, K)
584
+ As = As.reshape(M)
585
+ Bs = Bs.reshape(1)
586
+
587
+ C_shape = A.shape[:-1] + (N,)
588
+ C = A.new_empty(C_shape, dtype=output_dtype)
589
+ BLOCK_SIZE_M = adaptive_block_size_m(M)
590
+
591
+ def grid(META):
592
+ return (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, META["BLOCK_SIZE_N"]))
593
+
594
+ with device_context(A.device):
595
+ wrap_triton(w8a8_tensor_dynamic_fp8_matmul_kernel)[grid](
596
+ qA,
597
+ B,
598
+ C,
599
+ As,
600
+ Bs,
601
+ M,
602
+ N,
603
+ K,
604
+ qA.stride(-2),
605
+ qA.stride(-1),
606
+ B.stride(1),
607
+ B.stride(0),
608
+ C.stride(-2),
609
+ C.stride(-1),
610
+ As.stride(0),
611
+ # Meta-parameters (BLOCK_SIZE_N, BLOCK_SIZE_K come from autotune Config)
612
+ BLOCK_SIZE_M=BLOCK_SIZE_M,
613
+ GROUP_SIZE_M=GROUP_SIZE_M,
614
+ )
615
+
616
+ return C
617
+
618
+
619
+ @triton_op(add_op_namespace_prefix("mxfp_dynamic_matmul"), mutates_args=())
620
+ def _mxfp_dynamic_matmul(
621
+ A: torch.Tensor,
622
+ B: torch.Tensor,
623
+ Bs: torch.Tensor,
624
+ output_dtype: torch.dtype | None = None,
625
  ) -> torch.Tensor:
626
+ """MX matmul ``C = A @ B.T`` with fused activation quant. Weight format detected
627
+ from ``B.dtype``: ``int8`` → packed E2M1 (MXFP4, ``B`` is ``(N, K//2)``);
628
+ ``float8_e4m3fn`` → unpacked E4M3 (MXFP8, ``(N, K)``). Both use UE8M0 group-32 scales
629
+ ``(N, K//32)``; tile + dot path are autotuned (scale granularity fixed at 32).
630
+
631
+ A: (M, K) raw activations, bf16/fp16/fp32 (quantized inline to E4M3)
632
+ """
633
+ assert A.ndim == 2 and B.ndim == 2 and Bs.ndim == 2
634
+ assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
635
+ f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
636
+ )
637
+ assert Bs.dtype == torch.float8_e8m0fnu, (
638
+ f"Bs must be float8_e8m0fnu, got {Bs.dtype}"
639
+ )
640
+ assert A.is_contiguous(), "A must be contiguous"
641
+ assert B.is_contiguous(), "B must be contiguous"
642
+ VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
643
+
644
+ M, K = A.shape
645
+ N, K_b = B.shape
646
+ assert K == VALUES_PER_BYTE * K_b, (
647
+ f"K (={K}) must equal {VALUES_PER_BYTE} * B.shape[1] (={K_b})"
648
+ )
649
+ assert K % MX_SCALE_GROUP_K == 0, (
650
+ f"K (={K}) must be a multiple of {MX_SCALE_GROUP_K}"
651
+ )
652
+ assert Bs.shape == (N, K // MX_SCALE_GROUP_K), (
653
+ f"Bs shape {tuple(Bs.shape)} != ({N}, {K // MX_SCALE_GROUP_K})"
654
+ )
655
+
656
+ B = e2m1_as_uint8(B)
657
+ bs_u8 = ue8m0_as_uint8(Bs)
658
+ C = A.new_empty((M, N), dtype=output_dtype)
659
+ BLOCK_SIZE_M = adaptive_block_size_m(M)
660
+
661
+ def grid(META):
662
+ return (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, META["BLOCK_SIZE_N"]))
663
+
664
+ with device_context(A.device):
665
+ wrap_triton(mxfp_dynamic_matmul_kernel)[grid](
666
+ A,
667
+ B,
668
+ C,
669
+ bs_u8,
670
+ M,
671
+ N,
672
+ K,
673
+ A.stride(0),
674
+ A.stride(1),
675
+ B.stride(1),
676
+ B.stride(0),
677
+ C.stride(0),
678
+ C.stride(1),
679
+ bs_u8.stride(1),
680
+ bs_u8.stride(0),
681
+ # Meta-parameters (BLOCK_SIZE_N, BLOCK_SIZE_K come from autotune Config)
682
+ BLOCK_SIZE_M=BLOCK_SIZE_M,
683
+ GROUP_SIZE_M=GROUP_SIZE_M,
684
+ VALUES_PER_BYTE=VALUES_PER_BYTE,
685
+ SCALE_GROUP_K=MX_SCALE_GROUP_K,
686
+ )
687
+ return C
688
 
689
+
690
+ def w8a8_block_dynamic_fp8_matmul(
691
+ A: torch.Tensor,
692
+ B: torch.Tensor,
693
+ Bs: torch.Tensor,
694
+ block_size: list[int],
695
+ output_dtype: torch.dtype | None = None,
696
+ ) -> torch.Tensor:
697
+ """Block-wise W8A8 FP8 matrix multiplication with fused activation quantization.
698
 
699
  Args:
700
+ A: Raw activation tensor ``[..., M, K]`` in bf16/fp16/fp32 — quantized
701
+ inline to ``float8_e4m3fn`` against per-K-tile per-row UE8 scales.
702
+ B: FP8 weight tensor ``[N, K]`` in ``float8_e4m3fn``.
703
  Bs: Per-block weight scales ``[N // block_size[0], K // block_size[1]]``.
704
+ block_size: ``[block_n, block_k]`` weight quantization block dimensions.
705
  output_dtype: dtype of the returned tensor (default: ``torch.float32``).
706
 
707
  Returns:
708
+ Output tensor ``[..., M, N]`` in ``output_dtype``.
709
  """
710
+ return ops.w8a8_block_dynamic_fp8_matmul(A, B, Bs, block_size, output_dtype)
 
 
711
 
712
 
713
+ def w8a8_block_static_fp8_matmul(
714
  A: torch.Tensor,
715
  B: torch.Tensor,
716
+ Bs: torch.Tensor,
717
  As: torch.Tensor,
718
+ block_size: list[int],
719
+ output_dtype: torch.dtype | None = None,
720
+ ) -> torch.Tensor:
721
+ """Block-wise W8A8 FP8 matmul with static (per-tensor) activation quantization.
722
+
723
+ Args:
724
+ A: Raw activation tensor ``[..., M, K]`` in bf16/fp16/fp32 — quantized
725
+ inline against the per-tensor scalar ``As``.
726
+ B: FP8 weight tensor ``[N, K]`` in ``float8_e4m3fn``.
727
+ Bs: Per-block weight scales ``[N // block_size[0], K // block_size[1]]``.
728
+ As: Static per-tensor activation scale (scalar or ``[1]``).
729
+ block_size: ``[block_n, block_k]`` weight quantization block dimensions.
730
+ output_dtype: dtype of the returned tensor (default: ``torch.float32``).
731
+
732
+ Returns:
733
+ Output tensor ``[..., M, N]`` in ``output_dtype``.
734
+ """
735
+ return ops.w8a8_block_static_fp8_matmul(A, B, Bs, As, block_size, output_dtype)
736
+
737
+
738
+ def w8a8_tensor_dynamic_fp8_matmul(
739
+ A: torch.Tensor,
740
+ B: torch.Tensor,
741
  Bs: torch.Tensor,
742
+ output_dtype: torch.dtype | None = None,
743
  ) -> torch.Tensor:
744
+ """Tensor-scale W8A8 FP8 matmul with fused activation quantization.
745
 
746
+ Computes ``C = A @ B.T`` with raw bf16/fp16/fp32 ``A`` quantized to FP8
747
+ per-row (one scale per token) before the dot.
748
 
749
  Args:
750
+ A: Raw activation tensor ``[M, K]`` in bf16/fp16/fp32.
751
+ B: FP8 weight tensor ``[N, K]`` in ``float8_e4m3fn``.
 
752
  Bs: Single weight scale, scalar or ``[1]``.
753
  output_dtype: dtype of the returned tensor.
754
+ """
755
+ return ops.w8a8_tensor_dynamic_fp8_matmul(A, B, Bs, output_dtype)
756
 
757
+
758
+ def mxfp_dynamic_matmul(
759
+ A: torch.Tensor,
760
+ B: torch.Tensor,
761
+ Bs: torch.Tensor,
762
+ output_dtype: torch.dtype | None = None,
763
+ ) -> torch.Tensor:
764
+ """MX (MXFP4/MXFP8) matmul ``C = A @ B.T`` with fused activation quant; the weight
765
+ format is detected from ``B.dtype``. Tile + dot path are autotuned; MX scale
766
+ granularity is fixed at 32 (the MX-format spec).
767
+
768
+ Args:
769
+ A: Raw activations ``[M, K]`` in bf16/fp16/fp32.
770
+ B: Packed E2M1 ``[N, K // 2]`` (``int8``) or E4M3 ``[N, K]`` (``float8_e4m3fn``).
771
+ Bs: UE8M0 weight scales ``[N, K // 32]`` (``float8_e8m0fnu``).
772
+ output_dtype: dtype of the returned tensor (default ``bfloat16``).
773
  """
774
+ return ops.mxfp_dynamic_matmul(A, B, Bs, output_dtype)
775
 
776
 
777
+ def matmul_2d(
778
  A: torch.Tensor,
779
  B: torch.Tensor,
 
780
  Bs: torch.Tensor,
781
  block_size: list[int] | None,
782
+ output_dtype: torch.dtype | None = None,
783
+ activation_scale: torch.Tensor | None = None,
784
  ) -> torch.Tensor:
785
+ """Quantized matmul dispatcher (W8A8 FP8 or W4A8 FP4).
786
 
787
+ ``A`` is always raw bf16/fp16/fp32; quantization is fused into every path.
788
+ With ``activation_scale`` set, the kernel uses that per-tensor scalar
789
+ (static quant); otherwise it computes its own scale from ``A`` (dynamic).
 
790
 
791
+ ``output_dtype`` defaults to ``A.dtype``.
 
 
 
 
 
 
792
 
793
+ Routes by weight dtype and ``block_size``:
794
+ - MX weights — ``int8`` (packed E2M1) or ``float8_e4m3fn`` (E4M3) with UE8M0
795
+ group-32 ``Bs`` (shape ``[N, K//32]``) → ``mxfp_dynamic_matmul`` (``block_size``
796
+ ignored, ``activation_scale`` unsupported; scale granularity fixed at 32, autotuned).
797
+ - ``block_size`` None or full ``[N, K]`` → ``w8a8_tensor_dynamic_fp8_matmul``.
798
+ - otherwise → ``w8a8_block_dynamic_fp8_matmul`` (or its static variant when
799
+ ``activation_scale`` is given).
800
+ """
801
+ if is_mxfp(B, Bs):
802
+ if activation_scale is not None:
803
+ raise NotImplementedError(
804
+ "activation_scale (static activation quant) is not supported for MX weights — "
805
+ "the MX path quantizes activations dynamically per group. Omit activation_scale."
806
+ )
807
+ return mxfp_dynamic_matmul(A, B, Bs, output_dtype)
808
+
809
+ if is_tensor_wide(block_size, B):
810
+ return w8a8_tensor_dynamic_fp8_matmul(A, B, Bs, output_dtype)
811
+
812
+ # Block-wise FP8: static when a per-tensor activation scale is supplied, else dynamic.
813
+ if activation_scale is not None:
814
+ return w8a8_block_static_fp8_matmul(
815
+ A, B, Bs, activation_scale, block_size, output_dtype
816
+ )
817
+ return w8a8_block_dynamic_fp8_matmul(A, B, Bs, block_size, output_dtype)
build/torch-cuda/metadata.json CHANGED
@@ -1,10 +1,25 @@
1
  {
2
  "name": "finegrained-fp8",
3
- "id": "_finegrained_fp8_cuda_7c5619e",
4
- "version": 1,
5
  "license": "Apache-2.0",
6
  "python-depends": [],
7
  "backend": {
8
  "type": "cuda"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
  }
 
1
  {
2
  "name": "finegrained-fp8",
3
+ "id": "_finegrained_fp8_cuda_93d7641",
4
+ "version": 4,
5
  "license": "Apache-2.0",
6
  "python-depends": [],
7
  "backend": {
8
  "type": "cuda"
9
+ },
10
+ "digest": {
11
+ "algorithm": "sha256",
12
+ "files": {
13
+ "__init__.py": "zhfXqFHpZ1lPv0hUwaOhJHxmcDq2QcE9R+9cgVfLTI4=",
14
+ "_ops.py": "v4IWQ4TB7LvLjTio3GctOEdzLSpc45Z3OBcWILsDeSM=",
15
+ "batched.py": "siX6kWh3I9GzioEw58Y66p1aZX1m6kSAf4E/asiLnVA=",
16
+ "bayesian_autotuner.py": "jBUYvyfpEHsTBgFlpt2Ts5emz+CSMyvjc1TxUfZKVrg=",
17
+ "finegrained_fp8/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY=",
18
+ "fused_batched.py": "09a9bN8fzGBG5CapCwvIUL5589F7Ve+CbT+S1+4+BAI=",
19
+ "fused_grouped.py": "foyMOTru53VKJ1d7ASJpEGWqiF3OqbSQT91tTPXcWKk=",
20
+ "grouped.py": "Gq1YS9LXnFuKWQ+M0XW6WuP2Fg7zwKwwT50o6YhQXnw=",
21
+ "matmul.py": "PEdwUJGaJetm4lWF6de3eYHBHtnHsZhevoDQd6fJYKw=",
22
+ "utils.py": "aUUYq5/POJbaZKmo8QgM89XQrJN3HNHTuJOsPhgYxQw="
23
+ }
24
  }
25
  }
build/torch-cuda/metadata.json.sigstore ADDED
@@ -0,0 +1 @@
 
 
1
+ {"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIIHSzCCBtCgAwIBAgIUHLiFbbNYXDYPtqIG48HGoOqA7RcwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwNjI5MTcxNDQ5WhcNMjYwNjI5MTcyNDQ5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdD3jvq2C4tEZZAE37ydzk+UsPgoh++aHLcQELDY+a5KZgEqJqxpiW8OHSGEe07m85EHlRXB5sB9BmHJTBi7AuKOCBe8wggXrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUUaXQbM7F2hw5Au49d8kdUNd+gTkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wawYDVR0RAQH/BGEwX4ZdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoOTNkNzY0MTZlOTY1YTE0Y2IzY2Y1ZGFlOWNjYTdhNTNmNzBjNWU1YjATBgorBgEEAYO/MAEEBAVCdWlsZDArBgorBgEEAYO/MAEFBB1odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eTAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMG0GCisGAQQBg78wAQkEXwxdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoOTNkNzY0MTZlOTY1YTE0Y2IzY2Y1ZGFlOWNjYTdhNTNmNzBjNWU1YjAbBgorBgEEAYO/MAELBA0MC3NlbGYtaG9zdGVkMEAGCisGAQQBg78wAQwEMgwwaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5MDgGCisGAQQBg78wAQ0EKgwoOTNkNzY0MTZlOTY1YTE0Y2IzY2Y1ZGFlOWNjYTdhNTNmNzBjNWU1YjAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAaBgorBgEEAYO/MAEPBAwMCjEwNzE0NzU1MjkwLgYKKwYBBAGDvzABEAQgDB5odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2UwGAYKKwYBBAGDvzABEQQKDAgyNTcyMDc0MzBtBgorBgEEAYO/MAESBF8MXWh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS8uZ2l0aHViL3dvcmtmbG93cy9idWlsZC55YW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKDkzZDc2NDE2ZTk2NWExNGNiM2NmNWRhZTljY2E3YTUzZjcwYzVlNWIwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBkBgorBgEEAYO/MAEVBFYMVGh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS9hY3Rpb25zL3J1bnMvMjgzODk2ODc4NDkvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBGBgorBgEEAYO/MAEYBDgMNnJlcG86aHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHk6cmVmOnJlZnMvaGVhZHMvbWFpbjCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABnxRgZgUAAAQDAEYwRAIga3GJTEHsVlnuoI1kG25kPC3sc4On1G1CH7jgrcznaq4CIAEDXVUDkXPlbdYZztMVhEPydBetfSt9x44bOu1mhEqmMAoGCCqGSM49BAMDA2kAMGYCMQD2YFLb+M5svHbxwKLjbe7VlXaa7CuLvdujtpVpwR4WQYwAjZh/NsYfHRTK9YF5d6YCMQDPbSsy3rBtAI4QppFB48c8/PC8BS+YwTaWq1guriywQSt9JXkgOD7TlWMPSrvG5LE="},"tlogEntries":[{"logIndex":"2011412491","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1782753290","inclusionPromise":{"signedEntryTimestamp":"MEQCIBu7joAyJ1Scv9JziiQLgyQ7crhnWnP1MdOfZVZU/belAiATlq9G4be8PKp5mBqEjIZDvKaDOmj4wdqGHFq4ov+8eg=="},"inclusionProof":{"logIndex":"1889508229","rootHash":"Qc/hbi1qBcXqZet3VqITiwbaJ5KYuZZp4AZzYzDFmPY=","treeSize":"1889508650","hashes":["tWE1dKnr+xyVR9DqZl179d4a2pzILWEWoG3QQ016pQA=","eJDrwh6X93nkRtJXPluc2cbg01QfYAH0gkvcZW5fZdg=","2Bvz7mTmsTUXVkKOwxb/onqiaThvKrmBkjB83YlfoYk=","u97qIngM8q1QfW5Lo/9csT+WAgu8+rpNQjqarIgDrnE=","7dbKejwhyjxkywj03FXGECoQ7QTz1yxa3/gxCieD8WU=","RGsebap7/2pdY7rrT3oY8CUkcbwkB+6gHwnx1/CsGAs=","NYulN1dObgLker5XjMR4jAFzRdx6Sy0SXp6X3KRf0m4=","zsh1PjbsqQJic+3EcBbfMKAlRareth4KaBthTv/3/SA=","0Lm4vgxuYggxvPJg+Rhp0EJTjbPoCTRGIaGqkTkZ4Lo=","X9YQHakf+wqAGVCUCm2cEW/JGbf4dyp/Db+Ku1v1Du8=","PORSs03e4HDJYPwDuaM6rQrziDBN3e5Y4SuajVQFHg0=","DGp8UqFvSdPpdWfi6+/wVsDiGX858w1Jtjle0C4QGVc=","uxLbTK4c/VFXh7/w8pb8eAk6P1ppLRLvvQ5tU6i9MFg=","tvcQx4ohRLYBU5QOWeu4jhR3lEKztnlyYTspOSiOMcA=","pu3boNdI/2WREJSDmOgBX37bljG4IyCsSsPMDAzAB8Q=","f2GuSxsX8CM80Bzrdr+iaFcvdbcdx8PsOGfP1A5iZmg=","wznNTIJf/erZkw1aAaotGts1BhEB9zNmnJSKo0zdCeM=","+hhERR0matid5NvHk7ddsPOWJ5JAyyn11f+ruZpMW38=","Mp0blBzkxwj6OGAogVNNNMwQM4XxRTFCrHD5ZZy2wYg=","dIBftsHOWhWti/c7pucPNfamsTNyNuam3ViDx67krLA=","+/VZ56MsIPxMiyLAodzKXo5TEWdQp36z89qLhpzloAo=","daxmZaajRpZV+JxHiOYZhJBiSKN5ucqjh2WnGbHhirw=","DOCeoSMovIvLExkhIvisow9AuNXgeWs4ECkyR6EcqYU="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n1889508650\nQc/hbi1qBcXqZet3VqITiwbaJ5KYuZZp4AZzYzDFmPY=\n\n— rekor.sigstore.dev wNI9ajBFAiEAmeN2VwOoIwnC9dJ2+KWMPB3BmzlDx3rjop2jipH6bJECIEFFujbiD917hnqHY5pLSHj0TL9j6+R9/irZ4NLnl49k\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJkOTQxNDY1ZjQ1Y2FkOGE3M2Q5MjhiNWY1YWUxYmIyZDhjOGRlN2Y3ZjI5MDIzMTdkMGZiODUyOWI1ZjMyMmE0In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNGam5tbFJQbG4rUG52T2VNUTN2b1BCR0ZkaDRmMDcvcjJtY05JN2V5eDNBSWdXajhTck42N0FITWFDMHcxYmpEbG9IRXhGSmxCTkhNUUZtUWcyZ01OcHNzPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaFRla05EUW5SRFowRjNTVUpCWjBsVlNFeHBSbUppVGxsWVJGbFFkSEZKUnpRNFNFZHZUM0ZCTjFKamQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFpkMDVxU1RWTlZHTjRUa1JSTlZkb1kwNU5hbGwzVG1wSk5VMVVZM2xPUkZFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZrUkROcWRuRXlRelIwUlZwYVFVVXpOM2xrZW1zclZYTlFaMjlvS3l0aFNFeGpVVVVLVEVSWksyRTFTMXBuUlhGS2NYaHdhVmM0VDBoVFIwVmxNRGR0T0RWRlNHeFNXRUkxYzBJNVFtMUlTbFJDYVRkQmRVdFBRMEpsT0hkbloxaHlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZWWVZoUkNtSk5OMFl5YUhjMVFYVTBPV1E0YTJSVlRtUXJaMVJyZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJGM1dVUldVakJTUVZGSUwwSkhSWGRZTkZwa1lVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKb01Wb3laSEJpYldSdFdWZE9iQXBNTW5Sc1kyMDFiR0pJVFhSWk1qbDBZbGhXZFdGWVVqVk1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1rb3hZVmQ0YTB4dWJHaGlWM2hCQ21OdFZtMWplVGx2V2xkR2EyTjVPWFJaVjJ4MVRVUnJSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkxNbWd3WkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpBS1lWYzVkV041Tlc1aFdGSnZaRmRLTVdNeVZubFpNamwxWkVkV2RXUkROV3BpTWpCM1NIZFpTMHQzV1VKQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWN3cGlNMlJtV2tkc2VtTkhSakJaTW1kM1RtZFpTMHQzV1VKQ1FVZEVkbnBCUWtGM1VXOVBWRTVyVG5wWk1FMVVXbXhQVkZreFdWUkZNRmt5U1hwWk1sa3hDbHBIUm14UFYwNXFXVlJrYUU1VVRtMU9la0pxVGxkVk1WbHFRVlJDWjI5eVFtZEZSVUZaVHk5TlFVVkZRa0ZXUTJSWGJITmFSRUZ5UW1kdmNrSm5SVVVLUVZsUEwwMUJSVVpDUWpGdlpGZGtibUZYTlc1YWJVWnFXbE01Y2xwWVNuVmFWM2g2VEZkT2RtSlhNVEZpYld3d1pWUkJaRUpuYjNKQ1owVkZRVmxQTHdwTlFVVkhRa0U1ZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QzZFpTMHQzV1VKQ1FVZEVkbnBCUWtOQlVYUkVRM1J2WkVoU2QyTjZiM1pNTTFKMkNtRXlWblZNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVY3dSME5wYzBkQlVWRkNaemM0ZDBGUmEwVUtXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5YURGYU1tUndZbTFrYlZsWFRteE1NblJzWTIwMWJHSklUWFJaTWpsMFlsaFdkUXBoV0ZJMVRIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTmlNMlI2VERKS01XRlhlR3RNYm14b1lsZDRRV050Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxQ2sxRVowZERhWE5IUVZGUlFtYzNPSGRCVVc5RlMyZDNiMDlVVG10T2Vsa3dUVlJhYkU5VVdURlpWRVV3V1RKSmVsa3lXVEZhUjBac1QxZE9hbGxVWkdnS1RsUk9iVTU2UW1wT1YxVXhXV3BCWWtKbmIzSkNaMFZGUVZsUEwwMUJSVXhDUVRCTlF6Tk9iR0pIV1hSaFJ6bDZaRWRXYTAxRlFVZERhWE5IUVZGUlFncG5OemgzUVZGM1JVMW5kM2RoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbWd4V2pKa2NHSnRaRzFaVjA1c1RESjBiR050Tld4aVNFMTBDbGt5T1hSaVdGWjFZVmhTTlUxRVowZERhWE5IUVZGUlFtYzNPSGRCVVRCRlMyZDNiMDlVVG10T2Vsa3dUVlJhYkU5VVdURlpWRVV3V1RKSmVsa3lXVEVLV2tkR2JFOVhUbXBaVkdSb1RsUk9iVTU2UW1wT1YxVXhXV3BCWmtKbmIzSkNaMFZGUVZsUEwwMUJSVTlDUWtWTlJETktiRnB1VFhaaFIxWm9Xa2hOZGdwaVYwWndZbXBCWVVKbmIzSkNaMFZGUVZsUEwwMUJSVkJDUVhkTlEycEZkMDU2UlRCT2VsVXhUV3ByZDB4bldVdExkMWxDUWtGSFJIWjZRVUpGUVZGbkNrUkNOVzlrU0ZKM1kzcHZka3d5WkhCa1IyZ3hXV2sxYW1JeU1IWmhTRlp1V2pKc2RWb3lXbWhaTWxWM1IwRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVc0tSRUZuZVU1VVkzbE5SR013VFhwQ2RFSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1JqaE5XRmRvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVid3BrVjJSdVlWYzFibHB0Um1wYVV6bHlXbGhLZFZwWGVIcE1WMDUyWWxjeE1XSnRiREJsVXpoMVdqSnNNR0ZJVm1sTU0yUjJZMjEwYldKSE9UTmplVGxwQ21SWGJITmFRelUxV1ZjeGMxRklTbXhhYmsxMllVZFdhRnBJVFhaaVYwWndZbXBCTkVKbmIzSkNaMFZGUVZsUEwwMUJSVlJDUTI5TlMwUnJlbHBFWXpJS1RrUkZNbHBVYXpKT1YwVjRUa2RPYVUweVRtMU9WMUpvV2xSc2Fsa3lSVE5aVkZWNldtcGpkMWw2Vm14T1YwbDNTVkZaUzB0M1dVSkNRVWRFZG5wQlFncEdRVkZVUkVKR00ySXpTbkphYlhoMlpERTVhMkZZVG5kWldGSnFZVVJDYTBKbmIzSkNaMFZGUVZsUEwwMUJSVlpDUmxsTlZrZG9NR1JJUW5wUGFUaDJDbG95YkRCaFNGWnBURzFPZG1KVE9XOWtWMlJ1WVZjMWJscHRSbXBhVXpseVdsaEtkVnBYZUhwTVYwNTJZbGN4TVdKdGJEQmxVemxvV1ROU2NHSXlOWG9LVEROS01XSnVUWFpOYW1kNlQwUnJNazlFWXpST1JHdDJXVmhTTUZwWE1YZGtTRTEyVFZSQlYwSm5iM0pDWjBWRlFWbFBMMDFCUlZkQ1FXZE5RbTVDTVFwWmJYaHdXWHBDUjBKbmIzSkNaMFZGUVZsUEwwMUJSVmxDUkdkTlRtNUtiR05IT0RaaFNGWnVXakpzZFZveVdtaFpNbFYyWVRKV2VXSnRWbk5qZVRGcUNtSXlNWFJrVnpWd1pFaHJObU50Vm0xUGJrcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRMEpwVVZsTFMzZFpRa0pCU0ZkbFVVbEZRV2RTTjBKSWEwRUtaSGRDTVVGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFtNTRVbWRhWjFWQlFVRlJSQXBCUlZsM1VrRkpaMkV6UjBwVVJVaHpWbXh1ZFc5Sk1XdEhNalZyVUVNemMyTTBUMjR4UnpGRFNEZHFaM0pqZW01aGNUUkRTVUZGUkZoV1ZVUnJXRkJzQ21Ka1dWcDZkRTFXYUVWUWVXUkNaWFJtVTNRNWVEUTBZazkxTVcxb1JYRnRUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlhMEZOUjFsRFRWRkVNbGxHVEdJS0swMDFjM1pJWW5oM1MweHFZbVUzVm14WVlXRTNRM1ZNZG1SMWFuUndWbkIzVWpSWFVWbDNRV3BhYUM5T2MxbG1TRkpVU3psWlJqVmtObGxEVFZGRVVBcGlVM041TTNKQ2RFRkpORkZ3Y0VaQ05EaGpPQzlRUXpoQ1V5dFpkMVJoVjNFeFozVnlhWGwzVVZOME9VcFlhMmRQUkRkVWJGZE5VRk55ZGtjMVRFVTlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQguFU3H+mook948dfQKNgaoxHAoycH3GX2xe+B/+CeMJACFFbYYWssAU46XsBRISWFOQ8Li12eGA8yMDI2MDYyOTE3MTQ0OVowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwNjI5MTcxNDQ5WjAvBgkqhkiG9w0BCQQxIgQguvirpEmIhGn3xr9LWY2ECPSMWhGKtDD4yC7y/bDh1KswgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGcwZQIwDmJq7ErbKctLqPhm2iwOCTJDHIm8Je4xVwCY/fXufe2r+Z9Dgh8AW+PjeW6Rj9hZAjEAhiq+ySoRLE6b8eGDhPKHxDczbFbxVhtusLTA9bdwkTUDzb2bPMZKHP+wYEQ5La8d"}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"2UFGX0XK2Kc9kotfWuG7LYyN5/fykCMX0PuFKbXzIqQ="},"signature":"MEUCIQCFjnmlRPln+PnvOeMQ3voPBGFdh4f07/r2mcNI7eyx3AIgWj8SrN67AHMaC0w1bjDloHExFJlBNHMQFmQg2gMNpss="}}
build/torch-cuda/utils.py CHANGED
@@ -1,6 +1,40 @@
1
- import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from contextlib import contextmanager
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  @contextmanager
6
  def device_context(device: torch.device):
@@ -11,3 +45,723 @@ def device_context(device: torch.device):
11
  yield
12
  else:
13
  yield
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 functools
16
  from contextlib import contextmanager
17
 
18
+ import torch
19
+ import triton
20
+ import triton.language as tl
21
+ from torch.library import triton_op, wrap_triton
22
+
23
+ from ._ops import add_op_namespace_prefix, ops
24
+
25
+ # ── Format constants ──────────────────────────────────────────────────────────
26
+
27
+ # FP8 (E4M3) is the main format for weights and activations;
28
+ FP8_DTYPE = torch.float8_e4m3fn
29
+ # FP4 (E2M1) packs two 4-bit nibbles per byte. MX formats (MXFP4 weights, MXFP8
30
+ # E4M3 weights/activations) share one UE8M0 scale per 32-element K-group — the OCP
31
+ # MX block size, consumed by ``tl.dot_scaled``. Format constants, not tunables.
32
+ NIBBLES_PER_BYTE = 2
33
+ MX_SCALE_GROUP_K = 32
34
+
35
+
36
+ # ── Host-side helpers ─────────────────────────────────────────────────────────
37
+
38
 
39
  @contextmanager
40
  def device_context(device: torch.device):
 
45
  yield
46
  else:
47
  yield
48
+
49
+
50
+ @functools.lru_cache(maxsize=8)
51
+ def sm_count(device_index: int) -> int:
52
+ """Processor (CUDA SM / XPU Xe-core) count for a device (a constant) — cached so callers
53
+ don't re-query it each launch. Read from Triton's active driver so it works on any backend,
54
+ with a CUDA fallback."""
55
+ try:
56
+ return triton.runtime.driver.active.utils.get_device_properties(device_index)[
57
+ "multiprocessor_count"
58
+ ]
59
+ except Exception:
60
+ active_device = get_active_device_type()
61
+ if active_device == "cuda":
62
+ return torch.cuda.get_device_properties(device_index).multi_processor_count
63
+ elif active_device == "xpu":
64
+ return torch.xpu.get_device_properties(device_index).multi_processor_count
65
+ else:
66
+ raise RuntimeError(
67
+ f"Unsupported device type {active_device} for sm_count; only cuda/xpu are supported."
68
+ )
69
+
70
+
71
+ # H-tile each topk_reduce program reduces (one (token, tile) program per BLOCK_H span). The
72
+ # reduce is bandwidth-bound and saturates at BLOCK_H>=512: a sweep over {128..2048} x prefill
73
+ # shapes put 512/1024/2048 within ~7% (mostly noise) while 128/256 lagged — not worth a tuning
74
+ # axis for this small slice of runtime. 512 = smallest that saturates (best at low token counts,
75
+ # where bigger tiles waste masked lanes). Power of 2 (tl.arange).
76
+ TOPK_REDUCE_BLOCK_H = 512
77
+
78
+
79
+ @triton.jit
80
+ def topk_reduce_kernel(
81
+ ProjOut,
82
+ Out,
83
+ H,
84
+ stride_pm,
85
+ stride_ph,
86
+ stride_om,
87
+ stride_oh,
88
+ NUM_TOP_K: tl.constexpr,
89
+ BLOCK_H: tl.constexpr,
90
+ ):
91
+ """Sum the ``NUM_TOP_K`` flat ``(token, slot)`` rows of ``ProjOut`` into ``Out[token]`` —
92
+ a tight replacement for ``proj_out.view(T, K, H).sum(1)`` (~2.8x faster than torch's
93
+ generic reduce; fp32 accumulate, so bit-identical). Caller allocs Out and launches it."""
94
+ t = tl.program_id(0)
95
+ offs_h = tl.program_id(1) * BLOCK_H + tl.arange(0, BLOCK_H)
96
+ mask = offs_h < H
97
+ acc = tl.zeros((BLOCK_H,), tl.float32)
98
+ for k in tl.static_range(NUM_TOP_K):
99
+ acc += tl.load(
100
+ ProjOut + (t * NUM_TOP_K + k) * stride_pm + offs_h * stride_ph,
101
+ mask=mask,
102
+ other=0.0,
103
+ ).to(tl.float32)
104
+ tl.store(
105
+ Out + t * stride_om + offs_h * stride_oh,
106
+ acc.to(Out.dtype.element_ty),
107
+ mask=mask,
108
+ )
109
+
110
+
111
+ def ue8m0_as_uint8(scale: torch.Tensor) -> torch.Tensor:
112
+ """View UE8M0 (``float8_e8m0fnu``) weight scales as ``uint8`` for the Triton
113
+ binder, which doesn't recognize the dtype; kernels decode ``2^(exp-127)``
114
+ inline. fp32 (non-UE8M0) scales pass through unchanged."""
115
+ return scale.view(torch.uint8) if scale.dtype == torch.float8_e8m0fnu else scale
116
+
117
+
118
+ def e2m1_as_uint8(weight: torch.Tensor) -> torch.Tensor:
119
+ """View an ``int8``-stored MXFP4 (packed E2M1) weight as ``uint8`` — a zero-cost
120
+ reinterpret. ``tl.dot_scaled`` requires the packed rhs as ``uint8``, so do it once here
121
+ instead of casting in-kernel at every load. E4M3 (MXFP8) weights pass through unchanged."""
122
+ return weight.view(torch.uint8) if weight.dtype == torch.int8 else weight
123
+
124
+
125
+ def is_mxfp8(weight: torch.Tensor, scale: torch.Tensor) -> bool:
126
+ """MXFP8 weight/scale pair: E4M3 weights with UE8M0 group-32 scales — last dim
127
+ ``scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K``, matching leading dims.
128
+ Works for 2D ``(N, K)`` and 3D ``(E, N, K)`` weights. The group-32 layout is what
129
+ separates MXFP8 from 128-block FP8 (which may also carry UE8M0 scales)."""
130
+ return (
131
+ weight.dtype == torch.float8_e4m3fn
132
+ and scale.dtype == torch.float8_e8m0fnu
133
+ and scale.ndim == weight.ndim
134
+ and scale.shape[:-1] == weight.shape[:-1]
135
+ and scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K
136
+ )
137
+
138
+
139
+ def is_mxfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
140
+ """MXFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with
141
+ UE8M0 group-32 scales — ``scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE //
142
+ MX_SCALE_GROUP_K`` (unpacked K = ``2 * K_half``), matching leading dims. 2D or 3D."""
143
+ return (
144
+ weight.dtype == torch.int8
145
+ and scale.dtype == torch.float8_e8m0fnu
146
+ and scale.ndim == weight.ndim
147
+ and scale.shape[:-1] == weight.shape[:-1]
148
+ and scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE // MX_SCALE_GROUP_K
149
+ )
150
+
151
+
152
+ def is_mxfp(weight: torch.Tensor, scale: torch.Tensor) -> bool:
153
+ """Any MX weight/scale pair — MXFP8 (``float8_e4m3fn``, one value/byte) or MXFP4
154
+ (``int8``, two E2M1 codes/byte), both with UE8M0 group-32 scales. ``values_per_byte``
155
+ folds the two cases: the scale's last dim covers the unpacked K
156
+ (``weight.shape[-1] * values_per_byte``) in groups of ``MX_SCALE_GROUP_K``. The
157
+ dispatchers route on this; the op picks the format from ``weight.dtype``."""
158
+ if weight.dtype == torch.float8_e4m3fn:
159
+ values_per_byte = 1
160
+ elif weight.dtype == torch.int8:
161
+ values_per_byte = NIBBLES_PER_BYTE
162
+ else:
163
+ return False
164
+ return (
165
+ scale.dtype == torch.float8_e8m0fnu
166
+ and scale.ndim == weight.ndim
167
+ and scale.shape[:-1] == weight.shape[:-1]
168
+ and scale.shape[-1] == weight.shape[-1] * values_per_byte // MX_SCALE_GROUP_K
169
+ )
170
+
171
+
172
+ def is_tensor_wide(block_size, weight: torch.Tensor) -> bool:
173
+ """True when ``block_size`` selects per-tensor (tensor-dynamic) scaling: ``None`` or
174
+ equal to the weight's full ``(N, K)`` — one scale block spanning the whole matrix.
175
+ Handles 2D ``(N, K)`` and 3D ``(E, N, K)`` weights via the last two dims."""
176
+ return block_size is None or (
177
+ block_size[0] == weight.shape[-2] and block_size[1] == weight.shape[-1]
178
+ )
179
+
180
+
181
+ # The per-token batched/fused kernels are decode-shaped — one routed row per program, so
182
+ # BLOCK_SIZE_M is 1 (a larger tile would just recompute the same row). The 2D matmul sizes
183
+ # its M tile to the workload instead, via ``adaptive_block_size_m``.
184
+ DECODE_BLOCK_SIZE_M = 1
185
+
186
+
187
+ def adaptive_block_size_m(target_m: int) -> int:
188
+ """Smallest power-of-2 >= ``target_m``, floored at 16 and capped at 128.
189
+
190
+ Used by all matmul wrappers (single / batched / grouped) to size the M tile
191
+ to the workload — small per-expert M wants smaller tiles, large M caps out
192
+ at 128 to keep register pressure bounded. Pass ``M`` for single matmul, or
193
+ ``(S + E - 1) // E`` (avg tokens per expert) for batched/grouped.
194
+ """
195
+ return min(max(triton.next_power_of_2(target_m), 16), 128)
196
+
197
+
198
+ @functools.cache
199
+ def get_active_device_type() -> str:
200
+ """Active torch device type for the current Triton backend (``"cuda"``, ``"xpu"``, ...).
201
+
202
+ Falls back to ``"cuda"`` when no driver is loaded — Triton raises
203
+ ``RuntimeError: 0 active drivers ([])`` on driverless build boxes, and the
204
+ autotune-config builder is evaluated at module-import time under the
205
+ ``@triton.autotune`` decorator (no kernel launches there, so the default is
206
+ only used to shape the config list).
207
+ """
208
+ try:
209
+ return triton.runtime.driver.active.get_active_torch_device().type
210
+ except RuntimeError:
211
+ return "cuda"
212
+
213
+
214
+ def get_accelerator_autotuning_configs(
215
+ *,
216
+ tune_block_m: bool = False,
217
+ tune_block_nk: bool = False,
218
+ ):
219
+ """Autotune search grid for the current accelerator.
220
+
221
+ ``num_warps``, ``num_stages`` and ``blocks`` (the ``(BLOCK_SIZE_N, BLOCK_SIZE_K)``
222
+ tile shapes) are fixed up front from ``(is_xpu, tune_block_nk)``, then crossed
223
+ into the config list.
224
+
225
+ ``tune_block_nk=True`` sweeps the tile: used by kernels that have no caller
226
+ ``block_size`` to fix it — the MX ``tl.dot_scaled`` paths AND the tensor-dynamic
227
+ FP8 paths. ``tune_block_nk=False`` emits a single empty meta-dict (block-scaled
228
+ kernels take the tile from the caller's ``block_size``). ``tune_block_m=True`` also crosses
229
+ in BLOCK_SIZE_M ∈ ``(16, 32, 64, 128)`` (fused grouped, which lets the tuner pick M instead
230
+ of computing it via ``adaptive_block_size_m``).
231
+
232
+ The CUDA tile set is a data-driven prune of a B200 sweep across single (BM=128),
233
+ grouped MoE (BM=16/64) and decode (BM=1): winners only ever used these 4 tiles,
234
+ num_warps in {4,8,16}, num_stages in {2,3} (warps=2, stages=4 and 256x256 never
235
+ won) — 108 → 24. (Tuned on dot_scaled; tensor-dynamic tl.dot reuses it.)
236
+ """
237
+ is_xpu = get_active_device_type() == "xpu"
238
+ num_warps = [8, 16] if is_xpu else [2, 4, 8, 16]
239
+ num_stages = [2, 3, 4]
240
+
241
+ if tune_block_nk:
242
+ tiles = (
243
+ [(128, 128)] if is_xpu else [(128, 128), (256, 128), (128, 64), (64, 256)]
244
+ )
245
+ blocks = [{"BLOCK_SIZE_N": bn, "BLOCK_SIZE_K": bk} for bn, bk in tiles]
246
+ else:
247
+ blocks = [{}]
248
+
249
+ if tune_block_m:
250
+ blocks = [{**b, "BLOCK_SIZE_M": bm} for b in blocks for bm in (16, 32, 64, 128)]
251
+
252
+ return [
253
+ triton.Config(b, num_warps=w, num_stages=s)
254
+ for b in blocks
255
+ for w in num_warps
256
+ for s in num_stages
257
+ ]
258
+
259
+
260
+ def get_mxfp_autotuning_configs(
261
+ pre_hook=None,
262
+ compute_modes=("dot_scaled", "dot", "scalar"),
263
+ memory_modes=(None,),
264
+ tune_block_m=False,
265
+ ):
266
+ """Autotune grid for the MXFP8 MoE/matmul kernels. ``COMPUTE_MODE`` (a constexpr string) picks
267
+ the multiply-accumulate, and the Bayesian tuner selects it per workload (token count is
268
+ in the key):
269
+
270
+ - ``"dot_scaled"``: native group-32 scaled MMA (wide K, ``BLOCK_SIZE_K`` ∈ {128, 256}) —
271
+ wins once the grid saturates (prefill, ~S≥32).
272
+ - ``"dot"``: fp8 ``tl.dot`` + per-group software rescale, one scale group per K-step
273
+ (``BLOCK_SIZE_K == 32``).
274
+ - ``"scalar"``: scalar CUDA-core FMA reduction, no tensor core — same per-group structure
275
+ as ``"dot"`` (hence bit-exact with it) but avoids the scaled-MMA's M→16 pad, so it wins
276
+ for memory-bound **decode** (M=1; ≈2× over ``dot_scaled`` at S≥32 in a sweep). Only emit
277
+ for kernels that implement the branch.
278
+
279
+ ``num_warps=16`` and ``BLOCK_SIZE_K=64`` are omitted (dead at M=1 per a sweep).
280
+
281
+ ``memory_modes`` (fused_grouped only) is the ``MEMORY_MODE`` axis for the weight load — e.g.
282
+ ``("host_descriptor", "pointer")`` on CUDA, ``("device_descriptor", "pointer")`` on XPU
283
+ (a host/device tensor descriptor — TMA on NVIDIA — vs explicit pointers); the
284
+ ``(None,)`` (default) emits no axis. Pass all the modes a kernel implements; this drops
285
+ the device-inapplicable descriptor flavor (host-side is NVIDIA-TMA-only, device-side is the
286
+ XPU win). a given pre_hook is attached to every config and self-guards on ``MEMORY_MODE``."""
287
+ drop = (
288
+ "host_descriptor" if get_active_device_type() == "xpu" else "device_descriptor"
289
+ )
290
+ memory_modes = tuple(m for m in memory_modes if m != drop)
291
+ if "device_descriptor" in memory_modes:
292
+ # device-side descriptors build in-kernel tensormaps that need a scratch allocator (the
293
+ # XPU path; CUDA keeps host-built descriptors and never reaches this).
294
+ triton.set_allocator(
295
+ lambda size, alignment, stream: torch.empty(
296
+ size, dtype=torch.int8, device="xpu"
297
+ )
298
+ )
299
+ bms = (16, 32, 64, 128) if tune_block_m else (None,)
300
+ return [
301
+ triton.Config(
302
+ {
303
+ "COMPUTE_MODE": mode,
304
+ "BLOCK_SIZE_N": bn,
305
+ "BLOCK_SIZE_K": bk,
306
+ **({"MEMORY_MODE": mm} if mm is not None else {}),
307
+ **({"BLOCK_SIZE_M": bm} if bm is not None else {}),
308
+ },
309
+ num_warps=w,
310
+ num_stages=s,
311
+ # Always attached when given; the hook self-guards on MEMORY_MODE (only
312
+ # host_descriptor configs use the host-built descriptor).
313
+ pre_hook=pre_hook,
314
+ )
315
+ for mode in compute_modes
316
+ for mm in memory_modes
317
+ for bn in [32, 64, 128, 256]
318
+ # only "dot" pins BK to the group (one scale group per fp8 tl.dot step); "dot_scaled"
319
+ # and "scalar" handle multiple groups per K-chunk, so they use a wide BK.
320
+ for bk in ([32] if mode == "dot" else [128, 256])
321
+ for s in [2, 3, 4, 5, 6]
322
+ for w in [2, 4, 8]
323
+ for bm in bms
324
+ ]
325
+
326
+
327
+ @functools.lru_cache(maxsize=None)
328
+ def sm_shared_memory_limit(device_index: int) -> int:
329
+ """Max dynamic shared memory per block (bytes) for a CUDA device — the cap Triton
330
+ reports as the 'Hardware limit' on an ``out of resource: shared memory`` failure
331
+ (~232 KB on B200, ~227 KB on H100, much less on older/consumer parts). Queried from
332
+ the driver so the prune adapts to the hardware instead of hardcoding one GPU."""
333
+ try:
334
+ return triton.runtime.driver.active.utils.get_device_properties(device_index)[
335
+ "max_shared_mem"
336
+ ]
337
+ except Exception:
338
+ if get_active_device_type() == "cuda":
339
+ return torch.cuda.get_device_properties(
340
+ device_index
341
+ ).shared_memory_per_block_optin
342
+ elif get_active_device_type() == "xpu":
343
+ return torch.xpu.get_device_properties(
344
+ device_index
345
+ ).shared_memory_per_block_optin
346
+ else:
347
+ raise RuntimeError(
348
+ f"Unsupported device type {get_active_device_type()} for sm_shared_memory_limit; only cuda/xpu are supported."
349
+ )
350
+
351
+
352
+ def smem_config_pruner(act_bytes: int, n_weight_tiles: int, weight_bytes: int = 1):
353
+ """Build an ``early_config_prune`` that drops configs whose pipelined operand shared
354
+ memory would overflow the SM — the source of ``out of resource: shared memory`` autotune
355
+ failures (and the wasted compiles they cause).
356
+
357
+ Per-stage estimate (bytes) = ``BK · (act_bytes·BM + weight_bytes·n_weight_tiles·BN)``:
358
+ one activation tile ``[BM, BK]`` plus ``n_weight_tiles`` weight tiles ``[BN, BK]``
359
+ (gate_up fuses 2; down has 1), times ``num_stages``. ``BM`` is the routing-derived tile,
360
+ read from the launch args. MXFP4 packs 2 weights/byte, so ``weight_bytes=1`` (MXFP8) is a
361
+ safe upper bound. The limit is read from the active device. Never returns empty — keeps
362
+ the smallest-footprint config as a fallback."""
363
+
364
+ def prune(configs, named_args, **kwargs):
365
+ # The estimate needs all three tile dims. Each is either an autotuned config meta
366
+ # (MX) or a launch arg (block-dynamic), so look in both; raise clearly if missing.
367
+ all_args = {**named_args, **kwargs}
368
+ dev = triton.runtime.driver.active.get_active_torch_device()
369
+ limit = sm_shared_memory_limit(dev.index if dev.index is not None else 0)
370
+
371
+ def dim(c, name):
372
+ v = c.kwargs.get(name, all_args.get(name))
373
+ if v is None:
374
+ raise ValueError(
375
+ f"smem_config_pruner needs {name} (autotune config meta or launch arg) "
376
+ "to estimate shared memory; none found."
377
+ )
378
+ return v
379
+
380
+ def smem(c):
381
+ BM, BN, BK = (
382
+ dim(c, n) for n in ("BLOCK_SIZE_M", "BLOCK_SIZE_N", "BLOCK_SIZE_K")
383
+ )
384
+ return (
385
+ c.num_stages
386
+ * BK
387
+ * (act_bytes * BM + weight_bytes * n_weight_tiles * BN)
388
+ )
389
+
390
+ kept = [c for c in configs if smem(c) <= limit]
391
+ return kept or [min(configs, key=smem)]
392
+
393
+ return prune
394
+
395
+
396
+ # ── Triton-side helpers (inlined by ``@triton.jit`` callers) ──────────────────
397
+
398
+
399
+ @triton.jit
400
+ def fp8_act_quant_inline(a_raw):
401
+ """Inline FP8 (E4M3) activation quant for the W8A8 block-scale path.
402
+
403
+ Per-row amax → fp32 scale ``amax/448`` (floored at 1e-12 against zero rows)
404
+ → cast values to FP8. Returns ``(a_fp8, a_s)`` with shapes ``(M, K)`` and
405
+ ``(M,)``.
406
+ """
407
+ a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
408
+ a_fp8 = (a_raw / tl.maximum(a_s[:, None], 1e-12)).to(tl.float8e4nv)
409
+ return a_fp8, a_s
410
+
411
+
412
+ @triton.jit
413
+ def mxfp_act_quant_inline(
414
+ a_raw,
415
+ BLOCK_SIZE_M: tl.constexpr,
416
+ BLOCK_SIZE_K: tl.constexpr,
417
+ SCALE_GROUP_K: tl.constexpr,
418
+ ):
419
+ """Inline E4M3 activation quant for the MX paths (W4A8 MXFP4 / W8A8 MXFP8).
420
+
421
+ Per-row, per-K-group amax → UE8M0 scale (ceil to next power-of-2 via the
422
+ mantissa-nonzero bump trick) → cast values to FP8. Returns ``(a_fp8,
423
+ a_scale_u8)`` with shapes ``(M, K)`` and ``(M, K // SCALE_GROUP_K)``.
424
+ """
425
+ a_groups = tl.reshape(
426
+ a_raw, (BLOCK_SIZE_M, BLOCK_SIZE_K // SCALE_GROUP_K, SCALE_GROUP_K)
427
+ )
428
+ a_s_fp32 = tl.max(tl.abs(a_groups), axis=2) / 448.0
429
+ bits = a_s_fp32.to(tl.int32, bitcast=True)
430
+ # ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
431
+ exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
432
+ exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
433
+ a_scale_u8 = exp_ceil.to(tl.uint8)
434
+ a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
435
+ a_fp8 = tl.reshape(
436
+ a_groups / tl.maximum(a_s_pow2[:, :, None], 1e-12),
437
+ (BLOCK_SIZE_M, BLOCK_SIZE_K),
438
+ ).to(tl.float8e4nv)
439
+ return a_fp8, a_scale_u8
440
+
441
+
442
+ @triton.jit
443
+ def decode_ue8m0_scale(scale):
444
+ """Decode a UE8M0 weight scale to fp32: when it was loaded as ``uint8`` exponent
445
+ bits, ``value = 2^(exp - 127)``, built directly as the fp32 bit pattern. fp32
446
+ scales (block-dynamic with float scales) pass through. The dtype branch is a
447
+ compile-time constant, so only the taken path is emitted (single return — Triton
448
+ requires all ``return`` statements to share a type)."""
449
+ if scale.dtype == tl.uint8:
450
+ scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
451
+ return scale
452
+
453
+
454
+ @triton.jit
455
+ def expand_ue8m0_scale(
456
+ scale_u8, ROWS: tl.constexpr, BK: tl.constexpr, GK: tl.constexpr
457
+ ):
458
+ """Decode a UE8M0 group scale tile ``[ROWS, BK//GK]`` and broadcast each group scale
459
+ across its ``GK`` elements → ``[ROWS, BK]`` fp32 — one scale per K element. Used by the
460
+ scalar (CUDA-core FMA) MX paths to dequant the whole BK chunk before the reduction."""
461
+ s = decode_ue8m0_scale(scale_u8)
462
+ return tl.reshape(tl.broadcast_to(s[:, :, None], (ROWS, BK // GK, GK)), (ROWS, BK))
463
+
464
+
465
+ @triton.jit
466
+ def mx_dot_scaled(a, a_scale, w, w_scale, acc, VALUES_PER_BYTE: tl.constexpr):
467
+ """MX 'dot_scaled' path: scaled MMA folding the UE8M0 group scales into the tensor core —
468
+ ``a`` (E4M3) @ ``w`` (packed E2M1 if MXFP4, else E4M3). The rhs format is picked from
469
+ ``VALUES_PER_BYTE``. Caller pre-shapes ``w``/``w_scale`` (e.g. ``tl.trans(gu)``)."""
470
+ rhs_format: tl.constexpr = "e2m1" if VALUES_PER_BYTE == 2 else "e4m3"
471
+ return tl.dot_scaled(a, a_scale, "e4m3", w, w_scale, rhs_format, acc)
472
+
473
+
474
+ @triton.jit
475
+ def mx_dot_rescale(acc, a, wq, a_scale, w_scale):
476
+ """MX 'dot' path (BK == group): fp8 ``tl.dot`` + per-group software rescale, decoding both
477
+ UE8M0 scales internally and accumulating into ``acc`` (returned updated). Single weight — for
478
+ the gate∪up pair (shared activation scale) use ``mx_dot_rescale_gate_up``."""
479
+ return acc + tl.dot(a, wq) * decode_ue8m0_scale(a_scale) * tl.trans(
480
+ decode_ue8m0_scale(w_scale)
481
+ )
482
+
483
+
484
+ @triton.jit
485
+ def mx_dot_rescale_gate_up(
486
+ acc_gate, acc_up, a, w_gate, w_up, a_scale, gate_scale, up_scale
487
+ ):
488
+ """Gate∪up 'dot' path: decode the SHARED activation scale once, rescale both projections
489
+ and accumulate into ``acc_gate``/``acc_up`` (returned updated)."""
490
+ a_s = decode_ue8m0_scale(a_scale)
491
+ acc_gate += tl.dot(a, w_gate) * a_s * tl.trans(decode_ue8m0_scale(gate_scale))
492
+ acc_up += tl.dot(a, w_up) * a_s * tl.trans(decode_ue8m0_scale(up_scale))
493
+ return acc_gate, acc_up
494
+
495
+
496
+ @triton.jit
497
+ def mx_scalar_reduce(
498
+ acc,
499
+ a,
500
+ a_scale,
501
+ wq,
502
+ w_scale,
503
+ BLOCK_SIZE_M: tl.constexpr,
504
+ ROWS_W: tl.constexpr,
505
+ BK: tl.constexpr,
506
+ GK: tl.constexpr,
507
+ ):
508
+ """MX 'scalar' path: CUDA-core FMA GEMV, dequantizing activation + weight per-element by
509
+ their expanded group scales internally, then reducing and accumulating into ``acc`` (returned
510
+ updated). No tensor core (so no M→16 MMA pad) — wins for the memory-bound decode GEMV (M=1).
511
+ Single weight — for the gate∪up pair (shared activation dequant) use ``mx_scalar_reduce_gate_up``."""
512
+ a_deq = tl.trans(
513
+ a.to(tl.float32) * expand_ue8m0_scale(a_scale, BLOCK_SIZE_M, BK, GK)
514
+ )
515
+ w_exp = tl.trans(expand_ue8m0_scale(w_scale, ROWS_W, BK, GK))
516
+ return acc + tl.sum(a_deq * (wq.to(tl.float32) * w_exp), axis=0)[None, :]
517
+
518
+
519
+ @triton.jit
520
+ def mx_scalar_reduce_gate_up(
521
+ acc_gate,
522
+ acc_up,
523
+ a,
524
+ w_gate,
525
+ w_up,
526
+ a_scale,
527
+ gate_scale,
528
+ up_scale,
529
+ BLOCK_SIZE_M: tl.constexpr,
530
+ ROWS_W: tl.constexpr,
531
+ BK: tl.constexpr,
532
+ GK: tl.constexpr,
533
+ ):
534
+ """Gate∪up 'scalar' path: dequant the SHARED activation once, reduce both projections and
535
+ accumulate into ``acc_gate``/``acc_up`` (returned updated)."""
536
+ a_deq = tl.trans(
537
+ a.to(tl.float32) * expand_ue8m0_scale(a_scale, BLOCK_SIZE_M, BK, GK)
538
+ )
539
+ acc_gate += tl.sum(
540
+ a_deq
541
+ * (
542
+ w_gate.to(tl.float32)
543
+ * tl.trans(expand_ue8m0_scale(gate_scale, ROWS_W, BK, GK))
544
+ ),
545
+ axis=0,
546
+ )[None, :]
547
+ acc_up += tl.sum(
548
+ a_deq
549
+ * (
550
+ w_up.to(tl.float32) * tl.trans(expand_ue8m0_scale(up_scale, ROWS_W, BK, GK))
551
+ ),
552
+ axis=0,
553
+ )[None, :]
554
+ return acc_gate, acc_up
555
+
556
+
557
+ @triton.jit
558
+ def mx_compute_gate_up(
559
+ acc_gate,
560
+ acc_up,
561
+ a,
562
+ a_scale,
563
+ b_gate,
564
+ b_up,
565
+ gate_scale,
566
+ up_scale,
567
+ COMPUTE_MODE: tl.constexpr,
568
+ VALUES_PER_BYTE: tl.constexpr,
569
+ BLOCK_SIZE_M: tl.constexpr,
570
+ BLOCK_SIZE_N: tl.constexpr,
571
+ BLOCK_SIZE_K: tl.constexpr,
572
+ SCALE_GROUP_K: tl.constexpr,
573
+ ):
574
+ """Gate∪up MMA step dispatched on ``COMPUTE_MODE``: scaled-MMA on the raw weights
575
+ (``b_gate``/``b_up``), or fp8 ``tl.dot`` + per-group rescale / scalar reduce on the
576
+ E4M3-decoded weights. Returns the updated ``(acc_gate, acc_up)``."""
577
+ w_gate = mxfp4_e2m1_to_e4m3(b_gate) if VALUES_PER_BYTE == 2 else b_gate
578
+ w_up = mxfp4_e2m1_to_e4m3(b_up) if VALUES_PER_BYTE == 2 else b_up
579
+ if COMPUTE_MODE == "dot_scaled":
580
+ acc_gate = mx_dot_scaled(
581
+ a, a_scale, b_gate, gate_scale, acc_gate, VALUES_PER_BYTE
582
+ )
583
+ acc_up = mx_dot_scaled(a, a_scale, b_up, up_scale, acc_up, VALUES_PER_BYTE)
584
+ elif COMPUTE_MODE == "dot":
585
+ acc_gate, acc_up = mx_dot_rescale_gate_up(
586
+ acc_gate, acc_up, a, w_gate, w_up, a_scale, gate_scale, up_scale
587
+ )
588
+ else: # scalar
589
+ acc_gate, acc_up = mx_scalar_reduce_gate_up(
590
+ acc_gate,
591
+ acc_up,
592
+ a,
593
+ w_gate,
594
+ w_up,
595
+ a_scale,
596
+ gate_scale,
597
+ up_scale,
598
+ BLOCK_SIZE_M,
599
+ BLOCK_SIZE_N,
600
+ BLOCK_SIZE_K,
601
+ SCALE_GROUP_K,
602
+ )
603
+ return acc_gate, acc_up
604
+
605
+
606
+ @triton.jit
607
+ def mx_compute(
608
+ acc,
609
+ a,
610
+ a_scale,
611
+ w,
612
+ w_scale,
613
+ COMPUTE_MODE: tl.constexpr,
614
+ VALUES_PER_BYTE: tl.constexpr,
615
+ BLOCK_SIZE_M: tl.constexpr,
616
+ BLOCK_SIZE_N: tl.constexpr,
617
+ BLOCK_SIZE_K: tl.constexpr,
618
+ SCALE_GROUP_K: tl.constexpr,
619
+ ):
620
+ """Single-projection MMA step dispatched on ``COMPUTE_MODE``: scaled-MMA on the raw weight
621
+ (``w``), or fp8 ``tl.dot`` + per-group rescale / scalar reduce on the E4M3-decoded weight.
622
+ Returns the updated ``acc``."""
623
+ wq = mxfp4_e2m1_to_e4m3(w) if VALUES_PER_BYTE == 2 else w
624
+ if COMPUTE_MODE == "dot_scaled":
625
+ acc = mx_dot_scaled(a, a_scale, w, w_scale, acc, VALUES_PER_BYTE)
626
+ elif COMPUTE_MODE == "dot":
627
+ acc = mx_dot_rescale(acc, a, wq, a_scale, w_scale)
628
+ else: # scalar
629
+ acc = mx_scalar_reduce(
630
+ acc,
631
+ a,
632
+ a_scale,
633
+ wq,
634
+ w_scale,
635
+ BLOCK_SIZE_M,
636
+ BLOCK_SIZE_N,
637
+ BLOCK_SIZE_K,
638
+ SCALE_GROUP_K,
639
+ )
640
+ return acc
641
+
642
+
643
+ @triton.jit
644
+ def _activation(x, ACT_FN: tl.constexpr):
645
+ """Pointwise GLU gate activation. ``ACT_FN`` in ``{"silu", "gelu", "relu"}``; gelu is exact
646
+ (erf), matching torch's default ``F.gelu``."""
647
+ if ACT_FN == "silu":
648
+ return x * tl.sigmoid(x)
649
+ elif ACT_FN == "gelu":
650
+ return 0.5 * x * (1.0 + tl.erf(x * 0.7071067811865476))
651
+ elif ACT_FN == "relu":
652
+ return tl.maximum(x, 0.0)
653
+ else:
654
+ tl.static_assert(
655
+ False, "unsupported ACT_FN; expected 'silu', 'gelu', or 'relu'"
656
+ )
657
+
658
+
659
+ @triton.jit
660
+ def glu(
661
+ acc_gate,
662
+ acc_up,
663
+ ACT_FN: tl.constexpr,
664
+ SIMULATE_UNFUSED: tl.constexpr,
665
+ OUT_DTYPE: tl.constexpr,
666
+ ):
667
+ """Gated linear unit: ``ACT_FN(gate) * up`` (SwiGLU / GeGLU / ReGLU). When ``SIMULATE_UNFUSED``,
668
+ round each op through ``OUT_DTYPE`` to match the numerics of the unfused (separate-kernel)
669
+ path, where every intermediate is materialized in that dtype."""
670
+ if SIMULATE_UNFUSED:
671
+ g = acc_gate.to(OUT_DTYPE).to(tl.float32)
672
+ u = acc_up.to(OUT_DTYPE).to(tl.float32)
673
+ act = _activation(g, ACT_FN).to(OUT_DTYPE).to(tl.float32)
674
+ return (act * u).to(OUT_DTYPE).to(tl.float32)
675
+ return _activation(acc_gate, ACT_FN) * acc_up
676
+
677
+
678
+ @triton.jit
679
+ def _e2m1_code_to_f32(code):
680
+ """One E2M1 4-bit code -> fp32. Layout ``[sign | exp(2) | mant(1)]``; the 8
681
+ magnitudes are ``{0, .5, 1, 1.5, 2, 3, 4, 6}`` (exp==0 is the 0/0.5 subnormal)."""
682
+ code = code.to(tl.int32)
683
+ s = (code >> 3) & 1
684
+ e = (code >> 1) & 3
685
+ m = (code & 1).to(tl.float32)
686
+ pow2 = tl.exp2((e - 1).to(tl.float32)) # e in 0..3 -> 0.5, 1, 2, 4
687
+ mag = tl.where(e == 0, 0.5 * m, (1.0 + 0.5 * m) * pow2)
688
+ return (1.0 - 2.0 * s.to(tl.float32)) * mag
689
+
690
+
691
+ @triton.jit
692
+ def mxfp4_e2m1_to_e4m3(b_packed):
693
+ """Unpack packed MXFP4 (E2M1, two nibbles/byte along K) to E4M3, doubling the K
694
+ (row) dim: ``(R, C) uint8 -> (2R, C) E4M3``. E2M1's 8 magnitudes are all exact in
695
+ E4M3, so this is lossless — it lets the FP8 ``tl.dot`` path stand in for
696
+ ``tl.dot_scaled`` at decode (avoiding its M->128 pad). K order is the low nibble
697
+ first: ``[byte0_lo, byte0_hi, byte1_lo, ...]``."""
698
+ lo = _e2m1_code_to_f32(b_packed & 0xF)
699
+ hi = _e2m1_code_to_f32(b_packed >> 4)
700
+ # interleave along the K (row) dim via trans -> interleave-last-dim -> trans back
701
+ unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
702
+ return unpacked.to(tl.float8e4nv)
703
+
704
+
705
+ # ── fp8_act_quant kernel (used by tensor-mode FP8 wrappers) ───────────────────
706
+
707
+
708
+ @triton.jit
709
+ def _fp8_act_quant_kernel(
710
+ x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr, PADDED_BLOCK: tl.constexpr
711
+ ):
712
+ # ``tl.arange`` needs a power-of-2 length, so iterate over PADDED_BLOCK (the next
713
+ # power of 2) and mask the tail — lets block_size be non-power-of-2 (e.g. a full
714
+ # row K=14336 in tensor-mode). Masked lanes load 0, which can't affect ``amax``.
715
+ pid = tl.program_id(axis=0)
716
+ cols = tl.arange(0, PADDED_BLOCK)
717
+ mask = cols < BLOCK_SIZE
718
+ offs = pid * BLOCK_SIZE + cols
719
+ x = tl.load(x_ptr + offs, mask=mask, other=0.0).to(tl.float32)
720
+ s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
721
+ y = (x / tl.maximum(s, 1e-12)).to(y_ptr.dtype.element_ty)
722
+ tl.store(y_ptr + offs, y, mask=mask)
723
+ tl.store(s_ptr + pid, s)
724
+
725
+
726
+ @triton_op(add_op_namespace_prefix("fp8_act_quant"), mutates_args=())
727
+ def _fp8_act_quant(
728
+ x: torch.Tensor, block_size: int = 128
729
+ ) -> tuple[torch.Tensor, torch.Tensor]:
730
+ assert x.is_contiguous()
731
+ assert x.shape[-1] % block_size == 0
732
+ y = torch.empty_like(x, dtype=FP8_DTYPE)
733
+ grid = (triton.cdiv(x.numel(), block_size),)
734
+ s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
735
+
736
+ with device_context(x.device):
737
+ wrap_triton(_fp8_act_quant_kernel)[grid](
738
+ x,
739
+ y,
740
+ s,
741
+ BLOCK_SIZE=block_size,
742
+ PADDED_BLOCK=triton.next_power_of_2(block_size),
743
+ )
744
+
745
+ return y, s
746
+
747
+
748
+ def fp8_act_quant(
749
+ x: torch.Tensor, block_size: int = 128
750
+ ) -> tuple[torch.Tensor, torch.Tensor]:
751
+ """Quantize activations to FP8 with per-block dynamic scaling.
752
+
753
+ Splits the last dimension of ``x`` into blocks of ``block_size`` elements,
754
+ computes ``scale = max(|x_block|) / 448`` per block, and quantizes to
755
+ ``float8_e4m3fn``.
756
+
757
+ Args:
758
+ x: Input tensor in bf16/fp16/fp32. Last dimension must be divisible by
759
+ ``block_size`` and the tensor must be contiguous.
760
+ block_size: Number of elements per quantization block (default: 128).
761
+
762
+ Returns:
763
+ A tuple ``(quantized, scales)`` where ``quantized`` has dtype
764
+ ``float8_e4m3fn`` with the same shape as ``x``, and ``scales`` has
765
+ shape ``(*x.shape[:-1], x.shape[-1] // block_size)`` in float32.
766
+ """
767
+ return ops.fp8_act_quant(x, block_size)
build/torch-rocm/__init__.py DELETED
@@ -1,32 +0,0 @@
1
- from .act_quant import fp8_act_quant
2
- from .batched import (
3
- w8a8_fp8_matmul_batched,
4
- w8a8_block_fp8_matmul_batched,
5
- w8a8_tensor_fp8_matmul_batched,
6
- )
7
- from .grouped import (
8
- w8a8_fp8_matmul_grouped,
9
- w8a8_block_fp8_matmul_grouped,
10
- w8a8_tensor_fp8_matmul_grouped,
11
- )
12
- from .matmul import (
13
- w8a8_fp8_matmul,
14
- w8a8_block_fp8_matmul,
15
- w8a8_tensor_fp8_matmul,
16
- )
17
-
18
- __all__ = [
19
- "fp8_act_quant",
20
- # Single matmul
21
- "w8a8_fp8_matmul",
22
- "w8a8_block_fp8_matmul",
23
- "w8a8_tensor_fp8_matmul",
24
- # Batched matmul
25
- "w8a8_fp8_matmul_batched",
26
- "w8a8_block_fp8_matmul_batched",
27
- "w8a8_tensor_fp8_matmul_batched",
28
- # Grouped matmul
29
- "w8a8_fp8_matmul_grouped",
30
- "w8a8_block_fp8_matmul_grouped",
31
- "w8a8_tensor_fp8_matmul_grouped",
32
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/_ops.py DELETED
@@ -1,38 +0,0 @@
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_fp8"
25
- unique_id = "bbc7e0f"
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/act_quant.py DELETED
@@ -1,73 +0,0 @@
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 torch.library import triton_op, wrap_triton
19
-
20
- from .utils import device_context
21
-
22
-
23
- _FP8_DTYPE = torch.float8_e4m3fn
24
-
25
-
26
- # Copied from https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/inference/kernel.py
27
- @triton.jit
28
- def _fp8_act_quant_kernel(x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr):
29
- pid = tl.program_id(axis=0)
30
- offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
31
- x = tl.load(x_ptr + offs).to(tl.float32)
32
- s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
33
- y = (x / s).to(y_ptr.dtype.element_ty)
34
- tl.store(y_ptr + offs, y)
35
- tl.store(s_ptr + pid, s)
36
-
37
-
38
- @triton_op("finegrained_fp8::fp8_act_quant", mutates_args=())
39
- def _fp8_act_quant(
40
- x: torch.Tensor, block_size: int = 128
41
- ) -> tuple[torch.Tensor, torch.Tensor]:
42
- assert x.is_contiguous()
43
- assert x.shape[-1] % block_size == 0
44
- y = torch.empty_like(x, dtype=_FP8_DTYPE)
45
- grid = (triton.cdiv(x.numel(), block_size),)
46
- s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
47
-
48
- with device_context(x.device):
49
- wrap_triton(_fp8_act_quant_kernel)[grid](x, y, s, BLOCK_SIZE=block_size)
50
-
51
- return y, s
52
-
53
-
54
- def fp8_act_quant(
55
- x: torch.Tensor, block_size: int = 128
56
- ) -> tuple[torch.Tensor, torch.Tensor]:
57
- """Quantize activations to FP8 with per-block dynamic scaling.
58
-
59
- Splits the last dimension of ``x`` into blocks of ``block_size`` elements,
60
- computes ``scale = max(|x_block|) / 448`` per block, and quantizes to
61
- ``float8_e4m3fn``.
62
-
63
- Args:
64
- x: Input tensor in bf16/fp16/fp32. Last dimension must be divisible by
65
- ``block_size`` and the tensor must be contiguous.
66
- block_size: Number of elements per quantization block (default: 128).
67
-
68
- Returns:
69
- A tuple ``(quantized, scales)`` where ``quantized`` has dtype
70
- ``float8_e4m3fn`` with the same shape as ``x``, and ``scales`` has
71
- shape ``(*x.shape[:-1], x.shape[-1] // block_size)`` in float32.
72
- """
73
- return torch.ops.finegrained_fp8.fp8_act_quant(x, block_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/batched.py DELETED
@@ -1,390 +0,0 @@
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 .act_quant import fp8_act_quant
19
- from torch.library import triton_op, wrap_triton
20
-
21
- from .utils import device_context
22
-
23
-
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
- key=["N", "K"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_batched_kernel(
34
- A, # (S, K) raw BF16/FP16 activations
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
- ExpertIds, # (S,) — which expert each batch element routes to
39
- # Shape
40
- S,
41
- N,
42
- K,
43
- stride_am,
44
- stride_ak,
45
- stride_be,
46
- stride_bk,
47
- stride_bn,
48
- stride_cm,
49
- stride_cn,
50
- stride_bs_e,
51
- stride_bs_k,
52
- stride_bs_n,
53
- # Meta-parameters
54
- BLOCK_SIZE_N: tl.constexpr,
55
- BLOCK_SIZE_K: tl.constexpr,
56
- BLOCK_SIZE_M: tl.constexpr,
57
- ):
58
- """Block-scale batched FP8 expert matmul kernel.
59
-
60
- Each program handles one routed token row and one N-tile, looks up the
61
- owning expert from ``ExpertIds``, and applies fused activation quantization.
62
- """
63
- batch_id = tl.program_id(axis=0)
64
- pid_n = tl.program_id(axis=1)
65
-
66
- # Cast expert_id to int64 to prevent int32 overflow when computing
67
- # expert_id * stride_Eb (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
68
- # 3072×3072 FP8 weights).
69
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
70
-
71
- A = A + batch_id * stride_am
72
- B = B + expert_id * stride_be
73
- C = C + batch_id * stride_cm
74
- Bs = Bs + expert_id * stride_bs_e
75
-
76
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
77
- offs_k = tl.arange(0, BLOCK_SIZE_K)
78
- a_ptrs = A + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0 + offs_k[None, :] * stride_ak
79
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
80
-
81
- bs_ptrs = Bs + pid_n * stride_bs_n
82
-
83
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
84
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
85
- # ---- fused fp8_act_quant ----
86
- a_raw = tl.load(a_ptrs).to(tl.float32)
87
- a_s = tl.max(tl.abs(a_raw)) / 448.0
88
- a = (a_raw / tl.maximum(a_s, 1e-12)).to(tl.float8e4nv)
89
- # ---- matmul ----
90
- b = tl.load(b_ptrs)
91
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
92
- accumulator += tl.dot(a, b) * a_s * b_s[None, :]
93
- a_ptrs += BLOCK_SIZE_K * stride_ak
94
- b_ptrs += BLOCK_SIZE_K * stride_bk
95
-
96
- if C.dtype.element_ty == tl.bfloat16:
97
- c = accumulator.to(tl.bfloat16)
98
- elif C.dtype.element_ty == tl.float16:
99
- c = accumulator.to(tl.float16)
100
- else:
101
- c = accumulator.to(tl.float32)
102
-
103
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
104
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
105
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
106
- tl.store(c_ptrs, c)
107
-
108
-
109
- @triton.autotune(
110
- configs=[
111
- triton.Config({}, num_warps=w, num_stages=s)
112
- for w in [2, 4, 8, 16]
113
- for s in [2, 3, 4, 5]
114
- ],
115
- key=["N", "K"],
116
- )
117
- @triton.jit
118
- def w8a8_tensor_fp8_matmul_batched_kernel(
119
- A, # (S, K) pre-quantized FP8 activations
120
- B, # (E, N, K) FP8 weight matrices
121
- C, # (S, N) output
122
- As, # (S, 1) per-tensor activation scales
123
- Bs, # (E, 1, 1) per-tensor weight scales
124
- ExpertIds,
125
- S,
126
- N,
127
- K,
128
- stride_am,
129
- stride_ak,
130
- stride_be,
131
- stride_bk,
132
- stride_bn,
133
- stride_cm,
134
- stride_cn,
135
- stride_as_m,
136
- stride_bs_e,
137
- BLOCK_SIZE_N: tl.constexpr,
138
- BLOCK_SIZE_K: tl.constexpr,
139
- BLOCK_SIZE_M: tl.constexpr,
140
- ):
141
- """Tensor-scale batched FP8 expert matmul kernel.
142
-
143
- Activations are already quantized; the kernel applies per-token activation
144
- scales and per-expert tensor weight scales.
145
- """
146
- batch_id = tl.program_id(axis=0)
147
- pid_n = tl.program_id(axis=1)
148
-
149
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
150
-
151
- A = A + batch_id * stride_am
152
- B = B + expert_id * stride_be
153
- C = C + batch_id * stride_cm
154
- Bs = Bs + expert_id * stride_bs_e
155
-
156
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
157
- offs_k = tl.arange(0, BLOCK_SIZE_K)
158
- a_ptrs = A + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0 + offs_k[None, :] * stride_ak
159
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
160
-
161
- b_s = tl.load(Bs)
162
- a_s = tl.load(As + batch_id * stride_as_m)
163
-
164
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
165
- for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
166
- a = tl.load(a_ptrs)
167
- b = tl.load(b_ptrs)
168
- accumulator += tl.dot(a, b)
169
- a_ptrs += BLOCK_SIZE_K * stride_ak
170
- b_ptrs += BLOCK_SIZE_K * stride_bk
171
-
172
- accumulator = accumulator * a_s * b_s
173
-
174
- if C.dtype.element_ty == tl.bfloat16:
175
- c = accumulator.to(tl.bfloat16)
176
- elif C.dtype.element_ty == tl.float16:
177
- c = accumulator.to(tl.float16)
178
- else:
179
- c = accumulator.to(tl.float32)
180
-
181
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
182
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
183
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
184
- tl.store(c_ptrs, c)
185
-
186
-
187
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_batched", mutates_args=())
188
- def _w8a8_block_fp8_matmul_batched(
189
- A: torch.Tensor,
190
- B: torch.Tensor,
191
- Bs: torch.Tensor,
192
- expert_ids: torch.Tensor,
193
- block_size: list[int],
194
- ) -> torch.Tensor:
195
- """Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
196
-
197
- A: (S, K) raw bf16/fp16 activations
198
- B: (E, N, K) FP8 expert weights
199
- Bs: (E, N // block_n, K // block_k) per-block weight scales
200
- """
201
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
202
- assert A.is_contiguous(), "A must be contiguous"
203
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
204
- assert B.is_contiguous(), "B must be contiguous"
205
- assert A.shape[1] == B.shape[2], (
206
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
207
- )
208
-
209
- S, K = A.shape
210
- E, N, _ = B.shape
211
-
212
- assert len(block_size) == 2, (
213
- f"block_size must be [block_n, block_k], got {block_size}"
214
- )
215
- block_n, block_k = block_size[0], block_size[1]
216
- # MoE expert dimensions must be block-aligned; non-aligned N/K is not supported.
217
- assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
218
- assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
219
- assert Bs.ndim == 3, (
220
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
221
- )
222
- assert Bs.shape == (E, N // block_n, K // block_k), (
223
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
224
- )
225
-
226
- C = A.new_empty(S, N)
227
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
228
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
229
- # register pressure and a better-matched FP8 WGMMA instruction, improving
230
- # both accuracy and performance for small M (decode).
231
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
232
- grid = (S, triton.cdiv(N, block_n))
233
- with device_context(A.device):
234
- wrap_triton(w8a8_block_fp8_matmul_batched_kernel)[grid](
235
- A,
236
- B,
237
- C,
238
- Bs,
239
- expert_ids,
240
- S,
241
- N,
242
- K,
243
- A.stride(0),
244
- A.stride(1),
245
- B.stride(0),
246
- B.stride(2),
247
- B.stride(1),
248
- C.stride(0),
249
- C.stride(1),
250
- Bs.stride(0),
251
- Bs.stride(2),
252
- Bs.stride(1),
253
- BLOCK_SIZE_N=block_n,
254
- BLOCK_SIZE_K=block_k,
255
- BLOCK_SIZE_M=BLOCK_SIZE_M,
256
- )
257
-
258
- return C
259
-
260
-
261
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_batched", mutates_args=())
262
- def _w8a8_tensor_fp8_matmul_batched(
263
- A: torch.Tensor,
264
- B: torch.Tensor,
265
- Bs: torch.Tensor,
266
- expert_ids: torch.Tensor,
267
- ) -> torch.Tensor:
268
- """Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
269
-
270
- A: (S, K) raw bf16/fp16 activations
271
- B: (E, N, K) FP8 expert weights
272
- Bs: (E,) or (E, 1, 1) per-expert weight scales
273
- """
274
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
275
- assert A.is_contiguous(), "A must be contiguous"
276
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
277
- assert B.is_contiguous(), "B must be contiguous"
278
- assert A.shape[1] == B.shape[2], (
279
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
280
- )
281
-
282
- S, K = A.shape
283
- E, N, _ = B.shape
284
-
285
- # Normalize Bs to (E, 1, 1)
286
- if Bs.ndim == 1:
287
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
288
- Bs = Bs.reshape(E, 1, 1)
289
- else:
290
- assert Bs.shape == (E, 1, 1), (
291
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
292
- )
293
-
294
- BLOCK_SIZE_N = 128
295
- BLOCK_SIZE_K = 128
296
- C = A.new_empty(S, N)
297
- qA, As = fp8_act_quant(A, K)
298
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
299
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
300
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
301
- # register pressure and a better-matched FP8 WGMMA instruction, improving
302
- # both accuracy and performance for small M (decode).
303
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
304
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
305
- with device_context(A.device):
306
- wrap_triton(w8a8_tensor_fp8_matmul_batched_kernel)[grid](
307
- qA,
308
- B,
309
- C,
310
- As,
311
- Bs,
312
- expert_ids,
313
- S,
314
- N,
315
- K,
316
- qA.stride(0),
317
- qA.stride(1),
318
- B.stride(0),
319
- B.stride(2),
320
- B.stride(1),
321
- C.stride(0),
322
- C.stride(1),
323
- As.stride(0),
324
- Bs.stride(0),
325
- BLOCK_SIZE_N=BLOCK_SIZE_N,
326
- BLOCK_SIZE_K=BLOCK_SIZE_K,
327
- BLOCK_SIZE_M=BLOCK_SIZE_M,
328
- )
329
-
330
- return C
331
-
332
-
333
- def w8a8_block_fp8_matmul_batched(
334
- A: torch.Tensor,
335
- B: torch.Tensor,
336
- Bs: torch.Tensor,
337
- expert_ids: torch.Tensor,
338
- block_size: list[int],
339
- ) -> torch.Tensor:
340
- """Block-scale batched FP8 matmul with fused activation quantization.
341
-
342
- A: (S, K) raw activations, bf16/fp16/fp32
343
- B: (E, N, K) FP8 expert weights
344
- Bs: (E, N // block_n, K // block_k) per-block weight scales
345
- """
346
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_batched(
347
- A, B, Bs, expert_ids, block_size
348
- )
349
-
350
-
351
- def w8a8_tensor_fp8_matmul_batched(
352
- A: torch.Tensor,
353
- B: torch.Tensor,
354
- Bs: torch.Tensor,
355
- expert_ids: torch.Tensor,
356
- ) -> torch.Tensor:
357
- """Tensor-scale batched FP8 matmul with fused activation quantization.
358
-
359
- A: (S, K) raw activations, bf16/fp16/fp32
360
- B: (E, N, K) FP8 expert weights
361
- Bs: (E,) or (E, 1, 1) per-expert weight scales
362
- """
363
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_batched(
364
- A, B, Bs, expert_ids
365
- )
366
-
367
-
368
- def w8a8_fp8_matmul_batched(
369
- A: torch.Tensor,
370
- B: torch.Tensor,
371
- Bs: torch.Tensor,
372
- expert_ids: torch.Tensor,
373
- block_size: list[int] | None,
374
- ) -> torch.Tensor:
375
- """Unified batched W8A8 FP8 matmul dispatcher.
376
-
377
- Dispatch rules:
378
- - tensor mode when ``block_size is None``
379
- - tensor mode when ``block_size == [N, K]``
380
- - otherwise block mode
381
-
382
- Returns:
383
- Output tensor ``[S, N]`` in the same dtype as ``A``.
384
- """
385
- if block_size is None or (
386
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
387
- ):
388
- return w8a8_tensor_fp8_matmul_batched(A, B, Bs, expert_ids)
389
-
390
- return w8a8_block_fp8_matmul_batched(A, B, Bs, expert_ids, block_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/finegrained_fp8/__init__.py DELETED
@@ -1,26 +0,0 @@
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/grouped.py DELETED
@@ -1,477 +0,0 @@
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 .act_quant import fp8_act_quant
19
- from torch.library import triton_op, wrap_triton
20
-
21
- from .utils import device_context
22
-
23
-
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
- key=["N", "K", "BLOCK_SIZE_M"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_grouped_kernel(
34
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert id
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
- Offsets, # (E,) int32 — cumulative row-end per expert
39
- TileOffsets, # (E,) int32 — cumulative tile-end per expert
40
- # Shape
41
- S,
42
- N,
43
- K,
44
- # Strides
45
- stride_am,
46
- stride_ak,
47
- stride_be,
48
- stride_bk,
49
- stride_bn,
50
- stride_cm,
51
- stride_cn,
52
- stride_bs_e,
53
- stride_bs_k,
54
- stride_bs_n,
55
- # Meta-parameters
56
- NUM_EXPERTS: tl.constexpr,
57
- BLOCK_SIZE_N: tl.constexpr,
58
- BLOCK_SIZE_K: tl.constexpr,
59
- BLOCK_SIZE_M: tl.constexpr,
60
- NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
61
- ):
62
- """Block-scale grouped FP8 expert matmul kernel.
63
-
64
- Tokens are assumed sorted by expert. The kernel maps each M-tile to its
65
- owning expert via ``TileOffsets`` and applies fused activation quantization.
66
- """
67
- pid_m = tl.program_id(axis=0)
68
- pid_n = tl.program_id(axis=1)
69
-
70
- # Exit early for programs beyond the actual tile count.
71
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
72
- if pid_m >= total_tiles:
73
- return
74
-
75
- # Binary search in TileOffsets to find the owning expert.
76
- # Finds the smallest e such that TileOffsets[e] > pid_m (upper_bound semantics),
77
- # which is the expert whose tile range contains pid_m.
78
- # O(log2(NUM_EXPERTS)) loads instead of the O(NUM_EXPERTS) linear scan.
79
- # NUM_EXPERTS_BIT_LENGTH is ceil(log2(E))+1 for powers-of-two, giving one
80
- # harmless extra iteration when lo==hi; it's a compile-time constant so the
81
- # loop is fully unrolled by the compiler.
82
- lo = 0
83
- hi = NUM_EXPERTS
84
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
85
- mid = (lo + hi) >> 1
86
- mid_val = tl.load(TileOffsets + mid)
87
- is_left = mid_val <= pid_m
88
- lo = tl.where(is_left, mid + 1, lo)
89
- hi = tl.where(is_left, hi, mid)
90
-
91
- # Cast expert_id to int64 to prevent int32 overflow when computing
92
- # expert_id * stride_be (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
93
- # 3072×3072 FP8 weights).
94
- expert_id = lo.to(tl.int64)
95
-
96
- prev_eid = tl.maximum(expert_id - 1, 0)
97
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
98
- expert_end = tl.load(Offsets + expert_id)
99
- M_expert = expert_end - expert_start
100
-
101
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
102
- local_tile = pid_m - expert_tile_start
103
- m_off = local_tile * BLOCK_SIZE_M
104
-
105
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
106
- row_mask = offs_am < M_expert
107
- offs_global_m = expert_start + offs_am
108
-
109
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
110
- offs_k = tl.arange(0, BLOCK_SIZE_K)
111
-
112
- a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
113
- b_ptrs = (
114
- B
115
- + expert_id * stride_be
116
- + offs_k[:, None] * stride_bk
117
- + offs_bn[None, :] * stride_bn
118
- )
119
- bs_ptrs = Bs + expert_id * stride_bs_e + pid_n * stride_bs_n
120
-
121
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
122
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
123
- # ---- fused fp8_act_quant ----
124
- a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
125
- a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
126
- a = (a_raw / tl.maximum(a_s[:, None], 1e-12)).to(tl.float8e4nv)
127
- # ---- matmul ----
128
- b = tl.load(b_ptrs)
129
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
130
- accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
131
- a_ptrs += BLOCK_SIZE_K * stride_ak
132
- b_ptrs += BLOCK_SIZE_K * stride_bk
133
-
134
- if C.dtype.element_ty == tl.bfloat16:
135
- c = accumulator.to(tl.bfloat16)
136
- elif C.dtype.element_ty == tl.float16:
137
- c = accumulator.to(tl.float16)
138
- else:
139
- c = accumulator.to(tl.float32)
140
-
141
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
142
- c_mask = row_mask[:, None]
143
- tl.store(c_ptrs, c, mask=c_mask)
144
-
145
-
146
- @triton.autotune(
147
- configs=[
148
- triton.Config({}, num_warps=w, num_stages=s)
149
- for w in [2, 4, 8, 16]
150
- for s in [2, 3, 4, 5]
151
- ],
152
- key=["N", "K", "BLOCK_SIZE_M"],
153
- )
154
- @triton.jit
155
- def w8a8_tensor_fp8_matmul_grouped_kernel(
156
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert idc
157
- B, # (E, N, K) FP8 weight matrices
158
- C, # (S, N) output
159
- As, # (S, 1) activation scales
160
- Bs, # (E, 1, 1) per-tensor weight scales
161
- Offsets,
162
- TileOffsets,
163
- S,
164
- N,
165
- K,
166
- stride_am,
167
- stride_ak,
168
- stride_be,
169
- stride_bk,
170
- stride_bn,
171
- stride_cm,
172
- stride_cn,
173
- stride_as_m,
174
- stride_bs_e,
175
- NUM_EXPERTS: tl.constexpr,
176
- BLOCK_SIZE_N: tl.constexpr,
177
- BLOCK_SIZE_K: tl.constexpr,
178
- BLOCK_SIZE_M: tl.constexpr,
179
- NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
180
- ):
181
- """Tensor-scale grouped FP8 expert matmul kernel.
182
-
183
- Uses grouped expert scheduling with pre-quantized activations plus
184
- per-token activation scales and per-expert tensor weight scales.
185
- """
186
- pid_m = tl.program_id(axis=0)
187
- pid_n = tl.program_id(axis=1)
188
-
189
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
190
- if pid_m >= total_tiles:
191
- return
192
-
193
- lo = 0
194
- hi = NUM_EXPERTS
195
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
196
- mid = (lo + hi) >> 1
197
- mid_val = tl.load(TileOffsets + mid)
198
- is_left = mid_val <= pid_m
199
- lo = tl.where(is_left, mid + 1, lo)
200
- hi = tl.where(is_left, hi, mid)
201
- expert_id = lo.to(tl.int64)
202
-
203
- prev_eid = tl.maximum(expert_id - 1, 0)
204
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
205
- expert_end = tl.load(Offsets + expert_id)
206
- M_expert = expert_end - expert_start
207
-
208
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
209
- local_tile = pid_m - expert_tile_start
210
- m_off = local_tile * BLOCK_SIZE_M
211
-
212
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
213
- row_mask = offs_am < M_expert
214
- offs_global_m = expert_start + offs_am
215
-
216
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
217
- offs_k = tl.arange(0, BLOCK_SIZE_K)
218
-
219
- a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
220
- b_ptrs = (
221
- B
222
- + expert_id * stride_be
223
- + offs_k[:, None] * stride_bk
224
- + offs_bn[None, :] * stride_bn
225
- )
226
-
227
- a_s = tl.load(As + offs_global_m * stride_as_m, mask=row_mask, other=0.0)
228
- b_s = tl.load(Bs + expert_id * stride_bs_e)
229
-
230
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
231
- for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
232
- a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
233
- b = tl.load(b_ptrs)
234
-
235
- accumulator += tl.dot(a, b)
236
- a_ptrs += BLOCK_SIZE_K * stride_ak
237
- b_ptrs += BLOCK_SIZE_K * stride_bk
238
-
239
- accumulator = accumulator * a_s[:, None] * b_s
240
-
241
- if C.dtype.element_ty == tl.bfloat16:
242
- c = accumulator.to(tl.bfloat16)
243
- elif C.dtype.element_ty == tl.float16:
244
- c = accumulator.to(tl.float16)
245
- else:
246
- c = accumulator.to(tl.float32)
247
-
248
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
249
- c_mask = row_mask[:, None]
250
- tl.store(c_ptrs, c, mask=c_mask)
251
-
252
-
253
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_grouped", mutates_args=())
254
- def _w8a8_block_fp8_matmul_grouped(
255
- A: torch.Tensor,
256
- B: torch.Tensor,
257
- Bs: torch.Tensor,
258
- offsets: torch.Tensor,
259
- tokens_per_expert: torch.Tensor,
260
- block_size: list[int],
261
- ) -> torch.Tensor:
262
- """Block-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
263
-
264
- A: (S, K) raw bf16/fp16 activations, sorted by expert
265
- B: (E, N, K) FP8 expert weights
266
- Bs: (E, N // block_n, K // block_k) per-block weight scales
267
- """
268
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
269
- assert A.is_contiguous(), "A must be contiguous"
270
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
271
- assert B.is_contiguous(), "B must be contiguous"
272
- assert A.shape[1] == B.shape[2], (
273
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
274
- )
275
-
276
- S, K = A.shape
277
- E, N, _ = B.shape
278
-
279
- assert len(block_size) == 2, (
280
- f"block_size must be [block_n, block_k], got {block_size}"
281
- )
282
- block_n, block_k = block_size[0], block_size[1]
283
- # MoE expert dimensions must be block-aligned; non-aligned N/K is not supported.
284
- assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
285
- assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
286
- assert Bs.ndim == 3, (
287
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
288
- )
289
- assert Bs.shape == (E, N // block_n, K // block_k), (
290
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
291
- )
292
-
293
- C = A.new_empty(S, N)
294
- # Adaptive BLOCK_SIZE_M: match tile to average tokens per expert.
295
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
296
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
297
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
298
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
299
- # Programs beyond the real tile count exit immediately via the early-return
300
- # guard inside the kernel. This is faster than syncing for the exact count
301
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
302
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
303
- grid = (max_M_tiles, triton.cdiv(N, block_n))
304
- with device_context(A.device):
305
- wrap_triton(w8a8_block_fp8_matmul_grouped_kernel)[grid](
306
- A,
307
- B,
308
- C,
309
- Bs,
310
- offsets,
311
- tile_offsets,
312
- S,
313
- N,
314
- K,
315
- A.stride(0),
316
- A.stride(1),
317
- B.stride(0),
318
- B.stride(2),
319
- B.stride(1),
320
- C.stride(0),
321
- C.stride(1),
322
- Bs.stride(0),
323
- Bs.stride(2),
324
- Bs.stride(1),
325
- # Meta-parameters
326
- NUM_EXPERTS=E,
327
- BLOCK_SIZE_N=block_n,
328
- BLOCK_SIZE_K=block_k,
329
- BLOCK_SIZE_M=BLOCK_SIZE_M,
330
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
331
- )
332
-
333
- return C
334
-
335
-
336
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_grouped", mutates_args=())
337
- def _w8a8_tensor_fp8_matmul_grouped(
338
- A: torch.Tensor,
339
- B: torch.Tensor,
340
- Bs: torch.Tensor,
341
- offsets: torch.Tensor,
342
- tokens_per_expert: torch.Tensor,
343
- ) -> torch.Tensor:
344
- """Tensor-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
345
-
346
- A: (S, K) raw bf16/fp16 activations, sorted by expert
347
- B: (E, N, K) FP8 expert weights
348
- Bs: (E,) or (E, 1, 1) per-expert weight scales
349
- """
350
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
351
- assert A.is_contiguous(), "A must be contiguous"
352
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
353
- assert B.is_contiguous(), "B must be contiguous"
354
- assert A.shape[1] == B.shape[2], (
355
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
356
- )
357
-
358
- S, K = A.shape
359
- E, N, _ = B.shape
360
-
361
- # Normalize Bs to (E, 1, 1)
362
- if Bs.ndim == 1:
363
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
364
- Bs = Bs.reshape(E, 1, 1)
365
- else:
366
- assert Bs.shape == (E, 1, 1), (
367
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
368
- )
369
-
370
- BLOCK_SIZE_N = 128
371
- BLOCK_SIZE_K = 128
372
- C = A.new_empty(S, N)
373
- qA, As = fp8_act_quant(A, K)
374
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
375
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
376
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
377
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
378
- # Programs beyond the real tile count exit immediately via the early-return
379
- # guard inside the kernel. This is faster than syncing for the exact count
380
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
381
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
382
- grid = (max_M_tiles, triton.cdiv(N, BLOCK_SIZE_N))
383
- with device_context(A.device):
384
- wrap_triton(w8a8_tensor_fp8_matmul_grouped_kernel)[grid](
385
- qA,
386
- B,
387
- C,
388
- As,
389
- Bs,
390
- offsets,
391
- tile_offsets,
392
- S,
393
- N,
394
- K,
395
- qA.stride(0),
396
- qA.stride(1),
397
- B.stride(0),
398
- B.stride(2),
399
- B.stride(1),
400
- C.stride(0),
401
- C.stride(1),
402
- As.stride(0),
403
- Bs.stride(0),
404
- # Meta-parameters
405
- NUM_EXPERTS=E,
406
- BLOCK_SIZE_N=BLOCK_SIZE_N,
407
- BLOCK_SIZE_K=BLOCK_SIZE_K,
408
- BLOCK_SIZE_M=BLOCK_SIZE_M,
409
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
410
- )
411
-
412
- return C
413
-
414
-
415
- def w8a8_block_fp8_matmul_grouped(
416
- A: torch.Tensor,
417
- B: torch.Tensor,
418
- Bs: torch.Tensor,
419
- offsets: torch.Tensor,
420
- tokens_per_expert: torch.Tensor,
421
- block_size: list[int],
422
- ) -> torch.Tensor:
423
- """Block-scale grouped FP8 matmul with fused activation quantization.
424
-
425
- A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
426
- B: (E, N, K) FP8 expert weights
427
- Bs: (E, N // block_n, K // block_k) per-block weight scales
428
- """
429
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_grouped(
430
- A, B, Bs, offsets, tokens_per_expert, block_size
431
- )
432
-
433
-
434
- def w8a8_tensor_fp8_matmul_grouped(
435
- A: torch.Tensor,
436
- B: torch.Tensor,
437
- Bs: torch.Tensor,
438
- offsets: torch.Tensor,
439
- tokens_per_expert: torch.Tensor,
440
- ) -> torch.Tensor:
441
- """Tensor-scale grouped FP8 matmul with fused activation quantization.
442
-
443
- A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
444
- B: (E, N, K) FP8 expert weights
445
- Bs: (E,) or (E, 1, 1) per-expert weight scales
446
- """
447
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_grouped(
448
- A, B, Bs, offsets, tokens_per_expert
449
- )
450
-
451
-
452
- def w8a8_fp8_matmul_grouped(
453
- A: torch.Tensor,
454
- B: torch.Tensor,
455
- Bs: torch.Tensor,
456
- offsets: torch.Tensor,
457
- tokens_per_expert: torch.Tensor,
458
- block_size: list[int] | None,
459
- ) -> torch.Tensor:
460
- """Unified grouped W8A8 FP8 matmul dispatcher.
461
-
462
- Dispatch rules:
463
- - tensor mode when ``block_size is None``
464
- - tensor mode when ``block_size == [N, K]``
465
- - otherwise block mode
466
-
467
- Returns:
468
- Output tensor ``[S, N]`` in the same dtype as ``A``, in expert-sorted order.
469
- """
470
- if block_size is None or (
471
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
472
- ):
473
- return w8a8_tensor_fp8_matmul_grouped(A, B, Bs, offsets, tokens_per_expert)
474
-
475
- return w8a8_block_fp8_matmul_grouped(
476
- A, B, Bs, offsets, tokens_per_expert, block_size
477
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/matmul.py DELETED
@@ -1,411 +0,0 @@
1
- # Copyright 2025 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 torch.library import triton_op, wrap_triton
19
-
20
- from .utils import device_context
21
-
22
-
23
- # Adapted from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/layers/quantization/fp8_kernel.py
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4]
29
- ],
30
- key=["N", "K", "BLOCK_SIZE_M"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_kernel(
34
- # Pointers to inputs and output
35
- A,
36
- B,
37
- C,
38
- As,
39
- Bs,
40
- # Shape for matmul
41
- M,
42
- N,
43
- K,
44
- stride_am,
45
- stride_ak,
46
- stride_bk,
47
- stride_bn,
48
- stride_cm,
49
- stride_cn,
50
- stride_as_m,
51
- stride_as_k,
52
- stride_bs_k,
53
- stride_bs_n,
54
- # Meta-parameters
55
- BLOCK_SIZE_M: tl.constexpr,
56
- BLOCK_SIZE_N: tl.constexpr,
57
- BLOCK_SIZE_K: tl.constexpr,
58
- GROUP_SIZE_M: tl.constexpr,
59
- ):
60
- """Block-scale FP8 GEMM kernel.
61
-
62
- Computes ``C = A @ B.T`` with block-wise activation/weight scales.
63
- Uses a 2D grid with swizzle for L2 cache locality on B tiles.
64
- """
65
- pid_m = tl.program_id(axis=0)
66
- pid_n = tl.program_id(axis=1)
67
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
68
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
69
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
70
-
71
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
72
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
73
- offs_k = tl.arange(0, BLOCK_SIZE_K)
74
- a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
75
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
76
-
77
- as_ptrs = As + offs_am * stride_as_m
78
- offs_bsn = offs_bn // BLOCK_SIZE_N
79
- bs_ptrs = Bs + offs_bsn * stride_bs_n
80
-
81
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
82
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
83
- k_remaining = K - k * BLOCK_SIZE_K
84
- a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0)
85
- b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
86
-
87
- a_s = tl.load(as_ptrs + k * stride_as_k)
88
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
89
-
90
- accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
91
- a_ptrs += BLOCK_SIZE_K * stride_ak
92
- b_ptrs += BLOCK_SIZE_K * stride_bk
93
-
94
- if C.dtype.element_ty == tl.bfloat16:
95
- c = accumulator.to(tl.bfloat16)
96
- elif C.dtype.element_ty == tl.float16:
97
- c = accumulator.to(tl.float16)
98
- else:
99
- c = accumulator.to(tl.float32)
100
-
101
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
102
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
103
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
104
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
105
- tl.store(c_ptrs, c, mask=c_mask)
106
-
107
-
108
- @triton.autotune(
109
- configs=[
110
- triton.Config({}, num_warps=w, num_stages=s)
111
- for w in [2, 4, 8, 16]
112
- for s in [2, 3, 4]
113
- ],
114
- key=["N", "K", "BLOCK_SIZE_M"],
115
- )
116
- @triton.jit
117
- def w8a8_tensor_fp8_matmul_kernel(
118
- A,
119
- B,
120
- C,
121
- As,
122
- Bs,
123
- M,
124
- N,
125
- K,
126
- stride_am,
127
- stride_ak,
128
- stride_bk,
129
- stride_bn,
130
- stride_cm,
131
- stride_cn,
132
- stride_as_m,
133
- BLOCK_SIZE_M: tl.constexpr,
134
- BLOCK_SIZE_N: tl.constexpr,
135
- BLOCK_SIZE_K: tl.constexpr,
136
- GROUP_SIZE_M: tl.constexpr,
137
- ):
138
- """Tensor-scale FP8 GEMM kernel.
139
-
140
- Computes ``C = A @ B.T`` with one activation scale per row and one
141
- weight scale for the full matrix.
142
- Uses a 2D grid with swizzle for L2 cache locality on B tiles.
143
- """
144
- pid_m = tl.program_id(axis=0)
145
- pid_n = tl.program_id(axis=1)
146
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
147
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
148
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
149
-
150
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
151
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
152
- offs_k = tl.arange(0, BLOCK_SIZE_K)
153
-
154
- a_ptrs = A + offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak
155
- b_ptrs = B + offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn
156
-
157
- a_s = tl.load(As + offs_am * stride_as_m)
158
- b_s = tl.load(Bs)
159
-
160
- # Accumulate raw dot products, apply scales once after the loop.
161
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
162
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
163
- k_remaining = K - k * BLOCK_SIZE_K
164
- a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0)
165
- b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
166
- accumulator += tl.dot(a, b)
167
- a_ptrs += BLOCK_SIZE_K * stride_ak
168
- b_ptrs += BLOCK_SIZE_K * stride_bk
169
-
170
- accumulator = accumulator * a_s[:, None] * b_s
171
-
172
- if C.dtype.element_ty == tl.bfloat16:
173
- c = accumulator.to(tl.bfloat16)
174
- elif C.dtype.element_ty == tl.float16:
175
- c = accumulator.to(tl.float16)
176
- else:
177
- c = accumulator.to(tl.float32)
178
-
179
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
180
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
181
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
182
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
183
- tl.store(c_ptrs, c, mask=c_mask)
184
-
185
-
186
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul", mutates_args=())
187
- def _w8a8_block_fp8_matmul(
188
- A: torch.Tensor,
189
- B: torch.Tensor,
190
- As: torch.Tensor,
191
- Bs: torch.Tensor,
192
- block_size: list[int],
193
- output_dtype: torch.dtype = torch.float32,
194
- ) -> torch.Tensor:
195
- """Block-scale FP8 matmul: C = A @ B.T with per-block scales.
196
-
197
- As: (M, K // block_k) — per-token-group activation scales
198
- Bs: (N // block_n, K // block_k) — per-block weight scales
199
- """
200
- assert len(block_size) == 2, (
201
- f"block_size must be [block_n, block_k], got {block_size}"
202
- )
203
- block_n, block_k = block_size[0], block_size[1]
204
-
205
- assert A.shape[-1] == B.shape[-1], (
206
- f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
207
- )
208
- assert A.is_contiguous(), "A must be contiguous"
209
- assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
210
- assert B.is_contiguous(), "B must be contiguous"
211
-
212
- N, K = B.shape
213
- M = A.numel() // A.shape[-1]
214
-
215
- assert As.ndim >= 2, f"As must be at least 2D, got ndim={As.ndim}"
216
- assert As.shape[-1] == triton.cdiv(K, block_k), (
217
- f"As last dim {As.shape[-1]} != expected {triton.cdiv(K, block_k)} (cdiv(K={K}, block_k={block_k}))"
218
- )
219
- assert Bs.ndim == 2, f"Bs must be 2D (N//block_n, K//block_k), got ndim={Bs.ndim}"
220
- assert Bs.shape == (triton.cdiv(N, block_n), triton.cdiv(K, block_k)), (
221
- f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(N, block_n)}, {triton.cdiv(K, block_k)})"
222
- )
223
-
224
- BLOCK_SIZE_K = block_k
225
- BLOCK_SIZE_N = block_n
226
- C_shape = A.shape[:-1] + (N,)
227
- C = A.new_empty(C_shape, dtype=output_dtype)
228
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
229
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
230
- # register pressure and a better-matched FP8 WGMMA instruction, improving
231
- # both accuracy and performance for small M (decode).
232
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
233
- grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
234
- with device_context(A.device):
235
- wrap_triton(w8a8_block_fp8_matmul_kernel)[grid](
236
- A,
237
- B,
238
- C,
239
- As,
240
- Bs,
241
- M,
242
- N,
243
- K,
244
- A.stride(-2),
245
- A.stride(-1),
246
- B.stride(1),
247
- B.stride(0),
248
- C.stride(-2),
249
- C.stride(-1),
250
- As.stride(-2),
251
- As.stride(-1),
252
- Bs.stride(1),
253
- Bs.stride(0),
254
- # Meta-parameters
255
- BLOCK_SIZE_M=BLOCK_SIZE_M,
256
- BLOCK_SIZE_N=BLOCK_SIZE_N,
257
- BLOCK_SIZE_K=BLOCK_SIZE_K,
258
- GROUP_SIZE_M=8,
259
- )
260
-
261
- return C
262
-
263
-
264
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul", mutates_args=())
265
- def _w8a8_tensor_fp8_matmul(
266
- A: torch.Tensor,
267
- B: torch.Tensor,
268
- As: torch.Tensor,
269
- Bs: torch.Tensor,
270
- output_dtype: torch.dtype = torch.float32,
271
- ) -> torch.Tensor:
272
- """Tensor-scale FP8 matmul: C = A @ B.T with per-row / per-tensor scales.
273
-
274
- As: scalar, (M,), or (M, 1) — per-row activation scales
275
- Bs: scalar, (1,), or (1, 1) — single weight scale
276
- """
277
- assert A.shape[-1] == B.shape[-1], (
278
- f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
279
- )
280
- assert A.is_contiguous(), "A must be contiguous"
281
- assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
282
- assert B.is_contiguous(), "B must be contiguous"
283
-
284
- N, K = B.shape
285
- M = A.numel() // A.shape[-1]
286
-
287
- # Normalize As to (M,)
288
- if As.numel() == 1:
289
- As = As.reshape(1).expand(M).contiguous()
290
- elif As.ndim == 2:
291
- As = As.reshape(M)
292
- assert As.ndim == 1 and As.shape[0] == M, (
293
- f"As must be scalar, (M,), or (M,1) with M={M}, got {tuple(As.shape)}"
294
- )
295
-
296
- # Normalize Bs to (1,)
297
- assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
298
- Bs = Bs.reshape(1)
299
-
300
- BLOCK_SIZE_N = 128
301
- BLOCK_SIZE_K = 128
302
- C_shape = A.shape[:-1] + (N,)
303
- C = A.new_empty(C_shape, dtype=output_dtype)
304
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
305
- grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
306
- with device_context(A.device):
307
- wrap_triton(w8a8_tensor_fp8_matmul_kernel)[grid](
308
- A,
309
- B,
310
- C,
311
- As,
312
- Bs,
313
- M,
314
- N,
315
- K,
316
- A.stride(-2),
317
- A.stride(-1),
318
- B.stride(1),
319
- B.stride(0),
320
- C.stride(-2),
321
- C.stride(-1),
322
- As.stride(0),
323
- # Meta-parameters
324
- BLOCK_SIZE_M=BLOCK_SIZE_M,
325
- BLOCK_SIZE_N=BLOCK_SIZE_N,
326
- BLOCK_SIZE_K=BLOCK_SIZE_K,
327
- GROUP_SIZE_M=8,
328
- )
329
-
330
- return C
331
-
332
-
333
- def w8a8_block_fp8_matmul(
334
- A: torch.Tensor,
335
- B: torch.Tensor,
336
- As: torch.Tensor,
337
- Bs: torch.Tensor,
338
- block_size: list[int],
339
- output_dtype: torch.dtype = torch.float32,
340
- ) -> torch.Tensor:
341
- """Block-wise W8A8 FP8 matrix multiplication.
342
-
343
- Computes ``C = A @ B.T`` where both operands are pre-quantized to
344
- ``float8_e4m3fn`` with per-block scales, and accumulates in float32
345
- before casting to ``output_dtype``.
346
-
347
- Args:
348
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
349
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
350
- As: Per-token-group activation scales ``[M, K // block_size[1]]``.
351
- Bs: Per-block weight scales ``[N // block_size[0], K // block_size[1]]``.
352
- block_size: ``[block_n, block_k]`` quantization block dimensions, e.g. ``[128, 128]``.
353
- output_dtype: dtype of the returned tensor (default: ``torch.float32``).
354
-
355
- Returns:
356
- Output tensor ``[M, N]`` in ``output_dtype``.
357
- """
358
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul(
359
- A, B, As, Bs, block_size, output_dtype
360
- )
361
-
362
-
363
- def w8a8_tensor_fp8_matmul(
364
- A: torch.Tensor,
365
- B: torch.Tensor,
366
- As: torch.Tensor,
367
- Bs: torch.Tensor,
368
- output_dtype: torch.dtype = torch.float32,
369
- ) -> torch.Tensor:
370
- """Tensor-scale W8A8 FP8 matrix multiplication.
371
-
372
- Computes ``C = A @ B.T`` in tensor-scale mode using pre-quantized FP8
373
- activations/weights and tensor scales.
374
-
375
- Args:
376
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
377
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
378
- As: Per-row activation scales ``[M]``.
379
- Bs: Single weight scale, scalar or ``[1]``.
380
- output_dtype: dtype of the returned tensor.
381
-
382
- Returns:
383
- Output tensor ``[M, N]`` in ``output_dtype``.
384
- """
385
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
386
-
387
-
388
- def w8a8_fp8_matmul(
389
- A: torch.Tensor,
390
- B: torch.Tensor,
391
- As: torch.Tensor,
392
- Bs: torch.Tensor,
393
- block_size: list[int] | None,
394
- output_dtype: torch.dtype = torch.float32,
395
- ) -> torch.Tensor:
396
- """Unified W8A8 FP8 matmul dispatcher.
397
-
398
- Dispatch rules:
399
- - tensor mode when ``block_size is None``
400
- - tensor mode when ``block_size == [N, K]``
401
- - otherwise block mode
402
-
403
- Returns:
404
- Output tensor ``[M, N]`` in ``output_dtype``.
405
- """
406
- if block_size is None or (
407
- block_size[0] == B.size(0) and block_size[1] == B.size(1)
408
- ):
409
- return w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
410
-
411
- return w8a8_block_fp8_matmul(A, B, As, Bs, block_size, output_dtype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/metadata.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "name": "finegrained-fp8",
3
- "id": "_finegrained_fp8_rocm_bbc7e0f",
4
- "version": 1,
5
- "license": "Apache-2.0",
6
- "python-depends": [],
7
- "backend": {
8
- "type": "rocm"
9
- }
10
- }
 
 
 
 
 
 
 
 
 
 
 
build/torch-rocm/utils.py DELETED
@@ -1,13 +0,0 @@
1
- import torch
2
- from contextlib import contextmanager
3
-
4
-
5
- @contextmanager
6
- def device_context(device: torch.device):
7
- """Context manager that sets the active device for any backend (cuda, xpu, etc.)."""
8
- backend = getattr(torch, device.type, None)
9
- if backend is not None and hasattr(backend, "device"):
10
- with backend.device(device):
11
- yield
12
- else:
13
- yield
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/__init__.py DELETED
@@ -1,32 +0,0 @@
1
- from .act_quant import fp8_act_quant
2
- from .batched import (
3
- w8a8_fp8_matmul_batched,
4
- w8a8_block_fp8_matmul_batched,
5
- w8a8_tensor_fp8_matmul_batched,
6
- )
7
- from .grouped import (
8
- w8a8_fp8_matmul_grouped,
9
- w8a8_block_fp8_matmul_grouped,
10
- w8a8_tensor_fp8_matmul_grouped,
11
- )
12
- from .matmul import (
13
- w8a8_fp8_matmul,
14
- w8a8_block_fp8_matmul,
15
- w8a8_tensor_fp8_matmul,
16
- )
17
-
18
- __all__ = [
19
- "fp8_act_quant",
20
- # Single matmul
21
- "w8a8_fp8_matmul",
22
- "w8a8_block_fp8_matmul",
23
- "w8a8_tensor_fp8_matmul",
24
- # Batched matmul
25
- "w8a8_fp8_matmul_batched",
26
- "w8a8_block_fp8_matmul_batched",
27
- "w8a8_tensor_fp8_matmul_batched",
28
- # Grouped matmul
29
- "w8a8_fp8_matmul_grouped",
30
- "w8a8_block_fp8_matmul_grouped",
31
- "w8a8_tensor_fp8_matmul_grouped",
32
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/_ops.py DELETED
@@ -1,38 +0,0 @@
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_fp8"
25
- unique_id = "7c5619e"
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-xpu/act_quant.py DELETED
@@ -1,73 +0,0 @@
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 torch.library import triton_op, wrap_triton
19
-
20
- from .utils import device_context
21
-
22
-
23
- _FP8_DTYPE = torch.float8_e4m3fn
24
-
25
-
26
- # Copied from https://huggingface.co/deepseek-ai/DeepSeek-V3/blob/main/inference/kernel.py
27
- @triton.jit
28
- def _fp8_act_quant_kernel(x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr):
29
- pid = tl.program_id(axis=0)
30
- offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
31
- x = tl.load(x_ptr + offs).to(tl.float32)
32
- s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
33
- y = (x / s).to(y_ptr.dtype.element_ty)
34
- tl.store(y_ptr + offs, y)
35
- tl.store(s_ptr + pid, s)
36
-
37
-
38
- @triton_op("finegrained_fp8::fp8_act_quant", mutates_args=())
39
- def _fp8_act_quant(
40
- x: torch.Tensor, block_size: int = 128
41
- ) -> tuple[torch.Tensor, torch.Tensor]:
42
- assert x.is_contiguous()
43
- assert x.shape[-1] % block_size == 0
44
- y = torch.empty_like(x, dtype=_FP8_DTYPE)
45
- grid = (triton.cdiv(x.numel(), block_size),)
46
- s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
47
-
48
- with device_context(x.device):
49
- wrap_triton(_fp8_act_quant_kernel)[grid](x, y, s, BLOCK_SIZE=block_size)
50
-
51
- return y, s
52
-
53
-
54
- def fp8_act_quant(
55
- x: torch.Tensor, block_size: int = 128
56
- ) -> tuple[torch.Tensor, torch.Tensor]:
57
- """Quantize activations to FP8 with per-block dynamic scaling.
58
-
59
- Splits the last dimension of ``x`` into blocks of ``block_size`` elements,
60
- computes ``scale = max(|x_block|) / 448`` per block, and quantizes to
61
- ``float8_e4m3fn``.
62
-
63
- Args:
64
- x: Input tensor in bf16/fp16/fp32. Last dimension must be divisible by
65
- ``block_size`` and the tensor must be contiguous.
66
- block_size: Number of elements per quantization block (default: 128).
67
-
68
- Returns:
69
- A tuple ``(quantized, scales)`` where ``quantized`` has dtype
70
- ``float8_e4m3fn`` with the same shape as ``x``, and ``scales`` has
71
- shape ``(*x.shape[:-1], x.shape[-1] // block_size)`` in float32.
72
- """
73
- return torch.ops.finegrained_fp8.fp8_act_quant(x, block_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/batched.py DELETED
@@ -1,398 +0,0 @@
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 .act_quant import fp8_act_quant
19
- from torch.library import triton_op, wrap_triton
20
-
21
- from .utils import device_context
22
-
23
-
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
- key=["N", "K"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_batched_kernel(
34
- A, # (S, K) raw BF16/FP16 activations
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
- ExpertIds, # (S,) — which expert each batch element routes to
39
- # Shape
40
- S,
41
- N,
42
- K,
43
- stride_am,
44
- stride_ak,
45
- stride_be,
46
- stride_bk,
47
- stride_bn,
48
- stride_cm,
49
- stride_cn,
50
- stride_bs_e,
51
- stride_bs_k,
52
- stride_bs_n,
53
- # Meta-parameters
54
- BLOCK_SIZE_N: tl.constexpr,
55
- BLOCK_SIZE_K: tl.constexpr,
56
- BLOCK_SIZE_M: tl.constexpr,
57
- ):
58
- """Block-scale batched FP8 expert matmul kernel.
59
-
60
- Each program handles one routed token row and one N-tile, looks up the
61
- owning expert from ``ExpertIds``, and applies fused activation quantization.
62
- """
63
- batch_id = tl.program_id(axis=0)
64
- pid_n = tl.program_id(axis=1)
65
-
66
- # Cast expert_id to int64 to prevent int32 overflow when computing
67
- # expert_id * stride_Eb (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
68
- # 3072×3072 FP8 weights).
69
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
70
-
71
- A = A + batch_id * stride_am
72
- B = B + expert_id * stride_be
73
- C = C + batch_id * stride_cm
74
- Bs = Bs + expert_id * stride_bs_e
75
-
76
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
77
- offs_k = tl.arange(0, BLOCK_SIZE_K)
78
- a_ptrs = A + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0 + offs_k[None, :] * stride_ak
79
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
80
-
81
- bs_ptrs = Bs + pid_n * stride_bs_n
82
-
83
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
84
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
85
- # ---- fused fp8_act_quant ----
86
- a_raw = tl.load(a_ptrs).to(tl.float32)
87
- a_s = tl.max(tl.abs(a_raw)) / 448.0
88
- a = (a_raw / tl.maximum(a_s, 1e-12)).to(tl.float8e4nv)
89
- # ---- matmul ----
90
- b = tl.load(b_ptrs)
91
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
92
- accumulator += tl.dot(a, b) * a_s * b_s[None, :]
93
- a_ptrs += BLOCK_SIZE_K * stride_ak
94
- b_ptrs += BLOCK_SIZE_K * stride_bk
95
-
96
- if C.dtype.element_ty == tl.bfloat16:
97
- c = accumulator.to(tl.bfloat16)
98
- elif C.dtype.element_ty == tl.float16:
99
- c = accumulator.to(tl.float16)
100
- else:
101
- c = accumulator.to(tl.float32)
102
-
103
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
104
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
105
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
106
- # Fake-batch trick aliases all BLOCK_SIZE_M lanes to the same C row, so emitting
107
- # `tl.store(c_ptrs, c)` issues BLOCK_SIZE_M duplicate-address stores. On NVIDIA
108
- # WGMMA this is usually benign (last-write-wins of identical bytes), but on Intel
109
- # XPU the duplicate-address store has hardware-undefined behavior and corrupts the
110
- # output. Mask so only lane 0 stores — the (M, N) accumulator rows are
111
- # mathematically identical (same A row × same B), so lane 0 holds the right value.
112
- tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
113
-
114
-
115
- @triton.autotune(
116
- configs=[
117
- triton.Config({}, num_warps=w, num_stages=s)
118
- for w in [2, 4, 8, 16]
119
- for s in [2, 3, 4, 5]
120
- ],
121
- key=["N", "K"],
122
- )
123
- @triton.jit
124
- def w8a8_tensor_fp8_matmul_batched_kernel(
125
- A, # (S, K) pre-quantized FP8 activations
126
- B, # (E, N, K) FP8 weight matrices
127
- C, # (S, N) output
128
- As, # (S, 1) per-tensor activation scales
129
- Bs, # (E, 1, 1) per-tensor weight scales
130
- ExpertIds,
131
- S,
132
- N,
133
- K,
134
- stride_am,
135
- stride_ak,
136
- stride_be,
137
- stride_bk,
138
- stride_bn,
139
- stride_cm,
140
- stride_cn,
141
- stride_as_m,
142
- stride_bs_e,
143
- BLOCK_SIZE_N: tl.constexpr,
144
- BLOCK_SIZE_K: tl.constexpr,
145
- BLOCK_SIZE_M: tl.constexpr,
146
- ):
147
- """Tensor-scale batched FP8 expert matmul kernel.
148
-
149
- Activations are already quantized; the kernel applies per-token activation
150
- scales and per-expert tensor weight scales.
151
- """
152
- batch_id = tl.program_id(axis=0)
153
- pid_n = tl.program_id(axis=1)
154
-
155
- expert_id = tl.load(ExpertIds + batch_id).to(tl.int64)
156
-
157
- A = A + batch_id * stride_am
158
- B = B + expert_id * stride_be
159
- C = C + batch_id * stride_cm
160
- Bs = Bs + expert_id * stride_bs_e
161
-
162
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
163
- offs_k = tl.arange(0, BLOCK_SIZE_K)
164
- a_ptrs = A + tl.arange(0, BLOCK_SIZE_M)[:, None] * 0 + offs_k[None, :] * stride_ak
165
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
166
-
167
- b_s = tl.load(Bs)
168
- a_s = tl.load(As + batch_id * stride_as_m)
169
-
170
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
171
- for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
172
- a = tl.load(a_ptrs)
173
- b = tl.load(b_ptrs)
174
- accumulator += tl.dot(a, b)
175
- a_ptrs += BLOCK_SIZE_K * stride_ak
176
- b_ptrs += BLOCK_SIZE_K * stride_bk
177
-
178
- accumulator = accumulator * a_s * b_s
179
-
180
- if C.dtype.element_ty == tl.bfloat16:
181
- c = accumulator.to(tl.bfloat16)
182
- elif C.dtype.element_ty == tl.float16:
183
- c = accumulator.to(tl.float16)
184
- else:
185
- c = accumulator.to(tl.float32)
186
-
187
- offs_cm = tl.arange(0, BLOCK_SIZE_M)
188
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
189
- c_ptrs = C + offs_cm[:, None] * 0 + stride_cn * offs_cn[None, :]
190
- # See block-FP8 kernel above: BLOCK_SIZE_M lanes alias the same C row;
191
- # mask so only lane 0 stores to avoid hardware-undefined duplicate writes on XPU.
192
- tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
193
-
194
-
195
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_batched", mutates_args=())
196
- def _w8a8_block_fp8_matmul_batched(
197
- A: torch.Tensor,
198
- B: torch.Tensor,
199
- Bs: torch.Tensor,
200
- expert_ids: torch.Tensor,
201
- block_size: list[int],
202
- ) -> torch.Tensor:
203
- """Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
204
-
205
- A: (S, K) raw bf16/fp16 activations
206
- B: (E, N, K) FP8 expert weights
207
- Bs: (E, N // block_n, K // block_k) per-block weight scales
208
- """
209
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
210
- assert A.is_contiguous(), "A must be contiguous"
211
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
212
- assert B.is_contiguous(), "B must be contiguous"
213
- assert A.shape[1] == B.shape[2], (
214
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
215
- )
216
-
217
- S, K = A.shape
218
- E, N, _ = B.shape
219
-
220
- assert len(block_size) == 2, (
221
- f"block_size must be [block_n, block_k], got {block_size}"
222
- )
223
- block_n, block_k = block_size[0], block_size[1]
224
- # MoE expert dimensions must be block-aligned; non-aligned N/K is not supported.
225
- assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
226
- assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
227
- assert Bs.ndim == 3, (
228
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
229
- )
230
- assert Bs.shape == (E, N // block_n, K // block_k), (
231
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
232
- )
233
-
234
- C = A.new_empty(S, N)
235
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
236
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
237
- # register pressure and a better-matched FP8 WGMMA instruction, improving
238
- # both accuracy and performance for small M (decode).
239
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
240
- grid = (S, triton.cdiv(N, block_n))
241
- with device_context(A.device):
242
- wrap_triton(w8a8_block_fp8_matmul_batched_kernel)[grid](
243
- A,
244
- B,
245
- C,
246
- Bs,
247
- expert_ids,
248
- S,
249
- N,
250
- K,
251
- A.stride(0),
252
- A.stride(1),
253
- B.stride(0),
254
- B.stride(2),
255
- B.stride(1),
256
- C.stride(0),
257
- C.stride(1),
258
- Bs.stride(0),
259
- Bs.stride(2),
260
- Bs.stride(1),
261
- BLOCK_SIZE_N=block_n,
262
- BLOCK_SIZE_K=block_k,
263
- BLOCK_SIZE_M=BLOCK_SIZE_M,
264
- )
265
-
266
- return C
267
-
268
-
269
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_batched", mutates_args=())
270
- def _w8a8_tensor_fp8_matmul_batched(
271
- A: torch.Tensor,
272
- B: torch.Tensor,
273
- Bs: torch.Tensor,
274
- expert_ids: torch.Tensor,
275
- ) -> torch.Tensor:
276
- """Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T, with fused act quant.
277
-
278
- A: (S, K) raw bf16/fp16 activations
279
- B: (E, N, K) FP8 expert weights
280
- Bs: (E,) or (E, 1, 1) per-expert weight scales
281
- """
282
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
283
- assert A.is_contiguous(), "A must be contiguous"
284
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
285
- assert B.is_contiguous(), "B must be contiguous"
286
- assert A.shape[1] == B.shape[2], (
287
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
288
- )
289
-
290
- S, K = A.shape
291
- E, N, _ = B.shape
292
-
293
- # Normalize Bs to (E, 1, 1)
294
- if Bs.ndim == 1:
295
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
296
- Bs = Bs.reshape(E, 1, 1)
297
- else:
298
- assert Bs.shape == (E, 1, 1), (
299
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
300
- )
301
-
302
- BLOCK_SIZE_N = 128
303
- BLOCK_SIZE_K = 128
304
- C = A.new_empty(S, N)
305
- qA, As = fp8_act_quant(A, K)
306
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
307
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
308
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
309
- # register pressure and a better-matched FP8 WGMMA instruction, improving
310
- # both accuracy and performance for small M (decode).
311
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
312
- grid = (S, triton.cdiv(N, BLOCK_SIZE_N))
313
- with device_context(A.device):
314
- wrap_triton(w8a8_tensor_fp8_matmul_batched_kernel)[grid](
315
- qA,
316
- B,
317
- C,
318
- As,
319
- Bs,
320
- expert_ids,
321
- S,
322
- N,
323
- K,
324
- qA.stride(0),
325
- qA.stride(1),
326
- B.stride(0),
327
- B.stride(2),
328
- B.stride(1),
329
- C.stride(0),
330
- C.stride(1),
331
- As.stride(0),
332
- Bs.stride(0),
333
- BLOCK_SIZE_N=BLOCK_SIZE_N,
334
- BLOCK_SIZE_K=BLOCK_SIZE_K,
335
- BLOCK_SIZE_M=BLOCK_SIZE_M,
336
- )
337
-
338
- return C
339
-
340
-
341
- def w8a8_block_fp8_matmul_batched(
342
- A: torch.Tensor,
343
- B: torch.Tensor,
344
- Bs: torch.Tensor,
345
- expert_ids: torch.Tensor,
346
- block_size: list[int],
347
- ) -> torch.Tensor:
348
- """Block-scale batched FP8 matmul with fused activation quantization.
349
-
350
- A: (S, K) raw activations, bf16/fp16/fp32
351
- B: (E, N, K) FP8 expert weights
352
- Bs: (E, N // block_n, K // block_k) per-block weight scales
353
- """
354
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_batched(
355
- A, B, Bs, expert_ids, block_size
356
- )
357
-
358
-
359
- def w8a8_tensor_fp8_matmul_batched(
360
- A: torch.Tensor,
361
- B: torch.Tensor,
362
- Bs: torch.Tensor,
363
- expert_ids: torch.Tensor,
364
- ) -> torch.Tensor:
365
- """Tensor-scale batched FP8 matmul with fused activation quantization.
366
-
367
- A: (S, K) raw activations, bf16/fp16/fp32
368
- B: (E, N, K) FP8 expert weights
369
- Bs: (E,) or (E, 1, 1) per-expert weight scales
370
- """
371
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_batched(
372
- A, B, Bs, expert_ids
373
- )
374
-
375
-
376
- def w8a8_fp8_matmul_batched(
377
- A: torch.Tensor,
378
- B: torch.Tensor,
379
- Bs: torch.Tensor,
380
- expert_ids: torch.Tensor,
381
- block_size: list[int] | None,
382
- ) -> torch.Tensor:
383
- """Unified batched W8A8 FP8 matmul dispatcher.
384
-
385
- Dispatch rules:
386
- - tensor mode when ``block_size is None``
387
- - tensor mode when ``block_size == [N, K]``
388
- - otherwise block mode
389
-
390
- Returns:
391
- Output tensor ``[S, N]`` in the same dtype as ``A``.
392
- """
393
- if block_size is None or (
394
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
395
- ):
396
- return w8a8_tensor_fp8_matmul_batched(A, B, Bs, expert_ids)
397
-
398
- return w8a8_block_fp8_matmul_batched(A, B, Bs, expert_ids, block_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/finegrained_fp8/__init__.py DELETED
@@ -1,26 +0,0 @@
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-xpu/grouped.py DELETED
@@ -1,477 +0,0 @@
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 .act_quant import fp8_act_quant
19
- from torch.library import triton_op, wrap_triton
20
-
21
- from .utils import device_context
22
-
23
-
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4, 5]
29
- ],
30
- key=["N", "K", "BLOCK_SIZE_M"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_grouped_kernel(
34
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert id
35
- B, # (E, N, K) FP8 weight matrices
36
- C, # (S, N) output
37
- Bs, # (E, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales
38
- Offsets, # (E,) int32 — cumulative row-end per expert
39
- TileOffsets, # (E,) int32 — cumulative tile-end per expert
40
- # Shape
41
- S,
42
- N,
43
- K,
44
- # Strides
45
- stride_am,
46
- stride_ak,
47
- stride_be,
48
- stride_bk,
49
- stride_bn,
50
- stride_cm,
51
- stride_cn,
52
- stride_bs_e,
53
- stride_bs_k,
54
- stride_bs_n,
55
- # Meta-parameters
56
- NUM_EXPERTS: tl.constexpr,
57
- BLOCK_SIZE_N: tl.constexpr,
58
- BLOCK_SIZE_K: tl.constexpr,
59
- BLOCK_SIZE_M: tl.constexpr,
60
- NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
61
- ):
62
- """Block-scale grouped FP8 expert matmul kernel.
63
-
64
- Tokens are assumed sorted by expert. The kernel maps each M-tile to its
65
- owning expert via ``TileOffsets`` and applies fused activation quantization.
66
- """
67
- pid_m = tl.program_id(axis=0)
68
- pid_n = tl.program_id(axis=1)
69
-
70
- # Exit early for programs beyond the actual tile count.
71
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
72
- if pid_m >= total_tiles:
73
- return
74
-
75
- # Binary search in TileOffsets to find the owning expert.
76
- # Finds the smallest e such that TileOffsets[e] > pid_m (upper_bound semantics),
77
- # which is the expert whose tile range contains pid_m.
78
- # O(log2(NUM_EXPERTS)) loads instead of the O(NUM_EXPERTS) linear scan.
79
- # NUM_EXPERTS_BIT_LENGTH is ceil(log2(E))+1 for powers-of-two, giving one
80
- # harmless extra iteration when lo==hi; it's a compile-time constant so the
81
- # loop is fully unrolled by the compiler.
82
- lo = 0
83
- hi = NUM_EXPERTS
84
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
85
- mid = (lo + hi) >> 1
86
- mid_val = tl.load(TileOffsets + mid)
87
- is_left = mid_val <= pid_m
88
- lo = tl.where(is_left, mid + 1, lo)
89
- hi = tl.where(is_left, hi, mid)
90
-
91
- # Cast expert_id to int64 to prevent int32 overflow when computing
92
- # expert_id * stride_be (e.g. 255 * 9_437_184 > 2^31 for 256 experts of
93
- # 3072×3072 FP8 weights).
94
- expert_id = lo.to(tl.int64)
95
-
96
- prev_eid = tl.maximum(expert_id - 1, 0)
97
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
98
- expert_end = tl.load(Offsets + expert_id)
99
- M_expert = expert_end - expert_start
100
-
101
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
102
- local_tile = pid_m - expert_tile_start
103
- m_off = local_tile * BLOCK_SIZE_M
104
-
105
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
106
- row_mask = offs_am < M_expert
107
- offs_global_m = expert_start + offs_am
108
-
109
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
110
- offs_k = tl.arange(0, BLOCK_SIZE_K)
111
-
112
- a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
113
- b_ptrs = (
114
- B
115
- + expert_id * stride_be
116
- + offs_k[:, None] * stride_bk
117
- + offs_bn[None, :] * stride_bn
118
- )
119
- bs_ptrs = Bs + expert_id * stride_bs_e + pid_n * stride_bs_n
120
-
121
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
122
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
123
- # ---- fused fp8_act_quant ----
124
- a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
125
- a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
126
- a = (a_raw / tl.maximum(a_s[:, None], 1e-12)).to(tl.float8e4nv)
127
- # ---- matmul ----
128
- b = tl.load(b_ptrs)
129
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
130
- accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
131
- a_ptrs += BLOCK_SIZE_K * stride_ak
132
- b_ptrs += BLOCK_SIZE_K * stride_bk
133
-
134
- if C.dtype.element_ty == tl.bfloat16:
135
- c = accumulator.to(tl.bfloat16)
136
- elif C.dtype.element_ty == tl.float16:
137
- c = accumulator.to(tl.float16)
138
- else:
139
- c = accumulator.to(tl.float32)
140
-
141
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
142
- c_mask = row_mask[:, None]
143
- tl.store(c_ptrs, c, mask=c_mask)
144
-
145
-
146
- @triton.autotune(
147
- configs=[
148
- triton.Config({}, num_warps=w, num_stages=s)
149
- for w in [2, 4, 8, 16]
150
- for s in [2, 3, 4, 5]
151
- ],
152
- key=["N", "K", "BLOCK_SIZE_M"],
153
- )
154
- @triton.jit
155
- def w8a8_tensor_fp8_matmul_grouped_kernel(
156
- A, # (S, K) raw BF16/FP16 activations, sorted/grouped by expert idc
157
- B, # (E, N, K) FP8 weight matrices
158
- C, # (S, N) output
159
- As, # (S, 1) activation scales
160
- Bs, # (E, 1, 1) per-tensor weight scales
161
- Offsets,
162
- TileOffsets,
163
- S,
164
- N,
165
- K,
166
- stride_am,
167
- stride_ak,
168
- stride_be,
169
- stride_bk,
170
- stride_bn,
171
- stride_cm,
172
- stride_cn,
173
- stride_as_m,
174
- stride_bs_e,
175
- NUM_EXPERTS: tl.constexpr,
176
- BLOCK_SIZE_N: tl.constexpr,
177
- BLOCK_SIZE_K: tl.constexpr,
178
- BLOCK_SIZE_M: tl.constexpr,
179
- NUM_EXPERTS_BIT_LENGTH: tl.constexpr,
180
- ):
181
- """Tensor-scale grouped FP8 expert matmul kernel.
182
-
183
- Uses grouped expert scheduling with pre-quantized activations plus
184
- per-token activation scales and per-expert tensor weight scales.
185
- """
186
- pid_m = tl.program_id(axis=0)
187
- pid_n = tl.program_id(axis=1)
188
-
189
- total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
190
- if pid_m >= total_tiles:
191
- return
192
-
193
- lo = 0
194
- hi = NUM_EXPERTS
195
- for _ in tl.static_range(NUM_EXPERTS_BIT_LENGTH):
196
- mid = (lo + hi) >> 1
197
- mid_val = tl.load(TileOffsets + mid)
198
- is_left = mid_val <= pid_m
199
- lo = tl.where(is_left, mid + 1, lo)
200
- hi = tl.where(is_left, hi, mid)
201
- expert_id = lo.to(tl.int64)
202
-
203
- prev_eid = tl.maximum(expert_id - 1, 0)
204
- expert_start = tl.where(expert_id == 0, 0, tl.load(Offsets + prev_eid))
205
- expert_end = tl.load(Offsets + expert_id)
206
- M_expert = expert_end - expert_start
207
-
208
- expert_tile_start = tl.where(expert_id == 0, 0, tl.load(TileOffsets + prev_eid))
209
- local_tile = pid_m - expert_tile_start
210
- m_off = local_tile * BLOCK_SIZE_M
211
-
212
- offs_am = m_off + tl.arange(0, BLOCK_SIZE_M)
213
- row_mask = offs_am < M_expert
214
- offs_global_m = expert_start + offs_am
215
-
216
- offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
217
- offs_k = tl.arange(0, BLOCK_SIZE_K)
218
-
219
- a_ptrs = A + offs_global_m[:, None] * stride_am + offs_k[None, :] * stride_ak
220
- b_ptrs = (
221
- B
222
- + expert_id * stride_be
223
- + offs_k[:, None] * stride_bk
224
- + offs_bn[None, :] * stride_bn
225
- )
226
-
227
- a_s = tl.load(As + offs_global_m * stride_as_m, mask=row_mask, other=0.0)
228
- b_s = tl.load(Bs + expert_id * stride_bs_e)
229
-
230
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
231
- for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
232
- a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
233
- b = tl.load(b_ptrs)
234
-
235
- accumulator += tl.dot(a, b)
236
- a_ptrs += BLOCK_SIZE_K * stride_ak
237
- b_ptrs += BLOCK_SIZE_K * stride_bk
238
-
239
- accumulator = accumulator * a_s[:, None] * b_s
240
-
241
- if C.dtype.element_ty == tl.bfloat16:
242
- c = accumulator.to(tl.bfloat16)
243
- elif C.dtype.element_ty == tl.float16:
244
- c = accumulator.to(tl.float16)
245
- else:
246
- c = accumulator.to(tl.float32)
247
-
248
- c_ptrs = C + stride_cm * offs_global_m[:, None] + stride_cn * offs_bn[None, :]
249
- c_mask = row_mask[:, None]
250
- tl.store(c_ptrs, c, mask=c_mask)
251
-
252
-
253
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul_grouped", mutates_args=())
254
- def _w8a8_block_fp8_matmul_grouped(
255
- A: torch.Tensor,
256
- B: torch.Tensor,
257
- Bs: torch.Tensor,
258
- offsets: torch.Tensor,
259
- tokens_per_expert: torch.Tensor,
260
- block_size: list[int],
261
- ) -> torch.Tensor:
262
- """Block-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
263
-
264
- A: (S, K) raw bf16/fp16 activations, sorted by expert
265
- B: (E, N, K) FP8 expert weights
266
- Bs: (E, N // block_n, K // block_k) per-block weight scales
267
- """
268
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
269
- assert A.is_contiguous(), "A must be contiguous"
270
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
271
- assert B.is_contiguous(), "B must be contiguous"
272
- assert A.shape[1] == B.shape[2], (
273
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
274
- )
275
-
276
- S, K = A.shape
277
- E, N, _ = B.shape
278
-
279
- assert len(block_size) == 2, (
280
- f"block_size must be [block_n, block_k], got {block_size}"
281
- )
282
- block_n, block_k = block_size[0], block_size[1]
283
- # MoE expert dimensions must be block-aligned; non-aligned N/K is not supported.
284
- assert N % block_n == 0, f"N ({N}) must be divisible by block_n ({block_n})"
285
- assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
286
- assert Bs.ndim == 3, (
287
- f"Bs must be 3D (E, N//block_n, K//block_k), got ndim={Bs.ndim}"
288
- )
289
- assert Bs.shape == (E, N // block_n, K // block_k), (
290
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, {N // block_n}, {K // block_k})"
291
- )
292
-
293
- C = A.new_empty(S, N)
294
- # Adaptive BLOCK_SIZE_M: match tile to average tokens per expert.
295
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
296
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
297
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
298
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
299
- # Programs beyond the real tile count exit immediately via the early-return
300
- # guard inside the kernel. This is faster than syncing for the exact count
301
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
302
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
303
- grid = (max_M_tiles, triton.cdiv(N, block_n))
304
- with device_context(A.device):
305
- wrap_triton(w8a8_block_fp8_matmul_grouped_kernel)[grid](
306
- A,
307
- B,
308
- C,
309
- Bs,
310
- offsets,
311
- tile_offsets,
312
- S,
313
- N,
314
- K,
315
- A.stride(0),
316
- A.stride(1),
317
- B.stride(0),
318
- B.stride(2),
319
- B.stride(1),
320
- C.stride(0),
321
- C.stride(1),
322
- Bs.stride(0),
323
- Bs.stride(2),
324
- Bs.stride(1),
325
- # Meta-parameters
326
- NUM_EXPERTS=E,
327
- BLOCK_SIZE_N=block_n,
328
- BLOCK_SIZE_K=block_k,
329
- BLOCK_SIZE_M=BLOCK_SIZE_M,
330
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
331
- )
332
-
333
- return C
334
-
335
-
336
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul_grouped", mutates_args=())
337
- def _w8a8_tensor_fp8_matmul_grouped(
338
- A: torch.Tensor,
339
- B: torch.Tensor,
340
- Bs: torch.Tensor,
341
- offsets: torch.Tensor,
342
- tokens_per_expert: torch.Tensor,
343
- ) -> torch.Tensor:
344
- """Tensor-scale grouped FP8 matmul: C = A @ B.T per expert, with fused act quant.
345
-
346
- A: (S, K) raw bf16/fp16 activations, sorted by expert
347
- B: (E, N, K) FP8 expert weights
348
- Bs: (E,) or (E, 1, 1) per-expert weight scales
349
- """
350
- assert A.ndim == 2, f"A must be 2D (S, K), got ndim={A.ndim}"
351
- assert A.is_contiguous(), "A must be contiguous"
352
- assert B.ndim == 3, f"B must be 3D (E, N, K), got ndim={B.ndim}"
353
- assert B.is_contiguous(), "B must be contiguous"
354
- assert A.shape[1] == B.shape[2], (
355
- f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
356
- )
357
-
358
- S, K = A.shape
359
- E, N, _ = B.shape
360
-
361
- # Normalize Bs to (E, 1, 1)
362
- if Bs.ndim == 1:
363
- assert Bs.shape[0] == E, f"Bs shape {tuple(Bs.shape)} != expected ({E},)"
364
- Bs = Bs.reshape(E, 1, 1)
365
- else:
366
- assert Bs.shape == (E, 1, 1), (
367
- f"Bs shape {tuple(Bs.shape)} != expected ({E}, 1, 1)"
368
- )
369
-
370
- BLOCK_SIZE_N = 128
371
- BLOCK_SIZE_K = 128
372
- C = A.new_empty(S, N)
373
- qA, As = fp8_act_quant(A, K)
374
- BLOCK_SIZE_M = min(max(triton.next_power_of_2((S + E - 1) // E), 16), 128)
375
- tiles_per_expert = (tokens_per_expert + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
376
- tile_offsets = torch.cumsum(tiles_per_expert, dim=0).to(torch.int32)
377
- # Upper bound on M-tiles: sum_e ceil(M_e / BLOCK_M) <= ceil(S / BLOCK_M) + E.
378
- # Programs beyond the real tile count exit immediately via the early-return
379
- # guard inside the kernel. This is faster than syncing for the exact count
380
- # and keeps the grid size data-independent (cuda-graph / torch.compile safe).
381
- max_M_tiles = triton.cdiv(S, BLOCK_SIZE_M) + E
382
- grid = (max_M_tiles, triton.cdiv(N, BLOCK_SIZE_N))
383
- with device_context(A.device):
384
- wrap_triton(w8a8_tensor_fp8_matmul_grouped_kernel)[grid](
385
- qA,
386
- B,
387
- C,
388
- As,
389
- Bs,
390
- offsets,
391
- tile_offsets,
392
- S,
393
- N,
394
- K,
395
- qA.stride(0),
396
- qA.stride(1),
397
- B.stride(0),
398
- B.stride(2),
399
- B.stride(1),
400
- C.stride(0),
401
- C.stride(1),
402
- As.stride(0),
403
- Bs.stride(0),
404
- # Meta-parameters
405
- NUM_EXPERTS=E,
406
- BLOCK_SIZE_N=BLOCK_SIZE_N,
407
- BLOCK_SIZE_K=BLOCK_SIZE_K,
408
- BLOCK_SIZE_M=BLOCK_SIZE_M,
409
- NUM_EXPERTS_BIT_LENGTH=E.bit_length(),
410
- )
411
-
412
- return C
413
-
414
-
415
- def w8a8_block_fp8_matmul_grouped(
416
- A: torch.Tensor,
417
- B: torch.Tensor,
418
- Bs: torch.Tensor,
419
- offsets: torch.Tensor,
420
- tokens_per_expert: torch.Tensor,
421
- block_size: list[int],
422
- ) -> torch.Tensor:
423
- """Block-scale grouped FP8 matmul with fused activation quantization.
424
-
425
- A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
426
- B: (E, N, K) FP8 expert weights
427
- Bs: (E, N // block_n, K // block_k) per-block weight scales
428
- """
429
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul_grouped(
430
- A, B, Bs, offsets, tokens_per_expert, block_size
431
- )
432
-
433
-
434
- def w8a8_tensor_fp8_matmul_grouped(
435
- A: torch.Tensor,
436
- B: torch.Tensor,
437
- Bs: torch.Tensor,
438
- offsets: torch.Tensor,
439
- tokens_per_expert: torch.Tensor,
440
- ) -> torch.Tensor:
441
- """Tensor-scale grouped FP8 matmul with fused activation quantization.
442
-
443
- A: (S, K) raw activations sorted by expert, bf16/fp16/fp32
444
- B: (E, N, K) FP8 expert weights
445
- Bs: (E,) or (E, 1, 1) per-expert weight scales
446
- """
447
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul_grouped(
448
- A, B, Bs, offsets, tokens_per_expert
449
- )
450
-
451
-
452
- def w8a8_fp8_matmul_grouped(
453
- A: torch.Tensor,
454
- B: torch.Tensor,
455
- Bs: torch.Tensor,
456
- offsets: torch.Tensor,
457
- tokens_per_expert: torch.Tensor,
458
- block_size: list[int] | None,
459
- ) -> torch.Tensor:
460
- """Unified grouped W8A8 FP8 matmul dispatcher.
461
-
462
- Dispatch rules:
463
- - tensor mode when ``block_size is None``
464
- - tensor mode when ``block_size == [N, K]``
465
- - otherwise block mode
466
-
467
- Returns:
468
- Output tensor ``[S, N]`` in the same dtype as ``A``, in expert-sorted order.
469
- """
470
- if block_size is None or (
471
- block_size[0] == B.size(1) and block_size[1] == B.size(2)
472
- ):
473
- return w8a8_tensor_fp8_matmul_grouped(A, B, Bs, offsets, tokens_per_expert)
474
-
475
- return w8a8_block_fp8_matmul_grouped(
476
- A, B, Bs, offsets, tokens_per_expert, block_size
477
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/matmul.py DELETED
@@ -1,411 +0,0 @@
1
- # Copyright 2025 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 torch.library import triton_op, wrap_triton
19
-
20
- from .utils import device_context
21
-
22
-
23
- # Adapted from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/layers/quantization/fp8_kernel.py
24
- @triton.autotune(
25
- configs=[
26
- triton.Config({}, num_warps=w, num_stages=s)
27
- for w in [2, 4, 8, 16]
28
- for s in [2, 3, 4]
29
- ],
30
- key=["N", "K", "BLOCK_SIZE_M"],
31
- )
32
- @triton.jit
33
- def w8a8_block_fp8_matmul_kernel(
34
- # Pointers to inputs and output
35
- A,
36
- B,
37
- C,
38
- As,
39
- Bs,
40
- # Shape for matmul
41
- M,
42
- N,
43
- K,
44
- stride_am,
45
- stride_ak,
46
- stride_bk,
47
- stride_bn,
48
- stride_cm,
49
- stride_cn,
50
- stride_as_m,
51
- stride_as_k,
52
- stride_bs_k,
53
- stride_bs_n,
54
- # Meta-parameters
55
- BLOCK_SIZE_M: tl.constexpr,
56
- BLOCK_SIZE_N: tl.constexpr,
57
- BLOCK_SIZE_K: tl.constexpr,
58
- GROUP_SIZE_M: tl.constexpr,
59
- ):
60
- """Block-scale FP8 GEMM kernel.
61
-
62
- Computes ``C = A @ B.T`` with block-wise activation/weight scales.
63
- Uses a 2D grid with swizzle for L2 cache locality on B tiles.
64
- """
65
- pid_m = tl.program_id(axis=0)
66
- pid_n = tl.program_id(axis=1)
67
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
68
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
69
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
70
-
71
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
72
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
73
- offs_k = tl.arange(0, BLOCK_SIZE_K)
74
- a_ptrs = A + (offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak)
75
- b_ptrs = B + (offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn)
76
-
77
- as_ptrs = As + offs_am * stride_as_m
78
- offs_bsn = offs_bn // BLOCK_SIZE_N
79
- bs_ptrs = Bs + offs_bsn * stride_bs_n
80
-
81
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
82
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
83
- k_remaining = K - k * BLOCK_SIZE_K
84
- a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0)
85
- b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
86
-
87
- a_s = tl.load(as_ptrs + k * stride_as_k)
88
- b_s = tl.load(bs_ptrs + k * stride_bs_k)
89
-
90
- accumulator += tl.dot(a, b) * a_s[:, None] * b_s[None, :]
91
- a_ptrs += BLOCK_SIZE_K * stride_ak
92
- b_ptrs += BLOCK_SIZE_K * stride_bk
93
-
94
- if C.dtype.element_ty == tl.bfloat16:
95
- c = accumulator.to(tl.bfloat16)
96
- elif C.dtype.element_ty == tl.float16:
97
- c = accumulator.to(tl.float16)
98
- else:
99
- c = accumulator.to(tl.float32)
100
-
101
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
102
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
103
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
104
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
105
- tl.store(c_ptrs, c, mask=c_mask)
106
-
107
-
108
- @triton.autotune(
109
- configs=[
110
- triton.Config({}, num_warps=w, num_stages=s)
111
- for w in [2, 4, 8, 16]
112
- for s in [2, 3, 4]
113
- ],
114
- key=["N", "K", "BLOCK_SIZE_M"],
115
- )
116
- @triton.jit
117
- def w8a8_tensor_fp8_matmul_kernel(
118
- A,
119
- B,
120
- C,
121
- As,
122
- Bs,
123
- M,
124
- N,
125
- K,
126
- stride_am,
127
- stride_ak,
128
- stride_bk,
129
- stride_bn,
130
- stride_cm,
131
- stride_cn,
132
- stride_as_m,
133
- BLOCK_SIZE_M: tl.constexpr,
134
- BLOCK_SIZE_N: tl.constexpr,
135
- BLOCK_SIZE_K: tl.constexpr,
136
- GROUP_SIZE_M: tl.constexpr,
137
- ):
138
- """Tensor-scale FP8 GEMM kernel.
139
-
140
- Computes ``C = A @ B.T`` with one activation scale per row and one
141
- weight scale for the full matrix.
142
- Uses a 2D grid with swizzle for L2 cache locality on B tiles.
143
- """
144
- pid_m = tl.program_id(axis=0)
145
- pid_n = tl.program_id(axis=1)
146
- num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
147
- num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
148
- pid_m, pid_n = tl.swizzle2d(pid_m, pid_n, num_pid_m, num_pid_n, GROUP_SIZE_M)
149
-
150
- offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
151
- offs_bn = (pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)) % N
152
- offs_k = tl.arange(0, BLOCK_SIZE_K)
153
-
154
- a_ptrs = A + offs_am[:, None] * stride_am + offs_k[None, :] * stride_ak
155
- b_ptrs = B + offs_k[:, None] * stride_bk + offs_bn[None, :] * stride_bn
156
-
157
- a_s = tl.load(As + offs_am * stride_as_m)
158
- b_s = tl.load(Bs)
159
-
160
- # Accumulate raw dot products, apply scales once after the loop.
161
- accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
162
- for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
163
- k_remaining = K - k * BLOCK_SIZE_K
164
- a = tl.load(a_ptrs, mask=offs_k[None, :] < k_remaining, other=0.0)
165
- b = tl.load(b_ptrs, mask=offs_k[:, None] < k_remaining, other=0.0)
166
- accumulator += tl.dot(a, b)
167
- a_ptrs += BLOCK_SIZE_K * stride_ak
168
- b_ptrs += BLOCK_SIZE_K * stride_bk
169
-
170
- accumulator = accumulator * a_s[:, None] * b_s
171
-
172
- if C.dtype.element_ty == tl.bfloat16:
173
- c = accumulator.to(tl.bfloat16)
174
- elif C.dtype.element_ty == tl.float16:
175
- c = accumulator.to(tl.float16)
176
- else:
177
- c = accumulator.to(tl.float32)
178
-
179
- offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
180
- offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
181
- c_ptrs = C + stride_cm * offs_cm[:, None] + stride_cn * offs_cn[None, :]
182
- c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
183
- tl.store(c_ptrs, c, mask=c_mask)
184
-
185
-
186
- @triton_op("finegrained_fp8::w8a8_block_fp8_matmul", mutates_args=())
187
- def _w8a8_block_fp8_matmul(
188
- A: torch.Tensor,
189
- B: torch.Tensor,
190
- As: torch.Tensor,
191
- Bs: torch.Tensor,
192
- block_size: list[int],
193
- output_dtype: torch.dtype = torch.float32,
194
- ) -> torch.Tensor:
195
- """Block-scale FP8 matmul: C = A @ B.T with per-block scales.
196
-
197
- As: (M, K // block_k) — per-token-group activation scales
198
- Bs: (N // block_n, K // block_k) — per-block weight scales
199
- """
200
- assert len(block_size) == 2, (
201
- f"block_size must be [block_n, block_k], got {block_size}"
202
- )
203
- block_n, block_k = block_size[0], block_size[1]
204
-
205
- assert A.shape[-1] == B.shape[-1], (
206
- f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
207
- )
208
- assert A.is_contiguous(), "A must be contiguous"
209
- assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
210
- assert B.is_contiguous(), "B must be contiguous"
211
-
212
- N, K = B.shape
213
- M = A.numel() // A.shape[-1]
214
-
215
- assert As.ndim >= 2, f"As must be at least 2D, got ndim={As.ndim}"
216
- assert As.shape[-1] == triton.cdiv(K, block_k), (
217
- f"As last dim {As.shape[-1]} != expected {triton.cdiv(K, block_k)} (cdiv(K={K}, block_k={block_k}))"
218
- )
219
- assert Bs.ndim == 2, f"Bs must be 2D (N//block_n, K//block_k), got ndim={Bs.ndim}"
220
- assert Bs.shape == (triton.cdiv(N, block_n), triton.cdiv(K, block_k)), (
221
- f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(N, block_n)}, {triton.cdiv(K, block_k)})"
222
- )
223
-
224
- BLOCK_SIZE_K = block_k
225
- BLOCK_SIZE_N = block_n
226
- C_shape = A.shape[:-1] + (N,)
227
- C = A.new_empty(C_shape, dtype=output_dtype)
228
- # Adaptive BLOCK_SIZE_M: smallest power-of-2 >= M, floored at 16, capped at 128.
229
- # Matches the WGMMA tile to the actual row count — smaller tiles use less
230
- # register pressure and a better-matched FP8 WGMMA instruction, improving
231
- # both accuracy and performance for small M (decode).
232
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
233
- grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
234
- with device_context(A.device):
235
- wrap_triton(w8a8_block_fp8_matmul_kernel)[grid](
236
- A,
237
- B,
238
- C,
239
- As,
240
- Bs,
241
- M,
242
- N,
243
- K,
244
- A.stride(-2),
245
- A.stride(-1),
246
- B.stride(1),
247
- B.stride(0),
248
- C.stride(-2),
249
- C.stride(-1),
250
- As.stride(-2),
251
- As.stride(-1),
252
- Bs.stride(1),
253
- Bs.stride(0),
254
- # Meta-parameters
255
- BLOCK_SIZE_M=BLOCK_SIZE_M,
256
- BLOCK_SIZE_N=BLOCK_SIZE_N,
257
- BLOCK_SIZE_K=BLOCK_SIZE_K,
258
- GROUP_SIZE_M=8,
259
- )
260
-
261
- return C
262
-
263
-
264
- @triton_op("finegrained_fp8::w8a8_tensor_fp8_matmul", mutates_args=())
265
- def _w8a8_tensor_fp8_matmul(
266
- A: torch.Tensor,
267
- B: torch.Tensor,
268
- As: torch.Tensor,
269
- Bs: torch.Tensor,
270
- output_dtype: torch.dtype = torch.float32,
271
- ) -> torch.Tensor:
272
- """Tensor-scale FP8 matmul: C = A @ B.T with per-row / per-tensor scales.
273
-
274
- As: scalar, (M,), or (M, 1) — per-row activation scales
275
- Bs: scalar, (1,), or (1, 1) — single weight scale
276
- """
277
- assert A.shape[-1] == B.shape[-1], (
278
- f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
279
- )
280
- assert A.is_contiguous(), "A must be contiguous"
281
- assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
282
- assert B.is_contiguous(), "B must be contiguous"
283
-
284
- N, K = B.shape
285
- M = A.numel() // A.shape[-1]
286
-
287
- # Normalize As to (M,)
288
- if As.numel() == 1:
289
- As = As.reshape(1).expand(M).contiguous()
290
- elif As.ndim == 2:
291
- As = As.reshape(M)
292
- assert As.ndim == 1 and As.shape[0] == M, (
293
- f"As must be scalar, (M,), or (M,1) with M={M}, got {tuple(As.shape)}"
294
- )
295
-
296
- # Normalize Bs to (1,)
297
- assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
298
- Bs = Bs.reshape(1)
299
-
300
- BLOCK_SIZE_N = 128
301
- BLOCK_SIZE_K = 128
302
- C_shape = A.shape[:-1] + (N,)
303
- C = A.new_empty(C_shape, dtype=output_dtype)
304
- BLOCK_SIZE_M = min(max(triton.next_power_of_2(M), 16), 128)
305
- grid = (triton.cdiv(M, BLOCK_SIZE_M), triton.cdiv(N, BLOCK_SIZE_N))
306
- with device_context(A.device):
307
- wrap_triton(w8a8_tensor_fp8_matmul_kernel)[grid](
308
- A,
309
- B,
310
- C,
311
- As,
312
- Bs,
313
- M,
314
- N,
315
- K,
316
- A.stride(-2),
317
- A.stride(-1),
318
- B.stride(1),
319
- B.stride(0),
320
- C.stride(-2),
321
- C.stride(-1),
322
- As.stride(0),
323
- # Meta-parameters
324
- BLOCK_SIZE_M=BLOCK_SIZE_M,
325
- BLOCK_SIZE_N=BLOCK_SIZE_N,
326
- BLOCK_SIZE_K=BLOCK_SIZE_K,
327
- GROUP_SIZE_M=8,
328
- )
329
-
330
- return C
331
-
332
-
333
- def w8a8_block_fp8_matmul(
334
- A: torch.Tensor,
335
- B: torch.Tensor,
336
- As: torch.Tensor,
337
- Bs: torch.Tensor,
338
- block_size: list[int],
339
- output_dtype: torch.dtype = torch.float32,
340
- ) -> torch.Tensor:
341
- """Block-wise W8A8 FP8 matrix multiplication.
342
-
343
- Computes ``C = A @ B.T`` where both operands are pre-quantized to
344
- ``float8_e4m3fn`` with per-block scales, and accumulates in float32
345
- before casting to ``output_dtype``.
346
-
347
- Args:
348
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
349
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
350
- As: Per-token-group activation scales ``[M, K // block_size[1]]``.
351
- Bs: Per-block weight scales ``[N // block_size[0], K // block_size[1]]``.
352
- block_size: ``[block_n, block_k]`` quantization block dimensions, e.g. ``[128, 128]``.
353
- output_dtype: dtype of the returned tensor (default: ``torch.float32``).
354
-
355
- Returns:
356
- Output tensor ``[M, N]`` in ``output_dtype``.
357
- """
358
- return torch.ops.finegrained_fp8.w8a8_block_fp8_matmul(
359
- A, B, As, Bs, block_size, output_dtype
360
- )
361
-
362
-
363
- def w8a8_tensor_fp8_matmul(
364
- A: torch.Tensor,
365
- B: torch.Tensor,
366
- As: torch.Tensor,
367
- Bs: torch.Tensor,
368
- output_dtype: torch.dtype = torch.float32,
369
- ) -> torch.Tensor:
370
- """Tensor-scale W8A8 FP8 matrix multiplication.
371
-
372
- Computes ``C = A @ B.T`` in tensor-scale mode using pre-quantized FP8
373
- activations/weights and tensor scales.
374
-
375
- Args:
376
- A: Quantized activation tensor ``[M, K]`` in ``float8_e4m3fn``.
377
- B: Quantized weight tensor ``[N, K]`` in ``float8_e4m3fn``.
378
- As: Per-row activation scales ``[M]``.
379
- Bs: Single weight scale, scalar or ``[1]``.
380
- output_dtype: dtype of the returned tensor.
381
-
382
- Returns:
383
- Output tensor ``[M, N]`` in ``output_dtype``.
384
- """
385
- return torch.ops.finegrained_fp8.w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
386
-
387
-
388
- def w8a8_fp8_matmul(
389
- A: torch.Tensor,
390
- B: torch.Tensor,
391
- As: torch.Tensor,
392
- Bs: torch.Tensor,
393
- block_size: list[int] | None,
394
- output_dtype: torch.dtype = torch.float32,
395
- ) -> torch.Tensor:
396
- """Unified W8A8 FP8 matmul dispatcher.
397
-
398
- Dispatch rules:
399
- - tensor mode when ``block_size is None``
400
- - tensor mode when ``block_size == [N, K]``
401
- - otherwise block mode
402
-
403
- Returns:
404
- Output tensor ``[M, N]`` in ``output_dtype``.
405
- """
406
- if block_size is None or (
407
- block_size[0] == B.size(0) and block_size[1] == B.size(1)
408
- ):
409
- return w8a8_tensor_fp8_matmul(A, B, As, Bs, output_dtype)
410
-
411
- return w8a8_block_fp8_matmul(A, B, As, Bs, block_size, output_dtype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/metadata.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "name": "finegrained-fp8",
3
- "id": "_finegrained_fp8_xpu_7c5619e",
4
- "version": 1,
5
- "license": "Apache-2.0",
6
- "python-depends": [],
7
- "backend": {
8
- "type": "xpu"
9
- }
10
- }
 
 
 
 
 
 
 
 
 
 
 
build/torch-xpu/utils.py DELETED
@@ -1,13 +0,0 @@
1
- import torch
2
- from contextlib import contextmanager
3
-
4
-
5
- @contextmanager
6
- def device_context(device: torch.device):
7
- """Context manager that sets the active device for any backend (cuda, xpu, etc.)."""
8
- backend = getattr(torch, device.type, None)
9
- if backend is not None and hasattr(backend, "device"):
10
- with backend.device(device):
11
- yield
12
- else:
13
- yield