Kernels:
Trusted publisher
Uploaded using `kernel-builder`.
Browse files- build/torch-xpu/__init__.py +48 -0
- build/torch-xpu/_ops.py +38 -0
- build/torch-xpu/backward.py +1052 -0
- build/torch-xpu/batched.py +1947 -0
- build/torch-xpu/bayesian_autotuner.py +940 -0
- build/torch-xpu/compat.py +498 -0
- build/torch-xpu/descriptors.py +216 -0
- build/torch-xpu/epilogue.py +639 -0
- build/torch-xpu/finegrained_kernels/__init__.py +26 -0
- build/torch-xpu/formats.py +357 -0
- build/torch-xpu/grouped.py +0 -0
- build/torch-xpu/loading/__init__.py +17 -0
- build/torch-xpu/loading/scales.py +325 -0
- build/torch-xpu/loading/tiles.py +585 -0
- build/torch-xpu/matmul.py +1970 -0
- build/torch-xpu/metadata.json +48 -0
- build/torch-xpu/metadata.json.sigstore +1 -0
- build/torch-xpu/mma.py +367 -0
- build/torch-xpu/moe.py +864 -0
- build/torch-xpu/norm.py +156 -0
- build/torch-xpu/pruners.py +1213 -0
- build/torch-xpu/quant.py +955 -0
- build/torch-xpu/scheduling.py +403 -0
- build/torch-xpu/swizzle.py +170 -0
build/torch-xpu/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from .matmul import matmul_2d
|
| 17 |
+
from .batched import matmul_batched
|
| 18 |
+
from .grouped import matmul_grouped
|
| 19 |
+
from .moe import moe_fused_batched, moe_fused_grouped
|
| 20 |
+
from . import backward
|
| 21 |
+
from .formats import get_supported_act_fns
|
| 22 |
+
from .norm import get_supported_norms, rms_norm_rows
|
| 23 |
+
from .swizzle import swizzle_mx_scales, unswizzle_mx_scales
|
| 24 |
+
from .quant import mxfp4_act_quant, mxfp8_act_quant, nvfp4_act_quant
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
# the three GEMM dispatchers (the weight format is read off the tensors; `activation_format`
|
| 28 |
+
# names the activations', the gate|up fusion rides as kwargs)
|
| 29 |
+
"matmul_2d",
|
| 30 |
+
"matmul_batched",
|
| 31 |
+
"matmul_grouped",
|
| 32 |
+
# the fused MoE forwards over them
|
| 33 |
+
"moe_fused_batched",
|
| 34 |
+
"moe_fused_grouped",
|
| 35 |
+
# importing it registers the dgrad formulas on the ops, so a forward call differentiates
|
| 36 |
+
"backward",
|
| 37 |
+
"get_supported_act_fns",
|
| 38 |
+
# the per-expert output norm a model can hand the forwards by name, and its standalone op
|
| 39 |
+
"get_supported_norms",
|
| 40 |
+
"rms_norm_rows",
|
| 41 |
+
# load-time helpers: the swizzled scale layout the SM100 scaled-MMA reads, and the row-wise
|
| 42 |
+
# quantizers a loader uses to quantize weights into the group formats
|
| 43 |
+
"swizzle_mx_scales",
|
| 44 |
+
"unswizzle_mx_scales",
|
| 45 |
+
"mxfp8_act_quant",
|
| 46 |
+
"mxfp4_act_quant",
|
| 47 |
+
"nvfp4_act_quant",
|
| 48 |
+
]
|
build/torch-xpu/_ops.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
def get_backend() -> str:
|
| 4 |
+
"""Detect the backend by inspecting torch."""
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if hasattr(torch, "neuron"):
|
| 8 |
+
# Needs to be sorted before specific Torch builds, since Neuron
|
| 9 |
+
# extension can be loaded into e.g. CUDA Torch builds.
|
| 10 |
+
return "neuron"
|
| 11 |
+
elif torch.version.cuda is not None:
|
| 12 |
+
return "cuda"
|
| 13 |
+
elif torch.version.hip is not None:
|
| 14 |
+
return "rocm"
|
| 15 |
+
elif torch.backends.mps.is_available():
|
| 16 |
+
return "metal"
|
| 17 |
+
elif hasattr(torch.version, "xpu") and torch.version.xpu is not None:
|
| 18 |
+
return "xpu"
|
| 19 |
+
else:
|
| 20 |
+
return "cpu"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _find_ops_name() -> str:
|
| 24 |
+
kernel_name = "finegrained_kernels"
|
| 25 |
+
unique_id = "1348465"
|
| 26 |
+
backend = get_backend()
|
| 27 |
+
return f"_{kernel_name}_{backend}_{unique_id}"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
_OPS_NAME = _find_ops_name()
|
| 31 |
+
|
| 32 |
+
ops = getattr(torch.ops, _OPS_NAME)
|
| 33 |
+
|
| 34 |
+
def add_op_namespace_prefix(op_name: str) -> str:
|
| 35 |
+
"""
|
| 36 |
+
Prefix op by namespace.
|
| 37 |
+
"""
|
| 38 |
+
return f"{_OPS_NAME}::{op_name}"
|
build/torch-xpu/backward.py
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Backward for the quantized ops: dgrad only, registered on the forward ops themselves.
|
| 15 |
+
|
| 16 |
+
``torch.library.register_autograd`` on the ops themselves rather than an ``autograd.Function``
|
| 17 |
+
wrapper: the ops take the RAW activation (they quantize it in one pass internally), so the op
|
| 18 |
+
boundary is exactly where a usable gradient lives, and hanging autograd there means an ordinary
|
| 19 |
+
``matmul_2d`` call differentiates — through compile, export and AOTAutograd, with no second
|
| 20 |
+
entry point to keep in sync. ``compile_time_only_triton_op``'s dispatch routes to the op
|
| 21 |
+
whenever an input wants a gradient, so this fires in eager as well.
|
| 22 |
+
|
| 23 |
+
dgrad ONLY: ``dA = dY @ B``, contracting N. The quantized weight never receives a gradient, and
|
| 24 |
+
not merely by omission — autograd accumulates into a leaf, and an E4M3 leaf has no ``add``
|
| 25 |
+
("ufunc_add_CUDA not implemented for Float8_e4m3fn"), so an fp8/fp4 tensor cannot be an autograd
|
| 26 |
+
parameter at all. A frozen quantized base with trainable high-precision adapters in front of it
|
| 27 |
+
(QLoRA) is therefore the shape this serves, and the only gradient it needs is the one that flows
|
| 28 |
+
THROUGH the weight to whatever precedes it.
|
| 29 |
+
|
| 30 |
+
Block-FP8 and per-tensor formats reach that product on the forward kernel via transposed views
|
| 31 |
+
(their scale grids subdivide both axes). MX/NVFP4 cannot be reoriented by a view — group scales
|
| 32 |
+
bind to K and packed E2M1 packs along K — so they contract N against the weight's natural (N, K)
|
| 33 |
+
tile, dequantized per-tile in-register; see ``backward.py``. Either way the FORWARD-oriented
|
| 34 |
+
weight is the only copy that exists.
|
| 35 |
+
|
| 36 |
+
── the gradient products ──
|
| 37 |
+
|
| 38 |
+
Forward contracts K (``C = A @ B.T``, both operands K-contiguous). dgrad contracts N::
|
| 39 |
+
|
| 40 |
+
dX[m, k] = sum_n dY[m, n] * W[n, k]
|
| 41 |
+
|
| 42 |
+
The weight is STORED ``(N, K)`` K-contiguous, so the ``[BN, BK]`` tile this reduction wants is
|
| 43 |
+
already a natural contiguous tile — n leads, k is contiguous. Nothing is transposed and no second
|
| 44 |
+
quantized copy is kept: the tile is dequantized in-register and fed to a plain ``tl.dot``, exactly
|
| 45 |
+
as the weight-only forward's ``dot`` arm does. That is what makes a 4-bit frozen weight
|
| 46 |
+
differentiable at no memory cost, which is the whole QLoRA case.
|
| 47 |
+
|
| 48 |
+
The group axis stays K, which here is the OUTPUT axis rather than the contraction axis. That is
|
| 49 |
+
why ``dot_scaled`` cannot serve this pass — the tensor core's scaled MMA requires the scaled axis
|
| 50 |
+
to be the one it reduces over — so dgrad is plain-dot only. The tuner already prefers ``dot`` over
|
| 51 |
+
``dot_scaled`` on the gate|up tile, so little is given up.
|
| 52 |
+
|
| 53 |
+
Format genericity lives in ``_dgrad_weight_tile`` alone. A format is fully described to this
|
| 54 |
+
kernel by three numbers plus the value dtype:
|
| 55 |
+
|
| 56 |
+
SCALE_GROUP_K scale extent along K 32 (MX) 128 (block-FP8)
|
| 57 |
+
SCALE_ROW_DIV rows sharing a scale 1 (MX) 128 (block-FP8)
|
| 58 |
+
WEIGHT_VALUES_PER_BYTE 2 for packed E2M1, else 1
|
| 59 |
+
|
| 60 |
+
so MX (per-row K-groups) and block-FP8 (128x128 blocks) are the same code path at different
|
| 61 |
+
divisors, and no new arm is needed to add a format whose scales tile the same way.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
import torch
|
| 65 |
+
import triton
|
| 66 |
+
import triton.language as tl
|
| 67 |
+
|
| 68 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 69 |
+
from .compat import (
|
| 70 |
+
add_op_namespace_prefix,
|
| 71 |
+
compile_time_only_triton_op,
|
| 72 |
+
compile_time_only_triton_wrap,
|
| 73 |
+
device_context,
|
| 74 |
+
get_accelerator_autotuning_configs,
|
| 75 |
+
sm_count,
|
| 76 |
+
)
|
| 77 |
+
from .descriptors import maybe_descriptor, rebind_dgrad_descriptors, rebind_dgrad_grouped_descriptors
|
| 78 |
+
from .matmul import matmul_2d
|
| 79 |
+
from .pruners import (
|
| 80 |
+
PATH_ANCHOR_AXES,
|
| 81 |
+
block_within_dim_pruner,
|
| 82 |
+
config_filter,
|
| 83 |
+
descriptor_box_pruner,
|
| 84 |
+
compose_pruners,
|
| 85 |
+
smem_pruner,
|
| 86 |
+
warp_spec_compile_guard_pruner,
|
| 87 |
+
)
|
| 88 |
+
from .formats import global_scale_stride, normalize_global_scale
|
| 89 |
+
from .quant import e2m1_cols_to_bf16, e2m1_cols_to_f32
|
| 90 |
+
from .loading.scales import decode_group_scale
|
| 91 |
+
from .loading.tiles import load_grouped_act_tile, operand_tile_ptrs
|
| 92 |
+
from .scheduling import build_tile_layout, resolve_grouped_tile
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# Internal: these are the gradient PRODUCTS, not API. Backward hangs off the forward ops via
|
| 96 |
+
# ``register_autograd`` (the table at the bottom of this file), so an ordinary forward call
|
| 97 |
+
# differentiates and there is
|
| 98 |
+
# no second entry point to keep in sync. Nothing here is re-exported from the package root.
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@triton.jit
|
| 102 |
+
def _dgrad_load_weight(
|
| 103 |
+
b_ptrs, b_descriptor, row0, n_off, kb_off,
|
| 104 |
+
BLOCK_ROWS: tl.constexpr, BLOCK_COLS: tl.constexpr,
|
| 105 |
+
B_MEMORY_MODE: tl.constexpr, GROUPED: tl.constexpr = False,
|
| 106 |
+
):
|
| 107 |
+
"""One ``[BN, BK_bytes]`` weight tile: the explicit pointer tile, or the host-TMA box at the
|
| 108 |
+
same absolute offset. The box is a natural contiguous window — unlike the forward's gated
|
| 109 |
+
tile there is no interleaved span to express — so the descriptor arm needs no doubling.
|
| 110 |
+
Single return; only the taken arm compiles."""
|
| 111 |
+
if B_MEMORY_MODE == "pointer":
|
| 112 |
+
w = tl.load(b_ptrs)
|
| 113 |
+
elif GROUPED: # (E, N, K_bytes) box — the expert leads
|
| 114 |
+
w = tl.reshape(b_descriptor.load([row0, n_off, kb_off]), BLOCK_ROWS, BLOCK_COLS)
|
| 115 |
+
else:
|
| 116 |
+
w = b_descriptor.load([n_off, kb_off])
|
| 117 |
+
return w
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@triton.jit
|
| 121 |
+
def _dgrad_weight_tile(
|
| 122 |
+
w,
|
| 123 |
+
w_scale,
|
| 124 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 125 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 126 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 127 |
+
OUT_DTYPE: tl.constexpr,
|
| 128 |
+
):
|
| 129 |
+
"""One natural ``[BLOCK_SIZE_N, BLOCK_SIZE_K]`` weight tile, dequantized. The K-oriented
|
| 130 |
+
counterpart of ``mx_weight_upcast`` — the group axis is already last here, so unlike the
|
| 131 |
+
forward no transpose is needed.
|
| 132 |
+
|
| 133 |
+
The scale broadcast happens INSIDE the multiply: the real (contiguous) weight reshapes to
|
| 134 |
+
``[BN, ng, g]`` and the ``[BN, ng, 1]`` scale broadcasts across each group's columns.
|
| 135 |
+
Materializing the broadcast first and reshaping it does not lower — the same trap
|
| 136 |
+
``mx_weight_upcast`` documents.
|
| 137 |
+
|
| 138 |
+
``OUT_DTYPE`` picks the arm, and both are this codebase's measured verdicts:
|
| 139 |
+
bf16 for the MMA consumers (exact — E2M1/E4M3 codes and power-of-two UE8M0 scales all fit in
|
| 140 |
+
bf16, and the multiply is a cheap exponent shift), fp32 for the scalar reduce, which widens
|
| 141 |
+
anyway and for which the bf16/E4M3 hop is pure cost. Single return; only the taken arm
|
| 142 |
+
compiles."""
|
| 143 |
+
NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 144 |
+
if OUT_DTYPE == tl.float32: # scalar-reduce arm: unpack straight to fp32, no narrow hop
|
| 145 |
+
vals = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32)
|
| 146 |
+
sc = decode_group_scale(w_scale).to(tl.float32)[:, :, None]
|
| 147 |
+
else: # MMA arm: dequantize in bf16, exactly as the forward's weight upcast does
|
| 148 |
+
vals = e2m1_cols_to_bf16(w) if w.dtype == tl.uint8 else w.to(tl.bfloat16)
|
| 149 |
+
sc = decode_group_scale(w_scale).to(tl.bfloat16)[:, :, None]
|
| 150 |
+
tile = vals.reshape(BLOCK_SIZE_N, NG, SCALE_GROUP_K) * sc
|
| 151 |
+
return tile.reshape(BLOCK_SIZE_N, BLOCK_SIZE_K).to(OUT_DTYPE)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@bayesian_autotune(
|
| 155 |
+
# Plain-dot loop over the N reduction — same physics as the full-precision 2D kernel (tile +
|
| 156 |
+
# WS), no memory-mode axis: the dequantize is in-tile so both operands read through pointers,
|
| 157 |
+
# and no COMPUTE_MODE axis because the scaled MMA cannot reduce over an unscaled axis.
|
| 158 |
+
get_accelerator_autotuning_configs(
|
| 159 |
+
tune_block_nk=True,
|
| 160 |
+
tune_block_m=True,
|
| 161 |
+
warp_spec=True,
|
| 162 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 163 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 164 |
+
pre_hook=rebind_dgrad_descriptors,
|
| 165 |
+
),
|
| 166 |
+
["N", "K", "m_bit_length"],
|
| 167 |
+
n_trials=100,
|
| 168 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 169 |
+
finite_check_args=("C",),
|
| 170 |
+
prune_configs_by={
|
| 171 |
+
"early_config_prune": compose_pruners(
|
| 172 |
+
block_within_dim_pruner("N"), # exact N-loop: no tail mask, constant-stride advance
|
| 173 |
+
config_filter( # the scale row advances by BN // SCALE_ROW_DIV — must be whole
|
| 174 |
+
lambda c, a: c.kwargs["BLOCK_SIZE_N"] % max(a.get("SCALE_ROW_DIV", 1), 1) == 0
|
| 175 |
+
),
|
| 176 |
+
config_filter( # BK below one scale group gives NG=0 -> tl.arange(0, 0)
|
| 177 |
+
lambda c, a: c.kwargs["BLOCK_SIZE_K"] % max(a.get("SCALE_GROUP_K", 1), 1) == 0
|
| 178 |
+
),
|
| 179 |
+
warp_spec_compile_guard_pruner(),
|
| 180 |
+
descriptor_box_pruner("BLOCK_SIZE_K"),
|
| 181 |
+
smem_pruner("BLOCK_SIZE_K"),
|
| 182 |
+
)
|
| 183 |
+
},
|
| 184 |
+
)
|
| 185 |
+
@triton.jit
|
| 186 |
+
def dgrad_matmul_2d_kernel(
|
| 187 |
+
A, # (M, N) upstream gradient, raw bf16/fp16
|
| 188 |
+
ADescriptor, # host TMA descriptor over A, box (BM, BN); read iff A_MEMORY_MODE != "pointer"
|
| 189 |
+
B, # (N, K) quantized weight, K-contiguous — read in its FORWARD orientation
|
| 190 |
+
BDescriptor, # host TMA descriptor over B, box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
|
| 191 |
+
Bs, # (N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
|
| 192 |
+
BsGlobal, # fp32 NVFP4 second-level weight global; folded on the accumulator, None folds out
|
| 193 |
+
C, # (M, K) output gradient
|
| 194 |
+
# Shape
|
| 195 |
+
M,
|
| 196 |
+
N,
|
| 197 |
+
K,
|
| 198 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 199 |
+
# Strides
|
| 200 |
+
stride_a_m,
|
| 201 |
+
stride_a_n,
|
| 202 |
+
stride_b_n,
|
| 203 |
+
stride_b_k,
|
| 204 |
+
stride_bs_n,
|
| 205 |
+
stride_bs_k,
|
| 206 |
+
stride_c_m,
|
| 207 |
+
stride_c_k,
|
| 208 |
+
# Format
|
| 209 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 210 |
+
SCALE_ROW_DIV: tl.constexpr,
|
| 211 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 212 |
+
# Meta-parameters
|
| 213 |
+
A_MEMORY_MODE: tl.constexpr,
|
| 214 |
+
B_MEMORY_MODE: tl.constexpr,
|
| 215 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 216 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 217 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 218 |
+
WARP_SPEC: tl.constexpr,
|
| 219 |
+
):
|
| 220 |
+
pid_m = tl.program_id(0)
|
| 221 |
+
pid_k = tl.program_id(1)
|
| 222 |
+
|
| 223 |
+
offs_m = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 224 |
+
offs_k = pid_k * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K)
|
| 225 |
+
offs_kb = tl.minimum(
|
| 226 |
+
pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 227 |
+
+ tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
|
| 228 |
+
K // WEIGHT_VALUES_PER_BYTE - 1,
|
| 229 |
+
)
|
| 230 |
+
offs_kg = tl.minimum(
|
| 231 |
+
(pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
|
| 232 |
+
K // SCALE_GROUP_K - 1,
|
| 233 |
+
)
|
| 234 |
+
m_mask = offs_m < M
|
| 235 |
+
k_mask = offs_k < K
|
| 236 |
+
|
| 237 |
+
# BLOCK_SIZE_N divides N (block_within_dim_pruner), so the reduction loop is EXACT: no tail
|
| 238 |
+
# mask on the activation, no clamp on the weight rows, and the addresses advance by a constant
|
| 239 |
+
# stride instead of being rebuilt from offsets each step — the forward's loop shape.
|
| 240 |
+
offs_n0 = tl.arange(0, BLOCK_SIZE_N)
|
| 241 |
+
# SWAP_AB is the WEIGHT's viewpoint, inverted for activations: True = rows-major [BM, BN]
|
| 242 |
+
dy_ptrs = operand_tile_ptrs(A, offs_m, offs_n0, stride_a_m, stride_a_n, A_MEMORY_MODE, True)
|
| 243 |
+
b_ptrs = operand_tile_ptrs(B, offs_n0, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, True)
|
| 244 |
+
bs_ptrs = (
|
| 245 |
+
Bs + (offs_n0[:, None] // SCALE_ROW_DIV) * stride_bs_n + offs_kg[None, :] * stride_bs_k
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_K), dtype=tl.float32)
|
| 249 |
+
for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N), warp_specialize=WARP_SPEC):
|
| 250 |
+
n_off = n * BLOCK_SIZE_N
|
| 251 |
+
dy = load_grouped_act_tile(
|
| 252 |
+
dy_ptrs, ADescriptor, pid_m * BLOCK_SIZE_M, n_off, m_mask, 0, A_MEMORY_MODE
|
| 253 |
+
)
|
| 254 |
+
# the tile the reduction wants is the weight's NATURAL layout: n leads, k contiguous
|
| 255 |
+
w = _dgrad_load_weight(
|
| 256 |
+
b_ptrs, BDescriptor, 0, n_off,
|
| 257 |
+
pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
|
| 258 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE, B_MEMORY_MODE,
|
| 259 |
+
)
|
| 260 |
+
w_scale = tl.load(bs_ptrs)
|
| 261 |
+
accumulator += tl.dot(
|
| 262 |
+
dy.to(tl.bfloat16),
|
| 263 |
+
_dgrad_weight_tile(
|
| 264 |
+
w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.bfloat16
|
| 265 |
+
),
|
| 266 |
+
)
|
| 267 |
+
dy_ptrs += BLOCK_SIZE_N * stride_a_n
|
| 268 |
+
b_ptrs += BLOCK_SIZE_N * stride_b_n
|
| 269 |
+
bs_ptrs += (BLOCK_SIZE_N // SCALE_ROW_DIV) * stride_bs_n
|
| 270 |
+
|
| 271 |
+
if BsGlobal is not None: # NVFP4's second level — one matrix, so one value
|
| 272 |
+
accumulator = accumulator * tl.load(BsGlobal)
|
| 273 |
+
|
| 274 |
+
tl.store(
|
| 275 |
+
C + offs_m[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
|
| 276 |
+
accumulator.to(C.dtype.element_ty),
|
| 277 |
+
mask=m_mask[:, None] & k_mask[None, :],
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@compile_time_only_triton_op("finegrained::dgrad_matmul_2d", mutates_args=())
|
| 282 |
+
def dgrad_matmul_2d(
|
| 283 |
+
dY: torch.Tensor,
|
| 284 |
+
W: torch.Tensor,
|
| 285 |
+
Ws: torch.Tensor,
|
| 286 |
+
scale_group_k: int,
|
| 287 |
+
scale_row_div: int,
|
| 288 |
+
output_dtype: torch.dtype | None = None,
|
| 289 |
+
w_global_scale: torch.Tensor | None = None,
|
| 290 |
+
) -> torch.Tensor:
|
| 291 |
+
"""``dX = dY @ W`` against the FORWARD-oriented quantized weight — the gradient a frozen
|
| 292 |
+
quantized base weight passes to whatever trains in front of it (LoRA adapters, earlier
|
| 293 |
+
layers).
|
| 294 |
+
|
| 295 |
+
``W`` is ``(N, K)`` exactly as the forward holds it; nothing is transposed or re-quantized.
|
| 296 |
+
``scale_group_k``/``scale_row_div`` describe the scale grid (MX: ``32, 1``; block-FP8:
|
| 297 |
+
``128, 128``), which is the only thing that differs between formats."""
|
| 298 |
+
assert dY.ndim == 2 and W.ndim == 2, f"expected 2D operands, got {dY.ndim}D and {W.ndim}D"
|
| 299 |
+
M, N = dY.shape
|
| 300 |
+
values_per_byte = 2 if W.dtype == torch.uint8 else 1
|
| 301 |
+
K = W.shape[1] * values_per_byte
|
| 302 |
+
assert W.shape[0] == N, f"weight rows {W.shape[0]} != dY's N {N}"
|
| 303 |
+
assert K % scale_group_k == 0, (
|
| 304 |
+
f"K={K} must be a whole number of scale groups ({scale_group_k})"
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
dX = torch.empty(M, K, device=dY.device, dtype=output_dtype or dY.dtype)
|
| 308 |
+
grid = lambda META: ( # noqa: E731
|
| 309 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 310 |
+
triton.cdiv(K, META["BLOCK_SIZE_K"]),
|
| 311 |
+
)
|
| 312 |
+
with device_context(dY.device):
|
| 313 |
+
# built INSIDE the device context: the tensormap is created against the CURRENT context,
|
| 314 |
+
# so constructing it before the switch fails as "invalid device context" whenever the
|
| 315 |
+
# caller's device is not the active one. Boxes are placeholders; the pre_hook rebinds
|
| 316 |
+
# them to the tuned tile per config.
|
| 317 |
+
a_descriptor = maybe_descriptor(dY, [1, 128])
|
| 318 |
+
b_descriptor = maybe_descriptor(W, [1, 128])
|
| 319 |
+
compile_time_only_triton_wrap(dgrad_matmul_2d_kernel)[grid](
|
| 320 |
+
dY,
|
| 321 |
+
a_descriptor,
|
| 322 |
+
W,
|
| 323 |
+
b_descriptor,
|
| 324 |
+
Ws,
|
| 325 |
+
w_global_scale,
|
| 326 |
+
dX,
|
| 327 |
+
M,
|
| 328 |
+
N,
|
| 329 |
+
K,
|
| 330 |
+
max(int(M).bit_length(), 1),
|
| 331 |
+
dY.stride(0),
|
| 332 |
+
dY.stride(1),
|
| 333 |
+
W.stride(0),
|
| 334 |
+
W.stride(1),
|
| 335 |
+
Ws.stride(0),
|
| 336 |
+
Ws.stride(1),
|
| 337 |
+
dX.stride(0),
|
| 338 |
+
dX.stride(1),
|
| 339 |
+
SCALE_GROUP_K=scale_group_k,
|
| 340 |
+
SCALE_ROW_DIV=scale_row_div,
|
| 341 |
+
WEIGHT_VALUES_PER_BYTE=values_per_byte,
|
| 342 |
+
)
|
| 343 |
+
return dX
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
@bayesian_autotune(
|
| 347 |
+
# Persistent grouped loop, plain dot over the N reduction. No memory-mode axis (the
|
| 348 |
+
# dequantize is in-tile, so both operands read through pointers) and no COMPUTE_MODE axis
|
| 349 |
+
# (the scaled MMA cannot reduce over an unscaled axis — see the module docstring).
|
| 350 |
+
get_accelerator_autotuning_configs(
|
| 351 |
+
tune_block_nk=True,
|
| 352 |
+
tune_block_m=True,
|
| 353 |
+
warp_spec=True,
|
| 354 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 355 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 356 |
+
pre_hook=rebind_dgrad_grouped_descriptors,
|
| 357 |
+
),
|
| 358 |
+
["N", "K", "tokens_per_expert_bit_length"],
|
| 359 |
+
n_trials=100,
|
| 360 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 361 |
+
finite_check_args=("C",),
|
| 362 |
+
# C is ACCUMULATED into (tl.atomic_add: several routed rows share a source token), so the
|
| 363 |
+
# kernel is not idempotent. Benchmarking calls each config many times against one buffer, and
|
| 364 |
+
# without this every repetition adds again — the tune reads a correct kernel as 1.7e5x wrong.
|
| 365 |
+
reset_to_zero=("C",),
|
| 366 |
+
prune_configs_by={
|
| 367 |
+
"early_config_prune": compose_pruners(
|
| 368 |
+
block_within_dim_pruner("N"), # exact N-loop: no tail mask, constant-stride advance
|
| 369 |
+
config_filter( # the scale row advances by BN // SCALE_ROW_DIV — must be whole
|
| 370 |
+
lambda c, a: c.kwargs["BLOCK_SIZE_N"] % max(a.get("SCALE_ROW_DIV", 1), 1) == 0
|
| 371 |
+
),
|
| 372 |
+
config_filter( # BK below one scale group gives NG=0 -> tl.arange(0, 0)
|
| 373 |
+
lambda c, a: c.kwargs["BLOCK_SIZE_K"] % max(a.get("SCALE_GROUP_K", 1), 1) == 0
|
| 374 |
+
),
|
| 375 |
+
warp_spec_compile_guard_pruner(),
|
| 376 |
+
descriptor_box_pruner("BLOCK_SIZE_K"),
|
| 377 |
+
smem_pruner("BLOCK_SIZE_K"),
|
| 378 |
+
)
|
| 379 |
+
},
|
| 380 |
+
)
|
| 381 |
+
@triton.jit
|
| 382 |
+
def dgrad_matmul_grouped_kernel(
|
| 383 |
+
A, # (S, N) upstream gradient in the forward's OUTPUT row order
|
| 384 |
+
ADescriptor, # host TMA descriptor over A; box (1|BM, BN) — 1 row when gathering
|
| 385 |
+
B, # (E, N, K) quantized weights, K-contiguous — the forward orientation
|
| 386 |
+
BDescriptor, # host TMA descriptor over B, box (1, BN, BK_bytes)
|
| 387 |
+
Bs, # (E, N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
|
| 388 |
+
BsGlobal, # fp32 NVFP4 weight global, per tensor or per expert; folded on the accumulator
|
| 389 |
+
stride_bs_global, # expert stride of BsGlobal (0 broadcasts a scalar)
|
| 390 |
+
C, # (S, K) output gradient, written in the forward's INPUT row order
|
| 391 |
+
GatherIdx, # (S,) sorted position -> source row of the forward's A
|
| 392 |
+
ScatterIdx, # (S,) sorted position -> destination row of the forward's C
|
| 393 |
+
ExpertStart, # (NUM_EXPERTS_POW2 + 1,) cumulative row starts, S sentinel
|
| 394 |
+
# Shape
|
| 395 |
+
S,
|
| 396 |
+
N,
|
| 397 |
+
K,
|
| 398 |
+
tokens_per_expert_bit_length, # autotune key only; unused in body
|
| 399 |
+
# Strides
|
| 400 |
+
stride_a_m,
|
| 401 |
+
stride_a_n,
|
| 402 |
+
stride_b_e,
|
| 403 |
+
stride_b_n,
|
| 404 |
+
stride_b_k,
|
| 405 |
+
stride_bs_e,
|
| 406 |
+
stride_bs_n,
|
| 407 |
+
stride_bs_k,
|
| 408 |
+
stride_c_m,
|
| 409 |
+
stride_c_k,
|
| 410 |
+
# Format
|
| 411 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 412 |
+
SCALE_ROW_DIV: tl.constexpr,
|
| 413 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 414 |
+
# Meta-parameters
|
| 415 |
+
A_MEMORY_MODE: tl.constexpr,
|
| 416 |
+
NUM_EXPERTS_POW2: tl.constexpr,
|
| 417 |
+
NUM_SMS: tl.constexpr,
|
| 418 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 419 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 420 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 421 |
+
WARP_SPEC: tl.constexpr,
|
| 422 |
+
B_MEMORY_MODE: tl.constexpr,
|
| 423 |
+
ROUTED_OUT: tl.constexpr = False,
|
| 424 |
+
):
|
| 425 |
+
"""Grouped dgrad. The tile resolver is reused verbatim with K-tiles in its N-tile slot —
|
| 426 |
+
it is generic in that second axis, so ``offs_bn`` comes back as this kernel's K offsets and
|
| 427 |
+
``pid_n`` as its K-tile index (GATE=False: the gradient of a gate|up GEMM is an UNGATED
|
| 428 |
+
product at the doubled N extent, so no gated arm exists here).
|
| 429 |
+
|
| 430 |
+
The two row maps SWAP roles against the forward: the forward gathered A at ``in_row`` and
|
| 431 |
+
scattered C to ``out_row``, so its gradient reads ``A`` at ``out_row`` and writes ``C`` at
|
| 432 |
+
``in_row``. Both are already real parameters, which is why routing needs no new machinery."""
|
| 433 |
+
start_pid = tl.program_id(axis=0)
|
| 434 |
+
exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
|
| 435 |
+
ExpertStart, NUM_EXPERTS_POW2, BLOCK_SIZE_M
|
| 436 |
+
)
|
| 437 |
+
num_k_tiles = tl.cdiv(K, BLOCK_SIZE_K)
|
| 438 |
+
|
| 439 |
+
for tile_id in tl.range(start_pid, total_m_tiles * num_k_tiles, NUM_SMS):
|
| 440 |
+
pid_k, _, expert_id64, in_row, out_row, row_mask, offs_k, row0, _, m_start = (
|
| 441 |
+
resolve_grouped_tile(
|
| 442 |
+
tile_id,
|
| 443 |
+
num_k_tiles,
|
| 444 |
+
exp_start,
|
| 445 |
+
freqs,
|
| 446 |
+
tile_start_excl,
|
| 447 |
+
e_offs,
|
| 448 |
+
GatherIdx,
|
| 449 |
+
ScatterIdx,
|
| 450 |
+
BLOCK_SIZE_K,
|
| 451 |
+
BLOCK_SIZE_M,
|
| 452 |
+
False,
|
| 453 |
+
)
|
| 454 |
+
)
|
| 455 |
+
offs_kb = tl.minimum(
|
| 456 |
+
pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 457 |
+
+ tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
|
| 458 |
+
K // WEIGHT_VALUES_PER_BYTE - 1,
|
| 459 |
+
)
|
| 460 |
+
offs_kg = tl.minimum(
|
| 461 |
+
(pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
|
| 462 |
+
K // SCALE_GROUP_K - 1,
|
| 463 |
+
)
|
| 464 |
+
b_base = B + expert_id64 * stride_b_e
|
| 465 |
+
bs_base = Bs + expert_id64 * stride_bs_e
|
| 466 |
+
|
| 467 |
+
# exact N-loop (BLOCK_SIZE_N divides N): maskless weight, constant-stride advance
|
| 468 |
+
offs_n0 = tl.arange(0, BLOCK_SIZE_N)
|
| 469 |
+
# SWAP_AB is the WEIGHT's viewpoint and activation callers pass it INVERTED: False builds
|
| 470 |
+
# [K, rows], so both of these want True to get the rows-major tiles the dot expects
|
| 471 |
+
# ([BM, BN] and [BN, BK]). Only the pointer arm is affected — the descriptor arm returns
|
| 472 |
+
# the scalar base and reads its box from the descriptor.
|
| 473 |
+
dy_ptrs = operand_tile_ptrs(
|
| 474 |
+
A, out_row, offs_n0, stride_a_m, stride_a_n, A_MEMORY_MODE, True
|
| 475 |
+
)
|
| 476 |
+
b_ptrs = operand_tile_ptrs(
|
| 477 |
+
b_base, offs_n0, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, True
|
| 478 |
+
)
|
| 479 |
+
bs_ptrs = (
|
| 480 |
+
bs_base
|
| 481 |
+
+ (offs_n0[:, None] // SCALE_ROW_DIV) * stride_bs_n
|
| 482 |
+
+ offs_kg[None, :] * stride_bs_k
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_K), dtype=tl.float32)
|
| 486 |
+
for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N), warp_specialize=WARP_SPEC):
|
| 487 |
+
n_off = n * BLOCK_SIZE_N
|
| 488 |
+
# the forward gathered A at in_row and scattered C to out_row, so its gradient READS
|
| 489 |
+
# at out_row — ScatterIdx is this pass's gather map. The box anchor must be out_row's
|
| 490 |
+
# tile start too: the resolver's `m_start` is min(in_row) — the FORWARD's read side —
|
| 491 |
+
# and with a gather-only launch (ScatterIdx None, in_row = the accumulation targets)
|
| 492 |
+
# it anchors the box at a TOKEN id, reading the wrong dY rows.
|
| 493 |
+
dy = load_grouped_act_tile(
|
| 494 |
+
dy_ptrs, ADescriptor, tl.min(out_row).to(tl.int32), n_off, row_mask, out_row,
|
| 495 |
+
A_MEMORY_MODE, ScatterIdx is not None,
|
| 496 |
+
)
|
| 497 |
+
w = _dgrad_load_weight(
|
| 498 |
+
b_ptrs, BDescriptor, row0, n_off,
|
| 499 |
+
pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
|
| 500 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE, B_MEMORY_MODE, True,
|
| 501 |
+
)
|
| 502 |
+
w_scale = tl.load(bs_ptrs)
|
| 503 |
+
accumulator += tl.dot(
|
| 504 |
+
dy.to(tl.bfloat16),
|
| 505 |
+
_dgrad_weight_tile(
|
| 506 |
+
w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.bfloat16
|
| 507 |
+
),
|
| 508 |
+
)
|
| 509 |
+
dy_ptrs += BLOCK_SIZE_N * stride_a_n
|
| 510 |
+
b_ptrs += BLOCK_SIZE_N * stride_b_n
|
| 511 |
+
bs_ptrs += (BLOCK_SIZE_N // SCALE_ROW_DIV) * stride_bs_n
|
| 512 |
+
|
| 513 |
+
# Fallback when the rows are NOT token-major (no scatter map): accumulate straight into
|
| 514 |
+
# the source token. `in_row` is that token, and with top_k > 1 several routed rows share
|
| 515 |
+
# one, so a store would keep whichever tile landed last and drop the rest — invisible to
|
| 516 |
+
# any test whose gather map is the identity (S == M), which is how it survived one.
|
| 517 |
+
#
|
| 518 |
+
# The FORWARD reduces without atomics (`weighted_reduce`: (S, H) -> (T, H)) — but its
|
| 519 |
+
# routed-space tensor already exists, because the down GEMM's natural output IS (S, H), and
|
| 520 |
+
# its reduce also applies routing weights and skips EP sentinels, which an atomic could not
|
| 521 |
+
# express. dgrad is the transpose: its natural output is SOURCE space, so mirroring the
|
| 522 |
+
# forward would mean materializing a (S, K) buffer purely to avoid atomics.
|
| 523 |
+
#
|
| 524 |
+
# Measured and rejected: store-into-(S, K) + index_add_ ran 4038 vs 4181us at DSV3 top_k=8
|
| 525 |
+
# (-3.4%), 1278 vs 1294 at GPT-OSS top_k=4, and SLOWER at decode (411 vs 404) — to buy that
|
| 526 |
+
# you pay a top_k x fp32 intermediate, ~940MB at DSV3 shapes. top_k-way contention is not
|
| 527 |
+
# the bottleneck here; the GEMM is.
|
| 528 |
+
if BsGlobal is not None: # NVFP4's second level, this tile's expert
|
| 529 |
+
accumulator = accumulator * tl.load(BsGlobal + expert_id64 * stride_bs_global)
|
| 530 |
+
|
| 531 |
+
if ROUTED_OUT:
|
| 532 |
+
# Plain store at the TOKEN-MAJOR routed row (`out_row`), then the caller sums the
|
| 533 |
+
# top_k rows per token. This is the FORWARD's shape — it stores routed rows and
|
| 534 |
+
# reduces them with `weighted_reduce` — and it is 1.40x faster than atomics at DSV3
|
| 535 |
+
# top_k=8 (3003 vs 4215us), 1.13x at GPT-OSS top_k=4, break-even at decode. top_k-way
|
| 536 |
+
# atomic contention is the difference; the reduce itself is nearly free.
|
| 537 |
+
tl.store(
|
| 538 |
+
C + out_row[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
|
| 539 |
+
accumulator,
|
| 540 |
+
mask=row_mask[:, None] & (offs_k[None, :] < K),
|
| 541 |
+
)
|
| 542 |
+
else:
|
| 543 |
+
tl.atomic_add(
|
| 544 |
+
C + in_row[:, None] * stride_c_m + offs_k[None, :] * stride_c_k,
|
| 545 |
+
accumulator,
|
| 546 |
+
mask=row_mask[:, None] & (offs_k[None, :] < K),
|
| 547 |
+
)
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
@compile_time_only_triton_op("finegrained::dgrad_matmul_grouped", mutates_args=())
|
| 551 |
+
def dgrad_matmul_grouped(
|
| 552 |
+
dY: torch.Tensor,
|
| 553 |
+
W: torch.Tensor,
|
| 554 |
+
Ws: torch.Tensor,
|
| 555 |
+
expert_start: torch.Tensor,
|
| 556 |
+
scale_group_k: int,
|
| 557 |
+
scale_row_div: int,
|
| 558 |
+
gather_idx: torch.Tensor | None = None,
|
| 559 |
+
scatter_idx: torch.Tensor | None = None,
|
| 560 |
+
output_dtype: torch.dtype | None = None,
|
| 561 |
+
num_input_rows: int | None = None,
|
| 562 |
+
w_global_scale: torch.Tensor | None = None,
|
| 563 |
+
) -> torch.Tensor:
|
| 564 |
+
"""Routed ``dX = dY @ W[expert]`` over expert-sorted positions — the MoE counterpart of
|
| 565 |
+
``dgrad_matmul_2d``.
|
| 566 |
+
|
| 567 |
+
``gather_idx`` / ``scatter_idx`` are the FORWARD's maps, passed unchanged: the kernel swaps
|
| 568 |
+
their roles itself (reads ``dY`` at the forward's scatter destination, writes ``dX`` at its
|
| 569 |
+
gather source), so callers hand over the same two tensors the forward got."""
|
| 570 |
+
assert dY.ndim == 2 and W.ndim == 3, f"expected (S,N) and (E,N,K), got {dY.shape}, {W.shape}"
|
| 571 |
+
S, N = dY.shape
|
| 572 |
+
values_per_byte = 2 if W.dtype == torch.uint8 else 1
|
| 573 |
+
K = W.shape[2] * values_per_byte
|
| 574 |
+
assert W.shape[1] == N, f"weight N {W.shape[1]} != dY's N {N}"
|
| 575 |
+
assert K % scale_group_k == 0, (
|
| 576 |
+
f"K={K} must be a whole number of scale groups ({scale_group_k})"
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
num_experts = W.shape[0]
|
| 580 |
+
num_sms = sm_count(dY.device.index)
|
| 581 |
+
# (num_input_rows, K), ZERO-initialised: the kernel accumulates each routed row's gradient
|
| 582 |
+
# into its source token, so the buffer is indexed in SOURCE space and must start empty.
|
| 583 |
+
# fp32 because that is what tl.atomic_add takes.
|
| 584 |
+
rows = S if num_input_rows is None else num_input_rows
|
| 585 |
+
# Token-major store + a top_k sum beats atomics whenever the scatter map makes the rows
|
| 586 |
+
# token-major (see the kernel). It costs a top_k x fp32 intermediate, which is why the atomic
|
| 587 |
+
# arm stays as the fallback for gather-only launches.
|
| 588 |
+
routed_out = scatter_idx is not None and rows > 0 and S % rows == 0
|
| 589 |
+
dX = torch.zeros(S if routed_out else rows, K, device=dY.device, dtype=torch.float32)
|
| 590 |
+
with device_context(dY.device):
|
| 591 |
+
# inside the context — see dgrad_matmul_2d; placeholder boxes, rebound by the pre_hook
|
| 592 |
+
a_descriptor = maybe_descriptor(dY, [1, 128])
|
| 593 |
+
b_descriptor = maybe_descriptor(W, [1, 1, 128])
|
| 594 |
+
compile_time_only_triton_wrap(dgrad_matmul_grouped_kernel)[(num_sms,)](
|
| 595 |
+
dY,
|
| 596 |
+
a_descriptor,
|
| 597 |
+
W,
|
| 598 |
+
b_descriptor,
|
| 599 |
+
Ws,
|
| 600 |
+
w_global_scale,
|
| 601 |
+
global_scale_stride(w_global_scale),
|
| 602 |
+
dX,
|
| 603 |
+
gather_idx,
|
| 604 |
+
scatter_idx,
|
| 605 |
+
expert_start,
|
| 606 |
+
S,
|
| 607 |
+
N,
|
| 608 |
+
K,
|
| 609 |
+
max(int(max(S // max(num_experts, 1), 1)).bit_length(), 1),
|
| 610 |
+
dY.stride(0),
|
| 611 |
+
dY.stride(1),
|
| 612 |
+
W.stride(0),
|
| 613 |
+
W.stride(1),
|
| 614 |
+
W.stride(2),
|
| 615 |
+
Ws.stride(0),
|
| 616 |
+
Ws.stride(1),
|
| 617 |
+
Ws.stride(2),
|
| 618 |
+
dX.stride(0),
|
| 619 |
+
dX.stride(1),
|
| 620 |
+
SCALE_GROUP_K=scale_group_k,
|
| 621 |
+
SCALE_ROW_DIV=scale_row_div,
|
| 622 |
+
WEIGHT_VALUES_PER_BYTE=values_per_byte,
|
| 623 |
+
NUM_EXPERTS_POW2=triton.next_power_of_2(num_experts),
|
| 624 |
+
NUM_SMS=num_sms,
|
| 625 |
+
ROUTED_OUT=routed_out,
|
| 626 |
+
)
|
| 627 |
+
if routed_out:
|
| 628 |
+
dX = dX.view(rows, S // rows, K).sum(1)
|
| 629 |
+
return dX.to(output_dtype or dY.dtype)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
@triton.jit
|
| 633 |
+
def _glu_grad(g, u, dh, ACT_FN: tl.constexpr, SWIGLU_ALPHA: tl.constexpr,
|
| 634 |
+
SWIGLU_LIMIT: tl.constexpr):
|
| 635 |
+
"""(dgate, dup) for one tile, mirroring ``epilogue.glu`` arm for arm.
|
| 636 |
+
|
| 637 |
+
The alpha arm is ``(u + 1) * g * sigmoid(ALPHA * g)`` — the ``+1`` is load-bearing and easy to
|
| 638 |
+
drop: without it the gate gradient is wrong on exactly the GPT-OSS / MiniMax path and nowhere
|
| 639 |
+
else. ``SWIGLU_LIMIT`` clamps gate ABOVE and up to ``[-LIMIT, LIMIT]``, so the gradient is zero
|
| 640 |
+
wherever the forward saturated. Single return; only the taken arms compile."""
|
| 641 |
+
gc = tl.minimum(g, SWIGLU_LIMIT) if SWIGLU_LIMIT is not None else g
|
| 642 |
+
uc = (
|
| 643 |
+
tl.minimum(tl.maximum(u, -SWIGLU_LIMIT), SWIGLU_LIMIT)
|
| 644 |
+
if SWIGLU_LIMIT is not None
|
| 645 |
+
else u
|
| 646 |
+
)
|
| 647 |
+
if SWIGLU_ALPHA is not None:
|
| 648 |
+
sig = tl.sigmoid(gc * SWIGLU_ALPHA)
|
| 649 |
+
act = gc * sig
|
| 650 |
+
dact = sig + gc * SWIGLU_ALPHA * sig * (1.0 - sig)
|
| 651 |
+
dg = dh * (uc + 1.0) * dact
|
| 652 |
+
elif ACT_FN == "silu":
|
| 653 |
+
sig = tl.sigmoid(gc)
|
| 654 |
+
act = gc * sig
|
| 655 |
+
dg = dh * uc * (sig + gc * sig * (1.0 - sig))
|
| 656 |
+
elif ACT_FN == "relu":
|
| 657 |
+
act = tl.maximum(gc, 0.0)
|
| 658 |
+
dg = dh * uc * (gc > 0.0).to(tl.float32)
|
| 659 |
+
else: # gelu, exact via erf — the forward's form
|
| 660 |
+
cdf = 0.5 * (1.0 + tl.erf(gc * 0.7071067811865476))
|
| 661 |
+
act = gc * cdf
|
| 662 |
+
dg = dh * uc * (cdf + gc * tl.exp(-0.5 * gc * gc) * 0.3989422804014327)
|
| 663 |
+
du = dh * act
|
| 664 |
+
if SWIGLU_LIMIT is not None: # saturated inputs pass no gradient
|
| 665 |
+
dg = tl.where(g > SWIGLU_LIMIT, 0.0, dg)
|
| 666 |
+
du = tl.where(tl.abs(u) > SWIGLU_LIMIT, 0.0, du)
|
| 667 |
+
return dg, du
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
@triton.jit
|
| 671 |
+
def _glu_backward_kernel(
|
| 672 |
+
Z, # (M, 2*INTER) pre-activation, gate at even columns / up at odd
|
| 673 |
+
DH, # (M, INTER) gradient w.r.t. the GLU output
|
| 674 |
+
DZ, # (M, 2*INTER) output, interleaved to match Z
|
| 675 |
+
M, INTER,
|
| 676 |
+
stride_z_m, stride_z_n, stride_dh_m, stride_dh_n, stride_dz_m, stride_dz_n,
|
| 677 |
+
ACT_FN: tl.constexpr, SWIGLU_ALPHA: tl.constexpr, SWIGLU_LIMIT: tl.constexpr,
|
| 678 |
+
BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_I: tl.constexpr,
|
| 679 |
+
):
|
| 680 |
+
"""Elementwise GLU backward over the interleaved layout. Gate ``j`` sits at column ``2j`` and
|
| 681 |
+
up at ``2j+1``, so one tile reads both halves with a stride-2 pair of loads and writes them
|
| 682 |
+
back the same way — no split, no concatenate, no materialized halves."""
|
| 683 |
+
offs_m = tl.program_id(0) * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 684 |
+
offs_i = tl.program_id(1) * BLOCK_SIZE_I + tl.arange(0, BLOCK_SIZE_I)
|
| 685 |
+
mask = (offs_m[:, None] < M) & (offs_i[None, :] < INTER)
|
| 686 |
+
|
| 687 |
+
z_gate = Z + offs_m[:, None] * stride_z_m + (2 * offs_i[None, :]) * stride_z_n
|
| 688 |
+
g = tl.load(z_gate, mask=mask, other=0.0).to(tl.float32)
|
| 689 |
+
u = tl.load(z_gate + stride_z_n, mask=mask, other=0.0).to(tl.float32)
|
| 690 |
+
dh = tl.load(
|
| 691 |
+
DH + offs_m[:, None] * stride_dh_m + offs_i[None, :] * stride_dh_n,
|
| 692 |
+
mask=mask, other=0.0,
|
| 693 |
+
).to(tl.float32)
|
| 694 |
+
|
| 695 |
+
dg, du = _glu_grad(g, u, dh, ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT)
|
| 696 |
+
|
| 697 |
+
dz_gate = DZ + offs_m[:, None] * stride_dz_m + (2 * offs_i[None, :]) * stride_dz_n
|
| 698 |
+
tl.store(dz_gate, dg.to(DZ.dtype.element_ty), mask=mask)
|
| 699 |
+
tl.store(dz_gate + stride_dz_n, du.to(DZ.dtype.element_ty), mask=mask)
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
def glu_backward(
|
| 703 |
+
dH: torch.Tensor,
|
| 704 |
+
Z: torch.Tensor,
|
| 705 |
+
act_fn: str = "silu",
|
| 706 |
+
swiglu_alpha: float | None = None,
|
| 707 |
+
swiglu_limit: float | None = None,
|
| 708 |
+
) -> torch.Tensor:
|
| 709 |
+
"""Gradient of the fused GLU epilogue: ``dH`` (grad w.r.t. ``H = f(Z_gate) * Z_up``) and the
|
| 710 |
+
2I-wide pre-activation ``Z`` -> ``dZ``, interleaved.
|
| 711 |
+
|
| 712 |
+
One pass over Z, no materialized halves. The GEMM that consumes ``dZ`` is then an UNGATED
|
| 713 |
+
product at the doubled N extent — the forward kernel with no gate flag, because
|
| 714 |
+
``pid_n * 2BN == 2 * pid_n * BN`` — so no gated backward kernel exists or is needed."""
|
| 715 |
+
assert dH.ndim == 2 and Z.ndim == 2, f"expected 2D, got {dH.shape}, {Z.shape}"
|
| 716 |
+
M, INTER = dH.shape
|
| 717 |
+
assert Z.shape == (M, 2 * INTER), f"Z must be (M, 2I) = {(M, 2 * INTER)}, got {tuple(Z.shape)}"
|
| 718 |
+
dZ = torch.empty_like(Z)
|
| 719 |
+
grid = lambda META: ( # noqa: E731
|
| 720 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 721 |
+
triton.cdiv(INTER, META["BLOCK_SIZE_I"]),
|
| 722 |
+
)
|
| 723 |
+
with device_context(Z.device):
|
| 724 |
+
compile_time_only_triton_wrap(_glu_backward_kernel)[grid](
|
| 725 |
+
Z, dH, dZ, M, INTER,
|
| 726 |
+
Z.stride(0), Z.stride(1), dH.stride(0), dH.stride(1), dZ.stride(0), dZ.stride(1),
|
| 727 |
+
ACT_FN=act_fn, SWIGLU_ALPHA=swiglu_alpha, SWIGLU_LIMIT=swiglu_limit,
|
| 728 |
+
BLOCK_SIZE_M=32, BLOCK_SIZE_I=64, num_warps=4,
|
| 729 |
+
)
|
| 730 |
+
return dZ
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
@bayesian_autotune(
|
| 734 |
+
# Neither a BLOCK_SIZE_M nor a WARP_SPEC axis. One row per program (the rows are not
|
| 735 |
+
# expert-sorted, so a tile cannot span them) and the reduce is FMA rather than MMA, so there is
|
| 736 |
+
# no M tile to choose — and WS, which needs ``(BN if swapped else BM) >= 64`` of M work, has no
|
| 737 |
+
# M operand to satisfy it. Emitting either axis would search configs that cannot compile.
|
| 738 |
+
get_accelerator_autotuning_configs(tune_block_nk=True),
|
| 739 |
+
["N", "K", "S"],
|
| 740 |
+
n_trials=100,
|
| 741 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 742 |
+
finite_check_args=("C",),
|
| 743 |
+
# No pruners. The smem model is fit to MMA loops — ``num_stages`` buffers of an A tile and a B
|
| 744 |
+
# tile, keyed on BLOCK_SIZE_M — and this loop has neither: the activation is a ``[BN]`` vector
|
| 745 |
+
# and the reduce is FMA, so there is no M dimension for it to reason about. A config that
|
| 746 |
+
# genuinely will not fit raises OutOfResources, which the tuner already inf-forgives.
|
| 747 |
+
)
|
| 748 |
+
@triton.jit
|
| 749 |
+
def dgrad_matmul_batched_kernel(
|
| 750 |
+
A, # (S, N) upstream gradient
|
| 751 |
+
B, # (E, N, K) quantized weights, K-contiguous — the forward orientation
|
| 752 |
+
Bs, # (E, N // SCALE_ROW_DIV, K // SCALE_GROUP_K) weight scales
|
| 753 |
+
BsGlobal, # fp32 NVFP4 weight global, per tensor or per expert; folded on the accumulator
|
| 754 |
+
stride_bs_global, # expert stride of BsGlobal (0 broadcasts a scalar)
|
| 755 |
+
C, # (S, K) output gradient
|
| 756 |
+
ExpertIds, # (S,) which expert each row routed to
|
| 757 |
+
# Shape
|
| 758 |
+
S,
|
| 759 |
+
N,
|
| 760 |
+
K,
|
| 761 |
+
# Strides
|
| 762 |
+
stride_a_m,
|
| 763 |
+
stride_a_n,
|
| 764 |
+
stride_b_e,
|
| 765 |
+
stride_b_n,
|
| 766 |
+
stride_b_k,
|
| 767 |
+
stride_bs_e,
|
| 768 |
+
stride_bs_n,
|
| 769 |
+
stride_bs_k,
|
| 770 |
+
stride_c_m,
|
| 771 |
+
stride_c_k,
|
| 772 |
+
# Format
|
| 773 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 774 |
+
SCALE_ROW_DIV: tl.constexpr,
|
| 775 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 776 |
+
# Meta-parameters
|
| 777 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 778 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 779 |
+
):
|
| 780 |
+
"""Batched dgrad: ``dX[s] = dY[s] @ B[ExpertIds[s]]``.
|
| 781 |
+
|
| 782 |
+
Unlike the grouped form the rows are NOT expert-sorted — each row names its own expert — so a
|
| 783 |
+
tile must cover one expert. ``BLOCK_SIZE_M`` is pinned to 1 by the wrapper for that reason,
|
| 784 |
+
which is also the shape the batched forward runs at."""
|
| 785 |
+
pid_m = tl.program_id(0)
|
| 786 |
+
pid_k = tl.program_id(1)
|
| 787 |
+
row_ok = pid_m < S
|
| 788 |
+
expert_id = tl.load(ExpertIds + pid_m, mask=row_ok, other=0).to(tl.int64)
|
| 789 |
+
|
| 790 |
+
offs_k = pid_k * BLOCK_SIZE_K + tl.arange(0, BLOCK_SIZE_K)
|
| 791 |
+
offs_kb = tl.minimum(
|
| 792 |
+
pid_k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 793 |
+
+ tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE),
|
| 794 |
+
K // WEIGHT_VALUES_PER_BYTE - 1,
|
| 795 |
+
)
|
| 796 |
+
offs_kg = tl.minimum(
|
| 797 |
+
(pid_k * BLOCK_SIZE_K) // SCALE_GROUP_K + tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K),
|
| 798 |
+
K // SCALE_GROUP_K - 1,
|
| 799 |
+
)
|
| 800 |
+
b_base = B + expert_id * stride_b_e
|
| 801 |
+
bs_base = Bs + expert_id * stride_bs_e
|
| 802 |
+
|
| 803 |
+
# CUDA-core FMA reduce, NOT tl.dot: one row per program is an M=1 GEVM, and feeding the MMA a
|
| 804 |
+
# broadcast [1, BN] operand runs it emulated at a fraction of peak. The forward reaches the same
|
| 805 |
+
# conclusion for its decode path (``mx_weight_only_scalar_swapped``) — the scalar arm wins at
|
| 806 |
+
# M=1 and the tuner picks it there. Same reduction, no MMA, no padded M dimension.
|
| 807 |
+
accumulator = tl.zeros((BLOCK_SIZE_K,), dtype=tl.float32)
|
| 808 |
+
for n in tl.range(0, tl.cdiv(N, BLOCK_SIZE_N)):
|
| 809 |
+
offs_n = n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
|
| 810 |
+
dy = tl.load(
|
| 811 |
+
A + pid_m * stride_a_m + offs_n * stride_a_n,
|
| 812 |
+
mask=row_ok & (offs_n < N),
|
| 813 |
+
other=0.0,
|
| 814 |
+
).to(tl.float32)
|
| 815 |
+
offs_nc = tl.minimum(offs_n, N - 1) # clamp: dy is already zeroed past N
|
| 816 |
+
w = tl.load(b_base + offs_nc[:, None] * stride_b_n + offs_kb[None, :] * stride_b_k)
|
| 817 |
+
w_scale = tl.load(
|
| 818 |
+
bs_base
|
| 819 |
+
+ (offs_nc[:, None] // SCALE_ROW_DIV) * stride_bs_n
|
| 820 |
+
+ offs_kg[None, :] * stride_bs_k
|
| 821 |
+
)
|
| 822 |
+
wdeq = _dgrad_weight_tile(
|
| 823 |
+
w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, tl.float32
|
| 824 |
+
)
|
| 825 |
+
accumulator += tl.sum(dy[:, None] * wdeq, axis=0)
|
| 826 |
+
|
| 827 |
+
if BsGlobal is not None: # NVFP4's second level, this row's expert
|
| 828 |
+
accumulator = accumulator * tl.load(BsGlobal + expert_id * stride_bs_global)
|
| 829 |
+
|
| 830 |
+
tl.store(
|
| 831 |
+
C + pid_m * stride_c_m + offs_k * stride_c_k,
|
| 832 |
+
accumulator.to(C.dtype.element_ty),
|
| 833 |
+
mask=row_ok & (offs_k < K),
|
| 834 |
+
)
|
| 835 |
+
|
| 836 |
+
|
| 837 |
+
@compile_time_only_triton_op("finegrained::dgrad_matmul_batched", mutates_args=())
|
| 838 |
+
def dgrad_matmul_batched(
|
| 839 |
+
dY: torch.Tensor,
|
| 840 |
+
W: torch.Tensor,
|
| 841 |
+
Ws: torch.Tensor,
|
| 842 |
+
expert_ids: torch.Tensor,
|
| 843 |
+
scale_group_k: int,
|
| 844 |
+
scale_row_div: int,
|
| 845 |
+
output_dtype: torch.dtype | None = None,
|
| 846 |
+
w_global_scale: torch.Tensor | None = None,
|
| 847 |
+
) -> torch.Tensor:
|
| 848 |
+
"""``dX[s] = dY[s] @ W[expert_ids[s]]`` — the batched (per-row expert) counterpart of
|
| 849 |
+
``dgrad_matmul_grouped``. Registered because the batched experts forward is a SELECTABLE
|
| 850 |
+
dispatch in transformers, not a decode-only path: without this, finetuning against it
|
| 851 |
+
silently produces no gradient."""
|
| 852 |
+
assert dY.ndim == 2 and W.ndim == 3, f"expected (S,N) and (E,N,K), got {dY.shape}, {W.shape}"
|
| 853 |
+
S, N = dY.shape
|
| 854 |
+
values_per_byte = 2 if W.dtype == torch.uint8 else 1
|
| 855 |
+
K = W.shape[2] * values_per_byte
|
| 856 |
+
assert W.shape[1] == N, f"weight N {W.shape[1]} != dY's N {N}"
|
| 857 |
+
|
| 858 |
+
dX = torch.empty(S, K, device=dY.device, dtype=output_dtype or dY.dtype)
|
| 859 |
+
grid = lambda META: (S, triton.cdiv(K, META["BLOCK_SIZE_K"])) # noqa: E731
|
| 860 |
+
with device_context(dY.device):
|
| 861 |
+
compile_time_only_triton_wrap(dgrad_matmul_batched_kernel)[grid](
|
| 862 |
+
dY, W, Ws, w_global_scale, global_scale_stride(w_global_scale), dX, expert_ids, S, N, K,
|
| 863 |
+
dY.stride(0), dY.stride(1),
|
| 864 |
+
W.stride(0), W.stride(1), W.stride(2),
|
| 865 |
+
Ws.stride(0), Ws.stride(1), Ws.stride(2),
|
| 866 |
+
dX.stride(0), dX.stride(1),
|
| 867 |
+
SCALE_GROUP_K=scale_group_k,
|
| 868 |
+
SCALE_ROW_DIV=scale_row_div,
|
| 869 |
+
WEIGHT_VALUES_PER_BYTE=values_per_byte,
|
| 870 |
+
)
|
| 871 |
+
return dX
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
# ── gradient products, per format family ─────────────────────────────────────────
|
| 876 |
+
# What each format's dgrad actually computes. ``backward.py`` holds only the table saying which
|
| 877 |
+
# op gets which of these — compute lives here, attachment lives there.
|
| 878 |
+
|
| 879 |
+
def _dgrad_mx(dY, B, Bs, b_global, out_dtype):
|
| 880 |
+
"""dA for an MX/NVFP4 weight: contract N on the forward-oriented weight (see backward.py).
|
| 881 |
+
|
| 882 |
+
The scale grid is DERIVED FROM SHAPES, not from a format constant — ``Bs`` is
|
| 883 |
+
``(N // row_div, K // group)``, so the divisors read straight off it. That covers group-32
|
| 884 |
+
UE8M0 (MX) and group-16 E4M3 (NVFP4) without either being named here, and cannot drift when a
|
| 885 |
+
format's block changes.
|
| 886 |
+
|
| 887 |
+
Containers are normalized, not reinterpreted: packed E2M1 reaches the kernel as ``int8`` or
|
| 888 |
+
``uint8`` depending on the producer, and UE8M0 as ``float8_e8m0fnu`` or ``uint8``. The kernel
|
| 889 |
+
keys its unpack/decode off ``uint8``, and decoding UE8M0 bytes as floats is off by orders of
|
| 890 |
+
magnitude — the failure mode is a non-finite dX, which the tuner's numerics veto turns into an
|
| 891 |
+
all-inf tune rather than a wrong answer.
|
| 892 |
+
|
| 893 |
+
The NVFP4 second-level global rides into the kernel, which folds it on the accumulator —
|
| 894 |
+
one value here, a 2D weight being one matrix."""
|
| 895 |
+
if B.dtype == torch.int8:
|
| 896 |
+
B = B.view(torch.uint8)
|
| 897 |
+
if Bs.dtype == torch.float8_e8m0fnu:
|
| 898 |
+
Bs = Bs.view(torch.uint8)
|
| 899 |
+
values_per_byte = 2 if B.dtype == torch.uint8 else 1
|
| 900 |
+
K = B.shape[-1] * values_per_byte
|
| 901 |
+
group = K // Bs.shape[-1]
|
| 902 |
+
row_div = max(B.shape[-2] // Bs.shape[-2], 1)
|
| 903 |
+
dX = dgrad_matmul_2d(
|
| 904 |
+
dY.contiguous(), B, Bs, group, row_div, output_dtype=torch.float32,
|
| 905 |
+
w_global_scale=normalize_global_scale(b_global, 1),
|
| 906 |
+
)
|
| 907 |
+
return dX.to(out_dtype)
|
| 908 |
+
|
| 909 |
+
def _dgrad_transposed_weight(dY, B, Bs, out_dtype):
|
| 910 |
+
"""dA on the QUANTIZED weight, reoriented by transposition. Valid when the scale does not
|
| 911 |
+
bind a group to K: a per-tensor scalar is orientation-free, and a BLOCK grid subdivides both
|
| 912 |
+
axes, so transposing it is the same reindexing as transposing the tile.
|
| 913 |
+
|
| 914 |
+
The scale grid is made CONTIGUOUS, not just transposed: reading it column-major cost 79us on
|
| 915 |
+
a 4096x8192x6144 dgrad (405.7 -> 326.5us) while the copy is ~3KB, because every tile re-reads
|
| 916 |
+
it strided. The weight itself stays a view — materializing it saves a further 47us but costs
|
| 917 |
+
211us to produce, so that only pays when the caller already keeps both orientations."""
|
| 918 |
+
Bs_t = Bs if Bs.numel() == 1 else Bs.t().contiguous()
|
| 919 |
+
return matmul_2d(dY.contiguous(), B.t(), None, Bs_t, output_dtype=out_dtype)
|
| 920 |
+
|
| 921 |
+
def _dgrad_grouped(ctx, dY, B, Bs, expert_start, gather_idx, scatter_idx, b_global):
|
| 922 |
+
"""Routed dgrad. GATE is not a case to handle: the gradient of a gate|up GEMM is an UNGATED
|
| 923 |
+
product at the doubled N extent, which is what a 2N-row weight already is. The forward's two
|
| 924 |
+
row maps are passed through unchanged — the kernel swaps their roles itself. The NVFP4
|
| 925 |
+
global folds in the kernel, which already resolves each tile's expert."""
|
| 926 |
+
if B.dtype == torch.int8:
|
| 927 |
+
B = B.view(torch.uint8)
|
| 928 |
+
if Bs.dtype == torch.float8_e8m0fnu:
|
| 929 |
+
Bs = Bs.view(torch.uint8)
|
| 930 |
+
values_per_byte = 2 if B.dtype == torch.uint8 else 1
|
| 931 |
+
K = B.shape[-1] * values_per_byte
|
| 932 |
+
dX = dgrad_matmul_grouped(
|
| 933 |
+
dY.contiguous(), B, Bs, expert_start,
|
| 934 |
+
K // Bs.shape[-1], max(B.shape[-2] // Bs.shape[-2], 1),
|
| 935 |
+
gather_idx=gather_idx, scatter_idx=scatter_idx, output_dtype=torch.float32,
|
| 936 |
+
num_input_rows=getattr(ctx, "a_rows", None),
|
| 937 |
+
w_global_scale=normalize_global_scale(b_global, B.shape[0]),
|
| 938 |
+
)
|
| 939 |
+
return dX.to(ctx.a_dtype)
|
| 940 |
+
|
| 941 |
+
|
| 942 |
+
__all__: list[str] = []
|
| 943 |
+
|
| 944 |
+
def _register(name: str, saved: tuple[int, ...], dgrad):
|
| 945 |
+
"""Attach a backward to one op. ``saved`` names the positional inputs backward needs — they
|
| 946 |
+
differ per op, e.g. the MX signature is ``(A, B, As, Bs, ...)`` so its weight scale is index
|
| 947 |
+
3, not 2.
|
| 948 |
+
|
| 949 |
+
Everything but the activation gets None — weights and scales take no gradient, the rest is
|
| 950 |
+
non-differentiable config."""
|
| 951 |
+
|
| 952 |
+
def setup_context(ctx, inputs, output):
|
| 953 |
+
ctx.save_for_backward(*(inputs[i] for i in saved))
|
| 954 |
+
ctx.a_dtype = inputs[0].dtype
|
| 955 |
+
# routed dgrad accumulates into SOURCE tokens, which is a different row count from the
|
| 956 |
+
# routed rows the op consumed whenever a gather map is in play
|
| 957 |
+
ctx.a_rows = inputs[0].shape[0]
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
def backward(ctx, *grads):
|
| 961 |
+
# torch matches backward's return against `tree_flatten(args, is_leaf=not_list_of_tensor)`
|
| 962 |
+
# of the args AS INVOKED — which setup_context cannot see (it gets the bound args, with
|
| 963 |
+
# defaults filled). `needs_input_grad` is sized to that same flattened list plus the
|
| 964 |
+
# trailing metadata arg, so it is the one place the right count is observable. torch
|
| 965 |
+
# appends the metadata's own None itself, so this returns exactly that many.
|
| 966 |
+
n_grads = len(ctx.needs_input_grad)
|
| 967 |
+
if not ctx.needs_input_grad[0]:
|
| 968 |
+
return (None,) * n_grads
|
| 969 |
+
# the ops return list[Tensor] (a second entry under output_format), so autograd hands the
|
| 970 |
+
# gradient back in that shape — unwrap to the gradient of the primary output
|
| 971 |
+
dY = grads[0]
|
| 972 |
+
while isinstance(dY, (list, tuple)):
|
| 973 |
+
dY = dY[0]
|
| 974 |
+
return (dgrad(ctx, dY, *ctx.saved_tensors),) + (None,) * (n_grads - 1)
|
| 975 |
+
|
| 976 |
+
torch.library.register_autograd(
|
| 977 |
+
add_op_namespace_prefix(name), backward, setup_context=setup_context
|
| 978 |
+
)
|
| 979 |
+
|
| 980 |
+
|
| 981 |
+
# block-FP8 (128x128 / 1x128, fp32 or UE8M0 scales): the grid subdivides both axes, so dgrad
|
| 982 |
+
# reuses the quantized weight through a transposed view.
|
| 983 |
+
_register(
|
| 984 |
+
"w8a8_block_dynamic_fp8_matmul", saved=(1, 2),
|
| 985 |
+
dgrad=lambda ctx, dY, B, Bs: _dgrad_transposed_weight(dY, B, Bs, ctx.a_dtype),
|
| 986 |
+
)
|
| 987 |
+
|
| 988 |
+
# per-tensor FP8: a scalar scale is orientation-free, so the same route needs no reorientation.
|
| 989 |
+
_register(
|
| 990 |
+
"w8a8_tensor_dynamic_fp8_matmul", saved=(1, 2),
|
| 991 |
+
dgrad=lambda ctx, dY, B, Bs: _dgrad_transposed_weight(dY, B, Bs, ctx.a_dtype),
|
| 992 |
+
)
|
| 993 |
+
|
| 994 |
+
# MX / NVFP4: per-row group scales bind to K and packed E2M1 packs along K, so the weight cannot
|
| 995 |
+
# be reoriented by a VIEW — but it does not need to be. The reduction runs over N against the
|
| 996 |
+
# weight's natural (N, K) tile, dequantized per-tile in-register, so the FORWARD-oriented weight
|
| 997 |
+
# serves dgrad as-is: no transposed copy, no second quantized orientation, no fp32 materialization.
|
| 998 |
+
_register(
|
| 999 |
+
"mx_dynamic_matmul", saved=(1, 3, 13),
|
| 1000 |
+
dgrad=lambda ctx, dY, B, Bs, b_global: _dgrad_mx(dY, B, Bs, b_global, ctx.a_dtype),
|
| 1001 |
+
)
|
| 1002 |
+
|
| 1003 |
+
# Weight-only (W4A16 / W8A16) — the QLoRA format: a frozen 4-bit base weight with raw bf16
|
| 1004 |
+
# activations. Same route; the weight takes no gradient, so nothing here needs a master copy.
|
| 1005 |
+
_register(
|
| 1006 |
+
"mx_weight_only_matmul_2d", saved=(1, 2, 9),
|
| 1007 |
+
dgrad=lambda ctx, dY, B, Bs, b_global: _dgrad_mx(dY, B, Bs, b_global, ctx.a_dtype),
|
| 1008 |
+
)
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def _dgrad_batched(ctx, dY, B, Bs, expert_ids, b_global):
|
| 1012 |
+
"""Per-row-expert dgrad. Same shape-derived scale grid as the others; the only difference from
|
| 1013 |
+
``_dgrad_grouped`` is that the expert comes from a per-row index rather than sorted ranges."""
|
| 1014 |
+
if B.dtype == torch.int8:
|
| 1015 |
+
B = B.view(torch.uint8)
|
| 1016 |
+
if Bs.dtype == torch.float8_e8m0fnu:
|
| 1017 |
+
Bs = Bs.view(torch.uint8)
|
| 1018 |
+
values_per_byte = 2 if B.dtype == torch.uint8 else 1
|
| 1019 |
+
K = B.shape[-1] * values_per_byte
|
| 1020 |
+
dX = dgrad_matmul_batched(
|
| 1021 |
+
dY.contiguous(), B, Bs, expert_ids,
|
| 1022 |
+
K // Bs.shape[-1], max(B.shape[-2] // Bs.shape[-2], 1),
|
| 1023 |
+
output_dtype=torch.float32,
|
| 1024 |
+
w_global_scale=normalize_global_scale(b_global, B.shape[0]),
|
| 1025 |
+
)
|
| 1026 |
+
return dX.to(ctx.a_dtype)
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
# Routed MoE, weight-only — QLoRA on an MoE model. Positional slots: B=1, Bs=2, expert_start=3,
|
| 1030 |
+
# gather_idx=10, scatter_idx=11, b_global_scale=12.
|
| 1031 |
+
_register(
|
| 1032 |
+
"mx_weight_only_matmul_grouped", saved=(1, 2, 3, 10, 11, 12),
|
| 1033 |
+
dgrad=_dgrad_grouped,
|
| 1034 |
+
)
|
| 1035 |
+
|
| 1036 |
+
# Routed MoE, block-FP8. Same product as the weight-only grouped op — a 128x128 block grid is
|
| 1037 |
+
# the per-row K-group layout at wider divisors, which `_dgrad_grouped` derives from `Bs.shape` —
|
| 1038 |
+
# but it is a DIFFERENT op, and only registering the weight-only one left the grouped block-FP8
|
| 1039 |
+
# path with no grad_fn at all. That is the path a PEFT LoRA on fp8 experts actually runs.
|
| 1040 |
+
# Positional slots: B=1, Bs=3, expert_start=4, gather_idx=14, scatter_idx=15; no global scale.
|
| 1041 |
+
_register(
|
| 1042 |
+
"w8a8_block_dynamic_fp8_matmul_grouped", saved=(1, 3, 4, 14, 15),
|
| 1043 |
+
dgrad=lambda ctx, dY, B, Bs, es, gi, si: _dgrad_grouped(ctx, dY, B, Bs, es, gi, si, None),
|
| 1044 |
+
)
|
| 1045 |
+
|
| 1046 |
+
# The batched experts forward is a SELECTABLE dispatch in transformers, not a decode-only path, so
|
| 1047 |
+
# it needs a formula too — without one, finetuning against `batched_mm` silently trains nothing.
|
| 1048 |
+
# Rows are not expert-sorted here: slot 3 is per-row `expert_ids`, not `expert_start`.
|
| 1049 |
+
_register(
|
| 1050 |
+
"mx_weight_only_matmul_batched", saved=(1, 2, 3, 12),
|
| 1051 |
+
dgrad=_dgrad_batched,
|
| 1052 |
+
)
|
build/torch-xpu/batched.py
ADDED
|
@@ -0,0 +1,1947 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import triton
|
| 19 |
+
import triton.language as tl
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
|
| 23 |
+
|
| 24 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 25 |
+
|
| 26 |
+
from .compat import add_op_namespace_prefix, FP8_DTYPE, MX_SCALE_GROUP_K, NIBBLES_PER_BYTE, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, get_accelerator_autotuning_configs, tl_dtype, decode_pdl
|
| 27 |
+
from .descriptors import rebind_batched_mx_bs_descriptor
|
| 28 |
+
from .formats import check_activation_format, global_scale_stride, normalize_global_scale, e2m1_as_uint8, expert_weight_shape, is_mx, mx_scale_family, normalize_per_expert_scale, resolve_activation_format, resolve_output_dtype, ue8m0_as_uint8, validate_dense_operands, weight_block_size, weight_format
|
| 29 |
+
from .epilogue import fused_glu
|
| 30 |
+
from .quant import MX_ACT_QUANT, fp8_act_quant_block_dynamic, fp8_act_quant_tensor_wide
|
| 31 |
+
from .swizzle import swizzled_scale_descriptor
|
| 32 |
+
from .mma import block_dynamic_dot, fp8_dot, mx_compute, mx_weight_only_compute, static_dot
|
| 33 |
+
from .loading.tiles import (
|
| 34 |
+
advance_ptrs,
|
| 35 |
+
load_act_block_dynamic,
|
| 36 |
+
load_act_mx,
|
| 37 |
+
load_act_plain,
|
| 38 |
+
load_act_static,
|
| 39 |
+
load_weight_block_dynamic,
|
| 40 |
+
load_weight_mx,
|
| 41 |
+
load_weight_plain,
|
| 42 |
+
load_weight_static,
|
| 43 |
+
operand_tile_ptrs,
|
| 44 |
+
oriented_tile_ptrs,
|
| 45 |
+
weight_tile_ptrs,
|
| 46 |
+
)
|
| 47 |
+
from .epilogue import acc_finalize, acc_init, add_bias, bias_strides, gemm_epilogue
|
| 48 |
+
from .pruners import PATH_ANCHOR_AXES, fp8_dot_warp_pruner, dot_scaled_staging_pruner, block_fits_dim_pruner, block_within_dim_pruner, compose_pruners, mx_config_pruner, require_moe_dims_aligned, scale_subblock_pruner, smem_pruner, swizzled_scale_config_pruner, weight_only_swap_scope_pruner
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@triton.jit
|
| 52 |
+
def expert_setup(
|
| 53 |
+
A,
|
| 54 |
+
B,
|
| 55 |
+
C,
|
| 56 |
+
Bs,
|
| 57 |
+
ExpertIds,
|
| 58 |
+
GatherIdx,
|
| 59 |
+
ScatterIdx,
|
| 60 |
+
stride_a_m,
|
| 61 |
+
stride_b_e,
|
| 62 |
+
stride_c_m,
|
| 63 |
+
stride_bs_e,
|
| 64 |
+
stride_eid,
|
| 65 |
+
ADVANCE_BS: tl.constexpr = True,
|
| 66 |
+
):
|
| 67 |
+
"""Per-(row, expert) prologue shared by the batched kernels: read the program
|
| 68 |
+
ids, look up the routed expert, and advance the A/B/C/Bs base pointers to this
|
| 69 |
+
row's slice. Returns ``(batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row)`` — the
|
| 70 |
+
resolved source/destination rows fold the gather/scatter out of the kernel bodies.
|
| 71 |
+
|
| 72 |
+
``ADVANCE_BS=False`` leaves ``Bs`` at the buffer base (the mx scale leaf applies the expert
|
| 73 |
+
offset itself — its swizzled path indexes by 128-row block, not the row-major expert stride).
|
| 74 |
+
|
| 75 |
+
``A``'s source row is ``GatherIdx[batch_id]`` when ``GatherIdx`` is not None (the gate_up reading
|
| 76 |
+
unexpanded activations, many-to-one for top_k > 1) else ``batch_id``; ``C``'s destination
|
| 77 |
+
row is ``ScatterIdx[batch_id]`` when ``ScatterIdx`` is not None else ``batch_id`` — the same virtual
|
| 78 |
+
gather/scatter ``matmul_grouped`` does, so the routed rows need no materialized copy.
|
| 79 |
+
|
| 80 |
+
The caller must early-return on the EP sentinel (``expert_id >= num_experts``)
|
| 81 |
+
before any load — the pointer arithmetic itself is harmless, only the loads on a
|
| 82 |
+
non-local expert would be out of bounds."""
|
| 83 |
+
batch_id = tl.program_id(axis=0)
|
| 84 |
+
pid_n = tl.program_id(axis=1)
|
| 85 |
+
# Cast to int64 to prevent overflow on expert_id * stride_b_e.
|
| 86 |
+
expert_id = tl.load(ExpertIds + batch_id * stride_eid).to(tl.int64)
|
| 87 |
+
in_row = tl.load(GatherIdx + batch_id).to(tl.int64) if GatherIdx is not None else batch_id
|
| 88 |
+
out_row = tl.load(ScatterIdx + batch_id).to(tl.int64) if ScatterIdx is not None else batch_id
|
| 89 |
+
A = A + in_row * stride_a_m
|
| 90 |
+
B = B + expert_id * stride_b_e
|
| 91 |
+
C = C + out_row * stride_c_m
|
| 92 |
+
if ADVANCE_BS:
|
| 93 |
+
Bs = Bs + expert_id * stride_bs_e
|
| 94 |
+
return batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@triton.jit
|
| 98 |
+
def store_row(
|
| 99 |
+
C,
|
| 100 |
+
accumulator,
|
| 101 |
+
pid_n,
|
| 102 |
+
stride_c_n,
|
| 103 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 104 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 105 |
+
):
|
| 106 |
+
"""Output epilogue shared by the batched kernels (``C`` already advanced to the
|
| 107 |
+
row). The fake-batch trick aliases all ``BLOCK_SIZE_M`` lanes to the same C row,
|
| 108 |
+
so a plain store would issue ``BLOCK_SIZE_M`` duplicate-address writes — benign on
|
| 109 |
+
NVIDIA WGMMA (last-write-wins of identical bytes) but hardware-undefined on Intel
|
| 110 |
+
XPU, where it corrupts the output. Mask so only lane 0 stores; the accumulator
|
| 111 |
+
rows are mathematically identical (same A row × same B), so lane 0 is correct."""
|
| 112 |
+
c = accumulator.to(C.dtype.element_ty)
|
| 113 |
+
offs_cm = tl.arange(0, BLOCK_SIZE_M)
|
| 114 |
+
offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
|
| 115 |
+
# offs_cm[:, None] * 0: broadcast to a [BM, BN] pointer tile (all rows alias the one C row)
|
| 116 |
+
# so the lane-0 mask below has a row axis to select; the M stride is deliberately 0.
|
| 117 |
+
c_ptrs = C + offs_cm[:, None] * 0 + stride_c_n * offs_cn[None, :]
|
| 118 |
+
tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None])
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@bayesian_autotune(
|
| 122 |
+
get_accelerator_autotuning_configs(swap_ab=True, tune_block_n=True),
|
| 123 |
+
# one winner per (shape, requant): requant narrows the legal tiles to the quant block, so a
|
| 124 |
+
# shared entry could replay a sub-block winner on a launch whose output scale needs the block.
|
| 125 |
+
# GATE stacks a 2*BN-wide dot — a distinct config space that must not share a winner.
|
| 126 |
+
# BLOCK_N/BLOCK_K (the launch-pinned quant block) bound the legal tiles the same way —
|
| 127 |
+
# two checkpoints at one shape with different blocks must not share a winner.
|
| 128 |
+
["N", "K", "S", "OUTPUT_FORMAT", "GATE", "BLOCK_N", "BLOCK_K"],
|
| 129 |
+
n_trials=100,
|
| 130 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 131 |
+
prune_configs_by={
|
| 132 |
+
"early_config_prune": compose_pruners(
|
| 133 |
+
scale_subblock_pruner(),
|
| 134 |
+
fp8_dot_warp_pruner(),
|
| 135 |
+
)
|
| 136 |
+
},
|
| 137 |
+
)
|
| 138 |
+
@triton.jit
|
| 139 |
+
def w8a8_block_dynamic_fp8_matmul_batched_kernel(
|
| 140 |
+
A, # (S, K) E4M3 activations (pre-quantized once by the wrapper)
|
| 141 |
+
As, # (S, K // BLOCK_SIZE_K) fp32 per-row, per-K-block activation scales
|
| 142 |
+
B, # (num_experts, N, K) FP8 weights; under GATE the (num_experts, 2N, K) gate|up stack
|
| 143 |
+
Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (2N under GATE)
|
| 144 |
+
C, # (S, N) output; under an OUTPUT_FORMAT the FP8-requantized intermediate
|
| 145 |
+
Cs, # (S, N // BLOCK_SIZE_N) per-(row, block) output scale; written iff OUTPUT_FORMAT
|
| 146 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 147 |
+
ExpertIds, # (S,) — which expert each batch element routes to
|
| 148 |
+
GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
|
| 149 |
+
ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
|
| 150 |
+
# Shape
|
| 151 |
+
S,
|
| 152 |
+
N,
|
| 153 |
+
K,
|
| 154 |
+
# Strides
|
| 155 |
+
stride_a_m,
|
| 156 |
+
stride_a_k,
|
| 157 |
+
stride_as_m,
|
| 158 |
+
stride_b_e,
|
| 159 |
+
stride_b_k,
|
| 160 |
+
stride_b_n,
|
| 161 |
+
stride_bs_e,
|
| 162 |
+
stride_bs_k,
|
| 163 |
+
stride_bs_n,
|
| 164 |
+
stride_c_m,
|
| 165 |
+
stride_c_n,
|
| 166 |
+
stride_cs_m,
|
| 167 |
+
stride_cs_n,
|
| 168 |
+
stride_bias_e,
|
| 169 |
+
stride_bias_n,
|
| 170 |
+
stride_eid,
|
| 171 |
+
num_experts,
|
| 172 |
+
# Meta-parameters. BLOCK_N/BLOCK_K are the QUANT block (one scale per (BLOCK_N, BLOCK_K) tile of
|
| 173 |
+
# the weight); BLOCK_SIZE_* are the tuned COMPUTE tile. The N tile may subdivide the quant block
|
| 174 |
+
# (BLOCK_SIZE_N <= BLOCK_N); the K tile is the quant block (one scale step per K iteration).
|
| 175 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 176 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 177 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 178 |
+
BLOCK_N: tl.constexpr,
|
| 179 |
+
BLOCK_K: tl.constexpr,
|
| 180 |
+
SWAP_AB: tl.constexpr = False,
|
| 181 |
+
# Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
|
| 182 |
+
GATE: tl.constexpr = False,
|
| 183 |
+
ACT_FN: tl.constexpr = "silu",
|
| 184 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 185 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 186 |
+
# the output format name (None | "fp8")
|
| 187 |
+
OUTPUT_FORMAT: tl.constexpr = None,
|
| 188 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 189 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 190 |
+
PDL: tl.constexpr = False,
|
| 191 |
+
):
|
| 192 |
+
"""Block-scale batched FP8 expert matmul kernel.
|
| 193 |
+
|
| 194 |
+
Each program handles one routed token row and one N-tile, looking up the
|
| 195 |
+
owning expert from ``ExpertIds``. Activations arrive pre-quantized (one wrapper
|
| 196 |
+
pass — an inline quant would repeat per N-tile and pay a per-tile amax reduction).
|
| 197 |
+
|
| 198 |
+
``SWAP_AB`` (tuner axis, M=1 decode): load the weight output-rows-major ``[BN, BK]`` and put
|
| 199 |
+
those rows in the MMA M dim, padding the single token to the N=16 atom; column 0 of the
|
| 200 |
+
``[BN, 16]`` accumulator is the result. No-swap keeps the token in M (padded to 16).
|
| 201 |
+
|
| 202 |
+
``GATE`` fuses the gate|up projection: ``B`` is the ``(E, 2N, K)`` stack (gate rows [0,N),
|
| 203 |
+
interleaved rows), run as two dots (the decode-validated form), SwiGLU-combined, and — under
|
| 204 |
+
an ``OUTPUT_FORMAT`` — FP8-requantized into ``C`` + a per-(row, block) scalar ``Cs``. Every gate arm
|
| 205 |
+
folds out at compile time; ``GATE=False`` is the plain GEMM, bit-identical."""
|
| 206 |
+
if PDL:
|
| 207 |
+
gdc_wait()
|
| 208 |
+
batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
|
| 209 |
+
A,
|
| 210 |
+
B,
|
| 211 |
+
C,
|
| 212 |
+
Bs,
|
| 213 |
+
ExpertIds,
|
| 214 |
+
GatherIdx,
|
| 215 |
+
ScatterIdx,
|
| 216 |
+
stride_a_m,
|
| 217 |
+
stride_b_e,
|
| 218 |
+
stride_c_m,
|
| 219 |
+
stride_bs_e,
|
| 220 |
+
stride_eid,
|
| 221 |
+
)
|
| 222 |
+
# EP sentinel: row routed to a non-local expert; output is left uninit.
|
| 223 |
+
if expert_id >= num_experts:
|
| 224 |
+
return
|
| 225 |
+
|
| 226 |
+
# One scale per quant block, broadcast over the tile, so an N tile narrower than BLOCK_N just
|
| 227 |
+
# reads its own block: tile pid_n sits in block (pid_n * BLOCK_SIZE_N) // BLOCK_N, which is pid_n
|
| 228 |
+
# when the tile IS the block. Narrower tiles multiply the N grid (see scale_subblock_pruner).
|
| 229 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 230 |
+
offs_bn = pid_n * n_width + tl.arange(0, n_width)
|
| 231 |
+
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
| 232 |
+
a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 233 |
+
as_ptrs = As + in_row * stride_as_m + tl.zeros((BLOCK_SIZE_M,), tl.int32)
|
| 234 |
+
# One gate|up weight tile + one block-scale pointer, like every other kernel: the n_width
|
| 235 |
+
# span shares a tile + a single dot, each row's scale block following from its global row.
|
| 236 |
+
b_ptrs = weight_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
|
| 237 |
+
bs_ptr = Bs + (pid_n * n_width // BLOCK_N) * stride_bs_n
|
| 238 |
+
bs_off = ((pid_n * n_width % BLOCK_N + tl.arange(0, n_width)) // BLOCK_N) * stride_bs_n
|
| 239 |
+
acc = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 240 |
+
|
| 241 |
+
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 242 |
+
a, a_s = load_act_block_dynamic(
|
| 243 |
+
a_ptrs, as_ptrs, None, None, 0, 0, 0, 0, 0, "pointer", False, False, False
|
| 244 |
+
)
|
| 245 |
+
w, b_s = load_weight_block_dynamic(
|
| 246 |
+
b_ptrs, b_ptrs, bs_ptr + bs_off, None, 0, 0, 0, 0, 0, 0,
|
| 247 |
+
GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K,
|
| 248 |
+
)
|
| 249 |
+
acc = block_dynamic_dot(acc, a, a_s, w, b_s, BLOCK_SIZE_K, SWAP_AB, False, True)
|
| 250 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptr, _, _ = advance_ptrs(
|
| 251 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptr, b_ptrs, bs_ptr,
|
| 252 |
+
BLOCK_SIZE_K * stride_a_k, 1, BLOCK_SIZE_K * stride_b_k, stride_bs_k,
|
| 253 |
+
"pointer", "pointer", True, True, False,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
if PDL:
|
| 257 |
+
gdc_launch_dependents()
|
| 258 |
+
gemm_epilogue(
|
| 259 |
+
C, Cs, acc, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
|
| 260 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, BLOCK_K,
|
| 261 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 262 |
+
COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
|
| 263 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 264 |
+
global_row=expert_id,
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@bayesian_autotune(
|
| 269 |
+
get_accelerator_autotuning_configs(swap_ab=True, tune_block_n=True),
|
| 270 |
+
# keyed like the block-dynamic sibling: requant narrows the legal tiles to the quant block
|
| 271 |
+
["N", "K", "S", "OUTPUT_FORMAT", "GATE", "BLOCK_N", "BLOCK_K"],
|
| 272 |
+
n_trials=100,
|
| 273 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 274 |
+
prune_configs_by={
|
| 275 |
+
"early_config_prune": compose_pruners(
|
| 276 |
+
scale_subblock_pruner(),
|
| 277 |
+
fp8_dot_warp_pruner(),
|
| 278 |
+
)
|
| 279 |
+
},
|
| 280 |
+
)
|
| 281 |
+
@triton.jit
|
| 282 |
+
def w8a8_block_static_fp8_matmul_batched_kernel(
|
| 283 |
+
A, # (S, K) E4M3 activations (pre-quantized against the static scale by the wrapper)
|
| 284 |
+
As, # scalar — static per-tensor activation scale (calibration-time)
|
| 285 |
+
B, # (num_experts, N, K) FP8 weights; under GATE the (num_experts, 2N, K) gate|up stack
|
| 286 |
+
Bs, # (num_experts, N // BLOCK_SIZE_N, K // BLOCK_SIZE_K) weight scales (2N under GATE)
|
| 287 |
+
C, # (S, N) output; under an OUTPUT_FORMAT the FP8-requantized intermediate
|
| 288 |
+
Cs, # (S, N // BLOCK_SIZE_N) per-(row, block) output scale; written iff OUTPUT_FORMAT
|
| 289 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 290 |
+
ExpertIds, # (S,) — which expert each batch element routes to
|
| 291 |
+
GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
|
| 292 |
+
ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
|
| 293 |
+
# Shape
|
| 294 |
+
S,
|
| 295 |
+
N,
|
| 296 |
+
K,
|
| 297 |
+
# Strides
|
| 298 |
+
stride_a_m,
|
| 299 |
+
stride_a_k,
|
| 300 |
+
stride_b_e,
|
| 301 |
+
stride_b_k,
|
| 302 |
+
stride_b_n,
|
| 303 |
+
stride_bs_e,
|
| 304 |
+
stride_bs_k,
|
| 305 |
+
stride_bs_n,
|
| 306 |
+
stride_c_m,
|
| 307 |
+
stride_c_n,
|
| 308 |
+
stride_cs_m,
|
| 309 |
+
stride_cs_n,
|
| 310 |
+
stride_bias_e,
|
| 311 |
+
stride_bias_n,
|
| 312 |
+
stride_eid,
|
| 313 |
+
num_experts,
|
| 314 |
+
# Meta-parameters
|
| 315 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 316 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 317 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 318 |
+
BLOCK_N: tl.constexpr,
|
| 319 |
+
BLOCK_K: tl.constexpr,
|
| 320 |
+
SWAP_AB: tl.constexpr = False,
|
| 321 |
+
# Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
|
| 322 |
+
GATE: tl.constexpr = False,
|
| 323 |
+
ACT_FN: tl.constexpr = "silu",
|
| 324 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 325 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 326 |
+
OUTPUT_FORMAT: tl.constexpr = None, # None | "fp8" (per-(row, block) requant of the intermediate)
|
| 327 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 328 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 329 |
+
PDL: tl.constexpr = False,
|
| 330 |
+
):
|
| 331 |
+
"""Block-scale batched FP8 expert matmul with a static (per-tensor) activation scale — the
|
| 332 |
+
block-dynamic batched sibling (one program per routed token + N-tile, fake-batch decode,
|
| 333 |
+
``SWAP_AB``, ``GATE`` gate|up fusion) with the 2D ``block_static`` format: ``A`` arrives
|
| 334 |
+
pre-quantized against the calibrated scalar, per-block weight scales apply per-K-tile
|
| 335 |
+
(``accumulate`` ``"static"``, ``FAKE_BATCH``), and the scalar activation scale multiplies the
|
| 336 |
+
accumulator once after the loop. bf16 GLU output only (no fused requant). GATE=False is the plain GEMM."""
|
| 337 |
+
a_s_static = tl.load(As) # per-tensor static activation scale, applied post-loop
|
| 338 |
+
if PDL:
|
| 339 |
+
gdc_wait()
|
| 340 |
+
batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
|
| 341 |
+
A,
|
| 342 |
+
B,
|
| 343 |
+
C,
|
| 344 |
+
Bs,
|
| 345 |
+
ExpertIds,
|
| 346 |
+
GatherIdx,
|
| 347 |
+
ScatterIdx,
|
| 348 |
+
stride_a_m,
|
| 349 |
+
stride_b_e,
|
| 350 |
+
stride_c_m,
|
| 351 |
+
stride_bs_e,
|
| 352 |
+
stride_eid,
|
| 353 |
+
)
|
| 354 |
+
# EP sentinel: row routed to a non-local expert; output is left uninit.
|
| 355 |
+
if expert_id >= num_experts:
|
| 356 |
+
return
|
| 357 |
+
|
| 358 |
+
# the N tile may subdivide the quant block — see the dynamic sibling / scale_subblock_pruner
|
| 359 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 360 |
+
offs_bn = pid_n * n_width + tl.arange(0, n_width)
|
| 361 |
+
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
| 362 |
+
a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 363 |
+
# One gate|up weight tile over the interleaved rows + one block-scale pointer;
|
| 364 |
+
# each weight row's scale block follows from its global row.
|
| 365 |
+
b_ptrs = weight_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
|
| 366 |
+
bs_ptr = Bs + (pid_n * n_width // BLOCK_N) * stride_bs_n
|
| 367 |
+
bs_off = ((pid_n * n_width % BLOCK_N + tl.arange(0, n_width)) // BLOCK_N) * stride_bs_n
|
| 368 |
+
acc = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 369 |
+
|
| 370 |
+
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 371 |
+
a, _ = load_act_static(a_ptrs, 0, 0, 0, None, 0, 0.0, "pointer", False) # pre-quantized E4M3 token (fake-batch replicated)
|
| 372 |
+
w, b_s = load_weight_static(
|
| 373 |
+
b_ptrs, b_ptrs, bs_ptr + bs_off, None, 0, 0, 0, 0, 0, 0,
|
| 374 |
+
GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K,
|
| 375 |
+
)
|
| 376 |
+
acc = static_dot(acc, a, w, b_s, SWAP_AB, BLOCK_SIZE_K, True)
|
| 377 |
+
a_ptrs, _, b_ptrs, bs_ptr, _, _ = advance_ptrs(
|
| 378 |
+
a_ptrs, a_ptrs, b_ptrs, bs_ptr, b_ptrs, bs_ptr,
|
| 379 |
+
BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, stride_bs_k,
|
| 380 |
+
"pointer", "pointer", False, True, False,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
acc = acc * a_s_static
|
| 384 |
+
if PDL:
|
| 385 |
+
gdc_launch_dependents()
|
| 386 |
+
gemm_epilogue(
|
| 387 |
+
C, Cs, acc, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
|
| 388 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, BLOCK_K,
|
| 389 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 390 |
+
COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
|
| 391 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 392 |
+
global_row=expert_id,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
@bayesian_autotune(
|
| 397 |
+
# S (routed rows) keyed like the block-dynamic/mx batched siblings — decode re-tunes per batch.
|
| 398 |
+
get_accelerator_autotuning_configs(tune_block_nk=True, swap_ab=True),
|
| 399 |
+
["N", "K", "S"],
|
| 400 |
+
n_trials=100,
|
| 401 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 402 |
+
# BLOCK_SIZE_K/N are tuned axes; the K-loop is maskless and the N-tile store is
|
| 403 |
+
# row-masked only — veto non-dividing tiles on both.
|
| 404 |
+
prune_configs_by={
|
| 405 |
+
"early_config_prune": compose_pruners(
|
| 406 |
+
fp8_dot_warp_pruner(),
|
| 407 |
+
block_within_dim_pruner("K"),
|
| 408 |
+
block_within_dim_pruner("N", "BLOCK_SIZE_N"),
|
| 409 |
+
)
|
| 410 |
+
},
|
| 411 |
+
)
|
| 412 |
+
@triton.jit
|
| 413 |
+
def w8a8_tensor_dynamic_fp8_matmul_batched_kernel(
|
| 414 |
+
A, # (S, K) pre-quantized FP8 activations
|
| 415 |
+
As, # (S,) per-token activation scales
|
| 416 |
+
B, # (num_experts, N, K) FP8 weight matrices
|
| 417 |
+
Bs, # (num_experts, 1, 1) per-tensor weight scales
|
| 418 |
+
C, # (S, N) output
|
| 419 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 420 |
+
ExpertIds, # (S,) — which expert each batch element routes to
|
| 421 |
+
GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
|
| 422 |
+
ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
|
| 423 |
+
# Shape
|
| 424 |
+
S,
|
| 425 |
+
N,
|
| 426 |
+
K,
|
| 427 |
+
# Strides
|
| 428 |
+
stride_a_m,
|
| 429 |
+
stride_a_k,
|
| 430 |
+
stride_as_m,
|
| 431 |
+
stride_b_e,
|
| 432 |
+
stride_b_k,
|
| 433 |
+
stride_b_n,
|
| 434 |
+
stride_bs_e,
|
| 435 |
+
stride_c_m,
|
| 436 |
+
stride_c_n,
|
| 437 |
+
stride_bias_e,
|
| 438 |
+
stride_bias_n,
|
| 439 |
+
stride_eid,
|
| 440 |
+
num_experts,
|
| 441 |
+
# Meta-parameters
|
| 442 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 443 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 444 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 445 |
+
SWAP_AB: tl.constexpr = False,
|
| 446 |
+
PDL: tl.constexpr = False,
|
| 447 |
+
):
|
| 448 |
+
"""Tensor-scale batched FP8 expert matmul kernel.
|
| 449 |
+
|
| 450 |
+
Activations are already quantized; the kernel applies per-token activation
|
| 451 |
+
scales and per-expert tensor weight scales.
|
| 452 |
+
|
| 453 |
+
``SWAP_AB`` (tuner axis, M=1 decode): weight output rows in the MMA M dim (``B`` as ``[BN, BK]``,
|
| 454 |
+
single token padded to N=16); column 0 of the ``[BN, 16]`` accumulator is the result. Both
|
| 455 |
+
scales are per-token/per-tensor scalars, applied once after the loop, orientation-agnostic."""
|
| 456 |
+
if PDL:
|
| 457 |
+
gdc_wait()
|
| 458 |
+
batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
|
| 459 |
+
A,
|
| 460 |
+
B,
|
| 461 |
+
C,
|
| 462 |
+
Bs,
|
| 463 |
+
ExpertIds,
|
| 464 |
+
GatherIdx,
|
| 465 |
+
ScatterIdx,
|
| 466 |
+
stride_a_m,
|
| 467 |
+
stride_b_e,
|
| 468 |
+
stride_c_m,
|
| 469 |
+
stride_bs_e,
|
| 470 |
+
stride_eid,
|
| 471 |
+
)
|
| 472 |
+
# EP sentinel: row routed to a non-local expert; output is left uninit.
|
| 473 |
+
if expert_id >= num_experts:
|
| 474 |
+
return
|
| 475 |
+
|
| 476 |
+
offs_bn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
|
| 477 |
+
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
| 478 |
+
a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 479 |
+
b_ptrs = oriented_tile_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB)
|
| 480 |
+
b_s = tl.load(Bs)
|
| 481 |
+
a_s = tl.load(As + in_row * stride_as_m)
|
| 482 |
+
|
| 483 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB)
|
| 484 |
+
for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 485 |
+
a, _ = load_act_plain(a_ptrs, 0, 0, 0, None, 0, "pointer", False)
|
| 486 |
+
b, _ = load_weight_plain(
|
| 487 |
+
b_ptrs, b_ptrs, 0, 0, 0, False, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K
|
| 488 |
+
)
|
| 489 |
+
accumulator = accumulator + fp8_dot(a, b, SWAP_AB, BLOCK_SIZE_K)
|
| 490 |
+
a_ptrs, _, b_ptrs, _, _, _ = advance_ptrs(
|
| 491 |
+
a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
|
| 492 |
+
BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
|
| 493 |
+
"pointer", "pointer", False, False, False,
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
accumulator = acc_finalize(accumulator, "dot", BLOCK_SIZE_N, SWAP_AB) * a_s * b_s
|
| 497 |
+
# this split keeps its own store path (ungated, per-tensor dequant) but takes the same bias
|
| 498 |
+
accumulator = add_bias(
|
| 499 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, expert_id, pid_n, BLOCK_SIZE_N
|
| 500 |
+
)
|
| 501 |
+
if PDL:
|
| 502 |
+
gdc_launch_dependents()
|
| 503 |
+
store_row(C, accumulator, pid_n, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N)
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
# The MXFP4/MXFP8 (and packed-activation) splits key themselves — the tuner appends every tensor
|
| 507 |
+
# arg's dtype to its cache key. BLOCK_SIZE_M is always 1 here (per-token decode), so plain `dot`
|
| 508 |
+
# is excluded (only scalar / dot_scaled-swap are emitted); the swapped dot helper stays
|
| 509 |
+
# implemented for future shapes but is not fielded. Swap verdicts are B200 (sm_100) — re-measure
|
| 510 |
+
# on H100 or the target device before inheriting.
|
| 511 |
+
@bayesian_autotune(
|
| 512 |
+
get_accelerator_autotuning_configs(
|
| 513 |
+
mx=True,
|
| 514 |
+
tune_block_nk=True,
|
| 515 |
+
compute_modes=("dot_scaled", "scalar"),
|
| 516 |
+
swap_ab=True,
|
| 517 |
+
pre_hook=rebind_batched_mx_bs_descriptor,
|
| 518 |
+
),
|
| 519 |
+
# ACTIVATION_FORMAT keys the inline act-quant grid: A stays raw bf16 under every
|
| 520 |
+
# format, so the tuner's dtype-appended key can't split W4A8 from W4A4 itself.
|
| 521 |
+
# SWIZZLED_SCALES keys the weight-scale load: it constrains the config space (BK % 128 == 0)
|
| 522 |
+
# and picks a different optimum (full-block descriptor vs the pointer scale), so the swizzled
|
| 523 |
+
# and un-swizzled launches of one shape must not share a tune.
|
| 524 |
+
# GATE keys the tune AND the GATE-conditional prunes (the packed-fp4 silent-zeros
|
| 525 |
+
# fence in mx_config_pruner runs once per key, so a GATE=False-first tune must not
|
| 526 |
+
# hand its winner to a GATE=True launch at the same shape).
|
| 527 |
+
# OUTPUT_FORMAT keys the requant epilogue explicitly (the C/Cs dtype append splits it
|
| 528 |
+
# incidentally today; this kernel's requant Cs is always row-major — no SWIZZLED_OUT axis).
|
| 529 |
+
["N", "K", "S", "ACTIVATION_FORMAT", "SWIZZLED_SCALES", "GATE", "OUTPUT_FORMAT"],
|
| 530 |
+
n_trials=100,
|
| 531 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 532 |
+
# BK-within-K + the sm_10x MMA-shape guards (swapped dot_scaled needs BN >= 128 for the
|
| 533 |
+
# native scaled-MMA; smaller-BN swap configs never win and mislead the TPE).
|
| 534 |
+
prune_configs_by={
|
| 535 |
+
"early_config_prune": compose_pruners(
|
| 536 |
+
mx_config_pruner("K", "N"), swizzled_scale_config_pruner(allow_gate_subblock=True), smem_pruner(),
|
| 537 |
+
dot_scaled_staging_pruner(),
|
| 538 |
+
)
|
| 539 |
+
},
|
| 540 |
+
)
|
| 541 |
+
@triton.jit
|
| 542 |
+
def mx_dynamic_matmul_batched_kernel(
|
| 543 |
+
A, # (S, K) activations: raw BF16/FP16 (inline-quant) or E4M3 (pre-quantized, As set)
|
| 544 |
+
As, # (S, K // SCALE_GROUP_K) UE8M0 act scales; None ⇒ inline-quant, read iff not None
|
| 545 |
+
B, # (num_experts, N, K[/2]); under GATE the (num_experts, 2N, K[/2]) gate|up stack
|
| 546 |
+
Bs, # (num_experts, N, K // SCALE_GROUP_K) UE8M0 weight scales (2N under GATE)
|
| 547 |
+
BSDescriptor, # host TMA descriptor over Bs when SWIZZLED (BN=128 bulk load); dummy otherwise
|
| 548 |
+
C, # (S, N[/2]) output; under an OUTPUT_FORMAT the MX-requantized intermediate
|
| 549 |
+
Cs, # (S, N // SCALE_GROUP_K) UE8M0 output scale; written iff OUTPUT_FORMAT
|
| 550 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 551 |
+
AsGlobal, # (1,) fp32 NVFP4 activation global g_a — SOLELY normalizes the inline raw-A quant (A/g_a); read iff not None
|
| 552 |
+
AsBsGlobal, # (num_experts,) fp32 NVFP4 WEIGHT global g_b — recovers on the accumulator together with AsGlobal (in-register product); read iff not None
|
| 553 |
+
CsGlobal, # (1,) fp32 NVFP4 output global (next proj's provided input_scale); normalizes the requant; read iff not None
|
| 554 |
+
ExpertIds, # (S,) — which expert each routed row uses
|
| 555 |
+
GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
|
| 556 |
+
ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
|
| 557 |
+
# Shape
|
| 558 |
+
S,
|
| 559 |
+
N,
|
| 560 |
+
K,
|
| 561 |
+
# Strides
|
| 562 |
+
stride_a_m,
|
| 563 |
+
stride_a_k,
|
| 564 |
+
stride_as_m,
|
| 565 |
+
stride_b_e,
|
| 566 |
+
stride_b_k,
|
| 567 |
+
stride_b_n,
|
| 568 |
+
stride_bs_e,
|
| 569 |
+
stride_bs_k,
|
| 570 |
+
stride_bs_n,
|
| 571 |
+
stride_c_m,
|
| 572 |
+
stride_c_n,
|
| 573 |
+
stride_cs_m,
|
| 574 |
+
stride_cs_n,
|
| 575 |
+
stride_bias_e,
|
| 576 |
+
stride_bias_n,
|
| 577 |
+
stride_as_global, # expert stride of AsGlobal (0 broadcasts a shared scalar)
|
| 578 |
+
stride_bs_global, # expert stride of AsBsGlobal (``global_scale_stride``)
|
| 579 |
+
stride_cs_global, # expert stride of CsGlobal (0 broadcasts a shared scalar)
|
| 580 |
+
stride_eid,
|
| 581 |
+
num_experts,
|
| 582 |
+
# Meta-parameters
|
| 583 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 584 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 585 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 586 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 587 |
+
COMPUTE_MODE: tl.constexpr,
|
| 588 |
+
SWAP_AB: tl.constexpr = False,
|
| 589 |
+
# Gate|up fusion epilogue (GATE=False -> plain batched GEMM, every arm below folds out)
|
| 590 |
+
GATE: tl.constexpr = False,
|
| 591 |
+
ACT_FN: tl.constexpr = "silu",
|
| 592 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 593 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 594 |
+
# the output format name (None | "mxfp8" | "mxfp4" | "nvfp4")
|
| 595 |
+
OUTPUT_FORMAT: tl.constexpr = None,
|
| 596 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 597 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 598 |
+
ACTIVATION_FORMAT: tl.constexpr = "mxfp8",
|
| 599 |
+
# SWIZZLED_SCALES: Bs arrives pre-swizzled (SWIZZLE_32_4_4) — the checkpoint stores one layout,
|
| 600 |
+
# shared with the grouped (prefill) kernel. Read via load_weight's per-expert scale leaf off the
|
| 601 |
+
# single Bs pointer (+ BSDescriptor for the BN=128 bulk load); un-swizzled Bs takes the affine
|
| 602 |
+
# arm in the same leaf. The op never swizzles — a 3D caller runs un-swizzled at no penalty.
|
| 603 |
+
SWIZZLED_SCALES: tl.constexpr = False,
|
| 604 |
+
PDL: tl.constexpr = False,
|
| 605 |
+
):
|
| 606 |
+
"""Unified batched microscaled expert matmul (MXFP8/MXFP4/NVFP4, W4A8/W4A4) with
|
| 607 |
+
fused act quant.
|
| 608 |
+
|
| 609 |
+
One routed row + one N-tile per program; expert looked up from ``ExpertIds``. ``A`` is
|
| 610 |
+
quantized to E4M3 per K-group inline (UE8M0 scale). The weight dtype picks the
|
| 611 |
+
weight format (2 = packed E2M1 / MXFP4, 1 = unpacked E4M3 / MXFP8); ``COMPUTE_MODE``
|
| 612 |
+
picks ``tl.dot_scaled`` (native M=128) vs the scalar CUDA-core reduce (wins at decode).
|
| 613 |
+
|
| 614 |
+
``SWAP_AB`` (tuner axis, M=1 decode): weight output rows in the MMA M dim (``B`` as ``[BN, BK]``,
|
| 615 |
+
single token padded to N=16); column 0 of the ``[BN, 16]`` accumulator is the result. dot_scaled
|
| 616 |
+
uses the swapped scaled-MMA; scalar reduces over K with the weight output-rows-major.
|
| 617 |
+
"""
|
| 618 |
+
if PDL:
|
| 619 |
+
gdc_wait()
|
| 620 |
+
batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
|
| 621 |
+
A,
|
| 622 |
+
B,
|
| 623 |
+
C,
|
| 624 |
+
Bs,
|
| 625 |
+
ExpertIds,
|
| 626 |
+
GatherIdx,
|
| 627 |
+
ScatterIdx,
|
| 628 |
+
stride_a_m,
|
| 629 |
+
stride_b_e,
|
| 630 |
+
stride_c_m,
|
| 631 |
+
stride_bs_e,
|
| 632 |
+
stride_eid,
|
| 633 |
+
ADVANCE_BS=False, # scale leaf applies the per-expert offset (swizzled indexes by block)
|
| 634 |
+
)
|
| 635 |
+
# EP sentinel: row routed to a non-local expert; output is left uninit.
|
| 636 |
+
if expert_id >= num_experts:
|
| 637 |
+
return
|
| 638 |
+
|
| 639 |
+
# each operand's format is its dtype: uint8 = packed E2M1 (two values per byte, W4A4
|
| 640 |
+
# for A / MXFP4 for B — it also keys the autotune cache), else E4M3
|
| 641 |
+
ACT_VALUES_PER_BYTE: tl.constexpr = 2 if A.dtype.element_ty == tl.uint8 else 1
|
| 642 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr = 2 if B.dtype.element_ty == tl.uint8 else 1
|
| 643 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 644 |
+
# Non-128 N: the partial last N-tile's pointer rows wrap into B (offs_bn % N) so the load
|
| 645 |
+
# never reads past the expert's N rows; the wrapped columns' output is masked off (N_COLS)
|
| 646 |
+
# in the epilogue. Inert when N % BLOCK_SIZE_N == 0 (the affine arm's BN|N veto), so it is
|
| 647 |
+
# load-bearing only for the swizzled arm, whose scale rides pid_n's block index, not offs_bn.
|
| 648 |
+
offs_bn = (pid_n * n_width + tl.arange(0, n_width)) % (2 * N if GATE else N)
|
| 649 |
+
offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 650 |
+
offs_sf = tl.arange(0, BLOCK_SIZE_K // SCALE_GROUP_K)
|
| 651 |
+
offs_ka = tl.arange(0, BLOCK_SIZE_K // ACT_VALUES_PER_BYTE)
|
| 652 |
+
a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_ka, stride_a_m, stride_a_k, "pointer", True)
|
| 653 |
+
# As is not None ⇒ pre-quantized: A is E4M3 + As UE8M0 (the down reading a requantized
|
| 654 |
+
# intermediate). Else A is raw, quantized inline onto ACTIVATION_FORMAT's grid (gate_up / plain —
|
| 655 |
+
# packed E2M1 under fp4, one act row per program so the quant is decode-free); As stays None.
|
| 656 |
+
if As is not None: # build the scale pointers only when the scale is read
|
| 657 |
+
as_ptrs = (
|
| 658 |
+
As
|
| 659 |
+
+ in_row * stride_as_m
|
| 660 |
+
+ tl.arange(0, BLOCK_SIZE_M)[:, None] * 0
|
| 661 |
+
+ offs_sf[None, :]
|
| 662 |
+
)
|
| 663 |
+
else:
|
| 664 |
+
as_ptrs = a_ptrs # dead placeholder so advance_ptrs can take it unconditionally
|
| 665 |
+
# GATE reads one weight tile spanning the interleaved gate|up rows, oriented by
|
| 666 |
+
# SWAP_AB; load_weight reads value + scale (swizzled/un-swizzled hidden) off these pointers.
|
| 667 |
+
b_ptrs = weight_tile_ptrs(
|
| 668 |
+
B, offs_bn, offs_kb, stride_b_n, stride_b_k, SWAP_AB
|
| 669 |
+
)
|
| 670 |
+
accumulator = acc_init(COMPUTE_MODE, BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 671 |
+
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 672 |
+
a, a_scale = load_act_mx(
|
| 673 |
+
a_ptrs, as_ptrs, AsGlobal, expert_id * stride_as_global, None, None, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
| 674 |
+
"pointer", False, False, False,
|
| 675 |
+
BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, ACTIVATION_FORMAT,
|
| 676 |
+
)
|
| 677 |
+
b, b_s = load_weight_mx(
|
| 678 |
+
b_ptrs, b_ptrs, Bs, None, BSDescriptor, Bs, 0, 0, 0, 0, expert_id, pid_n, k, N, K,
|
| 679 |
+
stride_bs_e, stride_bs_n, stride_bs_k,
|
| 680 |
+
GATE, False, True, "pointer", SWAP_AB, SWIZZLED_SCALES,
|
| 681 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
|
| 682 |
+
)
|
| 683 |
+
accumulator = mx_compute(
|
| 684 |
+
accumulator, a, a_scale, b, b_s, COMPUTE_MODE,
|
| 685 |
+
n_width, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB,
|
| 686 |
+
)
|
| 687 |
+
a_ptrs, as_ptrs, b_ptrs, _, _, _ = advance_ptrs(
|
| 688 |
+
a_ptrs, as_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
|
| 689 |
+
(BLOCK_SIZE_K // ACT_VALUES_PER_BYTE) * stride_a_k,
|
| 690 |
+
BLOCK_SIZE_K // SCALE_GROUP_K,
|
| 691 |
+
(BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k,
|
| 692 |
+
0,
|
| 693 |
+
"pointer", "pointer", As is not None, False, False,
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
# NVFP4 two-level: block e4m3 scales rode through the reduce; recover the combined per-tensor
|
| 697 |
+
if PDL:
|
| 698 |
+
gdc_launch_dependents()
|
| 699 |
+
gemm_epilogue(
|
| 700 |
+
C, Cs, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, stride_cs_m, stride_cs_n,
|
| 701 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, SCALE_GROUP_K,
|
| 702 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 703 |
+
COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, FAKE_BATCH=True, N_COLS=N,
|
| 704 |
+
CsGlobal=CsGlobal,
|
| 705 |
+
stride_cs_global=stride_cs_global,
|
| 706 |
+
GlobalScale=AsBsGlobal,
|
| 707 |
+
stride_global_e=stride_bs_global,
|
| 708 |
+
GlobalScaleA=AsGlobal,
|
| 709 |
+
stride_global_a=stride_as_global,
|
| 710 |
+
global_row=expert_id,
|
| 711 |
+
Bias=Bias,
|
| 712 |
+
stride_bias_e=stride_bias_e,
|
| 713 |
+
stride_bias_n=stride_bias_n,
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
@bayesian_autotune(
|
| 718 |
+
# weight-only plain bf16 dot (fp4/fp8 weight upcast per-group in-loop), plus the swapped
|
| 719 |
+
# decode reduce (weight output rows lead the tile, no MMA — see the scope pruner).
|
| 720 |
+
get_accelerator_autotuning_configs(
|
| 721 |
+
tune_block_nk=True, compute_modes=("dot", "dot_scaled", "scalar"), swap_ab=True
|
| 722 |
+
),
|
| 723 |
+
["N", "K", "S", "GATE"],
|
| 724 |
+
n_trials=100,
|
| 725 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 726 |
+
prune_configs_by={
|
| 727 |
+
# No BK-within-K veto: this kernel's tail tile slides back in bounds (see the K-loop),
|
| 728 |
+
# so any BK the shape can hold is legal — that is the point, it lets a K=2880 decode
|
| 729 |
+
# take a 256-byte burst instead of 32. BK must still FIT (the clamp needs K >= BK).
|
| 730 |
+
"early_config_prune": compose_pruners(
|
| 731 |
+
block_fits_dim_pruner("K"),
|
| 732 |
+
block_within_dim_pruner("N", "BLOCK_SIZE_N"),
|
| 733 |
+
mx_config_pruner("K", "N", block_within_k=False), # dot_scaled shape gates
|
| 734 |
+
weight_only_swap_scope_pruner(),
|
| 735 |
+
)
|
| 736 |
+
},
|
| 737 |
+
)
|
| 738 |
+
@triton.jit
|
| 739 |
+
def mx_weight_only_matmul_batched_kernel(
|
| 740 |
+
A, # (rows, K) raw BF16/FP16 activations — NOT quantized
|
| 741 |
+
B, # (num_experts, N, K[/2]) MXFP4/NVFP4/MXFP8 weights; 2N under GATE
|
| 742 |
+
Bs, # (num_experts, N, K // SCALE_GROUP_K) group scales — UE8M0 (MX) or E4M3 (NVFP4)
|
| 743 |
+
BsGlobal, # (num_experts,) fp32 NVFP4 per-expert global — recovers on the accumulator; read iff not None
|
| 744 |
+
C,
|
| 745 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 746 |
+
ExpertIds,
|
| 747 |
+
GatherIdx,
|
| 748 |
+
ScatterIdx,
|
| 749 |
+
S,
|
| 750 |
+
N,
|
| 751 |
+
K,
|
| 752 |
+
stride_a_m,
|
| 753 |
+
stride_a_k,
|
| 754 |
+
stride_b_e,
|
| 755 |
+
stride_b_k,
|
| 756 |
+
stride_b_n,
|
| 757 |
+
stride_bs_e,
|
| 758 |
+
stride_bs_n,
|
| 759 |
+
stride_bs_k,
|
| 760 |
+
stride_c_m,
|
| 761 |
+
stride_c_n,
|
| 762 |
+
stride_bias_e,
|
| 763 |
+
stride_bias_n,
|
| 764 |
+
stride_bs_global, # expert stride of BsGlobal (``global_scale_stride``)
|
| 765 |
+
stride_eid,
|
| 766 |
+
num_experts,
|
| 767 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 768 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 769 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 770 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 771 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 772 |
+
COMPUTE_MODE: tl.constexpr = "dot",
|
| 773 |
+
SWAP_AB: tl.constexpr = False,
|
| 774 |
+
GATE: tl.constexpr = False,
|
| 775 |
+
ACT_FN: tl.constexpr = "silu",
|
| 776 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 777 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 778 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 779 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 780 |
+
PDL: tl.constexpr = False,
|
| 781 |
+
):
|
| 782 |
+
"""weight-only batched (decode) expert matmul: raw bf16 activations against MXFP4/MXFP8 weights upcast
|
| 783 |
+
to bf16 in-loop (unpack + per-group group-scale), plain ``tl.dot``. One routed row + one N-tile
|
| 784 |
+
per program (expert from ``ExpertIds``). Pointer/affine. ``SWAP_AB`` (tuner axis, M=1 decode):
|
| 785 |
+
weight output rows lead the tile and the CUDA-core reduce replaces the MMA. ``GATE`` fuses gate|up."""
|
| 786 |
+
if PDL:
|
| 787 |
+
gdc_wait()
|
| 788 |
+
batch_id, pid_n, expert_id, A, B, C, Bs, in_row, out_row = expert_setup(
|
| 789 |
+
A, B, C, Bs, ExpertIds, GatherIdx, ScatterIdx,
|
| 790 |
+
stride_a_m, stride_b_e, stride_c_m, stride_bs_e, stride_eid, ADVANCE_BS=False,
|
| 791 |
+
)
|
| 792 |
+
if expert_id >= num_experts: # EP sentinel: non-local expert, output left uninit
|
| 793 |
+
return
|
| 794 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 795 |
+
# non-128 N: the last N-tile's rows run past B (N=320, n_width=256 -> tile 2 wants rows
|
| 796 |
+
# 512..767 of 640), so wrap them back into the tensor. The wrapped columns are masked off by
|
| 797 |
+
# the epilogue's N_COLS. An unwrapped read is out of bounds and faults once the allocation
|
| 798 |
+
# layout puts an unmapped page there — invisible in a short run, an illegal access in a long one.
|
| 799 |
+
offs_bn = (pid_n * n_width + tl.arange(0, n_width)) % (2 * N if GATE else N)
|
| 800 |
+
offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 801 |
+
a_ptrs = operand_tile_ptrs(
|
| 802 |
+
A, tl.arange(0, BLOCK_SIZE_M) * 0, tl.arange(0, BLOCK_SIZE_K),
|
| 803 |
+
stride_a_m, stride_a_k, "pointer", True,
|
| 804 |
+
)
|
| 805 |
+
b_ptrs = weight_tile_ptrs(
|
| 806 |
+
B, offs_bn, offs_kb, stride_b_n, stride_b_k, SWAP_AB
|
| 807 |
+
)
|
| 808 |
+
accumulator = acc_init(COMPUTE_MODE, BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 809 |
+
# Each trip indexes off the BASE tile by the scalar ``k_start`` rather than advancing a
|
| 810 |
+
# pointer tile in place: an advanced tile stays live across the loop, and at [2*BN, BK/2]
|
| 811 |
+
# int64 that is ~128 registers per thread of addresses alone — enough to halve resident
|
| 812 |
+
# CTAs and starve the weight stream (measured n_regs 128 with spills vs 55 for the same
|
| 813 |
+
# math indexed from a base). The clamp below already computes ``k_start``, so the walk is
|
| 814 |
+
# the same addresses either way.
|
| 815 |
+
# The K-loop is maskless, which normally pins BK to a divisor of K — leaving a
|
| 816 |
+
# non-power-of-two K (gpt-oss 2880) with BK=64, a 32-byte burst per weight row over 45
|
| 817 |
+
# trips. Instead the LAST tile slides back to end at K (``k_start`` clamp): every load
|
| 818 |
+
# stays in bounds, the tile re-reads positions the previous trip already accumulated, and
|
| 819 |
+
# zeroing those activation lanes cancels them for every arm (a zero operand contributes
|
| 820 |
+
# nothing to the dot, the scaled MMA, or the reduce). BK | K makes the clamp a no-op, so
|
| 821 |
+
# aligned shapes keep today's pointer walk bit-for-bit.
|
| 822 |
+
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 823 |
+
k_start = tl.minimum(k * BLOCK_SIZE_K, K - BLOCK_SIZE_K)
|
| 824 |
+
overlap = k * BLOCK_SIZE_K - k_start
|
| 825 |
+
a, _ = load_act_plain(
|
| 826 |
+
a_ptrs + k_start * stride_a_k, 0, 0, 0, None, 0, "pointer", False
|
| 827 |
+
)
|
| 828 |
+
if overlap > 0: # only the slid tail trip pays the select
|
| 829 |
+
a = tl.where(tl.arange(0, BLOCK_SIZE_K)[None, :] >= overlap, a, 0.0)
|
| 830 |
+
b_k = b_ptrs + (k_start // WEIGHT_VALUES_PER_BYTE) * stride_b_k
|
| 831 |
+
w, w_s = load_weight_mx(
|
| 832 |
+
b_k, b_k, Bs, None, None, Bs, 0, 0, 0, 0, expert_id, pid_n, k, N, K,
|
| 833 |
+
stride_bs_e, stride_bs_n, stride_bs_k,
|
| 834 |
+
GATE, False, True, "pointer", SWAP_AB, False,
|
| 835 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
|
| 836 |
+
k_col_off=-(overlap // SCALE_GROUP_K),
|
| 837 |
+
)
|
| 838 |
+
accumulator = mx_weight_only_compute(
|
| 839 |
+
accumulator, a, w, w_s, COMPUTE_MODE, BLOCK_SIZE_K, n_width, SCALE_GROUP_K,
|
| 840 |
+
SWAP_AB,
|
| 841 |
+
)
|
| 842 |
+
|
| 843 |
+
if PDL:
|
| 844 |
+
gdc_launch_dependents()
|
| 845 |
+
gemm_epilogue(
|
| 846 |
+
C, None, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, 1, 1,
|
| 847 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
|
| 848 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 849 |
+
COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, FAKE_BATCH=True, N_COLS=N,
|
| 850 |
+
GlobalScale=BsGlobal,
|
| 851 |
+
stride_global_e=stride_bs_global,
|
| 852 |
+
global_row=expert_id,
|
| 853 |
+
Bias=Bias,
|
| 854 |
+
stride_bias_e=stride_bias_e,
|
| 855 |
+
stride_bias_n=stride_bias_n,
|
| 856 |
+
)
|
| 857 |
+
|
| 858 |
+
|
| 859 |
+
@bayesian_autotune(
|
| 860 |
+
get_accelerator_autotuning_configs(tune_block_nk=True, swap_ab=True),
|
| 861 |
+
# S (routed rows) keyed like the fp8/mx batched siblings — decode re-tunes per batch;
|
| 862 |
+
# GATE keys the gate|up arm separately (its stacked dot is 2*BN wide).
|
| 863 |
+
["N", "K", "S", "GATE"],
|
| 864 |
+
n_trials=100,
|
| 865 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 866 |
+
# BLOCK_SIZE_K/N are tuned axes; the K-loop is maskless and the N-tile store is
|
| 867 |
+
# row-masked only — veto non-dividing tiles on both.
|
| 868 |
+
prune_configs_by={
|
| 869 |
+
"early_config_prune": compose_pruners(
|
| 870 |
+
block_within_dim_pruner("K"),
|
| 871 |
+
block_within_dim_pruner("N", "BLOCK_SIZE_N"),
|
| 872 |
+
)
|
| 873 |
+
},
|
| 874 |
+
)
|
| 875 |
+
@triton.jit
|
| 876 |
+
def full_precision_matmul_batched_kernel(
|
| 877 |
+
A, # (rows, K) BF16/FP16 activations
|
| 878 |
+
B, # (num_experts, N, K) weights in A's dtype; under GATE the (num_experts, 2N, K) gate|up stack
|
| 879 |
+
C, # (S, N) output; under GATE the GLU intermediate
|
| 880 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 881 |
+
ExpertIds, # (S,) — which expert each batch element routes to
|
| 882 |
+
GatherIdx, # (S,) int — batch_id -> source row of A; read only when not None
|
| 883 |
+
ScatterIdx, # (S,) int — batch_id -> destination row of C; read only when not None
|
| 884 |
+
# Shape
|
| 885 |
+
S,
|
| 886 |
+
N,
|
| 887 |
+
K,
|
| 888 |
+
# Strides
|
| 889 |
+
stride_a_m,
|
| 890 |
+
stride_a_k,
|
| 891 |
+
stride_b_e,
|
| 892 |
+
stride_b_k,
|
| 893 |
+
stride_b_n,
|
| 894 |
+
stride_c_m,
|
| 895 |
+
stride_c_n,
|
| 896 |
+
stride_bias_e,
|
| 897 |
+
stride_bias_n,
|
| 898 |
+
stride_eid,
|
| 899 |
+
num_experts,
|
| 900 |
+
# Meta-parameters
|
| 901 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 902 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 903 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 904 |
+
SWAP_AB: tl.constexpr = False,
|
| 905 |
+
# Gate|up fusion epilogue (GATE=False -> plain batched GEMM). No requant arm: the
|
| 906 |
+
# full-precision chain has no quantized intermediate — down consumes the GLU output as is.
|
| 907 |
+
GATE: tl.constexpr = False,
|
| 908 |
+
ACT_FN: tl.constexpr = "silu",
|
| 909 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 910 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 911 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 912 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 913 |
+
PDL: tl.constexpr = False,
|
| 914 |
+
):
|
| 915 |
+
"""Full-precision batched expert matmul kernel: plain ``tl.dot`` over unquantized
|
| 916 |
+
BF16/FP16 activations and weights, fp32 accumulation, no scales anywhere. ``GATE``
|
| 917 |
+
computes gate|up as ONE stacked tile + dot (straight-line, both orientations) and
|
| 918 |
+
applies the ``ACT_FN``/SwiGLU ``glu``. ``SWAP_AB`` (tuner axis, M=1 decode): weight
|
| 919 |
+
output rows in the MMA M dim, the single token padded to the N=16 atom."""
|
| 920 |
+
if PDL:
|
| 921 |
+
gdc_wait()
|
| 922 |
+
batch_id, pid_n, expert_id, A, B, C, _, in_row, out_row = expert_setup(
|
| 923 |
+
A,
|
| 924 |
+
B,
|
| 925 |
+
C,
|
| 926 |
+
None, # no scales on the full-precision path
|
| 927 |
+
ExpertIds,
|
| 928 |
+
GatherIdx,
|
| 929 |
+
ScatterIdx,
|
| 930 |
+
stride_a_m,
|
| 931 |
+
stride_b_e,
|
| 932 |
+
stride_c_m,
|
| 933 |
+
0,
|
| 934 |
+
stride_eid,
|
| 935 |
+
ADVANCE_BS=False,
|
| 936 |
+
)
|
| 937 |
+
# EP sentinel: row routed to a non-local expert; output is left uninit.
|
| 938 |
+
if expert_id >= num_experts:
|
| 939 |
+
return
|
| 940 |
+
|
| 941 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 942 |
+
offs_bn = pid_n * n_width + tl.arange(0, n_width)
|
| 943 |
+
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
| 944 |
+
a_ptrs = operand_tile_ptrs(A, tl.arange(0, BLOCK_SIZE_M) * 0, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 945 |
+
# GATE reads one weight tile spanning the interleaved gate|up rows;
|
| 946 |
+
# GATE=False -> the plain oriented tile.
|
| 947 |
+
b_ptrs = weight_tile_ptrs(
|
| 948 |
+
B, offs_bn, offs_k, stride_b_n, stride_b_k, SWAP_AB
|
| 949 |
+
)
|
| 950 |
+
|
| 951 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 952 |
+
for _ in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
|
| 953 |
+
a, _ = load_act_plain(a_ptrs, 0, 0, 0, None, 0, "pointer", False)
|
| 954 |
+
w, _ = load_weight_plain(
|
| 955 |
+
b_ptrs, b_ptrs, 0, 0, 0, GATE, False, "pointer", SWAP_AB, BLOCK_SIZE_N, BLOCK_SIZE_K
|
| 956 |
+
)
|
| 957 |
+
accumulator = accumulator + fp8_dot(a, w, SWAP_AB, BLOCK_SIZE_K)
|
| 958 |
+
a_ptrs, _, b_ptrs, _, _, _ = advance_ptrs(
|
| 959 |
+
a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
|
| 960 |
+
BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
|
| 961 |
+
"pointer", "pointer", False, False, False,
|
| 962 |
+
)
|
| 963 |
+
|
| 964 |
+
if PDL:
|
| 965 |
+
gdc_launch_dependents()
|
| 966 |
+
gemm_epilogue(
|
| 967 |
+
C, None, accumulator, out_row, pid_n, 0, out_row, 1, stride_c_n, 1, 1,
|
| 968 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
|
| 969 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 970 |
+
COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, FAKE_BATCH=True,
|
| 971 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 972 |
+
global_row=expert_id,
|
| 973 |
+
)
|
| 974 |
+
|
| 975 |
+
|
| 976 |
+
GATE_UNSTACK_MAX_S = 16 # the unstacked-gate decode band (see the dispatch in the wrapper)
|
| 977 |
+
|
| 978 |
+
|
| 979 |
+
@compile_time_only_triton_op(
|
| 980 |
+
add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul_batched"),
|
| 981 |
+
mutates_args=(),
|
| 982 |
+
opaque=True,
|
| 983 |
+
)
|
| 984 |
+
def w8a8_block_dynamic_fp8_matmul_batched(
|
| 985 |
+
A: torch.Tensor,
|
| 986 |
+
B: torch.Tensor,
|
| 987 |
+
As: torch.Tensor | None,
|
| 988 |
+
Bs: torch.Tensor,
|
| 989 |
+
expert_ids: torch.Tensor,
|
| 990 |
+
block_size: list[int],
|
| 991 |
+
gate: bool = False,
|
| 992 |
+
act_fn: str = "silu",
|
| 993 |
+
swiglu_alpha: float | None = None,
|
| 994 |
+
swiglu_limit: float | None = None,
|
| 995 |
+
simulate_unfused: bool = False,
|
| 996 |
+
activation_format: str | None = None,
|
| 997 |
+
quantize_output: bool = False,
|
| 998 |
+
output_dtype: torch.dtype | None = None,
|
| 999 |
+
gather_idx: torch.Tensor | None = None,
|
| 1000 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1001 |
+
bias: torch.Tensor | None = None,
|
| 1002 |
+
) -> list[torch.Tensor]:
|
| 1003 |
+
"""Block-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T; activations
|
| 1004 |
+
quantized offline in one pass. The ``gate``/``act_fn``/``swiglu_*``/``requant``/``output_dtype``
|
| 1005 |
+
flags are the dispatcher's gate|up fusion kwargs (torch custom ops take only primitives —
|
| 1006 |
+
``matmul_batched`` unpacks the bundle). ``gather_idx``/``scatter_idx`` map the source row of A
|
| 1007 |
+
/ destination row of C per program (None = row s). Returns ``[C]``, or ``[C, Cs]`` under
|
| 1008 |
+
``requant``.
|
| 1009 |
+
|
| 1010 |
+
A: (rows, K) raw bf16/fp16 activations — rows addressed via ``gather_idx``
|
| 1011 |
+
B: (num_experts, N, K) FP8 weights; under ``gate`` the (num_experts, 2N, K) gate|up stack
|
| 1012 |
+
Bs: (num_experts, N // block_n, K // block_k) per-block weight scales (2N under gate)
|
| 1013 |
+
"""
|
| 1014 |
+
validate_dense_operands(A, B)
|
| 1015 |
+
|
| 1016 |
+
output_dtype = resolve_output_dtype(output_dtype, A, As)
|
| 1017 |
+
# S is the routed-row count (one program per expert_id); A may hold fewer rows when
|
| 1018 |
+
# gather_idx maps many programs to one source row (gate_up reading unexpanded hidden).
|
| 1019 |
+
K = A.shape[1]
|
| 1020 |
+
S = expert_ids.shape[0]
|
| 1021 |
+
num_experts, n_rows, N = expert_weight_shape(B, gate)
|
| 1022 |
+
|
| 1023 |
+
assert len(block_size) == 2, (
|
| 1024 |
+
f"block_size must be [block_n, block_k], got {block_size}"
|
| 1025 |
+
)
|
| 1026 |
+
block_n, block_k = block_size[0], block_size[1]
|
| 1027 |
+
require_moe_dims_aligned(N, K, block_n, block_k)
|
| 1028 |
+
assert Bs.shape == (num_experts, n_rows // block_n, K // block_k), (
|
| 1029 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {n_rows // block_n}, {K // block_k})"
|
| 1030 |
+
)
|
| 1031 |
+
|
| 1032 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1033 |
+
# Offline quant wins here even at decode. An inline quant would rerun once per N-tile
|
| 1034 |
+
# of the (S x N-tiles) grid, and block-FP8 quant is an fp32 amax+div per element, so
|
| 1035 |
+
# the redundant work outweighs the extra launch down to T=1 (inline only edges ahead
|
| 1036 |
+
# near T=64). UE8M0 quant is ~free per pass, which is why the MX kernels do it inline.
|
| 1037 |
+
assert activation_format in (None, "fp8"), (
|
| 1038 |
+
f"block-dynamic activations are E4M3 ('fp8'), got {activation_format!r}"
|
| 1039 |
+
)
|
| 1040 |
+
output_format = "fp8" if quantize_output else None
|
| 1041 |
+
requant = output_format is not None
|
| 1042 |
+
# the requantized intermediate's scale groups follow gate_up's block_n, and the
|
| 1043 |
+
# down consumes per-block_k — a non-square block format would misalign them
|
| 1044 |
+
assert not requant or block_size[0] == block_size[1], (
|
| 1045 |
+
f"the fused 'fp8' requant needs square quant blocks, got {block_size}"
|
| 1046 |
+
)
|
| 1047 |
+
# The decode band runs the ``gate`` contract UNSTACKED: this same op without the gate
|
| 1048 |
+
# (one plain GEMM over the stacked weight), then the one-kernel ``fused_glu`` — bit-
|
| 1049 |
+
# identical rounding, and exactly the unfused-reference order, so ``simulate_unfused``
|
| 1050 |
+
# needs no carve-out. Holding the gate AND up tiles per CTA doubles the smem footprint
|
| 1051 |
+
# and halves occupancy precisely where the launch is weight-bandwidth-bound, and the
|
| 1052 |
+
# fp8 ``tl.dot`` — unlike the MX/NVFP4 scaled MMA, whose wide M operand earns the
|
| 1053 |
+
# native instruction — gains nothing back: DSV3 shape, same 235MB weight read, stacked
|
| 1054 |
+
# 69.8µs vs unstacked 49.8µs at S=8, a wash by S=32. A requant is the same offline
|
| 1055 |
+
# quant the raw activation gets below, applied to the GLU output — quantizing the bf16
|
| 1056 |
+
# intermediate (the unfused-reference order) where the stacked epilogue quantizes its
|
| 1057 |
+
# fp32 accumulator, a sub-quantum difference consistent with the band's semantics.
|
| 1058 |
+
if gate and S <= GATE_UNSTACK_MAX_S:
|
| 1059 |
+
[gate_up] = w8a8_block_dynamic_fp8_matmul_batched(
|
| 1060 |
+
A, B, As, Bs, expert_ids, block_size,
|
| 1061 |
+
activation_format=activation_format, output_dtype=output_dtype,
|
| 1062 |
+
gather_idx=gather_idx, scatter_idx=scatter_idx,
|
| 1063 |
+
)
|
| 1064 |
+
out = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit,
|
| 1065 |
+
quant_group=block_n if requant else None,
|
| 1066 |
+
use_ue8m0=bs_u8.dtype == torch.uint8)
|
| 1067 |
+
return list(out) if requant else [out]
|
| 1068 |
+
# A raw (As is None) -> quantize here (offline); else pre-quantized (As given, e.g. the
|
| 1069 |
+
# requantized intermediate handed to the down projection).
|
| 1070 |
+
if As is None:
|
| 1071 |
+
A_q, A_s = fp8_act_quant_block_dynamic(
|
| 1072 |
+
A, block_k, use_ue8m0=bs_u8.dtype == torch.uint8
|
| 1073 |
+
)
|
| 1074 |
+
else:
|
| 1075 |
+
A_q, A_s = A, As
|
| 1076 |
+
if requant:
|
| 1077 |
+
C = A.new_empty(S, N, dtype=FP8_DTYPE)
|
| 1078 |
+
# UE8M0 model (ue8m0 weights) -> UE8M0 intermediate scales (whole-model contract);
|
| 1079 |
+
# the kernel infers the requant format from this dtype. fp32 weights keep fp32.
|
| 1080 |
+
cs_dtype = bs_u8.dtype # uint8 (UE8M0) or float32 — the whole-model scale format
|
| 1081 |
+
Cs = torch.empty(S, N // block_n, device=A.device, dtype=cs_dtype)
|
| 1082 |
+
else:
|
| 1083 |
+
C = A.new_empty(S, N, dtype=output_dtype)
|
| 1084 |
+
Cs = None # unread without an OUTPUT_FORMAT; strides literal below
|
| 1085 |
+
|
| 1086 |
+
# the N tile is tuned (it may subdivide the block scale — see scale_subblock_pruner)
|
| 1087 |
+
grid = lambda meta: (S, triton.cdiv(N, meta["BLOCK_SIZE_N"])) # noqa: E731
|
| 1088 |
+
|
| 1089 |
+
with device_context(A.device):
|
| 1090 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1091 |
+
compile_time_only_triton_wrap(w8a8_block_dynamic_fp8_matmul_batched_kernel)[
|
| 1092 |
+
grid
|
| 1093 |
+
](
|
| 1094 |
+
A_q,
|
| 1095 |
+
A_s,
|
| 1096 |
+
B,
|
| 1097 |
+
bs_u8,
|
| 1098 |
+
C,
|
| 1099 |
+
Cs,
|
| 1100 |
+
bias,
|
| 1101 |
+
expert_ids,
|
| 1102 |
+
gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
|
| 1103 |
+
scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
|
| 1104 |
+
S,
|
| 1105 |
+
N,
|
| 1106 |
+
K,
|
| 1107 |
+
A_q.stride(0),
|
| 1108 |
+
A_q.stride(1),
|
| 1109 |
+
A_s.stride(0),
|
| 1110 |
+
B.stride(0),
|
| 1111 |
+
B.stride(2),
|
| 1112 |
+
B.stride(1),
|
| 1113 |
+
bs_u8.stride(0),
|
| 1114 |
+
bs_u8.stride(2),
|
| 1115 |
+
bs_u8.stride(1),
|
| 1116 |
+
C.stride(0),
|
| 1117 |
+
C.stride(1),
|
| 1118 |
+
Cs.stride(0) if requant else 1,
|
| 1119 |
+
Cs.stride(1) if requant else 1,
|
| 1120 |
+
bias_stride_e,
|
| 1121 |
+
bias_stride_n,
|
| 1122 |
+
expert_ids.stride(0),
|
| 1123 |
+
BLOCK_SIZE_K=block_k,
|
| 1124 |
+
BLOCK_N=block_n,
|
| 1125 |
+
BLOCK_K=block_k,
|
| 1126 |
+
num_experts=num_experts,
|
| 1127 |
+
GATE=gate,
|
| 1128 |
+
ACT_FN=act_fn,
|
| 1129 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1130 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1131 |
+
OUTPUT_FORMAT=output_format,
|
| 1132 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1133 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1134 |
+
PDL=decode_pdl(),
|
| 1135 |
+
launch_pdl=decode_pdl(),
|
| 1136 |
+
)
|
| 1137 |
+
|
| 1138 |
+
return [C, Cs] if requant else [C]
|
| 1139 |
+
|
| 1140 |
+
|
| 1141 |
+
@compile_time_only_triton_op(
|
| 1142 |
+
add_op_namespace_prefix("w8a8_block_static_fp8_matmul_batched"),
|
| 1143 |
+
mutates_args=(),
|
| 1144 |
+
opaque=True,
|
| 1145 |
+
)
|
| 1146 |
+
def w8a8_block_static_fp8_matmul_batched(
|
| 1147 |
+
A: torch.Tensor,
|
| 1148 |
+
B: torch.Tensor,
|
| 1149 |
+
As: torch.Tensor,
|
| 1150 |
+
Bs: torch.Tensor,
|
| 1151 |
+
expert_ids: torch.Tensor,
|
| 1152 |
+
block_size: list[int],
|
| 1153 |
+
gate: bool = False,
|
| 1154 |
+
act_fn: str = "silu",
|
| 1155 |
+
swiglu_alpha: float | None = None,
|
| 1156 |
+
swiglu_limit: float | None = None,
|
| 1157 |
+
simulate_unfused: bool = False,
|
| 1158 |
+
activation_format: str | None = None,
|
| 1159 |
+
quantize_output: bool = False,
|
| 1160 |
+
output_dtype: torch.dtype | None = None,
|
| 1161 |
+
gather_idx: torch.Tensor | None = None,
|
| 1162 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1163 |
+
bias: torch.Tensor | None = None,
|
| 1164 |
+
) -> list[torch.Tensor]:
|
| 1165 |
+
"""Block-scale batched FP8 matmul with a static (per-tensor calibrated) activation scale — the
|
| 1166 |
+
block-dynamic batched sibling with the 2D ``block_static`` format. ``A`` is raw here: the op
|
| 1167 |
+
quantizes it against the scalar ``As`` (offline), the kernel applies the per-block weight scales
|
| 1168 |
+
in the K-loop and the scalar once post-loop. Returns ``[C]``, or ``[C, Cs]`` under
|
| 1169 |
+
``output_format="fp8"`` (the per-row output scale is independent of the per-tensor input scale).
|
| 1170 |
+
|
| 1171 |
+
A: (rows, K) raw bf16/fp16 activations — rows addressed via ``gather_idx``
|
| 1172 |
+
B: (num_experts, N, K) FP8 weights; under ``gate`` the (num_experts, 2N, K) gate|up stack
|
| 1173 |
+
As: scalar / (1,) — the calibrated per-tensor (static) activation scale
|
| 1174 |
+
Bs: (num_experts, N // block_n, K // block_k) per-block weight scales (2N under gate)
|
| 1175 |
+
"""
|
| 1176 |
+
validate_dense_operands(A, B)
|
| 1177 |
+
|
| 1178 |
+
output_dtype = resolve_output_dtype(output_dtype, A, None)
|
| 1179 |
+
K = A.shape[1]
|
| 1180 |
+
S = expert_ids.shape[0]
|
| 1181 |
+
num_experts, n_rows, N = expert_weight_shape(B, gate)
|
| 1182 |
+
|
| 1183 |
+
assert len(block_size) == 2, (
|
| 1184 |
+
f"block_size must be [block_n, block_k], got {block_size}"
|
| 1185 |
+
)
|
| 1186 |
+
block_n, block_k = block_size[0], block_size[1]
|
| 1187 |
+
require_moe_dims_aligned(N, K, block_n, block_k)
|
| 1188 |
+
assert Bs.shape == (num_experts, n_rows // block_n, K // block_k), (
|
| 1189 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, {n_rows // block_n}, {K // block_k})"
|
| 1190 |
+
)
|
| 1191 |
+
assert activation_format in (None, "fp8"), (
|
| 1192 |
+
f"block-static activations are E4M3 ('fp8'), got {activation_format!r}"
|
| 1193 |
+
)
|
| 1194 |
+
output_format = "fp8" if quantize_output else None
|
| 1195 |
+
requant = output_format is not None
|
| 1196 |
+
assert not requant or block_n == block_k, (
|
| 1197 |
+
f"the fused 'fp8' requant needs square quant blocks, got {block_size}"
|
| 1198 |
+
)
|
| 1199 |
+
|
| 1200 |
+
As = As.reshape(1).to(torch.float32)
|
| 1201 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1202 |
+
# Pre-quantize the raw activations against the calibrated scalar (offline; the kernel folds
|
| 1203 |
+
# the scalar back post-loop).
|
| 1204 |
+
A_q = (A.to(torch.float32) / As).to(FP8_DTYPE)
|
| 1205 |
+
if requant:
|
| 1206 |
+
C = A.new_empty(S, N, dtype=FP8_DTYPE)
|
| 1207 |
+
Cs = torch.empty(S, N // block_n, device=A.device, dtype=bs_u8.dtype)
|
| 1208 |
+
else:
|
| 1209 |
+
C = A.new_empty(S, N, dtype=output_dtype)
|
| 1210 |
+
Cs = None # unread without an OUTPUT_FORMAT; strides literal below
|
| 1211 |
+
|
| 1212 |
+
# the N tile is tuned (it may subdivide the block scale — see scale_subblock_pruner)
|
| 1213 |
+
grid = lambda meta: (S, triton.cdiv(N, meta["BLOCK_SIZE_N"])) # noqa: E731
|
| 1214 |
+
|
| 1215 |
+
with device_context(A.device):
|
| 1216 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1217 |
+
compile_time_only_triton_wrap(w8a8_block_static_fp8_matmul_batched_kernel)[
|
| 1218 |
+
grid
|
| 1219 |
+
](
|
| 1220 |
+
A_q,
|
| 1221 |
+
As,
|
| 1222 |
+
B,
|
| 1223 |
+
bs_u8,
|
| 1224 |
+
C,
|
| 1225 |
+
Cs,
|
| 1226 |
+
bias,
|
| 1227 |
+
expert_ids,
|
| 1228 |
+
gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
|
| 1229 |
+
scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
|
| 1230 |
+
S,
|
| 1231 |
+
N,
|
| 1232 |
+
K,
|
| 1233 |
+
A_q.stride(0),
|
| 1234 |
+
A_q.stride(1),
|
| 1235 |
+
B.stride(0),
|
| 1236 |
+
B.stride(2),
|
| 1237 |
+
B.stride(1),
|
| 1238 |
+
bs_u8.stride(0),
|
| 1239 |
+
bs_u8.stride(2),
|
| 1240 |
+
bs_u8.stride(1),
|
| 1241 |
+
C.stride(0),
|
| 1242 |
+
C.stride(1),
|
| 1243 |
+
Cs.stride(0) if requant else 1,
|
| 1244 |
+
Cs.stride(1) if requant else 1,
|
| 1245 |
+
bias_stride_e,
|
| 1246 |
+
bias_stride_n,
|
| 1247 |
+
expert_ids.stride(0),
|
| 1248 |
+
num_experts,
|
| 1249 |
+
BLOCK_SIZE_K=block_k,
|
| 1250 |
+
BLOCK_N=block_n,
|
| 1251 |
+
BLOCK_K=block_k,
|
| 1252 |
+
GATE=gate,
|
| 1253 |
+
ACT_FN=act_fn,
|
| 1254 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1255 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1256 |
+
OUTPUT_FORMAT=output_format,
|
| 1257 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1258 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1259 |
+
PDL=decode_pdl(),
|
| 1260 |
+
launch_pdl=decode_pdl(),
|
| 1261 |
+
)
|
| 1262 |
+
|
| 1263 |
+
return [C, Cs] if requant else [C]
|
| 1264 |
+
|
| 1265 |
+
|
| 1266 |
+
@compile_time_only_triton_op(
|
| 1267 |
+
add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul_batched"),
|
| 1268 |
+
mutates_args=(),
|
| 1269 |
+
opaque=True,
|
| 1270 |
+
)
|
| 1271 |
+
def w8a8_tensor_dynamic_fp8_matmul_batched(
|
| 1272 |
+
A: torch.Tensor,
|
| 1273 |
+
B: torch.Tensor,
|
| 1274 |
+
As: torch.Tensor | None,
|
| 1275 |
+
Bs: torch.Tensor,
|
| 1276 |
+
expert_ids: torch.Tensor,
|
| 1277 |
+
output_dtype: torch.dtype | None = None,
|
| 1278 |
+
gather_idx: torch.Tensor | None = None,
|
| 1279 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1280 |
+
bias: torch.Tensor | None = None,
|
| 1281 |
+
) -> torch.Tensor:
|
| 1282 |
+
"""Tensor-scale batched FP8 matmul: C[s] = A[s] @ B[expert_ids[s]].T. ``A`` raw
|
| 1283 |
+
(``As`` None) -> quantized here (offline, per-token); else pre-quantized (``As`` given).
|
| 1284 |
+
``gather_idx``/``scatter_idx`` map the source row of A / destination row of C per program
|
| 1285 |
+
(None = row s).
|
| 1286 |
+
|
| 1287 |
+
A: (rows, K) raw or pre-quantized FP8 activations — rows addressed via ``gather_idx``
|
| 1288 |
+
B: (num_experts, N, K) FP8 expert weights
|
| 1289 |
+
As: (rows,) per-token scales, or None when A is raw
|
| 1290 |
+
Bs: (num_experts,) or (num_experts, 1, 1) per-expert weight scales
|
| 1291 |
+
"""
|
| 1292 |
+
validate_dense_operands(A, B)
|
| 1293 |
+
|
| 1294 |
+
output_dtype = resolve_output_dtype(output_dtype, A, As)
|
| 1295 |
+
K = A.shape[1]
|
| 1296 |
+
S = expert_ids.shape[0]
|
| 1297 |
+
num_experts, N, _ = B.shape
|
| 1298 |
+
|
| 1299 |
+
# Normalize Bs to (num_experts, 1, 1)
|
| 1300 |
+
Bs = normalize_per_expert_scale(Bs, num_experts)
|
| 1301 |
+
|
| 1302 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1303 |
+
if As is None:
|
| 1304 |
+
qA, As = fp8_act_quant_tensor_wide(A, K)
|
| 1305 |
+
else:
|
| 1306 |
+
qA = A
|
| 1307 |
+
C = A.new_empty(S, N, dtype=output_dtype)
|
| 1308 |
+
|
| 1309 |
+
def grid(META):
|
| 1310 |
+
return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
|
| 1311 |
+
|
| 1312 |
+
with device_context(A.device):
|
| 1313 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1314 |
+
compile_time_only_triton_wrap(w8a8_tensor_dynamic_fp8_matmul_batched_kernel)[
|
| 1315 |
+
grid
|
| 1316 |
+
](
|
| 1317 |
+
qA,
|
| 1318 |
+
As,
|
| 1319 |
+
B,
|
| 1320 |
+
bs_u8,
|
| 1321 |
+
C,
|
| 1322 |
+
bias,
|
| 1323 |
+
expert_ids,
|
| 1324 |
+
gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
|
| 1325 |
+
scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
|
| 1326 |
+
S,
|
| 1327 |
+
N,
|
| 1328 |
+
K,
|
| 1329 |
+
qA.stride(0),
|
| 1330 |
+
qA.stride(1),
|
| 1331 |
+
As.stride(0),
|
| 1332 |
+
B.stride(0),
|
| 1333 |
+
B.stride(2),
|
| 1334 |
+
B.stride(1),
|
| 1335 |
+
bs_u8.stride(0),
|
| 1336 |
+
C.stride(0),
|
| 1337 |
+
C.stride(1),
|
| 1338 |
+
bias_stride_e,
|
| 1339 |
+
bias_stride_n,
|
| 1340 |
+
expert_ids.stride(0),
|
| 1341 |
+
num_experts=num_experts,
|
| 1342 |
+
PDL=decode_pdl(),
|
| 1343 |
+
launch_pdl=decode_pdl(),
|
| 1344 |
+
)
|
| 1345 |
+
|
| 1346 |
+
return C
|
| 1347 |
+
|
| 1348 |
+
|
| 1349 |
+
@compile_time_only_triton_op(
|
| 1350 |
+
add_op_namespace_prefix("mx_dynamic_matmul_batched"), mutates_args=(), opaque=True
|
| 1351 |
+
)
|
| 1352 |
+
def mx_dynamic_matmul_batched(
|
| 1353 |
+
A: torch.Tensor,
|
| 1354 |
+
B: torch.Tensor,
|
| 1355 |
+
As: torch.Tensor | None,
|
| 1356 |
+
Bs: torch.Tensor,
|
| 1357 |
+
expert_ids: torch.Tensor,
|
| 1358 |
+
gate: bool = False,
|
| 1359 |
+
act_fn: str = "silu",
|
| 1360 |
+
swiglu_alpha: float | None = None,
|
| 1361 |
+
swiglu_limit: float | None = None,
|
| 1362 |
+
simulate_unfused: bool = False,
|
| 1363 |
+
activation_format: str | None = None,
|
| 1364 |
+
quantize_output: bool = False,
|
| 1365 |
+
output_dtype: torch.dtype | None = None,
|
| 1366 |
+
gather_idx: torch.Tensor | None = None,
|
| 1367 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1368 |
+
a_global_scale: torch.Tensor | None = None,
|
| 1369 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1370 |
+
output_global_scale: torch.Tensor | None = None,
|
| 1371 |
+
bias: torch.Tensor | None = None,
|
| 1372 |
+
) -> list[torch.Tensor]:
|
| 1373 |
+
"""Batched MX matmul ``C[s] = A[s] @ B[expert_ids[s]].T``; activations quantized
|
| 1374 |
+
inline in the kernel (decode: one act row per program, inline is free). The
|
| 1375 |
+
``gate``/``act_fn``/``swiglu_*``/``requant``/``output_dtype`` flags are ``matmul_batched``'s
|
| 1376 |
+
gate|up fusion kwargs. ``gather_idx``/``scatter_idx`` map the
|
| 1377 |
+
source row of A / destination row of C per program (None = row s). Returns ``[C]``, or
|
| 1378 |
+
``[C, Cs]`` under ``requant``. Weight format is detected from ``B.dtype``: ``int8`` → packed
|
| 1379 |
+
E2M1 (MXFP4, ``B`` is ``(num_experts, N, K//2)``); ``float8_e4m3fn`` → unpacked E4M3 (MXFP8);
|
| 1380 |
+
both use UE8M0 group-32 scales; under ``gate`` the (num_experts, 2N, K[/2]) gate|up stack.
|
| 1381 |
+
|
| 1382 |
+
A: (rows, K) activations — raw bf16/fp16/fp32 (inline-quant) or pre-quantized E4M3
|
| 1383 |
+
expert_ids: (S,) which expert each routed row uses
|
| 1384 |
+
"""
|
| 1385 |
+
assert A.ndim == 2 and B.ndim == 3 and Bs.ndim in (3, 5) # 5D = pre-swizzled
|
| 1386 |
+
assert expert_ids.ndim == 1
|
| 1387 |
+
# A raw (As None) -> quantized inline in the kernel (decode-free UE8M0); pre-quantized
|
| 1388 |
+
# (As given, e.g. the down reading a requantized intermediate) -> loaded with its scales
|
| 1389 |
+
# (the kernel folds on As is None).
|
| 1390 |
+
# the kernel quantizes raw A inline on this grid (fp4 formats pack in-register);
|
| 1391 |
+
# NVFP4 batched runs on the software arms — decode grid BM <= 16 < the native
|
| 1392 |
+
# mxf4nvf4 M=128 staging (scalar / swap-scalar column-unpack + E4M3 scale decode)
|
| 1393 |
+
activation_format = resolve_activation_format(activation_format, B, Bs)
|
| 1394 |
+
output_format = activation_format if quantize_output else None
|
| 1395 |
+
requant = quantize_output
|
| 1396 |
+
if As is not None:
|
| 1397 |
+
assert (As.dtype == torch.float8_e4m3fn) == (Bs.dtype == torch.float8_e4m3fn), (
|
| 1398 |
+
f"activation scales ({As.dtype}) must match the weight scale family ({Bs.dtype})"
|
| 1399 |
+
)
|
| 1400 |
+
pre_quantized = As is not None
|
| 1401 |
+
assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
|
| 1402 |
+
f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
|
| 1403 |
+
)
|
| 1404 |
+
WEIGHT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
|
| 1405 |
+
# NVFP4 raw activations pre-quantize HERE, above the layout derivation below — the quant
|
| 1406 |
+
# packs A to E2M1, so ACT_VALUES_PER_BYTE and K must be read off the packed tensor. Offline
|
| 1407 |
+
# beats the inline arm at every M for NVFP4 (unlike the UE8M0 formats, whose exponent-only
|
| 1408 |
+
# inline quant is free): its E4M3 block scale costs an amax divide plus a global normalize
|
| 1409 |
+
# per group, and the decode grid re-runs that in every N-tile program against the SAME
|
| 1410 |
+
# routed row. Measured bit-identical, GLM decode gate_up 34.2 -> 31.9us, down 18.9 -> 16.9us.
|
| 1411 |
+
# The act scale stays affine under a swizzled weight: load_act_mx reads it off as_ptrs
|
| 1412 |
+
# row-major, SWIZZLED_SCALES governs only the weight side.
|
| 1413 |
+
if As is None and activation_format == "nvfp4" and A.dtype not in (torch.int8, torch.float8_e4m3fn):
|
| 1414 |
+
# a per-expert g_a quantizes each routed row against ITS expert's global; the rows are
|
| 1415 |
+
# routed slots, so the row -> expert map is this op's own `expert_ids`
|
| 1416 |
+
A, As = MX_ACT_QUANT["nvfp4"](
|
| 1417 |
+
A,
|
| 1418 |
+
global_scale=normalize_global_scale(a_global_scale, B.shape[0]),
|
| 1419 |
+
expert_index=expert_ids,
|
| 1420 |
+
)
|
| 1421 |
+
pre_quantized = True
|
| 1422 |
+
# int8 A = caller-provided packed-E2M1 activations (W4A4, native mxf4 MMA): K is two
|
| 1423 |
+
# values per stored byte and the scales are mandatory (nothing left to quantize).
|
| 1424 |
+
ACT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if A.dtype == torch.int8 else 1
|
| 1425 |
+
if ACT_VALUES_PER_BYTE == NIBBLES_PER_BYTE:
|
| 1426 |
+
assert As is not None, "packed-E2M1 activations need their UE8M0 scales (As)"
|
| 1427 |
+
|
| 1428 |
+
output_dtype = resolve_output_dtype(output_dtype, A, As)
|
| 1429 |
+
K = A.shape[1] * ACT_VALUES_PER_BYTE
|
| 1430 |
+
S = expert_ids.shape[0]
|
| 1431 |
+
num_experts, n_rows, N = expert_weight_shape(B, gate)
|
| 1432 |
+
K_b = B.shape[2]
|
| 1433 |
+
assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
|
| 1434 |
+
f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[2] (={K_b})"
|
| 1435 |
+
)
|
| 1436 |
+
# Bs arrives either row-major (num_experts, n_rows, K // scale_group) — read affine — or
|
| 1437 |
+
# already SWIZZLE_32_4_4 (5D: 1, num_experts * n_rows // 128, cols // 4, 2, 256), the shared
|
| 1438 |
+
# checkpoint layout swizzled once at model load (the deployment contract, no per-call
|
| 1439 |
+
# rearrange). The format is the scale dtype (E4M3 = NVFP4 group-16, UE8M0 = MX group-32); the
|
| 1440 |
+
# swizzled cols encode (K // scale_group) // 4.
|
| 1441 |
+
swizzled_scales = Bs.ndim == 5
|
| 1442 |
+
scale_group = mx_scale_family(Bs, K)
|
| 1443 |
+
if not swizzled_scales:
|
| 1444 |
+
assert Bs.shape == (num_experts, n_rows, K // scale_group), (
|
| 1445 |
+
f"Bs shape {tuple(Bs.shape)} != ({num_experts}, {n_rows}, {K // scale_group})"
|
| 1446 |
+
)
|
| 1447 |
+
else:
|
| 1448 |
+
# the artifact's expert count and per-expert row blocks must match THIS weight stack — a
|
| 1449 |
+
# wrong-layer artifact with a matching K would otherwise dequantize with garbage scales silently
|
| 1450 |
+
assert Bs.shape[:2] == (num_experts, triton.cdiv(n_rows, 128)), (
|
| 1451 |
+
f"swizzled Bs carries {tuple(Bs.shape[:2])} (experts, 128-row blocks), expected "
|
| 1452 |
+
f"{(num_experts, triton.cdiv(n_rows, 128))} for ({n_rows}, K) weights — wrong artifact"
|
| 1453 |
+
)
|
| 1454 |
+
|
| 1455 |
+
a_u8 = e2m1_as_uint8(A)
|
| 1456 |
+
as_u8 = ue8m0_as_uint8(As) # None when raw (A quantized inline)
|
| 1457 |
+
b_u8 = e2m1_as_uint8(B)
|
| 1458 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1459 |
+
# The op never swizzles: Bs is read in whatever layout it arrives (format-agnostic — MX or
|
| 1460 |
+
# NVFP4). A pre-swizzled SWIZZLE_32_4_4 Bs (5D, the shared checkpoint layout the grouped kernel
|
| 1461 |
+
# also consumes) takes the fast descriptor/gather path; a row-major (3D) Bs takes the affine
|
| 1462 |
+
# path at no penalty. Callers swizzle once at load (public swizzle_mx_scales) to opt into perf.
|
| 1463 |
+
# The descriptor is built only on the swizzled path — the un-swizzled arm never reads it (None).
|
| 1464 |
+
bs_descriptor = swizzled_scale_descriptor(bs_u8) if swizzled_scales else None
|
| 1465 |
+
# Requant scales are written ROW-MAJOR (never SWIZZLE_32_4_4), unlike the grouped/2D
|
| 1466 |
+
# requant which fuse the swizzle in-epilogue. This is deliberate, not a gap: batched is the
|
| 1467 |
+
# decode kernel — one distinct routed row per program (FAKE_BATCH replicates it across the BM
|
| 1468 |
+
# lanes), so it never forms the 128-distinct-row MMA tile the tcgen05 swizzled scaled-MMA
|
| 1469 |
+
# fast path needs. Swizzled scales give decode no speedup, and the 128-row swizzle block
|
| 1470 |
+
# can't be written from a one-row-per-program grid without cross-program collisions. The
|
| 1471 |
+
# down projection reads this row-major intermediate directly (its As is row-major too).
|
| 1472 |
+
if output_format in ("mxfp4", "nvfp4"):
|
| 1473 |
+
# packed E2M1 intermediate (nibble pairs along N) + group scales (UE8M0 for MX,
|
| 1474 |
+
# E4M3 for NVFP4) — feeds a W4A4 down as-is
|
| 1475 |
+
assert N % (2 * scale_group) == 0, (
|
| 1476 |
+
f"N (={N}) must be a multiple of {2 * scale_group} to pack E2M1 pairs"
|
| 1477 |
+
)
|
| 1478 |
+
C = a_u8.new_empty((S, N // 2), dtype=torch.int8)
|
| 1479 |
+
Cs = torch.empty(
|
| 1480 |
+
S,
|
| 1481 |
+
N // scale_group,
|
| 1482 |
+
device=a_u8.device,
|
| 1483 |
+
dtype=bs_u8.dtype, # UE8M0 -> uint8, NVFP4 -> e4m3 (the binder-safe weight-scale dtype)
|
| 1484 |
+
)
|
| 1485 |
+
elif requant:
|
| 1486 |
+
C = a_u8.new_empty((S, N), dtype=FP8_DTYPE)
|
| 1487 |
+
Cs = torch.empty(S, N // MX_SCALE_GROUP_K, device=a_u8.device, dtype=torch.uint8)
|
| 1488 |
+
else:
|
| 1489 |
+
C = a_u8.new_empty((S, N), dtype=output_dtype)
|
| 1490 |
+
Cs = None # unread without an OUTPUT_FORMAT; strides literal below
|
| 1491 |
+
|
| 1492 |
+
def grid(META):
|
| 1493 |
+
return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
|
| 1494 |
+
|
| 1495 |
+
# NVFP4 accumulator correction: the per-expert g_a·g_b product folded onto the fp32 accumulator.
|
| 1496 |
+
# g_b per expert and g_a scalar go down SEPARATELY; the kernel multiplies them in-register
|
| 1497 |
+
# (the old host-side g_a*g_b product was a torch launch per GEMM per call)
|
| 1498 |
+
input_global_scale = normalize_global_scale(b_global_scale, B.shape[0])
|
| 1499 |
+
act_global_scale = normalize_global_scale(a_global_scale, B.shape[0])
|
| 1500 |
+
out_global_scale = normalize_global_scale(output_global_scale, B.shape[0])
|
| 1501 |
+
with device_context(a_u8.device):
|
| 1502 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1503 |
+
compile_time_only_triton_wrap(mx_dynamic_matmul_batched_kernel)[grid](
|
| 1504 |
+
a_u8,
|
| 1505 |
+
as_u8, # None when raw (A quantized inline)
|
| 1506 |
+
b_u8,
|
| 1507 |
+
bs_u8,
|
| 1508 |
+
bs_descriptor,
|
| 1509 |
+
C,
|
| 1510 |
+
Cs,
|
| 1511 |
+
bias,
|
| 1512 |
+
act_global_scale, # AsGlobal: g_a per tensor or per expert (acc, and the inline-quant arm's A/g_a)
|
| 1513 |
+
input_global_scale, # AsBsGlobal = g_b per expert (acc; g_a folds in-kernel via AsGlobal)
|
| 1514 |
+
out_global_scale, # CsGlobal: requant output normalization (next proj's provided input_scale); None folds out
|
| 1515 |
+
expert_ids,
|
| 1516 |
+
gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
|
| 1517 |
+
scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
|
| 1518 |
+
S,
|
| 1519 |
+
N,
|
| 1520 |
+
K,
|
| 1521 |
+
a_u8.stride(0),
|
| 1522 |
+
a_u8.stride(1),
|
| 1523 |
+
as_u8.stride(0) if pre_quantized else 1,
|
| 1524 |
+
b_u8.stride(0),
|
| 1525 |
+
b_u8.stride(2),
|
| 1526 |
+
b_u8.stride(1),
|
| 1527 |
+
bs_u8.stride(0),
|
| 1528 |
+
bs_u8.stride(2),
|
| 1529 |
+
bs_u8.stride(1),
|
| 1530 |
+
C.stride(0),
|
| 1531 |
+
C.stride(1),
|
| 1532 |
+
Cs.stride(0) if requant else 1,
|
| 1533 |
+
Cs.stride(1) if requant else 1,
|
| 1534 |
+
bias_stride_e,
|
| 1535 |
+
bias_stride_n,
|
| 1536 |
+
global_scale_stride(act_global_scale),
|
| 1537 |
+
global_scale_stride(input_global_scale),
|
| 1538 |
+
global_scale_stride(out_global_scale),
|
| 1539 |
+
expert_ids.stride(0),
|
| 1540 |
+
SCALE_GROUP_K=scale_group,
|
| 1541 |
+
num_experts=num_experts,
|
| 1542 |
+
SWIZZLED_SCALES=swizzled_scales,
|
| 1543 |
+
ACTIVATION_FORMAT=activation_format,
|
| 1544 |
+
GATE=gate,
|
| 1545 |
+
ACT_FN=act_fn,
|
| 1546 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1547 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1548 |
+
OUTPUT_FORMAT=output_format,
|
| 1549 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1550 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1551 |
+
PDL=decode_pdl(),
|
| 1552 |
+
launch_pdl=decode_pdl(),
|
| 1553 |
+
)
|
| 1554 |
+
return [C, Cs] if requant else [C]
|
| 1555 |
+
|
| 1556 |
+
|
| 1557 |
+
@compile_time_only_triton_op(
|
| 1558 |
+
add_op_namespace_prefix("full_precision_matmul_batched"),
|
| 1559 |
+
mutates_args=(),
|
| 1560 |
+
opaque=True,
|
| 1561 |
+
)
|
| 1562 |
+
def full_precision_matmul_batched(
|
| 1563 |
+
A: torch.Tensor,
|
| 1564 |
+
B: torch.Tensor,
|
| 1565 |
+
expert_ids: torch.Tensor,
|
| 1566 |
+
gate: bool = False,
|
| 1567 |
+
act_fn: str = "silu",
|
| 1568 |
+
swiglu_alpha: float | None = None,
|
| 1569 |
+
swiglu_limit: float | None = None,
|
| 1570 |
+
simulate_unfused: bool = False,
|
| 1571 |
+
activation_format: str | None = None,
|
| 1572 |
+
quantize_output: bool = False,
|
| 1573 |
+
output_dtype: torch.dtype | None = None,
|
| 1574 |
+
gather_idx: torch.Tensor | None = None,
|
| 1575 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1576 |
+
bias: torch.Tensor | None = None,
|
| 1577 |
+
) -> list[torch.Tensor]:
|
| 1578 |
+
"""Full-precision (BF16/FP16) batched matmul: C[s] = A[s] @ B[expert_ids[s]].T — no
|
| 1579 |
+
quantization anywhere, fp32 accumulation. ``gate``/``act_fn``/``swiglu_*``/
|
| 1580 |
+
``simulate_unfused`` are the dispatcher's gate|up fusion kwargs (GLU only; ``requant`` is
|
| 1581 |
+
meaningless without a quantized format). ``gather_idx``/``scatter_idx`` map the source
|
| 1582 |
+
row of A / destination row of C per program (None = row s).
|
| 1583 |
+
|
| 1584 |
+
A: (rows, K) BF16/FP16 activations — rows addressed via ``gather_idx``
|
| 1585 |
+
B: (num_experts, N, K) expert weights in A's dtype; under ``gate`` the (num_experts, 2N, K) stack
|
| 1586 |
+
"""
|
| 1587 |
+
validate_dense_operands(A, B)
|
| 1588 |
+
assert A.dtype == B.dtype and A.dtype in (torch.bfloat16, torch.float16), (
|
| 1589 |
+
f"full-precision path needs matching BF16/FP16 A and B, got {A.dtype} / {B.dtype}"
|
| 1590 |
+
)
|
| 1591 |
+
assert activation_format in (None, "bf16") and not quantize_output, (
|
| 1592 |
+
"the full-precision path quantizes nothing — no quantized activation format or requant applies"
|
| 1593 |
+
)
|
| 1594 |
+
|
| 1595 |
+
output_dtype = resolve_output_dtype(output_dtype, A, None)
|
| 1596 |
+
K = A.shape[1]
|
| 1597 |
+
S = expert_ids.shape[0]
|
| 1598 |
+
num_experts, _, N = expert_weight_shape(B, gate)
|
| 1599 |
+
C = A.new_empty(S, N, dtype=output_dtype)
|
| 1600 |
+
|
| 1601 |
+
def grid(META):
|
| 1602 |
+
return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
|
| 1603 |
+
|
| 1604 |
+
with device_context(A.device):
|
| 1605 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1606 |
+
compile_time_only_triton_wrap(full_precision_matmul_batched_kernel)[grid](
|
| 1607 |
+
A,
|
| 1608 |
+
B,
|
| 1609 |
+
C,
|
| 1610 |
+
bias,
|
| 1611 |
+
expert_ids,
|
| 1612 |
+
gather_idx, # None = A is expert-sorted; read only when not None (folds at trace time)
|
| 1613 |
+
scatter_idx, # None = C is expert-sorted; read only when not None (folds at trace time)
|
| 1614 |
+
S,
|
| 1615 |
+
N,
|
| 1616 |
+
K,
|
| 1617 |
+
A.stride(0),
|
| 1618 |
+
A.stride(1),
|
| 1619 |
+
B.stride(0),
|
| 1620 |
+
B.stride(2),
|
| 1621 |
+
B.stride(1),
|
| 1622 |
+
C.stride(0),
|
| 1623 |
+
C.stride(1),
|
| 1624 |
+
bias_stride_e,
|
| 1625 |
+
bias_stride_n,
|
| 1626 |
+
expert_ids.stride(0),
|
| 1627 |
+
num_experts=num_experts,
|
| 1628 |
+
GATE=gate,
|
| 1629 |
+
ACT_FN=act_fn,
|
| 1630 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1631 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1632 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1633 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1634 |
+
PDL=decode_pdl(),
|
| 1635 |
+
launch_pdl=decode_pdl(),
|
| 1636 |
+
)
|
| 1637 |
+
|
| 1638 |
+
return [C]
|
| 1639 |
+
|
| 1640 |
+
|
| 1641 |
+
@compile_time_only_triton_op(
|
| 1642 |
+
add_op_namespace_prefix("mx_weight_only_matmul_batched"),
|
| 1643 |
+
mutates_args=(),
|
| 1644 |
+
opaque=True,
|
| 1645 |
+
)
|
| 1646 |
+
def mx_weight_only_matmul_batched(
|
| 1647 |
+
A: torch.Tensor,
|
| 1648 |
+
B: torch.Tensor,
|
| 1649 |
+
Bs: torch.Tensor,
|
| 1650 |
+
expert_ids: torch.Tensor,
|
| 1651 |
+
gate: bool = False,
|
| 1652 |
+
act_fn: str = "silu",
|
| 1653 |
+
swiglu_alpha: float | None = None,
|
| 1654 |
+
swiglu_limit: float | None = None,
|
| 1655 |
+
simulate_unfused: bool = False,
|
| 1656 |
+
output_dtype: torch.dtype | None = None,
|
| 1657 |
+
gather_idx: torch.Tensor | None = None,
|
| 1658 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1659 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1660 |
+
bias: torch.Tensor | None = None,
|
| 1661 |
+
) -> list[torch.Tensor]:
|
| 1662 |
+
"""weight-only batched (decode) matmul: ``C[s] = A[s] @ B[expert_ids[s]].T`` with raw bf16/fp16
|
| 1663 |
+
activations against MXFP4/NVFP4/MXFP8 weights upcast to bf16 in-loop — the ``matmul_ogs`` format.
|
| 1664 |
+
Affine (3D) weight scales — UE8M0 group-32 (MX) or E4M3 group-16 (NVFP4, whose per-expert
|
| 1665 |
+
fp32 ``b_global_scale`` ``(E,)`` recovers on the accumulator; ``None`` = single-level).
|
| 1666 |
+
Returns ``[C]``."""
|
| 1667 |
+
assert A.dtype in (torch.bfloat16, torch.float16, torch.float32), (
|
| 1668 |
+
f"weight-only takes a raw bf16/fp16/fp32 activation, got {A.dtype}"
|
| 1669 |
+
)
|
| 1670 |
+
assert Bs.ndim == 3, f"weight-only batched takes affine (3D) weight scales, got ndim={Bs.ndim}"
|
| 1671 |
+
# The ``gate`` contract runs UNSTACKED at every S: this same op without the gate (one
|
| 1672 |
+
# plain GEMM over the stacked weight), then the one-kernel ``fused_glu`` — bit-identical
|
| 1673 |
+
# rounding, the unfused-reference order, so ``simulate_unfused`` rides through. The
|
| 1674 |
+
# upcast weight feeds a plain bf16 dot, so the stacked tile buys no native MMA and only
|
| 1675 |
+
# halves occupancy on a weight-bandwidth-bound loop; unlike the block-FP8 band this
|
| 1676 |
+
# never inverts (GPT-OSS shape: 48.0->39.7µs at S=4, still ahead at S=512; 2026-08-06).
|
| 1677 |
+
if gate:
|
| 1678 |
+
# fp32 intermediate = the exact GEMM accumulators, so the GLU keeps the gated
|
| 1679 |
+
# epilogue's FUSED-order rounding (bf16 operands would drift steep-sigmoid elements
|
| 1680 |
+
# past the weight-only tests' exact-ish tolerances). Under ``simulate_unfused`` the
|
| 1681 |
+
# caller is asking for the UNFUSED order instead, where the reference lands its gate_up
|
| 1682 |
+
# in the activation dtype before the GLU — carrying fp32 there leaves the two a bf16 ULP
|
| 1683 |
+
# apart (256.0 absolute at magnitude 2^15), which the parity tolerance rejects.
|
| 1684 |
+
inter_dtype = (
|
| 1685 |
+
resolve_output_dtype(output_dtype, A, None) if simulate_unfused else torch.float32
|
| 1686 |
+
)
|
| 1687 |
+
[gate_up] = mx_weight_only_matmul_batched(
|
| 1688 |
+
A, B, Bs, expert_ids, output_dtype=inter_dtype,
|
| 1689 |
+
gather_idx=gather_idx, scatter_idx=scatter_idx, b_global_scale=b_global_scale,
|
| 1690 |
+
)
|
| 1691 |
+
return [fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit,
|
| 1692 |
+
out_dtype=resolve_output_dtype(output_dtype, A, None))]
|
| 1693 |
+
output_dtype = resolve_output_dtype(output_dtype, A, None)
|
| 1694 |
+
K = A.shape[1]
|
| 1695 |
+
S = expert_ids.shape[0]
|
| 1696 |
+
WEIGHT_VALUES_PER_BYTE = 2 if B.dtype == torch.int8 else 1
|
| 1697 |
+
num_experts, rows, K_b = B.shape
|
| 1698 |
+
assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
|
| 1699 |
+
f"K ({K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[2] ({K_b})"
|
| 1700 |
+
)
|
| 1701 |
+
N = rows // 2 if gate else rows
|
| 1702 |
+
scale_group = mx_scale_family(Bs, K)
|
| 1703 |
+
b_global_scale = normalize_global_scale(b_global_scale, num_experts)
|
| 1704 |
+
C = A.new_empty(S, N, dtype=output_dtype)
|
| 1705 |
+
b_u8 = e2m1_as_uint8(B)
|
| 1706 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1707 |
+
|
| 1708 |
+
def grid(META):
|
| 1709 |
+
return (S, triton.cdiv(N, META["BLOCK_SIZE_N"]))
|
| 1710 |
+
|
| 1711 |
+
with device_context(A.device):
|
| 1712 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1713 |
+
compile_time_only_triton_wrap(mx_weight_only_matmul_batched_kernel)[grid](
|
| 1714 |
+
A,
|
| 1715 |
+
b_u8,
|
| 1716 |
+
bs_u8,
|
| 1717 |
+
b_global_scale,
|
| 1718 |
+
C,
|
| 1719 |
+
bias,
|
| 1720 |
+
expert_ids,
|
| 1721 |
+
gather_idx,
|
| 1722 |
+
scatter_idx,
|
| 1723 |
+
S,
|
| 1724 |
+
N,
|
| 1725 |
+
K,
|
| 1726 |
+
A.stride(0),
|
| 1727 |
+
A.stride(1),
|
| 1728 |
+
b_u8.stride(0),
|
| 1729 |
+
b_u8.stride(2),
|
| 1730 |
+
b_u8.stride(1),
|
| 1731 |
+
bs_u8.stride(0),
|
| 1732 |
+
bs_u8.stride(1),
|
| 1733 |
+
bs_u8.stride(2),
|
| 1734 |
+
C.stride(0),
|
| 1735 |
+
C.stride(1),
|
| 1736 |
+
bias_stride_e,
|
| 1737 |
+
bias_stride_n,
|
| 1738 |
+
global_scale_stride(b_global_scale),
|
| 1739 |
+
expert_ids.stride(0),
|
| 1740 |
+
num_experts=num_experts,
|
| 1741 |
+
SCALE_GROUP_K=scale_group,
|
| 1742 |
+
WEIGHT_VALUES_PER_BYTE=WEIGHT_VALUES_PER_BYTE,
|
| 1743 |
+
GATE=gate,
|
| 1744 |
+
ACT_FN=act_fn,
|
| 1745 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1746 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1747 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1748 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1749 |
+
PDL=decode_pdl(),
|
| 1750 |
+
launch_pdl=decode_pdl(),
|
| 1751 |
+
)
|
| 1752 |
+
|
| 1753 |
+
return [C]
|
| 1754 |
+
|
| 1755 |
+
|
| 1756 |
+
def matmul_batched(
|
| 1757 |
+
A: torch.Tensor,
|
| 1758 |
+
B: torch.Tensor,
|
| 1759 |
+
As: torch.Tensor | None = None,
|
| 1760 |
+
Bs: torch.Tensor | None = None,
|
| 1761 |
+
*,
|
| 1762 |
+
expert_ids: torch.Tensor,
|
| 1763 |
+
bias: torch.Tensor | None = None, # (E, N_out) per-expert output bias; (N_out,) for 2D
|
| 1764 |
+
activation_format: str | None = None,
|
| 1765 |
+
gate: bool = False,
|
| 1766 |
+
act_fn: str = "silu",
|
| 1767 |
+
swiglu_alpha: float | None = None,
|
| 1768 |
+
swiglu_limit: float | None = None,
|
| 1769 |
+
quantize_output: bool = False,
|
| 1770 |
+
simulate_unfused: bool = False,
|
| 1771 |
+
output_dtype: torch.dtype | None = None,
|
| 1772 |
+
gather_idx: torch.Tensor | None = None,
|
| 1773 |
+
scatter_idx: torch.Tensor | None = None,
|
| 1774 |
+
a_global_scale: torch.Tensor | None = None,
|
| 1775 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1776 |
+
output_global_scale: torch.Tensor | None = None,
|
| 1777 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 1778 |
+
"""Batched matmul dispatcher (W8A8 FP8, W4A8/W4A4 FP4, or full-precision). Routes one
|
| 1779 |
+
program per routed row (``expert_ids`` gives its expert).
|
| 1780 |
+
|
| 1781 |
+
``As`` marks ``A`` as already quantized (framework-precomputed scales, or a requantized
|
| 1782 |
+
intermediate handed to the down projection); a per-tensor scalar ``As`` is instead the static
|
| 1783 |
+
(calibrated) activation scale for block-scale FP8 weights — the op quantizes raw ``A`` against
|
| 1784 |
+
it; ``None`` = raw ``A``, quantized dynamically by the op in ``activation_format`` (``None`` =
|
| 1785 |
+
the weights' own format — fp8/E4M3 offline for bd/tensor, inline for MX; ``"bf16"`` = weight-only;
|
| 1786 |
+
``"mxfp4"`` = packed E2M1 activations). ``Bs`` ``None`` = unquantized BF16/FP16 weights.
|
| 1787 |
+
``quantize_output`` requantizes the output into ``activation_format`` — the return is then
|
| 1788 |
+
``(C, Cs)``. ``gate``/``act_fn``/``swiglu_alpha``/``swiglu_limit`` fuse the gate|up GLU into the
|
| 1789 |
+
epilogue. ``As``/``Bs`` are each a bare block-scale tensor; the two-level NVFP4
|
| 1790 |
+
second-level scales ride the separate ``a_global_scale``/``b_global_scale`` (fp32 per-tensor,
|
| 1791 |
+
weights per-expert ``(E,)``; from ``nvfp4_quantize_two_level``), and the op folds ``g_a · g_b``
|
| 1792 |
+
onto the accumulator. The activation global ``g_a`` is CALIBRATED (the checkpoint's
|
| 1793 |
+
``input_scale``): ``a_global_scale=g_a`` with a raw ``A`` has the op quantize ``A / g_a`` per
|
| 1794 |
+
block, and rides a pre-quantized ``As`` the same way. Under an NVFP4 requant the
|
| 1795 |
+
fused requant normalizes the GLU intermediate by the PROVIDED ``output_global_scale`` (the next
|
| 1796 |
+
proj's calibrated ``input_scale``) before the block quant and returns ``[C, Cs]``; the down
|
| 1797 |
+
consumes it as ``As=Cs, a_global_scale=output_global_scale``. ``gather_idx``/``scatter_idx`` (each None or a ``(S,)`` map)
|
| 1798 |
+
address the source row of ``A`` / destination row of ``C`` per program — None means row
|
| 1799 |
+
``s``; the gather lets the gate_up read unexpanded activations (source row
|
| 1800 |
+
``s // num_top_k``) with no copy.
|
| 1801 |
+
|
| 1802 |
+
Routes by what the weight tensors themselves say (there is no ``block_size``
|
| 1803 |
+
parameter — the quantization block is derived from the scale shape,
|
| 1804 |
+
``weight_block_size``):
|
| 1805 |
+
- ``Bs`` None → ``full_precision_matmul_batched`` (plain dot, no scales anywhere).
|
| 1806 |
+
- MX weights — ``int8`` (packed E2M1) or ``float8_e4m3fn`` (E4M3) with UE8M0
|
| 1807 |
+
group-32 ``Bs`` → ``mx_dynamic_matmul_batched``.
|
| 1808 |
+
- one scale per expert (``Bs`` ``(E,)``/``(E, 1, 1)``) →
|
| 1809 |
+
``w8a8_tensor_dynamic_fp8_matmul_batched``.
|
| 1810 |
+
- block scales (``Bs`` ``(E, N/bn, K/bk)``) → ``w8a8_block_dynamic_fp8_matmul_batched``.
|
| 1811 |
+
"""
|
| 1812 |
+
check_activation_format(activation_format, quantize_output)
|
| 1813 |
+
assert (a_global_scale is None and b_global_scale is None) or (
|
| 1814 |
+
Bs is not None and weight_format(B, Bs) == "nvfp4"
|
| 1815 |
+
), "two-level globals (a_global_scale / b_global_scale) are NVFP4-only"
|
| 1816 |
+
assert output_global_scale is None or (
|
| 1817 |
+
Bs is not None and quantize_output and resolve_activation_format(activation_format, B, Bs) == "nvfp4"
|
| 1818 |
+
), (
|
| 1819 |
+
"output_global_scale is the NVFP4 requant second level — it requires quantize_output=True on NVFP4 "
|
| 1820 |
+
"(the epilogue would otherwise normalize by it with nothing downstream to compensate)"
|
| 1821 |
+
)
|
| 1822 |
+
if As is not None and As.numel() == 1:
|
| 1823 |
+
# static (per-tensor calibrated) activation quant: a per-tensor scalar As for block-scale FP8
|
| 1824 |
+
# weights — the caller hands raw A, the op quantizes it against the scalar (As IS the scale).
|
| 1825 |
+
assert Bs is not None and not is_mx(B, Bs) and weight_block_size(B, Bs) is not None, (
|
| 1826 |
+
"a per-tensor scalar As (static activation scale) needs block-scale FP8 weights"
|
| 1827 |
+
)
|
| 1828 |
+
out = w8a8_block_static_fp8_matmul_batched(
|
| 1829 |
+
A,
|
| 1830 |
+
B,
|
| 1831 |
+
As,
|
| 1832 |
+
Bs,
|
| 1833 |
+
expert_ids,
|
| 1834 |
+
weight_block_size(B, Bs),
|
| 1835 |
+
gate,
|
| 1836 |
+
act_fn,
|
| 1837 |
+
swiglu_alpha,
|
| 1838 |
+
swiglu_limit,
|
| 1839 |
+
simulate_unfused,
|
| 1840 |
+
activation_format,
|
| 1841 |
+
quantize_output,
|
| 1842 |
+
output_dtype,
|
| 1843 |
+
gather_idx,
|
| 1844 |
+
scatter_idx,
|
| 1845 |
+
bias=bias,
|
| 1846 |
+
)
|
| 1847 |
+
return out[0] if len(out) == 1 else tuple(out)
|
| 1848 |
+
|
| 1849 |
+
if Bs is None:
|
| 1850 |
+
assert As is None, (
|
| 1851 |
+
"the full-precision path (Bs=None) takes no activation scales"
|
| 1852 |
+
)
|
| 1853 |
+
out = full_precision_matmul_batched(
|
| 1854 |
+
A,
|
| 1855 |
+
B,
|
| 1856 |
+
expert_ids,
|
| 1857 |
+
gate,
|
| 1858 |
+
act_fn,
|
| 1859 |
+
swiglu_alpha,
|
| 1860 |
+
swiglu_limit,
|
| 1861 |
+
simulate_unfused,
|
| 1862 |
+
activation_format,
|
| 1863 |
+
quantize_output,
|
| 1864 |
+
output_dtype,
|
| 1865 |
+
gather_idx,
|
| 1866 |
+
scatter_idx,
|
| 1867 |
+
bias=bias,
|
| 1868 |
+
)
|
| 1869 |
+
elif is_mx(B, Bs) and activation_format == "bf16": # weight-only: raw bf16 acts, MX weight upcast in-MMA
|
| 1870 |
+
assert As is None and a_global_scale is None and not quantize_output, (
|
| 1871 |
+
"weight-only (activation_format='bf16') takes a raw activation, no As/global/requant"
|
| 1872 |
+
)
|
| 1873 |
+
assert output_global_scale is None, (
|
| 1874 |
+
"weight-only has no requant epilogue for output_global_scale to normalize"
|
| 1875 |
+
)
|
| 1876 |
+
out = mx_weight_only_matmul_batched(
|
| 1877 |
+
A,
|
| 1878 |
+
B,
|
| 1879 |
+
Bs,
|
| 1880 |
+
expert_ids,
|
| 1881 |
+
gate,
|
| 1882 |
+
act_fn,
|
| 1883 |
+
swiglu_alpha,
|
| 1884 |
+
swiglu_limit,
|
| 1885 |
+
simulate_unfused,
|
| 1886 |
+
output_dtype,
|
| 1887 |
+
gather_idx,
|
| 1888 |
+
scatter_idx,
|
| 1889 |
+
b_global_scale=b_global_scale,
|
| 1890 |
+
bias=bias,
|
| 1891 |
+
)
|
| 1892 |
+
elif is_mx(B, Bs):
|
| 1893 |
+
out = mx_dynamic_matmul_batched(
|
| 1894 |
+
A,
|
| 1895 |
+
B,
|
| 1896 |
+
As,
|
| 1897 |
+
Bs,
|
| 1898 |
+
expert_ids,
|
| 1899 |
+
gate,
|
| 1900 |
+
act_fn,
|
| 1901 |
+
swiglu_alpha,
|
| 1902 |
+
swiglu_limit,
|
| 1903 |
+
simulate_unfused,
|
| 1904 |
+
activation_format,
|
| 1905 |
+
quantize_output,
|
| 1906 |
+
output_dtype,
|
| 1907 |
+
gather_idx,
|
| 1908 |
+
scatter_idx,
|
| 1909 |
+
a_global_scale,
|
| 1910 |
+
b_global_scale,
|
| 1911 |
+
output_global_scale,
|
| 1912 |
+
bias=bias,
|
| 1913 |
+
)
|
| 1914 |
+
elif (block_size := weight_block_size(B, Bs)) is None:
|
| 1915 |
+
assert not gate, (
|
| 1916 |
+
"the batched op has no tensor-wide gate|up fusion (grouped and 2D support it)"
|
| 1917 |
+
)
|
| 1918 |
+
assert activation_format in (None, "fp8") and not quantize_output, (
|
| 1919 |
+
"tensor-wide supports neither packed activations nor a fused requant"
|
| 1920 |
+
)
|
| 1921 |
+
out = w8a8_tensor_dynamic_fp8_matmul_batched(
|
| 1922 |
+
A, B, As, Bs, expert_ids, output_dtype, gather_idx, scatter_idx, bias=bias
|
| 1923 |
+
)
|
| 1924 |
+
else:
|
| 1925 |
+
out = w8a8_block_dynamic_fp8_matmul_batched(
|
| 1926 |
+
A,
|
| 1927 |
+
B,
|
| 1928 |
+
As,
|
| 1929 |
+
Bs,
|
| 1930 |
+
expert_ids,
|
| 1931 |
+
block_size,
|
| 1932 |
+
gate,
|
| 1933 |
+
act_fn,
|
| 1934 |
+
swiglu_alpha,
|
| 1935 |
+
swiglu_limit,
|
| 1936 |
+
simulate_unfused,
|
| 1937 |
+
activation_format,
|
| 1938 |
+
quantize_output,
|
| 1939 |
+
output_dtype,
|
| 1940 |
+
gather_idx,
|
| 1941 |
+
scatter_idx,
|
| 1942 |
+
bias=bias,
|
| 1943 |
+
)
|
| 1944 |
+
# bd/mx/full-precision ops return a list ([C] or [C, Cs]); the tensor op returns a bare tensor.
|
| 1945 |
+
if isinstance(out, (list, tuple)):
|
| 1946 |
+
return out[0] if len(out) == 1 else tuple(out)
|
| 1947 |
+
return out
|
build/torch-xpu/bayesian_autotuner.py
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Bayesian-optimization autotuner: benches a budgeted sample of the config grid instead of
|
| 16 |
+
the full grid, via a Tree-structured Parzen Estimator (TPE) over the config *dimensions*
|
| 17 |
+
(``num_warps`` / ``num_stages`` / tile sizes / flags). After a short random seed phase it
|
| 18 |
+
models the good (top-``gamma`` by measured time) vs bad per-dimension value densities and
|
| 19 |
+
benches the unmeasured config that maximizes ``l(x)/g(x)`` — the Expected-Improvement proxy
|
| 20 |
+
— then a coordinate-descent pass polishes the best. Subclasses Triton's ``Autotuner`` and
|
| 21 |
+
uses only the Python stdlib (no external optimizer dependency).
|
| 22 |
+
|
| 23 |
+
Each new key's search is warm-started from the most recently tuned key's best config (nearby
|
| 24 |
+
workloads share tile-shape preferences). Grids smaller than ``n_trials`` defer to the stock
|
| 25 |
+
exhaustive bench-all.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import hashlib
|
| 31 |
+
import logging
|
| 32 |
+
import json
|
| 33 |
+
import math
|
| 34 |
+
import os
|
| 35 |
+
import traceback
|
| 36 |
+
import random
|
| 37 |
+
import time
|
| 38 |
+
from collections import defaultdict
|
| 39 |
+
from contextlib import contextmanager
|
| 40 |
+
|
| 41 |
+
import torch
|
| 42 |
+
|
| 43 |
+
from typing import Dict, List
|
| 44 |
+
|
| 45 |
+
from triton.runtime.autotuner import (
|
| 46 |
+
Autotuner,
|
| 47 |
+
Config,
|
| 48 |
+
JITFunction,
|
| 49 |
+
driver,
|
| 50 |
+
get_cache_invalidating_env_vars,
|
| 51 |
+
get_cache_manager,
|
| 52 |
+
knobs,
|
| 53 |
+
triton_key,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
logger = logging.getLogger(__name__)
|
| 57 |
+
|
| 58 |
+
# Env-gated host-execution counter (FINEGRAINED_AUTOTUNE_CALLCOUNT=<path>): every
|
| 59 |
+
# BayesianAutotuner.run() call increments its kernel's tally, dumped to <path> at exit.
|
| 60 |
+
# Discriminates cudagraph REPLAY (host code runs only at capture) from per-token
|
| 61 |
+
# re-execution — the decode-gap forensics tool.
|
| 62 |
+
_CALL_COUNTS = None
|
| 63 |
+
if os.environ.get("FINEGRAINED_AUTOTUNE_CALLCOUNT"):
|
| 64 |
+
import atexit
|
| 65 |
+
|
| 66 |
+
_CALL_COUNTS = {}
|
| 67 |
+
|
| 68 |
+
def _dump_call_counts(path=os.environ["FINEGRAINED_AUTOTUNE_CALLCOUNT"]):
|
| 69 |
+
with open(path, "w") as f:
|
| 70 |
+
json.dump(_CALL_COUNTS, f, indent=1, sort_keys=True)
|
| 71 |
+
|
| 72 |
+
atexit.register(_dump_call_counts)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class BayesianAutotuner(Autotuner):
|
| 76 |
+
"""Drop-in replacement for ``triton.runtime.autotuner.Autotuner`` that
|
| 77 |
+
benches ~``n_trials`` configs per key via TPE Bayesian optimization +
|
| 78 |
+
coordinate-descent refinement, instead of the full grid."""
|
| 79 |
+
|
| 80 |
+
def __init__(
|
| 81 |
+
self,
|
| 82 |
+
*args,
|
| 83 |
+
n_trials: int = 80,
|
| 84 |
+
max_failures: int | None = None,
|
| 85 |
+
n_startup_trials: int = 12,
|
| 86 |
+
gamma: float = 0.25,
|
| 87 |
+
refine: bool = True,
|
| 88 |
+
max_refine_iters: int = 5,
|
| 89 |
+
log_path: str | None = None,
|
| 90 |
+
path_anchor_axes: tuple[str, ...] = (),
|
| 91 |
+
finite_check_args: tuple[str, ...] = (),
|
| 92 |
+
**kwargs,
|
| 93 |
+
):
|
| 94 |
+
super().__init__(*args, **kwargs)
|
| 95 |
+
self.fn_name = getattr(self.fn, "__name__", str(self.fn))
|
| 96 |
+
# Bayesian trial budget — the per-decorator default, overridable via the
|
| 97 |
+
# FINEGRAINED_AUTOTUNE_TRIALS env var (quick sweeps / exhaustive runs without touching
|
| 98 |
+
# the decorators; set it >= grid size to fall back to stock exhaustive bench-all).
|
| 99 |
+
self.n_trials = int(os.environ.get("FINEGRAINED_AUTOTUNE_TRIALS") or n_trials)
|
| 100 |
+
# A tune aborts once this many configs have failed to compile/run without the measured
|
| 101 |
+
# budget being met. Defaults to n_trials: a grid that cannot land its measurements without
|
| 102 |
+
# that many rejects has a pruner gap, and the fix is to fence the dead region rather than
|
| 103 |
+
# to tolerate the compiles. Its own axis so a kernel can raise it deliberately.
|
| 104 |
+
self.max_failures = int(
|
| 105 |
+
os.environ.get("FINEGRAINED_AUTOTUNE_MAX_FAILURES")
|
| 106 |
+
or max_failures
|
| 107 |
+
or self.n_trials
|
| 108 |
+
)
|
| 109 |
+
self.n_startup_trials = n_startup_trials
|
| 110 |
+
# top fraction of measured configs the TPE treats as "good"
|
| 111 |
+
self.gamma = gamma
|
| 112 |
+
self.refine = refine
|
| 113 |
+
self.max_refine_iters = max_refine_iters
|
| 114 |
+
# Branch axes (by kwarg name) that relocate the TILE optimum — the caller's
|
| 115 |
+
# declaration, since it is configuration knowledge (the tuner never hardcodes
|
| 116 |
+
# axis names). Each distinct value combination of these axes gets one guaranteed
|
| 117 |
+
# max-tile anchor; empty = median anchors only.
|
| 118 |
+
self.path_anchor_axes = tuple(path_anchor_axes)
|
| 119 |
+
# Output arg NAMES (a declaration — the tuner never hardcodes kernel details) whose
|
| 120 |
+
# values are checked for NaN/inf after each config's bench: a config that writes
|
| 121 |
+
# non-finite output scores inf and can never be crowned. The tuner times, it never
|
| 122 |
+
# validates — this veto closes the NaN half of the "wrong-answer configs win
|
| 123 |
+
# tunes" class at negligible cost (one isfinite pass per benched config).
|
| 124 |
+
self.finite_check_args = tuple(finite_check_args)
|
| 125 |
+
# JSONL log of every benched (key, config, ms) — set here or via the
|
| 126 |
+
# FINEGRAINED_AUTOTUNE_LOG env var. Analyse offline to prune bad configs.
|
| 127 |
+
self.log_path = log_path or os.environ.get("FINEGRAINED_AUTOTUNE_LOG")
|
| 128 |
+
# steady-state launch support: key extraction without dict builds (eager decode
|
| 129 |
+
# is host-bound; the cache-hit path of run() is on the per-token critical path)
|
| 130 |
+
self._arg_position = {name: i for i, name in enumerate(self.arg_names)}
|
| 131 |
+
self._arg_name_set = frozenset(self.arg_names)
|
| 132 |
+
self._dtype_tag_cache: Dict[tuple, str] = {}
|
| 133 |
+
self._dtype_str_cache: Dict[torch.dtype, str] = {}
|
| 134 |
+
|
| 135 |
+
def _dtype_str(self, dtype) -> str:
|
| 136 |
+
s = self._dtype_str_cache.get(dtype)
|
| 137 |
+
if s is None:
|
| 138 |
+
s = self._dtype_str_cache[dtype] = str(dtype)
|
| 139 |
+
return s
|
| 140 |
+
|
| 141 |
+
# substrings marking a COMPILE-stage failure — the only class safe to memoize on
|
| 142 |
+
# disk (a benching/CUDA error can be transient or sticky-context contamination;
|
| 143 |
+
# persisting one would permanently fence a healthy config for this source version)
|
| 144 |
+
_COMPILE_FAILURE_MARKS = (
|
| 145 |
+
"PassManager",
|
| 146 |
+
"CompilationError",
|
| 147 |
+
"MLIR",
|
| 148 |
+
"ConvertTritonGPUToLLVM",
|
| 149 |
+
"TritonGPUAccelerateMatmul",
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def _bench(self, *args, config, **meta):
|
| 153 |
+
"""Score any failing config as inf instead of raising — a compile failure is data
|
| 154 |
+
for the search, not a fatal error. Stock Triton forgives only OutOfResources, but
|
| 155 |
+
e.g. Triton 3.7.1's ``warp_specialize`` raises RuntimeError at unsupported
|
| 156 |
+
(shape, config) combos; unguarded, one such config kills the whole tune when a
|
| 157 |
+
small grid falls through to the stock exhaustive path below. Every failure is
|
| 158 |
+
recorded and ``_report_bench_failures`` reports every distinct failure —
|
| 159 |
+
inf-scoring must not silently hide a broken path behind a healthy one.
|
| 160 |
+
(Stock already inf's OutOfResources / CompileTimeAssertionFailure / PTXASError
|
| 161 |
+
internally without reaching this handler — those are deliberate guard classes,
|
| 162 |
+
e.g. our own ``tl.static_assert`` fences; the reporter covers the UNEXPECTED
|
| 163 |
+
failure classes stock would otherwise let kill the tune.)
|
| 164 |
+
|
| 165 |
+
Compile-stage failures are memoized on disk keyed by everything that determines
|
| 166 |
+
compilation (kernel source hash + config + constexpr arg values + tensor dtypes +
|
| 167 |
+
invalidating env), so the next fresh tune of ANY shape skips the doomed compile
|
| 168 |
+
outright (~40-66 wasted compiles per nvfp4 tune before this). The memo dies with
|
| 169 |
+
the kernel source (``fn.cache_key``); bench-stage errors are never persisted."""
|
| 170 |
+
sig = self._compile_signature(config, meta)
|
| 171 |
+
memo = self._failed_compile_memo()
|
| 172 |
+
if sig is not None and sig in memo:
|
| 173 |
+
self._failures.append((config, memo[sig] + " [memoized]"))
|
| 174 |
+
return [float("inf")] * 3
|
| 175 |
+
try:
|
| 176 |
+
timing = super()._bench(*args, config=config, **meta)
|
| 177 |
+
bad = self._nonfinite_output(meta)
|
| 178 |
+
if bad is not None:
|
| 179 |
+
self._failures.append((config, f"non-finite output in {bad} (numerics veto)"))
|
| 180 |
+
return [float("inf")] * 3
|
| 181 |
+
return timing
|
| 182 |
+
except Exception as e:
|
| 183 |
+
# triton's CompilationError carries only the location + source snippet in str(e);
|
| 184 |
+
# the actual reason lives in the chained cause, so without it every compile failure
|
| 185 |
+
# reads as an anonymous caret. Keep the head of the location and the cause verbatim.
|
| 186 |
+
msg = str(e)[:250]
|
| 187 |
+
# triton nests a CompilationError per @jit frame, each carrying only its own location
|
| 188 |
+
# and source — the actual reason is at the BOTTOM of the chain. Walk to it, else every
|
| 189 |
+
# compile failure reads as an anonymous caret and cannot be fenced.
|
| 190 |
+
root, seen = e, 0
|
| 191 |
+
while (root.__cause__ or root.__context__) is not None and seen < 12:
|
| 192 |
+
root = root.__cause__ or root.__context__
|
| 193 |
+
seen += 1
|
| 194 |
+
if root is not e:
|
| 195 |
+
msg += f" || root: {type(root).__name__}: {str(root)[-300:]}"
|
| 196 |
+
err = f"{type(e).__name__}: {msg}"
|
| 197 |
+
if os.environ.get("FINEGRAINED_AUTOTUNE_TRACEBACKS"):
|
| 198 |
+
logger.warning(
|
| 199 |
+
"[autotune] %s failed on %s:\n%s",
|
| 200 |
+
self.fn_name,
|
| 201 |
+
config,
|
| 202 |
+
"".join(traceback.format_exception(type(e), e, e.__traceback__)),
|
| 203 |
+
)
|
| 204 |
+
self._failures.append((config, err))
|
| 205 |
+
if sig is not None and any(m in err for m in self._COMPILE_FAILURE_MARKS):
|
| 206 |
+
memo[sig] = err
|
| 207 |
+
self._persist_failed_compile_memo()
|
| 208 |
+
return [float("inf")] * 3
|
| 209 |
+
|
| 210 |
+
def _nonfinite_output(self, meta=None) -> str | None:
|
| 211 |
+
"""Name of the first declared output arg holding NaN/inf after a bench, else None.
|
| 212 |
+
fp8 tensors byte-check the E4M3 NaN pattern (no ``isfinite`` kernel for fp8);
|
| 213 |
+
integer/packed outputs are skipped (every bit pattern is a value)."""
|
| 214 |
+
if not self.finite_check_args:
|
| 215 |
+
return None
|
| 216 |
+
nargs = {**(getattr(self, "nargs", None) or {}), **(meta or {})}
|
| 217 |
+
for name in self.finite_check_args:
|
| 218 |
+
v = nargs.get(name)
|
| 219 |
+
if not isinstance(v, torch.Tensor) or v.numel() == 0:
|
| 220 |
+
continue
|
| 221 |
+
if v.dtype == torch.float8_e4m3fn:
|
| 222 |
+
flat = v.reshape(-1).view(torch.uint8)
|
| 223 |
+
bad = bool((((flat & 0x7F) == 0x7F)).any())
|
| 224 |
+
elif v.is_floating_point():
|
| 225 |
+
bad = bool((~v.reshape(-1).float().isfinite()).any())
|
| 226 |
+
else:
|
| 227 |
+
continue
|
| 228 |
+
if bad:
|
| 229 |
+
return name
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
def _compile_signature(self, config, meta=None) -> str | None:
|
| 233 |
+
"""Hash of the compile determinants for one config: the config itself plus the
|
| 234 |
+
constexpr argument VALUES and tensor argument dtypes from this launch (both feed
|
| 235 |
+
specialization — e.g. GATE flips arms, a uint8 A packs the loads). Source/env
|
| 236 |
+
live in the memo FILE's key, not here.
|
| 237 |
+
|
| 238 |
+
``meta`` carries the launch kwargs: every constexpr is kwarg-passed at the call
|
| 239 |
+
sites, so ``self.nargs`` (positional-only) alone would hash GATE/format flags as
|
| 240 |
+
None and collide memo entries across arms."""
|
| 241 |
+
nargs = {**(getattr(self, "nargs", None) or {}), **(meta or {})}
|
| 242 |
+
if not nargs:
|
| 243 |
+
return None
|
| 244 |
+
fn = self.fn
|
| 245 |
+
while not isinstance(fn, JITFunction):
|
| 246 |
+
fn = fn.fn
|
| 247 |
+
parts = [str(sorted(config.all_kwargs().items()))]
|
| 248 |
+
for param in fn.params:
|
| 249 |
+
v = nargs.get(param.name)
|
| 250 |
+
if hasattr(v, "dtype"):
|
| 251 |
+
parts.append(f"{param.name}:{v.dtype}")
|
| 252 |
+
elif param.is_constexpr and isinstance(
|
| 253 |
+
v, (bool, int, float, str, type(None))
|
| 254 |
+
):
|
| 255 |
+
parts.append(f"{param.name}={v}")
|
| 256 |
+
elif isinstance(v, int) and not isinstance(v, bool):
|
| 257 |
+
# non-constexpr ints feed Triton's specialization (==1 / %16 divisibility),
|
| 258 |
+
# which is (shape, config)-dependent for WS lowering failures — hashing the
|
| 259 |
+
# bucket keeps one shape's PassManager failure from fencing every shape
|
| 260 |
+
parts.append(
|
| 261 |
+
f"{param.name}:i{1 if v == 1 else (16 if v % 16 == 0 else 0)}"
|
| 262 |
+
)
|
| 263 |
+
return hashlib.sha256("-".join(parts).encode("utf-8")).hexdigest()
|
| 264 |
+
|
| 265 |
+
def _failed_compile_memo(self) -> dict:
|
| 266 |
+
"""The per-(kernel source, backend, env) failed-compile dict, loaded from
|
| 267 |
+
Triton's on-disk cache once per autotuner instance."""
|
| 268 |
+
if getattr(self, "_failed_memo", None) is not None:
|
| 269 |
+
return self._failed_memo
|
| 270 |
+
try:
|
| 271 |
+
from triton.compiler.compiler import make_backend
|
| 272 |
+
|
| 273 |
+
fn = self.fn
|
| 274 |
+
while not isinstance(fn, JITFunction):
|
| 275 |
+
fn = fn.fn
|
| 276 |
+
group = hashlib.sha256(
|
| 277 |
+
"-".join(
|
| 278 |
+
[
|
| 279 |
+
triton_key(),
|
| 280 |
+
make_backend(driver.active.get_current_target()).hash(),
|
| 281 |
+
fn.cache_key,
|
| 282 |
+
str(sorted(get_cache_invalidating_env_vars().items())),
|
| 283 |
+
]
|
| 284 |
+
).encode("utf-8")
|
| 285 |
+
).hexdigest()
|
| 286 |
+
self._failed_memo_cache = get_cache_manager(group)
|
| 287 |
+
self._failed_memo_file = f"{fn.__name__[:150]}.failed_compiles.json"
|
| 288 |
+
path = self._failed_memo_cache.get_file(self._failed_memo_file)
|
| 289 |
+
self._failed_memo = json.load(open(path)) if path else {}
|
| 290 |
+
except Exception:
|
| 291 |
+
self._failed_memo_cache = None
|
| 292 |
+
self._failed_memo = {}
|
| 293 |
+
return self._failed_memo
|
| 294 |
+
|
| 295 |
+
def _persist_failed_compile_memo(self):
|
| 296 |
+
if getattr(self, "_failed_memo_cache", None) is None:
|
| 297 |
+
return
|
| 298 |
+
try:
|
| 299 |
+
self._failed_memo_cache.put(
|
| 300 |
+
json.dumps(self._failed_memo), self._failed_memo_file, binary=False
|
| 301 |
+
)
|
| 302 |
+
except Exception:
|
| 303 |
+
pass # persistence is best-effort; the in-memory memo still holds
|
| 304 |
+
|
| 305 |
+
def _report_bench_failures(self):
|
| 306 |
+
"""After every tune, report every UNIQUE failure — a failure is never silent:
|
| 307 |
+
inf-scoring keeps the search alive, but a human must see what broke (e.g. a code
|
| 308 |
+
change that kills one compute path would otherwise silently degrade into "the other
|
| 309 |
+
path wins"). Distinct errors are deduped with a count and an example config; the
|
| 310 |
+
JSONL autotune log has per-config detail."""
|
| 311 |
+
if not self._failures:
|
| 312 |
+
return
|
| 313 |
+
by_err = defaultdict(list)
|
| 314 |
+
for c, err in self._failures:
|
| 315 |
+
by_err[err].append(c)
|
| 316 |
+
for err, cfgs in by_err.items():
|
| 317 |
+
c = cfgs[0]
|
| 318 |
+
example = ", ".join(f"{k}={v}" for k, v in c.kwargs.items())
|
| 319 |
+
logger.warning(
|
| 320 |
+
"[autotune] %s: %d config(s) failed to compile/run — %s (e.g. %s, w%d s%d)",
|
| 321 |
+
self.fn_name,
|
| 322 |
+
len(cfgs),
|
| 323 |
+
err,
|
| 324 |
+
example,
|
| 325 |
+
c.num_warps,
|
| 326 |
+
c.num_stages,
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
def run(self, *args, **kwargs):
|
| 330 |
+
if _CALL_COUNTS is not None:
|
| 331 |
+
_CALL_COUNTS[self.fn_name] = _CALL_COUNTS.get(self.fn_name, 0) + 1
|
| 332 |
+
self._failures = []
|
| 333 |
+
# Small grid → defer to parent (stock exhaustive bench-all). Steady state
|
| 334 |
+
# still short-circuits the parent's per-call dict builds: a crowned config
|
| 335 |
+
# with no pre_hook launches directly. The lookup key must be STOCK format
|
| 336 |
+
# (key values + untagged dtype strings) — it is the parent that populates
|
| 337 |
+
# this branch's cache entries.
|
| 338 |
+
if len(self.configs) <= 1 or self.n_trials >= len(self.configs):
|
| 339 |
+
config = self.configs[0] if len(self.configs) == 1 else None
|
| 340 |
+
if config is None and self.cache:
|
| 341 |
+
n_positional = len(args)
|
| 342 |
+
key = []
|
| 343 |
+
for k in self.keys:
|
| 344 |
+
i = self._arg_position.get(k)
|
| 345 |
+
if i is not None and i < n_positional:
|
| 346 |
+
key.append(args[i])
|
| 347 |
+
elif k in kwargs and k in self._arg_name_set:
|
| 348 |
+
key.append(kwargs[k])
|
| 349 |
+
for i in range(n_positional):
|
| 350 |
+
dtype = getattr(args[i], "dtype", None)
|
| 351 |
+
if dtype is not None:
|
| 352 |
+
key.append(self._dtype_str(dtype))
|
| 353 |
+
for k, v in kwargs.items():
|
| 354 |
+
if k in self._arg_name_set and self._arg_position[k] >= n_positional:
|
| 355 |
+
dtype = getattr(v, "dtype", None)
|
| 356 |
+
if dtype is not None:
|
| 357 |
+
key.append(self._dtype_str(dtype))
|
| 358 |
+
config = self.cache.get(tuple(key))
|
| 359 |
+
if config is not None and config.pre_hook is None:
|
| 360 |
+
self.best_config = config
|
| 361 |
+
config_kwargs = getattr(config, "_all_kwargs_memo", None)
|
| 362 |
+
if config_kwargs is None:
|
| 363 |
+
config_kwargs = config._all_kwargs_memo = config.all_kwargs()
|
| 364 |
+
return self.fn.run(*args, **kwargs, **config_kwargs)
|
| 365 |
+
ret = super().run(*args, **kwargs)
|
| 366 |
+
self._report_bench_failures()
|
| 367 |
+
return ret
|
| 368 |
+
|
| 369 |
+
# Key extraction runs on every launch and eager decode is host-bound, so it
|
| 370 |
+
# avoids dict builds: positional args by precomputed index, then kwargs —
|
| 371 |
+
# the same (arg_names-prefix, kwargs-order) iteration the dict merge gave,
|
| 372 |
+
# keeping keys byte-compatible with existing disk crown caches.
|
| 373 |
+
arg_names = self.arg_names
|
| 374 |
+
n_positional = len(args)
|
| 375 |
+
tags = self._dtype_tag_cache
|
| 376 |
+
key = []
|
| 377 |
+
for k in self.keys:
|
| 378 |
+
i = self._arg_position.get(k)
|
| 379 |
+
if i is not None and i < n_positional:
|
| 380 |
+
key.append(args[i])
|
| 381 |
+
elif k in kwargs and k in self._arg_name_set:
|
| 382 |
+
key.append(kwargs[k])
|
| 383 |
+
# name-tagged, with an explicit token for absent optional tensors: a positional
|
| 384 |
+
# dtype list lets a gather-only and a scatter-only launch alias to one entry,
|
| 385 |
+
# while descriptor-arm legality (descriptor_box_pruner) hangs off GatherIdx
|
| 386 |
+
# presence — the aliased replay would read wrong rows without ever re-pruning
|
| 387 |
+
for i in range(n_positional):
|
| 388 |
+
v = args[i]
|
| 389 |
+
dtype = getattr(v, "dtype", None) if v is not None else None
|
| 390 |
+
if v is not None and dtype is None:
|
| 391 |
+
continue
|
| 392 |
+
tag_key = (arg_names[i], dtype)
|
| 393 |
+
tag = tags.get(tag_key)
|
| 394 |
+
if tag is None:
|
| 395 |
+
tag = tags[tag_key] = f"{tag_key[0]}:{dtype}"
|
| 396 |
+
key.append(tag)
|
| 397 |
+
for k, v in kwargs.items():
|
| 398 |
+
if k not in self._arg_name_set or self._arg_position[k] < n_positional:
|
| 399 |
+
continue
|
| 400 |
+
dtype = getattr(v, "dtype", None) if v is not None else None
|
| 401 |
+
if v is not None and dtype is None:
|
| 402 |
+
continue
|
| 403 |
+
tag_key = (k, dtype)
|
| 404 |
+
tag = tags.get(tag_key)
|
| 405 |
+
if tag is None:
|
| 406 |
+
tag = tags[tag_key] = f"{tag_key[0]}:{dtype}"
|
| 407 |
+
key.append(tag)
|
| 408 |
+
key = tuple(key)
|
| 409 |
+
|
| 410 |
+
first_launch = key not in self.cache
|
| 411 |
+
if first_launch:
|
| 412 |
+
self.nargs = dict(zip(self.arg_names, args))
|
| 413 |
+
pruned = self.prune_configs(kwargs)
|
| 414 |
+
|
| 415 |
+
def benchmark():
|
| 416 |
+
t0 = time.time()
|
| 417 |
+
self.cache[key] = self._bayesian_search(pruned, args, kwargs, key)
|
| 418 |
+
self._report_bench_failures()
|
| 419 |
+
self.bench_time = time.time() - t0
|
| 420 |
+
if knobs.autotuning.print:
|
| 421 |
+
fn_name = getattr(self.fn, "__name__", str(self.fn))
|
| 422 |
+
print(
|
| 423 |
+
f"[bayesian-autotune] {fn_name} tuned "
|
| 424 |
+
f"{len(self.configs_timings)} configs in {self.bench_time:.1f}s — "
|
| 425 |
+
f"key={key}, best={self.cache[key].all_kwargs()}"
|
| 426 |
+
)
|
| 427 |
+
full_nargs = {**self.nargs, **kwargs, **self.cache[key].all_kwargs()}
|
| 428 |
+
self.pre_hook(full_nargs, reset_only=True)
|
| 429 |
+
|
| 430 |
+
if self.cache_results:
|
| 431 |
+
self.check_disk_cache(key, pruned, benchmark)
|
| 432 |
+
else:
|
| 433 |
+
benchmark()
|
| 434 |
+
|
| 435 |
+
config = self.cache[key]
|
| 436 |
+
self.best_config = config
|
| 437 |
+
config_kwargs = getattr(config, "_all_kwargs_memo", None)
|
| 438 |
+
if config_kwargs is None:
|
| 439 |
+
config_kwargs = config._all_kwargs_memo = config.all_kwargs()
|
| 440 |
+
if config.pre_hook is not None:
|
| 441 |
+
if not first_launch: # tuned path already built nargs for prune_configs
|
| 442 |
+
self.nargs = dict(zip(self.arg_names, args))
|
| 443 |
+
config.pre_hook({**self.nargs, **kwargs, **config_kwargs})
|
| 444 |
+
ret = self.fn.run(*args, **kwargs, **config_kwargs)
|
| 445 |
+
if first_launch and self.finite_check_args:
|
| 446 |
+
# First launch after a key miss: the one regime where a crowned launch has been
|
| 447 |
+
# observed returning non-finite output while every subsequent identical launch
|
| 448 |
+
# is correct (large multi-device models; root cause open — see the DSV3
|
| 449 |
+
# investigation record). Detect via the declared output args and relaunch once,
|
| 450 |
+
# loudly. Sentinel/padding rows can false-trigger (uninitialized by contract);
|
| 451 |
+
# the cost is one duplicate launch per key, never a dropped config.
|
| 452 |
+
bad = self._nonfinite_output(kwargs)
|
| 453 |
+
if bad is not None:
|
| 454 |
+
logger.warning(
|
| 455 |
+
"[autotune] %s: non-finite %s on the FIRST launch of key=%s — relaunching once",
|
| 456 |
+
self.fn_name, bad, key,
|
| 457 |
+
)
|
| 458 |
+
ret = self.fn.run(*args, **kwargs, **config.all_kwargs())
|
| 459 |
+
self.nargs = None
|
| 460 |
+
return ret
|
| 461 |
+
|
| 462 |
+
def _bayesian_search(self, configs: List[Config], args, kwargs, key) -> Config:
|
| 463 |
+
timings: Dict[int, float] = {}
|
| 464 |
+
sigs = [tuple(sorted(c.all_kwargs().items())) for c in configs]
|
| 465 |
+
|
| 466 |
+
def bench_idx(idx: int) -> float:
|
| 467 |
+
if idx in timings:
|
| 468 |
+
return timings[idx]
|
| 469 |
+
try:
|
| 470 |
+
ms = self._bench(*args, config=configs[idx], **kwargs)
|
| 471 |
+
if isinstance(ms, (tuple, list)):
|
| 472 |
+
ms = ms[0]
|
| 473 |
+
timings[idx] = float(ms)
|
| 474 |
+
except Exception:
|
| 475 |
+
timings[idx] = float("inf")
|
| 476 |
+
self._log_result(key, configs[idx], timings[idx])
|
| 477 |
+
return timings[idx]
|
| 478 |
+
|
| 479 |
+
# Distinct values per config dimension, for the TPE's Laplace smoothing.
|
| 480 |
+
dim_vals: Dict = defaultdict(set)
|
| 481 |
+
for sig in sigs:
|
| 482 |
+
for d, v in sig:
|
| 483 |
+
dim_vals[d].add(v)
|
| 484 |
+
|
| 485 |
+
# Seed phase: one BASIN ANCHOR per (COMPUTE_MODE, SWAP_AB) group, the most recent
|
| 486 |
+
# cached key's best (warm start), then seeded-random fill. The anchors guarantee every
|
| 487 |
+
# categorical basin gets at least one real measurement — without them the TPE's
|
| 488 |
+
# per-dimension model can write off a whole axis it never saw succeed (two dsv4 tunes
|
| 489 |
+
# shipped 25-60% slow winners because their random seeds only sampled a basin's dead
|
| 490 |
+
# configs), and coordinate descent can't recover a winner two coupled flips away.
|
| 491 |
+
n_startup = max(2, min(self.n_startup_trials, self.n_trials))
|
| 492 |
+
anchors = self._basin_anchor_indices(configs)
|
| 493 |
+
order = list(range(len(configs)))
|
| 494 |
+
random.Random(0).shuffle(order)
|
| 495 |
+
warm_idx = self._warm_start_index(configs)
|
| 496 |
+
head = anchors + ([warm_idx] if warm_idx is not None else [])
|
| 497 |
+
order = list(dict.fromkeys(head + order))
|
| 498 |
+
for idx in order[: max(n_startup, len(head))]:
|
| 499 |
+
bench_idx(idx)
|
| 500 |
+
|
| 501 |
+
# TPE: split measured configs into good (top-gamma) / bad, build per-dimension value
|
| 502 |
+
# densities for each, and bench the unmeasured config maximizing log l(x) - log g(x)
|
| 503 |
+
# (Expected-Improvement proxy), updating the model after each measurement.
|
| 504 |
+
# inf (failed-to-compile) configs are EXCLUDED from the densities: a compile failure is
|
| 505 |
+
# evidence about that one joint shape (usually shared memory), not about its dimension
|
| 506 |
+
# values — counting them as "bad" buried SWAP_AB under a wall of BN=256 smem failures
|
| 507 |
+
# and made the tuner ship a 53µs winner while the 41µs swap config sat unbenched.
|
| 508 |
+
# They don't consume the trial budget either (n_trials = MEASURED configs; a failure
|
| 509 |
+
# stays in ``timings`` only as a skip-list entry) — the compile it burned is the one
|
| 510 |
+
# cost that can't be refunded here, which is what the smem/compile-guard pruners avoid.
|
| 511 |
+
warned_failures = False
|
| 512 |
+
while sum(1 for t in timings.values() if t != float("inf")) < self.n_trials:
|
| 513 |
+
# Failures don't consume the trial budget (above), which is right for a handful of
|
| 514 |
+
# smem/regs rejects but unbounded if a launch is broadly broken: the loop keeps
|
| 515 |
+
# picking, each pick costs an O(configs) TPE rescan, and the tune degenerates into a
|
| 516 |
+
# silent multi-hour spin instead of a diagnosable error. Cap the failures at the same
|
| 517 |
+
# budget as the measurements and fail loudly.
|
| 518 |
+
if (
|
| 519 |
+
not warned_failures
|
| 520 |
+
and sum(1 for t in timings.values() if t == float("inf")) >= self.max_failures
|
| 521 |
+
):
|
| 522 |
+
# Loud, once, and non-fatal. Aborting here would break tunes that DO find a winner
|
| 523 |
+
# after many rejects (mxfp4 W4A4 at a non-128 N burns >100), trading a silent
|
| 524 |
+
# inefficiency for a hard failure. Warning keeps the waste visible — this is how
|
| 525 |
+
# ~100 dead compiles per mx tune (an IndexError in load_act_mx's inline-quant arm)
|
| 526 |
+
# were found at all — without gating the kernel on a diagnostic.
|
| 527 |
+
warned_failures = True
|
| 528 |
+
self._report_bench_failures()
|
| 529 |
+
logger.warning(
|
| 530 |
+
"[autotune] %s: %d config(s) scored inf before the trial budget was met "
|
| 531 |
+
"(%d of them UNEXPECTED failures — the rest are deliberate guard rejections: "
|
| 532 |
+
"OutOfResources and tl.static_assert fences, which stock inf's internally). "
|
| 533 |
+
"The unexpected ones are dead compile time every tune, and a pruner gap "
|
| 534 |
+
"worth closing.",
|
| 535 |
+
self.fn_name,
|
| 536 |
+
sum(1 for t in timings.values() if t == float("inf")),
|
| 537 |
+
len(self._failures),
|
| 538 |
+
)
|
| 539 |
+
ranked = sorted(
|
| 540 |
+
(i for i, t in timings.items() if t != float("inf")), key=timings.get
|
| 541 |
+
)
|
| 542 |
+
if not ranked: # nothing compiled yet — keep seeding in shuffled order
|
| 543 |
+
nxt = next((i for i in order if i not in timings), None)
|
| 544 |
+
if nxt is None:
|
| 545 |
+
break
|
| 546 |
+
bench_idx(nxt)
|
| 547 |
+
continue
|
| 548 |
+
n_good = max(1, round(self.gamma * len(ranked)))
|
| 549 |
+
good_c: Dict = defaultdict(lambda: defaultdict(int))
|
| 550 |
+
bad_c: Dict = defaultdict(lambda: defaultdict(int))
|
| 551 |
+
for j, i in enumerate(ranked):
|
| 552 |
+
tgt = good_c if j < n_good else bad_c
|
| 553 |
+
for d, v in sigs[i]:
|
| 554 |
+
tgt[d][v] += 1
|
| 555 |
+
n_bad = len(ranked) - n_good
|
| 556 |
+
best_i, best_score = None, -math.inf
|
| 557 |
+
for i in range(len(configs)):
|
| 558 |
+
if i in timings:
|
| 559 |
+
continue
|
| 560 |
+
score = 0.0
|
| 561 |
+
for d, v in sigs[i]:
|
| 562 |
+
V = len(dim_vals[d])
|
| 563 |
+
lp = (good_c[d][v] + 1.0) / (
|
| 564 |
+
n_good + V
|
| 565 |
+
) # P(v | good), Laplace-smoothed
|
| 566 |
+
gp = (bad_c[d][v] + 1.0) / (n_bad + V) # P(v | bad)
|
| 567 |
+
score += math.log(lp) - math.log(gp)
|
| 568 |
+
if score > best_score:
|
| 569 |
+
best_score, best_i = score, i
|
| 570 |
+
if best_i is None:
|
| 571 |
+
break
|
| 572 |
+
bench_idx(best_i)
|
| 573 |
+
|
| 574 |
+
# Coordinate-descent refinement: try single-dim perturbations around
|
| 575 |
+
# the current best until no neighbor improves.
|
| 576 |
+
if self.refine:
|
| 577 |
+
for _ in range(self.max_refine_iters):
|
| 578 |
+
best_idx = min(timings, key=timings.get)
|
| 579 |
+
best_sig = sigs[best_idx]
|
| 580 |
+
best_ms = timings[best_idx]
|
| 581 |
+
improved = False
|
| 582 |
+
for i, s in enumerate(sigs):
|
| 583 |
+
if i in timings:
|
| 584 |
+
continue
|
| 585 |
+
diff = sum(1 for (_, a), (_, b) in zip(best_sig, s) if a != b)
|
| 586 |
+
if diff != 1:
|
| 587 |
+
continue
|
| 588 |
+
if bench_idx(i) < best_ms:
|
| 589 |
+
improved = True
|
| 590 |
+
if not improved:
|
| 591 |
+
break
|
| 592 |
+
|
| 593 |
+
# Crown RUNOFF: the search's single-shot timings carry enough noise to flip crowns
|
| 594 |
+
# between same-code tune rolls (gpt-oss prefill grouped rolled 1727-2150us on identical
|
| 595 |
+
# grids; the tuner's one RNG site is seeded, so noise is the only roll source). Re-time
|
| 596 |
+
# the top candidates back-to-back — same warmed process, adjacent in time, so they see
|
| 597 |
+
# the same clocks — and crown the winner of THAT comparison. Costs ~3 extra benches.
|
| 598 |
+
finite = sorted((t, i) for i, t in timings.items() if t != float("inf"))
|
| 599 |
+
if len(finite) > 1:
|
| 600 |
+
for _, i in finite[:3]:
|
| 601 |
+
timings.pop(i, None)
|
| 602 |
+
bench_idx(i) # memoizing closure: re-benches and re-records the popped entry
|
| 603 |
+
|
| 604 |
+
self.configs_timings = {configs[i]: t for i, t in timings.items()}
|
| 605 |
+
best = min(timings, key=timings.get)
|
| 606 |
+
if timings[best] == float("inf"):
|
| 607 |
+
# crowning an arbitrary broken config would persist it as `best` on disk and
|
| 608 |
+
# every later healthy process would replay it without re-tuning — the exact
|
| 609 |
+
# sticky-contaminated-context scenario inf-forgiveness exists to survive
|
| 610 |
+
self._report_bench_failures()
|
| 611 |
+
raise RuntimeError(
|
| 612 |
+
f"[bayesian-autotune] {self.fn_name}: every benched config failed "
|
| 613 |
+
"(all-inf tune) — nothing correct to crown or persist; the context may "
|
| 614 |
+
"be trap-contaminated (retry in a fresh process) or the grid is broken "
|
| 615 |
+
"for this launch (see the failure report above)."
|
| 616 |
+
)
|
| 617 |
+
return configs[best]
|
| 618 |
+
|
| 619 |
+
def _log_result(self, key, config: Config, ms: float):
|
| 620 |
+
"""Append one ``(key, config, ms)`` record as JSONL for offline analysis —
|
| 621 |
+
e.g. pruning configs that are consistently far off the per-key best. ``inf`` ms
|
| 622 |
+
marks a config that failed to compile/run (out of resources, etc.)."""
|
| 623 |
+
if not self.log_path:
|
| 624 |
+
return
|
| 625 |
+
try:
|
| 626 |
+
rec = {
|
| 627 |
+
"timestamp": time.time(),
|
| 628 |
+
"fn_name": getattr(self.fn, "__name__", str(self.fn)),
|
| 629 |
+
"key": list(key),
|
| 630 |
+
"kwargs": config.kwargs,
|
| 631 |
+
"num_warps": config.num_warps,
|
| 632 |
+
"num_stages": config.num_stages,
|
| 633 |
+
"ms": ms,
|
| 634 |
+
}
|
| 635 |
+
with open(self.log_path, "a") as f:
|
| 636 |
+
f.write(json.dumps(rec, default=str) + "\n")
|
| 637 |
+
except Exception:
|
| 638 |
+
pass
|
| 639 |
+
|
| 640 |
+
def _basin_anchor_indices(self, configs: List[Config]) -> List[int]:
|
| 641 |
+
"""One representative config index per (COMPUTE_MODE, SWAP_AB) basin — the MEDIAN in
|
| 642 |
+
tile-sort order, a mid-sized tile with mid warps/stages. Not the smallest: a basin's
|
| 643 |
+
minimal corner (min BK x 2 warps) can be latency-bound pathological (a 131µs anchor in
|
| 644 |
+
a basin whose peak is 41µs re-poisons the axis it was meant to protect). Coordinate
|
| 645 |
+
descent climbs BN/BK/warps/stages from wherever the TPE lands within the basin.
|
| 646 |
+
Returns [] when the grid has no such axes (single basin)."""
|
| 647 |
+
# Basin axes are DERIVED, not declared: a config kwarg with string or boolean
|
| 648 |
+
# values is a branch axis (different code path — compute mode, operand swap,
|
| 649 |
+
# warp specialization), which partitions the grid into disjoint performance
|
| 650 |
+
# basins. Numeric kwargs (tiles/warps/stages) are ordinal — the TPE's densities
|
| 651 |
+
# and coordinate descent handle those.
|
| 652 |
+
basin_axes = sorted(
|
| 653 |
+
{
|
| 654 |
+
k
|
| 655 |
+
for c in configs
|
| 656 |
+
for k, v in c.kwargs.items()
|
| 657 |
+
if isinstance(v, (bool, str))
|
| 658 |
+
}
|
| 659 |
+
)
|
| 660 |
+
if not basin_axes:
|
| 661 |
+
return []
|
| 662 |
+
|
| 663 |
+
# A basin = the compute-path axes (constexprs selecting different compiled code).
|
| 664 |
+
groups: Dict = {}
|
| 665 |
+
for i, c in enumerate(configs):
|
| 666 |
+
basin = tuple(c.kwargs.get(k) for k in basin_axes)
|
| 667 |
+
groups.setdefault(basin, []).append(i)
|
| 668 |
+
|
| 669 |
+
if len(groups) <= 1:
|
| 670 |
+
return []
|
| 671 |
+
|
| 672 |
+
def tile_order(i):
|
| 673 |
+
return (
|
| 674 |
+
configs[i].kwargs.get("BLOCK_SIZE_N", 0),
|
| 675 |
+
configs[i].kwargs.get("BLOCK_SIZE_K", 0),
|
| 676 |
+
configs[i].num_warps,
|
| 677 |
+
configs[i].num_stages,
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
# Median anchor per basin, PLUS one max-tile anchor per declared PATH-axis combo
|
| 681 |
+
# (``path_anchor_axes``). The median alone left the big-tile end unsampled: TPE
|
| 682 |
+
# densities lock onto the first-timed mid-tile region and coordinate descent cannot
|
| 683 |
+
# cross DIAGONAL ridges — dsv4 mxfp8 prefill gate_up's BN=128/BK=256 winner (840us)
|
| 684 |
+
# needs BN and warps to move together, so descent from the BN=64 crown rejects the
|
| 685 |
+
# lone BN step and n_trials 100/200/400 all missed it; one guaranteed big-tile
|
| 686 |
+
# sample fixed it (crown 958 -> 880 at the default budget). The max anchor rides
|
| 687 |
+
# the declared COARSE grouping, NOT the full branch-axis cross: per-basin max
|
| 688 |
+
# anchors on the weight-only grouped grid (32 basins: modes x WS x PS x 2 memory
|
| 689 |
+
# modes each way) put 64 forced trials into a 100-trial budget and starved the
|
| 690 |
+
# search — gpt-oss prefill regressed 3463 -> 4099us on a quiet box before this was
|
| 691 |
+
# scoped. Which axes qualify is the KERNEL's declaration (configuration knowledge);
|
| 692 |
+
# axes absent from this grid are ignored.
|
| 693 |
+
anchors = []
|
| 694 |
+
coarse_seen = set()
|
| 695 |
+
coarse_axes = [k for k in self.path_anchor_axes if k in basin_axes]
|
| 696 |
+
for basin, idxs in groups.items():
|
| 697 |
+
ordered = sorted(idxs, key=tile_order)
|
| 698 |
+
anchors.append(ordered[len(ordered) // 2])
|
| 699 |
+
if not self.path_anchor_axes:
|
| 700 |
+
continue # no declaration -> median anchors only
|
| 701 |
+
# declared axes absent from THIS grid still coarse-group to () — one global
|
| 702 |
+
# max-tile anchor, the big-tile guarantee without per-basin anchor spam
|
| 703 |
+
coarse = tuple(basin[basin_axes.index(k)] for k in coarse_axes)
|
| 704 |
+
if coarse not in coarse_seen and ordered[-1] != anchors[-1]:
|
| 705 |
+
coarse_seen.add(coarse)
|
| 706 |
+
anchors.append(ordered[-1])
|
| 707 |
+
return anchors
|
| 708 |
+
|
| 709 |
+
def _warm_start_index(self, configs: List[Config]):
|
| 710 |
+
"""Return the index in ``configs`` matching the most recently cached
|
| 711 |
+
key's best config (or ``None`` if no prior tune or no match in the
|
| 712 |
+
current pruned list)."""
|
| 713 |
+
if not self.cache:
|
| 714 |
+
return None
|
| 715 |
+
# Python 3.7+ dicts preserve insertion order; last entry = most recent tune.
|
| 716 |
+
prev_best = next(reversed(self.cache.values()))
|
| 717 |
+
prev_kwargs = prev_best.all_kwargs()
|
| 718 |
+
for i, c in enumerate(configs):
|
| 719 |
+
if c.all_kwargs() == prev_kwargs:
|
| 720 |
+
return i
|
| 721 |
+
return None
|
| 722 |
+
|
| 723 |
+
def check_disk_cache(self, tuning_key, configs, bench_fn):
|
| 724 |
+
"""Persist the tuned best config to Triton's on-disk cache so a later run (or process)
|
| 725 |
+
skips the whole minutes-long search+compile — most of which is Triton codegen, not
|
| 726 |
+
benching. Keyed exactly like Triton's own autotune cache (triton version + backend +
|
| 727 |
+
kernel source hash + invalidating env + key + the full config grid), so any of those
|
| 728 |
+
changing re-tunes.
|
| 729 |
+
|
| 730 |
+
Unlike Triton's stock version we do NOT bail when configs carry a ``pre_hook`` (ours
|
| 731 |
+
always do): we persist each config's ``all_kwargs()`` — serializable and pre_hook-free —
|
| 732 |
+
and on a hit re-match it to the live ``Config`` object, so the pre_hook is preserved from
|
| 733 |
+
code rather than lost in (de)serialization. A hit launches only the winning config."""
|
| 734 |
+
if not tuning_key:
|
| 735 |
+
bench_fn()
|
| 736 |
+
return False
|
| 737 |
+
|
| 738 |
+
from triton.compiler.compiler import make_backend
|
| 739 |
+
|
| 740 |
+
fn = self.fn
|
| 741 |
+
while not isinstance(fn, JITFunction):
|
| 742 |
+
fn = fn.fn
|
| 743 |
+
env_vars = get_cache_invalidating_env_vars()
|
| 744 |
+
cache_key = [
|
| 745 |
+
triton_key(),
|
| 746 |
+
make_backend(driver.active.get_current_target()).hash(),
|
| 747 |
+
fn.cache_key,
|
| 748 |
+
str(sorted(env_vars.items())),
|
| 749 |
+
# Key NAMES and the search budget are determinants too: tuning_key holds
|
| 750 |
+
# values only (renaming a key or swapping two same-typed keys would collide),
|
| 751 |
+
# and FINEGRAINED_AUTOTUNE_TRIALS is not in Triton's invalidating-env list —
|
| 752 |
+
# without n_trials a 3-trial exploratory winner gets replayed at 100 trials.
|
| 753 |
+
str(self.keys),
|
| 754 |
+
f"n_trials={self.n_trials},n_startup_trials={self.n_startup_trials},gamma={self.gamma}",
|
| 755 |
+
str(tuning_key),
|
| 756 |
+
] + [str(c) for c in configs] # str(Config) is pre_hook-free and process-stable
|
| 757 |
+
cache_key = hashlib.sha256("-".join(cache_key).encode("utf-8")).hexdigest()
|
| 758 |
+
cache = get_cache_manager(cache_key)
|
| 759 |
+
file_name = f"{fn.__name__[:150]}.bayes_autotune.json"
|
| 760 |
+
keylog = os.environ.get("FINEGRAINED_AUTOTUNE_KEYLOG")
|
| 761 |
+
if keylog:
|
| 762 |
+
with open(keylog, "a") as f:
|
| 763 |
+
f.write(json.dumps({
|
| 764 |
+
"fn": fn.__name__,
|
| 765 |
+
"hit": cache.get_file(file_name) is not None,
|
| 766 |
+
"tuning_key": str(tuning_key),
|
| 767 |
+
"keys": str(self.keys),
|
| 768 |
+
"n_configs": len(configs),
|
| 769 |
+
"env": str(sorted(env_vars.items())),
|
| 770 |
+
"fn_cache_key": fn.cache_key[:16],
|
| 771 |
+
"hash": cache_key[:16],
|
| 772 |
+
}) + "\n")
|
| 773 |
+
|
| 774 |
+
# signature -> live Config (carries the pre_hook); used to re-match the cached winner
|
| 775 |
+
by_sig = {tuple(sorted(c.all_kwargs().items())): c for c in configs}
|
| 776 |
+
|
| 777 |
+
def load_crown() -> bool:
|
| 778 |
+
path = cache.get_file(file_name)
|
| 779 |
+
if not path:
|
| 780 |
+
return False
|
| 781 |
+
try:
|
| 782 |
+
with open(path) as f:
|
| 783 |
+
data = json.load(f)
|
| 784 |
+
best = by_sig.get(tuple(sorted(data["best"].items())))
|
| 785 |
+
if best is None:
|
| 786 |
+
return False
|
| 787 |
+
self.cache[tuning_key] = best
|
| 788 |
+
self.configs_timings = {
|
| 789 |
+
by_sig[s]: t
|
| 790 |
+
for kw, t in data["timings"]
|
| 791 |
+
for s in (tuple(sorted(kw.items())),)
|
| 792 |
+
if s in by_sig
|
| 793 |
+
}
|
| 794 |
+
return True
|
| 795 |
+
except Exception:
|
| 796 |
+
return False # corrupt/stale cache file → re-tune
|
| 797 |
+
|
| 798 |
+
if load_crown():
|
| 799 |
+
return True
|
| 800 |
+
|
| 801 |
+
# Cross-process tuning lock. Without it, TP/EP ranks sharing this cache dir all tune the
|
| 802 |
+
# same key CONCURRENTLY and each crowns its own winner: the search is seeded-random and
|
| 803 |
+
# near-ties break on measurement noise, so ranks end up on different tiles, round
|
| 804 |
+
# differently, and greedy decoding turns that into different tokens (measured: an 8-rank
|
| 805 |
+
# MXFP8 run where 3 ranks emitted a different token). Serializing means one rank tunes and
|
| 806 |
+
# the rest read the same crown — identical configs, and ~world_size less tuning wall-clock.
|
| 807 |
+
# Best-effort: any lock failure falls through to tuning locally (correct, just redundant).
|
| 808 |
+
with _tuning_lock(cache, file_name):
|
| 809 |
+
if load_crown(): # another rank tuned this key while we waited
|
| 810 |
+
return True
|
| 811 |
+
bench_fn()
|
| 812 |
+
# publish INSIDE the lock: a rank released to an empty cache would tune again
|
| 813 |
+
try:
|
| 814 |
+
cache.put(
|
| 815 |
+
json.dumps(
|
| 816 |
+
{
|
| 817 |
+
"best": self.cache[tuning_key].all_kwargs(),
|
| 818 |
+
"timings": [
|
| 819 |
+
(c.all_kwargs(), t) for c, t in self.configs_timings.items()
|
| 820 |
+
],
|
| 821 |
+
}
|
| 822 |
+
),
|
| 823 |
+
file_name,
|
| 824 |
+
binary=False,
|
| 825 |
+
)
|
| 826 |
+
except Exception:
|
| 827 |
+
pass
|
| 828 |
+
return False
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
@contextmanager
|
| 832 |
+
def _tuning_lock(cache, file_name: str):
|
| 833 |
+
"""Serialize tuning of ONE key across every process sharing the Triton cache directory.
|
| 834 |
+
|
| 835 |
+
Held while a key is tuned and its crown published, so concurrent TP/EP ranks converge on the
|
| 836 |
+
same config instead of each crowning its own (see ``check_disk_cache``). Advisory ``flock`` on
|
| 837 |
+
a sidecar file next to the crown; entirely best-effort — no cache dir, no ``fcntl``, or a lock
|
| 838 |
+
that cannot be taken within ``FINEGRAINED_TUNE_LOCK_TIMEOUT`` (default 30 min, since the holder
|
| 839 |
+
is doing a real tune) all fall through to tuning locally. Losing the lock costs redundant work,
|
| 840 |
+
never correctness."""
|
| 841 |
+
lock_file = None
|
| 842 |
+
try:
|
| 843 |
+
import fcntl
|
| 844 |
+
|
| 845 |
+
cache_dir = getattr(cache, "cache_dir", None)
|
| 846 |
+
if cache_dir:
|
| 847 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 848 |
+
deadline = time.time() + float(
|
| 849 |
+
os.environ.get("FINEGRAINED_TUNE_LOCK_TIMEOUT") or 1800
|
| 850 |
+
)
|
| 851 |
+
lock_file = open(os.path.join(cache_dir, f"{file_name}.lock"), "w")
|
| 852 |
+
while True:
|
| 853 |
+
try:
|
| 854 |
+
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 855 |
+
break
|
| 856 |
+
except BlockingIOError:
|
| 857 |
+
if time.time() > deadline:
|
| 858 |
+
logger.warning(
|
| 859 |
+
"[autotune] %s: tuning lock held elsewhere for >%.0fs — tuning locally",
|
| 860 |
+
file_name,
|
| 861 |
+
time.time() - (deadline - 1800),
|
| 862 |
+
)
|
| 863 |
+
lock_file.close()
|
| 864 |
+
lock_file = None
|
| 865 |
+
break
|
| 866 |
+
time.sleep(1.0)
|
| 867 |
+
except Exception:
|
| 868 |
+
lock_file = None
|
| 869 |
+
try:
|
| 870 |
+
yield
|
| 871 |
+
finally:
|
| 872 |
+
if lock_file is not None:
|
| 873 |
+
try:
|
| 874 |
+
import fcntl
|
| 875 |
+
|
| 876 |
+
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
| 877 |
+
finally:
|
| 878 |
+
lock_file.close()
|
| 879 |
+
|
| 880 |
+
|
| 881 |
+
|
| 882 |
+
def bayesian_autotune(
|
| 883 |
+
configs,
|
| 884 |
+
key,
|
| 885 |
+
*,
|
| 886 |
+
n_trials: int = 80,
|
| 887 |
+
max_failures: int | None = None,
|
| 888 |
+
n_startup_trials: int = 12,
|
| 889 |
+
gamma: float = 0.25,
|
| 890 |
+
refine: bool = True,
|
| 891 |
+
max_refine_iters: int = 5,
|
| 892 |
+
log_path: str | None = None,
|
| 893 |
+
path_anchor_axes: tuple[str, ...] = (),
|
| 894 |
+
finite_check_args: tuple[str, ...] = (),
|
| 895 |
+
cache_results: bool = True,
|
| 896 |
+
reset_to_zero=None,
|
| 897 |
+
restore_value=None,
|
| 898 |
+
**kwargs,
|
| 899 |
+
):
|
| 900 |
+
"""Decorator mirroring ``@triton.autotune``. Extra kwargs:
|
| 901 |
+
n_trials: successfully measured configs per key (TPE budget; configs
|
| 902 |
+
that fail to compile/run are skipped without consuming it)
|
| 903 |
+
n_startup_trials: random seed configs before the TPE model kicks in
|
| 904 |
+
gamma: top fraction of measured configs treated as "good"
|
| 905 |
+
refine, max_refine_iters: coordinate-descent refinement after the TPE
|
| 906 |
+
path_anchor_axes: branch axes (kwarg names) whose values relocate the tile
|
| 907 |
+
optimum — one guaranteed max-tile anchor per value combo
|
| 908 |
+
(see _basin_anchor_indices); the kernel declares them, the
|
| 909 |
+
tuner stays independent of configuration details
|
| 910 |
+
max_failures: abort the tune once this many configs fail to compile/run before
|
| 911 |
+
the measured budget is met (default n_trials; env override
|
| 912 |
+
FINEGRAINED_AUTOTUNE_MAX_FAILURES). Without it a broadly broken
|
| 913 |
+
grid spins: failures don't consume the trial budget, so the search
|
| 914 |
+
keeps picking and each pick costs an O(configs) rescan.
|
| 915 |
+
log_path: JSONL of benched configs (or FINEGRAINED_AUTOTUNE_LOG)
|
| 916 |
+
cache_results: persist the tuned best config to disk (on by default) so later
|
| 917 |
+
runs skip the search+compile — see BayesianAutotuner.check_disk_cache"""
|
| 918 |
+
|
| 919 |
+
def decorator(fn):
|
| 920 |
+
return BayesianAutotuner(
|
| 921 |
+
fn,
|
| 922 |
+
fn.arg_names,
|
| 923 |
+
configs,
|
| 924 |
+
key,
|
| 925 |
+
reset_to_zero,
|
| 926 |
+
restore_value,
|
| 927 |
+
n_trials=n_trials,
|
| 928 |
+
max_failures=max_failures,
|
| 929 |
+
n_startup_trials=n_startup_trials,
|
| 930 |
+
gamma=gamma,
|
| 931 |
+
refine=refine,
|
| 932 |
+
max_refine_iters=max_refine_iters,
|
| 933 |
+
log_path=log_path,
|
| 934 |
+
path_anchor_axes=path_anchor_axes,
|
| 935 |
+
finite_check_args=finite_check_args,
|
| 936 |
+
cache_results=cache_results,
|
| 937 |
+
**kwargs,
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
return decorator
|
build/torch-xpu/compat.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import contextvars
|
| 17 |
+
import functools
|
| 18 |
+
from contextlib import contextmanager
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
import os
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import triton
|
| 25 |
+
import triton.language as tl
|
| 26 |
+
from torch.library import triton_op, wrap_triton
|
| 27 |
+
|
| 28 |
+
# Programmatic dependent launch for the decode chain (batched GEMMs, GLU, act quant, reduce): each
|
| 29 |
+
# kernel waits on its grid dependency at the top and releases its dependents just before the
|
| 30 |
+
# epilogue. The LATE trigger is load-bearing: released early, the next kernel's CTAs co-reside and
|
| 31 |
+
# steal issue slots (-15us on a single-wave chain); released late they only hide launch latency
|
| 32 |
+
# (GLM/DSV4/GPT-OSS decode -4.5..-4.9%, bit-identical). Prefill grids are multi-wave and never
|
| 33 |
+
# take it. FINEGRAINED_PDL=0 disables.
|
| 34 |
+
DECODE_PDL = os.environ.get("FINEGRAINED_PDL", "1") == "1"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def decode_pdl() -> bool:
|
| 38 |
+
"""PDL for this launch: the flag, and not under torch.compile — dynamo's TTIR access analysis
|
| 39 |
+
cannot read the ``griddepcontrol`` inline asm and would mark every input mutated (extra copies,
|
| 40 |
+
no fusion). Deployment decode runs eager launches under cudagraphs, where PDL applies."""
|
| 41 |
+
return DECODE_PDL and not torch.compiler.is_compiling()
|
| 42 |
+
|
| 43 |
+
# kernel-builder generates ``_ops.py`` into the built variant (the op namespace carries the build's
|
| 44 |
+
# unique id) and refuses to build over a tracked one, so the source tree has none: an unbuilt
|
| 45 |
+
# checkout (tests, bench, FINEGRAINED_KERNELS_PATH) registers its ops under the plain package name.
|
| 46 |
+
try:
|
| 47 |
+
from ._ops import add_op_namespace_prefix
|
| 48 |
+
except ImportError:
|
| 49 |
+
|
| 50 |
+
def add_op_namespace_prefix(name: str) -> str:
|
| 51 |
+
return f"finegrained_kernels::{name}"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ── Format constants ──────────────────────────────────────────────────────────
|
| 58 |
+
|
| 59 |
+
# FP8 (E4M3) is the main format for weights and activations;
|
| 60 |
+
FP8_DTYPE = torch.float8_e4m3fn
|
| 61 |
+
|
| 62 |
+
# FP4 (E2M1) packs two 4-bit nibbles per byte. MX formats (MXFP4 weights, MXFP8
|
| 63 |
+
# E4M3 weights/activations) share one UE8M0 scale per 32-element K-group — the OCP
|
| 64 |
+
# MX block size, consumed by ``tl.dot_scaled``. Format constants, not tunables.
|
| 65 |
+
NIBBLES_PER_BYTE = 2
|
| 66 |
+
|
| 67 |
+
MX_SCALE_GROUP_K = 32
|
| 68 |
+
|
| 69 |
+
NVFP4_SCALE_GROUP_K = 16
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ── Host-side helpers ─────────────────────────────────────────────────────────
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# set ONLY while an opaque op's registered fake impl runs (shape inference); read by
|
| 77 |
+
# compile_time_only_triton_wrap to no-op the kernel launches within
|
| 78 |
+
_SKIP_LAUNCHES = contextvars.ContextVar("finegrained_kernels_skip_launches", default=False)
|
| 79 |
+
# Plain mirror of the contextvar, read on the launch path. Dynamo cannot trace `ContextVar.get`
|
| 80 |
+
# ("Unsupported method call ... method `get` of class `ContextVar`"), which broke tracing of every
|
| 81 |
+
# non-opaque op; a module-level bool it can constant-fold. The contextvar stays authoritative for
|
| 82 |
+
# set/reset (nesting, async isolation) — this only mirrors it for the read.
|
| 83 |
+
_SKIP_LAUNCHES_MIRROR = False
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def compile_time_only_triton_wrap(kernel):
|
| 88 |
+
"""``wrap_triton`` while torch.compile is tracing (required to capture a raw Triton
|
| 89 |
+
launch into the graph), the bare kernel in eager. Every kernel launch goes through
|
| 90 |
+
this: eager skips ``wrap_triton``'s per-call dispatch overhead, and it dodges a
|
| 91 |
+
correctness trap — eager ``wrap_triton`` of a stock ``@triton.autotune`` kernel that
|
| 92 |
+
has ``prune_configs_by`` re-runs the FULL tune on every call (torch's wrapper rebuilds
|
| 93 |
+
a fresh autotuner around the pruned configs per call, ``triton_kernel_wrap.py``:
|
| 94 |
+
``autotune(configs=pruned, key=[])``), so nothing ever lands in the original
|
| 95 |
+
``Autotuner.cache`` (~2.3s/call for ``_mx_act_quant_kernel``) and the mid-capture
|
| 96 |
+
tune invalidates cudagraph capture. Pruner-less kernels and the ``bayesian_autotune``
|
| 97 |
+
kernels reuse their cache through the wrapper (measured) — but eager never needs the
|
| 98 |
+
wrapper at all.
|
| 99 |
+
|
| 100 |
+
Inside an OPAQUE op's fake impl (see ``compile_time_only_triton_op``) the launch is
|
| 101 |
+
skipped outright: the op function has already allocated the correctly-shaped
|
| 102 |
+
outputs, which is all fake mode needs. The skip is an EXPLICIT contextvar, never
|
| 103 |
+
ambient fake-mode detection — ``triton_op``'s capture also runs under FakeTensor
|
| 104 |
+
mode, and skipping there would compile graphs with the kernel MISSING (uninitialized
|
| 105 |
+
outputs; found as identical garbage across formats, 2026-07-16). The skip is also
|
| 106 |
+
load-bearing in the other direction — probed: without it the opaque ops' fake impls
|
| 107 |
+
reach the launcher with FakeTensors ("RuntimeError when making fake tensor call";
|
| 108 |
+
``is_compiling`` is still true during fake prop, so they'd take the wrap_triton
|
| 109 |
+
branch and hit the pre_hook assert that made them opaque in the first place)."""
|
| 110 |
+
if _SKIP_LAUNCHES_MIRROR:
|
| 111 |
+
|
| 112 |
+
class _NoLaunch:
|
| 113 |
+
def __getitem__(self, grid):
|
| 114 |
+
return lambda *args, **kwargs: None
|
| 115 |
+
|
| 116 |
+
return _NoLaunch()
|
| 117 |
+
return wrap_triton(kernel) if torch.compiler.is_compiling() else kernel
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def compile_time_only_triton_op(name, mutates_args=(), opaque=False):
|
| 122 |
+
"""``@triton_op`` under torch.compile (the registered custom op is what dynamo
|
| 123 |
+
captures), the plain function in eager: the torch.library op dispatch stack costs
|
| 124 |
+
~160µs per eager call — the dominant decode CPU cost in the eager-breakdown probe —
|
| 125 |
+
and eager needs none of it. The op is still registered at import time, so compiled
|
| 126 |
+
callers and ``torch.ops`` introspection see it unchanged. Sibling of
|
| 127 |
+
``compile_time_only_triton_wrap`` (same rule one level down, at the kernel launch)."""
|
| 128 |
+
|
| 129 |
+
def decorator(fn):
|
| 130 |
+
if opaque:
|
| 131 |
+
# kernels whose configs carry a pre_hook (TMA descriptor rebinders) cannot
|
| 132 |
+
# be traced by triton_op's capture; register OPAQUE instead — the fake impl
|
| 133 |
+
# is the op fn itself, whose launches no-op under FakeTensor mode (see
|
| 134 |
+
# compile_time_only_triton_wrap), leaving just the shape-correct allocations
|
| 135 |
+
op = torch.library.custom_op(name, mutates_args=mutates_args)(fn)
|
| 136 |
+
|
| 137 |
+
@functools.wraps(fn)
|
| 138 |
+
def fake_impl(*args, **kwargs):
|
| 139 |
+
global _SKIP_LAUNCHES_MIRROR
|
| 140 |
+
token = _SKIP_LAUNCHES.set(True)
|
| 141 |
+
prev, _SKIP_LAUNCHES_MIRROR = _SKIP_LAUNCHES_MIRROR, True
|
| 142 |
+
try:
|
| 143 |
+
return fn(*args, **kwargs)
|
| 144 |
+
finally:
|
| 145 |
+
_SKIP_LAUNCHES.reset(token)
|
| 146 |
+
_SKIP_LAUNCHES_MIRROR = prev
|
| 147 |
+
|
| 148 |
+
op.register_fake(fake_impl)
|
| 149 |
+
else:
|
| 150 |
+
op = triton_op(name, mutates_args=mutates_args)(fn)
|
| 151 |
+
|
| 152 |
+
@functools.wraps(fn)
|
| 153 |
+
def dispatch(*args, **kwargs):
|
| 154 |
+
# The op path is also the AUTOGRAD path (`register_autograd` hangs off the op, and
|
| 155 |
+
# these ops take the RAW activation, so that is where a usable gradient exists).
|
| 156 |
+
# Calling `fn` skips the dispatcher entirely, so a differentiable caller would
|
| 157 |
+
# silently get no gradient — route through the op whenever a tensor wants one.
|
| 158 |
+
# Training runs at prefill shapes where the ~160us dispatch is noise; inference
|
| 159 |
+
# runs under no_grad and keeps the bare-function fast path.
|
| 160 |
+
if torch.compiler.is_compiling() or (
|
| 161 |
+
torch.is_grad_enabled()
|
| 162 |
+
and any(getattr(a, "requires_grad", False) for a in args)
|
| 163 |
+
):
|
| 164 |
+
return op(*args, **kwargs)
|
| 165 |
+
return fn(*args, **kwargs)
|
| 166 |
+
|
| 167 |
+
dispatch._triton_op = op
|
| 168 |
+
return dispatch
|
| 169 |
+
|
| 170 |
+
return decorator
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
@contextmanager
|
| 175 |
+
def device_context(device: torch.device):
|
| 176 |
+
"""Context manager that sets the active device for any backend (cuda, xpu, etc.)."""
|
| 177 |
+
backend = getattr(torch, device.type, None)
|
| 178 |
+
if backend is not None and hasattr(backend, "device"):
|
| 179 |
+
with backend.device(device):
|
| 180 |
+
yield
|
| 181 |
+
else:
|
| 182 |
+
yield
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
@functools.lru_cache(maxsize=8)
|
| 187 |
+
def sm_count(device_index: int) -> int:
|
| 188 |
+
"""Processor (CUDA SM / XPU Xe-core) count for a device (a constant) — cached so callers
|
| 189 |
+
don't re-query it each launch. Read from Triton's active driver so it works on any backend,
|
| 190 |
+
with a CUDA fallback."""
|
| 191 |
+
try:
|
| 192 |
+
return triton.runtime.driver.active.utils.get_device_properties(device_index)[
|
| 193 |
+
"multiprocessor_count"
|
| 194 |
+
]
|
| 195 |
+
except Exception:
|
| 196 |
+
active_device = get_active_device_type()
|
| 197 |
+
if active_device == "cuda":
|
| 198 |
+
return torch.cuda.get_device_properties(device_index).multi_processor_count
|
| 199 |
+
elif active_device == "xpu":
|
| 200 |
+
return torch.xpu.get_device_properties(device_index).multi_processor_count
|
| 201 |
+
else:
|
| 202 |
+
raise RuntimeError(
|
| 203 |
+
f"Unsupported device type {active_device} for sm_count; only cuda/xpu are supported."
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def tl_dtype(dtype: torch.dtype) -> tl.dtype:
|
| 209 |
+
"""The ``tl`` dtype matching a torch dtype (``torch.bfloat16`` → ``tl.bfloat16``) — the
|
| 210 |
+
attribute names line up, so no table. For passing a tensor's dtype as a kernel constexpr
|
| 211 |
+
when the kernel can't read it off a pointer argument."""
|
| 212 |
+
return getattr(tl, str(dtype).removeprefix("torch."))
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def get_active_device_type() -> str:
|
| 217 |
+
"""Active torch device type for the current Triton backend (``"cuda"``, ``"xpu"``, ...).
|
| 218 |
+
|
| 219 |
+
Falls back to ``"cuda"`` when no driver is loaded — Triton raises
|
| 220 |
+
``RuntimeError: 0 active drivers ([])`` on driverless build boxes, and the
|
| 221 |
+
autotune-config builder is evaluated at module-import time under the
|
| 222 |
+
``@triton.autotune`` decorator (no kernel launches there, so the default is
|
| 223 |
+
only used to shape the config list).
|
| 224 |
+
"""
|
| 225 |
+
try:
|
| 226 |
+
return triton.runtime.driver.active.get_active_torch_device().type
|
| 227 |
+
except RuntimeError:
|
| 228 |
+
return "cuda"
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def resolve_memory_modes(memory_modes):
|
| 233 |
+
"""Resolve the generic ``"descriptor"`` B_MEMORY_MODE to the device's flavor: host-built
|
| 234 |
+
(NVIDIA TMA) on CUDA, device-built in-kernel tensormap on XPU. Installs the XPU
|
| 235 |
+
scratch allocator device-built tensormaps need."""
|
| 236 |
+
descriptor = (
|
| 237 |
+
"device_descriptor" if get_active_device_type() == "xpu" else "host_descriptor"
|
| 238 |
+
)
|
| 239 |
+
memory_modes = tuple(descriptor if m == "descriptor" else m for m in memory_modes)
|
| 240 |
+
if "device_descriptor" in memory_modes:
|
| 241 |
+
# device-side descriptors build in-kernel tensormaps that need a scratch
|
| 242 |
+
# allocator — on whichever accelerator is active (XPU always; CUDA when a kernel
|
| 243 |
+
# requests "device_descriptor" explicitly to skip the host-descriptor plumbing).
|
| 244 |
+
device_type = get_active_device_type()
|
| 245 |
+
triton.set_allocator(
|
| 246 |
+
lambda size, alignment, stream: torch.empty(
|
| 247 |
+
size, dtype=torch.int8, device=device_type
|
| 248 |
+
)
|
| 249 |
+
)
|
| 250 |
+
return memory_modes
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def get_accelerator_autotuning_configs(
|
| 255 |
+
*,
|
| 256 |
+
mx: bool = False,
|
| 257 |
+
tune_block_m: bool = False,
|
| 258 |
+
tune_block_n: bool = False,
|
| 259 |
+
tune_block_nk: bool = False,
|
| 260 |
+
swap_ab: bool = False,
|
| 261 |
+
warp_spec: bool = False,
|
| 262 |
+
packed_schedule: bool = False,
|
| 263 |
+
compute_modes=None,
|
| 264 |
+
a_memory_modes=None,
|
| 265 |
+
b_memory_modes=None,
|
| 266 |
+
pre_hook=None,
|
| 267 |
+
):
|
| 268 |
+
"""Autotune search grid for the current accelerator — the single generator for every
|
| 269 |
+
GEMM kernel. One union span serves both families: num_warps {2,4,8,16} x num_stages
|
| 270 |
+
{2..6} (XPU: warps {8,16}); the tile comes from the caller's ``block_size``, or
|
| 271 |
+
``tune_block_nk`` spans BN {32,64,128,256} x BK {64,128,256,512}. ``mx=True`` (group-32
|
| 272 |
+
scale formats, MXFP4/MXFP8) requires ``compute_modes`` (BK is coupled per mode) and
|
| 273 |
+
``tune_block_nk`` (MX kernels have no caller block_size — the tile is always tuned).
|
| 274 |
+
|
| 275 |
+
``compute_modes`` emits the ``COMPUTE_MODE`` axis (``None`` — the default — emits no
|
| 276 |
+
axis, like ``b_memory_modes``), tuner-selected per workload (token count is in the
|
| 277 |
+
key): ``"dot_scaled"`` native group-32 scaled MMA (wide BK {128,256}; wins once the
|
| 278 |
+
grid saturates, ~S>=32), ``"dot"`` fp8 ``tl.dot`` + per-group software rescale
|
| 279 |
+
(under ``mx`` its BK is pinned to the 32-group), ``"scalar"`` CUDA-core FMA reduce
|
| 280 |
+
(no MMA M->16 pad; wins memory-bound decode, only emit where implemented — it is
|
| 281 |
+
pinned to BM=1 here because a bigger-BM scalar config at BN == BM COMPILES and
|
| 282 |
+
silently computes garbage the tuner would time and pick).
|
| 283 |
+
|
| 284 |
+
Axes, crossed per flag:
|
| 285 |
+
|
| 286 |
+
- ``tune_block_m``: BLOCK_SIZE_M in {16,32,64,128} instead of a fixed
|
| 287 |
+
launch-time choice.
|
| 288 |
+
- ``tune_block_n``: independent BLOCK_SIZE_N in {64,128,256} for the block-scale
|
| 289 |
+
kernels whose BK is pinned by the caller (activation scale groups are per
|
| 290 |
+
block_k) but whose N tile may grow past the scale granularity.
|
| 291 |
+
- ``swap_ab``: marks the single-token decode GEVM kernels — crosses in the coupled
|
| 292 |
+
decode ``(BLOCK_SIZE_M, SWAP_AB)`` pairs (see below). BK=512 earns its union-span
|
| 293 |
+
seat here: at decode's tiny memory-parallelism-bound grids, longer per-iteration
|
| 294 |
+
bursts are the one in-block lever that helps (+12% on the dsv4 swap GEMV; deeper
|
| 295 |
+
num_stages and split-K both measured WORSE). Prefill tunes carry the same rows as
|
| 296 |
+
trial-budget dilution (descriptor_box_pruner drops the illegal e4m3 boxes).
|
| 297 |
+
- ``warp_spec`` (CUDA only): warp-specialize the K-loop, crossed over every mode —
|
| 298 |
+
pair it with ``warp_spec_compile_guard_pruner``, which owns the can't-compile
|
| 299 |
+
regions (including dot_scaled + WS, a PassManager failure). Compile support is
|
| 300 |
+
(shape, config)-dependent on Triton 3.7.1, so it is a tuner axis — failures score
|
| 301 |
+
inf and self-prune; where it compiles it is both faster and (bd grouped gate_up)
|
| 302 |
+
load-bearing for correctness.
|
| 303 |
+
- ``a_memory_modes``: the A_MEMORY_MODE activation-load axis (descriptor legal only
|
| 304 |
+
without a gather — the tile's rows are the contiguous sorted positions; the pruner
|
| 305 |
+
fences descriptor rows when GatherIdx is passed).
|
| 306 |
+
- ``b_memory_modes``: the B_MEMORY_MODE weight-load axis — pass
|
| 307 |
+
``("descriptor", "pointer")`` to let the tuner pick the device's tensor-descriptor
|
| 308 |
+
flavor vs explicit pointers; ``None`` (default) emits no axis. In the plain-dot
|
| 309 |
+
kernels the descriptor arm is the SWAPPED loop (see the bd 2D kernel).
|
| 310 |
+
|
| 311 |
+
A given ``pre_hook`` is attached to every config and must self-guard on
|
| 312 |
+
``B_MEMORY_MODE``.
|
| 313 |
+
|
| 314 |
+
Winner census over 743 tunes (B200, all kernels/shapes), useful when reading tuner
|
| 315 |
+
logs: every value wins somewhere — MX lives in stages 5/6 (69 wins) and never picked
|
| 316 |
+
warps 16; plain-dot picked warps 16 (19 wins, e.g. the bd gate_up decode) and warps 2
|
| 317 |
+
(24 wins, the tensor batched decode 9/11) and concentrated in stages 3/4.
|
| 318 |
+
"""
|
| 319 |
+
assert not (mx and compute_modes is None), (
|
| 320 |
+
"the MX grid couples BLOCK_SIZE_K to COMPUTE_MODE — pass compute_modes"
|
| 321 |
+
)
|
| 322 |
+
assert not (mx and not tune_block_nk), (
|
| 323 |
+
"MX kernels have no caller block_size — their (BN, BK) tile is always tuned"
|
| 324 |
+
)
|
| 325 |
+
is_xpu = get_active_device_type() == "xpu"
|
| 326 |
+
|
| 327 |
+
# ONE union span for both families — the per-family sets were stock-tuner-era cost
|
| 328 |
+
# caps; the Bayesian tuner samples a fixed trial budget, values measured dead for
|
| 329 |
+
# one family (MX warps=16, MX BK=64; plain-dot stages 5/6 were simply never tested)
|
| 330 |
+
# just never win, and can't-compile configs (smem) score inf and stay out of the
|
| 331 |
+
# TPE densities. The 743-tune winner census (see docstring) maps where winners
|
| 332 |
+
# actually live per family. BK=512 is in the union span for every tune_block_nk
|
| 333 |
+
# kernel — its win is decode-only (see the swap_ab docstring bullet), and prefill
|
| 334 |
+
# rows are accepted trial-budget dilution.
|
| 335 |
+
#
|
| 336 |
+
# Axes measured DEAD and deliberately absent (do not re-add without fresh evidence):
|
| 337 |
+
# num_ctas slower everywhere + miscompiles with TMA at num_stages<=4
|
| 338 |
+
# flatten (tl.range) proxy +17% never survived e2e (<=1%; gate_up regressed)
|
| 339 |
+
# persistent 2D grid-stride 2D prototype's win was an inline-vs-helper artifact
|
| 340 |
+
# batched WARP_SPEC decode loops are launch/latency-bound; WS never won a batched
|
| 341 |
+
# cell, so the axis is not emitted at the six batched sites
|
| 342 |
+
num_warps = [8, 16] if is_xpu else [2, 4, 8, 16]
|
| 343 |
+
num_stages = [2, 3, 4, 5, 6]
|
| 344 |
+
bn_span = (128,) if is_xpu else (32, 64, 128, 256)
|
| 345 |
+
bk_span = (128,) if is_xpu else (64, 128, 256, 512)
|
| 346 |
+
|
| 347 |
+
# no tuned tile -> one empty meta-dict (the tile comes from the launch kwargs)
|
| 348 |
+
blocks = (
|
| 349 |
+
[{"BLOCK_SIZE_N": bn, "BLOCK_SIZE_K": bk} for bn in bn_span for bk in bk_span]
|
| 350 |
+
if tune_block_nk
|
| 351 |
+
else [{}]
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
if compute_modes is not None:
|
| 355 |
+
# generic COMPUTE_MODE axis; MX "dot"'s BK is STRUCTURALLY the UE8M0 scale group
|
| 356 |
+
# (one software rescale per K-step — a format constraint, not a tile choice), so
|
| 357 |
+
# it overrides the wide-BK span and the dedup below collapses the copies.
|
| 358 |
+
blocks = [
|
| 359 |
+
{
|
| 360 |
+
**b,
|
| 361 |
+
"COMPUTE_MODE": mode,
|
| 362 |
+
**({"BLOCK_SIZE_K": MX_SCALE_GROUP_K} if mx and mode == "dot" else {}),
|
| 363 |
+
}
|
| 364 |
+
for b in blocks
|
| 365 |
+
for mode in compute_modes
|
| 366 |
+
]
|
| 367 |
+
|
| 368 |
+
if tune_block_m:
|
| 369 |
+
# BM=256 is OPEN, not disproven (2026-07-29). A plain fp8 GEMM at the dsv4 attn prefill
|
| 370 |
+
# shape clearly prefers BM=BN=256 (350-367us vs 420-478us at 128, 7 interleaved repeats,
|
| 371 |
+
# no overlap), so the basin is real for an unscaled loop. Our kernel measured 523us vs
|
| 372 |
+
# 495us against it — but the run-to-run spread at BM=128 on this box is 13.9%, so a 5.8%
|
| 373 |
+
# difference decides nothing. Left out because nothing has shown it WINNING here and the
|
| 374 |
+
# axis widens every tune 25%; re-add only with repeated tunes that clear the noise floor.
|
| 375 |
+
blocks = [{**b, "BLOCK_SIZE_M": bm} for b in blocks for bm in (16, 32, 64, 128)]
|
| 376 |
+
|
| 377 |
+
if tune_block_n:
|
| 378 |
+
blocks = [{**b, "BLOCK_SIZE_N": bn} for b in blocks for bn in (64, 128, 256)]
|
| 379 |
+
|
| 380 |
+
if warp_spec and get_active_device_type() == "cuda":
|
| 381 |
+
blocks = [{**b, "WARP_SPEC": ws} for b in blocks for ws in (False, True)]
|
| 382 |
+
|
| 383 |
+
if packed_schedule:
|
| 384 |
+
# grouped M-tile resolution axis: packed-table loads vs the E-wide register-resident
|
| 385 |
+
# layout — pure register relief, bit-identical tiles; wins only where the inline
|
| 386 |
+
# resolve is pressure-walled (dsv4 BM128/BK256 regs 244->187, +8%), so the tuner picks
|
| 387 |
+
blocks = [{**b, "PACKED_SCHEDULE": ps} for b in blocks for ps in (False, True)]
|
| 388 |
+
|
| 389 |
+
# (descriptor-mode can't-win/can't-serve regions are fenced per kernel: descriptor_box_pruner,
|
| 390 |
+
# matched_memory_modes_pruner, descriptor_needs_prequant_pruner)
|
| 391 |
+
if a_memory_modes is not None:
|
| 392 |
+
blocks = [
|
| 393 |
+
{**b, "A_MEMORY_MODE": mm}
|
| 394 |
+
for b in blocks
|
| 395 |
+
for mm in resolve_memory_modes(a_memory_modes)
|
| 396 |
+
]
|
| 397 |
+
if b_memory_modes is not None:
|
| 398 |
+
blocks = [
|
| 399 |
+
{**b, "B_MEMORY_MODE": mm}
|
| 400 |
+
for b in blocks
|
| 401 |
+
for mm in resolve_memory_modes(b_memory_modes)
|
| 402 |
+
]
|
| 403 |
+
|
| 404 |
+
# The coupled decode (BLOCK_SIZE_M, SWAP_AB) pairs — the single source of the swap/BM
|
| 405 |
+
# coupling. At M=1 the MMA 16-atom is filled either by replicating the token (non-swap
|
| 406 |
+
# BM=16, ~40% over the degenerate BM=1 on plain tl.dot) or by putting the weight's
|
| 407 |
+
# output rows in M (swap, BM=1). Swap REQUIRES BM=1 (its rhs is the single token
|
| 408 |
+
# padded to the N=16 atom), so (16, swap) never exists; the tuner picks. Prefill
|
| 409 |
+
# kernels leave SWAP_AB unset — the prefill swap arms measured losers everywhere
|
| 410 |
+
# (see the bd 2D kernel's tuner note).
|
| 411 |
+
if swap_ab and tune_block_m:
|
| 412 |
+
# tuned-BM kernels (the 2D mx GEMM): the prefill BM sweep keeps no-swap; the swap
|
| 413 |
+
# decode rows are APPENDED at their structural BM=1 (dedup collapses the copies)
|
| 414 |
+
blocks = [{**b, "SWAP_AB": False} for b in blocks] + [
|
| 415 |
+
{**b, "BLOCK_SIZE_M": 1, "SWAP_AB": True} for b in blocks
|
| 416 |
+
]
|
| 417 |
+
elif swap_ab:
|
| 418 |
+
blocks = [
|
| 419 |
+
{**b, "BLOCK_SIZE_M": bm, "SWAP_AB": sw}
|
| 420 |
+
for b in blocks
|
| 421 |
+
for bm, sw in ((1, False), (16, False), (1, True))
|
| 422 |
+
]
|
| 423 |
+
|
| 424 |
+
# scalar's BM is STRUCTURALLY 1 (a single-row GEVM reduce — a bigger-BM scalar
|
| 425 |
+
# config at BN == BM even COMPILES and silently computes garbage the tuner would
|
| 426 |
+
# time and pick); the dedup below collapses the pinned copies of the BM sweep.
|
| 427 |
+
blocks = [
|
| 428 |
+
{**b, "BLOCK_SIZE_M": 1}
|
| 429 |
+
if b.get("COMPUTE_MODE") == "scalar" and "BLOCK_SIZE_M" in b
|
| 430 |
+
else b
|
| 431 |
+
for b in blocks
|
| 432 |
+
]
|
| 433 |
+
|
| 434 |
+
configs = [
|
| 435 |
+
triton.Config(b, num_warps=w, num_stages=s, pre_hook=pre_hook)
|
| 436 |
+
for b in blocks
|
| 437 |
+
for w in num_warps
|
| 438 |
+
for s in num_stages
|
| 439 |
+
]
|
| 440 |
+
# dedup: the scalar BM=1 pin and the MX "dot" BK override collapse swept axes onto
|
| 441 |
+
# one value, leaving identical copies that would each be benched.
|
| 442 |
+
return list({str(c): c for c in configs}.values())
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
@functools.cache
|
| 447 |
+
def is_sm10x() -> bool:
|
| 448 |
+
"""Whether the CURRENT device is Blackwell-datacenter (sm_10x, TMEM scaled-MMA) —
|
| 449 |
+
the target the ``dot_scaled`` compiler-bug guards are scoped to. False off-CUDA and on
|
| 450 |
+
driverless build boxes (``torch.cuda.is_available()`` short-circuits before the
|
| 451 |
+
capability query, which would otherwise raise); cached (pruners call this per tune, and
|
| 452 |
+
a process is pinned to one device)."""
|
| 453 |
+
return torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 10
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
@functools.cache
|
| 457 |
+
def is_sm90() -> bool:
|
| 458 |
+
"""Whether the CURRENT device is Hopper (sm_90) — the target the measured NVIDIA-backend
|
| 459 |
+
MMA-lowering guards are scoped to. A POSITIVE arch test on purpose: ``not is_sm10x()`` is
|
| 460 |
+
also true on XPU, ROCm and driverless build boxes, none of which run the CUDA codegen those
|
| 461 |
+
guards were probed against, so scoping by negation would restrict backends no one measured.
|
| 462 |
+
Same driverless-safe short-circuit and caching as ``is_sm10x``."""
|
| 463 |
+
return torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
@functools.lru_cache(maxsize=None)
|
| 468 |
+
def sm_shared_memory_limit() -> int:
|
| 469 |
+
"""Max dynamic shared memory per block (bytes) for the CURRENT device — the cap
|
| 470 |
+
Triton reports as the 'Hardware limit' on an ``out of resource: shared memory``
|
| 471 |
+
failure (~232 KB on B200, ~227 KB on H100, much less on older/consumer parts).
|
| 472 |
+
Queried from the driver so the prune adapts to the hardware instead of hardcoding
|
| 473 |
+
one GPU; cached (a process is pinned to one device)."""
|
| 474 |
+
dev = get_active_device_type()
|
| 475 |
+
device_index = (
|
| 476 |
+
torch.cuda.current_device()
|
| 477 |
+
if dev == "cuda"
|
| 478 |
+
else torch.xpu.current_device()
|
| 479 |
+
if dev == "xpu"
|
| 480 |
+
else 0
|
| 481 |
+
)
|
| 482 |
+
try:
|
| 483 |
+
return triton.runtime.driver.active.utils.get_device_properties(device_index)[
|
| 484 |
+
"max_shared_mem"
|
| 485 |
+
]
|
| 486 |
+
except Exception:
|
| 487 |
+
if dev == "cuda":
|
| 488 |
+
return torch.cuda.get_device_properties(
|
| 489 |
+
device_index
|
| 490 |
+
).shared_memory_per_block_optin
|
| 491 |
+
elif dev == "xpu":
|
| 492 |
+
return torch.xpu.get_device_properties(
|
| 493 |
+
device_index
|
| 494 |
+
).shared_memory_per_block_optin
|
| 495 |
+
else:
|
| 496 |
+
raise RuntimeError(
|
| 497 |
+
f"Unsupported device type {dev} for sm_shared_memory_limit; only cuda/xpu are supported."
|
| 498 |
+
)
|
build/torch-xpu/descriptors.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Host-TMA descriptors for the kernels' descriptor memory arms. ``maybe_descriptor`` builds one
|
| 16 |
+
where the operand layout can back it; the ``rebind_*`` functions are the autotuner per-config
|
| 17 |
+
``pre_hook``s that size each descriptor's box to the tuned tile (they MUST mutate ``block_shape`` in
|
| 18 |
+
place — a rebind never reaches the launch) and are no-ops for the pointer arms, whose descriptor
|
| 19 |
+
argument is a dead placeholder."""
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
from triton.tools.tensor_descriptor import TensorDescriptor
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def rebind_operand_box(nargs, mode_key, desc_key, rows, cols):
|
| 26 |
+
"""Set one operand's host-TMA box to ``[rows, cols]`` in place — MUST mutate (a rebind never
|
| 27 |
+
reaches the launch). No-op for a pointer config, whose descriptor is a dead int placeholder."""
|
| 28 |
+
desc = nargs[desc_key]
|
| 29 |
+
# None = the operand's layout cannot back a descriptor (see `maybe_descriptor`); those
|
| 30 |
+
# configs are pruned, so reaching here with one would mean a pruner gap, not a rebind target
|
| 31 |
+
if nargs.get(mode_key, "pointer") != "pointer" and not isinstance(desc, int) and desc is not None:
|
| 32 |
+
desc.block_shape = [rows, cols]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def maybe_descriptor(t: torch.Tensor, box: list[int]) -> TensorDescriptor | None:
|
| 36 |
+
"""A host-TMA descriptor over ``t``, or None when the layout cannot back one — a TMA box
|
| 37 |
+
needs a unit-stride innermost dim, which a transposed view (as a backward pass hands in to
|
| 38 |
+
contract over the other axis) does not have. ``None`` is the same signal the pointer arms
|
| 39 |
+
already take for an unused descriptor, and ``descriptor_box_pruner`` drops the configs that
|
| 40 |
+
would read it, so the tuner is left with the pointer arms the kernel serves stride-generally."""
|
| 41 |
+
if t is None or (t.ndim >= 2 and t.stride(-1) != 1):
|
| 42 |
+
return None
|
| 43 |
+
return TensorDescriptor.from_tensor(t, box)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def rebind_bd_descriptors(nargs):
|
| 47 |
+
"""Per-config pre_hook: set the A and B host-TMA boxes to the tuned tile over the
|
| 48 |
+
``(rows, K)`` matrices — ``[BLOCK_SIZE_M, block_k]`` and ``[BLOCK_SIZE_N, block_k]``."""
|
| 49 |
+
rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], nargs["block_k"])
|
| 50 |
+
rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], nargs["block_k"])
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def rebind_weight_only_descriptors(nargs):
|
| 54 |
+
"""Per-config pre_hook for the weight-only kernels — set the A/B host-TMA boxes to the tuned tile:
|
| 55 |
+
``[BM, BK]`` over the (M, K) bf16 activation and ``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` over the
|
| 56 |
+
packed (N, K_bytes) weight (bytes: uint8 = packed E2M1, two values/byte). No scale boxes — weight-only
|
| 57 |
+
scales are always affine (never swizzled), read through the pointer arm."""
|
| 58 |
+
wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
|
| 59 |
+
bk = nargs["BLOCK_SIZE_K"]
|
| 60 |
+
rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], bk)
|
| 61 |
+
rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], bk // wvpb)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def rebind_mx_descriptors(nargs):
|
| 65 |
+
"""Per-config pre_hook for the MX kernel — set the A/B host-TMA boxes to the tuned tile
|
| 66 |
+
in BYTES over the (rows, K_bytes) packed matrices: ``[BM, BK // ACT_VALUES_PER_BYTE]`` and
|
| 67 |
+
``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` (values-per-byte read off the operand dtype; uint8 =
|
| 68 |
+
packed E2M1), plus the SWIZZLE_32_4_4 scale boxes on the offline (pre-quantized A) path."""
|
| 69 |
+
avpb = 2 if nargs["A"].dtype == torch.uint8 else 1
|
| 70 |
+
wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
|
| 71 |
+
bk = nargs["BLOCK_SIZE_K"]
|
| 72 |
+
rebind_operand_box(nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], bk // avpb)
|
| 73 |
+
rebind_operand_box(nargs, "B_MEMORY_MODE", "BDescriptor", (2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"], bk // wvpb)
|
| 74 |
+
# SWIZZLE_32_4_4 scale boxes: [1, BLOCK//128, (BK // SCALE_GROUP_K) // 4, 2, 256]. Only where the
|
| 75 |
+
# scale is actually swizzled — else the SA/SB descriptor is a dummy aliased to the operand
|
| 76 |
+
# descriptor, and stamping a scale box would clobber its [BM, BK] operand box. The weight is
|
| 77 |
+
# swizzled iff SWIZZLED_SCALES; the act only when it was also offline-quantized (E4M3 / packed
|
| 78 |
+
# E2M1) — inline (raw bf16 A) stays affine even under a swizzled weight.
|
| 79 |
+
if nargs["SWIZZLED_SCALES"]:
|
| 80 |
+
# Sub-128 tiles read scales via the per-row pointer gather, leaving the descriptor
|
| 81 |
+
# unread — clamp its box to one block so it keeps a valid, non-degenerate shape
|
| 82 |
+
# (a 0-block box traps the descriptor-encoding pass; same clamp as the grouped hook).
|
| 83 |
+
rep_k = max((nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4, 1)
|
| 84 |
+
bn_blocks = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
|
| 85 |
+
nargs["BSDescriptor"].block_shape = [1, bn_blocks, rep_k, 2, 256]
|
| 86 |
+
if nargs["A"].dtype in (torch.float8_e4m3fn, torch.uint8):
|
| 87 |
+
bm_blocks = max(nargs["BLOCK_SIZE_M"] // 128, 1)
|
| 88 |
+
nargs["ASDescriptor"].block_shape = [1, bm_blocks, rep_k, 2, 256]
|
| 89 |
+
# swizzled requant output (Cs is a descriptor): store tile [1, 1, rep_n, 2, 256], rep_n per config
|
| 90 |
+
if nargs["CSDescriptor"] is not None:
|
| 91 |
+
rep_n = (nargs["BLOCK_SIZE_N"] // nargs["SCALE_GROUP_K"]) // 4
|
| 92 |
+
nargs["CSDescriptor"].block_shape = [1, 1, rep_n, 2, 256]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def rebind_batched_mx_bs_descriptor(nargs):
|
| 96 |
+
"""Per-config pre_hook: size the swizzled weight-scale descriptor box to the tile's 128-row
|
| 97 |
+
blocks (doubled under GATE, whose tile spans 2*BN interleaved rows). BN<128 (fp8 scalar)
|
| 98 |
+
pointer-gathers instead and never reads the descriptor. Only under SWIZZLED_SCALES; the
|
| 99 |
+
un-swizzled path keeps its dummy box."""
|
| 100 |
+
if not nargs.get("SWIZZLED_SCALES"):
|
| 101 |
+
return
|
| 102 |
+
rep = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
|
| 103 |
+
rep_k = (nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4
|
| 104 |
+
nargs["BSDescriptor"].block_shape = [1, rep, rep_k, 2, 256]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def rebind_grouped_weight_descriptor(nargs):
|
| 108 |
+
"""Per-config pre_hook: set the MX weight descriptor box to the tuned
|
| 109 |
+
``[1, (2 if GATE else 1) * BLOCK_SIZE_N, BLOCK_SIZE_K // values_per_byte]`` over the
|
| 110 |
+
``(E, 2N|N, K_bytes)`` weight view. MUST mutate ``block_shape`` in place — a rebind never reaches
|
| 111 |
+
the launch. No-op for pointer configs (they never read the descriptor)."""
|
| 112 |
+
if nargs.get("B_MEMORY_MODE", "pointer") == "pointer" or isinstance(
|
| 113 |
+
nargs["BDescriptor"], int
|
| 114 |
+
):
|
| 115 |
+
return
|
| 116 |
+
values_per_byte = 2 if nargs["B"].dtype == torch.uint8 else 1
|
| 117 |
+
nargs["BDescriptor"].block_shape = [
|
| 118 |
+
1,
|
| 119 |
+
(2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"],
|
| 120 |
+
nargs["BLOCK_SIZE_K"] // values_per_byte,
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def rebind_grouped_act_descriptor(nargs):
|
| 125 |
+
"""Per-config pre_hook: set the activation descriptor box to the tuned
|
| 126 |
+
``[BLOCK_SIZE_M, BLOCK_SIZE_K // act_values_per_byte]`` over the ``(rows, K_bytes)``
|
| 127 |
+
activation matrix. In-place mutate; no-op for pointer-A configs."""
|
| 128 |
+
if nargs.get("A_MEMORY_MODE", "pointer") == "pointer" or isinstance(
|
| 129 |
+
nargs["ADescriptor"], int
|
| 130 |
+
):
|
| 131 |
+
return
|
| 132 |
+
act_values_per_byte = 2 if nargs["A"].dtype == torch.uint8 else 1
|
| 133 |
+
# tma gather4 loads N independent rows: descriptor_gather requires a 1-row box;
|
| 134 |
+
# the contiguous (no-gather) arm loads the whole [BM, BK_bytes] tile in one box
|
| 135 |
+
nargs["ADescriptor"].block_shape = [
|
| 136 |
+
1 if nargs.get("GatherIdx") is not None else nargs["BLOCK_SIZE_M"],
|
| 137 |
+
nargs["BLOCK_SIZE_K"] // act_values_per_byte,
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def rebind_grouped_descriptors(nargs):
|
| 142 |
+
"""Composite pre_hook: both weight and activation descriptor boxes."""
|
| 143 |
+
rebind_grouped_weight_descriptor(nargs)
|
| 144 |
+
rebind_grouped_act_descriptor(nargs)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def build_grouped_operand_descriptors(a_operand, b_operand):
|
| 148 |
+
"""Operand host-TMA descriptors for a grouped launch: A box ``[16, 64]``, B box
|
| 149 |
+
``[1, 128, 64]`` over the ``(E, 2N|N, K_bytes)`` weight view. Placeholder boxes, re-bound to
|
| 150 |
+
the tuned tile per config by ``rebind_grouped_descriptors``."""
|
| 151 |
+
return (
|
| 152 |
+
TensorDescriptor.from_tensor(a_operand, block_shape=[16, 64]),
|
| 153 |
+
TensorDescriptor.from_tensor(b_operand, block_shape=[1, 128, 64]),
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def rebind_grouped_mx_descriptors(nargs):
|
| 158 |
+
"""MX composite pre_hook: the operand boxes plus the two SWIZZLE_32_4_4 scale boxes
|
| 159 |
+
``[1, BLOCK // 128, (BK // SCALE_GROUP_K) // 4, 2, 256]`` over the swizzled
|
| 160 |
+
``(1, rows // 128, cols // 4, 2, 256)`` views. Activation box is BM // 128 (BM pinned 128);
|
| 161 |
+
the weight box is ``(2 if GATE else 1) * BN // 128`` — the stacked gate|up tile is one 2*BN
|
| 162 |
+
block (BN pinned 128 under GATE by ``swizzled_scales_bm_pruner``). Mutate in place. Both scale
|
| 163 |
+
descriptors are None on the un-swizzled arm (affine read — one SWIZZLED_SCALES flag governs both
|
| 164 |
+
operands), so their boxes are skipped."""
|
| 165 |
+
rebind_grouped_descriptors(nargs)
|
| 166 |
+
# BK below 4 scale groups (BK=64 rows the pruner rejects) still passes through here when a
|
| 167 |
+
# descriptor is unread — clamp like bn_blocks below so it keeps a valid, non-degenerate
|
| 168 |
+
# shape (a 0-block box traps the descriptor-encoding pass).
|
| 169 |
+
rep_k = max((nargs["BLOCK_SIZE_K"] // nargs["SCALE_GROUP_K"]) // 4, 1)
|
| 170 |
+
if nargs["ASDescriptor"] is not None:
|
| 171 |
+
nargs["ASDescriptor"].block_shape = [1, nargs["BLOCK_SIZE_M"] // 128, rep_k, 2, 256]
|
| 172 |
+
if nargs["BSDescriptor"] is not None:
|
| 173 |
+
# One bulk-load spans (2 if GATE) * BN//128 blocks: GATE reads the block-interleaved gate|up
|
| 174 |
+
# pair ([g,u] adjacent) as one 2*BN tile. BN<128 (non-gate, non-128 N) reads via the per-row
|
| 175 |
+
# pointer gather instead — clamp the box to one block so the (unread) descriptor keeps a valid,
|
| 176 |
+
# non-degenerate shape (a 0-block box traps the descriptor-encoding pass).
|
| 177 |
+
bn_blocks = max(1, ((2 if nargs.get("GATE") else 1) * nargs["BLOCK_SIZE_N"]) // 128)
|
| 178 |
+
nargs["BSDescriptor"].block_shape = [1, bn_blocks, rep_k, 2, 256]
|
| 179 |
+
# Swizzled requant output (Cs is a descriptor): the store tile is [1, 1, rep_n, 2, 256], and
|
| 180 |
+
# rep_n = (BN // SCALE_GROUP_K) // 4 depends on the tuned BLOCK_SIZE_N and the group size — so
|
| 181 |
+
# rebind per config, else nvfp4 (group-16 -> rep_n=2) mismatches the [.,.,1,.,.] build default.
|
| 182 |
+
if nargs["CSDescriptor"] is not None:
|
| 183 |
+
rep_n = (nargs["BLOCK_SIZE_N"] // nargs["SCALE_GROUP_K"]) // 4
|
| 184 |
+
nargs["CSDescriptor"].block_shape = [1, 1, rep_n, 2, 256]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def rebind_dgrad_descriptors(nargs):
|
| 188 |
+
"""Per-config pre_hook: set the dgrad boxes to the tuned tile — ``[BM, BN]`` over the (M, N)
|
| 189 |
+
gradient and ``[BN, BK // WEIGHT_VALUES_PER_BYTE]`` over the (N, K_bytes) weight. Both are
|
| 190 |
+
natural contiguous windows in the FORWARD-oriented operands, which is why dgrad can take a
|
| 191 |
+
descriptor at all: a transposed view has no unit-stride innermost dim (see
|
| 192 |
+
``maybe_descriptor``). Scales stay affine on the pointer arm, as weight-only does."""
|
| 193 |
+
wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
|
| 194 |
+
rebind_operand_box(
|
| 195 |
+
nargs, "A_MEMORY_MODE", "ADescriptor", nargs["BLOCK_SIZE_M"], nargs["BLOCK_SIZE_N"]
|
| 196 |
+
)
|
| 197 |
+
rebind_operand_box(
|
| 198 |
+
nargs, "B_MEMORY_MODE", "BDescriptor",
|
| 199 |
+
nargs["BLOCK_SIZE_N"], nargs["BLOCK_SIZE_K"] // wvpb,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def rebind_dgrad_grouped_descriptors(nargs):
|
| 204 |
+
"""Grouped dgrad boxes. The activation box is 1 row when the pass gathers (tma gather4 needs a
|
| 205 |
+
1-row box) and ``[BM, BN]`` otherwise — ScatterIdx is the gather map here, since dgrad reads
|
| 206 |
+
dY at the forward's SCATTER destination. The weight box is ``[1, BN, BK_bytes]`` over
|
| 207 |
+
(E, N, K_bytes): the expert leads, so this descriptor takes THREE offsets."""
|
| 208 |
+
wvpb = 2 if nargs["B"].dtype == torch.uint8 else 1
|
| 209 |
+
gathering = nargs.get("ScatterIdx") is not None
|
| 210 |
+
rebind_operand_box(
|
| 211 |
+
nargs, "A_MEMORY_MODE", "ADescriptor",
|
| 212 |
+
1 if gathering else nargs["BLOCK_SIZE_M"], nargs["BLOCK_SIZE_N"],
|
| 213 |
+
)
|
| 214 |
+
desc = nargs["BDescriptor"]
|
| 215 |
+
if nargs.get("B_MEMORY_MODE", "pointer") != "pointer" and not isinstance(desc, int) and desc is not None:
|
| 216 |
+
desc.block_shape = [1, nargs["BLOCK_SIZE_N"], nargs["BLOCK_SIZE_K"] // wvpb]
|
build/torch-xpu/epilogue.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import triton
|
| 17 |
+
import triton.language as tl
|
| 18 |
+
from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
|
| 19 |
+
|
| 20 |
+
from .compat import compile_time_only_triton_wrap, decode_pdl, device_context
|
| 21 |
+
from .mma import MMA_N_ATOM
|
| 22 |
+
from .quant import fp8_act_quant_inline, mx_act_quant_inline
|
| 23 |
+
from .loading.scales import apply_global_scale
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@triton.jit
|
| 27 |
+
def store_masked(
|
| 28 |
+
C,
|
| 29 |
+
accumulator,
|
| 30 |
+
pid_m,
|
| 31 |
+
pid_n,
|
| 32 |
+
M,
|
| 33 |
+
N,
|
| 34 |
+
stride_c_m,
|
| 35 |
+
stride_c_n,
|
| 36 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 37 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 38 |
+
):
|
| 39 |
+
"""Shared output epilogue of the kernels below: cast the fp32 accumulator to
|
| 40 |
+
``C``'s dtype and store the ``(BLOCK_SIZE_M, BLOCK_SIZE_N)`` tile at the swizzled
|
| 41 |
+
``(pid_m, pid_n)``, masked to the ``(M, N)`` bounds. (A descriptor-store arm was
|
| 42 |
+
measured an EXACT tie at the store-heavy gap shape and dropped, 2026-07-16 —
|
| 43 |
+
stores are fire-and-forget, TMA has nothing to hide there. B200-only verdict;
|
| 44 |
+
re-measure on H100 or the target device.)"""
|
| 45 |
+
c = accumulator.to(C.dtype.element_ty)
|
| 46 |
+
offs_cm = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 47 |
+
offs_cn = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
|
| 48 |
+
c_ptrs = C + stride_c_m * offs_cm[:, None] + stride_c_n * offs_cn[None, :]
|
| 49 |
+
c_mask = (offs_cm[:, None] < M) & (offs_cn[None, :] < N)
|
| 50 |
+
tl.store(c_ptrs, c, mask=c_mask)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@triton.jit
|
| 54 |
+
def store_masked_oriented(
|
| 55 |
+
C,
|
| 56 |
+
accumulator,
|
| 57 |
+
pid_m,
|
| 58 |
+
pid_n,
|
| 59 |
+
M,
|
| 60 |
+
N,
|
| 61 |
+
stride_c_m,
|
| 62 |
+
stride_c_n,
|
| 63 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 64 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 65 |
+
SWAP_AB: tl.constexpr,
|
| 66 |
+
):
|
| 67 |
+
"""``store_masked`` with the (m, n) roles swapped under ``SWAP_AB``, where the
|
| 68 |
+
accumulator is ``(BLOCK_SIZE_N, BLOCK_SIZE_M)``. Only the taken branch compiles."""
|
| 69 |
+
if SWAP_AB:
|
| 70 |
+
store_masked(
|
| 71 |
+
C,
|
| 72 |
+
accumulator,
|
| 73 |
+
pid_n,
|
| 74 |
+
pid_m,
|
| 75 |
+
N,
|
| 76 |
+
M,
|
| 77 |
+
stride_c_n,
|
| 78 |
+
stride_c_m,
|
| 79 |
+
BLOCK_SIZE_N,
|
| 80 |
+
BLOCK_SIZE_M,
|
| 81 |
+
)
|
| 82 |
+
else:
|
| 83 |
+
store_masked(
|
| 84 |
+
C,
|
| 85 |
+
accumulator,
|
| 86 |
+
pid_m,
|
| 87 |
+
pid_n,
|
| 88 |
+
M,
|
| 89 |
+
N,
|
| 90 |
+
stride_c_m,
|
| 91 |
+
stride_c_n,
|
| 92 |
+
BLOCK_SIZE_M,
|
| 93 |
+
BLOCK_SIZE_N,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@triton.jit
|
| 98 |
+
def acc_init(
|
| 99 |
+
COMPUTE_MODE: tl.constexpr,
|
| 100 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 101 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 102 |
+
SWAP_AB: tl.constexpr,
|
| 103 |
+
):
|
| 104 |
+
"""Zero accumulator shaped for the layout: swapped scalar reduces into ``[1, N]``; any other
|
| 105 |
+
swapped mode keeps the ``[N, M]`` MMA acc (weight rows in M, act tile in N — padded up to
|
| 106 |
+
the ``MMA_N_ATOM`` when the caller is the single-token decode GEVM, col 0 taken after the
|
| 107 |
+
K-loop); no-swap uses ``[M, N]``. ``COMPUTE_MODE`` matters only under swap — kernels with no
|
| 108 |
+
mode axis (fp8 ``tl.dot``) pass ``"dot"``. ``N`` is the weight-output tile (``BLOCK_SIZE_H``
|
| 109 |
+
for the fp8 down projection). Single return: only the taken branch compiles."""
|
| 110 |
+
if SWAP_AB and COMPUTE_MODE == "scalar":
|
| 111 |
+
acc = tl.zeros((1, BLOCK_SIZE_N), dtype=tl.float32)
|
| 112 |
+
elif SWAP_AB and BLOCK_SIZE_M < MMA_N_ATOM:
|
| 113 |
+
acc = tl.zeros((BLOCK_SIZE_N, MMA_N_ATOM), dtype=tl.float32)
|
| 114 |
+
elif SWAP_AB:
|
| 115 |
+
acc = tl.zeros((BLOCK_SIZE_N, BLOCK_SIZE_M), dtype=tl.float32)
|
| 116 |
+
else:
|
| 117 |
+
acc = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=tl.float32)
|
| 118 |
+
return acc
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@triton.jit
|
| 122 |
+
def acc_finalize(
|
| 123 |
+
acc, COMPUTE_MODE: tl.constexpr, ROWS: tl.constexpr, SWAP_AB: tl.constexpr
|
| 124 |
+
):
|
| 125 |
+
"""Bookend to ``acc_init``: when the acc was built as the persistent ``[ROWS, MMA_N_ATOM]`` MMA
|
| 126 |
+
tile (any swapped non-scalar mode), collapse the padded token dim to column 0 → ``[1, ROWS]``.
|
| 127 |
+
Swapped scalar (already ``[1, ROWS]``) and no-swap pass through unchanged. ``COMPUTE_MODE``
|
| 128 |
+
matches ``acc_init``'s (fp8 ``tl.dot`` kernels, which have no mode axis, pass ``"dot"``)."""
|
| 129 |
+
if SWAP_AB and COMPUTE_MODE != "scalar":
|
| 130 |
+
# take column 0: the padded token dim collapses back to the single real token
|
| 131 |
+
acc = tl.reshape(
|
| 132 |
+
tl.sum(acc * (tl.arange(0, MMA_N_ATOM)[None, :] == 0), axis=1), (1, ROWS)
|
| 133 |
+
)
|
| 134 |
+
return acc
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def bias_strides(bias: torch.Tensor | None) -> tuple[int, int]:
|
| 138 |
+
"""(expert, column) strides for a per-expert output bias, for ``add_bias``. A 1-D bias is the
|
| 139 |
+
single-expert 2D case, so its expert stride is 0. ``None`` gives (0, 0) — the kernel arm folds
|
| 140 |
+
out at trace time and never reads them."""
|
| 141 |
+
if bias is None:
|
| 142 |
+
return 0, 0
|
| 143 |
+
if bias.dim() == 1:
|
| 144 |
+
return 0, bias.stride(0)
|
| 145 |
+
return bias.stride(0), bias.stride(1)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@triton.jit
|
| 149 |
+
def add_bias(acc, Bias, stride_bias_e, stride_bias_n, bias_row, pid_n,
|
| 150 |
+
BLOCK_SIZE_N: tl.constexpr, N_COLS: tl.constexpr = 0,
|
| 151 |
+
SWAP_AB: tl.constexpr = False):
|
| 152 |
+
"""Add this expert's slice of a per-expert output bias to an output tile. Gated callers pass
|
| 153 |
+
the doubled extent — an interleaved gate|up tile is an ungated tile of width ``2*BN``.
|
| 154 |
+
``None`` folds out at trace time.
|
| 155 |
+
|
| 156 |
+
Two shapes reach here. Post-``acc_finalize`` tiles are ``[rows, BN]`` for every orientation
|
| 157 |
+
(``SWAP_AB=False``, what ``gemm_epilogue`` passes) — finalize is what collapses the swapped
|
| 158 |
+
MMA accumulator, so adding before it would broadcast against the wrong axis. The 2D kernels'
|
| 159 |
+
ungated arm stores straight from the accumulator with no finalize, and there ``SWAP_AB``
|
| 160 |
+
leaves it transposed as ``(BN, BM)``, so the bias varies down axis 0 instead."""
|
| 161 |
+
if Bias is not None:
|
| 162 |
+
offs_bias = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N)
|
| 163 |
+
ptrs = Bias + bias_row * stride_bias_e + offs_bias * stride_bias_n
|
| 164 |
+
if N_COLS > 0: # dense N isn't BN-aligned; the tail columns are off the end of the bias
|
| 165 |
+
bias = tl.load(ptrs, mask=offs_bias < N_COLS, other=0.0)
|
| 166 |
+
else:
|
| 167 |
+
bias = tl.load(ptrs)
|
| 168 |
+
if SWAP_AB:
|
| 169 |
+
out = acc + bias.to(acc.dtype)[:, None]
|
| 170 |
+
else:
|
| 171 |
+
out = acc + bias.to(acc.dtype)[None, :]
|
| 172 |
+
else:
|
| 173 |
+
out = acc
|
| 174 |
+
return out
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@triton.jit
|
| 178 |
+
def split_gate_up(flat, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, SWAP_AB: tl.constexpr):
|
| 179 |
+
"""De-interleave a finalized ``[rows, 2*BN]`` gate|up tile into the ``(gate, up)`` pair, each
|
| 180 |
+
``[rows, BN]`` (rows = 1 under swap, else BM). The N axis alternates gate/up per column, so
|
| 181 |
+
this is a trailing-axis split. The caller finalizes (and biases) before calling."""
|
| 182 |
+
rows: tl.constexpr = 1 if SWAP_AB else BLOCK_SIZE_M
|
| 183 |
+
g, u = tl.split(tl.reshape(flat, (rows, BLOCK_SIZE_N, 2)))
|
| 184 |
+
return g, u
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@triton.jit
|
| 188 |
+
def glu(
|
| 189 |
+
gate,
|
| 190 |
+
up,
|
| 191 |
+
ACT_FN: tl.constexpr = "silu",
|
| 192 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 193 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 194 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 195 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.float32,
|
| 196 |
+
):
|
| 197 |
+
"""Gated linear unit on the gate/up matmul accumulators. ``SWIGLU_LIMIT`` clamps gate above and up
|
| 198 |
+
to ``[-LIMIT, LIMIT]``; ``SWIGLU_ALPHA`` gives the clamped/scaled SwiGLU ``(up + 1) * gate * sigmoid(ALPHA *
|
| 199 |
+
gate)`` (GPT-OSS / MiniMax), else ``ACT_FN(gate) * up`` (``ACT_FN`` in {silu, gelu, relu}, gelu exact
|
| 200 |
+
via erf). ``SIMULATE_UNFUSED`` rounds each materialized value through ``INTERMEDIATE_DTYPE`` (the dtype the unfused path lands intermediates in) to match the
|
| 201 |
+
unfused (separate-kernel) path, where every intermediate lands in that dtype."""
|
| 202 |
+
g = gate
|
| 203 |
+
u = up
|
| 204 |
+
|
| 205 |
+
if SIMULATE_UNFUSED:
|
| 206 |
+
g = g.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 207 |
+
u = u.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 208 |
+
|
| 209 |
+
if SWIGLU_LIMIT is not None:
|
| 210 |
+
g = tl.minimum(g, SWIGLU_LIMIT)
|
| 211 |
+
u = tl.minimum(tl.maximum(u, -SWIGLU_LIMIT), SWIGLU_LIMIT)
|
| 212 |
+
|
| 213 |
+
if SWIGLU_ALPHA is not None:
|
| 214 |
+
gate_scaled = g * SWIGLU_ALPHA
|
| 215 |
+
if SIMULATE_UNFUSED:
|
| 216 |
+
gate_scaled = gate_scaled.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 217 |
+
sig = tl.sigmoid(gate_scaled)
|
| 218 |
+
if SIMULATE_UNFUSED:
|
| 219 |
+
sig = sig.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 220 |
+
act = g * sig
|
| 221 |
+
u = u + 1.0
|
| 222 |
+
elif ACT_FN == "silu":
|
| 223 |
+
sig = tl.sigmoid(g)
|
| 224 |
+
# SIMULATE_UNFUSED must be bit-exact vs the unfused ``apply_glu`` (``g * torch.sigmoid(g)``),
|
| 225 |
+
# where torch.sigmoid returns bf16 — i.e. the sigmoid is rounded before the multiply. Round
|
| 226 |
+
# it here to match (the fp32 sigmoid otherwise flips e4m3 requant bits, ~35% on MXFP4 down).
|
| 227 |
+
if SIMULATE_UNFUSED:
|
| 228 |
+
sig = sig.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 229 |
+
act = g * sig
|
| 230 |
+
elif ACT_FN == "gelu":
|
| 231 |
+
if SIMULATE_UNFUSED:
|
| 232 |
+
# Bit-match the unfused ``apply_glu`` gelu ``0.5 * g * (1 + erf(g * c))``, which rounds
|
| 233 |
+
# to bf16 at every torch op (input to erf, erf, 1+erf, 0.5*g, final mul). Rounding only
|
| 234 |
+
# a subset diverges (~0.7 rel) on the MX requant — round each op, like torch does.
|
| 235 |
+
gc = (g * 0.7071067811865476).to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 236 |
+
e = tl.erf(gc).to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 237 |
+
one_plus = (1.0 + e).to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 238 |
+
half_g = (0.5 * g).to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 239 |
+
act = half_g * one_plus
|
| 240 |
+
else:
|
| 241 |
+
act = 0.5 * g * (1.0 + tl.erf(g * 0.7071067811865476))
|
| 242 |
+
elif ACT_FN == "relu":
|
| 243 |
+
act = tl.maximum(g, 0.0)
|
| 244 |
+
else:
|
| 245 |
+
tl.static_assert(
|
| 246 |
+
False, "unsupported ACT_FN; expected 'silu', 'gelu', or 'relu'"
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if SIMULATE_UNFUSED:
|
| 250 |
+
act = act.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 251 |
+
u = u.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 252 |
+
|
| 253 |
+
gated = act * u
|
| 254 |
+
|
| 255 |
+
if SIMULATE_UNFUSED:
|
| 256 |
+
gated = gated.to(INTERMEDIATE_DTYPE).to(tl.float32)
|
| 257 |
+
|
| 258 |
+
return gated
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@triton.jit
|
| 262 |
+
def _glu_kernel(
|
| 263 |
+
GateUp, # (S, 2*INTER) interleaved [g0,u0,g1,u1,...] GEMM output; fp32 = exact accumulators
|
| 264 |
+
Out, # (S, INTER) activation-dtype GLU result; the E4M3 tensor under the requant arm
|
| 265 |
+
QuantScales, # block-FP8 requant arm (``fused_glu(quant_group=...)``): per-``QUANT_GROUP``
|
| 266 |
+
# scales (fp32, or UE8M0 exponent bytes); ``None`` folds the arm out — the GLU
|
| 267 |
+
# result then rounds through bf16 (the reference two-kernel order) and quantizes
|
| 268 |
+
n_out,
|
| 269 |
+
INTER: tl.constexpr,
|
| 270 |
+
ACT_FN: tl.constexpr,
|
| 271 |
+
SWIGLU_ALPHA: tl.constexpr,
|
| 272 |
+
SWIGLU_LIMIT: tl.constexpr,
|
| 273 |
+
BLOCK: tl.constexpr,
|
| 274 |
+
QUANT_GROUP: tl.constexpr = 128,
|
| 275 |
+
UE8M0: tl.constexpr = False,
|
| 276 |
+
PDL: tl.constexpr = False,
|
| 277 |
+
):
|
| 278 |
+
"""Fused GLU over a contiguous (S, 2*INTER) interleaved gate|up tensor -> (S, INTER), covering every
|
| 279 |
+
``apply_glu`` variant (silu/gelu/relu, ``SWIGLU_ALPHA``/``SWIGLU_LIMIT`` clamped-SwiGLU
|
| 280 |
+
— ``None`` folds the arm out, the in-kernel ``glu``'s convention). Rounds through the
|
| 281 |
+
output dtype after each op exactly where the torch chain does (each torch tensor op
|
| 282 |
+
materializes), so the two are bit-identical."""
|
| 283 |
+
if PDL:
|
| 284 |
+
gdc_wait()
|
| 285 |
+
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
|
| 286 |
+
mask = offs < n_out
|
| 287 |
+
# gate|up columns alternate (gate 2j, up 2j+1) — the interleaved row order the GEMM emits
|
| 288 |
+
base = (offs // INTER) * 2 * INTER + 2 * (offs % INTER)
|
| 289 |
+
# fp32 input = exact GEMM accumulators (the unstacked fused-order path): compute the
|
| 290 |
+
# GLU in fp32 and round ONCE at the store — the gated epilogue's math. Narrow inputs
|
| 291 |
+
# keep the torch-chain per-op rounding (bit-identical to apply_glu).
|
| 292 |
+
FUSED_ORDER: tl.constexpr = GateUp.dtype.element_ty == tl.float32
|
| 293 |
+
dt = tl.float32 if FUSED_ORDER else (
|
| 294 |
+
tl.bfloat16 if QuantScales is not None else Out.dtype.element_ty)
|
| 295 |
+
g = tl.load(GateUp + base, mask=mask).to(tl.float32)
|
| 296 |
+
u = tl.load(GateUp + base + 1, mask=mask).to(tl.float32)
|
| 297 |
+
if SWIGLU_LIMIT is not None:
|
| 298 |
+
g = tl.minimum(g, SWIGLU_LIMIT)
|
| 299 |
+
u = tl.clamp(u, -SWIGLU_LIMIT, SWIGLU_LIMIT)
|
| 300 |
+
if SWIGLU_ALPHA is not None: # (up + 1) * gate * sigmoid(alpha * gate)
|
| 301 |
+
sig = tl.sigmoid((g * SWIGLU_ALPHA).to(dt).to(tl.float32)).to(dt).to(tl.float32)
|
| 302 |
+
act = (g * sig).to(dt).to(tl.float32)
|
| 303 |
+
out = ((u + 1.0).to(dt).to(tl.float32) * act).to(dt)
|
| 304 |
+
elif ACT_FN == "silu":
|
| 305 |
+
sig = tl.sigmoid(g).to(dt).to(tl.float32)
|
| 306 |
+
act = (g * sig).to(dt).to(tl.float32)
|
| 307 |
+
out = (act * u).to(dt)
|
| 308 |
+
elif ACT_FN == "gelu": # exact via erf, torch's evaluation order
|
| 309 |
+
e = tl.erf((g * 0.7071067811865476).to(dt).to(tl.float32)).to(dt).to(tl.float32)
|
| 310 |
+
d = (1.0 + e).to(dt).to(tl.float32)
|
| 311 |
+
a = (0.5 * g).to(dt).to(tl.float32)
|
| 312 |
+
out = ((a * d).to(dt).to(tl.float32) * u).to(dt)
|
| 313 |
+
else: # relu
|
| 314 |
+
out = (tl.maximum(g, 0.0) * u).to(dt)
|
| 315 |
+
if QuantScales is None:
|
| 316 |
+
tl.store(Out + offs, out.to(Out.dtype.element_ty), mask=mask)
|
| 317 |
+
else:
|
| 318 |
+
grp = tl.reshape(out.to(tl.float32), (BLOCK // QUANT_GROUP, QUANT_GROUP))
|
| 319 |
+
amax = tl.max(tl.abs(grp), axis=1)
|
| 320 |
+
if UE8M0:
|
| 321 |
+
sc = tl.exp2(tl.ceil(tl.log2(tl.maximum(amax, 1e-10) / 448.0)))
|
| 322 |
+
else:
|
| 323 |
+
sc = tl.maximum(amax, 1e-10) / 448.0
|
| 324 |
+
q = tl.clamp(grp / sc[:, None], -448.0, 448.0)
|
| 325 |
+
tl.store(Out + offs, tl.reshape(q, (BLOCK,)).to(tl.float8e4nv), mask=mask)
|
| 326 |
+
soffs = tl.program_id(0) * (BLOCK // QUANT_GROUP) + tl.arange(0, BLOCK // QUANT_GROUP)
|
| 327 |
+
smask = soffs * QUANT_GROUP < n_out
|
| 328 |
+
if UE8M0:
|
| 329 |
+
tl.store(QuantScales + soffs, (tl.log2(sc) + 127.0).to(tl.uint8), mask=smask)
|
| 330 |
+
else:
|
| 331 |
+
if PDL:
|
| 332 |
+
gdc_launch_dependents()
|
| 333 |
+
tl.store(QuantScales + soffs, sc, mask=smask)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def fused_glu(
|
| 337 |
+
gate_up: torch.Tensor,
|
| 338 |
+
act_fn: str = "silu",
|
| 339 |
+
swiglu_alpha: float | None = None,
|
| 340 |
+
swiglu_limit: float | None = None,
|
| 341 |
+
quant_group: int | None = None,
|
| 342 |
+
use_ue8m0: bool = False,
|
| 343 |
+
out_dtype: torch.dtype | None = None,
|
| 344 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 345 |
+
"""GLU over the interleaved (S, 2*INTER) gate|up GEMM output — the one-kernel ``_glu_kernel``
|
| 346 |
+
on a contiguous tensor, the torch ``apply_glu`` on any fallback layout; bit-identical
|
| 347 |
+
either way. ``quant_group`` additionally block-FP8-requantizes the result per group in
|
| 348 |
+
the same kernel (the unstacked decode band's handoff to the down projection) and
|
| 349 |
+
returns ``(q, scales)`` — scales fp32, or UE8M0 exponent bytes under ``use_ue8m0``;
|
| 350 |
+
non-SiLU/clamped variants take the two-step order (same bits — the fused arm rounds
|
| 351 |
+
through bf16 first for exactly this equivalence)."""
|
| 352 |
+
inter_dim = gate_up.shape[-1] // 2
|
| 353 |
+
if quant_group is not None:
|
| 354 |
+
if act_fn == "silu" and swiglu_alpha is None and swiglu_limit is None and gate_up.is_contiguous():
|
| 355 |
+
S = gate_up.shape[0]
|
| 356 |
+
q = torch.empty(S, inter_dim, dtype=torch.float8_e4m3fn, device=gate_up.device)
|
| 357 |
+
sc = torch.empty(S, inter_dim // quant_group,
|
| 358 |
+
dtype=torch.uint8 if use_ue8m0 else torch.float32,
|
| 359 |
+
device=gate_up.device)
|
| 360 |
+
n = q.numel()
|
| 361 |
+
with device_context(gate_up.device):
|
| 362 |
+
compile_time_only_triton_wrap(_glu_kernel)[(triton.cdiv(n, 1024),)](
|
| 363 |
+
gate_up, q, sc, n, inter_dim, "silu", None, None, BLOCK=1024,
|
| 364 |
+
QUANT_GROUP=quant_group, UE8M0=use_ue8m0,
|
| 365 |
+
PDL=decode_pdl(),
|
| 366 |
+
launch_pdl=decode_pdl(),
|
| 367 |
+
)
|
| 368 |
+
return q, sc
|
| 369 |
+
from .quant import fp8_act_quant_block_dynamic # deferred: module import order
|
| 370 |
+
|
| 371 |
+
inter = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit)
|
| 372 |
+
return fp8_act_quant_block_dynamic(inter, quant_group, use_ue8m0=use_ue8m0)
|
| 373 |
+
if gate_up.is_contiguous() and act_fn in ("silu", "gelu", "relu"):
|
| 374 |
+
out = torch.empty(*gate_up.shape[:-1], inter_dim,
|
| 375 |
+
dtype=out_dtype or gate_up.dtype, device=gate_up.device)
|
| 376 |
+
n = out.numel()
|
| 377 |
+
with device_context(gate_up.device):
|
| 378 |
+
compile_time_only_triton_wrap(_glu_kernel)[(triton.cdiv(n, 1024),)](
|
| 379 |
+
gate_up, out, None, n, inter_dim, act_fn, swiglu_alpha, swiglu_limit, BLOCK=1024,
|
| 380 |
+
PDL=decode_pdl(),
|
| 381 |
+
launch_pdl=decode_pdl(),
|
| 382 |
+
)
|
| 383 |
+
return out
|
| 384 |
+
gate, up = gate_up[..., 0::2], gate_up[..., 1::2]
|
| 385 |
+
return apply_glu(gate, up, act_fn, swiglu_alpha, swiglu_limit)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def apply_glu(
|
| 389 |
+
gate: torch.Tensor,
|
| 390 |
+
up: torch.Tensor,
|
| 391 |
+
act_fn: str = "silu",
|
| 392 |
+
swiglu_alpha: float | None = None,
|
| 393 |
+
swiglu_limit: float | None = None,
|
| 394 |
+
) -> torch.Tensor:
|
| 395 |
+
"""Host-side (torch) gated linear unit — the unfused path's activation, mirroring the triton
|
| 396 |
+
``glu``. ``swiglu_limit`` clamps gate above / up to ``[-limit, limit]``; ``swiglu_alpha`` gives
|
| 397 |
+
the clamped/scaled SwiGLU ``(up + 1) * gate * sigmoid(alpha * gate)`` (GPT-OSS / MiniMax), else
|
| 398 |
+
``act_fn(gate) * up`` (``act_fn`` in {silu, gelu, relu}, gelu exact via erf)."""
|
| 399 |
+
if swiglu_limit is not None:
|
| 400 |
+
gate = gate.clamp(max=swiglu_limit)
|
| 401 |
+
up = up.clamp(min=-swiglu_limit, max=swiglu_limit)
|
| 402 |
+
if swiglu_alpha is not None:
|
| 403 |
+
return (up + 1.0) * (gate * torch.sigmoid(gate * swiglu_alpha))
|
| 404 |
+
if act_fn == "silu":
|
| 405 |
+
act = gate * torch.sigmoid(gate)
|
| 406 |
+
elif act_fn == "gelu":
|
| 407 |
+
act = 0.5 * gate * (1.0 + torch.erf(gate * 0.7071067811865476))
|
| 408 |
+
elif act_fn == "relu":
|
| 409 |
+
act = gate.clamp(min=0.0)
|
| 410 |
+
else:
|
| 411 |
+
raise ValueError(
|
| 412 |
+
f"unsupported act_fn {act_fn!r}; expected 'silu', 'gelu', or 'relu'"
|
| 413 |
+
)
|
| 414 |
+
return act * up
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
@triton.jit
|
| 418 |
+
def split_gate_up_glu(
|
| 419 |
+
flat,
|
| 420 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 421 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 422 |
+
SWAP_AB: tl.constexpr,
|
| 423 |
+
ACT_FN: tl.constexpr = "silu",
|
| 424 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 425 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 426 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 427 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.float32,
|
| 428 |
+
):
|
| 429 |
+
"""Gate|up epilogue in one step: split the finalized stacked tile into its (gate, up) pair
|
| 430 |
+
(``split_gate_up``) and apply the ``ACT_FN``/SwiGLU gated linear unit (``glu``), returning
|
| 431 |
+
the combined intermediate. See those two for the orientation and activation details."""
|
| 432 |
+
gate, up = split_gate_up(flat, BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB)
|
| 433 |
+
return glu(
|
| 434 |
+
gate,
|
| 435 |
+
up,
|
| 436 |
+
ACT_FN,
|
| 437 |
+
SWIGLU_ALPHA,
|
| 438 |
+
SWIGLU_LIMIT,
|
| 439 |
+
SIMULATE_UNFUSED,
|
| 440 |
+
INTERMEDIATE_DTYPE,
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@triton.jit
|
| 445 |
+
def _store_out(
|
| 446 |
+
C, acc, out_row, pid_n, row_mask, stride_c_m, stride_c_n,
|
| 447 |
+
BLOCK_SIZE_M: tl.constexpr, WIDTH: tl.constexpr, FAKE_BATCH: tl.constexpr,
|
| 448 |
+
N_COLS: tl.constexpr = 0,
|
| 449 |
+
):
|
| 450 |
+
"""Cast + store one output tile of N-width ``WIDTH`` (halved when the format packs nibble
|
| 451 |
+
pairs). ``FAKE_BATCH`` (batched decode): the BM lanes alias one C row (``C`` pre-advanced), so
|
| 452 |
+
a plain store would duplicate-write the same bytes (hardware-undefined on Intel XPU) — mask to
|
| 453 |
+
lane 0 (the replicated rows are identical). Else a real scatter to global rows ``out_row`` under
|
| 454 |
+
``row_mask``. ``N_COLS`` > 0 also masks the column tail (the 2D dense output isn't ``BN``-aligned
|
| 455 |
+
like the ``N % BN == 0`` grouped/batched MoE outputs); 0 skips it. Single return."""
|
| 456 |
+
c = acc.to(C.dtype.element_ty)
|
| 457 |
+
offs_cm = tl.arange(0, BLOCK_SIZE_M)
|
| 458 |
+
offs_cn = pid_n * WIDTH + tl.arange(0, WIDTH)
|
| 459 |
+
col_ok = (offs_cn < N_COLS) if N_COLS > 0 else (offs_cn >= 0)
|
| 460 |
+
if FAKE_BATCH:
|
| 461 |
+
c_ptrs = C + offs_cm[:, None] * 0 + stride_c_n * offs_cn[None, :]
|
| 462 |
+
tl.store(c_ptrs, c, mask=(offs_cm == 0)[:, None] & col_ok[None, :])
|
| 463 |
+
else:
|
| 464 |
+
c_ptrs = C + stride_c_m * out_row[:, None] + stride_c_n * offs_cn[None, :]
|
| 465 |
+
tl.store(c_ptrs, c, mask=row_mask[:, None] & col_ok[None, :])
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
@triton.jit
|
| 469 |
+
def _epilogue_requant_fp8(
|
| 470 |
+
C, Cs, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
|
| 471 |
+
BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, FAKE_BATCH: tl.constexpr,
|
| 472 |
+
N_COLS: tl.constexpr,
|
| 473 |
+
):
|
| 474 |
+
"""Requantize the SwiGLU intermediate to fp8 with a per-(row, N-tile) scalar scale. UE8M0
|
| 475 |
+
intermediate scales under a UE8M0 model (inferred from Cs's dtype) keep the down proj's activation
|
| 476 |
+
scales power-of-two so its dot_scaled arm fires. FAKE_BATCH collapses the replicated rows to one
|
| 477 |
+
scalar per (row, N-tile)."""
|
| 478 |
+
q, q_s = fp8_act_quant_inline(out, UE8M0=Cs.dtype.element_ty == tl.uint8)
|
| 479 |
+
_store_out(C, q, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
|
| 480 |
+
cs_ptr = Cs + out_row * stride_cs_m + pid_n * stride_cs_n
|
| 481 |
+
if FAKE_BATCH: # replicated rows -> one scalar per (row, N-tile)
|
| 482 |
+
tl.store(cs_ptr, tl.max(q_s))
|
| 483 |
+
else:
|
| 484 |
+
tl.store(cs_ptr, q_s, mask=row_mask)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
@triton.jit
|
| 488 |
+
def _epilogue_requant_mx(
|
| 489 |
+
C, Cs, out, out_row, pid_n, pid_m, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
|
| 490 |
+
CSDescriptor, CsGlobal, cs_global_row,
|
| 491 |
+
BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
|
| 492 |
+
OUTPUT_FORMAT: tl.constexpr, SWIZZLED_OUT: tl.constexpr, FAKE_BATCH: tl.constexpr,
|
| 493 |
+
N_COLS: tl.constexpr,
|
| 494 |
+
):
|
| 495 |
+
"""Requantize the SwiGLU intermediate to MX group-``SCALE_GROUP_K`` (mxfp8 UE8M0 / mxfp4 / nvfp4
|
| 496 |
+
E4M3 — the fp4 formats pack nibble pairs so ``C`` halves). ``SWIZZLED_OUT`` writes ``Cs`` straight
|
| 497 |
+
into the down proj's SWIZZLE_32_4_4 descriptor at block ``(pid_m, pid_n)`` (BM/BN pinned 128), else
|
| 498 |
+
a row-major affine store. NVFP4 two-level: normalize by the next proj's provided input_scale
|
| 499 |
+
(``CsGlobal[cs_global_row]`` — per tensor or this expert's) before the block quant. FAKE_BATCH
|
| 500 |
+
collapses replicated rows via the row-max."""
|
| 501 |
+
if CsGlobal is not None:
|
| 502 |
+
# NVFP4 two-level requant: normalize the fp32 GLU intermediate by the NEXT proj's provided
|
| 503 |
+
# (calibrated) input_scale before the block quant — the canonical two-step. The down folds it
|
| 504 |
+
# back via its As pair ([Cs, g_out]); nothing is computed at runtime.
|
| 505 |
+
out = out / tl.load(CsGlobal + cs_global_row).to(tl.float32)
|
| 506 |
+
q, q_s = mx_act_quant_inline(out, BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K, OUTPUT_FORMAT)
|
| 507 |
+
width: tl.constexpr = BLOCK_SIZE_N if OUTPUT_FORMAT == "mxfp8" else BLOCK_SIZE_N // 2
|
| 508 |
+
_store_out(C, q, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, width, FAKE_BATCH,
|
| 509 |
+
N_COLS if OUTPUT_FORMAT == "mxfp8" else N_COLS // 2)
|
| 510 |
+
if SWIZZLED_OUT:
|
| 511 |
+
# group scales straight into the down proj's SWIZZLE_32_4_4 layout (inverse of
|
| 512 |
+
# load_swizzled_scale) at block (pid_m, pid_n) — BM/BN pinned 128.
|
| 513 |
+
REP_K_CS: tl.constexpr = (BLOCK_SIZE_N // SCALE_GROUP_K) // 4
|
| 514 |
+
sw = (
|
| 515 |
+
q_s.reshape(1, 4, 32, REP_K_CS, 4)
|
| 516 |
+
.trans(0, 3, 2, 1, 4)
|
| 517 |
+
.reshape(1, 1, REP_K_CS, 2, 256)
|
| 518 |
+
)
|
| 519 |
+
CSDescriptor.store([0, pid_m, pid_n * REP_K_CS, 0, 0], sw)
|
| 520 |
+
else:
|
| 521 |
+
offs_sc = pid_n * (BLOCK_SIZE_N // SCALE_GROUP_K) + tl.arange(0, BLOCK_SIZE_N // SCALE_GROUP_K)
|
| 522 |
+
if FAKE_BATCH:
|
| 523 |
+
# replicated rows -> the row-max IS the row's scale (f32-exact for UE8M0 exponent bytes
|
| 524 |
+
# and E4M3 values alike)
|
| 525 |
+
tl.store(
|
| 526 |
+
Cs + out_row * stride_cs_m + offs_sc[None, :] * stride_cs_n,
|
| 527 |
+
tl.reshape(tl.max(q_s.to(tl.float32), axis=0), (1, BLOCK_SIZE_N // SCALE_GROUP_K)),
|
| 528 |
+
)
|
| 529 |
+
else:
|
| 530 |
+
tl.store(
|
| 531 |
+
Cs + out_row[:, None] * stride_cs_m + offs_sc[None, :] * stride_cs_n,
|
| 532 |
+
q_s,
|
| 533 |
+
mask=row_mask[:, None],
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
@triton.jit
|
| 538 |
+
def gemm_epilogue(
|
| 539 |
+
C,
|
| 540 |
+
Cs, # row-major requant-scale pointer (None under SWIZZLED_OUT — CSDescriptor writes instead)
|
| 541 |
+
acc,
|
| 542 |
+
out_row,
|
| 543 |
+
pid_n,
|
| 544 |
+
pid_m,
|
| 545 |
+
row_mask,
|
| 546 |
+
stride_c_m,
|
| 547 |
+
stride_c_n,
|
| 548 |
+
stride_cs_m,
|
| 549 |
+
stride_cs_n,
|
| 550 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 551 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 552 |
+
GATE: tl.constexpr,
|
| 553 |
+
OUTPUT_FORMAT: tl.constexpr,
|
| 554 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 555 |
+
ACT_FN: tl.constexpr,
|
| 556 |
+
SWIGLU_ALPHA: tl.constexpr,
|
| 557 |
+
SWIGLU_LIMIT: tl.constexpr,
|
| 558 |
+
SIMULATE_UNFUSED: tl.constexpr,
|
| 559 |
+
INTERMEDIATE_DTYPE: tl.constexpr,
|
| 560 |
+
COMPUTE_MODE: tl.constexpr = "dot",
|
| 561 |
+
SWAP_AB: tl.constexpr = False,
|
| 562 |
+
SWIZZLED_OUT: tl.constexpr = False,
|
| 563 |
+
FAKE_BATCH: tl.constexpr = False,
|
| 564 |
+
N_COLS: tl.constexpr = 0, # >0 masks the column tail (2D dense N isn't BN-aligned); 0 = no mask
|
| 565 |
+
CSDescriptor=0, # SWIZZLE_32_4_4 requant-scale descriptor; read only under SWIZZLED_OUT (else dummy)
|
| 566 |
+
CsGlobal=None, # fp32 NVFP4 output global (the NEXT proj's provided input_scale), per tensor or per expert; normalizes the requant, None folds out
|
| 567 |
+
stride_cs_global=0, # expert stride of CsGlobal (0 broadcasts a scalar)
|
| 568 |
+
GlobalScale=None, # fp32 NVFP4 weight global g_b, per tensor or per expert; applied PRE-GLU, None folds out
|
| 569 |
+
stride_global_e=0, # expert stride of GlobalScale (global_scale_stride; 0 broadcasts a scalar)
|
| 570 |
+
global_row=0, # expert id indexing the globals and the bias (0 for the 2D case)
|
| 571 |
+
GlobalScaleA=None, # fp32 NVFP4 activation global g_a, per tensor or per expert; multiplied in-register with GlobalScale
|
| 572 |
+
stride_global_a=0, # expert stride of GlobalScaleA (0 broadcasts a scalar)
|
| 573 |
+
PreAct=None, # (M, 2N) pre-activation buffer; written iff not None — the GLU backward's Z
|
| 574 |
+
stride_pa_m=0,
|
| 575 |
+
stride_pa_n=0,
|
| 576 |
+
Bias=None, # (E, N_out) per-expert output bias, N_out = 2N under GATE; row is `global_row`
|
| 577 |
+
stride_bias_e=0,
|
| 578 |
+
stride_bias_n=0,
|
| 579 |
+
):
|
| 580 |
+
"""Unified output epilogue for grouped (a real scatter tile) and batched (fake-batch decode:
|
| 581 |
+
one token replicated across the BM lanes) GEMMs. Plain: cast + store the accumulator. ``GATE``:
|
| 582 |
+
split the stacked gate|up accumulator + SwiGLU (``split_gate_up_glu``); ``OUTPUT_FORMAT`` — the
|
| 583 |
+
``activation_format`` vocabulary — then requantizes into ``C`` + ``Cs``: ``"fp8"`` (per-(row, N-tile)
|
| 584 |
+
scalar), or MX group-``SCALE_GROUP_K`` (UE8M0/E4M3 — the fp4 formats pack nibble pairs so ``C``
|
| 585 |
+
halves), with ``SWIZZLED_OUT`` writing the MX ``Cs`` straight into the down proj's SWIZZLE_32_4_4
|
| 586 |
+
descriptor at block ``(pid_m, pid_n)`` (the tcgen05 fast path, BM/BN pinned 128). ``FAKE_BATCH``
|
| 587 |
+
shims the store: value masks to lane 0 (``C`` pre-advanced), the scale collapses the replicated
|
| 588 |
+
rows with ``tl.max``; else a real BM-row scatter (``out_row`` + ``row_mask``).
|
| 589 |
+
``COMPUTE_MODE``/``SWAP_AB`` orient the decode GLU/finalize (grouped passes ``"dot"``/no-swap,
|
| 590 |
+
both no-ops there). Every arm is constexpr-pruned."""
|
| 591 |
+
# The globals fold on the FINALIZED tile: a swapped accumulator still carries the token dim
|
| 592 |
+
# padded to the MMA atom, so folding before the collapse costs 16x the multiplies.
|
| 593 |
+
if GATE:
|
| 594 |
+
# Finalize + bias once, up here rather than inside the split: the gate|up bias belongs to
|
| 595 |
+
# the pre-activation, so PreAct below has to see it too.
|
| 596 |
+
flat = add_bias(
|
| 597 |
+
apply_global_scale(
|
| 598 |
+
acc_finalize(acc, COMPUTE_MODE, 2 * BLOCK_SIZE_N, SWAP_AB),
|
| 599 |
+
GlobalScale, stride_global_e, GlobalScaleA, stride_global_a, global_row,
|
| 600 |
+
),
|
| 601 |
+
Bias, stride_bias_e, stride_bias_n, global_row, pid_n,
|
| 602 |
+
2 * BLOCK_SIZE_N, 2 * N_COLS if N_COLS > 0 else 0,
|
| 603 |
+
)
|
| 604 |
+
# SAVE_PREACT: the GLU backward needs Z, the 2*BN-wide pre-activation. Interleaved, that
|
| 605 |
+
# tile IS an ungated output tile at doubled width (pid_n * 2BN == 2 * pid_n * BN), so the
|
| 606 |
+
# ordinary store serves it — no separate path, no gated store arm.
|
| 607 |
+
if PreAct is not None:
|
| 608 |
+
_store_out(
|
| 609 |
+
PreAct, flat,
|
| 610 |
+
out_row, pid_n, row_mask, stride_pa_m, stride_pa_n,
|
| 611 |
+
BLOCK_SIZE_M, 2 * BLOCK_SIZE_N, FAKE_BATCH,
|
| 612 |
+
2 * N_COLS if N_COLS > 0 else 0,
|
| 613 |
+
)
|
| 614 |
+
out = split_gate_up_glu(
|
| 615 |
+
flat, BLOCK_SIZE_M, BLOCK_SIZE_N, SWAP_AB,
|
| 616 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 617 |
+
)
|
| 618 |
+
if OUTPUT_FORMAT == "fp8":
|
| 619 |
+
_epilogue_requant_fp8(
|
| 620 |
+
C, Cs, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
|
| 621 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS,
|
| 622 |
+
)
|
| 623 |
+
elif OUTPUT_FORMAT is not None: # "mxfp8" | "mxfp4" | "nvfp4"
|
| 624 |
+
_epilogue_requant_mx(
|
| 625 |
+
C, Cs, out, out_row, pid_n, pid_m, row_mask, stride_c_m, stride_c_n, stride_cs_m,
|
| 626 |
+
stride_cs_n, CSDescriptor, CsGlobal, global_row * stride_cs_global,
|
| 627 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_GROUP_K, OUTPUT_FORMAT, SWIZZLED_OUT, FAKE_BATCH, N_COLS,
|
| 628 |
+
)
|
| 629 |
+
else: # bf16 (unquantized) SwiGLU output
|
| 630 |
+
_store_out(C, out, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
|
| 631 |
+
else: # plain GEMM: cast + store the accumulator (no fused requant on the non-gate path)
|
| 632 |
+
acc = add_bias(
|
| 633 |
+
apply_global_scale(
|
| 634 |
+
acc_finalize(acc, COMPUTE_MODE, BLOCK_SIZE_N, SWAP_AB),
|
| 635 |
+
GlobalScale, stride_global_e, GlobalScaleA, stride_global_a, global_row,
|
| 636 |
+
),
|
| 637 |
+
Bias, stride_bias_e, stride_bias_n, global_row, pid_n, BLOCK_SIZE_N, N_COLS,
|
| 638 |
+
)
|
| 639 |
+
_store_out(C, acc, out_row, pid_n, row_mask, stride_c_m, stride_c_n, BLOCK_SIZE_M, BLOCK_SIZE_N, FAKE_BATCH, N_COLS)
|
build/torch-xpu/finegrained_kernels/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ctypes
|
| 2 |
+
import importlib.util
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from types import ModuleType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _import_from_path(file_path: Path) -> ModuleType:
|
| 9 |
+
# We cannot use the module name as-is, after adding it to `sys.modules`,
|
| 10 |
+
# it would also be used for other imports. So, we make a module name that
|
| 11 |
+
# depends on the path for it to be unique using the hex-encoded hash of
|
| 12 |
+
# the path.
|
| 13 |
+
path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
|
| 14 |
+
module_name = path_hash
|
| 15 |
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
| 16 |
+
if spec is None:
|
| 17 |
+
raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
|
| 18 |
+
module = importlib.util.module_from_spec(spec)
|
| 19 |
+
if module is None:
|
| 20 |
+
raise ImportError(f"Cannot load module {module_name} from spec")
|
| 21 |
+
sys.modules[module_name] = module
|
| 22 |
+
spec.loader.exec_module(module) # type: ignore
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
|
build/torch-xpu/formats.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import triton
|
| 17 |
+
|
| 18 |
+
from .compat import MX_SCALE_GROUP_K, NIBBLES_PER_BYTE, NVFP4_SCALE_GROUP_K
|
| 19 |
+
|
| 20 |
+
def ue8m0_as_uint8(scale: torch.Tensor | None) -> torch.Tensor | None:
|
| 21 |
+
"""View UE8M0 (``float8_e8m0fnu``) weight scales as ``uint8`` for the Triton
|
| 22 |
+
binder, which doesn't recognize the dtype; kernels decode ``2^(exp-127)``
|
| 23 |
+
inline. fp32 (non-UE8M0) scales pass through unchanged; ``None`` (an absent
|
| 24 |
+
optional scale) passes through as ``None`` — kernels take the arg as None."""
|
| 25 |
+
if scale is None:
|
| 26 |
+
return None
|
| 27 |
+
return scale.view(torch.uint8) if scale.dtype == torch.float8_e8m0fnu else scale
|
| 28 |
+
|
| 29 |
+
def e2m1_as_uint8(weight: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
"""View an ``int8``-stored MXFP4 (packed E2M1) weight as ``uint8`` — a zero-cost
|
| 31 |
+
reinterpret. ``tl.dot_scaled`` requires the packed rhs as ``uint8``, so do it once here
|
| 32 |
+
instead of casting in-kernel at every load. E4M3 (MXFP8) weights pass through unchanged."""
|
| 33 |
+
return weight.view(torch.uint8) if weight.dtype == torch.int8 else weight
|
| 34 |
+
|
| 35 |
+
# UE8M0 group-32 scales arrive either as ``float8_e8m0fnu`` or as raw ``uint8`` — the same 8
|
| 36 |
+
# exponent bits, and a common on-disk encoding (e.g. group-32 "mxfp8" checkpoints store the
|
| 37 |
+
# scale tensor as uint8). Both are valid MX scales: ``ue8m0_as_uint8`` reinterprets to uint8
|
| 38 |
+
# and the kernels decode ``2^(exp-127)`` inline, so the detectors accept either dtype.
|
| 39 |
+
UE8M0_SCALE_DTYPES = (torch.float8_e8m0fnu, torch.uint8)
|
| 40 |
+
|
| 41 |
+
def _shapes_match(weight: torch.Tensor, scale: torch.Tensor, group: int) -> bool:
|
| 42 |
+
"""Shape leg of the family predicates: matching leading dims and a last dim of
|
| 43 |
+
one scale per ``group`` unpacked values. Early-return ``if``s, not an ``and``
|
| 44 |
+
chain: callers compare predicate results (``is_x(gate) != is_x(down)``), and an
|
| 45 |
+
``and`` chain hands them a lazy SymBool under dynamo — the resulting nested
|
| 46 |
+
symbolic Eq crashes ``evaluate_expr``. Control flow forces each comparison to a
|
| 47 |
+
real bool (weight shapes are static parameters, so the guards are correct)."""
|
| 48 |
+
packed = NIBBLES_PER_BYTE if weight.dtype == torch.int8 else 1
|
| 49 |
+
if scale.shape[:-1] != weight.shape[:-1]:
|
| 50 |
+
return False
|
| 51 |
+
if scale.shape[-1] != (weight.shape[-1] * packed) // group:
|
| 52 |
+
return False
|
| 53 |
+
return True
|
| 54 |
+
|
| 55 |
+
def is_mxfp8(weight: torch.Tensor, scale: torch.Tensor) -> bool:
|
| 56 |
+
"""MXFP8 weight/scale pair: E4M3 weights with UE8M0 group-32 scales — last dim
|
| 57 |
+
``scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K``, matching leading dims.
|
| 58 |
+
Works for 2D ``(N, K)`` and 3D ``(E, N, K)`` weights. The group-32 layout is what
|
| 59 |
+
separates MXFP8 from 128-block FP8 (which may also carry UE8M0 scales)."""
|
| 60 |
+
return (
|
| 61 |
+
weight.dtype == torch.float8_e4m3fn
|
| 62 |
+
and scale.dtype in UE8M0_SCALE_DTYPES
|
| 63 |
+
and _shapes_match(weight, scale, MX_SCALE_GROUP_K)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def is_mxfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
|
| 67 |
+
"""MXFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with
|
| 68 |
+
UE8M0 group-32 scales — ``scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE //
|
| 69 |
+
MX_SCALE_GROUP_K`` (unpacked K = ``2 * K_half``), matching leading dims. 2D or 3D."""
|
| 70 |
+
return (
|
| 71 |
+
weight.dtype == torch.int8
|
| 72 |
+
and scale.dtype in UE8M0_SCALE_DTYPES
|
| 73 |
+
and _shapes_match(weight, scale, MX_SCALE_GROUP_K)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
def is_nvfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
|
| 77 |
+
"""NVFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with E4M3
|
| 78 |
+
group-16 block scales — the scale DTYPE is the format carrier (UE8M0 = MX, E4M3 = NV), and
|
| 79 |
+
the group falls out of the shape. This predicate reads the block scale; the per-tensor
|
| 80 |
+
second-level global is a separate ``b_global_scale`` argument (``nvfp4_quantize_two_level``).
|
| 81 |
+
Layout-agnostic questions ("is this NVFP4, swizzled or not?") go through ``weight_format``,
|
| 82 |
+
which carries the same dtype rule without the shape check."""
|
| 83 |
+
return (
|
| 84 |
+
weight.dtype == torch.int8
|
| 85 |
+
and scale.dtype == torch.float8_e4m3fn
|
| 86 |
+
and _shapes_match(weight, scale, NVFP4_SCALE_GROUP_K)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
def is_preswizzled_mx(weight: torch.Tensor, scale: torch.Tensor) -> bool:
|
| 90 |
+
"""A weight scale already in the SWIZZLE_32_4_4 layout, swizzled once at model load (the
|
| 91 |
+
deployment contract — the same checkpoint feeds grouped prefill and batched decode with no
|
| 92 |
+
per-call rearrange). The 5D shape ``(G, row_blocks, cols//4, 2, 256)`` is the marker; the scale is
|
| 93 |
+
a 1-byte block scale (UE8M0 for MXFP8/MXFP4, E4M3 for NVFP4) against an MX weight (E4M3 or
|
| 94 |
+
packed E2M1). Format-agnostic — NVFP4 pre-swizzles the same way (the layout cuBLAS wants).
|
| 95 |
+
6D is the gate|up artifact: the same blocks with the gate-interleave carried in the shape."""
|
| 96 |
+
return (
|
| 97 |
+
weight.dtype in (torch.float8_e4m3fn, torch.int8)
|
| 98 |
+
and scale.ndim in (5, 6)
|
| 99 |
+
and scale.element_size() == 1
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
def is_mx(weight: torch.Tensor, scale: torch.Tensor) -> bool:
|
| 103 |
+
"""Any microscaled weight/scale pair — MXFP8 (``float8_e4m3fn`` values), MXFP4
|
| 104 |
+
(``int8``, two E2M1 codes/byte), both UE8M0 group-32, or NVFP4 (packed E2M1 + E4M3
|
| 105 |
+
group-16); also an already-swizzled MX scale (``is_preswizzled_mx``). The dispatchers route
|
| 106 |
+
on this into the ``mx_*`` kernels; the op picks the format from the dtypes."""
|
| 107 |
+
return (
|
| 108 |
+
is_mxfp8(weight, scale)
|
| 109 |
+
or is_mxfp4(weight, scale)
|
| 110 |
+
or is_nvfp4(weight, scale)
|
| 111 |
+
or is_preswizzled_mx(weight, scale)
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
def is_tensor_wide(block_size, weight: torch.Tensor) -> bool:
|
| 115 |
+
"""True when ``block_size`` selects per-tensor (tensor-dynamic) scaling: ``None`` or
|
| 116 |
+
equal to the weight's full ``(N, K)`` — one scale block spanning the whole matrix.
|
| 117 |
+
Handles 2D ``(N, K)`` and 3D ``(E, N, K)`` weights via the last two dims. (2D path
|
| 118 |
+
only — the grouped/batched dispatchers derive the format from the SCALE shape via
|
| 119 |
+
``weight_block_size``.)"""
|
| 120 |
+
return block_size is None or (
|
| 121 |
+
block_size[0] == weight.shape[-2] and block_size[1] == weight.shape[-1]
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
def weight_block_size(B: torch.Tensor, Bs: torch.Tensor) -> list[int] | None:
|
| 125 |
+
"""The fp8 weight-quantization block ``[block_n, block_k]``, derived from the scale
|
| 126 |
+
tensor's shape — the data already says how it was quantized, so no ``block_size``
|
| 127 |
+
parameter exists to disagree with it. ``None`` = tensor-wide (one scalar per expert:
|
| 128 |
+
``Bs`` ``(E,)`` or ``(E, 1, 1)`` spanning the full ``(N, K)``). Expects a 3D
|
| 129 |
+
``(E, N, K)`` fp8 ``B``; MX weights never reach here (format keyed by scale dtype)."""
|
| 130 |
+
num_experts, n_rows, K = B.shape
|
| 131 |
+
if Bs.numel() == num_experts:
|
| 132 |
+
return None
|
| 133 |
+
assert Bs.ndim == 3 and K % Bs.shape[2] == 0, (
|
| 134 |
+
f"Bs shape {tuple(Bs.shape)} does not tile B {tuple(B.shape)} along K"
|
| 135 |
+
)
|
| 136 |
+
# K tiles evenly, so block_k is exact; N (n_rows) may be non-aligned (a partial last block,
|
| 137 |
+
# n_rows % Bs.shape[1] != 0) — recover block_n from the even K dim (square FP8 blocks). The
|
| 138 |
+
# routed kernels then reject the non-aligned N via ``require_moe_dims_aligned``; matmul_2d masks it.
|
| 139 |
+
block_k = K // Bs.shape[2]
|
| 140 |
+
block_n = n_rows // Bs.shape[1] if n_rows % Bs.shape[1] == 0 else block_k
|
| 141 |
+
return [block_n, block_k]
|
| 142 |
+
|
| 143 |
+
def validate_dense_operands(A: torch.Tensor, B: torch.Tensor) -> None:
|
| 144 |
+
"""Shared (rows, K) x (num_experts, N, K) operand checks for the unpacked formats —
|
| 145 |
+
the packed-E2M1 ops do their own (K spans two values per stored byte)."""
|
| 146 |
+
assert A.ndim == 2, f"A must be 2D (rows, K), got ndim={A.ndim}"
|
| 147 |
+
assert A.is_contiguous(), "A must be contiguous"
|
| 148 |
+
assert B.ndim == 3, f"B must be 3D (num_experts, N, K), got ndim={B.ndim}"
|
| 149 |
+
assert B.is_contiguous(), "B must be contiguous"
|
| 150 |
+
assert A.shape[1] == B.shape[2], (
|
| 151 |
+
f"K mismatch: A has K={A.shape[1]}, B has K={B.shape[2]}"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def validate_dense_2d_operands(A: torch.Tensor, B: torch.Tensor) -> None:
|
| 155 |
+
"""Shared (rows, K) x (N, K) operand checks for the 2D dense unpacked-fp8 wrappers — matching
|
| 156 |
+
K, contiguous A, 2D B. The packed-E2M1 (MX) 2D op does its own (K is two values per
|
| 157 |
+
stored byte, so B is (N, K // 2)).
|
| 158 |
+
|
| 159 |
+
B may be row-major or a transposed view of a row-major tensor. The kernels read both of B's
|
| 160 |
+
strides, and ``descriptor_box_pruner`` keeps the TMA arms — which require a unit-stride
|
| 161 |
+
innermost dim — off the transposed form, so a caller can contract over the other axis by
|
| 162 |
+
passing ``B.t()`` (with its scale grid transposed to match) instead of materializing a copy."""
|
| 163 |
+
assert A.shape[-1] == B.shape[-1], (
|
| 164 |
+
f"K mismatch: A has K={A.shape[-1]}, B has K={B.shape[-1]}"
|
| 165 |
+
)
|
| 166 |
+
assert A.is_contiguous(), "A must be contiguous"
|
| 167 |
+
assert B.ndim == 2, f"B must be 2D (N, K), got ndim={B.ndim}"
|
| 168 |
+
assert B.is_contiguous() or B.t().is_contiguous(), (
|
| 169 |
+
"B must be contiguous or a transposed view of a contiguous tensor, got strides "
|
| 170 |
+
f"{tuple(B.stride())} for shape {tuple(B.shape)}"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
def expert_weight_shape(B: torch.Tensor, gate: bool) -> tuple[int, int, int]:
|
| 174 |
+
"""(num_experts, n_rows, N) of an expert weight stack — under a gate epilogue B is
|
| 175 |
+
the (E, 2N, K) gate|up stack, so the per-projection width N is half the stored rows."""
|
| 176 |
+
num_experts, n_rows, _ = B.shape
|
| 177 |
+
return num_experts, n_rows, (n_rows // 2 if gate else n_rows)
|
| 178 |
+
|
| 179 |
+
def routed_rows(A, gather_idx, scatter_idx, expert_start, num_experts) -> int:
|
| 180 |
+
"""S (routed rows) for a grouped launch, carried by the (S,) maps — A's rows are
|
| 181 |
+
gather SOURCES and under-count S whenever top_k > 1 (gate_up reading raw hidden);
|
| 182 |
+
only with no maps at all is A itself the expert-sorted (S, K) matrix. Validates the
|
| 183 |
+
maps and the ``(next_power_of_2(E) + 1,)`` ``expert_start`` schedule."""
|
| 184 |
+
if gather_idx is not None:
|
| 185 |
+
S = gather_idx.numel()
|
| 186 |
+
elif scatter_idx is not None:
|
| 187 |
+
S = scatter_idx.numel()
|
| 188 |
+
else:
|
| 189 |
+
S = A.shape[0]
|
| 190 |
+
for perm_map in (gather_idx, scatter_idx):
|
| 191 |
+
assert perm_map is None or (perm_map.numel() == S and perm_map.is_contiguous())
|
| 192 |
+
assert (
|
| 193 |
+
expert_start.is_contiguous()
|
| 194 |
+
and expert_start.numel() == triton.next_power_of_2(num_experts) + 1
|
| 195 |
+
), "expert_start must be contiguous (next_power_of_2(num_experts) + 1,)"
|
| 196 |
+
return S
|
| 197 |
+
|
| 198 |
+
def tokens_per_expert_bucket(S: int, num_experts: int) -> int:
|
| 199 |
+
"""log2 bucket of the average routed rows per expert — the grouped kernels' autotune
|
| 200 |
+
key (raw S would retune per unique token count)."""
|
| 201 |
+
return int((S + num_experts - 1) // num_experts).bit_length()
|
| 202 |
+
|
| 203 |
+
def normalize_global_scale(
|
| 204 |
+
g: torch.Tensor | None, num_experts: int
|
| 205 |
+
) -> torch.Tensor | None:
|
| 206 |
+
"""An NVFP4 second-level global in one of the kernels' fp32 load layouts: ``(1,)`` per tensor
|
| 207 |
+
or ``(num_experts,)`` per expert (``(E, 1, 1)`` accepted for the latter). The kernels index it
|
| 208 |
+
through ``global_scale_stride``, so a shared value broadcasts by stride and nothing is
|
| 209 |
+
materialized per call. ``None`` passes through.
|
| 210 |
+
|
| 211 |
+
A gate|up stack calibrated per half (modelopt's ``(E, 2)`` ``weight_scale_2``) is merged to one
|
| 212 |
+
global per expert at load — SwiGLU is linear in the up half, so its global folds into the down
|
| 213 |
+
projection's — never here: the ops take one global per matrix."""
|
| 214 |
+
if g is None:
|
| 215 |
+
return None
|
| 216 |
+
g = g.float()
|
| 217 |
+
if g.numel() == 1:
|
| 218 |
+
return g.reshape(1)
|
| 219 |
+
assert g.numel() == num_experts, (
|
| 220 |
+
f"global scale of shape {tuple(g.shape)}: expected a scalar or ({num_experts},)"
|
| 221 |
+
)
|
| 222 |
+
return g.reshape(num_experts).contiguous()
|
| 223 |
+
|
| 224 |
+
def is_per_expert_global(g: torch.Tensor | None) -> bool:
|
| 225 |
+
"""Whether an NVFP4 second-level global carries one value per expert rather than one for the
|
| 226 |
+
whole tensor. The activation quant branches on it: a per-expert global has to be applied by
|
| 227 |
+
each quantized row's own expert, which is what decides the grid that quant runs on."""
|
| 228 |
+
return g is not None and g.numel() > 1
|
| 229 |
+
|
| 230 |
+
def global_scale_stride(g: torch.Tensor | None) -> int:
|
| 231 |
+
"""The expert stride of a ``normalize_global_scale`` layout: 0 for a scalar (or ``None``),
|
| 232 |
+
which broadcasts it to every expert, else 1."""
|
| 233 |
+
return 0 if g is None or g.numel() == 1 else 1
|
| 234 |
+
|
| 235 |
+
def normalize_per_expert_scale(Bs: torch.Tensor, num_experts: int) -> torch.Tensor:
|
| 236 |
+
"""One per-tensor scale per expert, normalized to ``(num_experts, 1, 1)`` from
|
| 237 |
+
either that or a bare ``(num_experts,)``."""
|
| 238 |
+
if Bs.ndim == 1:
|
| 239 |
+
assert Bs.shape[0] == num_experts, (
|
| 240 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({num_experts},)"
|
| 241 |
+
)
|
| 242 |
+
return Bs.reshape(num_experts, 1, 1)
|
| 243 |
+
assert Bs.shape == (num_experts, 1, 1), (
|
| 244 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({num_experts}, 1, 1)"
|
| 245 |
+
)
|
| 246 |
+
return Bs
|
| 247 |
+
|
| 248 |
+
def mx_scale_family(Bs: torch.Tensor, K: int) -> int:
|
| 249 |
+
"""The group size of an MX/NV weight-scale tensor, in either layout — the wrapper hands ``Bs``
|
| 250 |
+
as-is and this reads the group off its shape. Row-major (2D/3D): ``K // Bs.shape[-1]``.
|
| 251 |
+
SWIZZLE_32_4_4 (5D ``(G, row_blocks, cols // 4, 2, 256)``): ``K // (Bs.shape[2] * 4)``. The scale
|
| 252 |
+
dtype IS the format carrier (E4M3 = NVFP4 group-16, UE8M0 = MX group-32) and the pairing is
|
| 253 |
+
validated; callers that need the format read it off ``Bs.dtype`` (``== torch.float8_e4m3fn``
|
| 254 |
+
is NVFP4)."""
|
| 255 |
+
nvfp4 = Bs.dtype == torch.float8_e4m3fn
|
| 256 |
+
assert nvfp4 or Bs.dtype in UE8M0_SCALE_DTYPES, (
|
| 257 |
+
f"Bs must be UE8M0 (float8_e8m0fnu/uint8) or E4M3 (NVFP4), got {Bs.dtype}"
|
| 258 |
+
)
|
| 259 |
+
scale_group = K // (Bs.shape[2] * 4) if Bs.ndim == 5 else K // Bs.shape[-1]
|
| 260 |
+
assert scale_group == (NVFP4_SCALE_GROUP_K if nvfp4 else MX_SCALE_GROUP_K), (
|
| 261 |
+
f"scale group {scale_group} does not match the scale dtype {Bs.dtype}"
|
| 262 |
+
)
|
| 263 |
+
assert K % scale_group == 0, f"K (={K}) must be a multiple of {scale_group}"
|
| 264 |
+
return scale_group
|
| 265 |
+
|
| 266 |
+
def get_supported_act_fns() -> tuple[str, ...]:
|
| 267 |
+
"""The activation names the gate|up epilogue fuses in-kernel; any other activation is applied
|
| 268 |
+
on the host by the MoE forwards (``act_fn`` as a callable)."""
|
| 269 |
+
return ("silu", "gelu", "relu")
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
ACTIVATION_FORMATS = (None, "bf16", "fp8", "mxfp8", "mxfp4", "nvfp4")
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def check_activation_format(activation_format: str | None, quantize_output: bool) -> None:
|
| 276 |
+
"""Validate the dispatchers' activation kwargs. ``activation_format`` is the activations'
|
| 277 |
+
quantization, in the same vocabulary transformers' ``FineGrainedConfig`` uses; the weights are
|
| 278 |
+
never named — their format is read off the weight/scale dtypes (``weight_format``).
|
| 279 |
+
|
| 280 |
+
- ``None`` (default): the weights' own format — W8A8 on FP8/MXFP8 weights, the all-fp4 W4A4
|
| 281 |
+
chain on MXFP4/NVFP4 weights.
|
| 282 |
+
- ``"bf16"``: weight-only (W4A16 / W8A16) — the activation stays high precision.
|
| 283 |
+
- ``"fp8"`` / ``"mxfp8"`` / ``"mxfp4"`` / ``"nvfp4"``: an explicit activation format, e.g.
|
| 284 |
+
``"mxfp8"`` on MXFP4 weights is the W4A8 chain. A raw ``A`` is quantized to it by the op; an
|
| 285 |
+
already-quantized ``A`` arrives with ``As`` (its dtype carries the format).
|
| 286 |
+
|
| 287 |
+
``quantize_output`` stores the (GLU) output requantized into that same format — bit-identical
|
| 288 |
+
to quantizing the values offline — so it feeds the next op's activation side directly; the op
|
| 289 |
+
then returns ``(C, Cs)``. A weight-only activation never requantizes.
|
| 290 |
+
|
| 291 |
+
Per weight family:
|
| 292 |
+
|
| 293 |
+
================== ===============================================================
|
| 294 |
+
weight family activation_format
|
| 295 |
+
================== ===============================================================
|
| 296 |
+
block-dynamic FP8 None / "fp8" (E4M3 + per-block scales); quantize_output ok
|
| 297 |
+
tensor-wide FP8 None / "fp8" (E4M3 + per-token scales); no quantize_output
|
| 298 |
+
MXFP8 / MXFP4 None / "mxfp8" (E4M3 + UE8M0) / "mxfp4" (packed E2M1 + UE8M0) / "bf16"
|
| 299 |
+
NVFP4 None / "nvfp4" (packed E2M1 + E4M3 group-16) / "bf16"
|
| 300 |
+
full-precision None (nothing to quantize)
|
| 301 |
+
================== ===============================================================
|
| 302 |
+
|
| 303 |
+
(Tensor-wide's whole-row activation scale can't be formed by a tile-local epilogue; the unfused
|
| 304 |
+
path quantizes on the host between GEMMs.) A format the weight family doesn't support fails
|
| 305 |
+
loudly at the op."""
|
| 306 |
+
assert activation_format in ACTIVATION_FORMATS, (
|
| 307 |
+
f"unknown activation_format {activation_format!r}; expected one of {ACTIVATION_FORMATS}"
|
| 308 |
+
)
|
| 309 |
+
assert not (quantize_output and activation_format == "bf16"), (
|
| 310 |
+
"quantize_output needs quantized activations (activation_format='bf16' is weight-only)"
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def weight_format(B: torch.Tensor, Bs: torch.Tensor) -> str:
|
| 315 |
+
"""The format the weights are ALREADY in, read off their dtypes — what ``activation_format=None``
|
| 316 |
+
resolves to. E4M3 scales -> ``"nvfp4"``; else packed E2M1 -> ``"mxfp4"``, MX E4M3 -> ``"mxfp8"``,
|
| 317 |
+
block/tensor-scaled E4M3 -> ``"fp8"``."""
|
| 318 |
+
if Bs.dtype == torch.float8_e4m3fn:
|
| 319 |
+
return "nvfp4"
|
| 320 |
+
if not is_mx(B, Bs):
|
| 321 |
+
return "fp8"
|
| 322 |
+
# packed E2M1 is a WEIGHT dtype fact; is_mxfp4's shape check can't see it through a
|
| 323 |
+
# pre-swizzled (5D) scale, which would silently downgrade a W4A4 default to W4A8.
|
| 324 |
+
return "mxfp4" if B.dtype in (torch.int8, torch.uint8) else "mxfp8"
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def resolve_activation_format(activation_format: str | None, B: torch.Tensor, Bs: torch.Tensor) -> str:
|
| 330 |
+
"""The format an MX/NVFP4 GEMM quantizes its activations to. ``None`` resolves to the weights'
|
| 331 |
+
own format (``weight_format``): NVFP4 weights pin ``"nvfp4"`` (the MMA kind needs matching
|
| 332 |
+
scale formats on both operands), packed E2M1 -> ``"mxfp4"`` (the all-fp4 W4A4 chain), else
|
| 333 |
+
``"mxfp8"``. An explicit name overrides (e.g. ``"mxfp8"`` on MXFP4 weights = the W4A8 chain)
|
| 334 |
+
and is validated against the family. ``"bf16"`` never reaches here — a weight-only chain is
|
| 335 |
+
routed to its own kernel by the dispatcher."""
|
| 336 |
+
if Bs.dtype == torch.float8_e4m3fn:
|
| 337 |
+
assert activation_format in (None, "nvfp4"), (
|
| 338 |
+
f"NVFP4 activations are packed E2M1 + E4M3 scales, got {activation_format!r}"
|
| 339 |
+
)
|
| 340 |
+
return "nvfp4"
|
| 341 |
+
assert activation_format in (None, "mxfp8", "mxfp4"), (
|
| 342 |
+
f"MX activations are E4M3 ('mxfp8') or packed E2M1 ('mxfp4'), got {activation_format!r}"
|
| 343 |
+
)
|
| 344 |
+
return activation_format if activation_format is not None else weight_format(B, Bs)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def resolve_output_dtype(
|
| 348 |
+
output_dtype: torch.dtype | None,
|
| 349 |
+
activation: torch.Tensor,
|
| 350 |
+
act_scale: torch.Tensor | None,
|
| 351 |
+
) -> torch.dtype:
|
| 352 |
+
"""Output element type for a quantized matmul: the explicit ``output_dtype`` if given, else
|
| 353 |
+
the raw activation dtype (``act_scale`` is None -> ``activation`` is high precision), else
|
| 354 |
+
``bfloat16`` (``activation`` is pre-quantized FP8, whose dtype is not a valid output)."""
|
| 355 |
+
if output_dtype is not None:
|
| 356 |
+
return output_dtype
|
| 357 |
+
return activation.dtype if act_scale is None else torch.bfloat16
|
build/torch-xpu/grouped.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
build/torch-xpu/loading/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""How a K-step's operands get from memory into registers: ``tiles`` loads the activation and
|
| 16 |
+
weight value tiles (pointer / host-TMA / gather arms), ``scales`` loads the block scales (affine or
|
| 17 |
+
SWIZZLE_32_4_4) and quantizes a raw activation tile inline."""
|
build/torch-xpu/loading/scales.py
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import triton
|
| 16 |
+
import triton.language as tl
|
| 17 |
+
|
| 18 |
+
from ..quant import fp8_act_quant_inline, mx_act_quant_inline
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@triton.jit
|
| 22 |
+
def load_block_fp8_act_tile(
|
| 23 |
+
a_ptrs,
|
| 24 |
+
as_ptrs,
|
| 25 |
+
a_descriptor=0,
|
| 26 |
+
m_off=0,
|
| 27 |
+
k_off=0,
|
| 28 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 29 |
+
as_mask=None,
|
| 30 |
+
TRANSPOSED: tl.constexpr = False,
|
| 31 |
+
):
|
| 32 |
+
"""Block-FP8 counterpart of ``load_mx_act_tile``: load one activation K-tile as
|
| 33 |
+
``(a_fp8, a_scale_f32)`` — the arm folds off ``A_MEMORY_MODE`` and the pointer dtype at
|
| 34 |
+
compile time. Descriptor mode loads the pre-quantized ``(BM, BK)`` host-TMA box at
|
| 35 |
+
``(m_off, k_off)`` (``a_ptrs`` unread); pointer fp8 loads the pre-quantized-offline tile;
|
| 36 |
+
raw bf16/fp16 pointers quantize inline (``as_ptrs`` then a constexpr-dead placeholder).
|
| 37 |
+
Either way the per-K-block scales come from ``as_ptrs``. ``TRANSPOSED`` marks a ``(K, M)``
|
| 38 |
+
tile (the swapped pointer arm) so the inline amax reduces the token axis either way.
|
| 39 |
+
Unmasked unless ``as_mask`` given: every caller's rows are %-wrapped, expert-advanced, or
|
| 40 |
+
token-replicated."""
|
| 41 |
+
if A_MEMORY_MODE != "pointer": # pre-quantized, host-TMA box [BM, BK]
|
| 42 |
+
a = a_descriptor.load([m_off, k_off])
|
| 43 |
+
# as_mask None (batched/grouped: %-wrapped/expert-advanced rows) -> unmasked load;
|
| 44 |
+
# a bounds mask (the 2D affine-scale path) -> masked. `other` is illegal without mask.
|
| 45 |
+
if as_mask is None:
|
| 46 |
+
a_s = tl.load(as_ptrs)
|
| 47 |
+
else:
|
| 48 |
+
a_s = tl.load(as_ptrs, mask=as_mask, other=0.0)
|
| 49 |
+
elif a_ptrs.dtype.element_ty == tl.float8e4nv: # pre-quantized offline
|
| 50 |
+
a = tl.load(a_ptrs)
|
| 51 |
+
if as_mask is None:
|
| 52 |
+
a_s = tl.load(as_ptrs)
|
| 53 |
+
else:
|
| 54 |
+
a_s = tl.load(as_ptrs, mask=as_mask, other=0.0)
|
| 55 |
+
else: # raw bf16/fp16 — quantize inline
|
| 56 |
+
a, a_s = fp8_act_quant_inline(tl.load(a_ptrs).to(tl.float32), TRANSPOSED)
|
| 57 |
+
return a, a_s
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@triton.jit
|
| 61 |
+
def load_swizzled_scale(
|
| 62 |
+
desc, blk_idx, k_idx,
|
| 63 |
+
REP: tl.constexpr, REP_K: tl.constexpr, BLOCK: tl.constexpr, SCALE_COLS: tl.constexpr,
|
| 64 |
+
):
|
| 65 |
+
"""Bulk-load one scale tile from a ``SWIZZLE_32_4_4`` descriptor and un-swizzle it to the
|
| 66 |
+
``(BLOCK, SCALE_COLS)`` layout ``tl.dot_scaled`` consumes. ``REP = BLOCK // 128``,
|
| 67 |
+
``REP_K = SCALE_COLS // 4``; ``blk_idx``/``k_idx`` are the tile's row-block / K-block ids.
|
| 68 |
+
Descriptor is over the swizzled scale viewed ``(1, rows//128, cols//4, 2, 256)``."""
|
| 69 |
+
s = desc.load([0, blk_idx * REP, k_idx * REP_K, 0, 0])
|
| 70 |
+
return s.reshape(REP, REP_K, 32, 4, 4).trans(0, 3, 2, 1, 4).reshape(BLOCK, SCALE_COLS)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@triton.jit
|
| 74 |
+
def load_swizzled_scale_tile(
|
| 75 |
+
descriptor,
|
| 76 |
+
ptr,
|
| 77 |
+
group_id,
|
| 78 |
+
pid,
|
| 79 |
+
k_idx,
|
| 80 |
+
rows,
|
| 81 |
+
K,
|
| 82 |
+
BLOCK: tl.constexpr,
|
| 83 |
+
SCALE_COLS: tl.constexpr,
|
| 84 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 85 |
+
):
|
| 86 |
+
"""One swizzled-scale tile ``(BLOCK, SCALE_COLS)`` for a row-tile of ANY operand — batched
|
| 87 |
+
weight (``group_id = expert``), 2D weight / 2D activation (``group_id = 0``, dense). Scales are
|
| 88 |
+
SWIZZLE_32_4_4 128-row blocks over ``rows`` (the operand's row count, ``rows//128`` blocks per
|
| 89 |
+
group).
|
| 90 |
+
|
| 91 |
+
- ``BLOCK`` a multiple of 128 with whole 4-group K bands (``BK % 128 == 0``): the fast path —
|
| 92 |
+
bulk-load the ``REP = BLOCK//128`` row-blocks via the TMA ``descriptor`` (box
|
| 93 |
+
``[1, REP, rep_k, 2, 256]``), un-swizzle, feed ``dot_scaled``. Today's pruners cap every
|
| 94 |
+
swizzled tile at 128 (REP=1); the REP>1 form is written for a future >128-tile descriptor
|
| 95 |
+
read, not reachable from the current grids.
|
| 96 |
+
- Otherwise (sub-128 tile — fp8 ``scalar`` decode / small-M offline; or ``BK<128``): the block
|
| 97 |
+
layout can't be TMA-sliced, so pointer-GATHER exactly this tile's rows. The swizzle is a fixed
|
| 98 |
+
permutation: logical (row ``r``, K-group ``col``) → byte
|
| 99 |
+
``(blk*cols4 + col//4)*512 + (r%32)*16 + ((r%128)//32)*4 + col%4``. Reads only the needed
|
| 100 |
+
bytes — no 128-block over-read, no un-swizzle transpose — the row-major fast path's cost with
|
| 101 |
+
the swizzled layout, so ``scalar`` competes on merit instead of eating a TMA penalty."""
|
| 102 |
+
# Per-expert 128-row-block count is a CEIL: a non-128-multiple ``rows`` (e.g. N=2880) still
|
| 103 |
+
# occupies ceil(rows/128) blocks in the buffer (the swizzle builder pads the partial last block;
|
| 104 |
+
# its tail rows read zero-weight via the TMA OOB clamp, so they don't contribute). ``cdiv == floor``
|
| 105 |
+
# when ``rows`` is 128-aligned, so this is inert for every aligned shape.
|
| 106 |
+
nrb = (rows + 127) // 128
|
| 107 |
+
if BLOCK % 128 == 0 and SCALE_COLS >= 4 and SCALE_COLS % 4 == 0:
|
| 108 |
+
REP: tl.constexpr = BLOCK // 128
|
| 109 |
+
# absolute 128-block base = group_id*nrb + pid*REP; load_swizzled_scale multiplies blk by REP.
|
| 110 |
+
# Non-128 ``rows`` (odd ``nrb``) pins REP=1 (BN=128) in the pruner, so group_id*nrb//REP is exact.
|
| 111 |
+
blk = (group_id * nrb // REP + pid).to(tl.int32)
|
| 112 |
+
return load_swizzled_scale(descriptor, blk, k_idx, REP, SCALE_COLS // 4, BLOCK, SCALE_COLS)
|
| 113 |
+
cols4 = (K // SCALE_GROUP_K + 3) // 4 # cdiv: the buffer pads cols to whole 4-group chunks
|
| 114 |
+
r = pid * BLOCK + tl.arange(0, BLOCK)
|
| 115 |
+
lin = r // 128
|
| 116 |
+
blk = group_id * nrb + lin
|
| 117 |
+
row = r % 128
|
| 118 |
+
col = k_idx * SCALE_COLS + tl.arange(0, SCALE_COLS)
|
| 119 |
+
off = (
|
| 120 |
+
(blk[:, None] * cols4 + col[None, :] // 4) * 512
|
| 121 |
+
+ (row[:, None] % 32) * 16
|
| 122 |
+
+ (row[:, None] // 32) * 4
|
| 123 |
+
+ col[None, :] % 4
|
| 124 |
+
)
|
| 125 |
+
return tl.load(ptr + off)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@triton.jit
|
| 129 |
+
def load_weight_scale_tile(
|
| 130 |
+
SWIZZLED_SCALES: tl.constexpr,
|
| 131 |
+
bs_descriptor,
|
| 132 |
+
bs_ptr,
|
| 133 |
+
expert_id,
|
| 134 |
+
pid_n,
|
| 135 |
+
k_idx,
|
| 136 |
+
N,
|
| 137 |
+
K,
|
| 138 |
+
stride_bs_e,
|
| 139 |
+
stride_bs_n,
|
| 140 |
+
stride_bs_k,
|
| 141 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 142 |
+
SCALE_COLS: tl.constexpr,
|
| 143 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 144 |
+
GATE: tl.constexpr,
|
| 145 |
+
k_col_off=0,
|
| 146 |
+
):
|
| 147 |
+
"""One batched-decode weight-scale tile ``(n_width, SCALE_COLS)``, hiding the swizzled vs
|
| 148 |
+
un-swizzled choice behind the ``SWIZZLED_SCALES`` flag — the kernel loop reads scales the same
|
| 149 |
+
way either layout. ``bs_ptr`` is the un-advanced buffer base; the per-expert offset is applied
|
| 150 |
+
here (the swizzled path indexes by 128-row block, the un-swizzled by the row-major stride, so
|
| 151 |
+
it can't be pre-advanced uniformly). Under ``GATE`` the gate|up sub-tiles stack into ``2*BN``.
|
| 152 |
+
|
| 153 |
+
- ``SWIZZLED_SCALES``: SWIZZLE_32_4_4 via ``load_swizzled_scale_tile`` (descriptor bulk at BN=128, or
|
| 154 |
+
pointer gather below), scales swizzled over the full ``2N`` rows/expert under GATE.
|
| 155 |
+
- else: affine per-group load off ``(expert, N-tile row, K-group)`` — no in-op swizzle, so an
|
| 156 |
+
un-swizzled caller pays nothing. ``k_col_off`` shifts that read off the tile grid (the
|
| 157 |
+
caller's K-tile was clamped back into bounds for a BK that doesn't divide K); it is 0
|
| 158 |
+
for every aligned caller, and the swizzled layouts don't take it."""
|
| 159 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 160 |
+
if SWIZZLED_SCALES:
|
| 161 |
+
# gate|up needs no arm: the scale grid is row-interleaved with the weight, so the tile's
|
| 162 |
+
# gate and up rows are the contiguous n_width span at the same pid_n, i.e. the plain read
|
| 163 |
+
# over the doubled extent.
|
| 164 |
+
b_s = load_swizzled_scale_tile(
|
| 165 |
+
bs_descriptor, bs_ptr, expert_id, pid_n, k_idx,
|
| 166 |
+
2 * N if GATE else N, K,
|
| 167 |
+
n_width, SCALE_COLS, SCALE_GROUP_K,
|
| 168 |
+
)
|
| 169 |
+
else:
|
| 170 |
+
# affine per-group load off (expert, N-tile row, K-group) from the un-advanced base.
|
| 171 |
+
# Clamp the row in-bounds instead of masking (a masked load's other=0.0 blocks WS
|
| 172 |
+
# lowering): a partial last N-tile reads a clamped scale the epilogue masks off
|
| 173 |
+
# (N_COLS) — without the clamp the GATE arm's up rows (N + offs_bn) run past 2N.
|
| 174 |
+
base = bs_ptr + expert_id * stride_bs_e
|
| 175 |
+
offs_bn = pid_n * n_width + tl.arange(0, n_width)
|
| 176 |
+
offs_bn = tl.minimum(offs_bn, (2 * N if GATE else N) - 1)
|
| 177 |
+
offs_sf = k_idx * SCALE_COLS + k_col_off + tl.arange(0, SCALE_COLS)
|
| 178 |
+
ptrs = base + offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k
|
| 179 |
+
b_s = tl.load(ptrs)
|
| 180 |
+
return b_s
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@triton.jit
|
| 184 |
+
def load_mx_act_tile(
|
| 185 |
+
a_ptrs,
|
| 186 |
+
as_ptrs,
|
| 187 |
+
as_global, # fp32 NVFP4 act global (None off nvfp4); normalizes the raw tile pre-block-quant
|
| 188 |
+
as_global_row, # element offset into ``as_global`` (the expert's entry; 0 for a shared scalar)
|
| 189 |
+
row_mask,
|
| 190 |
+
a_descriptor,
|
| 191 |
+
m_start,
|
| 192 |
+
ka_off,
|
| 193 |
+
gather_rows,
|
| 194 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 195 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 196 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 197 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 198 |
+
A_GATHER: tl.constexpr = False,
|
| 199 |
+
FORMAT: tl.constexpr = "mxfp8",
|
| 200 |
+
):
|
| 201 |
+
"""Load one MX activation K-tile as ``(a_vals, a_scale)`` — the arm is picked
|
| 202 |
+
off the pointer dtype at compile time: fp8 pointers load pre-quantized E4M3 values +
|
| 203 |
+
UE8M0 scales (``maybe_act_quant``'s offline arm), uint8 pointers load caller-provided
|
| 204 |
+
packed-E2M1 values (W4A4 — the ``a_ptrs`` tile spans ``BLOCK_SIZE_K // 2`` bytes) +
|
| 205 |
+
the same UE8M0 scales, raw bf16/fp16 pointers load and quantize inline onto
|
| 206 |
+
``FORMAT``'s grid (``mx_act_quant_inline`` — packed E2M1 under the fp4 formats;
|
| 207 |
+
``as_ptrs`` then points at a dead placeholder and is never read). Under NVFP4
|
| 208 |
+
two-level, ``as_global[as_global_row]`` (the calibrated activation global — per tensor
|
| 209 |
+
or this expert's) normalizes the raw tile before the block quant — bit-identical to the
|
| 210 |
+
offline ``nvfp4_act_quant(x, global_scale=g_a)`` pass. ``row_mask`` may be ``None``
|
| 211 |
+
(unmasked tiles, e.g. the %-wrapped 2D matmul). Callers advance both pointers
|
| 212 |
+
unconditionally."""
|
| 213 |
+
if a_ptrs.dtype.element_ty == tl.float8e4nv or a_ptrs.dtype.element_ty == tl.uint8:
|
| 214 |
+
# pre-quantized (E4M3 offline, or packed E2M1 handed in by the caller); under the
|
| 215 |
+
# descriptor arm the [BM, BK_bytes] box loads the tile's contiguous sorted rows
|
| 216 |
+
# (no gather — tail rows past the tensor clamp to zero and are store-masked)
|
| 217 |
+
if A_MEMORY_MODE != "pointer":
|
| 218 |
+
if A_GATHER:
|
| 219 |
+
# sm_100 tma gather4: bulk-load the tile's ARBITRARY source rows
|
| 220 |
+
a = a_descriptor.gather(gather_rows, ka_off)
|
| 221 |
+
else:
|
| 222 |
+
a = a_descriptor.load([m_start, ka_off])
|
| 223 |
+
if as_ptrs.dtype.element_ty == tl.float8e4nv:
|
| 224 |
+
# NVFP4 scales: 0.0 encodes as byte 0 — padded rows scale to exact 0
|
| 225 |
+
a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0.0)
|
| 226 |
+
else:
|
| 227 |
+
# UE8M0 scales: byte 0 decodes to 2^-127 — padded rows can't make 0*inf
|
| 228 |
+
a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0)
|
| 229 |
+
elif row_mask is None:
|
| 230 |
+
a = tl.load(a_ptrs)
|
| 231 |
+
a_scale = tl.load(as_ptrs)
|
| 232 |
+
else:
|
| 233 |
+
a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
|
| 234 |
+
if as_ptrs.dtype.element_ty == tl.float8e4nv:
|
| 235 |
+
# NVFP4 scales: 0.0 encodes as byte 0 — padded rows scale to exact 0
|
| 236 |
+
a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0.0)
|
| 237 |
+
else:
|
| 238 |
+
# UE8M0 scales: byte 0 decodes to 2^-127 — padded rows can't make 0*inf
|
| 239 |
+
a_scale = tl.load(as_ptrs, mask=row_mask[:, None], other=0)
|
| 240 |
+
else: # raw bf16/fp16 — quantize inline
|
| 241 |
+
# the descriptor arm mirrors the pre-quantized branch above. Without it every
|
| 242 |
+
# A_MEMORY_MODE != "pointer" config fell through to `tl.load(a_ptrs)`, and under a
|
| 243 |
+
# descriptor arm `operand_tile_ptrs` hands back the bare base pointer -- so that load
|
| 244 |
+
# produced a SCALAR and mx_act_quant_inline's reshape died with "'dtype' object has no
|
| 245 |
+
# attribute 'numel'". Raw-A is exactly the inline-quant (nvfp4/mxfp4 2D) path, so it
|
| 246 |
+
# killed every descriptor config there: 321 dead compiles in one tune.
|
| 247 |
+
if A_MEMORY_MODE != "pointer":
|
| 248 |
+
if A_GATHER:
|
| 249 |
+
a_raw = a_descriptor.gather(gather_rows, ka_off).to(tl.float32)
|
| 250 |
+
else:
|
| 251 |
+
a_raw = a_descriptor.load([m_start, ka_off]).to(tl.float32)
|
| 252 |
+
elif row_mask is None:
|
| 253 |
+
a_raw = tl.load(a_ptrs).to(tl.float32)
|
| 254 |
+
else:
|
| 255 |
+
a_raw = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0).to(tl.float32)
|
| 256 |
+
if as_global is not None: # NVFP4 two-level: normalize by the calibrated act global
|
| 257 |
+
a_raw = a_raw / tl.load(as_global + as_global_row).to(tl.float32)
|
| 258 |
+
a, a_scale = mx_act_quant_inline(
|
| 259 |
+
a_raw, BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, FORMAT
|
| 260 |
+
)
|
| 261 |
+
return a, a_scale
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
@triton.jit
|
| 265 |
+
def apply_global_scale(acc, GlobalScale, stride_gs_e, GlobalScaleA, stride_gsa_e, expert_id):
|
| 266 |
+
"""Fold the NVFP4 second-level globals onto the fp32 accumulator: the weight global ``g_b``
|
| 267 |
+
and the activation global ``g_a``, each per tensor (stride 0, broadcast) or per expert.
|
| 268 |
+
``None`` folds the arm out."""
|
| 269 |
+
if GlobalScale is not None:
|
| 270 |
+
acc = acc * tl.load(GlobalScale + expert_id * stride_gs_e).to(tl.float32)
|
| 271 |
+
if GlobalScaleA is not None:
|
| 272 |
+
acc = acc * tl.load(GlobalScaleA + expert_id * stride_gsa_e).to(tl.float32)
|
| 273 |
+
return acc
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
@triton.jit
|
| 277 |
+
def decode_group_scale(scale):
|
| 278 |
+
"""Decode a group scale to fp32 by its dtype: ``uint8`` = UE8M0 exponent bits
|
| 279 |
+
(``value = 2^(exp - 127)``, built directly as the fp32 bit pattern), E4M3 = NVFP4's
|
| 280 |
+
direct fp8 value, fp32 (block-dynamic with float scales) passes through. The dtype
|
| 281 |
+
branch is a compile-time constant, so only the taken path is emitted (single return —
|
| 282 |
+
Triton requires all ``return`` statements to share a type)."""
|
| 283 |
+
if scale.dtype == tl.uint8:
|
| 284 |
+
scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
|
| 285 |
+
elif scale.dtype == tl.float8e4nv:
|
| 286 |
+
scale = scale.to(tl.float32)
|
| 287 |
+
return scale
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
@triton.jit
|
| 291 |
+
def mx_2d_scale_ptrs(
|
| 292 |
+
As,
|
| 293 |
+
Bs,
|
| 294 |
+
pid_m,
|
| 295 |
+
pid_n,
|
| 296 |
+
M,
|
| 297 |
+
N,
|
| 298 |
+
stride_as_m,
|
| 299 |
+
stride_bs_n,
|
| 300 |
+
stride_bs_k,
|
| 301 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 302 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 303 |
+
SCALE_COLS: tl.constexpr,
|
| 304 |
+
SWIZZLED_SCALES: tl.constexpr,
|
| 305 |
+
GATE: tl.constexpr = False,
|
| 306 |
+
):
|
| 307 |
+
"""Prologue 2D MX scale-pointer tiles + bounds masks as ``(as_ptrs, bs_ptrs, as_mask,
|
| 308 |
+
bs_mask)``. Affine arm: per-(row, group) ``as``/``bs`` pointer tiles read off AFFINE
|
| 309 |
+
row/col offsets (the %-wrapped operand offsets would make the scale load a non-affine
|
| 310 |
+
gather) with row/col bounds masks. Swizzled arm: the scales are read via the SA/BS
|
| 311 |
+
descriptors in the loop, so these tiles are dead — return the base scalars + null masks.
|
| 312 |
+
Single return — the arms have divergent types (base scalars + null masks vs affine pointer
|
| 313 |
+
tiles + bounds masks), so an early return can't unify them; the constexpr selects one."""
|
| 314 |
+
if SWIZZLED_SCALES:
|
| 315 |
+
as_ptrs, bs_ptrs, as_mask, bs_mask = As, Bs, None, None
|
| 316 |
+
else:
|
| 317 |
+
offs_am_lin = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 318 |
+
ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 319 |
+
offs_bn_lin = pid_n * ROWS + tl.arange(0, ROWS)
|
| 320 |
+
offs_sf = tl.arange(0, SCALE_COLS)
|
| 321 |
+
as_ptrs = As + offs_am_lin[:, None] * stride_as_m + offs_sf[None, :]
|
| 322 |
+
bs_ptrs = Bs + (offs_bn_lin[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k)
|
| 323 |
+
as_mask = offs_am_lin < M
|
| 324 |
+
bs_mask = offs_bn_lin < (2 * N if GATE else N)
|
| 325 |
+
return as_ptrs, bs_ptrs, as_mask, bs_mask
|
build/torch-xpu/loading/tiles.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import triton
|
| 16 |
+
import triton.language as tl
|
| 17 |
+
|
| 18 |
+
from .scales import load_block_fp8_act_tile, load_mx_act_tile, load_swizzled_scale, load_swizzled_scale_tile, load_weight_scale_tile
|
| 19 |
+
|
| 20 |
+
# A-sub-tile byte budget for the 1-byte-activation grouped-swizzle depth cap (see
|
| 21 |
+
# swizzle_offsets): the co-scheduled rows' A tile per K-step (depth*BM*BK bytes) must stay
|
| 22 |
+
# L2-hot to reuse, and past this it thrashes and the win collapses. ~512KB is a wave
|
| 23 |
+
# reuse-window, NOT gross L2 (0.4% of the B200's 132MB) — a B200 measurement (2026-07-16), so
|
| 24 |
+
# RE-MEASURE per device. Packed fp4 bypasses this (full grouping wins; see swizzle_offsets).
|
| 25 |
+
SWIZZLE_GROUP_A_BYTES = tl.constexpr(524288)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@triton.jit
|
| 29 |
+
def swizzle_offsets(
|
| 30 |
+
M,
|
| 31 |
+
N,
|
| 32 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 33 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 34 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 35 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
|
| 36 |
+
GATE: tl.constexpr = False,
|
| 37 |
+
):
|
| 38 |
+
"""2D-grid tile scheduling shared by the kernels below: grouped-swizzle the
|
| 39 |
+
``(pid_m, pid_n)`` program ids for L2 locality on B, then build the operand offset
|
| 40 |
+
vectors. Returns ``(pid_m, pid_n, offs_am, offs_bn, offs_k)`` — the swizzled ids
|
| 41 |
+
(reused by the output store) and the ``%``-wrapped row/col offsets plus the K range.
|
| 42 |
+
|
| 43 |
+
``offs_bn`` is the WEIGHT-side row range: under ``GATE`` the doubled span
|
| 44 |
+
``[2*pid_n*BN, 2*(pid_n+1)*BN)`` wrapped at ``2N``. The grid is sized by the OUTPUT tiles.
|
| 45 |
+
|
| 46 |
+
The swizzle keeps the B (weight) column-tile L2-hot while the co-scheduled rows reuse it,
|
| 47 |
+
so the depth cap is set by the WEIGHT footprint, capped at ``min(num_pid_m, .)``. With
|
| 48 |
+
1-byte weights the reuse thrashes past ~512KB (a hard cliff): the growing rival is the
|
| 49 |
+
rows' A sub-tile per K-step (``depth * BM * BK`` bytes), so ``SWIZZLE_GROUP_A_BYTES //
|
| 50 |
+
(BM * BK)`` — MEASURED on B200 (2026-07-16), bd BK128->32 (cliffs at 64), MX BK256->16,
|
| 51 |
+
BM64->64 (g*BM*BK ~512KB across BM and BK; BN-independent). Packed-fp4 weights
|
| 52 |
+
(``WEIGHT_VALUES_PER_BYTE==2``) halve that hot set, so it never thrashes and full grouping
|
| 53 |
+
wins outright (monotone, no cliff — measured for both W4A4 and W4A8, i.e. weight-driven not
|
| 54 |
+
activation-driven). Uses an EXPLICIT grouped swizzle (linearize the 2D program ids, then
|
| 55 |
+
group), NOT ``tl.swizzle2d`` (which degrades with group depth, -3pp+ MFU at depth 32). Same
|
| 56 |
+
grid launch, same result set — only the program-id -> tile mapping changes."""
|
| 57 |
+
num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
|
| 58 |
+
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
|
| 59 |
+
pid = tl.program_id(axis=1) * num_pid_m + tl.program_id(axis=0)
|
| 60 |
+
if WEIGHT_VALUES_PER_BYTE == 2:
|
| 61 |
+
max_group = num_pid_m
|
| 62 |
+
else:
|
| 63 |
+
max_group = SWIZZLE_GROUP_A_BYTES // (BLOCK_SIZE_M * BLOCK_SIZE_K)
|
| 64 |
+
num_pid_in_group = max_group * num_pid_n
|
| 65 |
+
group_id = pid // num_pid_in_group
|
| 66 |
+
first_pid_m = group_id * max_group
|
| 67 |
+
group_size_m = min(num_pid_m - first_pid_m, max_group)
|
| 68 |
+
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
|
| 69 |
+
pid_n = (pid % num_pid_in_group) // group_size_m
|
| 70 |
+
WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 71 |
+
offs_am = (pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)) % M
|
| 72 |
+
offs_bn = (pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)) % (2 * N if GATE else N)
|
| 73 |
+
offs_k = tl.arange(0, BLOCK_SIZE_K)
|
| 74 |
+
return pid_m, pid_n, offs_am, offs_bn, offs_k
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@triton.jit
|
| 78 |
+
def operand_tile_descriptor(
|
| 79 |
+
HostDescriptor,
|
| 80 |
+
W,
|
| 81 |
+
N,
|
| 82 |
+
K,
|
| 83 |
+
stride_n,
|
| 84 |
+
stride_k,
|
| 85 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 86 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 87 |
+
B_MEMORY_MODE: tl.constexpr,
|
| 88 |
+
GATE: tl.constexpr = False,
|
| 89 |
+
):
|
| 90 |
+
"""Resolve one operand's tile descriptor once per program (weight OR activation — the bd 2D
|
| 91 |
+
kernel calls it for both): the host-built TMA descriptor as passed, a device-built in-kernel
|
| 92 |
+
tensormap, or 0 under "pointer" (never read — the constexpr branch folds it out of
|
| 93 |
+
``load_weight_tile``). Single return — only the taken branch compiles."""
|
| 94 |
+
if B_MEMORY_MODE == "host_descriptor":
|
| 95 |
+
descriptor = HostDescriptor
|
| 96 |
+
elif B_MEMORY_MODE == "device_descriptor":
|
| 97 |
+
# under GATE the operand is the (2N, K) gate|up weight and a tile is a contiguous 2*BN
|
| 98 |
+
# row span of it, so both the tensor extent and the box double
|
| 99 |
+
descriptor = tl.make_tensor_descriptor(
|
| 100 |
+
W,
|
| 101 |
+
shape=((2 * N) if GATE else N, K),
|
| 102 |
+
strides=(stride_n, stride_k),
|
| 103 |
+
block_shape=((2 * BLOCK_SIZE_N) if GATE else BLOCK_SIZE_N, BLOCK_SIZE_K),
|
| 104 |
+
)
|
| 105 |
+
else: # pointer
|
| 106 |
+
descriptor = 0
|
| 107 |
+
return descriptor
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@triton.jit
|
| 111 |
+
def load_grouped_act_tile(
|
| 112 |
+
a_ptrs,
|
| 113 |
+
a_descriptor,
|
| 114 |
+
m_start,
|
| 115 |
+
ka_off,
|
| 116 |
+
row_mask,
|
| 117 |
+
gather_rows,
|
| 118 |
+
A_MEMORY_MODE: tl.constexpr,
|
| 119 |
+
A_GATHER: tl.constexpr = False,
|
| 120 |
+
):
|
| 121 |
+
"""A plain (scale-less) grouped activation K-tile: the masked pointer load, or —
|
| 122 |
+
under the descriptor arm — sm_100 tma gather4 over the tile's ARBITRARY source rows
|
| 123 |
+
(gathered launches; padded rows read row 0 and are store-masked) or the ``[BM, BK]``
|
| 124 |
+
box at the tile's contiguous sorted-row start (no-gather launches; tail rows past
|
| 125 |
+
the tensor clamp to zero). ``row_mask`` None loads the pointer tile maskless (the
|
| 126 |
+
%-wrapped 2D fast path). Single return — only the taken arm compiles."""
|
| 127 |
+
if A_MEMORY_MODE == "pointer":
|
| 128 |
+
if row_mask is None:
|
| 129 |
+
a = tl.load(a_ptrs)
|
| 130 |
+
else:
|
| 131 |
+
a = tl.load(a_ptrs, mask=row_mask[:, None], other=0.0)
|
| 132 |
+
elif A_GATHER:
|
| 133 |
+
a = a_descriptor.gather(gather_rows, ka_off)
|
| 134 |
+
else:
|
| 135 |
+
a = a_descriptor.load([m_start, ka_off])
|
| 136 |
+
return a
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
@triton.jit
|
| 140 |
+
def load_grouped_weight_tile(
|
| 141 |
+
w_ptrs,
|
| 142 |
+
w_descriptor,
|
| 143 |
+
row0,
|
| 144 |
+
n_off,
|
| 145 |
+
kb_off,
|
| 146 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 147 |
+
KB: tl.constexpr,
|
| 148 |
+
GATE: tl.constexpr,
|
| 149 |
+
B_MEMORY_MODE: tl.constexpr,
|
| 150 |
+
SWAP_AB: tl.constexpr = False,
|
| 151 |
+
):
|
| 152 |
+
"""One K-major (optionally gate|up-stacked) MX weight K-tile for the grouped / batched loop:
|
| 153 |
+
the explicit-pointer tile flattened to the ``[KB, (2|1)*BN]`` rhs (or, under ``SWAP_AB``, the
|
| 154 |
+
``[(2|1)*BN, KB]`` rows-major lhs — the batched-decode orientation), or the ``[(2|1), BN, KB]``
|
| 155 |
+
descriptor box over the ``(E, 2N|N, K_bytes)`` weight view, reshaped and transposed to the same
|
| 156 |
+
form (the fused-era TMA arm: natural orientation + per-iteration trans; grouped/2D never swap).
|
| 157 |
+
Single return — only the taken arm compiles; the caller advances ``w_ptrs`` and passes the box
|
| 158 |
+
offsets either way."""
|
| 159 |
+
if B_MEMORY_MODE == "pointer":
|
| 160 |
+
w = tl.load(w_ptrs)
|
| 161 |
+
else:
|
| 162 |
+
w = tl.trans(
|
| 163 |
+
tl.reshape(
|
| 164 |
+
w_descriptor.load([row0, n_off, kb_off]),
|
| 165 |
+
((2 if GATE else 1) * BLOCK_SIZE_N, KB),
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
return w
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@triton.jit
|
| 172 |
+
def load_weight_tile(
|
| 173 |
+
w_ptrs, w_descriptor, row_off, k_off, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr = False
|
| 174 |
+
):
|
| 175 |
+
"""One weight K-tile. Descriptor modes load the ``(BN, BK)`` box at ``(row_off, k_off)``
|
| 176 |
+
and, in the natural orientation, transpose it once to the ``(BK, BN)`` K-major rhs the
|
| 177 |
+
pointer arm builds directly (``SWAP_AB`` keeps the box as-is: the weight rows then sit in
|
| 178 |
+
the MMA M dim). Pointer mode loads the explicit tile in whatever orientation ``w_ptrs``
|
| 179 |
+
was built with. Single return — only the taken branch compiles."""
|
| 180 |
+
if B_MEMORY_MODE == "pointer":
|
| 181 |
+
w = tl.load(w_ptrs)
|
| 182 |
+
else:
|
| 183 |
+
w = w_descriptor.load([row_off, k_off])
|
| 184 |
+
if not SWAP_AB:
|
| 185 |
+
w = tl.trans(w)
|
| 186 |
+
return w
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@triton.jit
|
| 190 |
+
def matmul_weight_ptrs(
|
| 191 |
+
B,
|
| 192 |
+
offs_n,
|
| 193 |
+
offs_k,
|
| 194 |
+
stride_b_n,
|
| 195 |
+
stride_b_k,
|
| 196 |
+
B_MEMORY_MODE: tl.constexpr,
|
| 197 |
+
SWAP_AB: tl.constexpr = False,
|
| 198 |
+
):
|
| 199 |
+
"""Prologue weight-tile pointers via ``operand_tile_ptrs`` (which folds the
|
| 200 |
+
descriptor-vs-pointer arm); ``offs_n`` carries the gate|up span."""
|
| 201 |
+
return operand_tile_ptrs(B, offs_n, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
@triton.jit
|
| 205 |
+
def load_act_mx(
|
| 206 |
+
a_ptrs, as_ptrs, as_global, as_global_row, value_mask, scale_mask, a_descriptor, m_off, k_off,
|
| 207 |
+
as_descriptor, as_ptr, gather_rows, stride_as_m, pid_m, k, M, K,
|
| 208 |
+
A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr, GROUPED: tl.constexpr,
|
| 209 |
+
SWIZZLED_SCALES: tl.constexpr, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_K: tl.constexpr,
|
| 210 |
+
SCALE_GROUP_K: tl.constexpr, ACTIVATION_FORMAT: tl.constexpr,
|
| 211 |
+
):
|
| 212 |
+
"""MX activation tile + scale. Offline-quantized A under a swizzled weight -> pre-swizzled
|
| 213 |
+
SWIZZLE_32_4_4 scale (tcgen05 fast path); grouped -> gathered per-(row, K-group) affine scale off
|
| 214 |
+
the source-order ``As``; else (2D / decode raw A) -> ``load_mx_act_tile`` inline-quant / in-register.
|
| 215 |
+
Raw bf16 A stays in-register affine even under a swizzled weight (the dot_scaled mix is fine)."""
|
| 216 |
+
SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 217 |
+
A_OFFLINE: tl.constexpr = (
|
| 218 |
+
a_ptrs.dtype.element_ty == tl.float8e4nv or a_ptrs.dtype.element_ty == tl.uint8
|
| 219 |
+
)
|
| 220 |
+
if SWIZZLED_SCALES and A_OFFLINE: # pre-swizzled SWIZZLE_32_4_4 scale (tcgen05 fast path)
|
| 221 |
+
a = load_grouped_act_tile(
|
| 222 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
|
| 223 |
+
)
|
| 224 |
+
a_s = load_swizzled_scale_tile(
|
| 225 |
+
as_descriptor, as_ptr, 0, pid_m, k, M, K, BLOCK_SIZE_M, SCALE_COLS, SCALE_GROUP_K
|
| 226 |
+
)
|
| 227 |
+
elif GROUPED: # gathered per-(row, K-group) affine scale off the row-major source-order As
|
| 228 |
+
a = load_grouped_act_tile(
|
| 229 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
|
| 230 |
+
)
|
| 231 |
+
offs_sf = k * SCALE_COLS + tl.arange(0, SCALE_COLS)
|
| 232 |
+
a_s = tl.load(
|
| 233 |
+
as_ptrs + gather_rows[:, None] * stride_as_m + offs_sf[None, :],
|
| 234 |
+
mask=scale_mask[:, None],
|
| 235 |
+
other=0.0,
|
| 236 |
+
)
|
| 237 |
+
else: # 2D / decode: inline-quant affine or in-register scale
|
| 238 |
+
a, a_s = load_mx_act_tile(
|
| 239 |
+
a_ptrs, as_ptrs, as_global, as_global_row, scale_mask, a_descriptor, m_off, k_off, 0,
|
| 240 |
+
BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, A_MEMORY_MODE,
|
| 241 |
+
FORMAT=ACTIVATION_FORMAT,
|
| 242 |
+
)
|
| 243 |
+
return a, a_s
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
@triton.jit
|
| 247 |
+
def load_act_block_dynamic(
|
| 248 |
+
a_ptrs, as_ptrs, value_mask, scale_mask, a_descriptor, m_off, k_off, gather_rows, k,
|
| 249 |
+
A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr, GROUPED: tl.constexpr,
|
| 250 |
+
SWAP_AB: tl.constexpr,
|
| 251 |
+
):
|
| 252 |
+
"""block_dynamic activation tile + per-row per-K-block scale. Grouped: gathered value + the scale
|
| 253 |
+
read contiguously at ``As + k``; else ``load_block_fp8_act_tile`` (swap-aware, inline-quant 2D)."""
|
| 254 |
+
if GROUPED: # gathered value + per-row per-K-block scale read contiguously at As + k
|
| 255 |
+
a = load_grouped_act_tile(
|
| 256 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
|
| 257 |
+
)
|
| 258 |
+
a_s = tl.load(as_ptrs + k, mask=scale_mask, other=0.0)
|
| 259 |
+
else:
|
| 260 |
+
a, a_s = load_block_fp8_act_tile(
|
| 261 |
+
a_ptrs, as_ptrs, a_descriptor, m_off, k_off, A_MEMORY_MODE, scale_mask, SWAP_AB
|
| 262 |
+
)
|
| 263 |
+
return a, a_s
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
@triton.jit
|
| 267 |
+
def load_act_static(
|
| 268 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, a_s_static,
|
| 269 |
+
A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr,
|
| 270 |
+
):
|
| 271 |
+
"""static activation tile. Pre-quantized fp8 A loads as the rows-major MMA lhs; raw bf16/fp16
|
| 272 |
+
(inline arm, small M, pointer-only) is quantized against the scalar ``a_s_static``. ``a_s`` = the
|
| 273 |
+
values (the static scale is a scalar folded post-loop)."""
|
| 274 |
+
if a_ptrs.dtype.element_ty == tl.float8e4nv: # pre-quantized fp8 A (MMA lhs, rows-major)
|
| 275 |
+
a = load_grouped_act_tile(
|
| 276 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
|
| 277 |
+
)
|
| 278 |
+
else: # raw bf16/fp16 (inline arm, M<threshold, pointer-only) — quantize vs the static scale
|
| 279 |
+
a = (tl.load(a_ptrs).to(tl.float32) / a_s_static).to(tl.float8e4nv)
|
| 280 |
+
a_s = a
|
| 281 |
+
return a, a_s
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
@triton.jit
|
| 285 |
+
def load_act_plain(
|
| 286 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows,
|
| 287 |
+
A_MEMORY_MODE: tl.constexpr, A_GATHER: tl.constexpr,
|
| 288 |
+
):
|
| 289 |
+
"""tensor / full_precision activation: plain value tile, no scale (returns ``(a, a)`` — the
|
| 290 |
+
second slot is dead, kept for the uniform (value, scale) shape)."""
|
| 291 |
+
a = load_grouped_act_tile(
|
| 292 |
+
a_ptrs, a_descriptor, m_off, k_off, value_mask, gather_rows, A_MEMORY_MODE, A_GATHER
|
| 293 |
+
)
|
| 294 |
+
return a, a
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
@triton.jit
|
| 298 |
+
def _weight_scale_mx(
|
| 299 |
+
bs_ptrs, bs_mask, bs_descriptor, bs_ptr, blk_idx, expert_id, pid_n, k, N, K,
|
| 300 |
+
stride_bs_e, stride_bs_n, stride_bs_k,
|
| 301 |
+
GROUPED: tl.constexpr, GATE: tl.constexpr, PER_EXPERT: tl.constexpr,
|
| 302 |
+
SWIZZLED_SCALES: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
|
| 303 |
+
SCALE_COLS: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
|
| 304 |
+
k_col_off=0,
|
| 305 |
+
):
|
| 306 |
+
"""MX weight-scale K-tile ``(rows, SCALE_COLS)``. Grouped: pre-swizzled SWIZZLE_32_4_4 via the
|
| 307 |
+
descriptor (``load_swizzled_scale`` — gate|up is one 2*BN tile off the block-interleaved buffer,
|
| 308 |
+
GATE folding into REP/width), else an inlined affine per-group read off the un-swizzled 3D ``Bs``
|
| 309 |
+
(inlined, not ``load_weight_scale_tile``: the nested leaf breaks warp-specialization partitioning
|
| 310 |
+
in the grouped loop). 2D-GATE / batched decode: the per-(expert, N, K) 3D leaf. 2D dense: the
|
| 311 |
+
descriptor bulk-load (BN=128) else the bounds-masked affine pointer."""
|
| 312 |
+
if GROUPED:
|
| 313 |
+
if SWIZZLED_SCALES:
|
| 314 |
+
NW: tl.constexpr = (2 if GATE else 1) * BLOCK_SIZE_N
|
| 315 |
+
# REP counts the tile's whole 128-row blocks, so it derives from the DOUBLED extent:
|
| 316 |
+
# BN=64 under GATE is one 128-row block, not 2 * (64 // 128) == 0
|
| 317 |
+
NREP: tl.constexpr = max(1, NW // 128)
|
| 318 |
+
b_s = load_swizzled_scale(
|
| 319 |
+
bs_descriptor, blk_idx, k, NREP, SCALE_COLS // 4, NW, SCALE_COLS
|
| 320 |
+
)
|
| 321 |
+
else: # affine per-group read off the un-swizzled 3D Bs (num_experts, n_rows, K//g)
|
| 322 |
+
NW: tl.constexpr = (2 if GATE else 1) * BLOCK_SIZE_N
|
| 323 |
+
base = bs_ptrs + expert_id * stride_bs_e
|
| 324 |
+
# clamp rather than mask (a masked load's other=0.0 blocks WS lowering): a partial
|
| 325 |
+
# last N-tile reads a clamped scale the epilogue masks off via N_COLS
|
| 326 |
+
offs_bn = tl.minimum(
|
| 327 |
+
pid_n * NW + tl.arange(0, NW), ((2 if GATE else 1) * N) - 1
|
| 328 |
+
)
|
| 329 |
+
offs_sf = k * SCALE_COLS + k_col_off + tl.arange(0, SCALE_COLS)
|
| 330 |
+
b_s = tl.load(base + offs_bn[:, None] * stride_bs_n + offs_sf[None, :] * stride_bs_k)
|
| 331 |
+
elif GATE or PER_EXPERT: # 2D-GATE (expert 0) / batched decode: per-(expert, N, K) scale leaf
|
| 332 |
+
b_s = load_weight_scale_tile(
|
| 333 |
+
SWIZZLED_SCALES, bs_descriptor, bs_ptr, expert_id, pid_n, k, N, K,
|
| 334 |
+
stride_bs_e, stride_bs_n, stride_bs_k, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K, GATE,
|
| 335 |
+
k_col_off=k_col_off,
|
| 336 |
+
)
|
| 337 |
+
elif SWIZZLED_SCALES: # pre-swizzled SWIZZLE_32_4_4 scale — descriptor at BN=128, gather below
|
| 338 |
+
b_s = load_swizzled_scale_tile(
|
| 339 |
+
bs_descriptor, bs_ptr, 0, pid_n, k, N, K, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K,
|
| 340 |
+
)
|
| 341 |
+
elif bs_mask is None: # weight-only 2D: N-clamped in-bounds ptrs, UNMASKED (no other=0.0 constant
|
| 342 |
+
b_s = tl.load(bs_ptrs) # -> the K-loop warp-specialization-lowers; tail N masked in epilogue)
|
| 343 |
+
else:
|
| 344 |
+
b_s = tl.load(bs_ptrs, mask=bs_mask[:, None], other=0.0) # 0.0 casts to fp8/uint8
|
| 345 |
+
return b_s
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
@triton.jit
|
| 349 |
+
def _weight_scale_block_dynamic(
|
| 350 |
+
bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k,
|
| 351 |
+
GROUPED: tl.constexpr, GATE: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
|
| 352 |
+
):
|
| 353 |
+
"""block_dynamic per-K-block weight scale. Grouped reads it at ``bs_ptrs + k*stride`` (GATE folds
|
| 354 |
+
gate on the first ``BN`` columns, up on the rest via ``up_s_ptr``); batched reads the pre-offset
|
| 355 |
+
maskless pointer; 2D reads the bounds-masked advanced pointer."""
|
| 356 |
+
if GROUPED:
|
| 357 |
+
if GATE: # gate scale on the first BN columns, up on the rest
|
| 358 |
+
b_s = tl.where(
|
| 359 |
+
tl.arange(0, 2 * BLOCK_SIZE_N) < BLOCK_SIZE_N,
|
| 360 |
+
tl.load(bs_ptrs + k * stride_bs_k),
|
| 361 |
+
tl.load(up_s_ptr + k * stride_bs_k),
|
| 362 |
+
)
|
| 363 |
+
else:
|
| 364 |
+
# One 128-block scale covers the whole BN tile, so the pointer is scalar and the load
|
| 365 |
+
# is rank-0. Broadcast to the tile's N extent so BOTH grouped arms hand back a rank-1
|
| 366 |
+
# [.., N] scale: `block_dynamic_dot`'s dot_scaled arm indexes `b_s.shape[0]`, which a
|
| 367 |
+
# rank-0 value cannot answer, and that killed every BLOCK_SIZE_M>=128 config (the only
|
| 368 |
+
# ones that take the tcgen05 arm) for the ungated grouped GEMM -- the MoE down proj.
|
| 369 |
+
# broadcast_to keeps the dtype, so decode_group_scale still sees UE8M0 as uint8.
|
| 370 |
+
b_s = tl.broadcast_to(tl.load(bs_ptrs + k * stride_bs_k)[None], [BLOCK_SIZE_N])
|
| 371 |
+
elif bs_mask is None: # batched: bs_ptrs pre-offset (gate/up folded), maskless decode tile
|
| 372 |
+
b_s = tl.load(bs_ptrs)
|
| 373 |
+
else:
|
| 374 |
+
b_s = tl.load(bs_ptrs, mask=bs_mask, other=0.0)
|
| 375 |
+
return b_s
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
@triton.jit
|
| 379 |
+
def _weight_scale_static(
|
| 380 |
+
bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k,
|
| 381 |
+
GROUPED: tl.constexpr, GATE: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,
|
| 382 |
+
):
|
| 383 |
+
"""static weight scale — as block_dynamic, but the grouped non-GATE scale broadcasts to a per-N
|
| 384 |
+
vector so ``accumulate("static")`` applies it in N (the static act scale is a scalar folded
|
| 385 |
+
post-loop)."""
|
| 386 |
+
if GROUPED:
|
| 387 |
+
if GATE: # gate on the first BN, up on the rest
|
| 388 |
+
b_s = tl.where(
|
| 389 |
+
tl.arange(0, 2 * BLOCK_SIZE_N) < BLOCK_SIZE_N,
|
| 390 |
+
tl.load(bs_ptrs + k * stride_bs_k),
|
| 391 |
+
tl.load(up_s_ptr + k * stride_bs_k),
|
| 392 |
+
)
|
| 393 |
+
else:
|
| 394 |
+
b_s = tl.load(bs_ptrs + k * stride_bs_k) + tl.zeros(
|
| 395 |
+
(BLOCK_SIZE_N,), bs_ptrs.dtype.element_ty
|
| 396 |
+
)
|
| 397 |
+
elif bs_mask is None: # batched: bs_ptrs pre-offset (gate/up folded), maskless decode tile
|
| 398 |
+
b_s = tl.load(bs_ptrs)
|
| 399 |
+
else:
|
| 400 |
+
b_s = tl.load(bs_ptrs, mask=bs_mask, other=0.0) # affine col index -> mask OOB last tile
|
| 401 |
+
return b_s
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
@triton.jit
|
| 405 |
+
def _weight_value(
|
| 406 |
+
b_ptrs, b_descriptor, row0, n_off, k_off,
|
| 407 |
+
GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr,
|
| 408 |
+
SWAP_AB: tl.constexpr, BLOCK_SIZE_N: tl.constexpr, KB: tl.constexpr,
|
| 409 |
+
):
|
| 410 |
+
"""Format-agnostic weight value tile: the per-expert 3D box (GROUPED) or the plain swap-aware
|
| 411 |
+
tile. Shared by every ``load_weight_<format>`` — the value load never depends on the scale
|
| 412 |
+
format. A 2D gate|up weight takes the PLAIN arm: its rows are interleaved, so the tile is a
|
| 413 |
+
contiguous ``2*BN`` span of a 2D ``(2N, K)`` tensor and needs no 3D box — routing it through
|
| 414 |
+
the grouped loader issued a 3-offset load against a 2D descriptor ("expected 2 offsets, but
|
| 415 |
+
got 3"), unreachable only while gate was fenced to pointer mode. Single-exit if/else (an early
|
| 416 |
+
return would type-check the untaken arm)."""
|
| 417 |
+
if GROUPED:
|
| 418 |
+
w = load_grouped_weight_tile(
|
| 419 |
+
b_ptrs, b_descriptor, row0, n_off, k_off, BLOCK_SIZE_N, KB, GATE, B_MEMORY_MODE, SWAP_AB
|
| 420 |
+
)
|
| 421 |
+
else:
|
| 422 |
+
w = load_weight_tile(b_ptrs, b_descriptor, n_off, k_off, B_MEMORY_MODE, SWAP_AB)
|
| 423 |
+
return w
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
@triton.jit
|
| 427 |
+
def load_weight_mx(
|
| 428 |
+
b_ptrs, b_descriptor, bs_ptrs, bs_mask, bs_descriptor, bs_ptr, row0, n_off, k_off,
|
| 429 |
+
blk_idx, expert_id, pid_n, k, N, K, stride_bs_e, stride_bs_n, stride_bs_k,
|
| 430 |
+
GATE: tl.constexpr, GROUPED: tl.constexpr, PER_EXPERT: tl.constexpr,
|
| 431 |
+
B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr, SWIZZLED_SCALES: tl.constexpr,
|
| 432 |
+
BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
|
| 433 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 434 |
+
k_col_off=0,
|
| 435 |
+
):
|
| 436 |
+
"""The MX weight path: value tile + pre-swizzled/affine group scale. ``k_col_off`` shifts the
|
| 437 |
+
affine scale read for a K-tile the caller clamped back in bounds (0 for aligned callers)."""
|
| 438 |
+
KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
|
| 439 |
+
SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 440 |
+
w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
|
| 441 |
+
w_s = _weight_scale_mx(
|
| 442 |
+
bs_ptrs, bs_mask, bs_descriptor, bs_ptr, blk_idx, expert_id, pid_n, k, N, K,
|
| 443 |
+
stride_bs_e, stride_bs_n, stride_bs_k,
|
| 444 |
+
GROUPED, GATE, PER_EXPERT, SWIZZLED_SCALES, BLOCK_SIZE_N, SCALE_COLS, SCALE_GROUP_K, k_col_off=k_col_off,
|
| 445 |
+
)
|
| 446 |
+
return w, w_s
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
@triton.jit
|
| 450 |
+
def load_weight_block_dynamic(
|
| 451 |
+
b_ptrs, b_descriptor, bs_ptrs, bs_mask, up_s_ptr, row0, n_off, k_off, k, stride_bs_k,
|
| 452 |
+
GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
|
| 453 |
+
BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
|
| 454 |
+
):
|
| 455 |
+
"""The block_dynamic weight path: value tile + per-K-block scale (gate on the first BN, up on the
|
| 456 |
+
rest via ``up_s_ptr``)."""
|
| 457 |
+
KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
|
| 458 |
+
w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
|
| 459 |
+
w_s = _weight_scale_block_dynamic(bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k, GROUPED, GATE, BLOCK_SIZE_N)
|
| 460 |
+
return w, w_s
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
@triton.jit
|
| 464 |
+
def load_weight_static(
|
| 465 |
+
b_ptrs, b_descriptor, bs_ptrs, bs_mask, up_s_ptr, row0, n_off, k_off, k, stride_bs_k,
|
| 466 |
+
GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
|
| 467 |
+
BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
|
| 468 |
+
):
|
| 469 |
+
"""The static weight path: value tile + per-K-block scale broadcast to a per-N vector (the static
|
| 470 |
+
act scale is a scalar folded post-loop)."""
|
| 471 |
+
KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
|
| 472 |
+
w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
|
| 473 |
+
w_s = _weight_scale_static(bs_ptrs, up_s_ptr, bs_mask, k, stride_bs_k, GROUPED, GATE, BLOCK_SIZE_N)
|
| 474 |
+
return w, w_s
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
@triton.jit
|
| 478 |
+
def load_weight_plain(
|
| 479 |
+
b_ptrs, b_descriptor, row0, n_off, k_off,
|
| 480 |
+
GATE: tl.constexpr, GROUPED: tl.constexpr, B_MEMORY_MODE: tl.constexpr, SWAP_AB: tl.constexpr,
|
| 481 |
+
BLOCK_SIZE_N: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, WEIGHT_VALUES_PER_BYTE: tl.constexpr = 1,
|
| 482 |
+
):
|
| 483 |
+
"""The tensor / full_precision weight path: plain value tile, no block scale (the per-tensor
|
| 484 |
+
scale, if any, is applied post-loop). Returns ``(w, w)`` so callers keep the uniform (value,
|
| 485 |
+
scale) shape; the second slot is dead."""
|
| 486 |
+
KB: tl.constexpr = BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE
|
| 487 |
+
w = _weight_value(b_ptrs, b_descriptor, row0, n_off, k_off, GATE, GROUPED, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, KB)
|
| 488 |
+
return w, w
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
@triton.jit
|
| 492 |
+
def advance_ptrs(
|
| 493 |
+
a_ptrs,
|
| 494 |
+
as_ptrs,
|
| 495 |
+
w_ptrs,
|
| 496 |
+
ws_ptrs,
|
| 497 |
+
w_up_ptrs,
|
| 498 |
+
ws_up_ptrs,
|
| 499 |
+
a_step,
|
| 500 |
+
as_step,
|
| 501 |
+
w_step,
|
| 502 |
+
ws_step,
|
| 503 |
+
A_MEMORY_MODE: tl.constexpr,
|
| 504 |
+
W_MEMORY_MODE: tl.constexpr,
|
| 505 |
+
ADVANCE_AS: tl.constexpr,
|
| 506 |
+
ADVANCE_WS: tl.constexpr,
|
| 507 |
+
GATE_STREAMS: tl.constexpr = False,
|
| 508 |
+
):
|
| 509 |
+
"""Advance the shared GEMM operand pointers one K-step, folding the memory-mode / scale-layout /
|
| 510 |
+
gate|up-stream conditionals out of every loop. The operand set is uniform across the kernels:
|
| 511 |
+
activation (``a_ptrs`` + affine scale ``as_ptrs``) and weight (``w_ptrs`` + affine scale
|
| 512 |
+
``ws_ptrs``), the weight either a single stream or — under ``GATE_STREAMS`` — the gate|up pair
|
| 513 |
+
(``w_up_ptrs`` + ``ws_up_ptrs``, bumped by the same steps). Value pointers advance only on the
|
| 514 |
+
pointer arm (a descriptor arm re-derives the box K offset from ``k``); scale pointers advance
|
| 515 |
+
only when read affine (``ADVANCE_AS`` / ``ADVANCE_WS`` — swizzled / in-leaf / per-tensor scales
|
| 516 |
+
don't). Pass a dead pointer + step 0 (flag off) for any stream a kernel doesn't carry. Returns
|
| 517 |
+
the six pointers in argument order."""
|
| 518 |
+
if A_MEMORY_MODE == "pointer":
|
| 519 |
+
a_ptrs += a_step
|
| 520 |
+
if W_MEMORY_MODE == "pointer":
|
| 521 |
+
w_ptrs += w_step
|
| 522 |
+
if GATE_STREAMS:
|
| 523 |
+
w_up_ptrs += w_step
|
| 524 |
+
if ADVANCE_AS:
|
| 525 |
+
as_ptrs += as_step
|
| 526 |
+
if ADVANCE_WS:
|
| 527 |
+
ws_ptrs += ws_step
|
| 528 |
+
if GATE_STREAMS:
|
| 529 |
+
ws_up_ptrs += ws_step
|
| 530 |
+
return a_ptrs, as_ptrs, w_ptrs, ws_ptrs, w_up_ptrs, ws_up_ptrs
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
@triton.jit
|
| 534 |
+
def weight_tile_ptrs(
|
| 535 |
+
base,
|
| 536 |
+
offs_n,
|
| 537 |
+
offs_k,
|
| 538 |
+
stride_n,
|
| 539 |
+
stride_k,
|
| 540 |
+
SWAP_AB: tl.constexpr,
|
| 541 |
+
):
|
| 542 |
+
"""Weight-tile pointers oriented by ``SWAP_AB``. A gate|up tile is a ``2*BN`` row span
|
| 543 |
+
(``offs_n`` carries it); the epilogue splits the projections."""
|
| 544 |
+
return oriented_tile_ptrs(base, offs_n, offs_k, stride_n, stride_k, SWAP_AB)
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
@triton.jit
|
| 548 |
+
def oriented_tile_ptrs(
|
| 549 |
+
base, offs_rows, offs_k, stride_rows, stride_k, SWAP_AB: tl.constexpr
|
| 550 |
+
):
|
| 551 |
+
"""Operand-tile pointers oriented by whether these rows sit in the MMA M dim
|
| 552 |
+
(``SWAP_AB``, from the weight's viewpoint — activation callers pass the flag inverted):
|
| 553 |
+
rows-major ``[rows, K]`` when they do, else K-major ``[K, rows]``. Only the taken
|
| 554 |
+
constexpr branch compiles, so the divergent shapes never meet. The per-step K-advance
|
| 555 |
+
is identical for both layouts, so the caller advances the returned pointer the same
|
| 556 |
+
way regardless of orientation."""
|
| 557 |
+
if SWAP_AB:
|
| 558 |
+
ptrs = base + (offs_rows[:, None] * stride_rows + offs_k[None, :] * stride_k)
|
| 559 |
+
else:
|
| 560 |
+
ptrs = base + (offs_k[:, None] * stride_k + offs_rows[None, :] * stride_rows)
|
| 561 |
+
return ptrs
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
@triton.jit
|
| 565 |
+
def operand_tile_ptrs(
|
| 566 |
+
base,
|
| 567 |
+
offs_rows,
|
| 568 |
+
offs_k,
|
| 569 |
+
stride_rows,
|
| 570 |
+
stride_k,
|
| 571 |
+
MEMORY_MODE: tl.constexpr,
|
| 572 |
+
SWAP_AB: tl.constexpr,
|
| 573 |
+
):
|
| 574 |
+
"""Prologue operand-tile pointer, folding the per-operand memory-mode branch: the explicit
|
| 575 |
+
oriented ``[rows,K]``/``[K,rows]`` tile on the pointer arm, or ``base`` as a scalar
|
| 576 |
+
placeholder on a descriptor arm (which reads its box via the descriptor — building the index
|
| 577 |
+
tensor there would only stay live across the K-loop and spill registers). ``SWAP_AB`` is the
|
| 578 |
+
orientation from the weight's viewpoint (activation callers pass it inverted). Single
|
| 579 |
+
return — the arms have divergent types (a ``[rows,K]`` tile vs the scalar base), so an early
|
| 580 |
+
return can't unify them; the constexpr selects one."""
|
| 581 |
+
if MEMORY_MODE == "pointer":
|
| 582 |
+
ptrs = oriented_tile_ptrs(base, offs_rows, offs_k, stride_rows, stride_k, SWAP_AB)
|
| 583 |
+
else:
|
| 584 |
+
ptrs = base
|
| 585 |
+
return ptrs
|
build/torch-xpu/matmul.py
ADDED
|
@@ -0,0 +1,1970 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import triton
|
| 20 |
+
import triton.language as tl
|
| 21 |
+
from triton.tools.tensor_descriptor import TensorDescriptor
|
| 22 |
+
|
| 23 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 24 |
+
from .compat import add_op_namespace_prefix, FP8_DTYPE, is_sm10x, NIBBLES_PER_BYTE, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, get_accelerator_autotuning_configs, tl_dtype
|
| 25 |
+
from .descriptors import maybe_descriptor, rebind_bd_descriptors, rebind_mx_descriptors, rebind_weight_only_descriptors
|
| 26 |
+
from .formats import check_activation_format, global_scale_stride, normalize_global_scale, e2m1_as_uint8, is_mx, mx_scale_family, resolve_activation_format, resolve_output_dtype, ue8m0_as_uint8, validate_dense_2d_operands, weight_format
|
| 27 |
+
from .swizzle import swizzle_mx_scales, swizzled_scale_descriptor
|
| 28 |
+
from .quant import MX_ACT_QUANT, fp8_act_quant_block_dynamic, fp8_act_quant_tensor_wide, maybe_act_quant, mxfp8_act_quant, nvfp4_act_quant
|
| 29 |
+
from .loading.scales import apply_global_scale, mx_2d_scale_ptrs
|
| 30 |
+
from .mma import block_dynamic_dot, fp8_dot, mx_compute, mx_weight_only_compute, static_dot
|
| 31 |
+
from .loading.tiles import (
|
| 32 |
+
advance_ptrs,
|
| 33 |
+
load_act_block_dynamic,
|
| 34 |
+
load_act_mx,
|
| 35 |
+
load_act_plain,
|
| 36 |
+
load_act_static,
|
| 37 |
+
load_weight_block_dynamic,
|
| 38 |
+
load_weight_mx,
|
| 39 |
+
load_weight_plain,
|
| 40 |
+
load_weight_static,
|
| 41 |
+
matmul_weight_ptrs,
|
| 42 |
+
operand_tile_descriptor,
|
| 43 |
+
operand_tile_ptrs,
|
| 44 |
+
swizzle_offsets,
|
| 45 |
+
)
|
| 46 |
+
from .epilogue import acc_init, add_bias, bias_strides, gemm_epilogue, store_masked, store_masked_oriented
|
| 47 |
+
from .pruners import PATH_ANCHOR_AXES, fp8_dot_warp_pruner, gate_stacked_tmem_trap_pruner, block_dynamic_2d_warp_spec_pruner, block_dynamic_mma_width_pruner, block_within_dim_pruner, compose_pruners, descriptor_box_pruner, descriptor_needs_prequant_pruner, matched_memory_modes_pruner, mx_2d_swap_scope_pruner, mx_config_pruner, scalar_max_m_pruner, smem_pruner, swizzled_out_bm_pruner, swizzled_scale_config_pruner, warp_spec_compile_guard_pruner
|
| 48 |
+
|
| 49 |
+
# The 2D-grid kernels' L2-locality swizzle depth is derived per-tile inside
|
| 50 |
+
# ``swizzle_offsets`` (see SWIZZLE_GROUP_A_BYTES there) — no per-kernel constant here.
|
| 51 |
+
|
| 52 |
+
# maybe_act_quant crossovers (min rows for offline pre-quant). MX: MEASURED — B200
|
| 53 |
+
# 2D-matmul sweep, graph-timed, H=6144 MXFP8 / H=4096 MXFP4: inline wins only at M=1
|
| 54 |
+
# (33 vs 44us / 20 vs 30us), offline from M=16 for MXFP8 (22 vs 33us), 2-3x by M>=64
|
| 55 |
+
# (MXFP4's M=16 cell marginally favors inline — outweighed); NVFP4 inherits the
|
| 56 |
+
# family gate (not swept — same pack/amax structure as MXFP4). STATIC: inherited
|
| 57 |
+
# estimate, not swept — its inline arm is cheaper elementwise work, so the true
|
| 58 |
+
# crossover is at or above the MX one; the M=1 decode case (the one that matters) is
|
| 59 |
+
# inline either way.
|
| 60 |
+
MX_MATMUL_ACT_PREQUANT_MIN_M = 16
|
| 61 |
+
STATIC_MATMUL_ACT_PREQUANT_MIN_M = 16
|
| 62 |
+
# Block-dynamic has NO gate: offline wins at EVERY M incl. M=1 (24.7 vs 31.0us, isolated
|
| 63 |
+
# arm A/B, graph-timed, H=6144 b128) — the inline arm pays a per-tile fp32 amax+div. The
|
| 64 |
+
# always-offline paths (bd 2D/batched/grouped, fused gate_ups, MX grouped) quantize
|
| 65 |
+
# unconditionally; only the two gates above have a real M=1 inline win.
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@bayesian_autotune(
|
| 69 |
+
# tune_block_m: BLOCK_SIZE_M is a config axis. tune_block_n: the N tile is DECOUPLED
|
| 70 |
+
# from the caller's scale granularity (block_n) — a BN=256 tile over 128-wide scale
|
| 71 |
+
# columns halves activation re-reads; any BN is numerically fine (the offs_bn //
|
| 72 |
+
# block_n gather spans or splits scale columns), while BK stays pinned to block_k
|
| 73 |
+
# (activation scale groups are per block_k).
|
| 74 |
+
#
|
| 75 |
+
# Two memory regimes, tuner-routed per (N, K, M) key:
|
| 76 |
+
# - pointer: wins decode / short-K (big tiles starve here — the loop is load-bound).
|
| 77 |
+
# - host-TMA on BOTH operands (A_MEMORY_MODE / B_MEMORY_MODE = descriptor) in the NATURAL
|
| 78 |
+
# orientation (no SWAP_AB — the descriptor box transposes once, in load_weight_tile, to the
|
| 79 |
+
# pointer arm's K-major rhs): wins wide-N compute prefill, feeding the big tiles the pointer
|
| 80 |
+
# arm cannot. WS is load-bearing here: the per-iteration transpose races without it.
|
| 81 |
+
# Emitting both axes lets the tuner route per regime. Verdicts B200 (sm_100) — re-chart on H100.
|
| 82 |
+
get_accelerator_autotuning_configs(
|
| 83 |
+
warp_spec=True,
|
| 84 |
+
tune_block_m=True,
|
| 85 |
+
tune_block_n=True,
|
| 86 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 87 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 88 |
+
pre_hook=rebind_bd_descriptors,
|
| 89 |
+
),
|
| 90 |
+
# m_bit_length (log2 M bucket) keys the M tile, mirroring mx_dynamic_matmul_kernel; GATE keys the
|
| 91 |
+
# gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space). block_n/block_k
|
| 92 |
+
# (the launch-pinned quant block) gate descriptor-box legality and the K tile — two checkpoints
|
| 93 |
+
# at one shape with different blocks must not share a winner.
|
| 94 |
+
["N", "K", "m_bit_length", "GATE", "block_n", "block_k"],
|
| 95 |
+
n_trials=100,
|
| 96 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 97 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 98 |
+
finite_check_args=("C", "Cs"),
|
| 99 |
+
# WS compile guard + descriptor TMA-box limits (256/dim) + shared-memory fit (the big
|
| 100 |
+
# TMA prefill tiles reach the smem ceiling; BM256 BN256 OOMs). The gate|up stack reads via the
|
| 101 |
+
# pointer arm (a contiguous descriptor box can't span the N-apart gate/up rows), so prune the
|
| 102 |
+
# B-descriptor configs under GATE. The implicit dot_scaled arm (UE8M0 As, native-M tile)
|
| 103 |
+
# carries the sm_10x scaled-MMA width cap — GATE at BN=256 would emit a width-512 trap.
|
| 104 |
+
prune_configs_by={
|
| 105 |
+
"early_config_prune": compose_pruners(
|
| 106 |
+
fp8_dot_warp_pruner(),
|
| 107 |
+
warp_spec_compile_guard_pruner(),
|
| 108 |
+
matched_memory_modes_pruner(),
|
| 109 |
+
descriptor_box_pruner("block_k"),
|
| 110 |
+
smem_pruner("block_k"),
|
| 111 |
+
block_dynamic_mma_width_pruner(),
|
| 112 |
+
block_dynamic_2d_warp_spec_pruner(),
|
| 113 |
+
)
|
| 114 |
+
},
|
| 115 |
+
)
|
| 116 |
+
@triton.jit
|
| 117 |
+
def w8a8_block_dynamic_fp8_matmul_kernel(
|
| 118 |
+
A, # (M, K) E4M3 activations (pre-quantized once by the wrapper)
|
| 119 |
+
ADescriptor, # host TMA descriptor over A (M, K), box (BLOCK_SIZE_M, block_k); read iff A_MEMORY_MODE != "pointer"
|
| 120 |
+
As, # (M, K // block_k) per-row, per-K-block activation scales (fp32 or uint8/UE8M0)
|
| 121 |
+
B, # (N, K) FP8 weights
|
| 122 |
+
BDescriptor, # host TMA descriptor over B (N, K), box (BLOCK_SIZE_N, block_k); read iff B_MEMORY_MODE != "pointer"
|
| 123 |
+
Bs, # (N // block_n, K // block_k) weight scales (fp32 or uint8/UE8M0)
|
| 124 |
+
C, # (M, N) output
|
| 125 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 126 |
+
# Shape
|
| 127 |
+
M,
|
| 128 |
+
N,
|
| 129 |
+
K,
|
| 130 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 131 |
+
# Strides
|
| 132 |
+
stride_a_m,
|
| 133 |
+
stride_a_k,
|
| 134 |
+
stride_as_m,
|
| 135 |
+
stride_as_k, # activation-scale group-axis stride: 1 for a row-major (M, K//block_k) grid,
|
| 136 |
+
# but a real stride when the caller hands in a transposed view (the weight side has carried
|
| 137 |
+
# both strides all along; this is the activation side catching up)
|
| 138 |
+
stride_b_k,
|
| 139 |
+
stride_b_n,
|
| 140 |
+
stride_bs_k,
|
| 141 |
+
stride_bs_n,
|
| 142 |
+
stride_c_m,
|
| 143 |
+
stride_c_n,
|
| 144 |
+
stride_bias_e,
|
| 145 |
+
stride_bias_n,
|
| 146 |
+
# Weight-quantization blocks (the caller's block_size); block_k is also the K tile
|
| 147 |
+
# (the activation scale groups are per block_k)
|
| 148 |
+
block_n: tl.constexpr,
|
| 149 |
+
block_k: tl.constexpr,
|
| 150 |
+
# Meta-parameters
|
| 151 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 152 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 153 |
+
WARP_SPEC: tl.constexpr = False,
|
| 154 |
+
B_MEMORY_MODE: tl.constexpr = "pointer",
|
| 155 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 156 |
+
SWAP_AB: tl.constexpr = False,
|
| 157 |
+
# Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output (bf16 — the
|
| 158 |
+
# requant output formats live on the MX kernel). Every arm folds out at compile time when
|
| 159 |
+
# GATE=False (the plain dense GEMM, unchanged).
|
| 160 |
+
GATE: tl.constexpr = False,
|
| 161 |
+
ACT_FN: tl.constexpr = "silu",
|
| 162 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 163 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 164 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 165 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 166 |
+
):
|
| 167 |
+
"""Block-scale FP8 GEMM kernel.
|
| 168 |
+
|
| 169 |
+
Computes ``C = A @ B.T``. Activations arrive pre-quantized (one pass in the
|
| 170 |
+
wrapper — the inline per-N-tile quant would repeat N//BN times per element; see the
|
| 171 |
+
grouped kernels). 2D grid with swizzle for L2 cache locality on B.
|
| 172 |
+
|
| 173 |
+
Two operand-feed regimes, tuner-routed (see the decorator): explicit pointers, or
|
| 174 |
+
host-TMA descriptors on both operands. Under the descriptor modes the natural
|
| 175 |
+
orientation loads the ``(BN, BK)`` weight box and transposes it once (in
|
| 176 |
+
``load_weight_tile``) to the same K-major rhs the pointer arm builds — no ``SWAP_AB``
|
| 177 |
+
(that trans is race-free only under ``WARP_SPEC``). ``SWAP_AB`` remains an
|
| 178 |
+
independent pointer-arm orientation knob (weight tile in the MMA M dim, activation
|
| 179 |
+
loaded transposed via strides); it is implemented but not currently emitted.
|
| 180 |
+
|
| 181 |
+
UE8M0 scales (activations quantized to power-of-two, weights UE8M0) on a native-M tile
|
| 182 |
+
(the MMA M operand is 128-wide: BLOCK_SIZE_M no-swap, BLOCK_SIZE_N swapped) fold the
|
| 183 |
+
128-group scales into a tcgen05 ``dot_scaled`` MMA — see ``block_dynamic_dot``. fp32
|
| 184 |
+
scales, or a narrow M tile (decode), keep the plain ``tl.dot`` + software rescale.
|
| 185 |
+
"""
|
| 186 |
+
USE_DOT_SCALED: tl.constexpr = (As.dtype.element_ty == tl.uint8) and (
|
| 187 |
+
(BLOCK_SIZE_N if SWAP_AB else BLOCK_SIZE_M) >= 128
|
| 188 |
+
)
|
| 189 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 190 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, block_k, GATE=GATE
|
| 191 |
+
)
|
| 192 |
+
# Scale pointers index off AFFINE row/col offsets + a bounds mask; the %-wrapped forms
|
| 193 |
+
# (offs_am/offs_bn from swizzle_offsets) drive ONLY the pointer arm's operand tiles
|
| 194 |
+
# (token replication at decode). Keeping scales affine avoids the non-affine gather
|
| 195 |
+
# the wrap induces (~460us / 18pp at prefill).
|
| 196 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 197 |
+
n_rows = 2 * N if GATE else N
|
| 198 |
+
offs_am_lin = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 199 |
+
offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
|
| 200 |
+
as_ptrs = As + offs_am_lin * stride_as_m
|
| 201 |
+
as_mask = offs_am_lin < M
|
| 202 |
+
bs_mask = offs_bn_lin < n_rows
|
| 203 |
+
a_descriptor = operand_tile_descriptor(
|
| 204 |
+
ADescriptor, A, M, K, stride_a_m, stride_a_k, BLOCK_SIZE_M, block_k, A_MEMORY_MODE
|
| 205 |
+
)
|
| 206 |
+
b_descriptor = operand_tile_descriptor(
|
| 207 |
+
BDescriptor,
|
| 208 |
+
B,
|
| 209 |
+
N,
|
| 210 |
+
K,
|
| 211 |
+
stride_b_n,
|
| 212 |
+
stride_b_k,
|
| 213 |
+
BLOCK_SIZE_N,
|
| 214 |
+
block_k,
|
| 215 |
+
B_MEMORY_MODE, GATE,
|
| 216 |
+
)
|
| 217 |
+
# Explicit operand-tile pointers on the pointer arm; a scalar placeholder on a descriptor
|
| 218 |
+
# arm (which reads its box via the descriptor — the [BM,BK]/[BK,BN] index tensor would stay
|
| 219 |
+
# live across the K-loop for nothing and spill registers, ~460us / 20pp MFU at wide-N prefill).
|
| 220 |
+
# Under GATE the weight is the stacked (2N, K) gate|up tile (matmul_weight_ptrs; pointer-only, the
|
| 221 |
+
# descriptor box can't span the N-apart rows), and its per-weight-row block scale is a 2*BN gather
|
| 222 |
+
# (gate rows [0,N), up rows offset by N//block_n scale blocks) — the block_dynamic_dot broadcast
|
| 223 |
+
# then folds it per row exactly as in the dense case.
|
| 224 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, not SWAP_AB)
|
| 225 |
+
b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB)
|
| 226 |
+
# the scale-index gather decouples the tile from the scale grid: a wide tile spans several
|
| 227 |
+
# scale columns, a narrow one shares a column
|
| 228 |
+
bs_ptrs = Bs + (offs_bn_lin // block_n) * stride_bs_n
|
| 229 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 230 |
+
|
| 231 |
+
for k in tl.range(0, tl.cdiv(K, block_k), warp_specialize=WARP_SPEC):
|
| 232 |
+
a, a_s = load_act_block_dynamic(
|
| 233 |
+
a_ptrs, as_ptrs, None, as_mask, a_descriptor, pid_m * BLOCK_SIZE_M, k * block_k,
|
| 234 |
+
0, 0, A_MEMORY_MODE, False, False, SWAP_AB,
|
| 235 |
+
)
|
| 236 |
+
b, b_s = load_weight_block_dynamic(
|
| 237 |
+
b_ptrs, b_descriptor, bs_ptrs, bs_mask, 0, 0, pid_n * n_width, k * block_k, 0, 0,
|
| 238 |
+
GATE, False, B_MEMORY_MODE, SWAP_AB, BLOCK_SIZE_N, block_k,
|
| 239 |
+
)
|
| 240 |
+
accumulator = block_dynamic_dot(
|
| 241 |
+
accumulator, a, a_s, b, b_s, block_k, SWAP_AB, USE_DOT_SCALED, False
|
| 242 |
+
)
|
| 243 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
|
| 244 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
|
| 245 |
+
block_k * stride_a_k, stride_as_k, block_k * stride_b_k, stride_bs_k,
|
| 246 |
+
A_MEMORY_MODE, B_MEMORY_MODE, True, True, False,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if GATE:
|
| 250 |
+
gemm_epilogue(
|
| 251 |
+
C, None, accumulator, offs_am_lin, pid_n, pid_m, as_mask,
|
| 252 |
+
stride_c_m, stride_c_n, 0, 0,
|
| 253 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, block_k,
|
| 254 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 255 |
+
COMPUTE_MODE="dot", SWAP_AB=SWAP_AB, N_COLS=N,
|
| 256 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
accumulator = add_bias(
|
| 260 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
|
| 261 |
+
BLOCK_SIZE_N, N, SWAP_AB,
|
| 262 |
+
)
|
| 263 |
+
store_masked_oriented(
|
| 264 |
+
C,
|
| 265 |
+
accumulator,
|
| 266 |
+
pid_m,
|
| 267 |
+
pid_n,
|
| 268 |
+
M,
|
| 269 |
+
N,
|
| 270 |
+
stride_c_m,
|
| 271 |
+
stride_c_n,
|
| 272 |
+
BLOCK_SIZE_M,
|
| 273 |
+
BLOCK_SIZE_N,
|
| 274 |
+
SWAP_AB,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@bayesian_autotune(
|
| 279 |
+
# tune_block_m mirrors the block-dynamic kernel (adaptive BM=128 cost it 17% at prefill).
|
| 280 |
+
get_accelerator_autotuning_configs(
|
| 281 |
+
tune_block_nk=True, warp_spec=True, tune_block_m=True
|
| 282 |
+
),
|
| 283 |
+
# GATE keys the gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space).
|
| 284 |
+
["N", "K", "m_bit_length", "GATE"],
|
| 285 |
+
n_trials=100,
|
| 286 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 287 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 288 |
+
finite_check_args=("C", "Cs"),
|
| 289 |
+
# block_k is a tuned axis and the loop below is maskless — veto non-dividing BKs;
|
| 290 |
+
# WS is a pure perf axis here (non-WS is the validated state), compile-guarded.
|
| 291 |
+
prune_configs_by={
|
| 292 |
+
"early_config_prune": compose_pruners(
|
| 293 |
+
fp8_dot_warp_pruner(),
|
| 294 |
+
block_within_dim_pruner("K"),
|
| 295 |
+
warp_spec_compile_guard_pruner(),
|
| 296 |
+
)
|
| 297 |
+
},
|
| 298 |
+
)
|
| 299 |
+
@triton.jit
|
| 300 |
+
def w8a8_tensor_dynamic_fp8_matmul_kernel(
|
| 301 |
+
A, # (M, K) pre-quantized FP8 activations
|
| 302 |
+
As, # (M,) per-token activation scales
|
| 303 |
+
B, # (N, K) FP8 weights
|
| 304 |
+
Bs, # scalar/(1,) per-tensor weight scale
|
| 305 |
+
C, # (M, N) output
|
| 306 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 307 |
+
# Shape
|
| 308 |
+
M,
|
| 309 |
+
N,
|
| 310 |
+
K,
|
| 311 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 312 |
+
# Strides
|
| 313 |
+
stride_a_m,
|
| 314 |
+
stride_a_k,
|
| 315 |
+
stride_as_m,
|
| 316 |
+
stride_b_k,
|
| 317 |
+
stride_b_n,
|
| 318 |
+
stride_c_m,
|
| 319 |
+
stride_c_n,
|
| 320 |
+
stride_bias_e,
|
| 321 |
+
stride_bias_n,
|
| 322 |
+
# Meta-parameters
|
| 323 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 324 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 325 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 326 |
+
WARP_SPEC: tl.constexpr = False,
|
| 327 |
+
# Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
|
| 328 |
+
# folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
|
| 329 |
+
GATE: tl.constexpr = False,
|
| 330 |
+
ACT_FN: tl.constexpr = "silu",
|
| 331 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 332 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 333 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 334 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 335 |
+
):
|
| 336 |
+
"""Tensor-scale FP8 GEMM kernel.
|
| 337 |
+
|
| 338 |
+
Computes ``C = A @ B.T`` with one activation scale per row and one
|
| 339 |
+
weight scale for the full matrix.
|
| 340 |
+
Uses a 2D grid with swizzle for L2 cache locality on B tiles.
|
| 341 |
+
"""
|
| 342 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 343 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 344 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GATE=GATE
|
| 345 |
+
)
|
| 346 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 347 |
+
# Under GATE the weight is the stacked (2N, K) gate|up tile; one per-tensor weight scale
|
| 348 |
+
# covers both projections, applied (with the per-row act scale) before the GLU split.
|
| 349 |
+
b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, "pointer")
|
| 350 |
+
|
| 351 |
+
a_s = tl.load(As + offs_am * stride_as_m)
|
| 352 |
+
b_s = tl.load(Bs)
|
| 353 |
+
|
| 354 |
+
# Accumulate raw dot products, apply scales once after the loop.
|
| 355 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
|
| 356 |
+
for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
|
| 357 |
+
a, _as = load_act_plain(a_ptrs, a_ptrs, 0, 0, None, 0, "pointer", False)
|
| 358 |
+
b, _bs = load_weight_plain(
|
| 359 |
+
b_ptrs, b_ptrs, 0, pid_n * n_width, k * BLOCK_SIZE_K,
|
| 360 |
+
GATE, False, "pointer", False, BLOCK_SIZE_N, BLOCK_SIZE_K,
|
| 361 |
+
)
|
| 362 |
+
accumulator = accumulator + fp8_dot(a, b, False, BLOCK_SIZE_K)
|
| 363 |
+
a_ptrs, _asp, b_ptrs, _bsp, _, _ = advance_ptrs(
|
| 364 |
+
a_ptrs, a_ptrs, b_ptrs, b_ptrs, b_ptrs, b_ptrs,
|
| 365 |
+
BLOCK_SIZE_K * stride_a_k, 0, BLOCK_SIZE_K * stride_b_k, 0,
|
| 366 |
+
"pointer", "pointer", False, False, False,
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
accumulator = accumulator * a_s[:, None] * b_s
|
| 370 |
+
|
| 371 |
+
if GATE:
|
| 372 |
+
out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 373 |
+
gemm_epilogue(
|
| 374 |
+
C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
|
| 375 |
+
stride_c_m, stride_c_n, 0, 0,
|
| 376 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
|
| 377 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 378 |
+
COMPUTE_MODE="dot", N_COLS=N,
|
| 379 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 380 |
+
)
|
| 381 |
+
else:
|
| 382 |
+
accumulator = add_bias(
|
| 383 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
|
| 384 |
+
BLOCK_SIZE_N, N, False,
|
| 385 |
+
)
|
| 386 |
+
store_masked(
|
| 387 |
+
C,
|
| 388 |
+
accumulator,
|
| 389 |
+
pid_m,
|
| 390 |
+
pid_n,
|
| 391 |
+
M,
|
| 392 |
+
N,
|
| 393 |
+
stride_c_m,
|
| 394 |
+
stride_c_n,
|
| 395 |
+
BLOCK_SIZE_M,
|
| 396 |
+
BLOCK_SIZE_N,
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
@bayesian_autotune(
|
| 401 |
+
# tune_block_m + tune_block_n + WARP_SPEC + host-TMA memory modes mirror the block-dynamic
|
| 402 |
+
# kernel above (same 2D swizzle loop physics). Both A/B memory axes are emitted and the tuner
|
| 403 |
+
# routes descriptor vs pointer per (N, K, M) key; the A descriptor is read only on the offline
|
| 404 |
+
# (pre-quantized fp8 A) arm — descriptor_needs_prequant_pruner keeps the M<threshold inline arm
|
| 405 |
+
# pointer-only. The N tile is decoupled from the scale granularity.
|
| 406 |
+
get_accelerator_autotuning_configs(
|
| 407 |
+
warp_spec=True,
|
| 408 |
+
tune_block_m=True,
|
| 409 |
+
tune_block_n=True,
|
| 410 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 411 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 412 |
+
pre_hook=rebind_bd_descriptors,
|
| 413 |
+
),
|
| 414 |
+
# GATE keys the gate|up arm separately (its stacked dot is 2*BN wide, a distinct config space);
|
| 415 |
+
# block_n/block_k (the launch-pinned quant block) gate descriptor-box legality and the K tile.
|
| 416 |
+
["N", "K", "m_bit_length", "GATE", "block_n", "block_k"],
|
| 417 |
+
n_trials=100,
|
| 418 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 419 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 420 |
+
finite_check_args=("C", "Cs"),
|
| 421 |
+
# The gate|up stack reads via the pointer arm (a contiguous descriptor box can't span the
|
| 422 |
+
# N-apart gate/up rows), so prune the B-descriptor configs under GATE.
|
| 423 |
+
prune_configs_by={
|
| 424 |
+
"early_config_prune": compose_pruners(
|
| 425 |
+
fp8_dot_warp_pruner(),
|
| 426 |
+
warp_spec_compile_guard_pruner(),
|
| 427 |
+
descriptor_needs_prequant_pruner(),
|
| 428 |
+
matched_memory_modes_pruner(),
|
| 429 |
+
descriptor_box_pruner("block_k"),
|
| 430 |
+
smem_pruner("block_k"),
|
| 431 |
+
)
|
| 432 |
+
},
|
| 433 |
+
)
|
| 434 |
+
@triton.jit
|
| 435 |
+
def w8a8_block_static_fp8_matmul_kernel(
|
| 436 |
+
A, # (M, K) E4M3 activations (pre-quantized against the static scale by the wrapper)
|
| 437 |
+
ADescriptor, # host TMA descriptor over A (M, K), box (BM, block_k); read iff A_MEMORY_MODE != "pointer"
|
| 438 |
+
As, # scalar — static per-tensor activation scale (calibration-time)
|
| 439 |
+
B, # (N, K) FP8 weights
|
| 440 |
+
BDescriptor, # host TMA descriptor over B (N, K), box (BN, block_k); read iff B_MEMORY_MODE != "pointer"
|
| 441 |
+
Bs, # (N // block_n, K // block_k) weight scales (fp32 or uint8/UE8M0)
|
| 442 |
+
C, # (M, N) output
|
| 443 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 444 |
+
# Shape
|
| 445 |
+
M,
|
| 446 |
+
N,
|
| 447 |
+
K,
|
| 448 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 449 |
+
# Strides
|
| 450 |
+
stride_a_m,
|
| 451 |
+
stride_a_k,
|
| 452 |
+
stride_b_k,
|
| 453 |
+
stride_b_n,
|
| 454 |
+
stride_bs_k,
|
| 455 |
+
stride_bs_n,
|
| 456 |
+
stride_c_m,
|
| 457 |
+
stride_c_n,
|
| 458 |
+
stride_bias_e,
|
| 459 |
+
stride_bias_n,
|
| 460 |
+
# Weight-quantization blocks (see the block-dynamic kernel)
|
| 461 |
+
block_n: tl.constexpr,
|
| 462 |
+
block_k: tl.constexpr,
|
| 463 |
+
# Meta-parameters
|
| 464 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 465 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 466 |
+
WARP_SPEC: tl.constexpr = False,
|
| 467 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 468 |
+
B_MEMORY_MODE: tl.constexpr = "pointer",
|
| 469 |
+
# Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
|
| 470 |
+
# folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
|
| 471 |
+
GATE: tl.constexpr = False,
|
| 472 |
+
ACT_FN: tl.constexpr = "silu",
|
| 473 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 474 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 475 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 476 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 477 |
+
):
|
| 478 |
+
"""Block-scale FP8 GEMM with static (per-tensor) activation scale.
|
| 479 |
+
|
| 480 |
+
``A`` arrives pre-quantized (one elementwise ``(A / As).to(fp8)`` pass in the
|
| 481 |
+
wrapper — an inline division would repeat per N-tile). Per-block weight scales apply
|
| 482 |
+
per-K-tile during accumulation; the scalar activation scale is applied once at
|
| 483 |
+
the end.
|
| 484 |
+
"""
|
| 485 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 486 |
+
n_rows = 2 * N if GATE else N
|
| 487 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 488 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, block_k, GATE=GATE
|
| 489 |
+
)
|
| 490 |
+
a_descriptor = operand_tile_descriptor(
|
| 491 |
+
ADescriptor, A, M, K, stride_a_m, stride_a_k, BLOCK_SIZE_M, block_k, A_MEMORY_MODE
|
| 492 |
+
)
|
| 493 |
+
b_descriptor = operand_tile_descriptor(
|
| 494 |
+
BDescriptor, B, N, K, stride_b_n, stride_b_k, BLOCK_SIZE_N, block_k, B_MEMORY_MODE, GATE,
|
| 495 |
+
)
|
| 496 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
|
| 497 |
+
# Weight-scale index off the AFFINE column offset + a bounds mask (the %-wrapped offs_bn
|
| 498 |
+
# from swizzle_offsets drives only the pointer operand tile); an affine gather avoids the
|
| 499 |
+
# non-affine scale read the wrap induces, matching the block-dynamic kernel above.
|
| 500 |
+
b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, B_MEMORY_MODE)
|
| 501 |
+
offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
|
| 502 |
+
bs_ptrs = Bs + (offs_bn_lin // block_n) * stride_bs_n
|
| 503 |
+
bs_mask = offs_bn_lin < n_rows
|
| 504 |
+
a_s_static = tl.load(As)
|
| 505 |
+
|
| 506 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
|
| 507 |
+
for k in tl.range(0, tl.cdiv(K, block_k), warp_specialize=WARP_SPEC):
|
| 508 |
+
a, _as = load_act_static(
|
| 509 |
+
a_ptrs, a_descriptor, pid_m * BLOCK_SIZE_M, k * block_k, None, 0, a_s_static,
|
| 510 |
+
A_MEMORY_MODE, False,
|
| 511 |
+
)
|
| 512 |
+
b, b_s = load_weight_static(
|
| 513 |
+
b_ptrs, b_descriptor, bs_ptrs, bs_mask, 0, 0, pid_n * n_width, k * block_k, 0, 0,
|
| 514 |
+
GATE, False, B_MEMORY_MODE, False, BLOCK_SIZE_N, block_k,
|
| 515 |
+
)
|
| 516 |
+
accumulator = static_dot(accumulator, a, b, b_s, False, block_k, False)
|
| 517 |
+
a_ptrs, _asp, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
|
| 518 |
+
a_ptrs, a_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
|
| 519 |
+
block_k * stride_a_k, 0, block_k * stride_b_k, stride_bs_k,
|
| 520 |
+
A_MEMORY_MODE, B_MEMORY_MODE, False, True, False,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
accumulator = accumulator * a_s_static
|
| 524 |
+
if GATE:
|
| 525 |
+
out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 526 |
+
gemm_epilogue(
|
| 527 |
+
C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
|
| 528 |
+
stride_c_m, stride_c_n, 0, 0,
|
| 529 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, block_k,
|
| 530 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 531 |
+
COMPUTE_MODE="dot", N_COLS=N,
|
| 532 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 533 |
+
)
|
| 534 |
+
else:
|
| 535 |
+
accumulator = add_bias(
|
| 536 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
|
| 537 |
+
BLOCK_SIZE_N, N, False,
|
| 538 |
+
)
|
| 539 |
+
store_masked(
|
| 540 |
+
C,
|
| 541 |
+
accumulator,
|
| 542 |
+
pid_m,
|
| 543 |
+
pid_n,
|
| 544 |
+
M,
|
| 545 |
+
N,
|
| 546 |
+
stride_c_m,
|
| 547 |
+
stride_c_n,
|
| 548 |
+
BLOCK_SIZE_M,
|
| 549 |
+
BLOCK_SIZE_N,
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
@bayesian_autotune(
|
| 554 |
+
# tune_block_m: BLOCK_SIZE_M becomes a config axis, so the tuner sizes the M tile
|
| 555 |
+
# per workload — small at decode, large at prefill.
|
| 556 |
+
# scalar is in the mode set for M=1 decode (attn projection): it avoids the MMA M->16 pad
|
| 557 |
+
# that held MXFP8 attn decode 35% over block-dynamic at identical weight bytes; fp4-scalar
|
| 558 |
+
# is dropped by the pruner (ALU-bound unpack). swap_ab emits the BM=1 swap decode rows,
|
| 559 |
+
# scoped by mx_2d_swap_scope_pruner to E4M3-scale (NVFP4) decode — its ONLY native M=1
|
| 560 |
+
# arm (dot_scaled has no sub-M=128 E4M3 form; scalar is unpack-bound: 60-142µs vs swap's
|
| 561 |
+
# 22-50µs on the GLM attn shape, 2026-08-05). UE8M0 formats keep swap OFF: an 18-cell
|
| 562 |
+
# forced-swap sweep (cudagraph, M3+dsv4 decode) showed swap losing there (M3 attn −38%).
|
| 563 |
+
get_accelerator_autotuning_configs(
|
| 564 |
+
mx=True,
|
| 565 |
+
tune_block_nk=True,
|
| 566 |
+
compute_modes=("dot_scaled", "dot", "scalar"),
|
| 567 |
+
tune_block_m=True,
|
| 568 |
+
swap_ab=True,
|
| 569 |
+
# host-TMA on both operands, tuner-picked vs pointers (measured +14-17pp: mxfp8
|
| 570 |
+
# 54->71%, W4A8 50->67% via mixed A=ptr/B=desc, W4A4 +14%). Mixed A/B modes KEPT
|
| 571 |
+
# (unlike dense bd): A=pointer + B=descriptor wins the mid-M band.
|
| 572 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 573 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 574 |
+
pre_hook=rebind_mx_descriptors,
|
| 575 |
+
# WS tuner axis: dot_scaled + WS + TMA compiles + wins at prefill (1918 -> ~1995 @M=4k);
|
| 576 |
+
# warp_spec_compile_guard_pruner keeps it to num_warps%4==0, BM>=64.
|
| 577 |
+
warp_spec=True,
|
| 578 |
+
),
|
| 579 |
+
# the MXFP4/MXFP8 split keys itself — the tuner appends every tensor arg's dtype to
|
| 580 |
+
# its cache key (memory and disk);
|
| 581 |
+
# m_bit_length (log2 M bucket) keys the M tile — the winner keeps shifting with M well past the
|
| 582 |
+
# BM ceiling and it is NOT noise: cross-applying configs (N=K=4096) costs +62% at M=128 and +245%
|
| 583 |
+
# at M=4096 (the thin M=128 tile can't saturate the wide GEMM), so don't collapse the buckets.
|
| 584 |
+
# ACTIVATION_FORMAT keys the inline act-quant grid: A stays raw bf16 under every
|
| 585 |
+
# format below the pre-quant M threshold, so the dtype-appended key can't
|
| 586 |
+
# split W4A8 from W4A4 itself. OUTPUT_FORMAT/SWIZZLED_OUT key the requant epilogue
|
| 587 |
+
# explicitly (today the C/Cs dtypes split them incidentally; a descriptor Cs has no
|
| 588 |
+
# dtype token, so don't lean on the append).
|
| 589 |
+
["N", "K", "m_bit_length", "ACTIVATION_FORMAT", "SWIZZLED_SCALES", "GATE", "OUTPUT_FORMAT", "SWIZZLED_OUT"],
|
| 590 |
+
n_trials=100,
|
| 591 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 592 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 593 |
+
finite_check_args=("C", "Cs"),
|
| 594 |
+
# BK-within-K veto (the loop loads are unmasked) + the sm_10x dot_scaled shape guards
|
| 595 |
+
# + scalar restricted to decode-sized M (a BM=1 GEVM at prefill is TPE poison) + the
|
| 596 |
+
# gate|up stack reads via the pointer arm (the descriptor's one contiguous box can't span
|
| 597 |
+
# the N-apart gate/up rows — a (2,N,K) box is a follow-up), so prune descriptor under GATE.
|
| 598 |
+
prune_configs_by={
|
| 599 |
+
"early_config_prune": compose_pruners(
|
| 600 |
+
gate_stacked_tmem_trap_pruner(),
|
| 601 |
+
swizzled_out_bm_pruner(),
|
| 602 |
+
mx_config_pruner("K"),
|
| 603 |
+
mx_2d_swap_scope_pruner(),
|
| 604 |
+
scalar_max_m_pruner("M"),
|
| 605 |
+
smem_pruner(),
|
| 606 |
+
descriptor_box_pruner("BLOCK_SIZE_K"),
|
| 607 |
+
swizzled_scale_config_pruner(),
|
| 608 |
+
warp_spec_compile_guard_pruner(),
|
| 609 |
+
)
|
| 610 |
+
},
|
| 611 |
+
)
|
| 612 |
+
@triton.jit
|
| 613 |
+
def mx_dynamic_matmul_kernel(
|
| 614 |
+
A, # (M, K) activations: E4M3 (pre-quantized) or raw bf16/fp16 (quantized inline)
|
| 615 |
+
ADescriptor, # host TMA descriptor over A (M, K_bytes), box (BM, BK_bytes); read iff A_MEMORY_MODE != "pointer"
|
| 616 |
+
As, # activation scales: SWIZZLE_32_4_4 (swizzled arm — bulk via ASDescriptor at BM=128, or this pointer for the BM<128 gather) or (M, K // 32) UE8M0 affine
|
| 617 |
+
ASDescriptor, # host TMA descriptor over the SWIZZLE_32_4_4 A scales (BM=128 bulk load); read iff SWIZZLED_SCALES
|
| 618 |
+
B, # (N, K) E4M3 (MXFP8) or (N, K // 2) packed E2M1 (MXFP4) weights
|
| 619 |
+
BDescriptor, # host TMA descriptor over B (N, K_bytes), box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
|
| 620 |
+
Bs, # weight scales: SWIZZLE_32_4_4 (swizzled arm — bulk via BSDescriptor at BN=128, or this pointer for the BN<128 gather) or (N, K // SCALE_GROUP_K) affine
|
| 621 |
+
BSDescriptor, # host TMA descriptor over the SWIZZLE_32_4_4 B scales (BN=128 bulk load); read iff SWIZZLED_SCALES
|
| 622 |
+
C, # (M, N) output (the GLU intermediate under GATE)
|
| 623 |
+
Cs, # (M, N // group) row-major requant output scale — written iff OUTPUT_FORMAT and not SWIZZLED_OUT
|
| 624 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 625 |
+
CSDescriptor, # SWIZZLE_32_4_4 requant-scale descriptor — written iff SWIZZLED_OUT (dummy else), like AS/BS
|
| 626 |
+
AsGlobal, # (1,) fp32 NVFP4 activation global g_a — SOLELY normalizes the inline raw-A quant (A/g_a); read iff not None
|
| 627 |
+
AsBsGlobal, # (1,) fp32 NVFP4 WEIGHT global g_b — recovers on the accumulator together with AsGlobal (in-register product); read iff not None
|
| 628 |
+
CsGlobal, # (1,) fp32 NVFP4 output global (next proj's provided input_scale); normalizes the requant; read iff not None
|
| 629 |
+
# Shape
|
| 630 |
+
M,
|
| 631 |
+
N,
|
| 632 |
+
K,
|
| 633 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 634 |
+
# Strides
|
| 635 |
+
stride_a_m,
|
| 636 |
+
stride_a_k,
|
| 637 |
+
stride_as_m,
|
| 638 |
+
stride_as_k, # activation-scale group-axis stride, in GROUPS (see the affine advance below):
|
| 639 |
+
# 1 for the row-major (M, K // SCALE_GROUP_K) grid, a real stride under a transposed view.
|
| 640 |
+
# Dead on the swizzled arm, which re-derives its block offsets instead of walking pointers.
|
| 641 |
+
stride_b_k,
|
| 642 |
+
stride_b_n,
|
| 643 |
+
stride_bs_k,
|
| 644 |
+
stride_bs_n,
|
| 645 |
+
stride_c_m,
|
| 646 |
+
stride_c_n,
|
| 647 |
+
stride_cs_m, # requant output-scale strides (dead unless OUTPUT_FORMAT)
|
| 648 |
+
stride_cs_n,
|
| 649 |
+
stride_bias_e,
|
| 650 |
+
stride_bias_n,
|
| 651 |
+
stride_as_global, # expert stride of AsGlobal (0 broadcasts a shared scalar)
|
| 652 |
+
stride_bs_global, # expert stride of AsBsGlobal (``global_scale_stride``)
|
| 653 |
+
stride_cs_global, # expert stride of CsGlobal (0 broadcasts a shared scalar)
|
| 654 |
+
# Meta-parameters
|
| 655 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 656 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 657 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 658 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 659 |
+
COMPUTE_MODE: tl.constexpr,
|
| 660 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 661 |
+
B_MEMORY_MODE: tl.constexpr = "pointer",
|
| 662 |
+
ACTIVATION_FORMAT: tl.constexpr = "mxfp8",
|
| 663 |
+
SWIZZLED_SCALES: tl.constexpr = False, # scales pre-swizzled (5D weight + matching acts); else affine/inline
|
| 664 |
+
SWIZZLED_OUT: tl.constexpr = False, # requant emits Cs in SWIZZLE_32_4_4 (CSDescriptor); single source: wrapper
|
| 665 |
+
# Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Every arm
|
| 666 |
+
# folds out at compile time when GATE=False (the plain dense GEMM, unchanged).
|
| 667 |
+
GATE: tl.constexpr = False,
|
| 668 |
+
ACT_FN: tl.constexpr = "silu",
|
| 669 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 670 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 671 |
+
OUTPUT_FORMAT: tl.constexpr = None,
|
| 672 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 673 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 674 |
+
WARP_SPEC: tl.constexpr = False, # tuner axis; +4% at prefill (guard: num_warps%4, BM>=64)
|
| 675 |
+
SWAP_AB: tl.constexpr = False, # swapped decode orientation (BM=1); scoped by mx_2d_swap_scope_pruner
|
| 676 |
+
):
|
| 677 |
+
"""Unified MXFP4/MXFP8 (W4A8/W8A8; a ``uint8`` ``A`` is packed E2M1 — W4A4) GEMM.
|
| 678 |
+
|
| 679 |
+
``C = A @ B.T``. ``A``'s dtype picks the activation form (compile-time folded):
|
| 680 |
+
pre-quantized E4M3 + UE8M0 group-32 scales (one wrapper pass — the inline
|
| 681 |
+
per-N-tile quant re-ran N//BN times per element, ~2x at prefill) vs raw bf16/fp16
|
| 682 |
+
quantized inline (small M: the GEMM is weight-bandwidth-bound with idle ALU, so
|
| 683 |
+
inline is free and a separate quant kernel only adds latency; both forms are
|
| 684 |
+
bit-exact, same group-32 boundaries). Each operand's format is its dtype (``uint8``
|
| 685 |
+
= packed E2M1, two values per byte; else E4M3). ``COMPUTE_MODE`` picks the MMA:
|
| 686 |
+
``tl.dot_scaled`` (native M=128 scaled MMA) vs fp8 ``tl.dot`` + per-group software
|
| 687 |
+
rescale (wins at decode; FP4 unpacks E2M1->E4M3 first — lossless). 2D grid with
|
| 688 |
+
swizzle for L2 reuse.
|
| 689 |
+
"""
|
| 690 |
+
# uint8 A = packed-E2M1 activations (W4A4 — the dtype IS the activation format)
|
| 691 |
+
ACT_VALUES_PER_BYTE: tl.constexpr = 2 if A.dtype.element_ty == tl.uint8 else 1
|
| 692 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr = 2 if B.dtype.element_ty == tl.uint8 else 1
|
| 693 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 694 |
+
SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 695 |
+
offs_ka = tl.arange(0, BLOCK_SIZE_K // ACT_VALUES_PER_BYTE)
|
| 696 |
+
offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 697 |
+
# packed-fp4 weights (WEIGHT_VALUES_PER_BYTE==2) route swizzle_offsets to full L2 grouping
|
| 698 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 699 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, WEIGHT_VALUES_PER_BYTE, GATE=GATE
|
| 700 |
+
)
|
| 701 |
+
# Scales read affine off row/col offsets + bounds masks (the %-wrapped operand offsets would
|
| 702 |
+
# make the scale load a non-affine gather); the SWIZZLED arm reads them via the SA/BS
|
| 703 |
+
# descriptors in the loop instead, so its pointer tiles are dead (base scalars + null masks).
|
| 704 |
+
as_ptrs, bs_ptrs, as_mask, bs_mask = mx_2d_scale_ptrs(
|
| 705 |
+
As, Bs, pid_m, pid_n, M, N, stride_as_m, stride_bs_n, stride_bs_k,
|
| 706 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, SCALE_COLS, SWIZZLED_SCALES, GATE,
|
| 707 |
+
)
|
| 708 |
+
# Operand tiles: activation + the (GATE-stacked-aware) weight, both single leaf calls (the
|
| 709 |
+
# descriptor-vs-pointer and gate|up-stack branches fold inside the leaves). The swap arm
|
| 710 |
+
# loads both naturally — mx_compute reorients at the dot under SWAP_AB, flattening the token tile.
|
| 711 |
+
if SWAP_AB:
|
| 712 |
+
tl.static_assert(
|
| 713 |
+
BLOCK_SIZE_M == 1, "the mx swap arm flattens the single-token tile"
|
| 714 |
+
)
|
| 715 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_ka, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
|
| 716 |
+
b_ptrs = matmul_weight_ptrs(
|
| 717 |
+
B, offs_bn, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE, SWAP_AB
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
accumulator = acc_init(COMPUTE_MODE if SWAP_AB else "dot", BLOCK_SIZE_M, n_width, SWAP_AB)
|
| 721 |
+
for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
|
| 722 |
+
a, a_s = load_act_mx(
|
| 723 |
+
a_ptrs, as_ptrs, AsGlobal, 0, None, as_mask, ADescriptor, pid_m * BLOCK_SIZE_M,
|
| 724 |
+
k * (BLOCK_SIZE_K // ACT_VALUES_PER_BYTE), ASDescriptor, As, 0, 0, pid_m, k, M, K,
|
| 725 |
+
A_MEMORY_MODE, False, False, SWIZZLED_SCALES,
|
| 726 |
+
BLOCK_SIZE_M, BLOCK_SIZE_K, SCALE_GROUP_K, ACTIVATION_FORMAT,
|
| 727 |
+
)
|
| 728 |
+
b, b_s = load_weight_mx(
|
| 729 |
+
b_ptrs, BDescriptor, bs_ptrs, bs_mask, BSDescriptor, Bs, 0, pid_n * n_width,
|
| 730 |
+
k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE), 0, 0, pid_n, k, N, K,
|
| 731 |
+
0, stride_bs_n, stride_bs_k,
|
| 732 |
+
GATE, False, False, B_MEMORY_MODE, SWAP_AB, SWIZZLED_SCALES,
|
| 733 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
|
| 734 |
+
)
|
| 735 |
+
accumulator = mx_compute(
|
| 736 |
+
accumulator, a, a_s, b, b_s, COMPUTE_MODE,
|
| 737 |
+
n_width, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB,
|
| 738 |
+
)
|
| 739 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptrs, _, _ = advance_ptrs(
|
| 740 |
+
a_ptrs, as_ptrs, b_ptrs, bs_ptrs, b_ptrs, bs_ptrs,
|
| 741 |
+
(BLOCK_SIZE_K // ACT_VALUES_PER_BYTE) * stride_a_k,
|
| 742 |
+
(BLOCK_SIZE_K // SCALE_GROUP_K) * stride_as_k,
|
| 743 |
+
(BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k,
|
| 744 |
+
(BLOCK_SIZE_K // SCALE_GROUP_K) * stride_bs_k,
|
| 745 |
+
A_MEMORY_MODE, B_MEMORY_MODE,
|
| 746 |
+
not SWIZZLED_SCALES, (not SWIZZLED_SCALES) and not GATE, False,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
# NVFP4 two-level: block e4m3 scales rode through dot_scaled; recover the combined per-tensor
|
| 750 |
+
# one epilogue for both arms: GATE splits+SwiGLUs (+ optional requant), plain casts+stores.
|
| 751 |
+
# affine output rows (swizzle's offs_am is %-wrapped — the store scatters to real rows);
|
| 752 |
+
# N_COLS masks the 2D-dense column tail.
|
| 753 |
+
out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 754 |
+
gemm_epilogue(
|
| 755 |
+
C, Cs, accumulator, out_row, pid_n, pid_m, out_row < M,
|
| 756 |
+
stride_c_m, stride_c_n, stride_cs_m, stride_cs_n,
|
| 757 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, OUTPUT_FORMAT, SCALE_GROUP_K,
|
| 758 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 759 |
+
COMPUTE_MODE=COMPUTE_MODE, SWAP_AB=SWAP_AB, N_COLS=N, SWIZZLED_OUT=SWIZZLED_OUT,
|
| 760 |
+
CSDescriptor=CSDescriptor, CsGlobal=CsGlobal, stride_cs_global=stride_cs_global,
|
| 761 |
+
GlobalScale=AsBsGlobal,
|
| 762 |
+
stride_global_e=stride_bs_global,
|
| 763 |
+
GlobalScaleA=AsGlobal,
|
| 764 |
+
stride_global_a=stride_as_global,
|
| 765 |
+
global_row=0,
|
| 766 |
+
Bias=Bias,
|
| 767 |
+
stride_bias_e=stride_bias_e,
|
| 768 |
+
stride_bias_n=stride_bias_n,
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
|
| 772 |
+
@bayesian_autotune(
|
| 773 |
+
# weight-only is a plain bf16 dot (the fp4/fp8 weight is upcast per-group in-loop), tuned like the
|
| 774 |
+
# full-precision kernel — tile + WS — PLUS a host-TMA memory-mode axis on both operands
|
| 775 |
+
# (tuner-routed vs pointers, like mx_dynamic): the descriptor box feeds the wide-N prefill tiles
|
| 776 |
+
# the pointer arm starves; pointer wins decode / short-K. Scales stay affine (pointer) — weight-only
|
| 777 |
+
# never swizzles — so the descriptor covers only the operand values. BK spans any number of
|
| 778 |
+
# groups (the dequant is in-tile), so no BK==group constraint.
|
| 779 |
+
get_accelerator_autotuning_configs(
|
| 780 |
+
tune_block_nk=True,
|
| 781 |
+
tune_block_m=True,
|
| 782 |
+
warp_spec=True,
|
| 783 |
+
compute_modes=("dot", "dot_scaled"),
|
| 784 |
+
a_memory_modes=("descriptor", "pointer"),
|
| 785 |
+
b_memory_modes=("descriptor", "pointer"),
|
| 786 |
+
pre_hook=rebind_weight_only_descriptors,
|
| 787 |
+
),
|
| 788 |
+
["N", "K", "m_bit_length", "GATE"],
|
| 789 |
+
n_trials=100,
|
| 790 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 791 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 792 |
+
finite_check_args=("C", "Cs"),
|
| 793 |
+
# WS compile guard + descriptor TMA-box limits (256/dim) + smem fit; the gate|up stack reads via
|
| 794 |
+
# the pointer arm (a contiguous box can't span the N-apart gate/up rows), so prune B-descriptor
|
| 795 |
+
# under GATE.
|
| 796 |
+
prune_configs_by={
|
| 797 |
+
"early_config_prune": compose_pruners(
|
| 798 |
+
block_within_dim_pruner("K"),
|
| 799 |
+
mx_config_pruner("K", "N"), # dot_scaled shape gates
|
| 800 |
+
warp_spec_compile_guard_pruner(),
|
| 801 |
+
descriptor_box_pruner("BLOCK_SIZE_K"),
|
| 802 |
+
smem_pruner("BLOCK_SIZE_K"),
|
| 803 |
+
)
|
| 804 |
+
},
|
| 805 |
+
)
|
| 806 |
+
@triton.jit
|
| 807 |
+
def mx_weight_only_matmul_2d_kernel(
|
| 808 |
+
A, # (M, K) raw BF16/FP16 activations — NOT quantized
|
| 809 |
+
ADescriptor, # host TMA descriptor over A (M, K), box (BM, BK); read iff A_MEMORY_MODE != "pointer"
|
| 810 |
+
B, # (N, K[/2]) MXFP4/NVFP4 (packed E2M1) / MXFP8 (E4M3) weights; under GATE the (2N, ...) gate|up stack
|
| 811 |
+
BDescriptor, # host TMA descriptor over B (N, K_bytes), box (BN, BK_bytes); read iff B_MEMORY_MODE != "pointer"
|
| 812 |
+
Bs, # (N, K // SCALE_GROUP_K) group scales — UE8M0 (MX) or E4M3 (NVFP4)
|
| 813 |
+
BsGlobal, # scalar fp32 NVFP4 per-tensor global — recovers on the accumulator; read iff not None
|
| 814 |
+
C, # (M, N) output (GLU intermediate under GATE)
|
| 815 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 816 |
+
M,
|
| 817 |
+
N,
|
| 818 |
+
K,
|
| 819 |
+
m_bit_length, # autotune key only (log2 M bucket)
|
| 820 |
+
stride_a_m,
|
| 821 |
+
stride_a_k,
|
| 822 |
+
stride_b_n,
|
| 823 |
+
stride_b_k,
|
| 824 |
+
stride_bs_n,
|
| 825 |
+
stride_bs_k,
|
| 826 |
+
stride_c_m,
|
| 827 |
+
stride_c_n,
|
| 828 |
+
stride_bias_e,
|
| 829 |
+
stride_bias_n,
|
| 830 |
+
stride_bs_global, # expert stride of BsGlobal (``global_scale_stride``)
|
| 831 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 832 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 833 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 834 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 835 |
+
WEIGHT_VALUES_PER_BYTE: tl.constexpr,
|
| 836 |
+
A_MEMORY_MODE: tl.constexpr = "pointer",
|
| 837 |
+
B_MEMORY_MODE: tl.constexpr = "pointer",
|
| 838 |
+
COMPUTE_MODE: tl.constexpr = "dot",
|
| 839 |
+
WARP_SPEC: tl.constexpr = False,
|
| 840 |
+
GATE: tl.constexpr = False,
|
| 841 |
+
ACT_FN: tl.constexpr = "silu",
|
| 842 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 843 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 844 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 845 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 846 |
+
):
|
| 847 |
+
"""weight-only dense matmul: ``C = A @ B.T`` with a RAW bf16 activation and an MXFP4/MXFP8 weight
|
| 848 |
+
upcast to bf16 in-loop (unpack E2M1->E4M3, per-group group-scale multiply), then a plain bf16
|
| 849 |
+
``tl.dot``. This is the ``matmul_ogs`` format (fp4/fp8 weight bytes kept in memory, bf16 acts,
|
| 850 |
+
weight upcast in-MMA) — no activation quant, no ``dot_scaled``. Pointer/affine scales only.
|
| 851 |
+
``GATE`` loads the stacked (2N, K) gate|up weight as one ``[BK, 2*BN]`` dot + applies the GLU."""
|
| 852 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 853 |
+
n_rows = 2 * N if GATE else N
|
| 854 |
+
SCALE_COLS: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 855 |
+
offs_kb = tl.arange(0, BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE)
|
| 856 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 857 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, WEIGHT_VALUES_PER_BYTE, GATE=GATE
|
| 858 |
+
)
|
| 859 |
+
# Only the weight scale is needed (bf16 acts carry no scale). Build it N-clamped + UNMASKED
|
| 860 |
+
# (bs_mask=None): valid columns (offs_bn_lin < N) read their real scale; the tail reads a clamped
|
| 861 |
+
# in-bounds scale that the epilogue masks off (N_COLS / store_masked). Dropping the other=0.0
|
| 862 |
+
# bounds mask is what lets the K-loop warp-specialization-lower (the masked load's fill constant
|
| 863 |
+
# is the WS partitioner blocker). GATE routes its stacked gate|up scale through the per-expert
|
| 864 |
+
# leaf, not these ptrs, so this only governs the plain dense arm.
|
| 865 |
+
offs_bn_lin = pid_n * n_width + tl.arange(0, n_width)
|
| 866 |
+
bs_ptrs = Bs + (
|
| 867 |
+
tl.minimum(offs_bn_lin, n_rows - 1)[:, None] * stride_bs_n
|
| 868 |
+
+ tl.arange(0, SCALE_COLS)[None, :] * stride_bs_k
|
| 869 |
+
)
|
| 870 |
+
bs_mask = None
|
| 871 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, A_MEMORY_MODE, True)
|
| 872 |
+
b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_kb, stride_b_n, stride_b_k, B_MEMORY_MODE)
|
| 873 |
+
|
| 874 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
|
| 875 |
+
for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
|
| 876 |
+
# raw bf16 [BM, BK]: pointer tile or the host-TMA box (no scale — the second slot is dead;
|
| 877 |
+
# a distinct name, not `_`, which is already bound pre-loop by mx_2d_scale_ptrs)
|
| 878 |
+
a, a_scale_dead = load_act_plain(
|
| 879 |
+
a_ptrs, ADescriptor, pid_m * BLOCK_SIZE_M, k * BLOCK_SIZE_K, None, 0, A_MEMORY_MODE, False
|
| 880 |
+
)
|
| 881 |
+
b, b_s = load_weight_mx(
|
| 882 |
+
b_ptrs, BDescriptor, bs_ptrs, bs_mask, None, Bs, 0, pid_n * n_width,
|
| 883 |
+
k * (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE), 0, 0, pid_n, k, N, K,
|
| 884 |
+
0, stride_bs_n, stride_bs_k,
|
| 885 |
+
GATE, False, False, B_MEMORY_MODE, False, False,
|
| 886 |
+
BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, WEIGHT_VALUES_PER_BYTE,
|
| 887 |
+
)
|
| 888 |
+
accumulator = mx_weight_only_compute(
|
| 889 |
+
accumulator, a, b, b_s, COMPUTE_MODE, BLOCK_SIZE_K, n_width, SCALE_GROUP_K
|
| 890 |
+
)
|
| 891 |
+
# descriptor arms read by absolute box offset (m/k above); the pointer arms advance here
|
| 892 |
+
a_ptrs += BLOCK_SIZE_K * stride_a_k
|
| 893 |
+
b_ptrs += (BLOCK_SIZE_K // WEIGHT_VALUES_PER_BYTE) * stride_b_k
|
| 894 |
+
bs_ptrs += SCALE_COLS * stride_bs_k
|
| 895 |
+
|
| 896 |
+
if GATE:
|
| 897 |
+
out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 898 |
+
gemm_epilogue(
|
| 899 |
+
C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
|
| 900 |
+
stride_c_m, stride_c_n, 0, 0,
|
| 901 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
|
| 902 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 903 |
+
COMPUTE_MODE="dot", N_COLS=N,
|
| 904 |
+
GlobalScale=BsGlobal, stride_global_e=stride_bs_global,
|
| 905 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 906 |
+
)
|
| 907 |
+
else:
|
| 908 |
+
# explicit (not the epilogue fold): this arm exits through store_masked, which has no
|
| 909 |
+
# GlobalScale slot
|
| 910 |
+
accumulator = apply_global_scale(
|
| 911 |
+
accumulator, BsGlobal, stride_bs_global, None, 0, 0
|
| 912 |
+
)
|
| 913 |
+
accumulator = add_bias(
|
| 914 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
|
| 915 |
+
BLOCK_SIZE_N, N, False,
|
| 916 |
+
)
|
| 917 |
+
store_masked(
|
| 918 |
+
C, accumulator, pid_m, pid_n, M, N, stride_c_m, stride_c_n,
|
| 919 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N,
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
@bayesian_autotune(
|
| 924 |
+
# Same 2D-swizzle loop physics as the tensor kernel — tune the tile, WS a pure perf axis
|
| 925 |
+
# (compile-guarded), no memory-mode/scale axes (unquantized, no scales anywhere).
|
| 926 |
+
get_accelerator_autotuning_configs(tune_block_nk=True, warp_spec=True, tune_block_m=True),
|
| 927 |
+
# GATE keys the gate|up arm separately (its stacked dot is 2*BN wide).
|
| 928 |
+
["N", "K", "m_bit_length", "GATE"],
|
| 929 |
+
n_trials=100,
|
| 930 |
+
path_anchor_axes=PATH_ANCHOR_AXES,
|
| 931 |
+
# 2D outputs are fully written (no sentinel/padding contract), so the numerics veto is safe
|
| 932 |
+
finite_check_args=("C", "Cs"),
|
| 933 |
+
prune_configs_by={
|
| 934 |
+
"early_config_prune": compose_pruners(
|
| 935 |
+
block_within_dim_pruner("K"),
|
| 936 |
+
warp_spec_compile_guard_pruner(),
|
| 937 |
+
)
|
| 938 |
+
},
|
| 939 |
+
)
|
| 940 |
+
@triton.jit
|
| 941 |
+
def full_precision_matmul_2d_kernel(
|
| 942 |
+
A, # (M, K) BF16/FP16 activations
|
| 943 |
+
B, # (N, K) weights in A's dtype; under GATE the (2N, K) gate|up stack
|
| 944 |
+
C, # (M, N) output; under GATE the GLU intermediate
|
| 945 |
+
Bias, # (E, N_out) per-expert output bias, N_out = 2N under GATE; read iff not None
|
| 946 |
+
# Shape
|
| 947 |
+
M,
|
| 948 |
+
N,
|
| 949 |
+
K,
|
| 950 |
+
m_bit_length, # autotune key only (log2 M bucket); unused in body
|
| 951 |
+
# Strides
|
| 952 |
+
stride_a_m,
|
| 953 |
+
stride_a_k,
|
| 954 |
+
stride_b_k,
|
| 955 |
+
stride_b_n,
|
| 956 |
+
stride_c_m,
|
| 957 |
+
stride_c_n,
|
| 958 |
+
stride_bias_e,
|
| 959 |
+
stride_bias_n,
|
| 960 |
+
# Meta-parameters
|
| 961 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 962 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 963 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 964 |
+
WARP_SPEC: tl.constexpr = False,
|
| 965 |
+
# Dense gate|up fusion: B is the (2N, K) gate|up stack, C the [M, N] GLU output. Folds out at
|
| 966 |
+
# compile time when GATE=False (the plain dense GEMM).
|
| 967 |
+
GATE: tl.constexpr = False,
|
| 968 |
+
ACT_FN: tl.constexpr = "silu",
|
| 969 |
+
SWIGLU_ALPHA: tl.constexpr = None,
|
| 970 |
+
SWIGLU_LIMIT: tl.constexpr = None,
|
| 971 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 972 |
+
INTERMEDIATE_DTYPE: tl.constexpr = tl.bfloat16,
|
| 973 |
+
):
|
| 974 |
+
"""Full-precision (BF16/FP16) 2D matmul — the single-GEMM sibling of
|
| 975 |
+
``full_precision_matmul_grouped/batched``: plain ``tl.dot``, fp32 accumulation, no scales.
|
| 976 |
+
``GATE`` loads the stacked (2N, K) gate|up weight as one ``[BK, 2*BN]`` dot and applies the
|
| 977 |
+
``ACT_FN``/SwiGLU GLU; ``GATE=False`` is the plain dense GEMM. 2D grid with swizzle for L2 reuse."""
|
| 978 |
+
n_width: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 979 |
+
pid_m, pid_n, offs_am, offs_bn, offs_k = swizzle_offsets(
|
| 980 |
+
M, N, BLOCK_SIZE_M, BLOCK_SIZE_N, BLOCK_SIZE_K, GATE=GATE
|
| 981 |
+
)
|
| 982 |
+
a_ptrs = operand_tile_ptrs(A, offs_am, offs_k, stride_a_m, stride_a_k, "pointer", True)
|
| 983 |
+
b_ptrs = matmul_weight_ptrs(B, offs_bn, offs_k, stride_b_n, stride_b_k, "pointer")
|
| 984 |
+
|
| 985 |
+
accumulator = acc_init("dot", BLOCK_SIZE_M, n_width, False)
|
| 986 |
+
for k in tl.range(0, tl.cdiv(K, BLOCK_SIZE_K), warp_specialize=WARP_SPEC):
|
| 987 |
+
a = tl.load(a_ptrs)
|
| 988 |
+
w, _ = load_weight_plain(
|
| 989 |
+
b_ptrs, b_ptrs, 0, pid_n * n_width, k * BLOCK_SIZE_K,
|
| 990 |
+
GATE, False, "pointer", False, BLOCK_SIZE_N, BLOCK_SIZE_K,
|
| 991 |
+
)
|
| 992 |
+
accumulator = accumulator + fp8_dot(a, w, False, BLOCK_SIZE_K)
|
| 993 |
+
a_ptrs += BLOCK_SIZE_K * stride_a_k
|
| 994 |
+
b_ptrs += BLOCK_SIZE_K * stride_b_k
|
| 995 |
+
|
| 996 |
+
if GATE:
|
| 997 |
+
out_row = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M)
|
| 998 |
+
gemm_epilogue(
|
| 999 |
+
C, None, accumulator, out_row, pid_n, pid_m, out_row < M,
|
| 1000 |
+
stride_c_m, stride_c_n, 0, 0,
|
| 1001 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N, GATE, None, BLOCK_SIZE_K,
|
| 1002 |
+
ACT_FN, SWIGLU_ALPHA, SWIGLU_LIMIT, SIMULATE_UNFUSED, INTERMEDIATE_DTYPE,
|
| 1003 |
+
COMPUTE_MODE="dot", N_COLS=N,
|
| 1004 |
+
Bias=Bias, stride_bias_e=stride_bias_e, stride_bias_n=stride_bias_n,
|
| 1005 |
+
)
|
| 1006 |
+
else:
|
| 1007 |
+
accumulator = add_bias(
|
| 1008 |
+
accumulator, Bias, stride_bias_e, stride_bias_n, 0, pid_n,
|
| 1009 |
+
BLOCK_SIZE_N, N, False,
|
| 1010 |
+
)
|
| 1011 |
+
store_masked(
|
| 1012 |
+
C, accumulator, pid_m, pid_n, M, N, stride_c_m, stride_c_n,
|
| 1013 |
+
BLOCK_SIZE_M, BLOCK_SIZE_N,
|
| 1014 |
+
)
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
@compile_time_only_triton_op(
|
| 1018 |
+
add_op_namespace_prefix("w8a8_block_dynamic_fp8_matmul"),
|
| 1019 |
+
mutates_args=(),
|
| 1020 |
+
opaque=True,
|
| 1021 |
+
)
|
| 1022 |
+
def w8a8_block_dynamic_fp8_matmul(
|
| 1023 |
+
A: torch.Tensor,
|
| 1024 |
+
B: torch.Tensor,
|
| 1025 |
+
Bs: torch.Tensor,
|
| 1026 |
+
block_size: list[int],
|
| 1027 |
+
output_dtype: torch.dtype | None = None,
|
| 1028 |
+
gate: bool = False,
|
| 1029 |
+
act_fn: str = "silu",
|
| 1030 |
+
swiglu_alpha: float | None = None,
|
| 1031 |
+
swiglu_limit: float | None = None,
|
| 1032 |
+
simulate_unfused: bool = False,
|
| 1033 |
+
bias: torch.Tensor | None = None,
|
| 1034 |
+
) -> list[torch.Tensor]:
|
| 1035 |
+
"""Block-scale FP8 matmul: ``C = A @ B.T``; activations quantized offline in one pass.
|
| 1036 |
+
|
| 1037 |
+
A: (..., K) raw activations, bf16/fp16/fp32 (quantized to FP8 in one wrapper pass)
|
| 1038 |
+
B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack (gate rows [0,N), up [N,2N))
|
| 1039 |
+
Bs: (N // block_n, K // block_k) per-block weight scales (2N rows under ``gate``)
|
| 1040 |
+
|
| 1041 |
+
``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
|
| 1042 |
+
``[..., N]`` GLU intermediate (``output_dtype``); the fused-requant output formats stay on
|
| 1043 |
+
the MX path. Returns a one-element list (mirrors the MX/grouped/batched op signature).
|
| 1044 |
+
"""
|
| 1045 |
+
assert len(block_size) == 2, (
|
| 1046 |
+
f"block_size must be [block_n, block_k], got {block_size}"
|
| 1047 |
+
)
|
| 1048 |
+
block_n, block_k = block_size[0], block_size[1]
|
| 1049 |
+
|
| 1050 |
+
validate_dense_2d_operands(A, B)
|
| 1051 |
+
|
| 1052 |
+
rows, K = B.shape
|
| 1053 |
+
# Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
|
| 1054 |
+
N = rows // 2 if gate else rows
|
| 1055 |
+
M = A.numel() // A.shape[-1]
|
| 1056 |
+
assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
|
| 1057 |
+
assert not gate or N % block_n == 0, (
|
| 1058 |
+
f"gate|up fusion needs N ({N}) divisible by block_n ({block_n}) — the up-proj scale block"
|
| 1059 |
+
)
|
| 1060 |
+
|
| 1061 |
+
assert Bs.ndim == 2, f"Bs must be 2D (rows//block_n, K//block_k), got ndim={Bs.ndim}"
|
| 1062 |
+
assert Bs.shape == (triton.cdiv(rows, block_n), K // block_k), (
|
| 1063 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(rows, block_n)}, {K // block_k})"
|
| 1064 |
+
)
|
| 1065 |
+
|
| 1066 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1067 |
+
# UE8M0 weight scales are the DeepGEMM-Blackwell format — quantize activations to UE8M0
|
| 1068 |
+
# too so the kernel folds both group scales into the tcgen05 dot_scaled MMA (else fp32).
|
| 1069 |
+
A_q, A_s = fp8_act_quant_block_dynamic(
|
| 1070 |
+
A.view(M, K), block_k, use_ue8m0=bs_u8.dtype == torch.uint8
|
| 1071 |
+
)
|
| 1072 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1073 |
+
# Host TMA descriptors over the (M, K) / (N, K) matrices — the placeholder box is
|
| 1074 |
+
# re-bound per tuned config by rebind_bd_descriptors. Read only by the descriptor
|
| 1075 |
+
# (host-TMA) configs the tuner picks for wide-N prefill; pointer configs never touch
|
| 1076 |
+
# them (the constexpr arm folds out in the load helpers).
|
| 1077 |
+
a_descriptor = TensorDescriptor.from_tensor(A_q, [1, block_k])
|
| 1078 |
+
b_descriptor = maybe_descriptor(B, [1, block_k])
|
| 1079 |
+
|
| 1080 |
+
def grid(META):
|
| 1081 |
+
return (
|
| 1082 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1083 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1084 |
+
)
|
| 1085 |
+
|
| 1086 |
+
with device_context(A.device):
|
| 1087 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1088 |
+
compile_time_only_triton_wrap(w8a8_block_dynamic_fp8_matmul_kernel)[grid](
|
| 1089 |
+
A_q,
|
| 1090 |
+
a_descriptor,
|
| 1091 |
+
A_s,
|
| 1092 |
+
B,
|
| 1093 |
+
b_descriptor,
|
| 1094 |
+
bs_u8,
|
| 1095 |
+
C,
|
| 1096 |
+
bias,
|
| 1097 |
+
M,
|
| 1098 |
+
N,
|
| 1099 |
+
K,
|
| 1100 |
+
int(M).bit_length(),
|
| 1101 |
+
A_q.stride(0),
|
| 1102 |
+
A_q.stride(1),
|
| 1103 |
+
A_s.stride(0),
|
| 1104 |
+
A_s.stride(1),
|
| 1105 |
+
B.stride(1),
|
| 1106 |
+
B.stride(0),
|
| 1107 |
+
bs_u8.stride(1),
|
| 1108 |
+
bs_u8.stride(0),
|
| 1109 |
+
C.stride(-2),
|
| 1110 |
+
C.stride(-1),
|
| 1111 |
+
bias_stride_e,
|
| 1112 |
+
bias_stride_n,
|
| 1113 |
+
block_n=block_n,
|
| 1114 |
+
block_k=block_k,
|
| 1115 |
+
GATE=gate,
|
| 1116 |
+
ACT_FN=act_fn,
|
| 1117 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1118 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1119 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1120 |
+
INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
|
| 1121 |
+
)
|
| 1122 |
+
|
| 1123 |
+
return [C]
|
| 1124 |
+
|
| 1125 |
+
|
| 1126 |
+
@compile_time_only_triton_op(
|
| 1127 |
+
add_op_namespace_prefix("w8a8_block_static_fp8_matmul"),
|
| 1128 |
+
mutates_args=(),
|
| 1129 |
+
opaque=True,
|
| 1130 |
+
)
|
| 1131 |
+
def w8a8_block_static_fp8_matmul(
|
| 1132 |
+
A: torch.Tensor,
|
| 1133 |
+
B: torch.Tensor,
|
| 1134 |
+
As: torch.Tensor,
|
| 1135 |
+
Bs: torch.Tensor,
|
| 1136 |
+
block_size: list[int],
|
| 1137 |
+
output_dtype: torch.dtype | None = None,
|
| 1138 |
+
gate: bool = False,
|
| 1139 |
+
act_fn: str = "silu",
|
| 1140 |
+
swiglu_alpha: float | None = None,
|
| 1141 |
+
swiglu_limit: float | None = None,
|
| 1142 |
+
simulate_unfused: bool = False,
|
| 1143 |
+
bias: torch.Tensor | None = None,
|
| 1144 |
+
) -> list[torch.Tensor]:
|
| 1145 |
+
"""Block-scale FP8 matmul with static (per-tensor) activation quantization.
|
| 1146 |
+
|
| 1147 |
+
A: (..., K) raw bf16/fp16 activations — pre-quantized against ``As`` in the wrapper
|
| 1148 |
+
B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack
|
| 1149 |
+
As: scalar / (1,) — per-tensor static activation scale
|
| 1150 |
+
Bs: (N // block_n, K // block_k) per-block weight scales (2N rows under ``gate``)
|
| 1151 |
+
|
| 1152 |
+
``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
|
| 1153 |
+
``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the MX/grouped op).
|
| 1154 |
+
"""
|
| 1155 |
+
assert len(block_size) == 2, (
|
| 1156 |
+
f"block_size must be [block_n, block_k], got {block_size}"
|
| 1157 |
+
)
|
| 1158 |
+
block_n, block_k = block_size[0], block_size[1]
|
| 1159 |
+
|
| 1160 |
+
assert B.dtype != torch.int8, (
|
| 1161 |
+
"static activation quant is not supported on the FP4 path"
|
| 1162 |
+
)
|
| 1163 |
+
assert not (block_n == B.size(0) and block_k == B.size(1)), (
|
| 1164 |
+
"static activation quant requires block-wise weights, not tensor-mode"
|
| 1165 |
+
)
|
| 1166 |
+
validate_dense_2d_operands(A, B)
|
| 1167 |
+
assert As.numel() == 1, f"As must be scalar or (1,), got {tuple(As.shape)}"
|
| 1168 |
+
|
| 1169 |
+
rows, K = B.shape
|
| 1170 |
+
# Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
|
| 1171 |
+
N = rows // 2 if gate else rows
|
| 1172 |
+
M = A.numel() // A.shape[-1]
|
| 1173 |
+
|
| 1174 |
+
assert Bs.ndim == 2, f"Bs must be 2D (rows//block_n, K//block_k), got ndim={Bs.ndim}"
|
| 1175 |
+
assert K % block_k == 0, f"K ({K}) must be divisible by block_k ({block_k})"
|
| 1176 |
+
assert not gate or N % block_n == 0, (
|
| 1177 |
+
f"gate|up fusion needs N ({N}) divisible by block_n ({block_n}) — the up-proj scale block"
|
| 1178 |
+
)
|
| 1179 |
+
assert Bs.shape == (triton.cdiv(rows, block_n), K // block_k), (
|
| 1180 |
+
f"Bs shape {tuple(Bs.shape)} != expected ({triton.cdiv(rows, block_n)}, {K // block_k})"
|
| 1181 |
+
)
|
| 1182 |
+
|
| 1183 |
+
def grid(META):
|
| 1184 |
+
return (
|
| 1185 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1186 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1187 |
+
)
|
| 1188 |
+
|
| 1189 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1190 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1191 |
+
As = As.reshape(1).to(torch.float32)
|
| 1192 |
+
# M-gated static pre-quant (bit-exact with the inline arm: same scalar, same cast);
|
| 1193 |
+
# like MX, the inline form is cheap elementwise work — at M=1 a separate kernel is
|
| 1194 |
+
# pure added latency. The kernel picks its arm off A's dtype.
|
| 1195 |
+
A_q, _ = maybe_act_quant(
|
| 1196 |
+
A.view(M, K),
|
| 1197 |
+
lambda x: ((x.to(torch.float32) / As).to(FP8_DTYPE), As),
|
| 1198 |
+
STATIC_MATMUL_ACT_PREQUANT_MIN_M,
|
| 1199 |
+
)
|
| 1200 |
+
# Host-TMA descriptors over (M, K)/(N, K); placeholder boxes re-bound per config by
|
| 1201 |
+
# rebind_bd_descriptors. Read only by the descriptor configs the tuner picks for wide-N
|
| 1202 |
+
# prefill (offline fp8 A); pointer/inline configs never touch them.
|
| 1203 |
+
a_descriptor = TensorDescriptor.from_tensor(A_q, [1, block_k])
|
| 1204 |
+
b_descriptor = maybe_descriptor(B, [1, block_k])
|
| 1205 |
+
|
| 1206 |
+
with device_context(A.device):
|
| 1207 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1208 |
+
compile_time_only_triton_wrap(w8a8_block_static_fp8_matmul_kernel)[grid](
|
| 1209 |
+
A_q,
|
| 1210 |
+
a_descriptor,
|
| 1211 |
+
As,
|
| 1212 |
+
B,
|
| 1213 |
+
b_descriptor,
|
| 1214 |
+
bs_u8,
|
| 1215 |
+
C,
|
| 1216 |
+
bias,
|
| 1217 |
+
M,
|
| 1218 |
+
N,
|
| 1219 |
+
K,
|
| 1220 |
+
int(M).bit_length(), # m_bit_length key bucket
|
| 1221 |
+
A_q.stride(0),
|
| 1222 |
+
A_q.stride(1),
|
| 1223 |
+
B.stride(1),
|
| 1224 |
+
B.stride(0),
|
| 1225 |
+
bs_u8.stride(1),
|
| 1226 |
+
bs_u8.stride(0),
|
| 1227 |
+
C.stride(-2),
|
| 1228 |
+
C.stride(-1),
|
| 1229 |
+
bias_stride_e,
|
| 1230 |
+
bias_stride_n,
|
| 1231 |
+
# Meta-parameters (BM and BN come from the config; BK is the caller's
|
| 1232 |
+
# block_k — see the block-dynamic kernel)
|
| 1233 |
+
block_n=block_n,
|
| 1234 |
+
block_k=block_k,
|
| 1235 |
+
GATE=gate,
|
| 1236 |
+
ACT_FN=act_fn,
|
| 1237 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1238 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1239 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1240 |
+
INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
|
| 1241 |
+
)
|
| 1242 |
+
|
| 1243 |
+
return [C]
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
@compile_time_only_triton_op(
|
| 1247 |
+
add_op_namespace_prefix("w8a8_tensor_dynamic_fp8_matmul"),
|
| 1248 |
+
mutates_args=(),
|
| 1249 |
+
opaque=True,
|
| 1250 |
+
)
|
| 1251 |
+
def w8a8_tensor_dynamic_fp8_matmul(
|
| 1252 |
+
A: torch.Tensor,
|
| 1253 |
+
B: torch.Tensor,
|
| 1254 |
+
Bs: torch.Tensor,
|
| 1255 |
+
output_dtype: torch.dtype | None = None,
|
| 1256 |
+
gate: bool = False,
|
| 1257 |
+
act_fn: str = "silu",
|
| 1258 |
+
swiglu_alpha: float | None = None,
|
| 1259 |
+
swiglu_limit: float | None = None,
|
| 1260 |
+
simulate_unfused: bool = False,
|
| 1261 |
+
bias: torch.Tensor | None = None,
|
| 1262 |
+
) -> list[torch.Tensor]:
|
| 1263 |
+
"""Tensor-scale FP8 matmul: ``C = A @ B.T``; activations quantized offline per row.
|
| 1264 |
+
|
| 1265 |
+
A: (..., K) raw activations, bf16/fp16/fp32 (flattened to (M, K)
|
| 1266 |
+
internally) — per-row scales computed via ``fp8_act_quant_tensor_wide(A, K)``.
|
| 1267 |
+
B: (N, K) FP8 weights — under ``gate`` the ``(2N, K)`` gate|up stack (one per-tensor scale).
|
| 1268 |
+
Bs: scalar, (1,), or (1, 1) — single tensor-scale weight scale.
|
| 1269 |
+
|
| 1270 |
+
``gate`` fuses the gate|up projection into one stacked GEMM + SwiGLU, returning the
|
| 1271 |
+
``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the MX/grouped op).
|
| 1272 |
+
"""
|
| 1273 |
+
validate_dense_2d_operands(A, B)
|
| 1274 |
+
|
| 1275 |
+
rows, K = B.shape
|
| 1276 |
+
# Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
|
| 1277 |
+
N = rows // 2 if gate else rows
|
| 1278 |
+
M = A.numel() // A.shape[-1]
|
| 1279 |
+
|
| 1280 |
+
assert Bs.numel() == 1, f"Bs must be scalar or (1,), got {tuple(Bs.shape)}"
|
| 1281 |
+
|
| 1282 |
+
# Per-row scalar activation scale (one per token).
|
| 1283 |
+
qA, As = fp8_act_quant_tensor_wide(A, K)
|
| 1284 |
+
As = As.reshape(M)
|
| 1285 |
+
Bs = Bs.reshape(1)
|
| 1286 |
+
|
| 1287 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1288 |
+
|
| 1289 |
+
def grid(META):
|
| 1290 |
+
return (
|
| 1291 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1292 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1293 |
+
)
|
| 1294 |
+
|
| 1295 |
+
with device_context(A.device):
|
| 1296 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1297 |
+
compile_time_only_triton_wrap(w8a8_tensor_dynamic_fp8_matmul_kernel)[grid](
|
| 1298 |
+
qA,
|
| 1299 |
+
As,
|
| 1300 |
+
B,
|
| 1301 |
+
Bs,
|
| 1302 |
+
C,
|
| 1303 |
+
bias,
|
| 1304 |
+
M,
|
| 1305 |
+
N,
|
| 1306 |
+
K,
|
| 1307 |
+
int(M).bit_length(), # m_bit_length key bucket
|
| 1308 |
+
qA.stride(-2),
|
| 1309 |
+
qA.stride(-1),
|
| 1310 |
+
As.stride(0),
|
| 1311 |
+
B.stride(1),
|
| 1312 |
+
B.stride(0),
|
| 1313 |
+
C.stride(-2),
|
| 1314 |
+
C.stride(-1),
|
| 1315 |
+
bias_stride_e,
|
| 1316 |
+
bias_stride_n,
|
| 1317 |
+
GATE=gate,
|
| 1318 |
+
ACT_FN=act_fn,
|
| 1319 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1320 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1321 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1322 |
+
INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
|
| 1323 |
+
)
|
| 1324 |
+
|
| 1325 |
+
return [C]
|
| 1326 |
+
|
| 1327 |
+
|
| 1328 |
+
@compile_time_only_triton_op(
|
| 1329 |
+
add_op_namespace_prefix("mx_dynamic_matmul"), mutates_args=(), opaque=True
|
| 1330 |
+
)
|
| 1331 |
+
def mx_dynamic_matmul(
|
| 1332 |
+
A: torch.Tensor,
|
| 1333 |
+
B: torch.Tensor,
|
| 1334 |
+
As: torch.Tensor | None,
|
| 1335 |
+
Bs: torch.Tensor,
|
| 1336 |
+
output_dtype: torch.dtype | None = None,
|
| 1337 |
+
activation_format: str | None = None,
|
| 1338 |
+
gate: bool = False,
|
| 1339 |
+
act_fn: str = "silu",
|
| 1340 |
+
swiglu_alpha: float | None = None,
|
| 1341 |
+
swiglu_limit: float | None = None,
|
| 1342 |
+
simulate_unfused: bool = False,
|
| 1343 |
+
quantize_output: bool = False,
|
| 1344 |
+
a_global_scale: torch.Tensor | None = None,
|
| 1345 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1346 |
+
output_global_scale: torch.Tensor | None = None,
|
| 1347 |
+
bias: torch.Tensor | None = None,
|
| 1348 |
+
) -> list[torch.Tensor]:
|
| 1349 |
+
"""MX/NVFP4 matmul ``C = A @ B.T``; activations quantized offline above the
|
| 1350 |
+
``maybe_act_quant`` M threshold, inline below it. ``activation_format`` sets the
|
| 1351 |
+
activation grid — E4M3 (``"mxfp8"``, the default) or packed E2M1 (``"mxfp4"``,
|
| 1352 |
+
W4A4); NVFP4 weights (E4M3 scales) pin ``"nvfp4"``. Weight format detected from
|
| 1353 |
+
``B.dtype``: ``int8`` → packed E2M1 (``B`` is ``(N, K//2)``); ``float8_e4m3fn`` →
|
| 1354 |
+
unpacked E4M3. The scale dtype carries the family: UE8M0 = MX group-32, E4M3 =
|
| 1355 |
+
NVFP4 group-16 (``Bs`` is ``(N, K//group)``); tile + dot path are autotuned.
|
| 1356 |
+
|
| 1357 |
+
A: (..., K) raw activations, bf16/fp16/fp32;
|
| 1358 |
+
leading dims are flattened to (M, K) and restored on the output
|
| 1359 |
+
"""
|
| 1360 |
+
assert B.ndim == 2 and Bs.ndim in (2, 5, 6) # 5D = pre-swizzled; 6D = the gate|up artifact
|
| 1361 |
+
assert B.dtype in (torch.int8, torch.float8_e4m3fn), (
|
| 1362 |
+
f"B must be int8 (packed E2M1) or float8_e4m3fn (E4M3), got {B.dtype}"
|
| 1363 |
+
)
|
| 1364 |
+
assert A.is_contiguous(), "A must be contiguous"
|
| 1365 |
+
# B may be row-major (the forward weight) or a transposed view of one: the kernel reads
|
| 1366 |
+
# both of B's strides, and `descriptor_box_pruner` keeps the TMA arms — which need a
|
| 1367 |
+
# unit-stride innermost dim — off the transposed form. This is what lets a backward pass
|
| 1368 |
+
# contract over N by handing in `B.t()` instead of materializing a transposed copy.
|
| 1369 |
+
assert B.is_contiguous() or B.t().is_contiguous(), (
|
| 1370 |
+
"B must be contiguous or a transposed view of a contiguous tensor, got strides "
|
| 1371 |
+
f"{tuple(B.stride())} for shape {tuple(B.shape)}"
|
| 1372 |
+
)
|
| 1373 |
+
# cuBLAS block-scaled fast path — INSIDE the op so every caller gets it (dispatchers,
|
| 1374 |
+
# direct op calls, prequantized-As callers) and so autograd keeps working: register_autograd
|
| 1375 |
+
# hangs on this op and saves INPUTS, so the registered dgrad serves a scaled_mm forward
|
| 1376 |
+
# unchanged. Measured 1.4-4.8x over the Triton kernel at every M (B200, 2026-08-27); a None
|
| 1377 |
+
# falls through to the Triton launch exactly as before. Skipped under fake/meta propagation
|
| 1378 |
+
# (the opaque op's fake impl runs this body with launches disabled — the shape-correct C
|
| 1379 |
+
# allocation below is all fake mode needs).
|
| 1380 |
+
from . import compat as _compat
|
| 1381 |
+
|
| 1382 |
+
if (
|
| 1383 |
+
not gate and not quantize_output and bias is None and not simulate_unfused
|
| 1384 |
+
and not _compat._SKIP_LAUNCHES_MIRROR
|
| 1385 |
+
):
|
| 1386 |
+
_smm = _torch_scaled_mm_2d(
|
| 1387 |
+
A, B, As, Bs, activation_format, a_global_scale, b_global_scale,
|
| 1388 |
+
resolve_output_dtype(output_dtype, A, None),
|
| 1389 |
+
)
|
| 1390 |
+
if _smm is not None:
|
| 1391 |
+
return [_smm]
|
| 1392 |
+
|
| 1393 |
+
WEIGHT_VALUES_PER_BYTE = NIBBLES_PER_BYTE if B.dtype == torch.int8 else 1
|
| 1394 |
+
|
| 1395 |
+
rows, K_b = B.shape
|
| 1396 |
+
K = A.shape[-1]
|
| 1397 |
+
M = A.numel() // K
|
| 1398 |
+
# Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
|
| 1399 |
+
N = rows // 2 if gate else rows
|
| 1400 |
+
assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
|
| 1401 |
+
f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[1] (={K_b})"
|
| 1402 |
+
)
|
| 1403 |
+
# Weight scales arrive row-major (rows, K // scale_group) — read affine, no gain swizzling only
|
| 1404 |
+
# one operand — or already SWIZZLE_32_4_4 (5D), swizzled once at load: the deployment contract,
|
| 1405 |
+
# the op never swizzles in the hot path. The format is the scale dtype (E4M3 = NVFP4 group-16,
|
| 1406 |
+
# UE8M0 = MX group-32). SWIZZLED_SCALES governs both operands (a swizzled weight is paired with
|
| 1407 |
+
# swizzled acts). Callers wanting the tcgen05 fast path pre-swizzle the weight (5D).
|
| 1408 |
+
swizzled_scales = Bs.ndim == 5
|
| 1409 |
+
scale_group = mx_scale_family(Bs, K)
|
| 1410 |
+
if not swizzled_scales:
|
| 1411 |
+
assert Bs.shape == (rows, K // scale_group), (
|
| 1412 |
+
f"Bs shape {tuple(Bs.shape)} != ({rows}, {K // scale_group})"
|
| 1413 |
+
)
|
| 1414 |
+
else:
|
| 1415 |
+
# the artifact's row blocks must match THIS weight (one matrix: leading dim 1) — a
|
| 1416 |
+
# wrong-layer artifact with a matching K would otherwise dequantize with garbage scales silently
|
| 1417 |
+
assert Bs.shape[:2] == (1, triton.cdiv(rows, 128)), (
|
| 1418 |
+
f"swizzled Bs carries {tuple(Bs.shape[:2])} (matrices, 128-row blocks), expected "
|
| 1419 |
+
f"{(1, triton.cdiv(rows, 128))} for this ({rows}, K) weight — wrong artifact"
|
| 1420 |
+
)
|
| 1421 |
+
b_u8 = e2m1_as_uint8(B)
|
| 1422 |
+
bs_u8 = ue8m0_as_uint8(Bs) # caller's layout (5D swizzled / 2D affine); the op never swizzles
|
| 1423 |
+
activation_format = resolve_activation_format(activation_format, B, Bs)
|
| 1424 |
+
# resolve BEFORE the C alloc: with a pre-quantized A (As given) the default output is
|
| 1425 |
+
# bf16, not A's storage dtype (fp8 / packed int8 are not valid output element types)
|
| 1426 |
+
output_dtype = resolve_output_dtype(output_dtype, A, As)
|
| 1427 |
+
# Activation quant is always maybe_act_quant — offline above the M threshold, inline in the
|
| 1428 |
+
# kernel below it (fp4 packs in-register). The offline arm writes swizzled scales directly when
|
| 1429 |
+
# the weight is swizzled (fused, no post-quant pass), else affine. Inline acts (small M) stay
|
| 1430 |
+
# affine in-register even under a swizzled weight — the dot_scaled reads the mixed layout fine.
|
| 1431 |
+
# NVFP4 two-level: a_global_scale (the calibrated activation global) normalizes the quant on both
|
| 1432 |
+
# arms — the offline kernel divides before the block quant, the inline arm via AsGlobal.
|
| 1433 |
+
act_global_scale = normalize_global_scale(a_global_scale, 1) # g_a; folds in-kernel
|
| 1434 |
+
if act_global_scale is not None:
|
| 1435 |
+
assert activation_format == "nvfp4", "an activation global is NVFP4-only"
|
| 1436 |
+
act_quant = lambda a: MX_ACT_QUANT[activation_format]( # noqa: E731
|
| 1437 |
+
a, swizzled=swizzled_scales, global_scale=act_global_scale
|
| 1438 |
+
)
|
| 1439 |
+
elif swizzled_scales:
|
| 1440 |
+
act_quant = lambda a: MX_ACT_QUANT[activation_format](a, swizzled=True) # noqa: E731
|
| 1441 |
+
else:
|
| 1442 |
+
act_quant = MX_ACT_QUANT[activation_format]
|
| 1443 |
+
# NVFP4 accumulator correction: the g_a·g_b product folded onto the fp32 accumulator
|
| 1444 |
+
input_global_scale = normalize_global_scale(b_global_scale, 1) # g_b
|
| 1445 |
+
out_global_scale = normalize_global_scale(output_global_scale, 1)
|
| 1446 |
+
# As given ⇒ A is already quantized (the routed-op parity: a pre-quantized activation + its
|
| 1447 |
+
# scales); else quantize raw A (offline above the M threshold, inline in the kernel below it).
|
| 1448 |
+
if As is not None:
|
| 1449 |
+
assert (As.dtype == torch.float8_e4m3fn) == (Bs.dtype == torch.float8_e4m3fn), (
|
| 1450 |
+
f"activation scales ({As.dtype}) must match the weight scale family ({Bs.dtype})"
|
| 1451 |
+
)
|
| 1452 |
+
assert swizzled_scales or As.ndim != 5, (
|
| 1453 |
+
"un-swizzled weights pair with affine (row-major) activation scales, got 5D As"
|
| 1454 |
+
)
|
| 1455 |
+
a_vals, as_scales = A.view(M, K), As
|
| 1456 |
+
# one SWIZZLED_SCALES flag governs both operands in-kernel: row-major activation
|
| 1457 |
+
# scales under a swizzled weight are swizzled here (the grouped wrapper does the same)
|
| 1458 |
+
if swizzled_scales and As.ndim == 2:
|
| 1459 |
+
as_scales = swizzle_mx_scales(As)
|
| 1460 |
+
else:
|
| 1461 |
+
a_vals, as_scales = maybe_act_quant(A.view(M, K), act_quant, MX_MATMUL_ACT_PREQUANT_MIN_M)
|
| 1462 |
+
A_q = e2m1_as_uint8(a_vals)
|
| 1463 |
+
as_u8 = ue8m0_as_uint8(as_scales)
|
| 1464 |
+
# the act-scale layout is carried by the buffer itself: 5D = swizzled (the offline arm under
|
| 1465 |
+
# a swizzled weight, or pre-swizzled As); the inline arm's raw activation stays 2D (affine)
|
| 1466 |
+
act_swizzled = as_u8.ndim == 5
|
| 1467 |
+
# gate|up output: the GLU intermediate [M, N] (output_dtype), or — under ``quantize_output`` —
|
| 1468 |
+
# requantized (mxfp8: fp8 + (M, N//32) uint8 Cs; fp4: packed (M, N//2) + (M, N//out_g) scale).
|
| 1469 |
+
output_format = activation_format if quantize_output and gate else None
|
| 1470 |
+
requant = output_format is not None
|
| 1471 |
+
out_g = 16 if output_format == "nvfp4" else 32
|
| 1472 |
+
cs_dtype = torch.float8_e4m3fn if output_format == "nvfp4" else torch.uint8
|
| 1473 |
+
# Swizzled in -> swizzled out: a swizzled block requants Cs straight into the down's
|
| 1474 |
+
# SWIZZLE_32_4_4 layout (CSDescriptor), so the fused down reads it on the fast path. 2D dense
|
| 1475 |
+
# output rows are contiguous (no scatter), so it's always swizzle-able. Format-general — the
|
| 1476 |
+
# swizzle is a byte-tiling over the N // out_g scale grid (mxfp8/mxfp4 group-32, nvfp4 group-16).
|
| 1477 |
+
swizzled_out = requant and swizzled_scales
|
| 1478 |
+
if output_format in ("mxfp4", "nvfp4"):
|
| 1479 |
+
C = A.new_empty(A.shape[:-1] + (N // 2,), dtype=torch.int8)
|
| 1480 |
+
elif output_format == "mxfp8":
|
| 1481 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=FP8_DTYPE)
|
| 1482 |
+
else:
|
| 1483 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1484 |
+
if swizzled_out:
|
| 1485 |
+
cb_cs = triton.cdiv(N // out_g, 4)
|
| 1486 |
+
cs_ret = torch.empty(1, triton.cdiv(M, 128), cb_cs, 2, 256, device=A.device, dtype=cs_dtype)
|
| 1487 |
+
CSDescriptor = TensorDescriptor.from_tensor(cs_ret, [1, 1, 1, 2, 256])
|
| 1488 |
+
Cs = None # row-major pointer unread here; CSDescriptor does the store
|
| 1489 |
+
elif requant:
|
| 1490 |
+
cs_ret = torch.empty(M, N // out_g, device=A.device, dtype=cs_dtype)
|
| 1491 |
+
Cs, CSDescriptor = cs_ret, None
|
| 1492 |
+
else:
|
| 1493 |
+
cs_ret, Cs, CSDescriptor = None, None, None # unread (no OUTPUT_FORMAT)
|
| 1494 |
+
stride_cs_m = cs_ret.stride(0) if (requant and not swizzled_out) else 0
|
| 1495 |
+
stride_cs_n = cs_ret.stride(1) if (requant and not swizzled_out) else 0
|
| 1496 |
+
# Host-TMA descriptors over the packed (M, K_bytes) / (N, K_bytes) matrices — placeholder
|
| 1497 |
+
# box rebound per tuned config by rebind_mx_descriptors. Read only by the descriptor
|
| 1498 |
+
# configs the tuner picks; pointer configs never touch them.
|
| 1499 |
+
a_descriptor = TensorDescriptor.from_tensor(A_q, [1, 32])
|
| 1500 |
+
b_descriptor = TensorDescriptor.from_tensor(b_u8, [1, 32])
|
| 1501 |
+
# One scale pointer per operand (as_u8/bs_u8) — the swizzled buffer on the fast path (also the
|
| 1502 |
+
# base for the BM<128 / BN<128 scalar gather), the affine scale otherwise — plus its descriptor,
|
| 1503 |
+
# built only on the swizzled path (dummy off the operand descriptor when un-swizzled, unread).
|
| 1504 |
+
as_descriptor = swizzled_scale_descriptor(as_u8) if act_swizzled else a_descriptor
|
| 1505 |
+
bs_descriptor = swizzled_scale_descriptor(bs_u8) if swizzled_scales else b_descriptor
|
| 1506 |
+
|
| 1507 |
+
def grid(META):
|
| 1508 |
+
return (
|
| 1509 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1510 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1511 |
+
)
|
| 1512 |
+
|
| 1513 |
+
with device_context(A.device):
|
| 1514 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1515 |
+
compile_time_only_triton_wrap(mx_dynamic_matmul_kernel)[grid](
|
| 1516 |
+
A_q,
|
| 1517 |
+
a_descriptor,
|
| 1518 |
+
as_u8,
|
| 1519 |
+
as_descriptor,
|
| 1520 |
+
b_u8,
|
| 1521 |
+
b_descriptor,
|
| 1522 |
+
bs_u8,
|
| 1523 |
+
bs_descriptor,
|
| 1524 |
+
C,
|
| 1525 |
+
Cs,
|
| 1526 |
+
bias,
|
| 1527 |
+
CSDescriptor,
|
| 1528 |
+
act_global_scale, # AsGlobal: g_a for the inline-quant arm (A/g_a)
|
| 1529 |
+
input_global_scale, # AsBsGlobal = g_b (acc; g_a folds in-kernel via AsGlobal)
|
| 1530 |
+
out_global_scale, # CsGlobal: requant output normalization (next proj's provided input_scale); None folds out
|
| 1531 |
+
M,
|
| 1532 |
+
N,
|
| 1533 |
+
K,
|
| 1534 |
+
int(
|
| 1535 |
+
M
|
| 1536 |
+
).bit_length(), # m_bit_length key bucket; int() concretizes M (a SymInt under torch.compile has no .bit_length)
|
| 1537 |
+
A_q.stride(0),
|
| 1538 |
+
A_q.stride(1),
|
| 1539 |
+
as_u8.stride(0), # As row stride (affine act-scale read; dead on the swizzled arm)
|
| 1540 |
+
as_u8.stride(-1), # As group-axis stride (1 when row-major; see stride_as_k)
|
| 1541 |
+
b_u8.stride(1),
|
| 1542 |
+
b_u8.stride(0),
|
| 1543 |
+
bs_u8.stride(1),
|
| 1544 |
+
bs_u8.stride(0),
|
| 1545 |
+
C.stride(-2),
|
| 1546 |
+
C.stride(-1),
|
| 1547 |
+
stride_cs_m,
|
| 1548 |
+
stride_cs_n,
|
| 1549 |
+
bias_stride_e,
|
| 1550 |
+
bias_stride_n,
|
| 1551 |
+
global_scale_stride(act_global_scale),
|
| 1552 |
+
global_scale_stride(input_global_scale),
|
| 1553 |
+
global_scale_stride(out_global_scale),
|
| 1554 |
+
SCALE_GROUP_K=scale_group,
|
| 1555 |
+
ACTIVATION_FORMAT=activation_format,
|
| 1556 |
+
SWIZZLED_SCALES=swizzled_scales,
|
| 1557 |
+
SWIZZLED_OUT=swizzled_out,
|
| 1558 |
+
GATE=gate,
|
| 1559 |
+
ACT_FN=act_fn,
|
| 1560 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1561 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1562 |
+
OUTPUT_FORMAT=output_format,
|
| 1563 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1564 |
+
INTERMEDIATE_DTYPE=tl_dtype(output_dtype),
|
| 1565 |
+
)
|
| 1566 |
+
# NVFP4's block scales are already normalized by the provided output global (CsGlobal =
|
| 1567 |
+
# output_global_scale) at requant, so the caller pairs cs_ret with that global as the down's As.
|
| 1568 |
+
return [C, cs_ret] if cs_ret is not None else [C]
|
| 1569 |
+
|
| 1570 |
+
|
| 1571 |
+
@compile_time_only_triton_op(
|
| 1572 |
+
add_op_namespace_prefix("full_precision_matmul_2d"), mutates_args=(), opaque=True
|
| 1573 |
+
)
|
| 1574 |
+
def full_precision_matmul_2d(
|
| 1575 |
+
A: torch.Tensor,
|
| 1576 |
+
B: torch.Tensor,
|
| 1577 |
+
output_dtype: torch.dtype | None = None,
|
| 1578 |
+
gate: bool = False,
|
| 1579 |
+
act_fn: str = "silu",
|
| 1580 |
+
swiglu_alpha: float | None = None,
|
| 1581 |
+
swiglu_limit: float | None = None,
|
| 1582 |
+
simulate_unfused: bool = False,
|
| 1583 |
+
bias: torch.Tensor | None = None,
|
| 1584 |
+
) -> list[torch.Tensor]:
|
| 1585 |
+
"""Full-precision (BF16/FP16) 2D matmul ``C = A @ B.T``; no quantization anywhere. ``gate``
|
| 1586 |
+
fuses the ``(2N, K)`` gate|up projection into one stacked GEMM + SwiGLU, returning the
|
| 1587 |
+
``[..., N]`` GLU intermediate. Returns a one-element list (mirrors the quantized 2D ops)."""
|
| 1588 |
+
validate_dense_2d_operands(A, B)
|
| 1589 |
+
assert A.dtype == B.dtype and A.dtype in (torch.bfloat16, torch.float16), (
|
| 1590 |
+
f"full-precision path needs matching BF16/FP16 A and B, got {A.dtype} / {B.dtype}"
|
| 1591 |
+
)
|
| 1592 |
+
rows, K = B.shape
|
| 1593 |
+
# Under gate|up fusion B is the (2N, K) gate|up stack; N is the per-projection output width.
|
| 1594 |
+
N = rows // 2 if gate else rows
|
| 1595 |
+
M = A.numel() // A.shape[-1]
|
| 1596 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1597 |
+
|
| 1598 |
+
def grid(META):
|
| 1599 |
+
return (
|
| 1600 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1601 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1602 |
+
)
|
| 1603 |
+
|
| 1604 |
+
with device_context(A.device):
|
| 1605 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1606 |
+
compile_time_only_triton_wrap(full_precision_matmul_2d_kernel)[grid](
|
| 1607 |
+
A,
|
| 1608 |
+
B,
|
| 1609 |
+
C,
|
| 1610 |
+
bias,
|
| 1611 |
+
M,
|
| 1612 |
+
N,
|
| 1613 |
+
K,
|
| 1614 |
+
int(M).bit_length(), # m_bit_length key bucket
|
| 1615 |
+
A.stride(-2),
|
| 1616 |
+
A.stride(-1),
|
| 1617 |
+
B.stride(1),
|
| 1618 |
+
B.stride(0),
|
| 1619 |
+
C.stride(-2),
|
| 1620 |
+
C.stride(-1),
|
| 1621 |
+
bias_stride_e,
|
| 1622 |
+
bias_stride_n,
|
| 1623 |
+
GATE=gate,
|
| 1624 |
+
ACT_FN=act_fn,
|
| 1625 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1626 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1627 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1628 |
+
INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
|
| 1629 |
+
)
|
| 1630 |
+
|
| 1631 |
+
return [C]
|
| 1632 |
+
|
| 1633 |
+
|
| 1634 |
+
@compile_time_only_triton_op(
|
| 1635 |
+
add_op_namespace_prefix("mx_weight_only_matmul_2d"),
|
| 1636 |
+
mutates_args=(),
|
| 1637 |
+
opaque=True,
|
| 1638 |
+
)
|
| 1639 |
+
def mx_weight_only_matmul_2d(
|
| 1640 |
+
A: torch.Tensor,
|
| 1641 |
+
B: torch.Tensor,
|
| 1642 |
+
Bs: torch.Tensor,
|
| 1643 |
+
output_dtype: torch.dtype | None = None,
|
| 1644 |
+
gate: bool = False,
|
| 1645 |
+
act_fn: str = "silu",
|
| 1646 |
+
swiglu_alpha: float | None = None,
|
| 1647 |
+
swiglu_limit: float | None = None,
|
| 1648 |
+
simulate_unfused: bool = False,
|
| 1649 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1650 |
+
bias: torch.Tensor | None = None,
|
| 1651 |
+
) -> list[torch.Tensor]:
|
| 1652 |
+
"""weight-only dense matmul ``C = A @ B.T``: a RAW bf16/fp16 activation (no quant) against an
|
| 1653 |
+
MXFP4/NVFP4/MXFP8 weight upcast to bf16 in-loop (NVFP4's per-tensor fp32 ``b_global_scale``
|
| 1654 |
+
recovers on the accumulator; ``None`` = single-level) — the ``matmul_ogs`` format (fp4/fp8 weight bytes in
|
| 1655 |
+
memory, bf16 acts, weight upcast in-MMA). Affine (2D) weight scales only. ``gate`` fuses the
|
| 1656 |
+
``(2N, K)`` gate|up projection + SwiGLU, returning the ``[..., N]`` GLU intermediate. Returns a
|
| 1657 |
+
one-element list (mirrors the other 2D ops). Packed-E2M1 K (B is (rows, K//2)) is handled
|
| 1658 |
+
below, so this op does its own shape checks (not ``validate_dense_2d_operands``)."""
|
| 1659 |
+
assert A.dtype in (torch.bfloat16, torch.float16, torch.float32), (
|
| 1660 |
+
f"weight-only takes a raw bf16/fp16/fp32 activation, got {A.dtype}"
|
| 1661 |
+
)
|
| 1662 |
+
assert Bs.ndim == 2, f"weight-only takes affine (2D) weight scales, got ndim={Bs.ndim}"
|
| 1663 |
+
WEIGHT_VALUES_PER_BYTE = 2 if B.dtype == torch.int8 else 1
|
| 1664 |
+
rows, K_b = B.shape
|
| 1665 |
+
K = A.shape[-1]
|
| 1666 |
+
M = A.numel() // K
|
| 1667 |
+
N = rows // 2 if gate else rows
|
| 1668 |
+
assert K == WEIGHT_VALUES_PER_BYTE * K_b, (
|
| 1669 |
+
f"K (={K}) must equal {WEIGHT_VALUES_PER_BYTE} * B.shape[1] (={K_b})"
|
| 1670 |
+
)
|
| 1671 |
+
scale_group = mx_scale_family(Bs, K)
|
| 1672 |
+
assert Bs.shape == (rows, K // scale_group), (
|
| 1673 |
+
f"Bs shape {tuple(Bs.shape)} != ({rows}, {K // scale_group})"
|
| 1674 |
+
)
|
| 1675 |
+
assert A.is_contiguous(), "A must be contiguous (the host-TMA descriptor arm reads its box)"
|
| 1676 |
+
b_u8 = e2m1_as_uint8(B)
|
| 1677 |
+
bs_u8 = ue8m0_as_uint8(Bs)
|
| 1678 |
+
b_global_scale = normalize_global_scale(b_global_scale, 1)
|
| 1679 |
+
C = A.new_empty(A.shape[:-1] + (N,), dtype=output_dtype)
|
| 1680 |
+
# Host-TMA descriptors over the (M, K) bf16 A and packed (N, K_bytes) weight — placeholder box
|
| 1681 |
+
# rebound per tuned config by rebind_weight_only_descriptors, read only by the descriptor configs the
|
| 1682 |
+
# tuner picks (pointer configs never touch them). Scales stay affine (no scale descriptor).
|
| 1683 |
+
a_descriptor = TensorDescriptor.from_tensor(A.view(M, K), [1, 32])
|
| 1684 |
+
b_descriptor = TensorDescriptor.from_tensor(b_u8, [1, 32])
|
| 1685 |
+
|
| 1686 |
+
def grid(META):
|
| 1687 |
+
return (
|
| 1688 |
+
triton.cdiv(M, META["BLOCK_SIZE_M"]),
|
| 1689 |
+
triton.cdiv(N, META["BLOCK_SIZE_N"]),
|
| 1690 |
+
)
|
| 1691 |
+
|
| 1692 |
+
with device_context(A.device):
|
| 1693 |
+
bias_stride_e, bias_stride_n = bias_strides(bias)
|
| 1694 |
+
compile_time_only_triton_wrap(mx_weight_only_matmul_2d_kernel)[grid](
|
| 1695 |
+
A,
|
| 1696 |
+
a_descriptor,
|
| 1697 |
+
b_u8,
|
| 1698 |
+
b_descriptor,
|
| 1699 |
+
bs_u8,
|
| 1700 |
+
b_global_scale,
|
| 1701 |
+
C,
|
| 1702 |
+
bias,
|
| 1703 |
+
M,
|
| 1704 |
+
N,
|
| 1705 |
+
K,
|
| 1706 |
+
int(M).bit_length(),
|
| 1707 |
+
A.stride(-2),
|
| 1708 |
+
A.stride(-1),
|
| 1709 |
+
b_u8.stride(0),
|
| 1710 |
+
b_u8.stride(1),
|
| 1711 |
+
bs_u8.stride(0),
|
| 1712 |
+
bs_u8.stride(1),
|
| 1713 |
+
C.stride(-2),
|
| 1714 |
+
C.stride(-1),
|
| 1715 |
+
bias_stride_e,
|
| 1716 |
+
bias_stride_n,
|
| 1717 |
+
global_scale_stride(b_global_scale),
|
| 1718 |
+
SCALE_GROUP_K=scale_group,
|
| 1719 |
+
WEIGHT_VALUES_PER_BYTE=WEIGHT_VALUES_PER_BYTE,
|
| 1720 |
+
GATE=gate,
|
| 1721 |
+
ACT_FN=act_fn,
|
| 1722 |
+
SWIGLU_ALPHA=swiglu_alpha,
|
| 1723 |
+
SWIGLU_LIMIT=swiglu_limit,
|
| 1724 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 1725 |
+
INTERMEDIATE_DTYPE=tl_dtype(resolve_output_dtype(output_dtype, A, None)),
|
| 1726 |
+
)
|
| 1727 |
+
|
| 1728 |
+
return [C]
|
| 1729 |
+
|
| 1730 |
+
|
| 1731 |
+
# ── torch scaled_mm fast path (2D dense MX, Blackwell) ────────────────────────
|
| 1732 |
+
#
|
| 1733 |
+
# cuBLAS's block-scaled GEMM beats our Triton 2D kernel at EVERY measured M on the
|
| 1734 |
+
# quantized-activation MX formats (B200, N=18432 K=6144, 200-trial tunes, 2026-08-27):
|
| 1735 |
+
# mxfp8 M=16 3.42x M=256 3.17x M=1024 1.57x M=4096 1.55x M=8192 1.42x
|
| 1736 |
+
# nvfp4 M=16 4.83x M=256 4.15x M=1024 2.48x M=4096 1.81x M=8192 1.80x
|
| 1737 |
+
# and its bf16 output is bit-identical to ours at every checked shape (same offline act
|
| 1738 |
+
# quant feeds both, fp32 accumulate). Only the M=1 swap-AB decode dispatch stays ahead
|
| 1739 |
+
# (18.4us vs ~33), so the route floor is M >= 2.
|
| 1740 |
+
#
|
| 1741 |
+
# Inductor refuses to lower scaled_mm with SWIZZLE_32_4_4 scales ("does not yet support
|
| 1742 |
+
# non-trivial swizzles" — repros/scaled_mm_swizzle_compile.py), so the call lives behind an
|
| 1743 |
+
# OPAQUE custom op: dynamo captures the op as a leaf and the real scaled_mm runs at runtime,
|
| 1744 |
+
# which serves compiled callers too. F.scaled_mm has no autograd formula while our ops carry
|
| 1745 |
+
# registered dgrads, so differentiating callers fall through to the Triton ops. CUDA-graph
|
| 1746 |
+
# capture is fine (measured under do_bench_cudagraph).
|
| 1747 |
+
|
| 1748 |
+
_SCALED_MM_MIN_M = int(os.environ.get("FINEGRAINED_SCALED_MM_MIN_M", "2"))
|
| 1749 |
+
|
| 1750 |
+
|
| 1751 |
+
@torch.library.custom_op(add_op_namespace_prefix("scaled_mm_2d_mx"), mutates_args=())
|
| 1752 |
+
def _scaled_mm_2d_op(
|
| 1753 |
+
Aq: torch.Tensor,
|
| 1754 |
+
As: torch.Tensor,
|
| 1755 |
+
B: torch.Tensor,
|
| 1756 |
+
Bs: torch.Tensor,
|
| 1757 |
+
g_a: torch.Tensor | None,
|
| 1758 |
+
g_b: torch.Tensor | None,
|
| 1759 |
+
fmt: str,
|
| 1760 |
+
) -> torch.Tensor:
|
| 1761 |
+
"""``F.scaled_mm`` behind an OPAQUE custom op. Inductor cannot lower scaled_mm with
|
| 1762 |
+
SWIZZLE_32_4_4 scales (repros/scaled_mm_swizzle_compile.py), but it never has to: dynamo
|
| 1763 |
+
captures this op as a leaf and the real call runs at runtime — so compiled callers get the
|
| 1764 |
+
cuBLAS fast path too, not just eager ones."""
|
| 1765 |
+
F = torch.nn.functional
|
| 1766 |
+
ST, SW = F.ScalingType, F.SwizzleType
|
| 1767 |
+
if fmt == "mxfp8":
|
| 1768 |
+
return F.scaled_mm(
|
| 1769 |
+
Aq, B.t(),
|
| 1770 |
+
As.view(torch.float8_e8m0fnu), ST.BlockWise1x32,
|
| 1771 |
+
Bs.view(torch.float8_e8m0fnu), ST.BlockWise1x32,
|
| 1772 |
+
swizzle_a=SW.SWIZZLE_32_4_4, swizzle_b=SW.SWIZZLE_32_4_4,
|
| 1773 |
+
output_dtype=torch.bfloat16,
|
| 1774 |
+
)
|
| 1775 |
+
one = torch.ones((), device=Aq.device, dtype=torch.float32)
|
| 1776 |
+
FP4 = torch.float4_e2m1fn_x2
|
| 1777 |
+
return F.scaled_mm(
|
| 1778 |
+
Aq.view(FP4), B.view(FP4).t(),
|
| 1779 |
+
[As.view(torch.float8_e4m3fn), one if g_a is None else g_a.reshape(()).float()],
|
| 1780 |
+
[ST.BlockWise1x16, ST.TensorWise],
|
| 1781 |
+
[Bs.view(torch.float8_e4m3fn), one if g_b is None else g_b.reshape(()).float()],
|
| 1782 |
+
[ST.BlockWise1x16, ST.TensorWise],
|
| 1783 |
+
swizzle_a=[SW.SWIZZLE_32_4_4, SW.NO_SWIZZLE],
|
| 1784 |
+
swizzle_b=[SW.SWIZZLE_32_4_4, SW.NO_SWIZZLE],
|
| 1785 |
+
output_dtype=torch.bfloat16,
|
| 1786 |
+
)
|
| 1787 |
+
|
| 1788 |
+
|
| 1789 |
+
@_scaled_mm_2d_op.register_fake
|
| 1790 |
+
def _(Aq, As, B, Bs, g_a, g_b, fmt):
|
| 1791 |
+
return Aq.new_empty(Aq.shape[0], B.shape[0], dtype=torch.bfloat16)
|
| 1792 |
+
|
| 1793 |
+
|
| 1794 |
+
def _smm_reject(reason):
|
| 1795 |
+
if os.environ.get("FINEGRAINED_SCALED_MM_DEBUG"):
|
| 1796 |
+
print(f"[scaled_mm route] rejected: {reason}", flush=True)
|
| 1797 |
+
return None
|
| 1798 |
+
|
| 1799 |
+
|
| 1800 |
+
def _torch_scaled_mm_2d(A, B, As, Bs, activation_format, a_global_scale, b_global_scale, out_dtype):
|
| 1801 |
+
"""The scaled_mm route, or ``None`` to take the Triton ops. Fires only where measured to
|
| 1802 |
+
win AND where the semantics are identical: plain ungated GEMM, quantized activations in
|
| 1803 |
+
the WEIGHT's fmt, SWIZZLE_32_4_4 weight scales (Blackwell scaled_mm rejects row-major
|
| 1804 |
+
outright), bf16 out, eager, no grad."""
|
| 1805 |
+
if os.environ.get("FINEGRAINED_DISABLE_SCALED_MM"):
|
| 1806 |
+
return _smm_reject("env-disabled")
|
| 1807 |
+
if not is_sm10x():
|
| 1808 |
+
return _smm_reject("not-sm10x")
|
| 1809 |
+
if not (hasattr(torch.nn.functional, "scaled_mm") and hasattr(torch.nn.functional, "ScalingType")):
|
| 1810 |
+
return _smm_reject("no-F.scaled_mm")
|
| 1811 |
+
fmt = weight_format(B, Bs)
|
| 1812 |
+
if fmt not in ("mxfp8", "nvfp4") or activation_format == "bf16":
|
| 1813 |
+
return _smm_reject("format/activation_format")
|
| 1814 |
+
if (a_global_scale is not None and a_global_scale.numel() > 1) or (
|
| 1815 |
+
b_global_scale is not None and b_global_scale.numel() > 1
|
| 1816 |
+
):
|
| 1817 |
+
# scaled_mm's second level is one TensorWise value per operand, so a gate|up stack whose
|
| 1818 |
+
# halves carry their own global has no form here — the Triton kernels fold it by column
|
| 1819 |
+
return _smm_reject("multi-valued global")
|
| 1820 |
+
# None follows the weights; resolve it before comparing (the bench and the integrations pass it)
|
| 1821 |
+
if resolve_activation_format(activation_format, B, Bs) != fmt:
|
| 1822 |
+
return _smm_reject("format-mismatch-after-resolve")
|
| 1823 |
+
if Bs.ndim != 5: # affine (row-major) scales: scaled_mm rejects them on Blackwell
|
| 1824 |
+
return _smm_reject("affine-Bs")
|
| 1825 |
+
if out_dtype is not torch.bfloat16:
|
| 1826 |
+
return _smm_reject("out-dtype")
|
| 1827 |
+
M = A.numel() // A.shape[-1]
|
| 1828 |
+
group = 32 if fmt == "mxfp8" else 16
|
| 1829 |
+
K = A.shape[-1] if As is None else (A.shape[-1] * (2 if fmt == "nvfp4" else 1))
|
| 1830 |
+
if M < _SCALED_MM_MIN_M or K % group or B.shape[0] % 16:
|
| 1831 |
+
return _smm_reject("shape/minM")
|
| 1832 |
+
if As is not None and As.ndim != 5:
|
| 1833 |
+
return _smm_reject("affine-As") # pre-quantized activations must arrive with swizzled scales too
|
| 1834 |
+
if fmt == "mxfp8":
|
| 1835 |
+
Aq = A.reshape(M, K) if As is not None else None
|
| 1836 |
+
if Aq is None:
|
| 1837 |
+
Aq, As = mxfp8_act_quant(A.reshape(M, K), swizzled=True)
|
| 1838 |
+
else:
|
| 1839 |
+
Aq = A.reshape(M, K // 2) if As is not None else None
|
| 1840 |
+
if Aq is None:
|
| 1841 |
+
Aq, As = nvfp4_act_quant(A.reshape(M, K), swizzled=True, global_scale=a_global_scale)[:2]
|
| 1842 |
+
out = _scaled_mm_2d_op(Aq, As, B, Bs, a_global_scale, b_global_scale, fmt)
|
| 1843 |
+
return out.reshape(*A.shape[:-1], B.shape[0])
|
| 1844 |
+
|
| 1845 |
+
|
| 1846 |
+
def matmul_2d(
|
| 1847 |
+
A: torch.Tensor,
|
| 1848 |
+
B: torch.Tensor,
|
| 1849 |
+
As: torch.Tensor | None = None,
|
| 1850 |
+
Bs: torch.Tensor | None = None,
|
| 1851 |
+
*,
|
| 1852 |
+
bias: torch.Tensor | None = None, # (E, N_out) per-expert output bias; (N_out,) for 2D
|
| 1853 |
+
activation_format: str | None = None,
|
| 1854 |
+
gate: bool = False,
|
| 1855 |
+
act_fn: str = "silu",
|
| 1856 |
+
swiglu_alpha: float | None = None,
|
| 1857 |
+
swiglu_limit: float | None = None,
|
| 1858 |
+
quantize_output: bool = False,
|
| 1859 |
+
simulate_unfused: bool = False,
|
| 1860 |
+
output_dtype: torch.dtype | None = None,
|
| 1861 |
+
a_global_scale: torch.Tensor | None = None,
|
| 1862 |
+
b_global_scale: torch.Tensor | None = None,
|
| 1863 |
+
output_global_scale: torch.Tensor | None = None,
|
| 1864 |
+
) -> torch.Tensor | list[torch.Tensor]:
|
| 1865 |
+
"""Dense (2D) quantized matmul dispatcher (W8A8 FP8, W4A8/W4A4 FP4) — the single-GEMM sibling of
|
| 1866 |
+
``matmul_grouped``/``matmul_batched``, taking the same ``As``/``Bs`` scale spec,
|
| 1867 |
+
``activation_format`` / gate|up fusion kwargs, and inferred quant block (no ``block_size`` argument — it
|
| 1868 |
+
falls out of the weight-scale shape, so the data can't disagree with a parameter).
|
| 1869 |
+
|
| 1870 |
+
``As`` is the activation block-scale spec, same as the routed ops: ``None`` → the op quantizes raw
|
| 1871 |
+
``A`` dynamically; a per-tensor scalar → static (calibrated) FP8 activation scale; block scales →
|
| 1872 |
+
pre-quantized ``A``. ``Bs`` is the weight block scale. ``a_global_scale``/``b_global_scale`` are the
|
| 1873 |
+
two-level NVFP4 per-tensor second-level scales (calibrated ``input_scale`` ``g_a`` and
|
| 1874 |
+
``weight_scale_2`` ``g_b``) — provided, never computed; the op folds ``g_a·g_b`` onto the
|
| 1875 |
+
accumulator (``a_global_scale`` rides raw ``A`` or a pre-quantized ``As`` alike).
|
| 1876 |
+
``output_global_scale`` (NVFP4 requant only) is the NEXT proj's provided ``input_scale``:
|
| 1877 |
+
the fused requant normalizes the GLU intermediate by it, returning ``(C, Cs)`` the down-proj
|
| 1878 |
+
consumes as ``As=Cs, a_global_scale=output_global_scale``.
|
| 1879 |
+
|
| 1880 |
+
``activation_format`` is the activations' quantization: ``None`` (default) = the weights' own
|
| 1881 |
+
format, ``"bf16"`` = weight-only (no activation quant), or an explicit ``"fp8"`` / ``"mxfp8"`` /
|
| 1882 |
+
``"mxfp4"`` / ``"nvfp4"`` (``"mxfp8"`` on MXFP4 weights = the W4A8 chain; ``check_activation_format``
|
| 1883 |
+
has the per-family matrix). ``gate=True`` reads ``B`` as the ``(2N, K)`` gate|up stack and fuses
|
| 1884 |
+
the ``act_fn`` GLU (``swiglu_alpha``/``swiglu_limit`` its clamped variant), returning the
|
| 1885 |
+
``[..., N]`` intermediate; ``quantize_output=True`` (MX weights only) stores that intermediate in
|
| 1886 |
+
``activation_format`` and returns ``(C, Cs)`` for the next op.
|
| 1887 |
+
|
| 1888 |
+
Routes by weight dtype + inferred block: ``Bs`` None → full-precision; MX/NVFP4 (int8/E4M3 weights
|
| 1889 |
+
with UE8M0 group-32 or E4M3 group-16 ``Bs``) → ``mx_dynamic_matmul``; tensor-wide FP8 (scalar
|
| 1890 |
+
``Bs``) → ``w8a8_tensor_dynamic_fp8_matmul``; block FP8 → the static variant when ``As`` is a
|
| 1891 |
+
per-tensor scalar, else dynamic.
|
| 1892 |
+
"""
|
| 1893 |
+
check_activation_format(activation_format, quantize_output)
|
| 1894 |
+
assert (a_global_scale is None and b_global_scale is None) or (
|
| 1895 |
+
Bs is not None and weight_format(B, Bs) == "nvfp4"
|
| 1896 |
+
), "two-level globals (a_global_scale / b_global_scale) are NVFP4-only"
|
| 1897 |
+
assert output_global_scale is None or (
|
| 1898 |
+
Bs is not None and quantize_output and resolve_activation_format(activation_format, B, Bs) == "nvfp4"
|
| 1899 |
+
), (
|
| 1900 |
+
"output_global_scale is the NVFP4 requant second level — it requires quantize_output=True on NVFP4 "
|
| 1901 |
+
"(the epilogue would otherwise normalize by it with nothing downstream to compensate)"
|
| 1902 |
+
)
|
| 1903 |
+
|
| 1904 |
+
def _unwrap(ret: list[torch.Tensor]) -> torch.Tensor | tuple[torch.Tensor, ...]:
|
| 1905 |
+
# tuple, matching the routed siblings — callers branch on isinstance(out, tuple)
|
| 1906 |
+
return ret[0] if len(ret) == 1 else tuple(ret)
|
| 1907 |
+
|
| 1908 |
+
if Bs is None: # unquantized BF16/FP16 weights — plain dot, no scales
|
| 1909 |
+
assert As is None and a_global_scale is None and activation_format in (None, "bf16") and not quantize_output, (
|
| 1910 |
+
"the full-precision path (Bs=None) takes no activation scale or quantized activation format"
|
| 1911 |
+
)
|
| 1912 |
+
return _unwrap(
|
| 1913 |
+
full_precision_matmul_2d(
|
| 1914 |
+
A, B, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
|
| 1915 |
+
)
|
| 1916 |
+
|
| 1917 |
+
if is_mx(B, Bs):
|
| 1918 |
+
if activation_format == "bf16": # weight-only: raw bf16 activation, MX weight upcast in-MMA
|
| 1919 |
+
assert As is None and a_global_scale is None and not quantize_output, (
|
| 1920 |
+
"weight-only (activation_format='bf16') takes a raw bf16 activation, no As/global/requant"
|
| 1921 |
+
)
|
| 1922 |
+
assert output_global_scale is None, (
|
| 1923 |
+
"weight-only has no requant epilogue for output_global_scale to normalize"
|
| 1924 |
+
)
|
| 1925 |
+
return _unwrap(
|
| 1926 |
+
mx_weight_only_matmul_2d(
|
| 1927 |
+
A, B, Bs, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit,
|
| 1928 |
+
simulate_unfused, b_global_scale=b_global_scale, bias=bias)
|
| 1929 |
+
)
|
| 1930 |
+
return _unwrap(
|
| 1931 |
+
mx_dynamic_matmul(
|
| 1932 |
+
A, B, As, Bs, output_dtype, activation_format,
|
| 1933 |
+
gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, quantize_output,
|
| 1934 |
+
a_global_scale, b_global_scale, output_global_scale, bias=bias)
|
| 1935 |
+
)
|
| 1936 |
+
# FP8 activations are always E4M3 with the weight-implied scale granularity, so "fp8" is a
|
| 1937 |
+
# no-op format name (accepted for symmetry with the MoE ops); no other name applies here.
|
| 1938 |
+
assert activation_format in (None, "fp8"), (
|
| 1939 |
+
f"FP8-weight activations are E4M3 ('fp8'), got {activation_format!r}"
|
| 1940 |
+
)
|
| 1941 |
+
assert not quantize_output, "quantize_output (fused-requant gate|up output) is MX-only"
|
| 1942 |
+
# Infer the FP8 quant block from the weight-scale shape (scalar Bs = tensor-wide). The contraction
|
| 1943 |
+
# dim K tiles evenly, so block_k is exact; N may be non-aligned (a partial last block, so
|
| 1944 |
+
# N % Bs.shape[0] != 0 and plain division would under-count) — recover block_n from the even dim,
|
| 1945 |
+
# the square-block convention for these FP8 weights.
|
| 1946 |
+
if Bs.numel() == 1:
|
| 1947 |
+
block_size = None
|
| 1948 |
+
else:
|
| 1949 |
+
block_k = B.shape[1] // Bs.shape[1]
|
| 1950 |
+
block_n = B.shape[0] // Bs.shape[0] if B.shape[0] % Bs.shape[0] == 0 else block_k
|
| 1951 |
+
block_size = [block_n, block_k]
|
| 1952 |
+
if block_size is None: # tensor-wide (per-tensor) scale
|
| 1953 |
+
assert As is None, "tensor-wide FP8 quantizes A dynamically — no As"
|
| 1954 |
+
return _unwrap(
|
| 1955 |
+
w8a8_tensor_dynamic_fp8_matmul(
|
| 1956 |
+
A, B, Bs, output_dtype, gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
|
| 1957 |
+
)
|
| 1958 |
+
# Block-wise FP8: a per-tensor scalar As is the static (calibrated) activation scale; else dynamic.
|
| 1959 |
+
if As is not None:
|
| 1960 |
+
assert As.numel() == 1, "block-wise FP8 As is a per-tensor static scale (scalar)"
|
| 1961 |
+
return _unwrap(
|
| 1962 |
+
w8a8_block_static_fp8_matmul(
|
| 1963 |
+
A, B, As, Bs, block_size, output_dtype,
|
| 1964 |
+
gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
|
| 1965 |
+
)
|
| 1966 |
+
return _unwrap(
|
| 1967 |
+
w8a8_block_dynamic_fp8_matmul(
|
| 1968 |
+
A, B, Bs, block_size, output_dtype,
|
| 1969 |
+
gate, act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, bias=bias)
|
| 1970 |
+
)
|
build/torch-xpu/metadata.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "finegrained-kernels",
|
| 3 |
+
"id": "_finegrained_kernels_xpu_1348465",
|
| 4 |
+
"version": 0,
|
| 5 |
+
"license": "Apache-2.0",
|
| 6 |
+
"python-depends": [],
|
| 7 |
+
"backend": {
|
| 8 |
+
"type": "xpu"
|
| 9 |
+
},
|
| 10 |
+
"digest": {
|
| 11 |
+
"algorithm": "sha256",
|
| 12 |
+
"files": {
|
| 13 |
+
"__init__.py": "43nBPHxybam5ChWZmsz1Ji+pjJ6K6zF5lzwbqPcMwE4=",
|
| 14 |
+
"_ops.py": "Ssu+B/vlZZOXRh+hKJNWiv3V59Z7J6Wf0w/+9c7uT+M=",
|
| 15 |
+
"backward.py": "ri9lKPhb9SmkYBvUS8KvuORiW5PcKViNamFNVWxb69g=",
|
| 16 |
+
"batched.py": "cSkV7Mnv6kK+k5MANm8f6ZFu3Jdbh0tAz9u9t4e+R3c=",
|
| 17 |
+
"bayesian_autotuner.py": "DPpIATTls8aY7n12oKggpuAOvC3RlugYl1Wid1t5J+U=",
|
| 18 |
+
"compat.py": "yj7b7kCAOrq3j1Y3WxSb/YKoVcPRFfkeXmMj7dkqQoc=",
|
| 19 |
+
"descriptors.py": "4acRMazGPFlbydNt1Hbo8RpAqb3fOtnOQBv5yb4onqw=",
|
| 20 |
+
"epilogue.py": "tGALTI/VMgO3Mz0sQ6H6wuRZpO4UKqM1/4h7d81SnVM=",
|
| 21 |
+
"finegrained_kernels/__init__.py": "DFYPlrhXwYjEqCl/8n0SmWGZV8NFml5DPhMjKfv98GY=",
|
| 22 |
+
"formats.py": "V6F0PXdNhXQn/UBz0/frbvcp5Lgterh053XDrvQ/Cpw=",
|
| 23 |
+
"grouped.py": "PXhZU9Rn6UIIiQtR6IUmI060Mank5GMuooTWapRvya8=",
|
| 24 |
+
"loading/__init__.py": "so7kYmlVoiASfoOzemdU5fBTiP8yA85gVPNQExIq/KU=",
|
| 25 |
+
"loading/scales.py": "8HJpflmImEXYJquA6hVLStUUEecvExAxBEYVb3qPPWE=",
|
| 26 |
+
"loading/tiles.py": "hvFIDikI1k0+vV1QOjqdl1bovI/JEElfNwLWg7VC3ZM=",
|
| 27 |
+
"matmul.py": "qLV1KsPyCeAq3O0swnk38GUOT+lS12WxUM8bQZ9DF9A=",
|
| 28 |
+
"mma.py": "1fg2an7Dc8Vd6qpSmq+Q0781A6sNilOJHU0PqEbouT8=",
|
| 29 |
+
"moe.py": "rpiYMM+kzTtNuxXFUrQARLO9kLHcEgKR8rs/dZ0wn/g=",
|
| 30 |
+
"norm.py": "VvE6GUE0Oztf7PDcviLCcB4fhnXt7/zgdE+aApv6DBM=",
|
| 31 |
+
"pruners.py": "v03bXbvsZp1znVIj/eiR7LAWctU66DJlJr51wn8Pheg=",
|
| 32 |
+
"quant.py": "PZhzgOHSDH8lLV6zvrv040B4g9SgFE6Eq6bc8XxfPY8=",
|
| 33 |
+
"scheduling.py": "UGgQ5uVdm4cWPnH+4w6Ffou/PSX3iUarMeRo2E04BhY=",
|
| 34 |
+
"swizzle.py": "ueIJ9FsO8QZhVt3nMFlA+8b+U9/ZHabHFlhufsONV04="
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"provenance": {
|
| 38 |
+
"kernel-builder": {
|
| 39 |
+
"version": "0.17.0-dev0",
|
| 40 |
+
"sha": "81580bb92577f2f7228661ca2a221fc052375709",
|
| 41 |
+
"dirty": false
|
| 42 |
+
},
|
| 43 |
+
"kernel": {
|
| 44 |
+
"sha": "1348465af00f2d4c596d37bdb262ae0875dc1d77",
|
| 45 |
+
"dirty": false
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
build/torch-xpu/metadata.json.sigstore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"certificate":{"rawBytes":"MIIHTDCCBtGgAwIBAgIUM4GNfSYroYkWYDDZXzHK00oLmRMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwOTE2MTU0MTUwWhcNMjYwOTE2MTU1MTUwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEG+lYD9k1Ls9GcZaDLrpBm752JZ41WJCjRAztY9QiUh8kXjTIKy9U2w7CdIPlTSHV0qKMJDGwRgUa8ozijaUY66OCBfAwggXsMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUe/Jio6pzoqhrBsr+1PFohQ8fbm4wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wawYDVR0RAQH/BGEwX4ZdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzATBgorBgEEAYO/MAEEBAVCdWlsZDArBgorBgEEAYO/MAEFBB1odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eTAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMG0GCisGAQQBg78wAQkEXwxdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzAbBgorBgEEAYO/MAELBA0MC3NlbGYtaG9zdGVkMEAGCisGAQQBg78wAQwEMgwwaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5MDgGCisGAQQBg78wAQ0EKgwoMTM0ODQ2NWFmMDBmMmQ0YzU5NmQzN2JkYjI2MmFlMDg3NWRjMWQ3NzAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAaBgorBgEEAYO/MAEPBAwMCjEwNzE0NzU1MjkwLgYKKwYBBAGDvzABEAQgDB5odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2UwGAYKKwYBBAGDvzABEQQKDAgyNTcyMDc0MzBtBgorBgEEAYO/MAESBF8MXWh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS8uZ2l0aHViL3dvcmtmbG93cy9idWlsZC55YW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKDEzNDg0NjVhZjAwZjJkNGM1OTZkMzdiZGIyNjJhZTA4NzVkYzFkNzcwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBkBgorBgEEAYO/MAEVBFYMVGh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS9hY3Rpb25zL3J1bnMvMzUxMTY2NDkyOTcvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBGBgorBgEEAYO/MAEYBDgMNnJlcG86aHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHk6cmVmOnJlZnMvaGVhZHMvbWFpbjCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABoKrhpzcAAAQDAEcwRQIgGyrPrsAfMYmscB12UFFf7kD+1bipafRM//aAuvV2dFQCIQCgGob1Yl+HitxgyIHlQl6r2GlwZ2yNGS+HX4s03uTZAjAKBggqhkjOPQQDAwNpADBmAjEAvDoqq7foV1gKiMKW3gGJPJsHLC41/I6991rf3JNNwxX1pkZJIwkaEet4mOV7EdoiAjEA2Gka3vmF9g6T13t8M52SE+xHw8i9OJ8xvzf4yJb33qkBvVtmmCD1zc3Y3cGBMGVK"}, "tlogEntries":[{"logIndex":"2863675321", "logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion":{"kind":"hashedrekord", "version":"0.0.1"}, "integratedTime":"1789573310", "inclusionPromise":{"signedEntryTimestamp":"MEYCIQC+o/3DMjjuD1hsrsuDTGVZhSznVJ6a+DKjsKdphUfy8gIhAMuAqkDm66bl86gMp1U3WIGocswmT/0wKYG2E7tyzftF"}, "inclusionProof":{"logIndex":"2741771059", "rootHash":"QGoibsR3NlTIn36GQsrbRIxzaRSfNbhwZlySNEJCtp0=", "treeSize":"2741771089", "hashes":["ZSD2Kpy/tiLN7vBqmqxij5JkfT7h4zc/Nk9tqlW2LkU=", "wkmUVSQ5u7vup6QNn/sn1lnat2fEdir7oq7txk6PS04=", "57sfpiCeE0ol/i12IRwMO91oZdKtQi1Z9A6Qh00LalI=", "t/Q251ItS3/Oi+sqQvtWv2QBTf3pStgjN97a0diUzI4=", "txAJzpj0B2mytkBoEh+PmyPUrQtAQAe8HdQ5s7knJH0=", "r3mnC/aohNx5VrpuxgTeNnQgWBod9uxUqZUNG4rRZvE=", "ogN54UnekyIq8pu2OFB85zW4DqcTWPSYtURkt7YkAd4=", "ilHar1JYahqjkBAhrOtrhqEiO3Qbok3nTSf8oolx2AI=", "cIEAHuXcFQDOybeRS9RRMtM8EX8xJPLi0sl28LYNwvc=", "NVc8p5N/GOUrkHmEBPGzLyigWuNqMMCP7NWufg7PtDA=", "6z4kGRhDuEqrR5IIg/hEuSV40rtUWGF4oVhbug6DS5Y=", "g+kZXC8JlqtkZn+3BtMPZOIjPv/1QgiVvlH7tpuF5rU=", "z7hfW7IyWq58tcWTZuli3Brmb7xA7392X7mpl47fnCc=", "/DBUF30Sdk4LDt4XUgHCGSr10fT7nDkSitYbygYwfNg=", "vC+yZlSpf9tVD+3MdNtHCe71eNNHHi6gplgltL/tmDc=", "gGfwuhE97xnhmxu6exffPtS05ABS4/ldyg66sFcxSRE=", "jhevWFOJ2cWU6SvzMnAe08zHGcVquRlP7gAed39+wI4=", "qxzHanAzz57SDdmJe0B7bJK72NTIBbwEMGBBKvDOROw=", "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY="], "checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n2741771089\nQGoibsR3NlTIn36GQsrbRIxzaRSfNbhwZlySNEJCtp0=\n\n— rekor.sigstore.dev wNI9ajBFAiEA6/YRpGoC3bRbHpSvTUr/w8hWrrXRTMzc/pkQaE3nou4CIEZNHHXxcKJPDrBjo+uD34Gd3xcZi4hBaJWzgW21tmv+\n"}}, "canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmN2FlMWE2NDgwMzRlYmY5OGU5NTUyMWIzNDQ0ODgwYzdlYjZiNDY1ZDFjNTNhNDU3NTA5OGZhNTY5OTQwOWQxIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURGM1hpWkNtSUZUdEwwemkvN1NUeFpVT0cyZmY0bmd4a3F4QTVhUVcwV1pBSWhBTk92U21NR2N6M015WS9Ra1h4UEZ1aVo2SExGSTNlTVlsbXVHWU5GQUJzQiIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaFVSRU5EUW5SSFowRjNTVUpCWjBsVlRUUkhUbVpUV1hKdldXdFhXVVJFV2xoNlNFc3dNRzlNYlZKTmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFpkMDlVUlRKTlZGVXdUVlJWZDFkb1kwNU5hbGwzVDFSRk1rMVVWVEZOVkZWM1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZISzJ4WlJEbHJNVXh6T1VkaldtRkVUSEp3UW0wM05USktXalF4VjBwRGFsSkJlblFLV1RsUmFWVm9PR3RZYWxSSlMzazVWVEozTjBOa1NWQnNWRk5JVmpCeFMwMUtSRWQzVW1kVllUaHZlbWxxWVZWWk5qWlBRMEptUVhkbloxaHpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZsTDBwcENtODJjSHB2Y1doeVFuTnlLekZRUm05b1VUaG1ZbTAwZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJGM1dVUldVakJTUVZGSUwwSkhSWGRZTkZwa1lVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKb01Wb3laSEJpYldSdFdWZE9iQXBNTW5Sc1kyMDFiR0pJVFhSWk1qbDBZbGhXZFdGWVVqVk1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1rb3hZVmQ0YTB4dWJHaGlWM2hCQ21OdFZtMWplVGx2V2xkR2EyTjVPWFJaVjJ4MVRVUnJSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkxNbWd3WkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpBS1lWYzVkV041Tlc1aFdGSnZaRmRLTVdNeVZubFpNamwxWkVkV2RXUkROV3BpTWpCM1NIZFpTMHQzV1VKQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWN3cGlNMlJtV2tkc2VtTkhSakJaTW1kM1RtZFpTMHQzV1VKQ1FVZEVkbnBCUWtGM1VXOU5WRTB3VDBSUk1rNVhSbTFOUkVKdFRXMVJNRmw2VlRWT2JWRjZDazR5U210WmFra3lUVzFHYkUxRVp6Tk9WMUpxVFZkUk0wNTZRVlJDWjI5eVFtZEZSVUZaVHk5TlFVVkZRa0ZXUTJSWGJITmFSRUZ5UW1kdmNrSm5SVVVLUVZsUEwwMUJSVVpDUWpGdlpGZGtibUZYTlc1YWJVWnFXbE01Y2xwWVNuVmFWM2g2VEZkT2RtSlhNVEZpYld3d1pWUkJaRUpuYjNKQ1owVkZRVmxQTHdwTlFVVkhRa0U1ZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QzZFpTMHQzV1VKQ1FVZEVkbnBCUWtOQlVYUkVRM1J2WkVoU2QyTjZiM1pNTTFKMkNtRXlWblZNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVY3dSME5wYzBkQlVWRkNaemM0ZDBGUmEwVUtXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5YURGYU1tUndZbTFrYlZsWFRteE1NblJzWTIwMWJHSklUWFJaTWpsMFlsaFdkUXBoV0ZJMVRIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTmlNMlI2VERKS01XRlhlR3RNYm14b1lsZDRRV050Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxQ2sxRVowZERhWE5IUVZGUlFtYzNPSGRCVVc5RlMyZDNiMDFVVFRCUFJGRXlUbGRHYlUxRVFtMU5iVkV3V1hwVk5VNXRVWHBPTWtwcldXcEpNazF0Um13S1RVUm5NMDVYVW1wTlYxRXpUbnBCWWtKbmIzSkNaMFZGUVZsUEwwMUJSVXhDUVRCTlF6Tk9iR0pIV1hSaFJ6bDZaRWRXYTAxRlFVZERhWE5IUVZGUlFncG5OemgzUVZGM1JVMW5kM2RoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbWd4V2pKa2NHSnRaRzFaVjA1c1RESjBiR050Tld4aVNFMTBDbGt5T1hSaVdGWjFZVmhTTlUxRVowZERhWE5IUVZGUlFtYzNPSGRCVVRCRlMyZDNiMDFVVFRCUFJGRXlUbGRHYlUxRVFtMU5iVkV3V1hwVk5VNXRVWG9LVGpKS2ExbHFTVEpOYlVac1RVUm5NMDVYVW1wTlYxRXpUbnBCWmtKbmIzSkNaMFZGUVZsUEwwMUJSVTlDUWtWTlJETktiRnB1VFhaaFIxWm9Xa2hOZGdwaVYwWndZbXBCWVVKbmIzSkNaMFZGUVZsUEwwMUJSVkJDUVhkTlEycEZkMDU2UlRCT2VsVXhUV3ByZDB4bldVdExkMWxDUWtGSFJIWjZRVUpGUVZGbkNrUkNOVzlrU0ZKM1kzcHZka3d5WkhCa1IyZ3hXV2sxYW1JeU1IWmhTRlp1V2pKc2RWb3lXbWhaTWxWM1IwRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVc0tSRUZuZVU1VVkzbE5SR013VFhwQ2RFSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1JqaE5XRmRvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVid3BrVjJSdVlWYzFibHB0Um1wYVV6bHlXbGhLZFZwWGVIcE1WMDUyWWxjeE1XSnRiREJsVXpoMVdqSnNNR0ZJVm1sTU0yUjJZMjEwYldKSE9UTmplVGxwQ21SWGJITmFRelUxV1ZjeGMxRklTbXhhYmsxMllVZFdhRnBJVFhaaVYwWndZbXBCTkVKbmIzSkNaMFZGUVZsUEwwMUJSVlJDUTI5TlMwUkZlazVFWnpBS1RtcFdhRnBxUVhkYWFrcHJUa2ROTVU5VVdtdE5lbVJwV2tkSmVVNXFTbWhhVkVFMFRucFdhMWw2Um10T2VtTjNTVkZaUzB0M1dVSkNRVWRFZG5wQlFncEdRVkZVUkVKR00ySXpTbkphYlhoMlpERTVhMkZZVG5kWldGSnFZVVJDYTBKbmIzSkNaMFZGUVZsUEwwMUJSVlpDUmxsTlZrZG9NR1JJUW5wUGFUaDJDbG95YkRCaFNGWnBURzFPZG1KVE9XOWtWMlJ1WVZjMWJscHRSbXBhVXpseVdsaEtkVnBYZUhwTVYwNTJZbGN4TVdKdGJEQmxVemxvV1ROU2NHSXlOWG9LVEROS01XSnVUWFpOZWxWNFRWUlpNazVFYTNsUFZHTjJXVmhTTUZwWE1YZGtTRTEyVFZSQlYwSm5iM0pDWjBWRlFWbFBMMDFCUlZkQ1FXZE5RbTVDTVFwWmJYaHdXWHBDUjBKbmIzSkNaMFZGUVZsUEwwMUJSVmxDUkdkTlRtNUtiR05IT0RaaFNGWnVXakpzZFZveVdtaFpNbFYyWVRKV2VXSnRWbk5qZVRGcUNtSXlNWFJrVnpWd1pFaHJObU50Vm0xUGJrcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRMEpwWjFsTFMzZFpRa0pCU0ZkbFVVbEZRV2RTT0VKSWIwRUtaVUZDTWtGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFtOUxjbWh3ZW1OQlFVRlJSQXBCUldOM1VsRkpaMGQ1Y2xCeWMwRm1UVmx0YzJOQ01USlZSa1ptTjJ0RUt6RmlhWEJoWmxKTkx5OWhRWFYyVmpKa1JsRkRTVkZEWjBkdllqRlpiQ3RJQ21sMGVHZDVTVWhzVVd3MmNqSkhiSGRhTW5sT1IxTXJTRmcwY3pBemRWUmFRV3BCUzBKblozRm9hMnBQVUZGUlJFRjNUbkJCUkVKdFFXcEZRWFpFYjNFS2NUZG1iMVl4WjB0cFRVdFhNMmRIU2xCS2MwaE1RelF4TDBrMk9Ua3hjbVl6U2s1T2QzaFlNWEJyV2twSmQydGhSV1YwTkcxUFZqZEZaRzlwUVdwRlFRb3lSMnRoTTNadFJqbG5ObFF4TTNRNFRUVXlVMFVyZUVoM09HazVUMG80ZUhaNlpqUjVTbUl6TTNGclFuWldkRzF0UTBReGVtTXpXVE5qUjBKTlIxWkxDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}], "timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgVZZm5fTEylnms4MX4grvtUau4cMLqWPGCA9rKxgXSUgCFH5dDrgdaE/ZEsQCbT8usyh2rOu/GA8yMDI2MDkxNjE1NDE1MFowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdwwggHYAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwOTE2MTU0MTUwWjAvBgkqhkiG9w0BCQQxIgQgpefVkHwuaMjIVojVdSa3FQRdFIr5Ti3Ij4JVUCoteQgwgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGgwZgIxAKp5mHwMqJdsTYrvBy8Dp/kgStwA58ibghJu+ZH33A3ScHePUs3A3e6lY7LCOfXdMAIxAOGXZ1C76R22QrOGdakYSrpt/+e6c1HVupZtJQMMMsnBl0IWf+ghIS9wFytk40lNPA=="}]}}, "messageSignature":{"messageDigest":{"algorithm":"SHA2_256", "digest":"964aZIA06/mOlVIbNESIDH62tGXRxTpFdQmPpWmUCdE="}, "signature":"MEYCIQDF3XiZCmIFTtL0zi/7STxZUOG2ff4ngxkqxA5aQW0WZAIhANOvSmMGcz3MyY/QkXxPFuiZ6HLFI3eMYlmuGYNFABsB"}}
|
build/torch-xpu/mma.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import triton
|
| 16 |
+
import triton.language as tl
|
| 17 |
+
|
| 18 |
+
from .quant import e2m1_cols_to_e4m3, e2m1_cols_to_f32, e2m1_to_bf16, e2m1_to_e4m3, e2m1_to_f32
|
| 19 |
+
from .loading.scales import decode_group_scale
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@triton.jit
|
| 23 |
+
def mx_dot_scaled(
|
| 24 |
+
acc,
|
| 25 |
+
a,
|
| 26 |
+
a_scale,
|
| 27 |
+
w,
|
| 28 |
+
w_scale,
|
| 29 |
+
SWAP_AB: tl.constexpr = False,
|
| 30 |
+
BLOCK_SIZE_K: tl.constexpr = 0,
|
| 31 |
+
SCALE_GROUP_K: tl.constexpr = 0,
|
| 32 |
+
):
|
| 33 |
+
"""MX 'dot_scaled' step: the tcgen05 scaled MMA folds the group scales into the tensor core;
|
| 34 |
+
each operand's format is its tile's dtype (``uint8`` = packed E2M1, E4M3, or bf16 for the
|
| 35 |
+
weight-only unscaled lhs). fp4 on BOTH operands lowers to the native ``kind::mxf4`` MMA (2x the
|
| 36 |
+
fp8 rate; native iff the M operand is 128 — same gate as mxf8f6f4). Plain: ``a`` [BM, BK] x
|
| 37 |
+
``w`` [BK, BN] (the caller pre-shapes ``w``/``w_scale``, e.g. ``tl.trans(gu)``). ``SWAP_AB``
|
| 38 |
+
(decode): the weight ``w`` [BN, BK] is the lhs (output rows in M) and the single [BK] token is
|
| 39 |
+
padded to the N=16 rhs (col 0 real — 16 is Triton's tcgen05-selection gate, N=8 drops to the
|
| 40 |
+
bf16-upcast fallback, bare-1 was 1.83x); ``acc`` is the persistent ``[BN, MMA_N_ATOM]``
|
| 41 |
+
accumulator the caller takes column 0 of (never a fresh per-step init, which trips the sm_100
|
| 42 |
+
accumulator-init pass). A packed ``a`` stays packed (the E4M3-scaled mxf4nvf4 kind is fp4 x fp4
|
| 43 |
+
only) and the token's group scale broadcasts to the rhs columns."""
|
| 44 |
+
a_fmt: tl.constexpr = (
|
| 45 |
+
"e2m1" if a.dtype == tl.uint8
|
| 46 |
+
else ("e4m3" if a.dtype == tl.float8e4nv else "bf16")
|
| 47 |
+
)
|
| 48 |
+
w_fmt: tl.constexpr = "e2m1" if w.dtype == tl.uint8 else "e4m3"
|
| 49 |
+
if SWAP_AB:
|
| 50 |
+
rhs = swap_pad_rhs(a, BLOCK_SIZE_K // 2 if a.dtype == tl.uint8 else BLOCK_SIZE_K)
|
| 51 |
+
if a_scale.dtype == tl.uint8: # UE8M0 broadcast via the zero-add idiom
|
| 52 |
+
asc = tl.trans(
|
| 53 |
+
a_scale[:, None]
|
| 54 |
+
+ tl.zeros((BLOCK_SIZE_K // SCALE_GROUP_K, MMA_N_ATOM), tl.uint8)
|
| 55 |
+
)
|
| 56 |
+
else: # E4M3 (NVFP4) — no fp8 arithmetic; materialize the broadcast directly
|
| 57 |
+
asc = tl.trans(
|
| 58 |
+
tl.broadcast_to(
|
| 59 |
+
a_scale[:, None], (BLOCK_SIZE_K // SCALE_GROUP_K, MMA_N_ATOM)
|
| 60 |
+
)
|
| 61 |
+
)
|
| 62 |
+
acc = tl.dot_scaled(w, w_scale, w_fmt, rhs, asc, a_fmt, acc)
|
| 63 |
+
else:
|
| 64 |
+
acc = tl.dot_scaled(a, a_scale, a_fmt, w, w_scale, w_fmt, acc)
|
| 65 |
+
return acc
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@triton.jit
|
| 69 |
+
def mx_dot_rescale(
|
| 70 |
+
acc,
|
| 71 |
+
a,
|
| 72 |
+
a_scale,
|
| 73 |
+
w,
|
| 74 |
+
w_scale,
|
| 75 |
+
SWAP_AB: tl.constexpr = False,
|
| 76 |
+
BLOCK_SIZE_K: tl.constexpr = 0,
|
| 77 |
+
):
|
| 78 |
+
"""MX 'dot' step (BK == one scale group): fp8 ``tl.dot`` on E4M3-decoded operands + per-group
|
| 79 |
+
software rescale (both UE8M0 scales decoded here), accumulating into ``acc``. Plain: ``a``
|
| 80 |
+
[BM, BK] x ``w`` [BK, BN] with the weight's per-column scale transposed onto the product (the
|
| 81 |
+
batched gate_up kernel passes the stacked 2*BN tile — per-column independence keeps that
|
| 82 |
+
bit-exact). ``SWAP_AB`` (decode): the weight ``w`` [ROWS, BK] is the lhs (E2M1 column-unpacked,
|
| 83 |
+
K order low nibble first) and the [BK] token padded to the N=16 atom is the rhs — the
|
| 84 |
+
well-shaped fp8 MMA at M=1 (M quantizes to 64/128, N only to 8, so weight rows fill the big
|
| 85 |
+
atom); the weight's per-output-row scale broadcasts down the acc columns and the token's single
|
| 86 |
+
group scale is a scalar; ``acc`` is the persistent ``[ROWS, MMA_N_ATOM]`` accumulator."""
|
| 87 |
+
aq = e2m1_cols_to_e4m3(a) if a.dtype == tl.uint8 else a
|
| 88 |
+
a_s = decode_group_scale(a_scale)
|
| 89 |
+
w_s = decode_group_scale(w_scale)
|
| 90 |
+
if SWAP_AB:
|
| 91 |
+
wq = e2m1_cols_to_e4m3(w) if w.dtype == tl.uint8 else w
|
| 92 |
+
acc = acc + tl.dot(wq, swap_pad_rhs(aq, BLOCK_SIZE_K)) * w_s * a_s
|
| 93 |
+
else:
|
| 94 |
+
wq = e2m1_to_e4m3(w) if w.dtype == tl.uint8 else w
|
| 95 |
+
acc = acc + tl.dot(aq, wq) * a_s * tl.trans(w_s)
|
| 96 |
+
return acc
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@triton.jit
|
| 100 |
+
def mx_weight_upcast(w, w_scale, BLOCK_SIZE_K: tl.constexpr, N: tl.constexpr, SCALE_GROUP_K: tl.constexpr,
|
| 101 |
+
OUT_DTYPE: tl.constexpr = tl.bfloat16):
|
| 102 |
+
"""Upcast one MXFP4/MXFP8 weight K-tile ``[BK, N]`` to ``OUT_DTYPE`` for the weight-only path: unpack
|
| 103 |
+
E2M1 -> bf16 directly (fp8 passes through a cast) and apply each ``SCALE_GROUP_K``-row K-group's
|
| 104 |
+
UE8M0 group scale. ``w_scale`` is ``[N, BK // SCALE_GROUP_K]`` (per-N-row, per-group); transposed
|
| 105 |
+
and broadcast across each group's rows to ``[BK, N]``. Unlike the ``dot`` arm (BK == group, scale
|
| 106 |
+
folded onto the [M,N] product) this dequantizes IN the tile, so BK spans any number of groups — a
|
| 107 |
+
full-BK bf16 ``tl.dot`` (the matmul_ogs format: fp4 weight, bf16 acts).
|
| 108 |
+
|
| 109 |
+
The scale is applied as a plain bf16 multiply, NOT a hand-rolled exponent-add: the UE8M0 scale is
|
| 110 |
+
a power of two so a ``bf16 * 2^k`` is already an exact exponent shift (a cheap FMA), and folding it
|
| 111 |
+
into the bits as an int add + zero-mask ``tl.where`` measured ~13% SLOWER (gate_up 4.68->5.29ms) —
|
| 112 |
+
the mask + uint16<->int32 bitcast chain costs more than the multiply it replaces."""
|
| 113 |
+
ng: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 114 |
+
wq = e2m1_to_bf16(w) if w.dtype == tl.uint8 else w.to(tl.bfloat16) # [BK, N] bf16 (direct, no E4M3)
|
| 115 |
+
# Reshape the (real, contiguous) weight to [ng, g, N] and broadcast the group scale [ng, 1, N]
|
| 116 |
+
# across each group's g rows in the multiply, then reshape back to [BK, N]. (Reshaping a
|
| 117 |
+
# broadcasted stride-0 tensor doesn't lower, so broadcast happens IN the op, not before it.)
|
| 118 |
+
ws = tl.trans(decode_group_scale(w_scale)).to(tl.bfloat16)[:, None, :] # [ng, 1, N]
|
| 119 |
+
w3 = wq.reshape(ng, SCALE_GROUP_K, N) * ws
|
| 120 |
+
# dequant + scale happen in bf16 (exact: E2M1/E4M3 codes and power-of-two UE8M0 scales all fit),
|
| 121 |
+
# then widen if the activation is wider — bf16->fp32 is lossless and keeps the multiply cheap.
|
| 122 |
+
return w3.reshape(BLOCK_SIZE_K, N).to(OUT_DTYPE)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@triton.jit
|
| 126 |
+
def mx_weight_only_compute(
|
| 127 |
+
acc,
|
| 128 |
+
a,
|
| 129 |
+
w,
|
| 130 |
+
w_scale,
|
| 131 |
+
COMPUTE_MODE: tl.constexpr,
|
| 132 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 133 |
+
N_WIDTH: tl.constexpr,
|
| 134 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 135 |
+
SWAP_AB: tl.constexpr = False,
|
| 136 |
+
):
|
| 137 |
+
"""Weight-only (W4A16/W8A16) MMA step: raw bf16/fp16 activations against an MXFP4/MXFP8 weight
|
| 138 |
+
tile, dispatched on ``COMPUTE_MODE``. ``"dot_scaled"`` hands the packed weight and its UE8M0
|
| 139 |
+
group scales straight to the tcgen05 scaled MMA with an UNSCALED activation operand (the
|
| 140 |
+
``lhs_scale=None`` / bf16-format form), so the tensor core does the fp4 decode and the group
|
| 141 |
+
rescale; ``"dot"`` upcasts the tile to the activation dtype in-loop and runs a plain
|
| 142 |
+
``tl.dot`` (the matmul_ogs Hopper format). Both are correct everywhere and the tuner picks per
|
| 143 |
+
workload — the two differ by arm: measured on gpt-oss K=2880, ``dot_scaled`` wins the down
|
| 144 |
+
projection by 18.5% while the stacked gate|up tile prefers ``dot`` by 7.3``%``. ``SWAP_AB``
|
| 145 |
+
is the decode form: weight output rows lead the tile and the FMA reduce replaces the MMA
|
| 146 |
+
(both dot arms keep the token in M). Single return — only the taken branch compiles."""
|
| 147 |
+
if SWAP_AB:
|
| 148 |
+
acc = mx_weight_only_scalar_swapped(
|
| 149 |
+
acc, tl.reshape(a, (BLOCK_SIZE_K,)), w, w_scale,
|
| 150 |
+
N_WIDTH, BLOCK_SIZE_K, SCALE_GROUP_K,
|
| 151 |
+
)
|
| 152 |
+
elif COMPUTE_MODE == "dot_scaled":
|
| 153 |
+
acc = mx_dot_scaled(acc, a, None, w, w_scale)
|
| 154 |
+
else:
|
| 155 |
+
acc = acc + tl.dot(
|
| 156 |
+
a, mx_weight_upcast(w, w_scale, BLOCK_SIZE_K, N_WIDTH, SCALE_GROUP_K, a.dtype)
|
| 157 |
+
)
|
| 158 |
+
return acc
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@triton.jit
|
| 162 |
+
def mx_weight_only_scalar_swapped(
|
| 163 |
+
acc,
|
| 164 |
+
a,
|
| 165 |
+
w,
|
| 166 |
+
w_scale,
|
| 167 |
+
ROWS_W: tl.constexpr,
|
| 168 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 169 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 170 |
+
):
|
| 171 |
+
"""Swapped weight-only reduce: CUDA-core FMA GEVM against RAW bf16 activations (no act
|
| 172 |
+
scale — the weight-only counterpart of ``mx_scalar_reduce_swapped``). Weight
|
| 173 |
+
``w`` is output-rows-major ``[ROWS_W, BK]``, ``a`` the ``[BK]`` token; the UE8M0 group
|
| 174 |
+
scale factors out of the inner sum, so it costs one multiply per group instead of per
|
| 175 |
+
element. Returns ``acc + [1, ROWS_W]``.
|
| 176 |
+
|
| 177 |
+
This is the decode structure for W4A16: with no MMA there is no M->16 pad to waste, and
|
| 178 |
+
the loop runs at the dequantized weight stream's own rate (gpt-oss gate_up 32.0 -> 13.7us,
|
| 179 |
+
down 19.8 -> 9.1us, against a 12.3us/6us dequant-inclusive load floor)."""
|
| 180 |
+
NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 181 |
+
wq = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [ROWS_W, BK]
|
| 182 |
+
prod = wq * a.to(tl.float32)[None, :]
|
| 183 |
+
grp = tl.sum(tl.reshape(prod, (ROWS_W, NG, SCALE_GROUP_K)), axis=2) # [ROWS_W, NG]
|
| 184 |
+
return acc + tl.reshape(
|
| 185 |
+
tl.sum(grp * decode_group_scale(w_scale), axis=1), (1, ROWS_W)
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@triton.jit
|
| 190 |
+
def mx_scalar_reduce(
|
| 191 |
+
acc,
|
| 192 |
+
a,
|
| 193 |
+
a_scale,
|
| 194 |
+
w,
|
| 195 |
+
w_scale,
|
| 196 |
+
ROWS_W: tl.constexpr,
|
| 197 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 198 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 199 |
+
SWAP_AB: tl.constexpr = False,
|
| 200 |
+
):
|
| 201 |
+
"""MX 'scalar' step: CUDA-core FMA GEMV, unpacking MXFP4 to fp32 and dequantizing activation +
|
| 202 |
+
weight by their group scales, reducing over K into ``acc``. No tensor core (so no M->16 MMA
|
| 203 |
+
pad) — wins the memory-bound decode GEMV (M=1). The gate_up kernels pass the stacked gate|up
|
| 204 |
+
tile (ROWS_W = 2*BN). The UE8M0 scale is constant within each ``SCALE_GROUP_K`` group, so it
|
| 205 |
+
factors out of the inner sum: reduce the raw products per group, then apply ONE combined
|
| 206 |
+
(act x weight) scale per group — ``SCALE_GROUP_K``x fewer scale-muls (~18% faster, bit-identical
|
| 207 |
+
to the expanded form). Plain: ``a`` [BM, BK] against ``w`` [BK, ROWS_W]. ``SWAP_AB`` (decode):
|
| 208 |
+
``w`` output-rows-major [ROWS_W, BK] against the [BK] token, MXFP4 unpacked along columns (K),
|
| 209 |
+
no transposes. Both return ``acc + [1, ROWS_W]``."""
|
| 210 |
+
NG: tl.constexpr = BLOCK_SIZE_K // SCALE_GROUP_K
|
| 211 |
+
aq = e2m1_cols_to_f32(a) if a.dtype == tl.uint8 else a.to(tl.float32)
|
| 212 |
+
if SWAP_AB:
|
| 213 |
+
wq = e2m1_cols_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [ROWS_W, BK]
|
| 214 |
+
grp = tl.sum(tl.reshape(aq[None, :] * wq, (ROWS_W, NG, SCALE_GROUP_K)), axis=2)
|
| 215 |
+
scale = decode_group_scale(a_scale)[None, :] * decode_group_scale(w_scale) # [ROWS_W, NG]
|
| 216 |
+
acc = acc + tl.reshape(tl.sum(grp * scale, axis=1), (1, ROWS_W))
|
| 217 |
+
else:
|
| 218 |
+
wq = e2m1_to_f32(w) if w.dtype == tl.uint8 else w.to(tl.float32) # [BK, ROWS_W]
|
| 219 |
+
grp = tl.sum(tl.reshape(tl.trans(aq) * wq, (NG, SCALE_GROUP_K, ROWS_W)), axis=1)
|
| 220 |
+
scale = tl.trans(decode_group_scale(a_scale)) * tl.trans(decode_group_scale(w_scale))
|
| 221 |
+
acc = acc + tl.sum(grp * scale, axis=0)[None, :]
|
| 222 |
+
return acc
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@triton.jit
|
| 226 |
+
def mx_compute(
|
| 227 |
+
acc,
|
| 228 |
+
a,
|
| 229 |
+
a_scale,
|
| 230 |
+
w,
|
| 231 |
+
w_scale,
|
| 232 |
+
COMPUTE_MODE: tl.constexpr,
|
| 233 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 234 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 235 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 236 |
+
SWAP_AB: tl.constexpr = False,
|
| 237 |
+
):
|
| 238 |
+
"""Single-projection MMA step, dispatched on ``COMPUTE_MODE`` (scaled-MMA on the raw weight,
|
| 239 |
+
fp8 ``tl.dot`` + per-group rescale, or the scalar reduce); ``SWAP_AB`` selects each leaf's
|
| 240 |
+
decode form — weight output rows in the MMA M dim, the single token flattened to the [BK] rhs
|
| 241 |
+
(a packed-E2M1 token flattens to its BYTE length; dot_scaled consumes it packed, dot/scalar
|
| 242 |
+
column-unpack it losslessly). The acc shapes diverge across modes and orientations, but only
|
| 243 |
+
the taken constexpr branch compiles, so the single return never has to unify them.
|
| 244 |
+
``BLOCK_SIZE_N`` is the weight tile's row count — the gate_up kernels pass ``2*BN`` with the
|
| 245 |
+
stacked gate|up tile (split back via ``split_gate_up``): one load and one MMA for both
|
| 246 |
+
projections keeps the native microscaled-MMA M=128 operand at BN=64, doubling the CTAs on the
|
| 247 |
+
parallelism-starved decode grid (dsv4 gate_up 1.34x, bit-exact)."""
|
| 248 |
+
if SWAP_AB:
|
| 249 |
+
if a.dtype == tl.uint8:
|
| 250 |
+
a1 = tl.reshape(a, (BLOCK_SIZE_K // 2,))
|
| 251 |
+
else:
|
| 252 |
+
a1 = tl.reshape(a, (BLOCK_SIZE_K,))
|
| 253 |
+
as1 = tl.reshape(a_scale, (BLOCK_SIZE_K // SCALE_GROUP_K,))
|
| 254 |
+
else:
|
| 255 |
+
a1 = a
|
| 256 |
+
as1 = a_scale
|
| 257 |
+
if COMPUTE_MODE == "dot_scaled":
|
| 258 |
+
acc = mx_dot_scaled(acc, a1, as1, w, w_scale, SWAP_AB, BLOCK_SIZE_K, SCALE_GROUP_K)
|
| 259 |
+
elif COMPUTE_MODE == "dot":
|
| 260 |
+
acc = mx_dot_rescale(acc, a1, as1, w, w_scale, SWAP_AB, BLOCK_SIZE_K)
|
| 261 |
+
else: # scalar
|
| 262 |
+
acc = mx_scalar_reduce(acc, a1, as1, w, w_scale, BLOCK_SIZE_N, BLOCK_SIZE_K, SCALE_GROUP_K, SWAP_AB)
|
| 263 |
+
return acc
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# ── swap-AB decode compute: M=1 batched GEMV with output rows in the MMA M dim ───────
|
| 267 |
+
#
|
| 268 |
+
# The batched (decode) kernels are structurally M=1, where the sm_100 scaled MMA pads M→128.
|
| 269 |
+
# Putting the WEIGHT's output rows in M (fully used) and the single token in N cuts that pad to
|
| 270 |
+
# the N-atom (16) — a ~1.5× decode win for fp4 (dot_scaled), neutral-to-worse for fp8 (scalar
|
| 271 |
+
# stays memory-bound and wins, so the tuner keeps it). Weight is loaded output-rows-major
|
| 272 |
+
# ``[BN, BK]`` for every mode, so the kernel does ONE load; each helper returns ``[1, BN]``.
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# The sm_100 MMA's minimum N tile (16). In the swap path the single decode token sits in the MMA's
|
| 276 |
+
# N dim, so it must be padded up to this width (col 0 = the token, cols 1..15 = zero). It is NOT a
|
| 277 |
+
# block size — BLOCK_SIZE_M stays 1 under swap; this is the token's *padded N extent*, fixed by the
|
| 278 |
+
# hardware. The plain int is the source of truth: host-side consumers (the autotune pruners, which
|
| 279 |
+
# filter configs in ordinary Python) compare against it directly, while kernels read the
|
| 280 |
+
# tl.constexpr view derived from it — the only module-global form a @triton.jit fn can read.
|
| 281 |
+
MMA_N_ATOM_WIDTH = 16
|
| 282 |
+
MMA_N_ATOM = tl.constexpr(MMA_N_ATOM_WIDTH)
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
@triton.jit
|
| 286 |
+
def swap_pad_rhs(a, BLOCK_SIZE_K: tl.constexpr):
|
| 287 |
+
"""Pad the ``[BLOCK_SIZE_K]`` M=1 token to the ``[BLOCK_SIZE_K, MMA_N_ATOM]`` swap-AB MMA rhs —
|
| 288 |
+
only column 0 is the real token (16 is measured, not a hardware floor — see acc_init). Used
|
| 289 |
+
by the M=1 batched / fused-MoE fp8 ``tl.dot`` swap paths (weight output rows in the MMA M dim);
|
| 290 |
+
the caller takes column 0 of the ``[BN, MMA_N_ATOM]`` result after the K-loop."""
|
| 291 |
+
return tl.where(
|
| 292 |
+
tl.arange(0, MMA_N_ATOM)[None, :] == 0,
|
| 293 |
+
a[:, None],
|
| 294 |
+
tl.zeros((BLOCK_SIZE_K, MMA_N_ATOM), a.dtype),
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
@triton.jit
|
| 299 |
+
def fp8_dot(a, b, SWAP_AB: tl.constexpr, BLOCK_SIZE_K: tl.constexpr):
|
| 300 |
+
"""Swap-aware plain ``tl.dot`` for the fp8 (block/tensor-dynamic) paths — no scaling; the caller
|
| 301 |
+
applies its own per-block/per-tensor scales. Swap: weight ``b`` ``[N, BK]`` × the single token
|
| 302 |
+
padded to the N=16 atom → ``[N, MMA_N_ATOM]`` (col 0 real). No-swap: token ``a`` ``[M, BK]`` ×
|
| 303 |
+
weight ``b`` ``[BK, N]`` → ``[M, N]``. ``BLOCK_SIZE_K`` is the contraction tile (the down
|
| 304 |
+
projection passes its intermediate tile). Single return: only the taken branch compiles."""
|
| 305 |
+
if SWAP_AB:
|
| 306 |
+
out = tl.dot(b, swap_pad_rhs(tl.reshape(a, (BLOCK_SIZE_K,)), BLOCK_SIZE_K))
|
| 307 |
+
else:
|
| 308 |
+
out = tl.dot(a, b)
|
| 309 |
+
return out
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
@triton.jit
|
| 313 |
+
def block_dynamic_dot(
|
| 314 |
+
acc, a, a_s, b, b_s,
|
| 315 |
+
block_k: tl.constexpr, SWAP_AB: tl.constexpr, USE_DOT_SCALED: tl.constexpr,
|
| 316 |
+
FAKE_BATCH: tl.constexpr = False,
|
| 317 |
+
):
|
| 318 |
+
"""Accumulate one block-dynamic (1x128/128x128) fp8 K-tile into ``acc``, oriented by
|
| 319 |
+
``SWAP_AB``. ``USE_DOT_SCALED`` (UE8M0 scales on a native-M tile): fold the group scales
|
| 320 |
+
into the tcgen05 MMA — the tile's single 128-group scale broadcasts in-register to the
|
| 321 |
+
``block_k // 32`` group-32 columns ``dot_scaled`` consumes, identical to a 128-group
|
| 322 |
+
rescale but with no 4x scale memory and no software multiply. Else: plain fp8 ``tl.dot``
|
| 323 |
+
+ per-group software rescale (``decode_group_scale`` is a no-op on fp32 scales, decodes
|
| 324 |
+
UE8M0). ``FAKE_BATCH`` (single-token decode): ``fp8_dot`` pads the lone token to the MMA N
|
| 325 |
+
atom and both scales broadcast down the weight-row (M) dim. Single-exit if/else so only the
|
| 326 |
+
taken arm type-checks (a trailing fall-through arm would be checked even when an earlier
|
| 327 |
+
branch is taken)."""
|
| 328 |
+
if USE_DOT_SCALED:
|
| 329 |
+
reps: tl.constexpr = block_k // 32
|
| 330 |
+
a_sg = a_s[:, None].broadcast_to(a_s.shape[0], reps)
|
| 331 |
+
b_sg = b_s[:, None].broadcast_to(b_s.shape[0], reps)
|
| 332 |
+
if SWAP_AB:
|
| 333 |
+
acc = tl.dot_scaled(b, b_sg, "e4m3", a, a_sg, "e4m3", acc)
|
| 334 |
+
else:
|
| 335 |
+
acc = tl.dot_scaled(a, a_sg, "e4m3", b, b_sg, "e4m3", acc)
|
| 336 |
+
else:
|
| 337 |
+
# plain fp8 tl.dot + per-group decoded scales, oriented by SWAP_AB (weight rows in the MMA
|
| 338 |
+
# M dim under swap). decode_group_scale: fp32 passthrough, UE8M0 -> 2^(e-127).
|
| 339 |
+
a_sd = decode_group_scale(a_s)
|
| 340 |
+
b_sd = decode_group_scale(b_s)
|
| 341 |
+
if FAKE_BATCH:
|
| 342 |
+
d = fp8_dot(a, b, SWAP_AB, block_k)
|
| 343 |
+
if SWAP_AB: # [BN, N-atom]: weight-row scale down the M=BN dim, act scalar broadcasts
|
| 344 |
+
acc = acc + d * a_sd[:, None] * b_sd[:, None]
|
| 345 |
+
else: # no-swap [BM, BN] GEVM: act scale down M, weight-N scale across N (upstream form)
|
| 346 |
+
acc = acc + d * a_sd[:, None] * b_sd[None, :]
|
| 347 |
+
elif SWAP_AB:
|
| 348 |
+
acc = acc + tl.dot(b, a) * b_sd[:, None] * a_sd[None, :]
|
| 349 |
+
else:
|
| 350 |
+
acc = acc + tl.dot(a, b) * a_sd[:, None] * b_sd[None, :]
|
| 351 |
+
return acc
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
@triton.jit
|
| 355 |
+
def static_dot(acc, a, b, b_s, SWAP_AB: tl.constexpr, BLOCK_SIZE_K: tl.constexpr, FAKE_BATCH: tl.constexpr):
|
| 356 |
+
"""static format K-step: plain (swap-aware) fp8 dot + per-K-block weight rescale. FAKE_BATCH
|
| 357 |
+
(single-token decode) routes the rescale down the weight-row (M) dim; else it broadcasts across
|
| 358 |
+
the N columns. The per-tensor activation scale is applied post-loop."""
|
| 359 |
+
b_sd = decode_group_scale(b_s)
|
| 360 |
+
d = fp8_dot(a, b, SWAP_AB, BLOCK_SIZE_K)
|
| 361 |
+
if FAKE_BATCH:
|
| 362 |
+
acc = acc + d * b_sd[:, None]
|
| 363 |
+
else:
|
| 364 |
+
acc = acc + d * b_sd[None, :]
|
| 365 |
+
return acc
|
| 366 |
+
|
| 367 |
+
|
build/torch-xpu/moe.py
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""MoE forwards — thin orchestrations over the base ``matmul_grouped`` / ``matmul_batched`` ops.
|
| 16 |
+
|
| 17 |
+
The base ops carry the gate|up fusion (SwiGLU + FP8/MX requant) and the gather/scatter row
|
| 18 |
+
maps, so both the fused and unfused MoE forwards are pure sequencing here — no MoE-specific
|
| 19 |
+
kernels live in this module:
|
| 20 |
+
|
| 21 |
+
fused: gate_up (``gate=True`` + ``quantize_output=True``) -> down -> ``weighted_reduce``. The
|
| 22 |
+
SwiGLU + intermediate requant happen inside the gate_up kernel epilogue. A model's
|
| 23 |
+
``post_expert_norm`` runs on the down's routed rows, before the reduce.
|
| 24 |
+
unfused: gate_up (plain GEMM) -> host ``apply_glu`` -> down (plain GEMM) -> ``weighted_reduce``.
|
| 25 |
+
The activation + requant happen between two plain GEMMs; the GEMMs self-quantize their
|
| 26 |
+
raw inputs (``As=None``). Same math as the fused path, split across kernels.
|
| 27 |
+
|
| 28 |
+
grouped (prefill) shares one on-device routing pass (``compute_grouped_scheduling``): gate_up
|
| 29 |
+
gathers hidden by routed row and leaves its output expert-ordered; down reads it in place and
|
| 30 |
+
scatters to routed rows. batched (decode) dispatches per token: ``gather_idx`` reads each routed
|
| 31 |
+
row from the unexpanded hidden in-kernel (no copy), and EP-sentinel rows (``id >= num_experts``)
|
| 32 |
+
are left uninit by the GEMM and skipped in ``weighted_reduce``. ``moe_fused_*`` / ``moe_unfused_*`` are format-neutral:
|
| 33 |
+
the base ops dispatch on the weight dtypes / scale layout (block-dynamic FP8, MXFP4/MXFP8,
|
| 34 |
+
NVFP4), and every forward takes ``activation_format`` naming the block's activation quantization
|
| 35 |
+
(``None`` = the weights' own format, ``"bf16"`` = weight-only, or an explicit format such as
|
| 36 |
+
``"mxfp8"`` on MXFP4 weights for W4A8)."""
|
| 37 |
+
|
| 38 |
+
import functools
|
| 39 |
+
from collections.abc import Callable
|
| 40 |
+
|
| 41 |
+
import torch
|
| 42 |
+
import torch.nn.functional as F
|
| 43 |
+
import triton
|
| 44 |
+
import triton.language as tl
|
| 45 |
+
from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
|
| 46 |
+
|
| 47 |
+
from .grouped import matmul_grouped
|
| 48 |
+
from .batched import GATE_UNSTACK_MAX_S, matmul_batched
|
| 49 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 50 |
+
from .compat import MX_SCALE_GROUP_K, NVFP4_SCALE_GROUP_K, compile_time_only_triton_wrap, decode_pdl, device_context
|
| 51 |
+
from .formats import get_supported_act_fns, is_mx, is_mxfp4, weight_format
|
| 52 |
+
from .norm import norm_column_factor, rms_inv_rows, rms_norm_rows
|
| 53 |
+
from .quant import _launch_act_quant
|
| 54 |
+
from .scheduling import compute_grouped_scheduling
|
| 55 |
+
from .epilogue import fused_glu
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@bayesian_autotune(
|
| 59 |
+
[
|
| 60 |
+
triton.Config({"BLOCK_H": block_h}, num_warps=warps)
|
| 61 |
+
for block_h in (256, 512, 1024, 2048)
|
| 62 |
+
for warps in (4, 8)
|
| 63 |
+
],
|
| 64 |
+
# the H tile width trades off against grid occupancy: at few groups (decode) narrow
|
| 65 |
+
# tiles spread more H-blocks across SMs, at many groups (prefill) wide tiles amortize
|
| 66 |
+
# the per-row weight load — so key on H and the group-count bucket.
|
| 67 |
+
["H", "num_groups_bit_length", "NORM"],
|
| 68 |
+
n_trials=8,
|
| 69 |
+
)
|
| 70 |
+
@triton.jit
|
| 71 |
+
def weighted_reduce_kernel(
|
| 72 |
+
Rows, # (num_groups * NUM_TOP_K, H) — rows to reduce, group-major
|
| 73 |
+
Out, # (num_groups, H) — one reduced row per group
|
| 74 |
+
Ids, # (num_groups, NUM_TOP_K) — per-row id; a row is skipped when its id >= NUM_EXPERTS
|
| 75 |
+
Weights, # (num_groups * NUM_TOP_K,) — per-row scale
|
| 76 |
+
NormWeight, # (H,) fused post-expert norm weight; None (with NormInv) folds the arm out
|
| 77 |
+
NormInv, # (num_groups * NUM_TOP_K,) fp32 per-row rsqrt from rms_inv_rows
|
| 78 |
+
H,
|
| 79 |
+
stride_rows_m,
|
| 80 |
+
stride_rows_h,
|
| 81 |
+
stride_o_m,
|
| 82 |
+
stride_o_h,
|
| 83 |
+
stride_ids_m,
|
| 84 |
+
stride_ids_k,
|
| 85 |
+
num_groups_bit_length, # autotune key only (log2 group-count bucket); unused in body
|
| 86 |
+
NUM_TOP_K: tl.constexpr,
|
| 87 |
+
NUM_EXPERTS: tl.constexpr,
|
| 88 |
+
BLOCK_H: tl.constexpr,
|
| 89 |
+
NORM: tl.constexpr = None, # a get_supported_norms() name folded into the reduce
|
| 90 |
+
SIMULATE_UNFUSED: tl.constexpr = False,
|
| 91 |
+
PDL: tl.constexpr = False,
|
| 92 |
+
):
|
| 93 |
+
"""Per group ``g``, the weighted sum of its ``NUM_TOP_K`` rows into ``Out[g]``:
|
| 94 |
+
``sum_k Weights[g*NUM_TOP_K + k] * Rows[g*NUM_TOP_K + k]``, skipping rows whose id is
|
| 95 |
+
``>= NUM_EXPERTS`` (out-of-range rows are never written upstream and contribute 0).
|
| 96 |
+
fp32 accumulate; ~2.8x a generic ``view(g, k, H).sum(1)``. ``SIMULATE_UNFUSED`` rounds
|
| 97 |
+
each weighted row to ``Out``'s dtype before summing, matching a reference that weights
|
| 98 |
+
in that dtype; production leaves the accumulation in fp32.
|
| 99 |
+
|
| 100 |
+
``NORM`` folds a per-expert output norm into the pass: each row is scaled by its
|
| 101 |
+
``NormInv`` (``rms_inv_rows``, one pass ahead of this one) and the norm's column factor
|
| 102 |
+
before the routing weight, so the normalized rows are never written or re-read."""
|
| 103 |
+
if PDL:
|
| 104 |
+
gdc_wait()
|
| 105 |
+
g = tl.program_id(0)
|
| 106 |
+
offs_h = tl.program_id(1) * BLOCK_H + tl.arange(0, BLOCK_H)
|
| 107 |
+
mask = offs_h < H
|
| 108 |
+
acc = tl.zeros((BLOCK_H,), tl.float32)
|
| 109 |
+
if NORM is not None: # the tile's column factor is row-independent — load it once
|
| 110 |
+
factor = norm_column_factor(NormWeight, offs_h, mask, NORM)
|
| 111 |
+
for k in tl.static_range(NUM_TOP_K):
|
| 112 |
+
flat = g * NUM_TOP_K + k
|
| 113 |
+
valid = tl.load(Ids + g * stride_ids_m + k * stride_ids_k) < NUM_EXPERTS
|
| 114 |
+
weight = tl.load(Weights + flat)
|
| 115 |
+
row = tl.load(
|
| 116 |
+
Rows + flat * stride_rows_m + offs_h * stride_rows_h,
|
| 117 |
+
mask=mask & valid,
|
| 118 |
+
other=0.0,
|
| 119 |
+
).to(tl.float32)
|
| 120 |
+
if NORM is not None:
|
| 121 |
+
# masked like the value load: a sentinel row's rsqrt comes off uninitialized memory,
|
| 122 |
+
# and 0 * NaN would poison the sum
|
| 123 |
+
row = row * factor * tl.load(NormInv + flat, mask=valid, other=0.0)
|
| 124 |
+
if SIMULATE_UNFUSED: # the reference materializes the normalized row in Out's dtype
|
| 125 |
+
row = row.to(Out.dtype.element_ty).to(tl.float32)
|
| 126 |
+
contrib = weight * row
|
| 127 |
+
if SIMULATE_UNFUSED:
|
| 128 |
+
contrib = contrib.to(Out.dtype.element_ty).to(tl.float32)
|
| 129 |
+
acc += contrib
|
| 130 |
+
if PDL:
|
| 131 |
+
gdc_launch_dependents()
|
| 132 |
+
tl.store(
|
| 133 |
+
Out + g * stride_o_m + offs_h * stride_o_h,
|
| 134 |
+
acc.to(Out.dtype.element_ty),
|
| 135 |
+
mask=mask,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def weighted_reduce(
|
| 141 |
+
rows: torch.Tensor,
|
| 142 |
+
top_k_index: torch.Tensor,
|
| 143 |
+
top_k_weights: torch.Tensor,
|
| 144 |
+
num_experts: int,
|
| 145 |
+
simulate_unfused: bool = False,
|
| 146 |
+
norm: str | None = None,
|
| 147 |
+
norm_weight: torch.Tensor | None = None,
|
| 148 |
+
norm_inv: torch.Tensor | None = None,
|
| 149 |
+
) -> torch.Tensor:
|
| 150 |
+
"""Routing-weighted top-k reduce — the bookend of the fused-MoE chain. Folds each token's
|
| 151 |
+
``num_top_k`` expert-output rows (``rows``, group-major, scaled by ``top_k_weights``, with
|
| 152 |
+
EP-sentinel rows ``id >= num_experts`` skipped) from the routed-row layout back to
|
| 153 |
+
``(num_tokens, H)``. See ``weighted_reduce_kernel``. ``norm`` (a ``get_supported_norms()``
|
| 154 |
+
name, with ``norm_weight`` and the ``rms_inv_rows`` ``norm_inv``) folds a per-expert output
|
| 155 |
+
norm into this pass rather than normalizing the rows in one of their own."""
|
| 156 |
+
assert (norm is None) == (norm_inv is None), "a fused norm needs its per-row rsqrt"
|
| 157 |
+
num_tokens, num_top_k = top_k_index.shape
|
| 158 |
+
H = rows.size(1)
|
| 159 |
+
reduced = torch.empty(num_tokens, H, device=rows.device, dtype=rows.dtype)
|
| 160 |
+
with device_context(rows.device):
|
| 161 |
+
compile_time_only_triton_wrap(weighted_reduce_kernel)[
|
| 162 |
+
lambda meta: (num_tokens, triton.cdiv(H, meta["BLOCK_H"]))
|
| 163 |
+
](
|
| 164 |
+
rows,
|
| 165 |
+
reduced,
|
| 166 |
+
top_k_index,
|
| 167 |
+
top_k_weights,
|
| 168 |
+
norm_weight,
|
| 169 |
+
norm_inv,
|
| 170 |
+
H,
|
| 171 |
+
rows.stride(0),
|
| 172 |
+
rows.stride(1),
|
| 173 |
+
reduced.stride(0),
|
| 174 |
+
reduced.stride(1),
|
| 175 |
+
top_k_index.stride(0),
|
| 176 |
+
top_k_index.stride(1),
|
| 177 |
+
num_groups_bit_length=int(num_tokens).bit_length(),
|
| 178 |
+
NUM_TOP_K=num_top_k,
|
| 179 |
+
NUM_EXPERTS=num_experts,
|
| 180 |
+
NORM=norm,
|
| 181 |
+
SIMULATE_UNFUSED=simulate_unfused,
|
| 182 |
+
PDL=decode_pdl(),
|
| 183 |
+
launch_pdl=decode_pdl(),
|
| 184 |
+
)
|
| 185 |
+
return reduced
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _validate_moe(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale):
|
| 189 |
+
"""gate_up and down must share the format (both MX or both block-dynamic FP8 — the
|
| 190 |
+
intermediate handed between them carries one quant format). Returns whether the format
|
| 191 |
+
is MX (the fused dispatchers branch on it); the fp8 quantization block is derived from
|
| 192 |
+
the scale shapes (``weight_block_size``), never passed. Scales are the pure block scales
|
| 193 |
+
(per the decoupled API — per-tensor globals ride as separate ``*_global_scale`` args);
|
| 194 |
+
the format predicates read the block scale's dtype/grouping."""
|
| 195 |
+
gate_up_is_mx = is_mx(gate_up_proj, gate_up_proj_scale)
|
| 196 |
+
if gate_up_is_mx != is_mx(down_proj, down_proj_scale):
|
| 197 |
+
raise ValueError(
|
| 198 |
+
"gate_up_proj and down_proj must use the same format (both MX or both block-dynamic FP8)."
|
| 199 |
+
)
|
| 200 |
+
if is_mxfp4(gate_up_proj, gate_up_proj_scale) != is_mxfp4(down_proj, down_proj_scale):
|
| 201 |
+
raise ValueError("gate_up_proj and down_proj must use the same MX format (both MXFP4 or both MXFP8).")
|
| 202 |
+
return gate_up_is_mx
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _gather_idx(top_k_index: torch.Tensor) -> torch.Tensor:
|
| 206 |
+
"""The batched routed-row gather: routed row ``s`` (``= t*K + k``) reads token ``s // num_top_k``
|
| 207 |
+
of the unexpanded hidden. ``matmul_batched`` applies it in-kernel, so no ``(S, H)`` copy.
|
| 208 |
+
The map depends only on the SHAPE, so it is cached per (tokens, top_k, device) — its two
|
| 209 |
+
elementwise launches (~3µs) were the largest non-GEMM cost of the fp8 decode chain."""
|
| 210 |
+
return _gather_idx_cached(
|
| 211 |
+
top_k_index.shape[0], top_k_index.shape[1], top_k_index.device
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
@functools.lru_cache(maxsize=64)
|
| 216 |
+
def _gather_idx_cached(
|
| 217 |
+
num_tokens: int, num_top_k: int, device: torch.device
|
| 218 |
+
) -> torch.Tensor:
|
| 219 |
+
return (
|
| 220 |
+
torch.arange(num_tokens * num_top_k, device=device, dtype=torch.int32)
|
| 221 |
+
// num_top_k
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def _torch_weighted_reduce(down_out, top_k_index, top_k_weights, num_experts):
|
| 226 |
+
"""Naive (unfused) routing-weighted top-k reduce in plain torch — NOT the fused
|
| 227 |
+
``weighted_reduce`` kernel. Materializes the (bf16) weighted contribs, masks EP-sentinel rows
|
| 228 |
+
(``id >= num_experts``, left uninit in ``down_out``) to 0, and torch-sums to ``(num_tokens, H)``
|
| 229 |
+
(fp32 accumulate, activation-dtype out). This is the independent reference the fused
|
| 230 |
+
``weighted_reduce`` is checked against; the fused path's ``simulate_unfused`` reproduces its
|
| 231 |
+
bf16-contrib rounding."""
|
| 232 |
+
num_tokens, num_top_k = top_k_index.shape
|
| 233 |
+
dropped = (top_k_index.reshape(-1) >= num_experts).reshape(-1, 1)
|
| 234 |
+
# masked in place on the PRODUCT: a sentinel row's down_out is uninitialized, so zeroing the
|
| 235 |
+
# weight instead would leave 0 * NaN == NaN.
|
| 236 |
+
contrib = down_out * top_k_weights.reshape(-1, 1)
|
| 237 |
+
contrib.masked_fill_(dropped, 0)
|
| 238 |
+
return contrib.view(num_tokens, num_top_k, down_out.size(1)).sum(dim=1)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate):
|
| 242 |
+
"""The activation on the host, for the arms the epilogue does not fuse: a callable takes the
|
| 243 |
+
raw GEMM output (interleaved gate|up columns when ``gate``) and returns the intermediate —
|
| 244 |
+
the caller owns its semantics, so any activation runs without a kernel change; a name from
|
| 245 |
+
``get_supported_act_fns()`` runs the torch GLU (``gate``) or the bare activation."""
|
| 246 |
+
if callable(act_fn):
|
| 247 |
+
return act_fn(gate_up_out)
|
| 248 |
+
if act_fn not in get_supported_act_fns():
|
| 249 |
+
raise ValueError(f"act_fn must be one of {get_supported_act_fns()} or a callable, got {act_fn!r}")
|
| 250 |
+
if gate:
|
| 251 |
+
return fused_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit)
|
| 252 |
+
return {"silu": F.silu, "gelu": F.gelu, "relu": F.relu}[act_fn](gate_up_out)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate) -> dict:
|
| 256 |
+
"""The gate_up GEMM's fused-GLU kwargs when the activation is a fusable name; ``{}`` (a plain
|
| 257 |
+
GEMM) sends a callable — or an unfusable name, rejected in ``_host_glu`` — to the host."""
|
| 258 |
+
if not isinstance(act_fn, str) or act_fn not in get_supported_act_fns():
|
| 259 |
+
return {}
|
| 260 |
+
return dict(
|
| 261 |
+
gate=gate,
|
| 262 |
+
act_fn=act_fn,
|
| 263 |
+
swiglu_alpha=swiglu_alpha,
|
| 264 |
+
swiglu_limit=swiglu_limit,
|
| 265 |
+
simulate_unfused=simulate_unfused,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# ── Fused (gate_up epilogue owns SwiGLU + intermediate requant) ──────────────
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _block_format(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale, activation_format):
|
| 273 |
+
"""The MoE block's activation format, resolved to a name: validates the weight pairing; an
|
| 274 |
+
explicit ``activation_format`` (``"bf16"`` weight-only, or a format) is respected as-is,
|
| 275 |
+
``None`` follows the weights (fp8 / mxfp8 / mxfp4 / nvfp4 — mxfp4 weights default to mxfp4
|
| 276 |
+
activations, the all-fp4 W4A4 chain; unquantized BF16/FP16 weights carry no scales, so their
|
| 277 |
+
activations stay ``"bf16"``, the full-precision path)."""
|
| 278 |
+
_validate_moe(gate_up_proj, gate_up_proj_scale, down_proj, down_proj_scale)
|
| 279 |
+
if activation_format is not None:
|
| 280 |
+
return activation_format
|
| 281 |
+
if gate_up_proj_scale is None:
|
| 282 |
+
return "bf16"
|
| 283 |
+
return weight_format(gate_up_proj, gate_up_proj_scale)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _post_expert_norm(down_out, post_expert_norm, weight, eps):
|
| 287 |
+
"""A model's per-expert output norm applied to the down projection's routed rows — one row
|
| 288 |
+
per (token, expert) application, which is what such a norm is defined over. ``None`` passes
|
| 289 |
+
through; a ``get_supported_norms()`` name runs ``rms_norm_rows`` against ``weight``; anything
|
| 290 |
+
else is a host callable, like ``act_fn``'s."""
|
| 291 |
+
if post_expert_norm is None:
|
| 292 |
+
return down_out
|
| 293 |
+
if isinstance(post_expert_norm, str):
|
| 294 |
+
return rms_norm_rows(down_out, weight, eps, post_expert_norm)
|
| 295 |
+
return post_expert_norm(down_out)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def _fused_post_expert_norm(down_out, post_expert_norm, weight, eps):
|
| 299 |
+
"""``(rows, weighted_reduce kwargs)`` for a fused forward: a named norm rides INTO the reduce
|
| 300 |
+
— one pass for the row's ``rsqrt``, then the reduce it already runs applies that and the
|
| 301 |
+
column factor — so the normalized rows are never materialized. A callable has to run first."""
|
| 302 |
+
if isinstance(post_expert_norm, str):
|
| 303 |
+
return down_out, {
|
| 304 |
+
"norm": post_expert_norm,
|
| 305 |
+
"norm_weight": weight,
|
| 306 |
+
"norm_inv": rms_inv_rows(down_out, weight, eps, post_expert_norm),
|
| 307 |
+
}
|
| 308 |
+
return _post_expert_norm(down_out, post_expert_norm, weight, eps), {}
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def moe_fused_grouped(
|
| 312 |
+
hidden_states: torch.Tensor, # (T, H)
|
| 313 |
+
top_k_index: torch.Tensor, # (T, K) int
|
| 314 |
+
top_k_weights: torch.Tensor, # (T, K)
|
| 315 |
+
gate_up_proj: torch.Tensor, # (E, 2I, H)
|
| 316 |
+
down_proj: torch.Tensor, # (E, H, I)
|
| 317 |
+
gate_up_proj_scale_inv: torch.Tensor,
|
| 318 |
+
down_proj_scale_inv: torch.Tensor,
|
| 319 |
+
gate_up_proj_weight_global_scale: torch.Tensor | None = None,
|
| 320 |
+
down_proj_weight_global_scale: torch.Tensor | None = None,
|
| 321 |
+
gate_up_proj_input_global_scale: torch.Tensor | None = None,
|
| 322 |
+
down_proj_input_global_scale: torch.Tensor | None = None,
|
| 323 |
+
post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
|
| 324 |
+
# get_supported_norms() name (fused into the reduce) or a host callable
|
| 325 |
+
post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
|
| 326 |
+
post_expert_norm_eps: float = 1e-6,
|
| 327 |
+
gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
|
| 328 |
+
down_proj_bias: torch.Tensor | None = None, # (E, H)
|
| 329 |
+
act_fn: str | Callable = "silu",
|
| 330 |
+
swiglu_alpha: float | None = None,
|
| 331 |
+
swiglu_limit: float | None = None,
|
| 332 |
+
simulate_unfused: bool = False,
|
| 333 |
+
activation_format: str | None = None,
|
| 334 |
+
gate: bool = True,
|
| 335 |
+
) -> torch.Tensor:
|
| 336 |
+
"""Fused grouped MoE (prefill): gather gate_up + SiLU + requant epilogue → quantized
|
| 337 |
+
expert-ordered intermediate → grouped down → routing-weighted top-k reduce. Returns
|
| 338 |
+
``(num_tokens, hidden_dim)``. The base ops dispatch on the weight dtypes / scale
|
| 339 |
+
layout (block-dynamic FP8, MXFP8/MXFP4, NVFP4); ``activation_format`` names the activation
|
| 340 |
+
quantization for the whole block — activations and the fused intermediate requant
|
| 341 |
+
carry it (``"mxfp4"``/``"nvfp4"`` run all-fp4 W4A4 chains, ``"bf16"`` is weight-only);
|
| 342 |
+
``None`` follows the weight format, and the ops validate the pairing. The
|
| 343 |
+
``*_proj_input_global_scale`` pair is the NVFP4 activation second level (a checkpoint's
|
| 344 |
+
calibrated per-projection ``input_scale``): the gate_up quantizes hidden against its own,
|
| 345 |
+
requants the intermediate against the down's, and the down consumes that as its activation
|
| 346 |
+
global. ``None`` is dynamic quant everywhere else. ``simulate_unfused`` (testing) rounds each step through
|
| 347 |
+
the activation dtype so the output matches the unfused reference to reduce order. ``act_fn`` is a
|
| 348 |
+
``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
|
| 349 |
+
callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
|
| 350 |
+
projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
|
| 351 |
+
— the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
|
| 352 |
+
fmt = _block_format(
|
| 353 |
+
gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
|
| 354 |
+
)
|
| 355 |
+
num_top_k = top_k_index.size(-1)
|
| 356 |
+
glu = _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate)
|
| 357 |
+
NUM_EXPERTS = gate_up_proj.size(0)
|
| 358 |
+
expert_start, gather_idx, scatter_idx = compute_grouped_scheduling(
|
| 359 |
+
top_k_index, NUM_EXPERTS, num_top_k
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# Phase 1: gate_up + SiLU + requant in the block format -> expert-ordered quantized
|
| 363 |
+
# intermediate (the op quantizes the raw hidden itself and owns the expand-vs-gather
|
| 364 |
+
# regime policy — this forward is pure sequencing). scatter_idx=None: the down reads
|
| 365 |
+
# the intermediate in place. (C, Cs) under a requant format; a bare Tensor otherwise.
|
| 366 |
+
gate_up_out = matmul_grouped(
|
| 367 |
+
hidden_states,
|
| 368 |
+
gate_up_proj,
|
| 369 |
+
Bs=gate_up_proj_scale_inv,
|
| 370 |
+
a_global_scale=gate_up_proj_input_global_scale,
|
| 371 |
+
b_global_scale=gate_up_proj_weight_global_scale,
|
| 372 |
+
# the intermediate requant normalizes against the DOWN's calibrated input global,
|
| 373 |
+
# which the down then consumes as its activation global — the two-level handoff
|
| 374 |
+
output_global_scale=down_proj_input_global_scale,
|
| 375 |
+
expert_start=expert_start,
|
| 376 |
+
**glu,
|
| 377 |
+
bias=gate_up_proj_bias,
|
| 378 |
+
# fmt is the resolved format; "bf16" (weight-only) leaves the GLU intermediate bf16,
|
| 379 |
+
# no requant.
|
| 380 |
+
activation_format=fmt,
|
| 381 |
+
quantize_output=bool(glu) and fmt != "bf16",
|
| 382 |
+
output_dtype=hidden_states.dtype,
|
| 383 |
+
gather_idx=gather_idx,
|
| 384 |
+
)
|
| 385 |
+
inter, inter_scale = (
|
| 386 |
+
gate_up_out if isinstance(gate_up_out, tuple) else (gate_up_out, None)
|
| 387 |
+
)
|
| 388 |
+
if not glu:
|
| 389 |
+
inter = _host_glu(inter, act_fn, swiglu_alpha, swiglu_limit, gate)
|
| 390 |
+
# Phase 2: grouped down over the expert-ordered pre-quantized intermediate (its dtypes
|
| 391 |
+
# carry the format; gather_idx=None), scattering to routed rows (scatter_idx).
|
| 392 |
+
down_out = matmul_grouped(
|
| 393 |
+
inter,
|
| 394 |
+
down_proj,
|
| 395 |
+
As=inter_scale,
|
| 396 |
+
Bs=down_proj_scale_inv,
|
| 397 |
+
a_global_scale=down_proj_input_global_scale,
|
| 398 |
+
b_global_scale=down_proj_weight_global_scale,
|
| 399 |
+
expert_start=expert_start,
|
| 400 |
+
bias=down_proj_bias,
|
| 401 |
+
# weight-only: the intermediate is bf16 (As None) — the down goes weight-only too; a
|
| 402 |
+
# quantized intermediate carries its format in its own dtypes.
|
| 403 |
+
activation_format=fmt if inter_scale is None else None,
|
| 404 |
+
output_dtype=hidden_states.dtype,
|
| 405 |
+
scatter_idx=scatter_idx,
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# Phase 3: routing-weighted top-k reduce -> (num_tokens, hidden_dim). simulate_unfused
|
| 409 |
+
# rounds each weighted contrib to the activation dtype before summing, matching the
|
| 410 |
+
# unfused path's torch reduce (which materializes bf16 contribs); production
|
| 411 |
+
# accumulates in fp32.
|
| 412 |
+
rows, norm_kwargs = _fused_post_expert_norm(
|
| 413 |
+
down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
|
| 414 |
+
)
|
| 415 |
+
return weighted_reduce(
|
| 416 |
+
rows, top_k_index, top_k_weights, NUM_EXPERTS, simulate_unfused, **norm_kwargs
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def moe_fused_batched(
|
| 421 |
+
hidden_states: torch.Tensor, # (T, H)
|
| 422 |
+
top_k_index: torch.Tensor, # (T, K) int
|
| 423 |
+
top_k_weights: torch.Tensor, # (T, K)
|
| 424 |
+
gate_up_proj: torch.Tensor, # (E, 2I, H)
|
| 425 |
+
down_proj: torch.Tensor, # (E, H, I)
|
| 426 |
+
gate_up_proj_scale_inv: torch.Tensor,
|
| 427 |
+
down_proj_scale_inv: torch.Tensor,
|
| 428 |
+
gate_up_proj_weight_global_scale: torch.Tensor | None = None,
|
| 429 |
+
down_proj_weight_global_scale: torch.Tensor | None = None,
|
| 430 |
+
gate_up_proj_input_global_scale: torch.Tensor | None = None,
|
| 431 |
+
down_proj_input_global_scale: torch.Tensor | None = None,
|
| 432 |
+
post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
|
| 433 |
+
# get_supported_norms() name (fused into the reduce) or a host callable
|
| 434 |
+
post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
|
| 435 |
+
post_expert_norm_eps: float = 1e-6,
|
| 436 |
+
gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
|
| 437 |
+
down_proj_bias: torch.Tensor | None = None, # (E, H)
|
| 438 |
+
act_fn: str | Callable = "silu",
|
| 439 |
+
swiglu_alpha: float | None = None,
|
| 440 |
+
swiglu_limit: float | None = None,
|
| 441 |
+
simulate_unfused: bool = False,
|
| 442 |
+
activation_format: str | None = None,
|
| 443 |
+
gate: bool = True,
|
| 444 |
+
) -> torch.Tensor:
|
| 445 |
+
"""Fused batched MoE (decode): gate_up + SiLU + requant epilogue → per-row quantized
|
| 446 |
+
intermediate → batched down → routing-weighted top-k reduce. Returns
|
| 447 |
+
``(num_tokens, hidden_dim)``. The base ops dispatch on the weight dtypes / scale
|
| 448 |
+
layout (block-dynamic FP8, MXFP8/MXFP4, NVFP4 — decode runs the software/swap arms
|
| 449 |
+
below the native mxf4nvf4 M=128 staging); ``activation_format`` names the activation
|
| 450 |
+
quantization for the whole block — activations and the fused intermediate requant
|
| 451 |
+
carry it (``"mxfp4"`` runs the all-fp4 W4A4 chain, ``"bf16"`` is weight-only); ``None``
|
| 452 |
+
follows the weight format, and the ops validate the pairing. The
|
| 453 |
+
``*_proj_input_global_scale`` pair is the NVFP4 activation second level (a checkpoint's
|
| 454 |
+
calibrated per-projection ``input_scale``): the gate_up quantizes hidden against its own,
|
| 455 |
+
requants the intermediate against the down's, and the down consumes that as its activation
|
| 456 |
+
global. ``None`` is dynamic quant everywhere else. ``simulate_unfused`` (testing) rounds each
|
| 457 |
+
step through the activation dtype so the output matches the unfused reference to
|
| 458 |
+
reduce order. ``act_fn`` is a
|
| 459 |
+
``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
|
| 460 |
+
callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
|
| 461 |
+
projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
|
| 462 |
+
— the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
|
| 463 |
+
fmt = _block_format(
|
| 464 |
+
gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
|
| 465 |
+
)
|
| 466 |
+
glu = _fused_glu(act_fn, swiglu_alpha, swiglu_limit, simulate_unfused, gate)
|
| 467 |
+
NUM_EXPERTS = gate_up_proj.size(0)
|
| 468 |
+
expert_ids = top_k_index.reshape(-1)
|
| 469 |
+
gather_idx = _gather_idx(top_k_index)
|
| 470 |
+
|
| 471 |
+
# Phase 1: gate_up + SiLU + requant in the block format -> per-row quantized
|
| 472 |
+
# intermediate (the op quantizes the raw activations). gather_idx reads each routed
|
| 473 |
+
# row from the unexpanded hidden in-kernel (no copy).
|
| 474 |
+
# (C, Cs) under a requant format; a bare Tensor on the full-precision path
|
| 475 |
+
gate_up_out = matmul_batched(
|
| 476 |
+
hidden_states,
|
| 477 |
+
gate_up_proj,
|
| 478 |
+
Bs=gate_up_proj_scale_inv,
|
| 479 |
+
a_global_scale=gate_up_proj_input_global_scale,
|
| 480 |
+
b_global_scale=gate_up_proj_weight_global_scale,
|
| 481 |
+
# the two-level handoff, as in the grouped sibling
|
| 482 |
+
output_global_scale=down_proj_input_global_scale,
|
| 483 |
+
expert_ids=expert_ids,
|
| 484 |
+
**glu,
|
| 485 |
+
bias=gate_up_proj_bias,
|
| 486 |
+
# Decode (batched): "bf16" (weight-only) leaves the intermediate bf16, no requant.
|
| 487 |
+
# Block-FP8: INSIDE the unstacked decode band the requant fuses into the GLU kernel
|
| 488 |
+
# (``fused_glu(quant_group=...)`` — one launch, hands the down a ready fp8+scales intermediate and
|
| 489 |
+
# kills its offline act quant); ABOVE the band the stacked epilogue's requant pins
|
| 490 |
+
# the gate|up tile to the whole block scale and halves the grid, so the bf16 handoff
|
| 491 |
+
# (down inline-quants) stays the win there.
|
| 492 |
+
activation_format=fmt,
|
| 493 |
+
quantize_output=(
|
| 494 |
+
bool(glu) and fmt != "bf16" and (fmt != "fp8" or expert_ids.numel() <= GATE_UNSTACK_MAX_S)
|
| 495 |
+
),
|
| 496 |
+
output_dtype=hidden_states.dtype,
|
| 497 |
+
gather_idx=gather_idx,
|
| 498 |
+
)
|
| 499 |
+
inter, inter_scale = (
|
| 500 |
+
gate_up_out if isinstance(gate_up_out, tuple) else (gate_up_out, None)
|
| 501 |
+
)
|
| 502 |
+
if not glu:
|
| 503 |
+
inter = _host_glu(inter, act_fn, swiglu_alpha, swiglu_limit, gate)
|
| 504 |
+
# Phase 2: batched down over the intermediate (its dtypes carry the format; already
|
| 505 |
+
# routed-order, no gather).
|
| 506 |
+
down_out = matmul_batched(
|
| 507 |
+
inter,
|
| 508 |
+
down_proj,
|
| 509 |
+
As=inter_scale,
|
| 510 |
+
Bs=down_proj_scale_inv,
|
| 511 |
+
a_global_scale=down_proj_input_global_scale,
|
| 512 |
+
b_global_scale=down_proj_weight_global_scale,
|
| 513 |
+
expert_ids=expert_ids,
|
| 514 |
+
bias=down_proj_bias,
|
| 515 |
+
# weight-only / block-FP8: the intermediate is bf16 (As is None), so the down carries the
|
| 516 |
+
# format and quantizes it, mirroring the unfused sibling; a quantized MX intermediate
|
| 517 |
+
# carries its format in its own dtypes.
|
| 518 |
+
activation_format=fmt if inter_scale is None or fmt == "fp8" else None,
|
| 519 |
+
output_dtype=hidden_states.dtype,
|
| 520 |
+
)
|
| 521 |
+
# Phase 3: routing-weighted top-k reduce -> (num_tokens, hidden_dim). simulate_unfused
|
| 522 |
+
# rounds each weighted contrib to the activation dtype before summing, matching the
|
| 523 |
+
# unfused path's torch reduce (which materializes bf16 contribs); production
|
| 524 |
+
# accumulates in fp32.
|
| 525 |
+
rows, norm_kwargs = _fused_post_expert_norm(
|
| 526 |
+
down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
|
| 527 |
+
)
|
| 528 |
+
return weighted_reduce(
|
| 529 |
+
rows, top_k_index, top_k_weights, NUM_EXPERTS, simulate_unfused, **norm_kwargs
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
# ── Unfused (plain GEMMs + host GLU) ──────────────────────────────────────────
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def moe_unfused_grouped(
|
| 537 |
+
hidden_states: torch.Tensor,
|
| 538 |
+
top_k_index: torch.Tensor,
|
| 539 |
+
top_k_weights: torch.Tensor,
|
| 540 |
+
gate_up_proj: torch.Tensor,
|
| 541 |
+
down_proj: torch.Tensor,
|
| 542 |
+
gate_up_proj_scale_inv: torch.Tensor,
|
| 543 |
+
down_proj_scale_inv: torch.Tensor,
|
| 544 |
+
gate_up_proj_weight_global_scale: torch.Tensor | None = None,
|
| 545 |
+
down_proj_weight_global_scale: torch.Tensor | None = None,
|
| 546 |
+
gate_up_proj_input_global_scale: torch.Tensor | None = None,
|
| 547 |
+
down_proj_input_global_scale: torch.Tensor | None = None,
|
| 548 |
+
post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
|
| 549 |
+
# get_supported_norms() name (fused into the reduce) or a host callable
|
| 550 |
+
post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
|
| 551 |
+
post_expert_norm_eps: float = 1e-6,
|
| 552 |
+
gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
|
| 553 |
+
down_proj_bias: torch.Tensor | None = None, # (E, H)
|
| 554 |
+
act_fn: str | Callable = "silu",
|
| 555 |
+
swiglu_alpha: float | None = None,
|
| 556 |
+
swiglu_limit: float | None = None,
|
| 557 |
+
activation_format: str | None = None,
|
| 558 |
+
gate: bool = True,
|
| 559 |
+
) -> torch.Tensor:
|
| 560 |
+
"""Unfused grouped MoE: gate_up (plain grouped GEMM, gather hidden) → host ``apply_glu`` →
|
| 561 |
+
down (plain grouped GEMM, scatter to routed rows) → routing-weighted reduce. Same math as
|
| 562 |
+
``moe_fused_grouped`` but the SwiGLU + intermediate quant happen between two plain GEMMs
|
| 563 |
+
rather than inside the gate_up epilogue; each GEMM quantizes its raw input in
|
| 564 |
+
``activation_format`` (``None`` follows the weight format, mirroring the fused forward — mxfp4
|
| 565 |
+
weights run the all-fp4 W4A4 chain). All formats route through the shared ``matmul_grouped``. The NVFP4 activation globals thread the same way as the fused sibling:
|
| 566 |
+
each GEMM quantizes its raw input against its ``*_input_global_scale``. ``act_fn`` is a
|
| 567 |
+
``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
|
| 568 |
+
callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
|
| 569 |
+
projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
|
| 570 |
+
— the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
|
| 571 |
+
fmt = _block_format(
|
| 572 |
+
gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
num_top_k = top_k_index.size(-1)
|
| 576 |
+
NUM_EXPERTS = gate_up_proj.size(0)
|
| 577 |
+
expert_start, gather_idx, scatter_idx = compute_grouped_scheduling(
|
| 578 |
+
top_k_index, NUM_EXPERTS, num_top_k
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
# gate_up as a plain GEMM (no gate epilogue) over gathered hidden -> expert-ordered (S, 2I).
|
| 582 |
+
gate_up_out = matmul_grouped(
|
| 583 |
+
hidden_states,
|
| 584 |
+
gate_up_proj,
|
| 585 |
+
Bs=gate_up_proj_scale_inv,
|
| 586 |
+
a_global_scale=gate_up_proj_input_global_scale,
|
| 587 |
+
b_global_scale=gate_up_proj_weight_global_scale,
|
| 588 |
+
expert_start=expert_start,
|
| 589 |
+
bias=gate_up_proj_bias,
|
| 590 |
+
activation_format=fmt,
|
| 591 |
+
output_dtype=hidden_states.dtype,
|
| 592 |
+
gather_idx=gather_idx,
|
| 593 |
+
)
|
| 594 |
+
inter = _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate)
|
| 595 |
+
# down over the expert-ordered intermediate (quantized in the same format), scattering
|
| 596 |
+
# to routed rows.
|
| 597 |
+
down_out = matmul_grouped(
|
| 598 |
+
inter,
|
| 599 |
+
down_proj,
|
| 600 |
+
Bs=down_proj_scale_inv,
|
| 601 |
+
a_global_scale=down_proj_input_global_scale,
|
| 602 |
+
b_global_scale=down_proj_weight_global_scale,
|
| 603 |
+
expert_start=expert_start,
|
| 604 |
+
bias=down_proj_bias,
|
| 605 |
+
activation_format=fmt,
|
| 606 |
+
output_dtype=hidden_states.dtype,
|
| 607 |
+
scatter_idx=scatter_idx,
|
| 608 |
+
)
|
| 609 |
+
return _torch_weighted_reduce(
|
| 610 |
+
_post_expert_norm(down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps),
|
| 611 |
+
top_k_index, top_k_weights, NUM_EXPERTS,
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
def moe_torch_grouped(
|
| 616 |
+
hidden_states: torch.Tensor, # (T, H)
|
| 617 |
+
top_k_index: torch.Tensor, # (T, K) int
|
| 618 |
+
top_k_weights: torch.Tensor, # (T, K)
|
| 619 |
+
gate_up_proj: torch.Tensor, # (E, 2I, H) E4M3
|
| 620 |
+
down_proj: torch.Tensor, # (E, H, I) E4M3
|
| 621 |
+
gate_up_proj_scale_inv: torch.Tensor, # gate_up scale through torchao's triton_mx_block_rearrange_per_group_3d (NOT swizzle_mx_scales)
|
| 622 |
+
down_proj_scale_inv: torch.Tensor, # down scale through the same torchao rearrange
|
| 623 |
+
gate_up_proj_weight_global_scale: torch.Tensor | None = None,
|
| 624 |
+
down_proj_weight_global_scale: torch.Tensor | None = None,
|
| 625 |
+
gate_up_proj_input_global_scale: torch.Tensor | None = None,
|
| 626 |
+
down_proj_input_global_scale: torch.Tensor | None = None,
|
| 627 |
+
post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
|
| 628 |
+
# get_supported_norms() name (fused into the reduce) or a host callable
|
| 629 |
+
post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
|
| 630 |
+
post_expert_norm_eps: float = 1e-6,
|
| 631 |
+
gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
|
| 632 |
+
down_proj_bias: torch.Tensor | None = None, # (E, H)
|
| 633 |
+
act_fn: str = "silu",
|
| 634 |
+
swiglu_alpha: float | None = None,
|
| 635 |
+
swiglu_limit: float | None = None,
|
| 636 |
+
activation_format: str | None = None,
|
| 637 |
+
) -> torch.Tensor:
|
| 638 |
+
"""Torch-only MX grouped MoE — the fair cuBLAS baseline for ``moe_fused_grouped`` /
|
| 639 |
+
``moe_unfused_grouped`` on the PUBLIC ``torch.nn.functional.scaled_grouped_mm``. Same weights,
|
| 640 |
+
scales, and routing as our forwards; the only difference is the machinery torch forces:
|
| 641 |
+
|
| 642 |
+
- Routing by **sort**, not our on-device gather/scatter: stable-argsort the ``T*K`` routed slots
|
| 643 |
+
by expert into contiguous groups (cumulative ``offs``).
|
| 644 |
+
- Two ``scaled_grouped_mm`` calls (per-format ``ScalingType``: group-32 ``BlockWise1x32`` for
|
| 645 |
+
mxfp8/mxfp4, group-16 ``BlockWise1x16`` for nvfp4; fp4 operands viewed as ``e2m1_x2``).
|
| 646 |
+
- Our Triton MX act-quant (so torch is timed on the same fast quant), the shared host ``apply_glu``,
|
| 647 |
+
and the shared ``_torch_weighted_reduce``. All three MX formats.
|
| 648 |
+
|
| 649 |
+
WEIGHT scales arrive already SWIZZLE_32_4_4-blocked by **torchao's**
|
| 650 |
+
``triton_mx_block_rearrange_per_group_3d`` (done once offline — a real deployment doesn't
|
| 651 |
+
reblock a fixed weight every forward); this is scaled_grouped_mm's own layout, NOT the
|
| 652 |
+
``swizzle_mx_scales`` artifact the other four forwards consume. The timed loop only blocks the
|
| 653 |
+
ACTIVATION scale (which changes each call). The format is read off the dtypes (the block
|
| 654 |
+
preserves them: E4M3 scale = NVFP4, uint8 = MX; packed-E2M1 weight = int8) since the blocked
|
| 655 |
+
shape no longer matches the group-shape detectors."""
|
| 656 |
+
assert gate_up_proj.dtype in (torch.int8, torch.float8_e4m3fn), (
|
| 657 |
+
"torch grouped baseline is MX-only (packed E2M1 or E4M3 weights)"
|
| 658 |
+
)
|
| 659 |
+
assert gate_up_proj_scale_inv.ndim not in (5, 6) and down_proj_scale_inv.ndim not in (5, 6), (
|
| 660 |
+
"the torch baseline consumes torchao's triton_mx_block_rearrange_per_group_3d layout, "
|
| 661 |
+
"not the swizzle_mx_scales artifact — a byte-compatible wrong layout would misread silently"
|
| 662 |
+
)
|
| 663 |
+
assert activation_format != "bf16", (
|
| 664 |
+
"the torch baseline always quantizes activations (scaled_grouped_mm has no bf16-act x "
|
| 665 |
+
"MX-weight form) — activation_format='bf16' (W4A16/W8A16) is not representable here"
|
| 666 |
+
)
|
| 667 |
+
|
| 668 |
+
import torch.nn.functional as F
|
| 669 |
+
from torch.nn.functional import ScalingType, SwizzleType
|
| 670 |
+
|
| 671 |
+
# torchao >= 0.18 required with cutlass-dsl >= 4.6 (0.17 imports a helper path 4.6
|
| 672 |
+
# removed; fixed upstream in pytorch/ao).
|
| 673 |
+
# torchao's per-group blocked-scale builder for the per-forward ACTIVATION scale (graph-capturable
|
| 674 |
+
# @triton_op, same S+128·E static padding + SWIZZLE_32_4_4 layout scaled_grouped_mm consumes). The
|
| 675 |
+
# weight scale is already blocked (offline); only the act scale is blocked here.
|
| 676 |
+
from torchao.prototype.moe_training.kernels.mxfp8 import (
|
| 677 |
+
triton_mx_block_rearrange_2d_M_groups,
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
nvfp4 = gate_up_proj_scale_inv.dtype == torch.float8_e4m3fn
|
| 681 |
+
packed = gate_up_proj.dtype == torch.int8 # fp4 formats pack e2m1
|
| 682 |
+
family = "nvfp4" if nvfp4 else "mxfp4" if packed else "mxfp8"
|
| 683 |
+
act_format = family if activation_format is None else activation_format
|
| 684 |
+
scale_group = NVFP4_SCALE_GROUP_K if nvfp4 else MX_SCALE_GROUP_K
|
| 685 |
+
scale_dtype = (
|
| 686 |
+
torch.float8_e4m3fn if nvfp4 else torch.uint8
|
| 687 |
+
) # our act-quant/swizzle carry uint8
|
| 688 |
+
# scaled_grouped_mm dispatches on the scale dtype — view the uint8 MX scales as e8m0 for it
|
| 689 |
+
f_dtype = torch.float8_e4m3fn if nvfp4 else torch.float8_e8m0fnu
|
| 690 |
+
SWZ = SwizzleType.SWIZZLE_32_4_4
|
| 691 |
+
BW = ScalingType.BlockWise1x16 if nvfp4 else ScalingType.BlockWise1x32
|
| 692 |
+
FP4 = getattr(torch, "float4_e2m1fn_x2", None)
|
| 693 |
+
E = gate_up_proj.shape[0]
|
| 694 |
+
# NVFP4's tcgen05 MMA kind requires TWO-level scaling: the e4m3 per-16 block scale AND a
|
| 695 |
+
# per-tensor global fp32 scale — unlike our kernels there is no "no global" form, so a
|
| 696 |
+
# missing calibrated global rides as identity 1.0 (dynamic quant). Weight globals arrive
|
| 697 |
+
# as the separate *_global_scale args; a calibrated *_input_global_scale normalizes that
|
| 698 |
+
# GEMM's activation quant and rides as its TensorWise scale. MX formats are single-level.
|
| 699 |
+
tensorwise = ScalingType.TensorWise
|
| 700 |
+
|
| 701 |
+
def _tensorwise_global(g, n): # (n,) fp32 TensorWise operand, identity when uncalibrated
|
| 702 |
+
if g is None:
|
| 703 |
+
return torch.ones(n, device=hidden_states.device, dtype=torch.float32)
|
| 704 |
+
if g.numel() == 1:
|
| 705 |
+
return g.reshape(-1).float().expand(n).contiguous()
|
| 706 |
+
assert g.shape == (n,), (
|
| 707 |
+
f"torch's scaled_grouped_mm takes ONE TensorWise global per expert, got "
|
| 708 |
+
f"{tuple(g.shape)} — a per-expert activation global has no slot in this baseline "
|
| 709 |
+
"(the Triton ops take one)"
|
| 710 |
+
)
|
| 711 |
+
return g.float()
|
| 712 |
+
|
| 713 |
+
top_k = top_k_index.shape[1]
|
| 714 |
+
out_dtype = hidden_states.dtype
|
| 715 |
+
|
| 716 |
+
# route: stable-sort routed slots by expert into contiguous groups (torch has no gather/scatter fuse)
|
| 717 |
+
flat_e = top_k_index.reshape(-1)
|
| 718 |
+
order = torch.argsort(flat_e, stable=True)
|
| 719 |
+
counts = torch.histc(flat_e.float(), bins=E, min=0, max=E - 1).to(torch.int32)
|
| 720 |
+
offs = counts.cumsum(0).to(torch.int32)
|
| 721 |
+
tok = (order // top_k).to(torch.long) # source token of each sorted slot
|
| 722 |
+
slot_e = flat_e[order].to(torch.long) # expert of each sorted slot, for the per-expert biases
|
| 723 |
+
|
| 724 |
+
def pk(t): # view a packed-e2m1 operand as torch's fp4 dtype for scaled_grouped_mm
|
| 725 |
+
return t.view(FP4) if packed else t
|
| 726 |
+
|
| 727 |
+
def aswz(a_s): # (S, K//G) -> per-group blocked layout, one launch
|
| 728 |
+
return triton_mx_block_rearrange_2d_M_groups(a_s.view(torch.uint8), offs).view(
|
| 729 |
+
f_dtype
|
| 730 |
+
)
|
| 731 |
+
|
| 732 |
+
def wswz(
|
| 733 |
+
w_s,
|
| 734 |
+
): # weight scale is pre-blocked offline (SWIZZLE_32_4_4) — pass through, no per-call kernel
|
| 735 |
+
return w_s.view(f_dtype)
|
| 736 |
+
|
| 737 |
+
def grouped_mm(a, w_q, w_s, w_g=None, a_g=None):
|
| 738 |
+
assert a_g is None or nvfp4, "an activation global is NVFP4-only" # match the ops
|
| 739 |
+
assert a_g is None or a_g.numel() == 1, (
|
| 740 |
+
"this baseline quantizes the routed rows in one pass, so the activation global is "
|
| 741 |
+
"per tensor; per-expert activation globals are a Triton-op path"
|
| 742 |
+
)
|
| 743 |
+
# our Triton MX act-quant (format-taking launcher) — torch is timed on the same fast quant
|
| 744 |
+
aq, a_s = _launch_act_quant(
|
| 745 |
+
a, act_format, scale_group, scale_dtype, global_scale=a_g
|
| 746 |
+
)
|
| 747 |
+
sa, ra = aswz(a_s), BW
|
| 748 |
+
sb, rb = wswz(w_s), BW
|
| 749 |
+
if nvfp4: # two-level: block e4m3 + the per-tensor/per-expert fp32 globals
|
| 750 |
+
sa, ra = [sa, _tensorwise_global(a_g, 1)], [BW, tensorwise]
|
| 751 |
+
sb, rb = [sb, _tensorwise_global(w_g, E)], [BW, tensorwise]
|
| 752 |
+
return F.scaled_grouped_mm(
|
| 753 |
+
pk(aq),
|
| 754 |
+
pk(w_q).transpose(-2, -1),
|
| 755 |
+
sa,
|
| 756 |
+
ra,
|
| 757 |
+
sb,
|
| 758 |
+
rb,
|
| 759 |
+
swizzle_a=SWZ,
|
| 760 |
+
swizzle_b=SWZ,
|
| 761 |
+
offs=offs,
|
| 762 |
+
output_dtype=out_dtype,
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
gate_up = grouped_mm(
|
| 766 |
+
hidden_states[tok],
|
| 767 |
+
gate_up_proj,
|
| 768 |
+
gate_up_proj_scale_inv,
|
| 769 |
+
gate_up_proj_weight_global_scale,
|
| 770 |
+
gate_up_proj_input_global_scale,
|
| 771 |
+
)
|
| 772 |
+
# torch has no fused bias, so this baseline adds both host-side; rows are expert-sorted here,
|
| 773 |
+
# so the per-expert bias indexes by the sorted expert ids
|
| 774 |
+
if gate_up_proj_bias is not None:
|
| 775 |
+
gate_up = gate_up + gate_up_proj_bias[slot_e]
|
| 776 |
+
inter = fused_glu(gate_up, act_fn, swiglu_alpha, swiglu_limit)
|
| 777 |
+
down_out = grouped_mm(
|
| 778 |
+
inter, down_proj, down_proj_scale_inv,
|
| 779 |
+
down_proj_weight_global_scale, down_proj_input_global_scale,
|
| 780 |
+
)
|
| 781 |
+
|
| 782 |
+
# One weighted scatter-reduce: down_out is expert-sorted, so index_add_ over the source-token
|
| 783 |
+
# map fuses unroute + routing-weight + top-k sum into (T, H) directly — no separate unsort pass.
|
| 784 |
+
out = torch.zeros_like(hidden_states)
|
| 785 |
+
w = top_k_weights.reshape(-1)[order].unsqueeze(-1).to(out.dtype)
|
| 786 |
+
if down_proj_bias is not None:
|
| 787 |
+
down_out = down_out + down_proj_bias[slot_e]
|
| 788 |
+
down_out = _post_expert_norm(
|
| 789 |
+
down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps
|
| 790 |
+
)
|
| 791 |
+
return out.index_add_(0, tok, down_out * w)
|
| 792 |
+
|
| 793 |
+
|
| 794 |
+
def moe_unfused_batched(
|
| 795 |
+
hidden_states: torch.Tensor,
|
| 796 |
+
top_k_index: torch.Tensor,
|
| 797 |
+
top_k_weights: torch.Tensor,
|
| 798 |
+
gate_up_proj: torch.Tensor,
|
| 799 |
+
down_proj: torch.Tensor,
|
| 800 |
+
gate_up_proj_scale_inv: torch.Tensor,
|
| 801 |
+
down_proj_scale_inv: torch.Tensor,
|
| 802 |
+
gate_up_proj_weight_global_scale: torch.Tensor | None = None,
|
| 803 |
+
down_proj_weight_global_scale: torch.Tensor | None = None,
|
| 804 |
+
gate_up_proj_input_global_scale: torch.Tensor | None = None,
|
| 805 |
+
down_proj_input_global_scale: torch.Tensor | None = None,
|
| 806 |
+
post_expert_norm=None, # the model's per-expert output norm on the routed rows: a
|
| 807 |
+
# get_supported_norms() name (fused into the reduce) or a host callable
|
| 808 |
+
post_expert_norm_weight: torch.Tensor | None = None, # (H,) weight of a named norm
|
| 809 |
+
post_expert_norm_eps: float = 1e-6,
|
| 810 |
+
gate_up_proj_bias: torch.Tensor | None = None, # (E, 2I) pre-activation bias
|
| 811 |
+
down_proj_bias: torch.Tensor | None = None, # (E, H)
|
| 812 |
+
act_fn: str | Callable = "silu",
|
| 813 |
+
swiglu_alpha: float | None = None,
|
| 814 |
+
swiglu_limit: float | None = None,
|
| 815 |
+
activation_format: str | None = None,
|
| 816 |
+
gate: bool = True,
|
| 817 |
+
) -> torch.Tensor:
|
| 818 |
+
"""Unfused batched MoE: gate_up (plain batched GEMM, gather hidden) → host ``apply_glu`` →
|
| 819 |
+
down (plain batched GEMM) → routing-weighted reduce. Same math as ``moe_fused_batched`` but
|
| 820 |
+
the SwiGLU + intermediate quant happen between two plain GEMMs; each GEMM quantizes its raw
|
| 821 |
+
input in ``activation_format`` (``None`` follows the weight format, ``"bf16"`` is weight-only). All
|
| 822 |
+
formats route through the shared ``matmul_batched``. The NVFP4 activation globals thread the same way as the fused sibling:
|
| 823 |
+
each GEMM quantizes its raw input against its ``*_input_global_scale``. ``act_fn`` is a
|
| 824 |
+
``get_supported_act_fns()`` name (fused into the gate_up epilogue where the forward fuses) or any
|
| 825 |
+
callable applied on the host to the raw gate_up output; ``gate=False`` runs an ungated
|
| 826 |
+
projection. Scales are affine or pre-swizzled (``SWIZZLE_32_4_4``, self-describing 5-D)
|
| 827 |
+
— the swizzled layout is for the dot_scaled arm; weight-only chains take affine."""
|
| 828 |
+
fmt = _block_format(
|
| 829 |
+
gate_up_proj, gate_up_proj_scale_inv, down_proj, down_proj_scale_inv, activation_format
|
| 830 |
+
)
|
| 831 |
+
NUM_EXPERTS = gate_up_proj.size(0)
|
| 832 |
+
expert_ids = top_k_index.reshape(-1)
|
| 833 |
+
gather_idx = _gather_idx(top_k_index)
|
| 834 |
+
|
| 835 |
+
# gate_up as a plain GEMM (no gate epilogue) over gathered hidden -> (S, 2I).
|
| 836 |
+
gate_up_out = matmul_batched(
|
| 837 |
+
hidden_states,
|
| 838 |
+
gate_up_proj,
|
| 839 |
+
Bs=gate_up_proj_scale_inv,
|
| 840 |
+
a_global_scale=gate_up_proj_input_global_scale,
|
| 841 |
+
b_global_scale=gate_up_proj_weight_global_scale,
|
| 842 |
+
expert_ids=expert_ids,
|
| 843 |
+
bias=gate_up_proj_bias,
|
| 844 |
+
activation_format=fmt,
|
| 845 |
+
output_dtype=hidden_states.dtype,
|
| 846 |
+
gather_idx=gather_idx,
|
| 847 |
+
)
|
| 848 |
+
inter = _host_glu(gate_up_out, act_fn, swiglu_alpha, swiglu_limit, gate)
|
| 849 |
+
# down over the intermediate (quantized in the same format), routed-order output.
|
| 850 |
+
down_out = matmul_batched(
|
| 851 |
+
inter,
|
| 852 |
+
down_proj,
|
| 853 |
+
Bs=down_proj_scale_inv,
|
| 854 |
+
a_global_scale=down_proj_input_global_scale,
|
| 855 |
+
b_global_scale=down_proj_weight_global_scale,
|
| 856 |
+
expert_ids=expert_ids,
|
| 857 |
+
bias=down_proj_bias,
|
| 858 |
+
activation_format=fmt,
|
| 859 |
+
output_dtype=hidden_states.dtype,
|
| 860 |
+
)
|
| 861 |
+
return _torch_weighted_reduce(
|
| 862 |
+
_post_expert_norm(down_out, post_expert_norm, post_expert_norm_weight, post_expert_norm_eps),
|
| 863 |
+
top_k_index, top_k_weights, NUM_EXPERTS,
|
| 864 |
+
)
|
build/torch-xpu/norm.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Row-wise RMS normalization, in the three forms models write it.
|
| 16 |
+
|
| 17 |
+
Every form is ``x * factor(h) * rsqrt(mean(y**2) + eps)`` over the row: the per-column
|
| 18 |
+
``factor`` is the norm weight (``"rms_norm"``) or ``1 + weight`` (the zero-centered
|
| 19 |
+
parameterization), and ``y`` is the raw row except under ``"input_scaled_rms_norm"``, which
|
| 20 |
+
scales BEFORE normalizing so the row's mean square is taken on the scaled values.
|
| 21 |
+
|
| 22 |
+
The MoE chain never materializes the normalized rows: ``rms_inv_rows`` computes the per-row
|
| 23 |
+
``rsqrt`` in one pass and ``weighted_reduce`` folds it — with the column factor — into the
|
| 24 |
+
reduce it already runs. ``rms_norm_rows`` is the standalone form for the paths that have no
|
| 25 |
+
reduce to fold into."""
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import triton
|
| 29 |
+
import triton.language as tl
|
| 30 |
+
|
| 31 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 32 |
+
from .compat import compile_time_only_triton_wrap, device_context
|
| 33 |
+
|
| 34 |
+
# the fused names, by how a model composes the weight and the normalization
|
| 35 |
+
RMS_NORMS = ("rms_norm", "centered_rms_norm", "input_scaled_rms_norm")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_supported_norms() -> tuple[str, ...]:
|
| 39 |
+
"""The norm names the kernels fuse (``post_expert_norm``), each ``x * factor * rsqrt(mean +
|
| 40 |
+
eps)`` over the row: ``"rms_norm"`` scales the normalized row by ``weight``,
|
| 41 |
+
``"centered_rms_norm"`` by ``1 + weight`` (the zero-centered parameterization), and
|
| 42 |
+
``"input_scaled_rms_norm"`` scales by ``1 + weight`` BEFORE normalizing, so the mean square
|
| 43 |
+
is taken on the scaled row. Anything else stays a host callable."""
|
| 44 |
+
return RMS_NORMS
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@triton.jit
|
| 48 |
+
def norm_column_factor(W, offs_h, mask, NORM: tl.constexpr):
|
| 49 |
+
"""The per-column multiplier of a fused norm: the weight, or ``1 + weight`` under the
|
| 50 |
+
zero-centered parameterizations."""
|
| 51 |
+
w = tl.load(W + offs_h, mask=mask, other=0.0).to(tl.float32)
|
| 52 |
+
if NORM == "rms_norm":
|
| 53 |
+
factor = w
|
| 54 |
+
else:
|
| 55 |
+
factor = 1.0 + w
|
| 56 |
+
return factor
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@bayesian_autotune(
|
| 60 |
+
[
|
| 61 |
+
triton.Config({"BLOCK_H": block_h}, num_warps=warps)
|
| 62 |
+
for block_h in (256, 512, 1024, 2048)
|
| 63 |
+
for warps in (4, 8)
|
| 64 |
+
],
|
| 65 |
+
# one program per row, so the tile width trades the row's loop length against occupancy
|
| 66 |
+
["H", "NORM", "WRITE_ROWS"],
|
| 67 |
+
n_trials=8,
|
| 68 |
+
)
|
| 69 |
+
@triton.jit
|
| 70 |
+
def _rms_norm_kernel(
|
| 71 |
+
X, # (S, H) rows to normalize
|
| 72 |
+
W, # (H,) norm weight
|
| 73 |
+
Out, # (S, H) normalized rows; None writes only Inv (the fused chain's one-pass arm)
|
| 74 |
+
Inv, # (S,) fp32 per-row rsqrt(mean + eps); None skips it
|
| 75 |
+
H,
|
| 76 |
+
eps,
|
| 77 |
+
stride_x_m,
|
| 78 |
+
stride_x_h,
|
| 79 |
+
stride_o_m,
|
| 80 |
+
stride_o_h,
|
| 81 |
+
NORM: tl.constexpr,
|
| 82 |
+
WRITE_ROWS: tl.constexpr, # Out is written (else the pass stops at Inv)
|
| 83 |
+
BLOCK_H: tl.constexpr,
|
| 84 |
+
):
|
| 85 |
+
"""One program per row: accumulate the row's mean square in fp32 over ``BLOCK_H`` tiles, then
|
| 86 |
+
``Inv[row] = rsqrt(mean + eps)`` and, under ``WRITE_ROWS``, a second pass writing
|
| 87 |
+
``x * factor * inv`` (the row is L2-resident by then). ``input_scaled_rms_norm`` squares the
|
| 88 |
+
SCALED values, the other forms the raw ones — the one place the three differ."""
|
| 89 |
+
row = tl.program_id(0)
|
| 90 |
+
x_row = X + row * stride_x_m
|
| 91 |
+
square_sum = tl.zeros((), tl.float32)
|
| 92 |
+
for h in range(0, tl.cdiv(H, BLOCK_H)):
|
| 93 |
+
offs_h = h * BLOCK_H + tl.arange(0, BLOCK_H)
|
| 94 |
+
mask = offs_h < H
|
| 95 |
+
x = tl.load(x_row + offs_h * stride_x_h, mask=mask, other=0.0).to(tl.float32)
|
| 96 |
+
if NORM == "input_scaled_rms_norm":
|
| 97 |
+
x = x * norm_column_factor(W, offs_h, mask, NORM)
|
| 98 |
+
square_sum += tl.sum(x * x, 0)
|
| 99 |
+
inv = tl.rsqrt(square_sum / H + eps)
|
| 100 |
+
if Inv is not None:
|
| 101 |
+
tl.store(Inv + row, inv)
|
| 102 |
+
if WRITE_ROWS:
|
| 103 |
+
out_row = Out + row * stride_o_m
|
| 104 |
+
for h in range(0, tl.cdiv(H, BLOCK_H)):
|
| 105 |
+
offs_h = h * BLOCK_H + tl.arange(0, BLOCK_H)
|
| 106 |
+
mask = offs_h < H
|
| 107 |
+
x = tl.load(x_row + offs_h * stride_x_h, mask=mask, other=0.0).to(tl.float32)
|
| 108 |
+
out = x * norm_column_factor(W, offs_h, mask, NORM) * inv
|
| 109 |
+
tl.store(out_row + offs_h * stride_o_h, out.to(Out.dtype.element_ty), mask=mask)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _launch_rms_norm(x, weight, eps, norm, write_rows):
|
| 113 |
+
"""One launch of ``_rms_norm_kernel`` over ``x``'s rows, returning the normalized rows or the
|
| 114 |
+
per-row ``rsqrt`` — whichever was asked for is the only one allocated."""
|
| 115 |
+
assert norm in RMS_NORMS, f"norm must be one of {RMS_NORMS} or a callable, got {norm!r}"
|
| 116 |
+
assert x.ndim == 2, f"rows must be 2D (rows, H), got ndim={x.ndim}"
|
| 117 |
+
assert weight.ndim == 1 and weight.shape[0] == x.shape[1], (
|
| 118 |
+
f"norm weight {tuple(weight.shape)} does not match the row width ({x.shape[1]},)"
|
| 119 |
+
)
|
| 120 |
+
S, H = x.shape
|
| 121 |
+
out = torch.empty_like(x) if write_rows else None
|
| 122 |
+
inv = None if write_rows else torch.empty(S, device=x.device, dtype=torch.float32)
|
| 123 |
+
with device_context(x.device):
|
| 124 |
+
compile_time_only_triton_wrap(_rms_norm_kernel)[(S,)](
|
| 125 |
+
x,
|
| 126 |
+
weight,
|
| 127 |
+
out,
|
| 128 |
+
inv,
|
| 129 |
+
H,
|
| 130 |
+
eps,
|
| 131 |
+
x.stride(0),
|
| 132 |
+
x.stride(1),
|
| 133 |
+
out.stride(0) if write_rows else 0,
|
| 134 |
+
out.stride(1) if write_rows else 0,
|
| 135 |
+
NORM=norm,
|
| 136 |
+
WRITE_ROWS=write_rows,
|
| 137 |
+
)
|
| 138 |
+
return out if write_rows else inv
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def rms_norm_rows(
|
| 142 |
+
x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6, norm: str = "rms_norm"
|
| 143 |
+
) -> torch.Tensor:
|
| 144 |
+
"""RMS-normalize ``(S, H)`` rows in one launch, in the ``get_supported_norms()`` form named by
|
| 145 |
+
``norm`` (fp32 accumulate, input dtype out). The MoE chain folds this into
|
| 146 |
+
``weighted_reduce`` instead; this is the standalone form."""
|
| 147 |
+
return _launch_rms_norm(x, weight, eps, norm, write_rows=True)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def rms_inv_rows(
|
| 151 |
+
x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6, norm: str = "rms_norm"
|
| 152 |
+
) -> torch.Tensor:
|
| 153 |
+
"""The ``(S,)`` fp32 ``rsqrt(mean + eps)`` of each row under ``norm`` — the half of
|
| 154 |
+
``rms_norm_rows`` a consumer that already reads the rows needs, so the rows are never
|
| 155 |
+
written back."""
|
| 156 |
+
return _launch_rms_norm(x, weight, eps, norm, write_rows=False)
|
build/torch-xpu/pruners.py
ADDED
|
@@ -0,0 +1,1213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import triton
|
| 17 |
+
|
| 18 |
+
from .compat import get_active_device_type, is_sm10x, is_sm90, sm_count, sm_shared_memory_limit
|
| 19 |
+
from .mma import MMA_N_ATOM_WIDTH
|
| 20 |
+
|
| 21 |
+
# ── config pruners ────────────────────────────────────────────────────────────
|
| 22 |
+
# Every guard exists for one of four reasons; the map (pruner -> rule -> attached to):
|
| 23 |
+
#
|
| 24 |
+
# SILENTLY-WRONG configs (must be pruned, the tuner would time and pick them):
|
| 25 |
+
# block_within_dim_pruner BK not dividing K over-reads rows (maskless K-loops) —
|
| 26 |
+
# tensor/weight-only/full-precision 2D+batched+grouped, and the
|
| 27 |
+
# first stage of mx_config_pruner, whose BN|N stage is scoped
|
| 28 |
+
# to the mx-dynamic AFFINE arm (its grouped scale gather has no
|
| 29 |
+
# wrap; swizzled masks its N tail). The weight-only batched and
|
| 30 |
+
# grouped kernels carry their own ungated BN|N stages.
|
| 31 |
+
# scalar "scalar" -> BM=1 the scalar GEVM broadcasts wrong at BM>1 (config builder)
|
| 32 |
+
# descriptor_needs_prequant_pruner raw-A inline-quant arm reads a_ptrs, dead under a
|
| 33 |
+
# descriptor mode — static 2D
|
| 34 |
+
# weight_only_swap_scope_pruner SWAP_AB iff COMPUTE_MODE == "scalar" — the swapped dot
|
| 35 |
+
# arms are unimplemented and a non-swapped scalar has no
|
| 36 |
+
# accumulator shape — weight-only batched
|
| 37 |
+
# mma_trap_ok swapped dot_scaled BK<128 (sticky 716), single-trip
|
| 38 |
+
# accumulator-init miscompile, width > 256 (sticky trap),
|
| 39 |
+
# packed-fp4 GATE BN<64 silent zeros — mx 2D/batched/grouped;
|
| 40 |
+
# raises when empty (never falls back to a miscompile)
|
| 41 |
+
# gate_stacked_tmem_trap_pruner GATE 2*BN=512 acc overflows tensor memory undetected
|
| 42 |
+
# at exactly BN=256/BM=64 (checker gap -> sticky 716) —
|
| 43 |
+
# weight-only grouped; raises when empty
|
| 44 |
+
# block_dynamic_mma_width_pruner the same width trap on the bd implicit dot_scaled
|
| 45 |
+
# arm (GATE 2*BN) — bd 2D only (grouped/batched bd pin their
|
| 46 |
+
# N tile to the quant block, within the cap); UE8M0-As-gated
|
| 47 |
+
# scale_subblock_pruner (whole-block clause) a tile wider than the quant block reads
|
| 48 |
+
# one scale across two blocks; raises when the grid has no
|
| 49 |
+
# fitting tile — bd/static batched
|
| 50 |
+
# COMPILER BUGS / unsupported combos on this triton+arch (benign inf, pruned to save
|
| 51 |
+
# compiles and to keep can't-win configs from poisoning the TPE densities):
|
| 52 |
+
# warp_spec_compile_guard_pruner WS compiles iff (BN if swapped else BM)>=64 &
|
| 53 |
+
# num_warps%4==0 — bd/tensor/mx/weight-only/fp 2D+grouped
|
| 54 |
+
# gated_pointer_weight_warp_spec_pruner GATE+pointer-weight WS lowering failures —
|
| 55 |
+
# weight-only grouped
|
| 56 |
+
# affine_scale_warp_spec_pruner affine scale gather's other=0.0 can't take
|
| 57 |
+
# ttg.partition under WS — mx-dynamic grouped
|
| 58 |
+
# global_scale_warp_spec_pruner the NVFP4 global load can't take ttg.partition
|
| 59 |
+
# under WS — weight-only grouped, BsGlobal-gated
|
| 60 |
+
# nvfp4_native_ok (mx_config_pruner) E4M3-scale dot_scaled PassManager-fails off the
|
| 61 |
+
# native staging: M operand < 128, num_warps outside {4, 8},
|
| 62 |
+
# or a weight-only kernel (no As — never native)
|
| 63 |
+
# descriptor_box_pruner TMA box > 256/dim is illegal — all descriptor-axis kernels
|
| 64 |
+
# smem_pruner empirical smem model (B200-sampled, keep-side error) —
|
| 65 |
+
# bd/static/mx/weight-only 2D; tensor/mx/weight-only/fp grouped;
|
| 66 |
+
# mx batched
|
| 67 |
+
# RACE guards (Triton 3.7.1 pipeliner, per-loop-structure flake maps):
|
| 68 |
+
# block_dynamic_grouped_matmul_pruner WS-only at BM>=64, non-WS below (disjoint) —
|
| 69 |
+
# bd + static grouped (plain and GATE arms, same loop)
|
| 70 |
+
# block_dynamic_2d_warp_spec_pruner bd 2D WS compile-fail laws: w16+WS at any tile,
|
| 71 |
+
# and the implicit dot_scaled arm (UE8M0 As, native-M tile)
|
| 72 |
+
# + WS — bd 2D only (COMPILER BUG class, listed here next to
|
| 73 |
+
# its sibling)
|
| 74 |
+
# TPE-POISON fences (valid configs that can't win in a regime):
|
| 75 |
+
# scalar_max_m_pruner scalar above M=64 (prefill GEVM) — mx 2D
|
| 76 |
+
# mx_config_pruner poison fences the dot arm where the
|
| 77 |
+
# native MMA exists (A/B'd 2026-07-14; weight-only kernels
|
| 78 |
+
# keep dot), mma_poison_ok: no-swap BK<128 where 128 divides K
|
| 79 |
+
# (basin claim) + swapped rows<128 (bf16 fallback never wins)
|
| 80 |
+
# scale_subblock_pruner sub-quant-block BN only when the grid starves — bd/static
|
| 81 |
+
# batched
|
| 82 |
+
# mx_2d_swap_scope_pruner 2D mx SWAP_AB rows only for E4M3-scale decode (BM=1,
|
| 83 |
+
# M<=16) — swap loses everywhere else (18-cell sweep)
|
| 84 |
+
# packed_schedule_scope_pruner PACKED_SCHEDULE rows only at BM>=128 (register relief
|
| 85 |
+
# is the whole win) — full-precision grouped
|
| 86 |
+
# swizzled_scales_bm_pruner grouped mx swizzled arm pinned to BM=BN=128 (slab layout);
|
| 87 |
+
# affine arm keeps every BM/BN
|
| 88 |
+
# swizzled_scale_config_pruner swizzled-arm BK%128, BN<=128, GATE block pairs —
|
| 89 |
+
# mx 2D/batched
|
| 90 |
+
# matched_memory_modes_pruner mixed pointer/descriptor never wins a DENSE 2D GEMM —
|
| 91 |
+
# bd/static 2D (grouped keeps mixed: it wins there)
|
| 92 |
+
# Every pruner is a ``config_filter``: a per-config ``ok(config, args)`` predicate, an
|
| 93 |
+
# optional ``when(args)`` regime gate, and an empty-survivor policy (advisory guards fall
|
| 94 |
+
# back to the untouched grid; the BK-within-K correctness veto raises via ``on_empty`` —
|
| 95 |
+
# silence there means wrong results). Ordered below: shared infrastructure, then the four
|
| 96 |
+
# categories above.
|
| 97 |
+
|
| 98 |
+
# sm_10x caps a single scaled MMA (dot_scaled) at N=256; Triton miscompiles wider ones
|
| 99 |
+
# into a sticky illegal-address device trap. Plain-dot MMAs have NO such cap: a forced
|
| 100 |
+
# width-512 GATE sweep on the tensor grouped kernel (2026-07-14) ran bit-exact wherever
|
| 101 |
+
# it fit shared memory and failed only as benign launch-time smem overflows.
|
| 102 |
+
SM10X_SCALED_MMA_MAX_N = 256
|
| 103 |
+
|
| 104 |
+
# Branch axes that RELOCATE the tile optimum (compute unit / operand orientation): the
|
| 105 |
+
# tuner's guaranteed max-tile anchors group by these (``path_anchor_axes`` — a declaration,
|
| 106 |
+
# so the tuner itself stays independent of configuration details). Scheduling axes
|
| 107 |
+
# (WARP_SPEC, PACKED_SCHEDULE, memory modes) share the tile basin shape and stay out —
|
| 108 |
+
# per-basin anchors over the full branch cross starved a 100-trial budget (see
|
| 109 |
+
# _basin_anchor_indices).
|
| 110 |
+
PATH_ANCHOR_AXES = ("COMPUTE_MODE", "SWAP_AB")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def config_dim(c, all_args, name):
|
| 114 |
+
"""A tile dimension for config ``c`` — from its tuned meta, else the launch args.
|
| 115 |
+
No default: a pruner reasoning about a dimension the kernel doesn't have is a wiring
|
| 116 |
+
bug, and a made-up value would silently mis-prune."""
|
| 117 |
+
v = c.kwargs.get(name, all_args.get(name))
|
| 118 |
+
if v is None:
|
| 119 |
+
raise ValueError(
|
| 120 |
+
f"pruner needs {name} (autotune config meta or launch arg); none found"
|
| 121 |
+
)
|
| 122 |
+
return v
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def config_filter(ok, when=None, on_empty=None):
|
| 126 |
+
"""Wrap a per-config predicate into an ``early_config_prune``: keeps the configs where
|
| 127 |
+
``ok(config, all_args)`` (``all_args`` = launch args + autotune kwargs). Every pruner
|
| 128 |
+
below is one of these — the predicate is always named ``ok``, so the factories differ
|
| 129 |
+
only in their rule, not their plumbing. ``when(all_args)`` gates the whole filter —
|
| 130 |
+
hardware/launch-regime checks live there, per-config logic in ``ok``. When nothing
|
| 131 |
+
survives, the untouched grid is returned (guards are advisory) unless ``on_empty(configs,
|
| 132 |
+
all_args)`` is given — correctness vetoes pass one that raises, because falling back to
|
| 133 |
+
configs that compute wrong results must be loud."""
|
| 134 |
+
|
| 135 |
+
def prune(configs, named_args, **kwargs):
|
| 136 |
+
all_args = {**named_args, **kwargs}
|
| 137 |
+
if when is not None and not when(all_args):
|
| 138 |
+
return configs
|
| 139 |
+
kept = [c for c in configs if ok(c, all_args)]
|
| 140 |
+
if kept:
|
| 141 |
+
return kept
|
| 142 |
+
return configs if on_empty is None else on_empty(configs, all_args)
|
| 143 |
+
|
| 144 |
+
return prune
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def compose_pruners(*pruners):
|
| 148 |
+
"""Chain ``early_config_prune`` callbacks left to right (each sees the previous
|
| 149 |
+
survivors)."""
|
| 150 |
+
|
| 151 |
+
def prune(configs, named_args, **kwargs):
|
| 152 |
+
for p in pruners:
|
| 153 |
+
configs = p(configs, named_args, **kwargs)
|
| 154 |
+
return configs
|
| 155 |
+
|
| 156 |
+
return prune
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def block_within_dim_pruner(dim_arg: str, block_key: str = "BLOCK_SIZE_K", when=None):
|
| 160 |
+
"""``early_config_prune`` dropping configs whose ``block_key`` tile does not divide the
|
| 161 |
+
launch dim named by ``dim_arg``: the K-loops load unmasked and the batched/grouped
|
| 162 |
+
N-tiles store row-masked only, so a non-dividing tile's last trip reads or writes past
|
| 163 |
+
the row — silently wrong results the tuner would happily time and pick (a BN=256
|
| 164 |
+
winner at N=128 corrupted 40/64 rows before the N veto existed). A dim smaller than
|
| 165 |
+
every grid tile is a hard error. ``when`` gates the veto to a launch regime (e.g. the
|
| 166 |
+
grouped swizzled arm masks its N-tail — %-wrapped value load + ``N_COLS`` store mask — so
|
| 167 |
+
BN needn't divide N there). Used standalone by the tensor-dynamic kernels and as the first
|
| 168 |
+
stages of ``mx_config_pruner``."""
|
| 169 |
+
|
| 170 |
+
def ok(c, args):
|
| 171 |
+
block = c.kwargs.get(block_key, 0)
|
| 172 |
+
return block == 0 or args[dim_arg] % block == 0
|
| 173 |
+
|
| 174 |
+
def raise_no_dividing_block(configs, args):
|
| 175 |
+
min_block = min(c.kwargs.get(block_key, 0) for c in configs)
|
| 176 |
+
raise ValueError(
|
| 177 |
+
f"{dim_arg}={args[dim_arg]} is not a multiple of any {block_key} in the "
|
| 178 |
+
f"autotune grid; the unmasked tile would run past the row. Pad the problem "
|
| 179 |
+
f"along {dim_arg} (smallest grid tile: {min_block})."
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
return config_filter(ok, when=when, on_empty=raise_no_dividing_block)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def block_fits_dim_pruner(dim_arg: str, block_key: str = "BLOCK_SIZE_K"):
|
| 186 |
+
"""``early_config_prune`` for a loop whose tail tile SLIDES back in bounds instead of
|
| 187 |
+
running past the row (the weight-only batched K-loop): the tile need not divide the dim,
|
| 188 |
+
it only has to fit inside it, since the clamp would otherwise walk backwards off the
|
| 189 |
+
buffer. Everywhere the tail is unmasked, use ``block_within_dim_pruner`` — this one keeps
|
| 190 |
+
non-dividing tiles, and on a maskless loop those read past the row."""
|
| 191 |
+
|
| 192 |
+
def ok(c, args):
|
| 193 |
+
block = c.kwargs.get(block_key, 0)
|
| 194 |
+
return block == 0 or block <= args[dim_arg]
|
| 195 |
+
|
| 196 |
+
def raise_no_fitting_block(configs, args):
|
| 197 |
+
min_block = min(c.kwargs.get(block_key, 0) for c in configs)
|
| 198 |
+
raise ValueError(
|
| 199 |
+
f"{dim_arg}={args[dim_arg]} is smaller than every {block_key} in the autotune "
|
| 200 |
+
f"grid (smallest: {min_block}); the sliding tail tile has nowhere to sit."
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
return config_filter(ok, on_empty=raise_no_fitting_block)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def require_moe_dims_aligned(N: int, K: int, block_n: int, block_k: int) -> None:
|
| 207 |
+
"""Routed MoE (grouped/batched) GEMMs tile expert dims by the quant block — N-tiles store
|
| 208 |
+
column-unmasked and K loads unmasked — so ``N`` and ``K`` must each be a multiple of the block.
|
| 209 |
+
Non-aligned dims are a dense capability: use ``matmul_2d`` (its single GEMM masks the N/K tail).
|
| 210 |
+
Raised early with a clear message rather than letting a tile run past the row."""
|
| 211 |
+
if N % block_n != 0 or K % block_k != 0:
|
| 212 |
+
raise ValueError(
|
| 213 |
+
f"routed MoE GEMM needs N ({N}) and K ({K}) each a multiple of the quant block "
|
| 214 |
+
f"(block_n={block_n}, block_k={block_k}); non-aligned dims are supported only by the "
|
| 215 |
+
f"dense matmul_2d op."
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def warp_spec_compile_guard_pruner():
|
| 220 |
+
"""``early_config_prune`` dropping ``warp_specialize`` configs that can never compile: WS needs
|
| 221 |
+
``num_warps % 4 == 0`` (its async producer/consumer partitions) and enough M work — the MMA's M
|
| 222 |
+
operand, which ``SWAP_AB`` moves to ``BLOCK_SIZE_N`` (so BM=1 decode can WS when swapped), so
|
| 223 |
+
``(BN if swapped else BM) >= 64``. ``dot_scaled`` + WS + TMA + gate all compile and win here
|
| 224 |
+
(measured 1995 TFLOPS on a clean context) — the earlier "dot_scaled/gate never WS-compiles"
|
| 225 |
+
verdicts were a ``num_warps=2`` / trap-contaminated-GPU misdiagnosis. Non-WS configs all stay —
|
| 226 |
+
WS is purely a perf axis. CUDA-only."""
|
| 227 |
+
|
| 228 |
+
def ok(c, args):
|
| 229 |
+
if not c.kwargs.get("WARP_SPEC"):
|
| 230 |
+
return True
|
| 231 |
+
# WS needs num_warps a multiple of 4 (its async producer/consumer partitions) and enough M
|
| 232 |
+
# work — the MMA's M operand, which SWAP_AB moves to BLOCK_SIZE_N (so BM=1 decode can still
|
| 233 |
+
# WS when swapped). dot_scaled + WS + TMA DOES compile here (measured 1995 on a clean
|
| 234 |
+
# context) — the old "dot_scaled never WS-compiles" was a num_warps=2 misdiagnosis on a
|
| 235 |
+
# trap-contaminated GPU. Callers needing dot_scaled+WS off (block-dynamic swap arm) fence it.
|
| 236 |
+
mma_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
|
| 237 |
+
return config_dim(c, args, mma_m) >= 64 and c.num_warps % 4 == 0
|
| 238 |
+
|
| 239 |
+
return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def gated_pointer_weight_warp_spec_pruner():
|
| 243 |
+
"""``early_config_prune`` dropping the ``warp_specialize`` configs that cannot lower on the
|
| 244 |
+
weight-only grouped MX kernel's GATE arm, where the stacked gate|up tile doubles the weight
|
| 245 |
+
extent to ``2 * BLOCK_SIZE_N``. With a pointer-mode weight (its masked load carries the
|
| 246 |
+
``other=0.0`` constant the WS partitioner cannot tag) the automatic-warp-specialization pass
|
| 247 |
+
fails in ``TritonGPUOptimizePartitionWarps`` with ``RuntimeError: PassManager::run failed``,
|
| 248 |
+
but only where the partitioner also has to split a descriptor-mode A, 16 warps, or a
|
| 249 |
+
sub-128 M tile — plain pointer/pointer at BM=128 and w4/w8 compiles and is often the winner.
|
| 250 |
+
|
| 251 |
+
Region measured by tuning the GPT-OSS W4A16 problem (N=K=2880) with a fresh Triton cache and
|
| 252 |
+
``FINEGRAINED_AUTOTUNE_LOG``: of 129 WS configs, the 12 that failed are exactly the ones this
|
| 253 |
+
predicate names, and it drops none of the 117 that compiled. Every failure sat in the GATE
|
| 254 |
+
shape — the ungated shape's 63 WS configs all compiled. Unfenced these are wasted compiles per
|
| 255 |
+
new shape (the tuner memoizes them only after paying once). The ``BM < 128`` clause rests on a
|
| 256 |
+
single observation, so it is the clause to revisit if a later probe contradicts it. CUDA-only;
|
| 257 |
+
a no-op for ungated launches.
|
| 258 |
+
|
| 259 |
+
NOT covered: the later ``dot_scaled`` arm adds 4 UNGATED failures (descriptor-A +
|
| 260 |
+
pointer-B at num_warps>=8), outside this predicate's GATE scope. Left unfenced on
|
| 261 |
+
purpose — the region separates only on a GATE x COMPUTE_MODE x A-mode x warps
|
| 262 |
+
interaction, and every predicate wide enough to catch it also dropped working configs
|
| 263 |
+
across the five probes. The tuner memoizes the 4 after paying once per shape, and this
|
| 264 |
+
fence was re-checked against the two-arm grid: 0/207 working configs dropped."""
|
| 265 |
+
|
| 266 |
+
def ok(c, args):
|
| 267 |
+
if not c.kwargs.get("WARP_SPEC"):
|
| 268 |
+
return True
|
| 269 |
+
if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer":
|
| 270 |
+
return True
|
| 271 |
+
return not (
|
| 272 |
+
c.kwargs.get("A_MEMORY_MODE", "pointer") != "pointer"
|
| 273 |
+
or c.num_warps >= 16
|
| 274 |
+
or config_dim(c, args, "BLOCK_SIZE_M") < 128
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
return config_filter(
|
| 278 |
+
ok, when=lambda args: args.get("GATE", False) and get_active_device_type() == "cuda"
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def affine_scale_warp_spec_pruner():
|
| 283 |
+
"""``early_config_prune`` dropping ``warp_specialize`` on the grouped MX AFFINE (row-major,
|
| 284 |
+
non-``SWIZZLED_SCALES``) scale path. Its per-(gathered-row, K-group) 2D pointer-gather scale
|
| 285 |
+
load carries an ``other=0.0`` constant the WS partitioner cannot tag — the automatic-warp-
|
| 286 |
+
specialization pass fails with ``'arith.constant' op does not have expected attribute
|
| 287 |
+
ttg.partition``. The swizzled arm (gate + non-gate) loads the scale through a TMA descriptor
|
| 288 |
+
(no such constant) and WS-compiles + wins, so the fused / deployment path keeps WS. CUDA-only;
|
| 289 |
+
a no-op where ``SWIZZLED_SCALES`` isn't a kernel arg (defaults to allowing WS)."""
|
| 290 |
+
|
| 291 |
+
def ok(c, args):
|
| 292 |
+
if not c.kwargs.get("WARP_SPEC"):
|
| 293 |
+
return True
|
| 294 |
+
return args.get("SWIZZLED_SCALES", True)
|
| 295 |
+
|
| 296 |
+
return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def mx_config_pruner(k_arg: str, n_arg: str | None = None, block_within_k: bool = True):
|
| 300 |
+
"""``early_config_prune`` for the MX kernels (2D, batched, grouped — their tile is always
|
| 301 |
+
tuned): a BK-within-K veto plus sm_10x MMA-shape guards (no-ops elsewhere and for scalar
|
| 302 |
+
configs). GATE-aware: under the GATE constexpr (read from the launch args) the kernel
|
| 303 |
+
computes gate|up as one stacked 2*BN extent, so the swapped dot_scaled lhs has ``2*BN``
|
| 304 |
+
rows and the no-swap combined dot is ``2*BN`` wide; a plain launch's counts are just
|
| 305 |
+
``BN``.
|
| 306 |
+
|
| 307 |
+
``block_within_k=False`` drops the BK veto for a caller whose K-loop masks or slides its
|
| 308 |
+
tail (the weight-only batched kernel); the MMA-shape guards still apply.
|
| 309 |
+
|
| 310 |
+
- ``BLOCK_SIZE_K`` not dividing the launch's contraction dim (``k_arg`` names it) →
|
| 311 |
+
dropped: the K-loop loads are unmasked, so any non-dividing BK's last trip reads
|
| 312 |
+
past the row — silently wrong results the tuner would happily time and pick (bit us when
|
| 313 |
+
BK=512 met a K=256 test problem).
|
| 314 |
+
- MXFP4 ``scalar`` configs are KEPT. They used to be dropped on sm_10x: fp4 scalar decode
|
| 315 |
+
was ALU-bound in the E2M1 unpack and measured 1.8x slower than dot_scaled. That verdict
|
| 316 |
+
was against the INTEGER unpack. Decoding E2M1 through the sm_100 CVT unit
|
| 317 |
+
(``_e2m1_pair_to_e4m3``) inverts it — on the dsv4 W4A8 decode, scalar+swap beats the
|
| 318 |
+
dot_scaled crown 20.7 vs 41.8us (gate_up) and 11.3 vs 22.0us (down), bit-identical —
|
| 319 |
+
because at BM=1 dot_scaled cannot reach the native microscaled MMA either (it needs an
|
| 320 |
+
M=128 operand) and emulates through the same upcast, while scalar skips the MMA
|
| 321 |
+
entirely. Re-fence only against a fresh measurement.
|
| 322 |
+
- Swapped ``dot_scaled`` rows < 128 → dropped: the native microscaled MMA gates on the M
|
| 323 |
+
operand being exactly 128, so smaller rows run the bf16-upcast fallback and never win —
|
| 324 |
+
the same poison mechanism (an earlier dsv4 gate_up tune shipped 63µs missing the 43µs
|
| 325 |
+
swap winner).
|
| 326 |
+
- No-swap ``dot_scaled`` width > ``SM10X_SCALED_MMA_MAX_N`` → dropped: Triton
|
| 327 |
+
miscompiles wider scaled MMAs into a sticky illegal-address device trap that poisons
|
| 328 |
+
the context mid-autotune.
|
| 329 |
+
|
| 330 |
+
Packed-E2M1 activations (a ``uint8`` activation tile, W4A4 — caller-packed or
|
| 331 |
+
quantized inline from a raw tile) are consumed by EVERY arm: dot_scaled natively
|
| 332 |
+
(the ``kind::mxf4`` MMA was probed bit-exact: BK down to 64, single-trip, and
|
| 333 |
+
BN=256 all clean; BM<128 falls back to the correct-but-slow f16 kind), and the
|
| 334 |
+
dot/scalar/swap arms column-unpack them to E4M3 (lossless) first — no arm is
|
| 335 |
+
structurally packed-incompatible, so W4A4 needs no shape gate of its own.
|
| 336 |
+
|
| 337 |
+
The shape gates above are scoped to ``dot_scaled`` — they are native scaled-MMA bug
|
| 338 |
+
gates. The ``dot`` arm (BK structurally the UE8M0 group, 32) is CORRECT everywhere
|
| 339 |
+
probed (forced-config sweep 2026-07-14, GATE and plain, MXFP4/MXFP8, incl. width-512
|
| 340 |
+
stacked dots) but is fenced on sm_10x as TPE POISON: an A/B of fresh 100-trial tunes
|
| 341 |
+
(mx grouped, S=4096 E=32 N=1024 K=2048 MXFP8) shipped dot winners 2-3x SLOWER with the
|
| 342 |
+
dot rows in the grid (gate 0.297ms vs 0.095ms, plain 0.147ms vs 0.079ms) — the ~2x
|
| 343 |
+
larger grid dilutes the trial budget and the early-sampled dot basin hijacks the TPE
|
| 344 |
+
densities, so the native scaled-MMA winner is never found. dot never wins on sm_10x
|
| 345 |
+
(native dot_scaled owns prefill, scalar owns M=1 decode); other targets keep the arm.
|
| 346 |
+
Revisit if a dot win materializes (e.g. the batched decode BM=16 observation).
|
| 347 |
+
|
| 348 |
+
The fence applies only where the kernel carries an ``As`` activation-scale operand, so
|
| 349 |
+
the weight-only (W4A16/W8A16) kernels keep their dot rows while still getting the shape
|
| 350 |
+
gates. Their ``dot`` is a weight upcast + plain ``tl.dot`` over a free BK — with raw
|
| 351 |
+
activations there is no native microscaled MMA to lose to, so both arms upcast and this
|
| 352 |
+
one WINS the stacked gate|up tile (gpt-oss K=2880: dot 4645.7us vs dot_scaled 5011.3us,
|
| 353 |
+
while the down projection goes the other way, 2348.1us vs 2882.6us).
|
| 354 |
+
|
| 355 |
+
The gates are two classes with opposite empty-survivor policies, composed trap-first:
|
| 356 |
+
``mma_trap_ok`` (miscompiles: swapped BK<128, single-trip, width, fp4 GATE zeros) raises
|
| 357 |
+
when nothing survives — falling back to configs that compute wrong results must be loud —
|
| 358 |
+
while the poison fences (dot/scalar arms, ``mma_poison_ok``) stay advisory, so emptying
|
| 359 |
+
one of them re-admits CORRECT-but-slow configs, never a miscompile. A contraction dim
|
| 360 |
+
smaller than every grid BK is a hard error: any config would over-read past the row and
|
| 361 |
+
return silently wrong results."""
|
| 362 |
+
|
| 363 |
+
def scaled_mma_available(args):
|
| 364 |
+
# dot_scaled rows exist for this launch iff the grid's smallest BK (64)
|
| 365 |
+
# divides the contraction dim with at least two trips; off the 64 grid no
|
| 366 |
+
# row survives the shape gates, so the software arms are the only correct
|
| 367 |
+
# ones and their poison fences lift.
|
| 368 |
+
return args[k_arg] % 64 == 0 and args[k_arg] > 64
|
| 369 |
+
|
| 370 |
+
def mma_trap_ok(c, args):
|
| 371 |
+
# MISCOMPILE gates only — every rule here names a config that runs and returns
|
| 372 |
+
# wrong results or traps the device; the composed filter raises when they empty.
|
| 373 |
+
if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
|
| 374 |
+
return True
|
| 375 |
+
# Swapped dot_scaled below BK=128 is a Triton sm_10x lowering bug — a STICKY
|
| 376 |
+
# misaligned-address device trap — on the UE8M0 (group-32) scale family. Probed
|
| 377 |
+
# per-arm 2026-07-15 (fresh processes): batched W4A4 swap BK=64 traps at K=2880
|
| 378 |
+
# AND K=2944 alike (K-alignment is NOT the variable), swap BK=128 runs clean and
|
| 379 |
+
# correct, no-swap BK=64 is compute-sanitizer-clean, and the grouped kernel (no
|
| 380 |
+
# swap axis) is clean at BK=64 in every memory mode. The E4M3 (NVFP4 group-16)
|
| 381 |
+
# variant does NOT trap: 40 raw-grid sweep configs + 6 forced fresh-process runs
|
| 382 |
+
# at K∈{256,2048,2944} all bit-correct (2026-08-05) — and swap BN=64 GATE is the
|
| 383 |
+
# native M=128 staging for gate decode, so it stays admitted. All probing on
|
| 384 |
+
# B200 — re-chart on H100 or the target device before trusting the fence there.
|
| 385 |
+
if (
|
| 386 |
+
c.kwargs.get("SWAP_AB")
|
| 387 |
+
and config_dim(c, args, "BLOCK_SIZE_K") < 128
|
| 388 |
+
and getattr(args.get("Bs"), "dtype", None) != torch.float8_e4m3fn
|
| 389 |
+
):
|
| 390 |
+
return False
|
| 391 |
+
# Single-trip dot_scaled (BK >= contraction dim) trips the sm_10x
|
| 392 |
+
# accumulator-init miscompile (uninitialized TMEM alloc must be mutable).
|
| 393 |
+
# Bites only small K (e.g. a K=512 gate_up with BK=512): silently wrong results
|
| 394 |
+
# the tuner would happily time and pick (surfaced as a 35% MXFP4 fused-MoE error).
|
| 395 |
+
if config_dim(c, args, "BLOCK_SIZE_K") >= args[k_arg]:
|
| 396 |
+
return False
|
| 397 |
+
# tcgen05 microscaled-fp4 MMA silently no-ops at the GATE stacked N=2*BN=64 shape (BN<64):
|
| 398 |
+
# the IR emits a correct tc_gen5_mma_scaled (128x64 acc, 64x2 scale) but it never writes the
|
| 399 |
+
# accumulator, which reads back its zero-init → EXACTLY-ZERO output. N-independent (probed
|
| 400 |
+
# zero at both non-aligned N=320 and aligned N=512; BN=64 / stacked N=128 is correct
|
| 401 |
+
# everywhere), so this is a LATENT poison config: any packed-fp4 gate|up whose tuner crowned
|
| 402 |
+
# BN=32 (favoured at small N) would silently return zeros — larger shapes pass only because
|
| 403 |
+
# BN>=64 wins. (BN=32 is recoverable only by emitting gate/up as two separate BN dots
|
| 404 |
+
# instead of one stacked 2*BN dot; not worth it — a sub-64 gated tile rarely wins over BN=64.)
|
| 405 |
+
if (
|
| 406 |
+
args.get("GATE")
|
| 407 |
+
and config_dim(c, args, "BLOCK_SIZE_N") < 64
|
| 408 |
+
and getattr(args.get("B"), "dtype", None) == torch.uint8
|
| 409 |
+
):
|
| 410 |
+
return False
|
| 411 |
+
# no-swap width cap: Triton miscompiles a scaled MMA wider than 256 into a sticky
|
| 412 |
+
# illegal-address device trap; the stacked 2*BN extent under GATE counts in full
|
| 413 |
+
rows = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
|
| 414 |
+
return c.kwargs.get("SWAP_AB") or rows <= SM10X_SCALED_MMA_MAX_N
|
| 415 |
+
|
| 416 |
+
def raise_all_mma_trapped(configs, args):
|
| 417 |
+
raise ValueError(
|
| 418 |
+
f"every autotune config hits an sm_10x scaled-MMA miscompile for "
|
| 419 |
+
f"{k_arg}={args[k_arg]} (GATE={bool(args.get('GATE'))}); no correct config "
|
| 420 |
+
f"exists in this grid — extend the grid (a no-swap BK strictly inside the "
|
| 421 |
+
f"contraction dim) rather than lifting the fence: the pruned configs return "
|
| 422 |
+
f"wrong results or trap the device."
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
def mma_poison_ok(c, args):
|
| 426 |
+
# CORRECT-but-can't-win fences (advisory: emptying re-admits correct configs)
|
| 427 |
+
if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
|
| 428 |
+
return True
|
| 429 |
+
# no-swap BK<128 rows are CORRECT but admitted only where BK=128 can't divide the
|
| 430 |
+
# contraction dim (e.g. gpt-oss K=2880). "The BK>=128 basin owns the shape" is a
|
| 431 |
+
# basin claim, not an A/B — the fence only trims trial-budget dilution, so lifting
|
| 432 |
+
# it costs tune time, not results.
|
| 433 |
+
if (
|
| 434 |
+
not c.kwargs.get("SWAP_AB")
|
| 435 |
+
and config_dim(c, args, "BLOCK_SIZE_K") < 128
|
| 436 |
+
and args[k_arg] % 128 == 0
|
| 437 |
+
):
|
| 438 |
+
return False
|
| 439 |
+
# swapped rows < 128 run the bf16-upcast fallback (the native microscaled MMA
|
| 440 |
+
# gates on the M operand being exactly 128) — correct, never wins, TPE poison
|
| 441 |
+
rows = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
|
| 442 |
+
return not c.kwargs.get("SWAP_AB") or rows >= 128
|
| 443 |
+
|
| 444 |
+
def dot_arm_ok(c, args):
|
| 445 |
+
return c.kwargs.get("COMPUTE_MODE") != "dot"
|
| 446 |
+
|
| 447 |
+
def acts_are_scaled(args):
|
| 448 |
+
# The dot-arm poison verdict holds only where dot_scaled can reach the NATIVE
|
| 449 |
+
# microscaled MMA, which needs both operands microscaled — i.e. the kernel carries an
|
| 450 |
+
# activation-scale operand. The weight-only (W4A16/W8A16) kernels have no ``As``: their
|
| 451 |
+
# activations are raw bf16/fp16, so no native low-precision MMA exists, both arms upcast,
|
| 452 |
+
# and their "dot" (weight upcast + plain tl.dot) genuinely competes. Keyed on the operand,
|
| 453 |
+
# not A's dtype — the MX-dynamic inline-quant arm takes a raw bf16 A and still reaches the
|
| 454 |
+
# native MMA.
|
| 455 |
+
return "As" in args
|
| 456 |
+
|
| 457 |
+
def nvfp4_native_ok(c, args):
|
| 458 |
+
# mxf4nvf4 (E4M3 scales) has NO fallback below the native staging — the decompose
|
| 459 |
+
# path materializes the scale through an integer-only DenseElementsAttr and
|
| 460 |
+
# PassManager fails outright (charted BM {16,32,64} x BK {128,256}, 2026-07-15;
|
| 461 |
+
# warps/operand law 2026-08-12). Native staging needs the M operand >= 128 AND
|
| 462 |
+
# num_warps in {4, 8} (2 and 16 both fail across every BN/BK/stages cell) AND
|
| 463 |
+
# both operands microscaled — the weight-only kernels (no ``As``: raw bf16 acts,
|
| 464 |
+
# in-register inline quant) never stage it, so their E4M3-scale dot_scaled fails
|
| 465 |
+
# at every tile (their dot/scalar arms compete instead). dot_scaled-only
|
| 466 |
+
# (dot/scalar decode E4M3 scales in software at any M);
|
| 467 |
+
# the MMA's M operand is BLOCK_SIZE_M, or the weight rows when SWAP_AB puts the
|
| 468 |
+
# token in N.
|
| 469 |
+
if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
|
| 470 |
+
return True
|
| 471 |
+
if not acts_are_scaled(args) or not 4 <= c.num_warps <= 8:
|
| 472 |
+
return False
|
| 473 |
+
if c.kwargs.get("SWAP_AB"):
|
| 474 |
+
return (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N") >= 128
|
| 475 |
+
return config_dim(c, args, "BLOCK_SIZE_M") >= 128
|
| 476 |
+
|
| 477 |
+
def scales_are_e4m3(args):
|
| 478 |
+
return getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn
|
| 479 |
+
|
| 480 |
+
stages = [block_within_dim_pruner(k_arg)] if block_within_k else []
|
| 481 |
+
if n_arg is not None:
|
| 482 |
+
# BN | N is needed only on the mx-dynamic AFFINE arm: its weight-scale gather has no
|
| 483 |
+
# column wrap. The swizzled arm masks its N-tail (grouped/batched: %-wrapped value load
|
| 484 |
+
# + N_COLS store mask; 2D: swizzle-offset wrap), and the weight-only kernels (no
|
| 485 |
+
# SWIZZLED_SCALES axis) clamp their scale pointers in-bounds and mask the tail in the
|
| 486 |
+
# epilogue (N_COLS), so both keep non-dividing BN.
|
| 487 |
+
stages.append(
|
| 488 |
+
block_within_dim_pruner(
|
| 489 |
+
n_arg, "BLOCK_SIZE_N", when=lambda args: not args.get("SWIZZLED_SCALES", True)
|
| 490 |
+
)
|
| 491 |
+
)
|
| 492 |
+
# trap gates run FIRST (on the full grid, raising when empty); the poison fences after
|
| 493 |
+
# them are advisory, so an emptying poison fence falls back to trap-clean survivors —
|
| 494 |
+
# never to a miscompile (the old single mixed filter fell back to the untouched grid,
|
| 495 |
+
# re-admitting the single-trip/width traps it existed to remove).
|
| 496 |
+
return compose_pruners(
|
| 497 |
+
*stages,
|
| 498 |
+
config_filter(nvfp4_native_ok, when=scales_are_e4m3),
|
| 499 |
+
config_filter(
|
| 500 |
+
mma_trap_ok, when=lambda args: is_sm10x(), on_empty=raise_all_mma_trapped
|
| 501 |
+
),
|
| 502 |
+
config_filter(
|
| 503 |
+
dot_arm_ok,
|
| 504 |
+
when=lambda args: is_sm10x()
|
| 505 |
+
and scaled_mma_available(args)
|
| 506 |
+
and acts_are_scaled(args),
|
| 507 |
+
),
|
| 508 |
+
config_filter(mma_poison_ok, when=lambda args: is_sm10x()),
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def dot_scaled_staging_pruner():
|
| 513 |
+
"""``early_config_prune`` dropping ``dot_scaled`` where the MMA's M operand degenerates to a
|
| 514 |
+
single row: ``BLOCK_SIZE_M == 1`` with no ``SWAP_AB`` to move the operand to ``BLOCK_SIZE_N``.
|
| 515 |
+
The scaled MMA has no 1-row staging and the config dies in ``PassManager::run failed`` at every
|
| 516 |
+
warps/stages cell — the decode arms that DO serve BM=1 are scalar, or dot_scaled swapped (which
|
| 517 |
+
puts the weight rows in M). Measured on the batched mx kernel: the last 6 dead compiles per
|
| 518 |
+
tune after the swapped-scalar act fence.
|
| 519 |
+
|
| 520 |
+
Two staging laws, both of which ``mx_config_pruner`` already enforces for NVFP4/E4M3 and
|
| 521 |
+
neither of which was applied to the UE8M0 half:
|
| 522 |
+
|
| 523 |
+
- ``num_warps`` outside {4, 8}: the scaled MMA has no staging for it (w16 s6 measured failing).
|
| 524 |
+
- a 1-row M operand, per above.
|
| 525 |
+
|
| 526 |
+
Measured on the batched mx kernel: 128 dead compiles per tune on HEAD, 2 after both clauses."""
|
| 527 |
+
|
| 528 |
+
def ok(c, args):
|
| 529 |
+
if c.kwargs.get("COMPUTE_MODE") != "dot_scaled":
|
| 530 |
+
return True
|
| 531 |
+
if c.num_warps not in (4, 8):
|
| 532 |
+
return False
|
| 533 |
+
if c.kwargs.get("SWAP_AB"):
|
| 534 |
+
return True
|
| 535 |
+
return config_dim(c, args, "BLOCK_SIZE_M") > 1
|
| 536 |
+
|
| 537 |
+
return config_filter(ok)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def block_dynamic_2d_warp_spec_pruner():
|
| 541 |
+
"""``early_config_prune`` for the block-dynamic 2D kernel's two WARP_SPEC compile-fail
|
| 542 |
+
laws (Triton 3.7.1 PassManager, TritonGPUPipeline; charted 2026-08-13 at the
|
| 543 |
+
DeepSeek-V4-Flash ue8m0 checkpoint shapes, forced-config matrices):
|
| 544 |
+
- WS + num_warps=16 fails at EVERY tile/scale-dtype (w8 compiles everywhere). The mx
|
| 545 |
+
2D kernel WS-compiles w16 fine; the bd GROUPED kernel does NOT on its pointer arm
|
| 546 |
+
(same law, see block_dynamic_grouped_matmul_pruner) — so this tracks the loop's
|
| 547 |
+
structure and memory mode, not a global WS law.
|
| 548 |
+
- the implicit ``dot_scaled`` arm (``USE_DOT_SCALED``: UE8M0/uint8 ``As`` on a
|
| 549 |
+
native-M tile — (BN if SWAP_AB else BM) >= 128) + WS fails at every warps/stages/
|
| 550 |
+
memory-mode cell; BM=64 and non-WS all compile. fp32-scale launches never emit the
|
| 551 |
+
scaled MMA and keep the (w<=8) WS grid.
|
| 552 |
+
Mostly 2D-only: the grouped bd loop WS-compiles from its descriptor arm and its own
|
| 553 |
+
race guard REQUIRES WS at BM >= 64, but it shares the dot_scaled+WS failure on the
|
| 554 |
+
POINTER arm (see block_dynamic_grouped_matmul_pruner). Compile soundness is
|
| 555 |
+
per-loop-structure AND per-memory-mode — chart both before reusing a law."""
|
| 556 |
+
|
| 557 |
+
def ok(c, args):
|
| 558 |
+
if not c.kwargs.get("WARP_SPEC"):
|
| 559 |
+
return True
|
| 560 |
+
if c.num_warps > 8:
|
| 561 |
+
return False
|
| 562 |
+
if getattr(args.get("As"), "dtype", None) == torch.uint8:
|
| 563 |
+
native_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
|
| 564 |
+
return config_dim(c, args, native_m) < 128
|
| 565 |
+
return True
|
| 566 |
+
|
| 567 |
+
return config_filter(ok, when=lambda args: is_sm10x())
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def block_dynamic_mma_width_pruner():
|
| 571 |
+
"""``early_config_prune`` for the block-dynamic kernels' implicit ``dot_scaled`` arm
|
| 572 |
+
(UE8M0 scales on a native-M tile — ``USE_DOT_SCALED`` in the kernel): the sm_10x
|
| 573 |
+
width cap ``mx_config_pruner``'s trap gate applies to the MX kernels, which the bd
|
| 574 |
+
family never got. Under GATE the stacked gate|up dot is ``2 * BLOCK_SIZE_N`` wide, so
|
| 575 |
+
a BN=256 GATE config emits a width-512 scaled MMA — Triton miscompiles those into a
|
| 576 |
+
sticky illegal-address device trap the tuner trips mid-tune. Scoped to launches whose
|
| 577 |
+
``As`` is UE8M0 (uint8) on sm_10x; fp32-scale launches never emit the scaled MMA.
|
| 578 |
+
Configs below the native-M tile keep the plain ``tl.dot`` + software rescale and pass
|
| 579 |
+
untouched; the swapped N side is the token tile (within the cap on every grid)."""
|
| 580 |
+
|
| 581 |
+
def ok(c, args):
|
| 582 |
+
native_m = "BLOCK_SIZE_N" if c.kwargs.get("SWAP_AB") else "BLOCK_SIZE_M"
|
| 583 |
+
if config_dim(c, args, native_m) < 128 or c.kwargs.get("SWAP_AB"):
|
| 584 |
+
return True
|
| 585 |
+
width = (2 if args.get("GATE") else 1) * config_dim(c, args, "BLOCK_SIZE_N")
|
| 586 |
+
return width <= SM10X_SCALED_MMA_MAX_N
|
| 587 |
+
|
| 588 |
+
def raise_all_too_wide(configs, args):
|
| 589 |
+
raise ValueError(
|
| 590 |
+
"every autotune config emits a scaled MMA wider than "
|
| 591 |
+
f"{SM10X_SCALED_MMA_MAX_N} (GATE={bool(args.get('GATE'))}) — sm_10x "
|
| 592 |
+
"miscompiles those into a sticky device trap; extend the grid below the cap."
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
return config_filter(
|
| 596 |
+
ok,
|
| 597 |
+
when=lambda args: is_sm10x()
|
| 598 |
+
and getattr(args.get("As"), "dtype", None) == torch.uint8,
|
| 599 |
+
on_empty=raise_all_too_wide,
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def fp8_dot_warp_pruner():
|
| 604 |
+
"""SILENTLY-WRONG fence: the fp8 ``tl.dot`` paths lose accuracy above ``num_warps=2`` at both
|
| 605 |
+
ends of the M-tile range, on sm_90. Two clauses, one phenomenon.
|
| 606 |
+
|
| 607 |
+
(1) The single-token swapped GEVM. ``swap_pad_rhs`` pads the lone token to the ``MMA_N_ATOM``
|
| 608 |
+
rhs and the epilogue takes column 0 after the K-loop (``acc_init``'s ``[BN, MMA_N_ATOM]``
|
| 609 |
+
accumulator). Above 2 warps that column comes back wrong.
|
| 610 |
+
|
| 611 |
+
(2) Large M tiles, ``BLOCK_SIZE_M >= 64``, in the grouped and 2D kernels.
|
| 612 |
+
|
| 613 |
+
Measured on H100 (sm_90), triton 3.6 AND 3.8, by forcing every admitted config against the
|
| 614 |
+
torch oracle and scoring error in bf16-ULP units (the floor — what perfect bf16 output
|
| 615 |
+
storage costs — is 0.49 mean / 1.00 max; the torch reference itself sits exactly there)::
|
| 616 |
+
|
| 617 |
+
BLOCK_SIZE_M 16 / 32, any num_warps 0.49 mean / 1.0 max ULP bit-optimal
|
| 618 |
+
BLOCK_SIZE_M 64 / 128, num_warps 8 0.58 mean / 52.1 max ULP WRONG (grouped)
|
| 619 |
+
BLOCK_SIZE_M 64 / 128, num_warps >2 280 of 578 configs WRONG (2D)
|
| 620 |
+
SWAP_AB + BLOCK_SIZE_M=1, num_warps >2 WRONG (batched)
|
| 621 |
+
|
| 622 |
+
The tuner benches by SPEED, so these get crowned silently: block-static crowned a wrong one
|
| 623 |
+
outright, while block-dynamic / tensor-dynamic (batched) and the grouped kernel carried
|
| 624 |
+
30-90 wrong configs each behind a GREEN test, because the winner happened to be correct.
|
| 625 |
+
|
| 626 |
+
In the grouped kernel ``BLOCK_SIZE_M >= 64`` is admitted only at ``num_warps=8``, so those two
|
| 627 |
+
axes are confounded there and the data cannot separate them; the 2D kernel does separate them
|
| 628 |
+
(BM=64 at num_warps=2 is clean), which is why the rule is written on num_warps.
|
| 629 |
+
|
| 630 |
+
Scoped by a POSITIVE ``is_sm90()`` test, not by ``not is_sm10x()``: the latter is also true on
|
| 631 |
+
XPU, ROCm and driverless build boxes, which do not run the NVIDIA codegen this was probed
|
| 632 |
+
against, and restricting them on sm_90 evidence would be a guess. sm_10x is likewise untouched
|
| 633 |
+
— it is where this repo's tuning and validation were done and its tcgen05 MMA lowers these
|
| 634 |
+
paths differently. Other CUDA arches (sm_89, sm_120) are unmeasured and so unfenced; each
|
| 635 |
+
needs its own probe. The sm_90 cost is real — the grouped kernel is the prefill path, where
|
| 636 |
+
the large-M tiles are the throughput — and is accepted: the alternative is a crowned config
|
| 637 |
+
that returns wrong results."""
|
| 638 |
+
|
| 639 |
+
def ok(c, args):
|
| 640 |
+
bm = c.kwargs.get("BLOCK_SIZE_M", 1)
|
| 641 |
+
if c.num_warps <= 2:
|
| 642 |
+
return True
|
| 643 |
+
if bm >= 64: # (2) large-M tile
|
| 644 |
+
return False
|
| 645 |
+
# (1) swapped single-token GEVM; native-M tiles need no pad/column-extract
|
| 646 |
+
return not (c.kwargs.get("SWAP_AB") and bm < MMA_N_ATOM_WIDTH)
|
| 647 |
+
|
| 648 |
+
def raise_all_wrong(configs, args):
|
| 649 |
+
raise ValueError(
|
| 650 |
+
"every autotune config is an fp8 tl.dot at num_warps>2 with a BLOCK_SIZE_M that "
|
| 651 |
+
"returns WRONG results off sm_10x; no correct config exists in this grid — extend it "
|
| 652 |
+
"with a num_warps=2 or a 16/32-row M tile rather than lifting the fence."
|
| 653 |
+
)
|
| 654 |
+
|
| 655 |
+
return config_filter(ok, when=lambda args: is_sm90(), on_empty=raise_all_wrong)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def scale_subblock_pruner(min_ctas_per_sm: int = 4):
|
| 659 |
+
"""``early_config_prune`` for kernels whose compute tile (``BLOCK_SIZE_N``) may subdivide the
|
| 660 |
+
quant block (``BLOCK_N``). One scale covers the whole block, so a narrower tile just reads its
|
| 661 |
+
own block — and it multiplies the N grid, the lever while the whole-block grid is too thin to
|
| 662 |
+
hide the loop's latency (a gate|up fusion halves that grid again by folding both into one program).
|
| 663 |
+
|
| 664 |
+
Sub-block tiles are admitted ONLY there: below ``min_ctas_per_sm`` whole-block programs per SM.
|
| 665 |
+
"Fills the device" (one program per SM) is NOT enough at decode — the loop is latency-bound, so
|
| 666 |
+
a 192-448-program grid still gains from more, narrower tiles: forced BN=64 vs the BN=128 crown,
|
| 667 |
+
bit-identical, DSV4 fp8 decode gate 24.9 -> 23.4us and down 13.4 -> 12.2, DSV3 down 22.3 -> 21.7
|
| 668 |
+
but DSV3 gate 0.88x (2026-08-29) — a tuner call, so the fence only trims prefill, where a
|
| 669 |
+
thousands-of-programs grid never wants the 3x wider config space (measurably worse winners
|
| 670 |
+
when it carried it).
|
| 671 |
+
|
| 672 |
+
A fused requant always pins the tile to the whole block: its output scale is one amax per
|
| 673 |
+
(row, ``BLOCK_N``) group, which a partial tile can't compute. Raises when nothing
|
| 674 |
+
survives: a tile wider than the quant block reads ONE scale across two blocks — silently
|
| 675 |
+
wrong results — so a checkpoint whose ``BLOCK_N`` is off the tile grid must fail loudly,
|
| 676 |
+
not fall back to the full grid."""
|
| 677 |
+
|
| 678 |
+
def ok(c, args):
|
| 679 |
+
block_n = args.get("BLOCK_N")
|
| 680 |
+
if block_n is None:
|
| 681 |
+
return True
|
| 682 |
+
bn = config_dim(c, args, "BLOCK_SIZE_N")
|
| 683 |
+
starved = args["S"] * triton.cdiv(args["N"], block_n) < min_ctas_per_sm * sm_count(
|
| 684 |
+
args["B"].device.index
|
| 685 |
+
)
|
| 686 |
+
if args.get("OUTPUT_FORMAT") is not None or not starved:
|
| 687 |
+
return bn == block_n
|
| 688 |
+
return bn <= block_n and args["N"] % bn == 0
|
| 689 |
+
|
| 690 |
+
def raise_no_block_tile(configs, args):
|
| 691 |
+
raise ValueError(
|
| 692 |
+
f"no BLOCK_SIZE_N in the autotune grid fits the weight quant block "
|
| 693 |
+
f"(BLOCK_N={args.get('BLOCK_N')}); a wider tile would read one scale across "
|
| 694 |
+
f"two quant blocks and return wrong results — extend the grid or requantize "
|
| 695 |
+
f"the checkpoint to a gridded block size."
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
return config_filter(ok, on_empty=raise_no_block_tile)
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def packed_schedule_scope_pruner(min_bm: int = 128):
|
| 702 |
+
"""``early_config_prune`` scoping ``PACKED_SCHEDULE`` rows to big-BM configs — the only
|
| 703 |
+
regime the packed resolve wins (register relief unlocking pressure-walled tiles: DSV3
|
| 704 |
+
bf16 forced pairs +10.7% at BM128 vs −12.5% at BM64/BN256 and a wash at BM32,
|
| 705 |
+
2026-08-06; both tuned crowns chose BM128). Everywhere else the inline resolve's free
|
| 706 |
+
ALU beats the packed arm's per-tile dependent loads, so those rows are tune-budget
|
| 707 |
+
dilution (the axis would double the grid; this keeps it +25%)."""
|
| 708 |
+
|
| 709 |
+
def ok(c, args):
|
| 710 |
+
if not c.kwargs.get("PACKED_SCHEDULE"):
|
| 711 |
+
return True
|
| 712 |
+
return config_dim(c, args, "BLOCK_SIZE_M") >= min_bm
|
| 713 |
+
|
| 714 |
+
return config_filter(ok)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
def weight_only_warp_spec_matched_mode_pruner():
|
| 718 |
+
"""``early_config_prune`` for the weight-only grouped kernel's WARP_SPEC lowering wall
|
| 719 |
+
(Triton 3.7.1 ``TritonGPUOptimizePartitionWarps`` -> PassManager::run failed; the pass
|
| 720 |
+
cannot partition this loop when BOTH operand loads share a memory mode). Charted
|
| 721 |
+
2026-08-26 (48-cell forced-config matrix, BM 64 and 128 IDENTICAL — BM-independent):
|
| 722 |
+
|
| 723 |
+
CM A/B modes w4 w8 w16
|
| 724 |
+
dot ptr/ptr ok ok FAIL
|
| 725 |
+
dot desc/desc ok ok ok (was charted FAIL/FAIL — refuted, see _TRAPPING_NUM_WARPS)
|
| 726 |
+
dot_scaled ptr/ptr FAIL FAIL FAIL
|
| 727 |
+
dot_scaled desc/desc ok ok ok (w8 was charted FAIL — refuted)
|
| 728 |
+
MIXED modes (ptr/desc, desc/ptr): 24/24 ok at every warp count.
|
| 729 |
+
BN=32 + WS: FAIL in every cell (the tile law, fenced here).
|
| 730 |
+
|
| 731 |
+
The non-monotone warp dependence (w16 rescues desc/desc dot but breaks ptr/ptr dot) marks
|
| 732 |
+
this as the pass's internal partition feasibility, not kernel source — the source is
|
| 733 |
+
identical across passing and failing cells. Two root-cause attempts on the sibling family
|
| 734 |
+
are refuted and recorded (block_dynamic_grouped_matmul_pruner); the kernel-side remedy is
|
| 735 |
+
to stop emitting configs the pass cannot lower. Cost in the wild: 19 dead compiles per
|
| 736 |
+
weight-only tune, and under inductor ONE failing config kills the whole torch.compile
|
| 737 |
+
cell instead of scoring inf — this fence is what recovers those cells."""
|
| 738 |
+
|
| 739 |
+
# desc/desc rows REMOVED 2026-08-29: forced through the tuner's own Config objects (pre_hook
|
| 740 |
+
# intact) at the charted GPT-OSS shape, (dot, desc/desc) w4 AND w8 and (dot_scaled, desc/desc) w8
|
| 741 |
+
# all compile and run bit-identical — dot+WS+BK=128 is the gate_up's best config (2128 vs 2235us).
|
| 742 |
+
# The original matrix was charted with hand-built Configs, which skip the descriptor pre_hooks
|
| 743 |
+
# and fail every descriptor cell for the wrong reason. The pointer rows stand unrefuted.
|
| 744 |
+
_TRAPPING_NUM_WARPS = {
|
| 745 |
+
("dot", "pointer"): {16},
|
| 746 |
+
("dot_scaled", "pointer"): {4, 8, 16},
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
def ok(c, args):
|
| 750 |
+
if not c.kwargs.get("WARP_SPEC"):
|
| 751 |
+
return True
|
| 752 |
+
# BN=32 + WS fails to lower in every memory mode / warp count / compute mode probed
|
| 753 |
+
# (9/9 cells, GPT-OSS N=K=2880, 2026-08-29) — the tile law the matrix never encoded
|
| 754 |
+
if config_dim(c, args, "BLOCK_SIZE_N") == 32:
|
| 755 |
+
return False
|
| 756 |
+
a, b = c.kwargs.get("A_MEMORY_MODE"), c.kwargs.get("B_MEMORY_MODE")
|
| 757 |
+
if a != b: # mixed modes lower everywhere
|
| 758 |
+
return True
|
| 759 |
+
return c.num_warps not in _TRAPPING_NUM_WARPS.get((c.kwargs.get("COMPUTE_MODE"), a), set())
|
| 760 |
+
|
| 761 |
+
return config_filter(ok, when=lambda args: is_sm10x())
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def weight_only_swap_scope_pruner():
|
| 765 |
+
"""``early_config_prune`` binding the weight-only kernel's ``SWAP_AB`` axis to its one
|
| 766 |
+
implemented arm: the CUDA-core reduce. Swap exists here to drop the MMA entirely — with
|
| 767 |
+
raw bf16 activations there is no native fp4 x bf16 MMA, so both dot arms pay an M->16 pad
|
| 768 |
+
and a full-tile upcast, and a swapped ``dot_scaled`` measured no better than the row-major
|
| 769 |
+
one (35.2 vs 38.0us, gpt-oss gate_up decode). So ``SWAP_AB`` iff ``COMPUTE_MODE`` is
|
| 770 |
+
``"scalar"``: the swapped-dot rows would compile into an arm the compute helper does not
|
| 771 |
+
implement, and a non-swapped scalar row has no accumulator shape."""
|
| 772 |
+
|
| 773 |
+
def ok(c, args):
|
| 774 |
+
return bool(c.kwargs.get("SWAP_AB")) == (c.kwargs.get("COMPUTE_MODE") == "scalar")
|
| 775 |
+
|
| 776 |
+
return config_filter(ok)
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
def mx_2d_swap_scope_pruner(max_m: int = 16):
|
| 780 |
+
"""``early_config_prune`` scoping the 2D mx kernel's ``SWAP_AB`` rows to the ONE regime
|
| 781 |
+
they exist for: E4M3-scale (NVFP4) single-token decode. NVFP4 has no other native M=1
|
| 782 |
+
arm — ``dot_scaled`` has no sub-M=128 form for E4M3 scales (PassManager) and scalar is
|
| 783 |
+
E2M1-unpack-bound (measured 60-142µs vs swap's 22-50µs on the GLM attn shape,
|
| 784 |
+
2026-08-05). Everywhere else swap is a proven loser (an 18-cell forced-swap sweep:
|
| 785 |
+
UE8M0 formats' scalar/dot arms win, M3 attn swap −38%), so its rows are dropped:
|
| 786 |
+
UE8M0-scale launches, M above the decode band, and any non-BM=1 row (the mx swap
|
| 787 |
+
compute flattens the token tile — BM=1 is structural, enforced by a static_assert).
|
| 788 |
+
|
| 789 |
+
Also drops UNSWAPPED ``scalar``: the arm exists only in its swapped form here, so those
|
| 790 |
+
configs cannot build an accumulator and fail to compile."""
|
| 791 |
+
|
| 792 |
+
def ok(c, args):
|
| 793 |
+
swap = bool(c.kwargs.get("SWAP_AB"))
|
| 794 |
+
# Under GATE, scalar exists only as the swapped arm: unswapped it has no accumulator
|
| 795 |
+
# shape (``acc_init`` is handed "dot" when not swapped) and every such config dies in
|
| 796 |
+
# CompilationError (3 dead compiles per gated mxfp4 tune). NOT true ungated — unswapped
|
| 797 |
+
# scalar is a legitimate (and sometimes winning) W4A4 arm there, and fencing it globally
|
| 798 |
+
# crowned a wrong config at N=320.
|
| 799 |
+
if c.kwargs.get("COMPUTE_MODE") == "scalar" and not swap and args.get("GATE"):
|
| 800 |
+
return False
|
| 801 |
+
if not swap:
|
| 802 |
+
return True
|
| 803 |
+
return (
|
| 804 |
+
config_dim(c, args, "BLOCK_SIZE_M") == 1
|
| 805 |
+
and getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn
|
| 806 |
+
and args["M"] <= max_m
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
+
return config_filter(ok)
|
| 810 |
+
|
| 811 |
+
|
| 812 |
+
def swizzled_out_bm_pruner():
|
| 813 |
+
"""``early_config_prune`` pinning the SWIZZLE_32_4_4 requant OUTPUT arm (``SWIZZLED_OUT``) to
|
| 814 |
+
``BLOCK_SIZE_M == BLOCK_SIZE_N == 128`` — the output-side twin of
|
| 815 |
+
``swizzled_scales_bm_pruner``.
|
| 816 |
+
|
| 817 |
+
``_epilogue_requant_mx`` writes the group scales straight into the down proj's swizzled
|
| 818 |
+
layout as ``q_s.reshape(1, 4, 32, REP_K_CS, 4)`` with ``REP_K_CS = (BN // group) // 4``.
|
| 819 |
+
``q_s`` is ``[BM, BN // group]``, so the reshape balances only at ``BM == 128``
|
| 820 |
+
(``BM * (BN//group) == 128 * (BN//group)``); every other BM raises "reshape() cannot change
|
| 821 |
+
total number of elements". BN is pinned with it because the descriptor's block shape is the
|
| 822 |
+
tuned ``REP_K_CS``. The code comment there already said "BM/BN pinned 128" — this makes the
|
| 823 |
+
config space agree, instead of handing the tuner configs that cannot compile.
|
| 824 |
+
|
| 825 |
+
Measured 2026-08-26 on the gated fp4-requant cells: 288 dead compiles per tune, i.e. every
|
| 826 |
+
non-128 config of the swizzled-out key (``SWIZZLED_OUT`` is in the tune key, so the key is
|
| 827 |
+
entirely swizzled). Un-swizzled requant keeps every BM/BN — it stores Cs row-major."""
|
| 828 |
+
|
| 829 |
+
def ok(c, args):
|
| 830 |
+
return (
|
| 831 |
+
config_dim(c, args, "BLOCK_SIZE_M") == 128
|
| 832 |
+
and config_dim(c, args, "BLOCK_SIZE_N") == 128
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
return config_filter(ok, when=lambda args: args.get("SWIZZLED_OUT"))
|
| 836 |
+
|
| 837 |
+
|
| 838 |
+
def swizzled_scales_bm_pruner():
|
| 839 |
+
"""``early_config_prune`` pinning the grouped MX pre-swizzled (``SWIZZLED_SCALES``) arm to
|
| 840 |
+
``BLOCK_SIZE_M == 128`` and a whole-128-block ``BLOCK_SIZE_N``. BM: the offline act-quant lays each expert's scale
|
| 841 |
+
slab out 128-padded, and the kernel's per-tile scale-block index (``pid_m``) only lines up
|
| 842 |
+
when the M tile is exactly 128 (``build_tile_layout`` pads experts on the same granularity).
|
| 843 |
+
BN: the scale is read as whole 128-row SWIZZLE_32_4_4 blocks off the descriptor (under GATE,
|
| 844 |
+
the gate 128-block + the up 128-block stacked into the 2*BN tile); a sub-128 BN would need a
|
| 845 |
+
partial-block read the descriptor can't express, while any whole multiple is a multi-block bulk
|
| 846 |
+
load the pre_hook sizes per config — BN=256 wins the down GEMM (665 -> 576us at DSV4 prefill,
|
| 847 |
+
bit-identical; GATE at 256 is the scaled-MMA width cap's veto, ``mx_config_pruner``).
|
| 848 |
+
|
| 849 |
+
Both pins are swizzled-slab properties, so the un-swizzled (affine) arm keeps every BM and
|
| 850 |
+
BN: ``build_tile_layout`` tiles any BM with a row mask, and the affine act-scale read is
|
| 851 |
+
indexed by the gathered row, not ``pid_m``."""
|
| 852 |
+
|
| 853 |
+
def ok(c, args):
|
| 854 |
+
return (
|
| 855 |
+
config_dim(c, args, "BLOCK_SIZE_M") == 128
|
| 856 |
+
and config_dim(c, args, "BLOCK_SIZE_N") % 128 == 0
|
| 857 |
+
)
|
| 858 |
+
|
| 859 |
+
return config_filter(ok, when=lambda args: args.get("SWIZZLED_SCALES"))
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
def swizzled_scale_config_pruner(allow_gate_subblock=False):
|
| 863 |
+
"""Drop pre-swizzled-scale configs the SWIZZLE_32_4_4 descriptor load can't serve, gated on
|
| 864 |
+
``SWIZZLED_SCALES`` (the un-swizzled arm loads scales per group and takes any tile):
|
| 865 |
+
|
| 866 |
+
- ``BLOCK_SIZE_K < 4 * SCALE_GROUP_K``: the un-swizzle reads whole 4-group K bands
|
| 867 |
+
(``REP_K = (BK // SCALE_GROUP_K) // 4``), so a smaller BK collapses ``REP_K`` to 0 and the
|
| 868 |
+
reshape traps — 128 for group-32 (MX), 64 for group-16 (NVFP4). BK must also tile whole
|
| 869 |
+
bands (``BK % (4 * group) == 0``).
|
| 870 |
+
- ``BLOCK_SIZE_N > 128`` not a whole-block multiple: the descriptor arm bulk-loads
|
| 871 |
+
``BN // 128`` swizzled blocks (each kernel's pre_hook sizes the box per config), a sub-128 BN
|
| 872 |
+
reads a scalar slice of one block; a BN between is neither, and a multi-block BN whose
|
| 873 |
+
last tile passes the 128-padded N extent zero-fills (see the fit bound below). BN=256 is
|
| 874 |
+
a legal two-block load where it fits (the GATE tile has always read one) and wins at
|
| 875 |
+
prefill — grouped down 665 -> 576us, dense 2D 703 -> 552us at M=49152, bit-identical; the
|
| 876 |
+
scaled-MMA width cap is ``mx_config_pruner``'s.
|
| 877 |
+
- under ``GATE``, ``BLOCK_SIZE_N != 128``: the gate|up scale is interleaved as whole 128-row
|
| 878 |
+
block pairs [g0,u0,g1,u1,...], read as one 2*BN tile; a sub-128 BN can't index a block pair
|
| 879 |
+
off the descriptor. ``allow_gate_subblock`` (batched decode) admits BN in (32, 64): its
|
| 880 |
+
scale leaf pointer-GATHERs the sub-128 gate|up tile out of the interleaved buffer, which is
|
| 881 |
+
the raw path's BN=32 gate|up tile without un-swizzling (the whole swizzled-vs-raw decode
|
| 882 |
+
gap). Packed fp4 is admitted too: the historical sub-tile fp4->bf16 lowering trap
|
| 883 |
+
(``xVals.size() % 4``) no longer reproduces — mxfp4 AND nvfp4 forced at BN∈{32,64},
|
| 884 |
+
BK=128, scalar, K=2048 all run bit-correct (2026-08-05); a resurfacing trap is a loud
|
| 885 |
+
compile failure (forgiven inf), never silent."""
|
| 886 |
+
|
| 887 |
+
def ok(c, args):
|
| 888 |
+
band = 4 * args["SCALE_GROUP_K"] # one whole swizzled K band (4 scale groups)
|
| 889 |
+
if config_dim(c, args, "BLOCK_SIZE_K") % band != 0:
|
| 890 |
+
return False
|
| 891 |
+
bn = config_dim(c, args, "BLOCK_SIZE_N")
|
| 892 |
+
if args.get("GATE"):
|
| 893 |
+
return bn == 128 or (allow_gate_subblock and bn in (32, 64))
|
| 894 |
+
if bn > 128:
|
| 895 |
+
# BN=256 pipelines OOM smem at num_stages >= 5 (253 KB at s5, 304 KB at s6 for the
|
| 896 |
+
# fp8 x fp4 and fp8 x fp8 dot_scaled tiles, BK=128; s4 fits) — below smem_pruner's
|
| 897 |
+
# keep-side bound, so without this the TPE burns whole 200-trial budgets on dead
|
| 898 |
+
# compiles (observed 2026-08-29).
|
| 899 |
+
if c.num_stages >= 5:
|
| 900 |
+
return False
|
| 901 |
+
# A multi-block tile must FIT the swizzled grid: the bulk load reads BN // 128 whole
|
| 902 |
+
# blocks with no row mask, and the grid is padded to 128 rows — a tile whose blocks
|
| 903 |
+
# pass the padded extent TMA-zero-fills the scales: silent garbage, never a trap
|
| 904 |
+
# (mxfp4_swizzled N=128 forced at BN=256 fails parity on triton 3.7.1 and 3.8 alike;
|
| 905 |
+
# the BN=256 prefill wins were measured at N % 256 == 0 shapes).
|
| 906 |
+
n = args.get("N")
|
| 907 |
+
if n is not None and -(-n // bn) * bn > -(-n // 128) * 128:
|
| 908 |
+
return False
|
| 909 |
+
return bn <= 128 or bn % 128 == 0
|
| 910 |
+
|
| 911 |
+
def raise_no_swizzled_tile(configs, args):
|
| 912 |
+
raise ValueError(
|
| 913 |
+
"no autotune config can serve pre-swizzled scales for this launch (the "
|
| 914 |
+
"SWIZZLE_32_4_4 read needs BLOCK_SIZE_K % 128 == 0 and a <=128-row or 128-multiple N tile; "
|
| 915 |
+
f"GATE={bool(args.get('GATE'))}) — the contraction dim likely has no "
|
| 916 |
+
"128-dividing tile; pass affine (row-major) scales for this shape."
|
| 917 |
+
)
|
| 918 |
+
|
| 919 |
+
return config_filter(
|
| 920 |
+
ok, when=lambda args: args.get("SWIZZLED_SCALES"), on_empty=raise_no_swizzled_tile
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
def block_dynamic_grouped_matmul_pruner():
|
| 925 |
+
"""``early_config_prune`` for the block-dynamic grouped kernel: the Triton 3.7.1
|
| 926 |
+
pipeliner-race guard, sized to its four-load-stream single-dot K-loop. The GATE arm
|
| 927 |
+
shares the guard: post-activation-prequant it dots the stacked gate|up as the same
|
| 928 |
+
single-dot loop (one wider weight tile, not a second dot/load stream) and was
|
| 929 |
+
flake-verified race-free cross-process. Measured on sm_100, 15-fresh-process flake
|
| 930 |
+
runs per cell, big and tiny shapes:
|
| 931 |
+
|
| 932 |
+
- ``warp_specialize`` compiles iff ``num_warps % 4 == 0`` (its async partitions) and
|
| 933 |
+
``BLOCK_SIZE_M >= 64``, and is race-free everywhere it compiles — but on the POINTER
|
| 934 |
+
arm it hits a lowering wall: "'arith.constant' op does not have expected attribute
|
| 935 |
+
ttg.partition" -> PassManager::run failed (TritonGPUPipeline). The descriptor arm
|
| 936 |
+
always compiles. The obvious suspect — the masked activation load's ``other=0.0``,
|
| 937 |
+
which is the known cause of the analogous mx-grouped fence — was PROBED and is NOT
|
| 938 |
+
it: dropping the mask entirely leaves every pointer cell below still failing, so the
|
| 939 |
+
trigger is structural to the pointer arm, not that constant. Do not re-try the
|
| 940 |
+
clamp-not-mask rewrite on this basis. Two INDEPENDENT legs, forced-config matrices,
|
| 941 |
+
2026-08-21 B200:
|
| 942 |
+
* ``num_warps >= 16`` fails at BM 64 AND 128, with fp32 scales as well as UE8M0 —
|
| 943 |
+
a property of this loop under WS, not of the scaled MMA.
|
| 944 |
+
* uint8/UE8M0 ``As`` on a native-M ``BLOCK_SIZE_M >= 128`` tile (the implicit
|
| 945 |
+
``dot_scaled`` arm) fails at every warps/stages cell: pointer 0/6, descriptor
|
| 946 |
+
4/6 + 2 smem.
|
| 947 |
+
Combined with the race guard below, the descriptor arm is the only BM=128 route for
|
| 948 |
+
UE8M0 grouped — which is what makes the native tcgen05 tile reachable at all here.
|
| 949 |
+
- The default pipeliner RACES at ``BLOCK_SIZE_M >= 64`` (3/15 wrong at BM64/w8) and is
|
| 950 |
+
clean at BM16/32.
|
| 951 |
+
|
| 952 |
+
The regions are disjoint, so per ``BLOCK_SIZE_M``: BM >= 64 keeps only the compilable
|
| 953 |
+
WS configs, BM < 64 keeps only non-WS. CUDA-only — the race is a CUDA pipeliner
|
| 954 |
+
artifact and the WS axis isn't emitted elsewhere.
|
| 955 |
+
|
| 956 |
+
Also attached to the STATIC grouped kernel as an INHERITED guard: its three-load-stream
|
| 957 |
+
K-loop (scalar act scale preloaded, no per-row ``as`` stream) was never flake-mapped —
|
| 958 |
+
race soundness is per-loop-structure, so this map may over-fence it (perf cost) or, if
|
| 959 |
+
the 3-stream loop races elsewhere, under-fence it. Re-map with multi-process flake runs
|
| 960 |
+
before trusting the static attachment."""
|
| 961 |
+
|
| 962 |
+
def ok(c, args):
|
| 963 |
+
bm = config_dim(c, args, "BLOCK_SIZE_M")
|
| 964 |
+
if bm < 64:
|
| 965 |
+
return not c.kwargs.get("WARP_SPEC")
|
| 966 |
+
if not (c.kwargs.get("WARP_SPEC") and c.num_warps % 4 == 0):
|
| 967 |
+
return False
|
| 968 |
+
if c.kwargs.get("A_MEMORY_MODE") == "pointer":
|
| 969 |
+
if c.num_warps >= 16:
|
| 970 |
+
return False
|
| 971 |
+
if getattr(args.get("As"), "dtype", None) == torch.uint8 and bm >= 128:
|
| 972 |
+
return False
|
| 973 |
+
return True
|
| 974 |
+
|
| 975 |
+
return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
def scalar_max_m_pruner(m_arg: str, max_m: int = 64):
|
| 979 |
+
"""``early_config_prune`` dropping ``scalar`` configs when the launch's row count
|
| 980 |
+
(``m_arg``) exceeds ``max_m``: scalar is a BM=1 GEVM — sensible for decode-sized M,
|
| 981 |
+
hopeless at prefill, and hopeless-but-benched configs poison the TPE's per-dimension
|
| 982 |
+
densities (measured: with scalar in the M=8192 grid the 2D MX attn prefill tune
|
| 983 |
+
landed 0.48x vs hub; without it, 2.06x)."""
|
| 984 |
+
|
| 985 |
+
def ok(c, args):
|
| 986 |
+
return c.kwargs.get("COMPUTE_MODE") != "scalar"
|
| 987 |
+
|
| 988 |
+
return config_filter(ok, when=lambda args: args[m_arg] > max_m)
|
| 989 |
+
|
| 990 |
+
def matched_memory_modes_pruner():
|
| 991 |
+
"""Drop configs that mix pointer and descriptor operand loads — for a DENSE 2D GEMM
|
| 992 |
+
only (contiguous A, whole M in one tile). Measured across M=1..8192 on the block-scale
|
| 993 |
+
2D matmul: mixed only ever TIES the matched combos (<1us at small M) and loses at large
|
| 994 |
+
M, so pruning it costs no win and halves the descriptor search. Do NOT use this on the
|
| 995 |
+
GROUPED kernels: there M is distributed across experts (S/E small), A is token-GATHERED,
|
| 996 |
+
and A=pointer + B=descriptor (mixed) is the dominant winner — those keep both axes free.
|
| 997 |
+
Non-empty (the matched configs remain)."""
|
| 998 |
+
|
| 999 |
+
def ok(c, args):
|
| 1000 |
+
a_ptr = c.kwargs.get("A_MEMORY_MODE", "pointer") == "pointer"
|
| 1001 |
+
b_ptr = c.kwargs.get("B_MEMORY_MODE", "pointer") == "pointer"
|
| 1002 |
+
return a_ptr == b_ptr
|
| 1003 |
+
|
| 1004 |
+
return config_filter(ok)
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
def descriptor_needs_prequant_pruner():
|
| 1008 |
+
"""Keep only pointer-mode configs when ``A`` is raw (bf16/fp16 — the ``maybe_act_quant``
|
| 1009 |
+
M<threshold inline-quant arm). A descriptor operand load needs a pre-quantized fp8 ``A``: the
|
| 1010 |
+
inline arm reads ``a_ptrs``, which ``operand_tile_ptrs`` leaves a dead placeholder under a
|
| 1011 |
+
descriptor mode, so a descriptor config there would compute garbage yet bench fast (a broken
|
| 1012 |
+
config the tuner would crown). Gated on ``A``'s dtype so the offline (fp8) path keeps both
|
| 1013 |
+
memory axes; a no-op for callers that always pre-quantize (``A`` already fp8)."""
|
| 1014 |
+
|
| 1015 |
+
def ok(c, args):
|
| 1016 |
+
return (
|
| 1017 |
+
c.kwargs.get("A_MEMORY_MODE", "pointer") == "pointer"
|
| 1018 |
+
and c.kwargs.get("B_MEMORY_MODE", "pointer") == "pointer"
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
return config_filter(
|
| 1022 |
+
ok, when=lambda args: args["A"].dtype not in (torch.float8_e4m3fn, torch.int8, torch.uint8)
|
| 1023 |
+
)
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
def descriptor_box_pruner(k_dim="BLOCK_SIZE_K"):
|
| 1027 |
+
"""Drop descriptor configs whose TMA box exceeds the tensormap's 256-per-dim limit:
|
| 1028 |
+
the weight inner dim is the K tile in BYTES over values-per-byte (packed E2M1
|
| 1029 |
+
halves it; E4M3 is one byte per value), so BK=512 e4m3 boxes are illegal while
|
| 1030 |
+
BK=512 packed ones fit. Pointer configs pass untouched. ``k_dim`` names the K-tile
|
| 1031 |
+
axis — ``BLOCK_SIZE_K`` for the tune_block_nk kernels, ``block_k`` for the block-scale
|
| 1032 |
+
kernels whose K tile is the caller's fixed scale block."""
|
| 1033 |
+
|
| 1034 |
+
def ok(c, args):
|
| 1035 |
+
# tiles may be tuned (config kwargs) or pinned launch args — config_dim reads both
|
| 1036 |
+
bk = config_dim(c, args, k_dim)
|
| 1037 |
+
if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer":
|
| 1038 |
+
weight_vpb = (
|
| 1039 |
+
2 if getattr(args.get("B"), "dtype", None) == torch.uint8 else 1
|
| 1040 |
+
)
|
| 1041 |
+
if bk // weight_vpb > 256 or config_dim(c, args, "BLOCK_SIZE_N") > 256:
|
| 1042 |
+
return False
|
| 1043 |
+
# A TMA descriptor needs a unit-stride innermost dim; a transposed operand
|
| 1044 |
+
# (column-major view, as a backward pass hands in) has none, so only the
|
| 1045 |
+
# pointer arm can serve it. The kernel itself is stride-general.
|
| 1046 |
+
b = args.get("B")
|
| 1047 |
+
if b is not None and getattr(b, "ndim", 0) == 2 and b.stride(-1) != 1:
|
| 1048 |
+
return False
|
| 1049 |
+
if c.kwargs.get("A_MEMORY_MODE", "pointer") != "pointer":
|
| 1050 |
+
# descriptor A, both arms tuner-routed per the tokens-per-expert key:
|
| 1051 |
+
# contiguous boxes (no gather) win at dense routing (-7.1/-7.4%) and lose at
|
| 1052 |
+
# skew; tma gather4 (gathered rows, 1-row box, sm_100 only) is MONOTONIC in
|
| 1053 |
+
# tokens/expert — 2x loss at 32/expert, parity at 1024, -5.3/-4.7% at 4096
|
| 1054 |
+
# (32k tokens, 2026-07-16). Only the arch is fenced.
|
| 1055 |
+
act_vpb = 2 if getattr(args.get("A"), "dtype", None) == torch.uint8 else 1
|
| 1056 |
+
if args.get("GatherIdx") is not None and not is_sm10x():
|
| 1057 |
+
return False
|
| 1058 |
+
if bk // act_vpb > 256:
|
| 1059 |
+
return False
|
| 1060 |
+
return True
|
| 1061 |
+
|
| 1062 |
+
return config_filter(ok)
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
def global_scale_warp_spec_pruner():
|
| 1066 |
+
"""Drop WARP_SPEC configs when the launch carries an NVFP4 global (``BsGlobal`` not None):
|
| 1067 |
+
the per-expert global load cannot take ``ttg.partition`` under warp specialization — the
|
| 1068 |
+
same Triton lowering gap as the affine scale gather's ``other=0.0`` — so every such config
|
| 1069 |
+
is a benign compile-failure inf. Fenced to save the compiles; single-level launches
|
| 1070 |
+
(``BsGlobal`` None) keep their WS rows."""
|
| 1071 |
+
|
| 1072 |
+
def ok(c, args):
|
| 1073 |
+
return not (c.kwargs.get("WARP_SPEC") and args.get("BsGlobal") is not None)
|
| 1074 |
+
|
| 1075 |
+
return config_filter(ok)
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def gate_stacked_tmem_trap_pruner():
|
| 1079 |
+
"""Miscompile gate: drop GATE configs at exactly ``BLOCK_SIZE_N == 256, BLOCK_SIZE_M == 64``
|
| 1080 |
+
— the stacked gate|up accumulator (``2*BN = 512`` columns) exceeds sm_100 tensor memory, and
|
| 1081 |
+
at BM=64 Triton's resource checker misses the overflow, so the config runs and TRAPS the
|
| 1082 |
+
device (sticky CUDA 716 "misaligned address"; one such trial poisons the whole tune and
|
| 1083 |
+
every launch after it). Probed per-arm in fresh processes (2026-08-10, B200, weight-only
|
| 1084 |
+
grouped, K=2880 and K=2048 alike — N-divisibility is NOT the variable): BM=64 traps in every
|
| 1085 |
+
memory mode, WS on and off, BK 64 and 128; BM=128 fails CLEANLY (OutOfResources: tensor
|
| 1086 |
+
memory 544 > 512) and self-prunes; BM=32 and BN<=128 run correct. The checker gap is the
|
| 1087 |
+
only reason this needs a fence at all — drop precisely the probed trap point and let every
|
| 1088 |
+
neighbor keep self-reporting.
|
| 1089 |
+
|
| 1090 |
+
ON sm_90 the whole ``2*BN = 512`` stacked tile goes, not just the BM=64 point. The
|
| 1091 |
+
self-pruning above is a TENSOR-MEMORY overflow, and sm_90 has no tensor memory to overflow:
|
| 1092 |
+
nothing raises OutOfResources, so the config runs and returns garbage instead. Measured on
|
| 1093 |
+
H100, triton 3.8, nvfp4 gate|silu with fused requant through the 2D kernel — sampling the
|
| 1094 |
+
admitted grid, ``BLOCK_SIZE_N`` splits cleanly::
|
| 1095 |
+
|
| 1096 |
+
BN 32 / 64 / 128 60 correct, 0 wrong
|
| 1097 |
+
BN 256 0 correct, 9 wrong (BM=128; the crowned config varies per tune,
|
| 1098 |
+
so the scenario test passes or fails by luck)
|
| 1099 |
+
|
| 1100 |
+
The mechanism is the stacked accumulator width, which is format-independent, so this fences
|
| 1101 |
+
every GATE config at BN=256 rather than only the nvfp4 cell that exposed it. The BM=64 trap
|
| 1102 |
+
point stays fenced on every arch (it was probed on B200); the wider sm_90 clause is scoped by
|
| 1103 |
+
a positive arch test so XPU/ROCm, which never ran this codegen, keep their full grid."""
|
| 1104 |
+
|
| 1105 |
+
def ok(c, args):
|
| 1106 |
+
if not args.get("GATE") or config_dim(c, args, "BLOCK_SIZE_N") != 256:
|
| 1107 |
+
return True
|
| 1108 |
+
if config_dim(c, args, "BLOCK_SIZE_M") == 64:
|
| 1109 |
+
return False # the probed sm_100 trap point — fenced on every arch
|
| 1110 |
+
return not is_sm90() # sm_90 has no tensor memory, so nothing self-reports the overflow
|
| 1111 |
+
|
| 1112 |
+
def raise_all_trap(configs, args):
|
| 1113 |
+
raise ValueError(
|
| 1114 |
+
"every config in the grid is the (GATE, BN=256, BM=64) tensor-memory trap point; "
|
| 1115 |
+
"falling back would run configs that trap the device (sticky CUDA 716)"
|
| 1116 |
+
)
|
| 1117 |
+
|
| 1118 |
+
return config_filter(ok, on_empty=raise_all_trap)
|
| 1119 |
+
|
| 1120 |
+
|
| 1121 |
+
def smem_pruner(k_dim="BLOCK_SIZE_K", min_sets: float = 0.5):
|
| 1122 |
+
"""``early_config_prune`` dropping configs whose shared memory certainly cannot fit,
|
| 1123 |
+
with the bound picked by the operand dtypes (sampled from ``metadata.shared`` across
|
| 1124 |
+
every kernel family, 22 cells):
|
| 1125 |
+
|
| 1126 |
+
- both operands >= 2-byte floats (the unquantized loops): ``num_stages`` full
|
| 1127 |
+
buffers of both tiles (exact) plus the fixed barrier terms at their sampled
|
| 1128 |
+
MINIMA (+16 base; +28 warp-spec, observed up to 48 varying with warps; +32
|
| 1129 |
+
host-descriptor) — minima keep the bound below the true usage, so a config is
|
| 1130 |
+
only ever dropped when its operand buffers alone cannot fit.
|
| 1131 |
+
- quantized ``dot_scaled`` (the only quantized arm whose tiles can reach the limit):
|
| 1132 |
+
the per-allocation law, every term named by its TTGIR ``local_alloc`` and exact to
|
| 1133 |
+
the allocator's alignment/packing slack (<= 16 B observed; barriers pack into
|
| 1134 |
+
padding, so no fixed constant is added — the slack is keep-side) —
|
| 1135 |
+
``num_stages - 1`` buffers of operands+scales at the native M=128 staging (weights
|
| 1136 |
+
full-width even when fp4-packed; GATE shifts the A tile to a full ``num_stages``),
|
| 1137 |
+
or the bf16-upcast fallback below the gate (one single-buffered upcast tile,
|
| 1138 |
+
packed weights, no A-scale buffers).
|
| 1139 |
+
- quantized ``dot``/``scalar``: the raw-operand floor (their 32-wide K tiles cannot
|
| 1140 |
+
reach the limit).
|
| 1141 |
+
|
| 1142 |
+
``min_sets`` scales the quantized ``dot_scaled`` bound: the default HALF a set is the
|
| 1143 |
+
family-wide keep-side envelope (the 2D nvfp4 arm sub-tiles a request into half a set);
|
| 1144 |
+
the GROUPED kernel never compiles below TWO full sets — fresh-tune logs at the DSV4 W4A8
|
| 1145 |
+
gate_up key (244 trials, 2026-08-29): 0 of 82 compiling configs had 2*set > smem, while
|
| 1146 |
+
120 of 162 dead ones did (every BK=512 request, 59/59), so ``min_sets=2`` there halves the
|
| 1147 |
+
dead compiles a 100-trial tune pays without dropping a single compiling config.
|
| 1148 |
+
|
| 1149 |
+
Both arms are EMPIRICAL models of this Triton's allocator, fit to and verified
|
| 1150 |
+
against ``metadata.shared`` samples (not derived from a spec) — chosen so the error
|
| 1151 |
+
direction under the sampled behavior is always toward KEEPING a config, which then
|
| 1152 |
+
merely compiles and self-prunes as inf. All samples taken on B200 (sm_100) —
|
| 1153 |
+
re-verify on a Triton bump AND on H100 or the target device (smem capacity and
|
| 1154 |
+
allocator behavior are per-arch). Non-empty fallback."""
|
| 1155 |
+
|
| 1156 |
+
def ok(c, args):
|
| 1157 |
+
a, b = args["A"], args["B"]
|
| 1158 |
+
tiles = 2 if args.get("GATE") else 1
|
| 1159 |
+
bk = config_dim(c, args, k_dim)
|
| 1160 |
+
unquantized = a.element_size() >= 2 and b.element_size() >= 2
|
| 1161 |
+
stages = c.num_stages if unquantized else c.num_stages - 1
|
| 1162 |
+
packed = 2 if b.dtype == torch.uint8 else 1
|
| 1163 |
+
bm = config_dim(c, args, "BLOCK_SIZE_M")
|
| 1164 |
+
bn = config_dim(c, args, "BLOCK_SIZE_N")
|
| 1165 |
+
if unquantized:
|
| 1166 |
+
# sampled minima of the fixed allocations beside the operand buffers
|
| 1167 |
+
# (the barrier terms vary by tens of bytes with warps; minima only ever
|
| 1168 |
+
# err toward keeping a config)
|
| 1169 |
+
fixed = (
|
| 1170 |
+
16
|
| 1171 |
+
+ (28 if c.kwargs.get("WARP_SPEC") else 0)
|
| 1172 |
+
+ (32 if c.kwargs.get("B_MEMORY_MODE", "pointer") != "pointer" else 0)
|
| 1173 |
+
)
|
| 1174 |
+
need = fixed + stages * bk * (
|
| 1175 |
+
a.element_size() * bm + b.element_size() * tiles * bn
|
| 1176 |
+
)
|
| 1177 |
+
elif c.kwargs.get("COMPUTE_MODE") == "dot_scaled":
|
| 1178 |
+
# KEEP-SIDE bound = HALF of one buffer set: operands stored PACKED (fp4 = 2
|
| 1179 |
+
# values/byte) plus both group scale tiles (BK/group cols; group 32 UE8M0, 16
|
| 1180 |
+
# E4M3). Requested-stages models over-estimate — the pipeliner CLAMPS stages
|
| 1181 |
+
# (an s3 request can compile with an s2 footprint) and the 2D nvfp4 arm can
|
| 1182 |
+
# compile half a set — so any tighter bound drops configs the compiler would
|
| 1183 |
+
# have rescued. Proven est<=actual on 1,099 metadata.shared samples across the
|
| 1184 |
+
# family (see OPTIMIZATION_LOG: smem-keep-side-refit).
|
| 1185 |
+
group = 16 if getattr(args.get("Bs"), "dtype", None) == torch.float8_e4m3fn else 32
|
| 1186 |
+
# A's smem bytes follow its LAUNCH dtype: packed E2M1 (int8/uint8) = 2 values/byte,
|
| 1187 |
+
# E4M3 = 1 B/value, raw bf16/fp16 (weight-only) = element_size B/value
|
| 1188 |
+
a_dtype = a.dtype
|
| 1189 |
+
if a_dtype in (torch.int8, torch.uint8):
|
| 1190 |
+
a_bytes = (bk // 2) * bm
|
| 1191 |
+
else:
|
| 1192 |
+
a_bytes = bk * bm * a.element_size()
|
| 1193 |
+
# HALF a set: the sampled lower envelope. The 2D kernel's nvfp4 arm can compile an
|
| 1194 |
+
# s2 request into half a buffer set (sub-tile pipelining: BN=64/128/256 x BK=512
|
| 1195 |
+
# all landed at set/2 + <=8 B), so the full set is not a lower bound; the half set
|
| 1196 |
+
# is, over all 1099 samples (largest remaining margin 68 B on a 4 KB decode tile,
|
| 1197 |
+
# covered by the halving). A config whose HALF set exceeds the limit cannot compile
|
| 1198 |
+
# in any observed form — everything else is kept and self-reports as a benign inf.
|
| 1199 |
+
# The family-wide keep-side model counts fp4 weights PACKED; the grouped pipeline stages
|
| 1200 |
+
# them at full E4M3 width (BN=256, BK=128, s5 OOMs at 253 KB = 5 x 50 KB sets, which only
|
| 1201 |
+
# a full-width B tile adds up to), so the whole-set bound (min_sets >= 1) uses full width.
|
| 1202 |
+
b_bytes = bk * tiles * bn if min_sets >= 1 else (bk // packed) * tiles * bn
|
| 1203 |
+
need = int(
|
| 1204 |
+
(a_bytes + b_bytes + (bk // group) * bm + (bk // group) * tiles * bn) * min_sets
|
| 1205 |
+
)
|
| 1206 |
+
else:
|
| 1207 |
+
# dot (BK = the 32-group) / scalar: tiles too small to reach the limit —
|
| 1208 |
+
# the raw-operand floor suffices
|
| 1209 |
+
need = stages * (bk * bm + (bk // packed) * tiles * bn)
|
| 1210 |
+
return need <= sm_shared_memory_limit()
|
| 1211 |
+
|
| 1212 |
+
return config_filter(ok, when=lambda args: get_active_device_type() == "cuda")
|
| 1213 |
+
|
build/torch-xpu/quant.py
ADDED
|
@@ -0,0 +1,955 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import triton
|
| 17 |
+
import triton.language as tl
|
| 18 |
+
from triton.language.extra.cuda import gdc_launch_dependents, gdc_wait
|
| 19 |
+
|
| 20 |
+
from .bayesian_autotuner import bayesian_autotune
|
| 21 |
+
from .formats import global_scale_stride, is_per_expert_global
|
| 22 |
+
from .compat import add_op_namespace_prefix, FP8_DTYPE, MX_SCALE_GROUP_K, NVFP4_SCALE_GROUP_K, compile_time_only_triton_op, compile_time_only_triton_wrap, decode_pdl, device_context, is_sm10x
|
| 23 |
+
from .swizzle import swizzle_store_block
|
| 24 |
+
from .scheduling import build_tile_layout, resolve_tile_inline
|
| 25 |
+
|
| 26 |
+
# ── Triton-side helpers (inlined by ``@triton.jit`` callers) ──────────────────
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@triton.jit
|
| 30 |
+
def fp8_act_quant_inline(
|
| 31 |
+
a_raw, TRANSPOSED: tl.constexpr = False, UE8M0: tl.constexpr = False
|
| 32 |
+
):
|
| 33 |
+
"""Inline FP8 (E4M3) activation quant for the W8A8 block-scale path.
|
| 34 |
+
|
| 35 |
+
Per-token amax → fp32 scale ``amax/448`` (floored at 1e-12 against zero rows)
|
| 36 |
+
→ cast values to FP8. Returns ``(a_fp8, a_s)`` with ``a_s`` shaped ``(M,)``;
|
| 37 |
+
``TRANSPOSED`` marks a ``(K, M)`` tile (the swapped descriptor arm), where the
|
| 38 |
+
token axis is 0 instead of 1.
|
| 39 |
+
|
| 40 |
+
``UE8M0`` ceils each scale up to a power of two and returns the E8M0 exponent
|
| 41 |
+
byte (uint8) instead of the fp32 scale — the group format the tcgen05
|
| 42 |
+
``dot_scaled`` MMA consumes. The ceil mirrors DeepGEMM's ``ceil_to_ue8m0``
|
| 43 |
+
(add ``0x7FFFFF``, clear the mantissa) so our scales match its checkpoints.
|
| 44 |
+
"""
|
| 45 |
+
if TRANSPOSED:
|
| 46 |
+
a_s = tl.max(tl.abs(a_raw), axis=0) / 448.0
|
| 47 |
+
else:
|
| 48 |
+
a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
|
| 49 |
+
denom = tl.maximum(a_s, 1e-12)
|
| 50 |
+
if UE8M0:
|
| 51 |
+
bits = (denom.to(tl.int32, bitcast=True) + 0x7FFFFF) & ~0x7FFFFF
|
| 52 |
+
denom = bits.to(tl.float32, bitcast=True)
|
| 53 |
+
if TRANSPOSED:
|
| 54 |
+
a_fp8 = (a_raw / denom[None, :]).to(tl.float8e4nv)
|
| 55 |
+
else:
|
| 56 |
+
a_fp8 = (a_raw / denom[:, None]).to(tl.float8e4nv)
|
| 57 |
+
if UE8M0:
|
| 58 |
+
a_scale = ((bits >> 23) & 0xFF).to(tl.uint8)
|
| 59 |
+
else:
|
| 60 |
+
a_scale = a_s
|
| 61 |
+
return a_fp8, a_scale
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# cvt.e2m1x2.f32 (hardware FP4 pack) exists only on sm_100 (Blackwell). Resolved once at
|
| 65 |
+
# import as a compile-time constexpr for the jit helper below; the ALU fallback compiles
|
| 66 |
+
# everywhere else. ``is_sm10x`` is driverless-safe, so no import-time guard is needed.
|
| 67 |
+
_E2M1_HW_CVT = tl.constexpr(is_sm10x())
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@triton.jit
|
| 71 |
+
def _quant_e2m1_packed(v, BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_K: tl.constexpr):
|
| 72 |
+
"""Pack signed, pre-scaled values on the E2M1 grid to ``(M, K//2)`` uint8 (first value
|
| 73 |
+
of each pair in the low nibble). On sm_100 the Blackwell hardware convert
|
| 74 |
+
``cvt.rn.satfinite.e2m1x2.f32`` (two f32 → one packed byte, first operand → HIGH nibble)
|
| 75 |
+
does it in one instruction; elsewhere a ``>=``-threshold bucketize builds the code. The
|
| 76 |
+
two agree except at exact E2M1 midpoints (0.25, 0.75, …), where the hardware rounds to
|
| 77 |
+
nearest-even and the ALU form rounds half-up. Only the taken arm compiles."""
|
| 78 |
+
if _E2M1_HW_CVT:
|
| 79 |
+
lo, hi = tl.split(tl.reshape(v, (BLOCK_SIZE_M, BLOCK_SIZE_K // 2, 2)))
|
| 80 |
+
packed = tl.inline_asm_elementwise(
|
| 81 |
+
"{ .reg .b8 t8; cvt.rn.satfinite.e2m1x2.f32 t8, $1, $2; cvt.u16.u8 $0, t8; }",
|
| 82 |
+
"=h,f,f",
|
| 83 |
+
[hi, lo],
|
| 84 |
+
dtype=tl.uint16,
|
| 85 |
+
is_pure=True,
|
| 86 |
+
pack=1,
|
| 87 |
+
)
|
| 88 |
+
values = (packed & 0xFF).to(tl.uint8)
|
| 89 |
+
else:
|
| 90 |
+
av = tl.abs(v)
|
| 91 |
+
code = (
|
| 92 |
+
(av >= 0.25).to(tl.int32)
|
| 93 |
+
+ (av >= 0.75).to(tl.int32)
|
| 94 |
+
+ (av >= 1.25).to(tl.int32)
|
| 95 |
+
+ (av >= 1.75).to(tl.int32)
|
| 96 |
+
+ (av >= 2.5).to(tl.int32)
|
| 97 |
+
+ (av >= 3.5).to(tl.int32)
|
| 98 |
+
+ (av >= 5.0).to(tl.int32)
|
| 99 |
+
) | ((v < 0).to(tl.int32) << 3)
|
| 100 |
+
lo, hi = tl.split(tl.reshape(code, (BLOCK_SIZE_M, BLOCK_SIZE_K // 2, 2)))
|
| 101 |
+
values = (lo | (hi << 4)).to(tl.uint8)
|
| 102 |
+
return values
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@triton.jit
|
| 106 |
+
def mx_act_quant_inline(
|
| 107 |
+
a_raw,
|
| 108 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 109 |
+
BLOCK_SIZE_K: tl.constexpr,
|
| 110 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 111 |
+
FORMAT: tl.constexpr = "mxfp8",
|
| 112 |
+
):
|
| 113 |
+
"""Inline MX activation quant, one helper for both value grids. Per-row, per-K-group
|
| 114 |
+
amax → UE8M0 scale (ceil to the next power of two via the exponent-bump trick, the
|
| 115 |
+
divisor being the grid's largest magnitude) → values onto the format's grid:
|
| 116 |
+
|
| 117 |
+
- ``"mxfp8"``: cast to E4M3 — returns ``((M, K) fp8, (M, K // SCALE_GROUP_K) uint8)``.
|
| 118 |
+
- ``"mxfp4"``: round to E2M1 and pack nibble pairs (``_quant_e2m1_packed`` — hardware
|
| 119 |
+
``cvt.e2m1x2`` on sm_100, else a ``>=``-threshold bucketize; they agree off exact
|
| 120 |
+
midpoints) — returns ``((M, K//2) uint8, (M, K // SCALE_GROUP_K) uint8)``.
|
| 121 |
+
- ``"nvfp4"``: E4M3 scale (amax/6, not a power of two), values divide by the DECODED
|
| 122 |
+
scale before the E2M1 grid — returns ``((M, K//2) uint8, (M, K // SCALE_GROUP_K) E4M3)``.
|
| 123 |
+
|
| 124 |
+
Only the taken format arm compiles."""
|
| 125 |
+
a_groups = tl.reshape(
|
| 126 |
+
a_raw, (BLOCK_SIZE_M, BLOCK_SIZE_K // SCALE_GROUP_K, SCALE_GROUP_K)
|
| 127 |
+
)
|
| 128 |
+
amax = tl.max(tl.abs(a_groups), axis=2)
|
| 129 |
+
if FORMAT == "nvfp4":
|
| 130 |
+
# E4M3 scale (amax/6 rounded to E4M3, NOT a power of two); values divide by the
|
| 131 |
+
# DECODED scale before hitting the E2M1 grid — the standard NVFP4 two-step
|
| 132 |
+
scales = (amax / 6.0).to(tl.float8e4nv)
|
| 133 |
+
decoded = tl.maximum(scales.to(tl.float32), 1.1754944e-38)
|
| 134 |
+
v = tl.reshape(a_groups / decoded[:, :, None], (BLOCK_SIZE_M, BLOCK_SIZE_K))
|
| 135 |
+
values = _quant_e2m1_packed(v, BLOCK_SIZE_M, BLOCK_SIZE_K)
|
| 136 |
+
elif FORMAT == "mxfp4":
|
| 137 |
+
bits = (amax / 6.0).to(tl.int32, bitcast=True)
|
| 138 |
+
# ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
|
| 139 |
+
exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
|
| 140 |
+
exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
|
| 141 |
+
exp_ceil = tl.where(amax == 0, 127, exp_ceil)
|
| 142 |
+
scales = exp_ceil.to(tl.uint8)
|
| 143 |
+
a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
|
| 144 |
+
v = tl.reshape(a_groups / a_s_pow2[:, :, None], (BLOCK_SIZE_M, BLOCK_SIZE_K))
|
| 145 |
+
values = _quant_e2m1_packed(v, BLOCK_SIZE_M, BLOCK_SIZE_K)
|
| 146 |
+
else:
|
| 147 |
+
bits = (amax / 448.0).to(tl.int32, bitcast=True)
|
| 148 |
+
# ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
|
| 149 |
+
exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
|
| 150 |
+
exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
|
| 151 |
+
scales = exp_ceil.to(tl.uint8)
|
| 152 |
+
a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
|
| 153 |
+
values = tl.reshape(
|
| 154 |
+
a_groups / tl.maximum(a_s_pow2[:, :, None], 1e-12),
|
| 155 |
+
(BLOCK_SIZE_M, BLOCK_SIZE_K),
|
| 156 |
+
).to(tl.float8e4nv)
|
| 157 |
+
return values, scales
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@triton.jit
|
| 161 |
+
def _e2m1_code_to_e4m3_bits(code):
|
| 162 |
+
"""One E2M1 4-bit code -> the E4M3 byte holding the same value, in pure integer
|
| 163 |
+
ops. Every E2M1 magnitude ``{0, .5, 1, 1.5, 2, 3, 4, 6}`` is exact in E4M3, and
|
| 164 |
+
above the 0.5 subnormal the mapping is affine in the code: ``bits = (mag + 12) << 2``
|
| 165 |
+
(exponent re-bias +6, mantissa bit lands at bit 2). No float math, no converts —
|
| 166 |
+
callers bitcast the byte to ``float8e4nv``."""
|
| 167 |
+
code = code.to(tl.int32)
|
| 168 |
+
mag = code & 7
|
| 169 |
+
bits = tl.where(mag == 0, 0, tl.where(mag == 1, 0x30, (mag + 12) << 2))
|
| 170 |
+
return bits | ((code >> 3) << 7)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
@triton.jit
|
| 174 |
+
def _e2m1_code_to_bf16_bits(code):
|
| 175 |
+
"""One E2M1 4-bit code -> the bf16 bit pattern (uint16) holding the same value, pure int ops.
|
| 176 |
+
The 8 magnitudes ``{0,.5,1,1.5,2,3,4,6}`` are exact in bf16; above the 0.5 subnormal the mapping
|
| 177 |
+
is affine in the code: ``bits = 0x3F80 + (mag - 2) * 0x40`` (each step doubles/1.5x's cleanly on
|
| 178 |
+
the bf16 exponent/mantissa). Callers bitcast the uint16 to bf16 — no float convert, and no E4M3
|
| 179 |
+
intermediate. bf16 is the exact intermediate for every activation dtype, not the final one: the
|
| 180 |
+
caller widens the dequantized tile if the activation is wider."""
|
| 181 |
+
code = code.to(tl.int32)
|
| 182 |
+
mag = code & 7
|
| 183 |
+
bits = tl.where(mag == 0, 0, tl.where(mag == 1, 0x3F00, 0x3F80 + (mag - 2) * 0x40))
|
| 184 |
+
return bits | ((code >> 3) << 15) # sign at bit 15
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@triton.jit
|
| 188 |
+
def _e2m1_pair_to_f16(packed):
|
| 189 |
+
"""One packed E2M1 byte -> the two fp16 values it holds, as a uint32 (low half = low nibble),
|
| 190 |
+
through the sm_100 CVT unit: ONE instruction per pair against the bit-twiddle form's ~10
|
| 191 |
+
integer ops. Every E2M1 magnitude is exact in fp16, so this is a value-identical decode, and
|
| 192 |
+
the direction of the unpack is irrelevant to the cost — the arithmetic is the whole bill
|
| 193 |
+
(measured on the gpt-oss W4A16 decode weight stream: 39.6us bit-twiddle, either axis, vs
|
| 194 |
+
11.6us here, against an 8.0us load-only floor). PTX has no 8-bit asm operand, so the byte
|
| 195 |
+
rides in a 16-bit register and is narrowed inside the block."""
|
| 196 |
+
return tl.inline_asm_elementwise(
|
| 197 |
+
"{ .reg .b8 t; cvt.u8.u16 t, $1; cvt.rn.f16x2.e2m1x2 $0, t; }",
|
| 198 |
+
"=r,h",
|
| 199 |
+
[packed.to(tl.uint16)],
|
| 200 |
+
dtype=tl.uint32,
|
| 201 |
+
is_pure=True,
|
| 202 |
+
pack=1,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@triton.jit
|
| 207 |
+
def e2m1_to_bf16(b_packed):
|
| 208 |
+
"""Unpack packed MXFP4 (E2M1, two nibbles/byte along K) straight to bf16, doubling the K (row)
|
| 209 |
+
dim: ``(R, C) uint8 -> (2R, C) bf16``. Direct (no E4M3 round-trip), and the low/high nibbles
|
| 210 |
+
interleave along K with a SINGLE 3D axis-swap (``join`` -> ``[R, C, 2]`` -> swap last two ->
|
| 211 |
+
reshape) instead of the three 2D transposes ``e2m1_to_e4m3`` needs — the weight-only unpack hot path
|
| 212 |
+
(fp4 weight upcast in-loop, the caller widening to the activation dtype). K order low-nibble-first, matching
|
| 213 |
+
``e2m1_to_e4m3``: ``[byte0_lo, byte0_hi, byte1_lo, ...]``. On sm_100 the codes decode through
|
| 214 |
+
the hardware CVT (``_e2m1_pair_to_f16``, fp16 being exact for the grid); elsewhere the integer
|
| 215 |
+
bit-build. Only the taken arm compiles."""
|
| 216 |
+
if _E2M1_HW_CVT:
|
| 217 |
+
pair = _e2m1_pair_to_f16(b_packed)
|
| 218 |
+
lo = (pair & 0xFFFF).to(tl.uint16)
|
| 219 |
+
hi = (pair >> 16).to(tl.uint16)
|
| 220 |
+
paired = tl.trans(tl.join(lo, hi), 0, 2, 1)
|
| 221 |
+
f16 = tl.reshape(
|
| 222 |
+
paired, (2 * b_packed.shape[0], b_packed.shape[1])
|
| 223 |
+
).to(tl.float16, bitcast=True)
|
| 224 |
+
return f16.to(tl.bfloat16)
|
| 225 |
+
lo = _e2m1_code_to_bf16_bits(b_packed & 0xF) # [R, C] int32 (bf16 bits)
|
| 226 |
+
hi = _e2m1_code_to_bf16_bits(b_packed >> 4) # [R, C]
|
| 227 |
+
paired = tl.join(lo, hi) # [R, C, 2] (pair in last dim)
|
| 228 |
+
paired = tl.trans(paired, 0, 2, 1) # [R, 2, C] (pair adjacent in row dim)
|
| 229 |
+
out = tl.reshape(paired, (2 * b_packed.shape[0], b_packed.shape[1])) # [2R, C]
|
| 230 |
+
return out.to(tl.uint16).to(tl.bfloat16, bitcast=True)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@triton.jit
|
| 234 |
+
def _e2m1_pair_to_e4m3(packed):
|
| 235 |
+
"""One packed E2M1 byte -> its two values as an E4M3 pair (uint16, low byte = low nibble),
|
| 236 |
+
through the CVT unit in two instructions — fp4 to fp16 to fp8 — against the bit-build's ~8
|
| 237 |
+
integer ops per value. Both hops are exact: every E2M1 magnitude is representable in fp16
|
| 238 |
+
and in E4M3, so the pair is value-identical to ``_e2m1_code_to_e4m3_bits``."""
|
| 239 |
+
return tl.inline_asm_elementwise(
|
| 240 |
+
"{ .reg .b8 t; .reg .b32 f; cvt.u8.u16 t, $1; cvt.rn.f16x2.e2m1x2 f, t; "
|
| 241 |
+
"cvt.rn.satfinite.e4m3x2.f16x2 $0, f; }",
|
| 242 |
+
"=h,h",
|
| 243 |
+
[packed.to(tl.uint16)],
|
| 244 |
+
dtype=tl.uint16,
|
| 245 |
+
is_pure=True,
|
| 246 |
+
pack=1,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@triton.jit
|
| 251 |
+
def e2m1_cols_to_bf16(packed):
|
| 252 |
+
"""Column-unpack packed E2M1 to bf16: ``(..., C) uint8 -> (..., 2C)``, low nibble first — the
|
| 253 |
+
column-axis counterpart of the row-doubling ``e2m1_to_bf16``, for the swapped weight-only
|
| 254 |
+
tile (output rows leading, K contiguous). Same sm_100 hardware decode; the pair lands in
|
| 255 |
+
adjacent columns, so ``tl.interleave`` places it (no axis swap)."""
|
| 256 |
+
if _E2M1_HW_CVT:
|
| 257 |
+
pair = _e2m1_pair_to_f16(packed)
|
| 258 |
+
lo = (pair & 0xFFFF).to(tl.uint16)
|
| 259 |
+
hi = (pair >> 16).to(tl.uint16)
|
| 260 |
+
f16 = tl.interleave(lo, hi).to(tl.float16, bitcast=True)
|
| 261 |
+
return f16.to(tl.bfloat16)
|
| 262 |
+
bits = tl.interleave(
|
| 263 |
+
_e2m1_code_to_bf16_bits(packed & 0xF), _e2m1_code_to_bf16_bits(packed >> 4)
|
| 264 |
+
)
|
| 265 |
+
return bits.to(tl.uint16).to(tl.bfloat16, bitcast=True)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@triton.jit
|
| 269 |
+
def e2m1_cols_to_f32(packed):
|
| 270 |
+
"""Column-unpack packed E2M1 straight to fp32, for the CUDA-core reduces: ``(..., C) uint8 ->
|
| 271 |
+
(..., 2C)``. The narrow-float hop its siblings owe their MMA callers (bf16 for ``tl.dot``, E4M3
|
| 272 |
+
for the scaled MMA) is pure work for a reduce that widens to fp32 anyway — measured 12.6 ->
|
| 273 |
+
15.4us on the gpt-oss decode tile for the bf16 round trip alone. Bit-identical to going via
|
| 274 |
+
either: every E2M1 value is exact in f16, bf16, E4M3 and fp32."""
|
| 275 |
+
if _E2M1_HW_CVT:
|
| 276 |
+
pair = _e2m1_pair_to_f16(packed)
|
| 277 |
+
lo = (pair & 0xFFFF).to(tl.uint16)
|
| 278 |
+
hi = (pair >> 16).to(tl.uint16)
|
| 279 |
+
f16 = tl.interleave(lo, hi).to(tl.float16, bitcast=True)
|
| 280 |
+
return f16.to(tl.float32)
|
| 281 |
+
bits = tl.interleave(
|
| 282 |
+
_e2m1_code_to_bf16_bits(packed & 0xF), _e2m1_code_to_bf16_bits(packed >> 4)
|
| 283 |
+
)
|
| 284 |
+
return bits.to(tl.uint16).to(tl.bfloat16, bitcast=True).to(tl.float32)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@triton.jit
|
| 288 |
+
def e2m1_cols_to_e4m3(packed):
|
| 289 |
+
"""Column-unpack packed E2M1 (two nibbles per byte along the last dim, low nibble
|
| 290 |
+
first) to E4M3: ``(..., C) uint8 -> (..., 2C)`` — the column-axis counterpart of the
|
| 291 |
+
row-doubling ``e2m1_to_e4m3``; lossless (every E2M1 value is exact in E4M3). sm_100
|
| 292 |
+
decodes through the CVT unit (``_e2m1_pair_to_e4m3``), elsewhere the integer bit-build;
|
| 293 |
+
either way the bytes are bitcast once. Only the taken arm compiles."""
|
| 294 |
+
if _E2M1_HW_CVT:
|
| 295 |
+
pair = _e2m1_pair_to_e4m3(packed)
|
| 296 |
+
bits = tl.interleave(pair & 0xFF, pair >> 8)
|
| 297 |
+
else:
|
| 298 |
+
bits = tl.interleave(
|
| 299 |
+
_e2m1_code_to_e4m3_bits(packed & 0xF), _e2m1_code_to_e4m3_bits(packed >> 4)
|
| 300 |
+
)
|
| 301 |
+
return bits.to(tl.uint8).to(tl.float8e4nv, bitcast=True)
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
@triton.jit
|
| 305 |
+
def e2m1_to_f32(b_packed):
|
| 306 |
+
"""Row-doubling counterpart of ``e2m1_cols_to_f32``: ``(R, C) uint8 -> (2R, C)`` fp32, for the
|
| 307 |
+
non-swapped CUDA-core reduce. Skips the E4M3 hop ``e2m1_to_e4m3`` owes its ``tl.dot`` callers;
|
| 308 |
+
bit-identical, since every E2M1 value is exact in E4M3, f16 and fp32."""
|
| 309 |
+
if _E2M1_HW_CVT:
|
| 310 |
+
pair = _e2m1_pair_to_f16(b_packed)
|
| 311 |
+
lo = (pair & 0xFFFF).to(tl.uint16)
|
| 312 |
+
hi = (pair >> 16).to(tl.uint16)
|
| 313 |
+
unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
|
| 314 |
+
return unpacked.to(tl.uint16).to(tl.float16, bitcast=True).to(tl.float32)
|
| 315 |
+
lo = _e2m1_code_to_bf16_bits(b_packed & 0xF)
|
| 316 |
+
hi = _e2m1_code_to_bf16_bits(b_packed >> 4)
|
| 317 |
+
unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
|
| 318 |
+
return unpacked.to(tl.uint16).to(tl.bfloat16, bitcast=True).to(tl.float32)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
@triton.jit
|
| 322 |
+
def e2m1_to_e4m3(b_packed):
|
| 323 |
+
"""Unpack packed MXFP4 (E2M1, two nibbles/byte along K) to E4M3, doubling the K
|
| 324 |
+
(row) dim: ``(R, C) uint8 -> (2R, C) E4M3``. E2M1's 8 magnitudes are all exact in
|
| 325 |
+
E4M3, so this is lossless — it lets the FP8 ``tl.dot`` path stand in for
|
| 326 |
+
``tl.dot_scaled`` at decode (avoiding its M->128 pad). K order is the low nibble
|
| 327 |
+
first: ``[byte0_lo, byte0_hi, byte1_lo, ...]``. sm_100 decodes through the CVT unit
|
| 328 |
+
(``_e2m1_pair_to_e4m3``); elsewhere the integer bit-build. Only the taken arm compiles."""
|
| 329 |
+
if _E2M1_HW_CVT:
|
| 330 |
+
pair = _e2m1_pair_to_e4m3(b_packed)
|
| 331 |
+
lo, hi = pair & 0xFF, pair >> 8
|
| 332 |
+
else:
|
| 333 |
+
lo = _e2m1_code_to_e4m3_bits(b_packed & 0xF)
|
| 334 |
+
hi = _e2m1_code_to_e4m3_bits(b_packed >> 4)
|
| 335 |
+
# interleave along the K (row) dim via trans -> interleave-last-dim -> trans back
|
| 336 |
+
unpacked = tl.trans(tl.interleave(tl.trans(lo), tl.trans(hi)))
|
| 337 |
+
return unpacked.to(tl.uint8).to(tl.float8e4nv, bitcast=True)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _quant_block_k_pruner(configs, named_args, **kwargs):
|
| 341 |
+
"""Keep configs whose BLOCK_K divides K (the quant grid is K // BLOCK_K programs per row;
|
| 342 |
+
K is always a multiple of 32, so the BLOCK_K=32 configs guarantee a non-empty list). On the
|
| 343 |
+
SWIZZLED path, additionally require BLOCK_K a multiple of ``4 * SCALE_GROUP_K`` so a whole
|
| 344 |
+
SWIZZLE_32_4_4 col-block (4 scale groups) lands inside one K-tile. BLOCK_T is free on the dense
|
| 345 |
+
grid (the per-element store handles any tile height) but pinned to 128 on the GROUPED grid,
|
| 346 |
+
where one program == one 128-row expert-sorted tile (``build_tile_layout``'s pad granularity)."""
|
| 347 |
+
args = {**named_args, **kwargs}
|
| 348 |
+
k, g = args["K"], args["SCALE_GROUP_K"]
|
| 349 |
+
swizzled, grouped = args.get("SWIZZLED", False), args.get("GROUPED", True)
|
| 350 |
+
return [
|
| 351 |
+
c
|
| 352 |
+
for c in configs
|
| 353 |
+
if k % c.kwargs["BLOCK_K"] == 0
|
| 354 |
+
and (not swizzled or c.kwargs["BLOCK_K"] % (4 * g) == 0)
|
| 355 |
+
and (not grouped or c.kwargs["BLOCK_T"] == 128)
|
| 356 |
+
]
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
@triton.jit
|
| 360 |
+
def store_mx_act_scales(
|
| 361 |
+
S,
|
| 362 |
+
SOut,
|
| 363 |
+
out_row, # per-lane output row (any int vector: contiguous grid rows or scattered dests)
|
| 364 |
+
s,
|
| 365 |
+
row_mask,
|
| 366 |
+
kb,
|
| 367 |
+
T,
|
| 368 |
+
K: tl.constexpr,
|
| 369 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 370 |
+
BLOCK_K: tl.constexpr,
|
| 371 |
+
SWIZZLED: tl.constexpr,
|
| 372 |
+
MASK: tl.constexpr, # "rows" (per-lane), "blocks" (whole 128-row blocks past T), "none"
|
| 373 |
+
):
|
| 374 |
+
"""Store one act-quant scale tile. Row-major (``S``, per ``out_row``) or straight into the
|
| 375 |
+
SWIZZLE_32_4_4 buffer (``SOut``) as PER-ELEMENT ptr arithmetic (no 128-row reshape): s[t, g]
|
| 376 |
+
lands at (block*cb_total + kb*REP_K + rep)*512 + r32*16 + outer4*4 + c4, where block =
|
| 377 |
+
out_row//128, r32 = out_row%32, outer4 = (out_row%128)//32, and the col-block splits g into
|
| 378 |
+
rep = g//4, c4 = g%4. Byte-identical to the old reshape form at BLOCK_T=128 (grouped grid)
|
| 379 |
+
but valid for any out_row vector, so the dense grid, the grouped-sorted grid, and the
|
| 380 |
+
scatter (quantize-once) stores all share it. Masking is the caller\'s store policy: the
|
| 381 |
+
grouped-sorted grid writes its padding rows too ("none" — quantized zeros the GEMM never
|
| 382 |
+
reads), the dense grid masks whole tiles past the real row-blocks ("blocks"), scattered
|
| 383 |
+
and row-major stores mask per lane ("rows")."""
|
| 384 |
+
groups: tl.constexpr = BLOCK_K // SCALE_GROUP_K
|
| 385 |
+
if SWIZZLED:
|
| 386 |
+
cb_total = K // SCALE_GROUP_K // 4
|
| 387 |
+
lg = tl.arange(0, groups)
|
| 388 |
+
rep = lg // 4
|
| 389 |
+
block = out_row // 128
|
| 390 |
+
off = (
|
| 391 |
+
(block[:, None] * cb_total + kb * (groups // 4) + rep[None, :]) * 512
|
| 392 |
+
+ (out_row % 32)[:, None] * 16
|
| 393 |
+
+ ((out_row % 128) // 32)[:, None] * 4
|
| 394 |
+
+ (lg % 4)[None, :]
|
| 395 |
+
)
|
| 396 |
+
if MASK == "rows":
|
| 397 |
+
tl.store(SOut + off, s, mask=row_mask[:, None])
|
| 398 |
+
elif MASK == "blocks":
|
| 399 |
+
tl.store(SOut + off, s, mask=block[:, None] < tl.cdiv(T, 128))
|
| 400 |
+
else:
|
| 401 |
+
tl.store(SOut + off, s)
|
| 402 |
+
else:
|
| 403 |
+
sg = kb * groups + tl.arange(0, groups)
|
| 404 |
+
tl.store(
|
| 405 |
+
S + out_row.to(tl.int64)[:, None] * (K // SCALE_GROUP_K) + sg[None, :],
|
| 406 |
+
s,
|
| 407 |
+
mask=row_mask[:, None],
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@bayesian_autotune(
|
| 412 |
+
[
|
| 413 |
+
triton.Config({"BLOCK_K": bk, "BLOCK_T": bt}, num_warps=w)
|
| 414 |
+
for bk in (32, 64, 128, 256)
|
| 415 |
+
for bt in (8, 16, 32, 64, 128)
|
| 416 |
+
for w in (2, 4, 8)
|
| 417 |
+
],
|
| 418 |
+
# t_bucket (log2 of the token count) is in the key: at small T the tile is the only
|
| 419 |
+
# parallelism lever while at prefill scale it isn't — same bucketing as the grouped
|
| 420 |
+
# kernels (raw T would retune per unique token count). SWIZZLED keys the swizzled-scale
|
| 421 |
+
# store separately (a disjoint config basin), GROUPED the grid — its 128-row expert tiles
|
| 422 |
+
# pin BLOCK_T, so its crowns cannot be shared with the dense grid's. FORMAT keys the value
|
| 423 |
+
# dtype/packing (E4M3 vs packed E2M1, and SCALE_GROUP_K 32 vs 16) — a dtype-blind key hands
|
| 424 |
+
# packed MXFP4 the E4M3 config and mistunes it.
|
| 425 |
+
["K", "t_bucket", "SWIZZLED", "GROUPED", "FORMAT"],
|
| 426 |
+
n_trials=100,
|
| 427 |
+
prune_configs_by={"early_config_prune": _quant_block_k_pruner},
|
| 428 |
+
)
|
| 429 |
+
@triton.jit
|
| 430 |
+
def _mx_act_quant_kernel(
|
| 431 |
+
X,
|
| 432 |
+
Y,
|
| 433 |
+
S, # (T, K // SCALE_GROUP_K) row-major scales; None on the swizzled path
|
| 434 |
+
SOut, # flat SWIZZLE_32_4_4 scale buffer (1, n_tiles, cb, 2, 256); None on the plain path
|
| 435 |
+
GatherIdx, # (S,) int32 sorted position -> source row of X; read only when GROUPED and not None
|
| 436 |
+
ExpertStart, # (NUM_EXPERTS_POW2 + 1,) int32 cumulative sorted-row starts; read iff GROUPED
|
| 437 |
+
GlobalScale, # fp32 NVFP4 second-level global, per tensor or per expert; None ⇒ single-level (arm folds out)
|
| 438 |
+
GlobalIdx, # (T,) row -> expert map, for a per-expert global on the DENSE grid (the batched op's own expert_ids); None on the grouped grid, whose tiles carry an expert
|
| 439 |
+
stride_global, # expert stride of GlobalScale (0 broadcasts a scalar)
|
| 440 |
+
last_global, # index of GlobalScale's last entry — clamps GlobalIdx, whose EP sentinel rows name an out-of-range expert (they are masked out of the GEMM, so any in-range global serves them)
|
| 441 |
+
stride_x_t,
|
| 442 |
+
stride_x_k,
|
| 443 |
+
T,
|
| 444 |
+
t_bucket, # autotune key only (log2 token-count bucket); unused in body
|
| 445 |
+
K: tl.constexpr,
|
| 446 |
+
SCALE_GROUP_K: tl.constexpr,
|
| 447 |
+
# dynamo's triton wrapper appends the tuner's config kwargs after the call kwargs
|
| 448 |
+
# and requires signature order — the tuned axes stay LAST
|
| 449 |
+
FORMAT: tl.constexpr = "mxfp8",
|
| 450 |
+
SWIZZLED: tl.constexpr = False,
|
| 451 |
+
GROUPED: tl.constexpr = True, # grid: expert-sorted tiles (True) vs plain dense rows (False)
|
| 452 |
+
NUM_EXPERTS_POW2: tl.constexpr = 1, # always passed explicitly; see the dense launch
|
| 453 |
+
BLOCK_K: tl.constexpr = 32,
|
| 454 |
+
BLOCK_T: tl.constexpr = 32,
|
| 455 |
+
PDL: tl.constexpr = False,
|
| 456 |
+
):
|
| 457 |
+
"""One-pass activation quant, one launch per format (``mx_act_quant_inline`` does
|
| 458 |
+
the math, so the offline and inline forms are bit-identical by construction): E4M3 +
|
| 459 |
+
UE8M0 ("mxfp8"), packed E2M1 + UE8M0 ("mxfp4"), or packed E2M1 + E4M3 group-16
|
| 460 |
+
("nvfp4"). Group boundaries are identical across forms (SCALE_GROUP_K | BLOCK_K | K).
|
| 461 |
+
Arbitrary input strides.
|
| 462 |
+
|
| 463 |
+
Two independent axes. ``GROUPED`` picks the GRID: dense ``(cdiv(T, BLOCK_T), K // BLOCK_K)``
|
| 464 |
+
over plain row tiles (the one-row-per-program form starved memory at 1.5-1.8 TB/s on the
|
| 465 |
+
packed formats; the row tile coalesces), or ``(n_m_tiles, K // BLOCK_K)`` over the
|
| 466 |
+
expert-sorted, 128-padded tile layout (``build_tile_layout``, so BLOCK_T is pinned there).
|
| 467 |
+
A grouped program gathers its tile's source rows through ``GatherIdx`` (padding masked) and
|
| 468 |
+
scatters the VALUES back to source row order — the GEMM still TMA-gathers them, and duplicate
|
| 469 |
+
sorted->source writes store identical bytes — and its tile carries the expert a per-expert
|
| 470 |
+
NVFP4 global is indexed by, which is why only the dense grid needs a row map for that.
|
| 471 |
+
|
| 472 |
+
``SWIZZLED`` picks the SCALE STORE: straight into the SWIZZLE_32_4_4 layout the grouped GEMM
|
| 473 |
+
reads affine (the inverse of ``load_swizzled_scale``), so no post-quant gather/swizzle pass,
|
| 474 |
+
or row-major ``S``. Padding-row scales are quantized zeros (harmless: the GEMM masks those
|
| 475 |
+
rows' values to 0)."""
|
| 476 |
+
if PDL:
|
| 477 |
+
gdc_wait()
|
| 478 |
+
kb = tl.program_id(1)
|
| 479 |
+
if SWIZZLED:
|
| 480 |
+
pid_m = tl.program_id(0)
|
| 481 |
+
# scale output-row position (== source row on the dense path); the swizzled block index is
|
| 482 |
+
# so // 128, valid for ANY BLOCK_T (no 128-row pin) so the dense grid autotunes BLOCK_T.
|
| 483 |
+
so = pid_m * BLOCK_T + tl.arange(0, BLOCK_T)
|
| 484 |
+
if GROUPED:
|
| 485 |
+
exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
|
| 486 |
+
ExpertStart, NUM_EXPERTS_POW2, BLOCK_T
|
| 487 |
+
)
|
| 488 |
+
expert_id, sorted_idx, row_mask = resolve_tile_inline(
|
| 489 |
+
tl.program_id(0), exp_start, freqs, tile_start_excl, e_offs, BLOCK_T
|
| 490 |
+
)
|
| 491 |
+
if GatherIdx is not None:
|
| 492 |
+
in_row = tl.load(GatherIdx + sorted_idx, mask=row_mask, other=0).to(tl.int64)
|
| 493 |
+
else:
|
| 494 |
+
in_row = sorted_idx.to(tl.int64)
|
| 495 |
+
else:
|
| 496 |
+
expert_id = 0
|
| 497 |
+
rows = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
|
| 498 |
+
in_row = rows.to(tl.int64)
|
| 499 |
+
row_mask = rows < T
|
| 500 |
+
offs = kb * BLOCK_K + tl.arange(0, BLOCK_K)
|
| 501 |
+
x = tl.load(
|
| 502 |
+
X + in_row[:, None] * stride_x_t + offs[None, :] * stride_x_k,
|
| 503 |
+
mask=row_mask[:, None],
|
| 504 |
+
other=0.0,
|
| 505 |
+
).to(tl.float32)
|
| 506 |
+
if GlobalScale is not None:
|
| 507 |
+
# NVFP4 two-level: normalize by the calibrated global before the block quant — block
|
| 508 |
+
# scales are then amax/6 of x/g (the canonical two-step); the GEMM folds g back onto the
|
| 509 |
+
# accumulator (g_a·g_b). A per-expert global is indexed by the tile's OWN expert, which
|
| 510 |
+
# the grouped grid resolved for free — the rows of a tile belong to one expert; the dense
|
| 511 |
+
# grid has no such layout and takes the caller's row map. None folds the arm out.
|
| 512 |
+
if GlobalIdx is not None:
|
| 513 |
+
e = tl.minimum(tl.load(GlobalIdx + in_row, mask=row_mask, other=0), last_global)
|
| 514 |
+
x = x / tl.load(GlobalScale + e * stride_global).to(tl.float32)[:, None]
|
| 515 |
+
else:
|
| 516 |
+
x = x / tl.load(GlobalScale + expert_id * stride_global).to(tl.float32)
|
| 517 |
+
y, s = mx_act_quant_inline(x, BLOCK_T, BLOCK_K, SCALE_GROUP_K, FORMAT)
|
| 518 |
+
width: tl.constexpr = BLOCK_K // 2 if FORMAT != "mxfp8" else BLOCK_K
|
| 519 |
+
y_row: tl.constexpr = K // (BLOCK_K // width) # per-row element count of Y
|
| 520 |
+
yo = kb * width + tl.arange(0, width)
|
| 521 |
+
# values -> source row order (the swizzled grid scatters via the gathered in_row)
|
| 522 |
+
if PDL:
|
| 523 |
+
gdc_launch_dependents()
|
| 524 |
+
tl.store(Y + in_row[:, None] * y_row + yo[None, :], y, mask=row_mask[:, None])
|
| 525 |
+
if SWIZZLED:
|
| 526 |
+
store_mx_act_scales(
|
| 527 |
+
S, SOut, so, s, row_mask, kb, T, K, SCALE_GROUP_K, BLOCK_K, True,
|
| 528 |
+
"none" if GROUPED else "blocks",
|
| 529 |
+
)
|
| 530 |
+
else:
|
| 531 |
+
store_mx_act_scales(
|
| 532 |
+
S, SOut, in_row, s, row_mask, kb, T, K, SCALE_GROUP_K, BLOCK_K, False, "rows"
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def mx_act_quant_grouped(
|
| 537 |
+
x: torch.Tensor,
|
| 538 |
+
activation_format: str,
|
| 539 |
+
scale_group: int,
|
| 540 |
+
scale_dtype: torch.dtype,
|
| 541 |
+
gather_idx: torch.Tensor | None,
|
| 542 |
+
expert_start: torch.Tensor,
|
| 543 |
+
global_scale: torch.Tensor | None = None,
|
| 544 |
+
swizzled: bool = True,
|
| 545 |
+
) -> tuple[torch.Tensor, torch.Tensor, int]:
|
| 546 |
+
"""Offline MX act-quant on the GROUPED grid of ``_mx_act_quant_kernel``: one program per
|
| 547 |
+
expert-sorted tile, so each tile knows its own expert and a per-expert NVFP4 global needs no
|
| 548 |
+
per-row index. Returns ``(values, scales, num_m_tiles)`` — the values in source order (E4M3,
|
| 549 |
+
or packed E2M1 uint8 for the fp4 formats) and the scales either as the ``(1, num_m_tiles, cb,
|
| 550 |
+
2, 256)`` SWIZZLE_32_4_4 tensor the GEMM reads through a descriptor (``swizzled``) or
|
| 551 |
+
row-major ``(T, K // scale_group)`` for the affine arm. Only the ``expert_start[-1]``
|
| 552 |
+
scheduled rows are laid out; expert padding is per 128 (the ``BLOCK_T`` pin).
|
| 553 |
+
|
| 554 |
+
``n_m_tiles`` is a STATIC host-side upper bound (``S//128 + E``) on the padded tile count —
|
| 555 |
+
never ``.item()`` (a CPU-GPU sync / cudagraph break). The buffer/grid over-allocate to it; the
|
| 556 |
+
kernel writes only the real tiles (``build_tile_layout`` in-kernel), extra tile-programs mask
|
| 557 |
+
out, and the GEMM reads only ``pid_m < total_m_tiles``. ``sum(ceil(freq/128)) <= S//128 + E``."""
|
| 558 |
+
T, K = x.shape
|
| 559 |
+
E = expert_start.numel() - 1
|
| 560 |
+
S = gather_idx.numel() if gather_idx is not None else T
|
| 561 |
+
n_m_tiles = S // 128 + E
|
| 562 |
+
packed = activation_format != "mxfp8"
|
| 563 |
+
y = torch.empty(
|
| 564 |
+
T, K // 2 if packed else K, device=x.device, dtype=torch.uint8 if packed else FP8_DTYPE
|
| 565 |
+
)
|
| 566 |
+
if swizzled:
|
| 567 |
+
cb = triton.cdiv(K // scale_group, 4)
|
| 568 |
+
scales = torch.empty(1, n_m_tiles, cb, 2, 256, device=x.device, dtype=scale_dtype)
|
| 569 |
+
else:
|
| 570 |
+
scales = torch.empty(T, K // scale_group, device=x.device, dtype=scale_dtype)
|
| 571 |
+
with device_context(x.device):
|
| 572 |
+
compile_time_only_triton_wrap(_mx_act_quant_kernel)[
|
| 573 |
+
lambda META: (n_m_tiles, K // META["BLOCK_K"])
|
| 574 |
+
](
|
| 575 |
+
x,
|
| 576 |
+
y,
|
| 577 |
+
None if swizzled else scales, # S: row-major scales, affine arm only
|
| 578 |
+
scales if swizzled else None, # SOut: SWIZZLE_32_4_4 buffer, swizzled arm only
|
| 579 |
+
gather_idx, # None = no gather (the is-not-None guard folds the load out)
|
| 580 |
+
expert_start,
|
| 581 |
+
global_scale, # fp32 NVFP4 two-level global; None ⇒ single-level (arm folds out)
|
| 582 |
+
None, # GlobalIdx: the grouped tiles carry their own expert
|
| 583 |
+
global_scale_stride(global_scale),
|
| 584 |
+
0, # last_global: unread without a row map
|
| 585 |
+
x.stride(0),
|
| 586 |
+
x.stride(1),
|
| 587 |
+
T,
|
| 588 |
+
T.bit_length(),
|
| 589 |
+
K=K,
|
| 590 |
+
SCALE_GROUP_K=scale_group,
|
| 591 |
+
FORMAT=activation_format,
|
| 592 |
+
SWIZZLED=swizzled,
|
| 593 |
+
GROUPED=True,
|
| 594 |
+
NUM_EXPERTS_POW2=E,
|
| 595 |
+
PDL=decode_pdl(),
|
| 596 |
+
launch_pdl=decode_pdl(),
|
| 597 |
+
)
|
| 598 |
+
return y, scales, n_m_tiles
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
@triton.jit
|
| 602 |
+
def _swizzle_grouped_scales_kernel(
|
| 603 |
+
SRC, # (rows, cols) row-major pre-quantized group scales (uint8 / e8m0 / e4m3, 1 byte)
|
| 604 |
+
DST, # flat SWIZZLE_32_4_4 output buffer (1, n_tiles, NCB, 2, 256)
|
| 605 |
+
GatherIdx, # (S,) int sorted position -> source row of SRC; read only when not None
|
| 606 |
+
ExpertStart, # (NUM_EXPERTS_POW2 + 1,) cumulative sorted-row starts, S sentinel
|
| 607 |
+
COLS,
|
| 608 |
+
NCB, # number of 4-wide column blocks (cols // 4)
|
| 609 |
+
stride_src_m,
|
| 610 |
+
NUM_EXPERTS_POW2: tl.constexpr,
|
| 611 |
+
):
|
| 612 |
+
"""Gather + expert-sorted 128-pad + swizzle a PRE-QUANTIZED grouped scale into the
|
| 613 |
+
SWIZZLE_32_4_4 layout, in one launch — the padded tile layout is derived in-kernel from
|
| 614 |
+
``ExpertStart`` (``build_tile_layout``), so no torch index build. One 128x4 block per
|
| 615 |
+
(M-tile, col-block): inverse of ``load_swizzled_scale``. Padding rows write quantized-zero
|
| 616 |
+
scales (the GEMM masks those rows)."""
|
| 617 |
+
exp_start, freqs, tile_start_excl, total_m_tiles, e_offs = build_tile_layout(
|
| 618 |
+
ExpertStart, NUM_EXPERTS_POW2, 128
|
| 619 |
+
)
|
| 620 |
+
pid_m = tl.program_id(0)
|
| 621 |
+
cb = tl.program_id(1)
|
| 622 |
+
_, sorted_idx, row_mask = resolve_tile_inline(
|
| 623 |
+
pid_m, exp_start, freqs, tile_start_excl, e_offs, 128
|
| 624 |
+
)
|
| 625 |
+
if GatherIdx is not None:
|
| 626 |
+
src = tl.load(GatherIdx + sorted_idx, mask=row_mask, other=0)
|
| 627 |
+
else:
|
| 628 |
+
src = sorted_idx
|
| 629 |
+
cj = cb * 4 + tl.arange(0, 4)
|
| 630 |
+
s = tl.load(
|
| 631 |
+
SRC + src[:, None] * stride_src_m + cj[None, :],
|
| 632 |
+
mask=row_mask[:, None] & (cj[None, :] < COLS),
|
| 633 |
+
other=0,
|
| 634 |
+
)
|
| 635 |
+
swizzle_store_block(DST, s, pid_m, cb, NCB)
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
def swizzle_grouped_mx_scales(
|
| 639 |
+
scale: torch.Tensor,
|
| 640 |
+
expert_start: torch.Tensor,
|
| 641 |
+
gather_idx: torch.Tensor | None = None,
|
| 642 |
+
pad_bm: int = 128,
|
| 643 |
+
) -> tuple[torch.Tensor, int]:
|
| 644 |
+
"""Fused gather + expert-sorted 128-pad + swizzle of a pre-quantized grouped scale (a
|
| 645 |
+
down projection's externally supplied ``As``), one triton launch — the layout comes from
|
| 646 |
+
``expert_start`` in-kernel (no torch index build). Returns ``(swizzled_scale, num_m_tiles)``
|
| 647 |
+
as the ``(1, num_m_tiles, cols // 4, 2, 256)`` tensor; the caller builds the GEMM's scale
|
| 648 |
+
descriptor from it. ``gather_idx`` None = activations already expert-sorted.
|
| 649 |
+
|
| 650 |
+
``n_m_tiles`` is a STATIC host-side upper bound (``S//pad_bm + E``) — never ``.item()`` (a
|
| 651 |
+
CPU-GPU sync / cudagraph break); the buffer/grid over-allocate and the extra tile-programs
|
| 652 |
+
mask out (``sum(ceil(freq/pad_bm)) <= S//pad_bm + E``)."""
|
| 653 |
+
E = expert_start.numel() - 1
|
| 654 |
+
S = gather_idx.numel() if gather_idx is not None else scale.shape[0]
|
| 655 |
+
n_m_tiles = S // pad_bm + E
|
| 656 |
+
cols = scale.shape[1]
|
| 657 |
+
cb = triton.cdiv(cols, 4)
|
| 658 |
+
src = scale.view(torch.uint8) # byte-level; the binder rejects e8m0/e4m3
|
| 659 |
+
out = torch.empty(1, n_m_tiles, cb, 2, 256, device=scale.device, dtype=torch.uint8)
|
| 660 |
+
with device_context(scale.device):
|
| 661 |
+
compile_time_only_triton_wrap(_swizzle_grouped_scales_kernel)[(n_m_tiles, cb)](
|
| 662 |
+
src,
|
| 663 |
+
out, # flat SWIZZLE_32_4_4 buffer (pointer store; no descriptor)
|
| 664 |
+
gather_idx, # None = no gather (the is-not-None guard folds the load out)
|
| 665 |
+
expert_start,
|
| 666 |
+
cols,
|
| 667 |
+
cb,
|
| 668 |
+
src.stride(0),
|
| 669 |
+
NUM_EXPERTS_POW2=E,
|
| 670 |
+
)
|
| 671 |
+
return out.view(scale.dtype), n_m_tiles
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def maybe_act_quant(x, act_quant, min_m):
|
| 675 |
+
"""Row-count-gated offline activation pre-quant. Apply ``act_quant`` (a one-pass
|
| 676 |
+
quant kernel, e.g. ``mxfp8_act_quant``) when the GEMM consuming ``x`` is
|
| 677 |
+
compute-bound (``rows >= min_m``) — the inline form re-quantizes per N-tile there.
|
| 678 |
+
``min_m`` is the consumer kernel's crossover, defined next to its wrapper with its
|
| 679 |
+
provenance (measured sweep or inherited estimate). Below the threshold return ``x``
|
| 680 |
+
raw: the weight-bandwidth-bound GEMM quantizes its
|
| 681 |
+
one thin tile inline for free (the UE8M0 inline quant is exponent-only), and a
|
| 682 |
+
separate quant kernel is pure added latency (M=1 graph attn decode measured
|
| 683 |
+
0.66-0.85x offline). Bit-exact either way. Returns ``(a, a_scale)``; the consumer
|
| 684 |
+
kernel picks its arm off ``a``'s dtype at compile time (fp8 = pre-quantized, raw
|
| 685 |
+
bf16/fp16 = quantize inline), so in the inline arm ``a_scale`` is a constexpr-dead
|
| 686 |
+
placeholder."""
|
| 687 |
+
if x.shape[0] >= min_m:
|
| 688 |
+
return act_quant(x)
|
| 689 |
+
return x, x
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
def mxfp8_act_quant(x: torch.Tensor, swizzled: bool = False) -> tuple[torch.Tensor, torch.Tensor]:
|
| 693 |
+
"""Quantize ``(T, K)`` activations to MX once (E4M3 values + UE8M0 group-32 uint8 scales)
|
| 694 |
+
instead of inline per weight-tile — the fused gate_up re-ran the inline quant per N-tile
|
| 695 |
+
(16x redundant amax/convert ALU + 2x act bytes), which held it at ~380 TFLOPS while the
|
| 696 |
+
pre-quantized down kernel ran at ~1080. One pass costs ~50µs at 8k tokens. Bit-exact with
|
| 697 |
+
the inline form (same group boundaries). ``swizzled=True`` emits the scale directly in
|
| 698 |
+
SWIZZLE_32_4_4 for the tcgen05 fast path (same dense grid, per-element store)."""
|
| 699 |
+
return _launch_act_quant(x, "mxfp8", MX_SCALE_GROUP_K, torch.uint8, swizzled)
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
def mxfp4_act_quant(x: torch.Tensor, swizzled: bool = False) -> tuple[torch.Tensor, torch.Tensor]:
|
| 703 |
+
"""Quantize ``(T, K)`` activations to MXFP4 in one kernel pass: packed-E2M1 values
|
| 704 |
+
(``(T, K//2)`` int8, two codes per byte, first value in the low nibble) + UE8M0
|
| 705 |
+
group-32 uint8 scales (``(T, K//32)``, amax/6 ceil'd to a power of two). Bit-identical
|
| 706 |
+
to the fused epilogues' inline form (shared ``mx_act_quant_inline`` arm). Feeds the
|
| 707 |
+
W4A4 arm of the MX matmul ops; quantizing activations to fp4 at runtime is an
|
| 708 |
+
accuracy call the caller owns — the ops never do it implicitly. ``swizzled=True`` emits the
|
| 709 |
+
scale directly in SWIZZLE_32_4_4 for the tcgen05 fast path."""
|
| 710 |
+
return _launch_act_quant(x, "mxfp4", MX_SCALE_GROUP_K, torch.uint8, swizzled)
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
def nvfp4_act_quant(
|
| 714 |
+
x: torch.Tensor,
|
| 715 |
+
swizzled: bool = False,
|
| 716 |
+
global_scale: torch.Tensor | None = None,
|
| 717 |
+
expert_index: torch.Tensor | None = None,
|
| 718 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 719 |
+
"""Quantize ``(T, K)`` activations to NVFP4 in one kernel pass: packed-E2M1 values
|
| 720 |
+
(``(T, K//2)`` int8) + E4M3 group-16 block scales (``(T, K//16)`` — ``amax/6`` rounded to
|
| 721 |
+
E4M3, NOT a power of two; values divide by the DECODED scale before the E2M1 grid,
|
| 722 |
+
the standard two-step). ``global_scale`` is the CALIBRATED per-tensor second level
|
| 723 |
+
(fp32, the checkpoint's ``input_scale``): values are normalized by it before
|
| 724 |
+
the block quant, so block scales stay in e4m3 range regardless of the activation's
|
| 725 |
+
dynamic range — the canonical two-level format. The GEMM folds ``g_a·g_b`` back onto
|
| 726 |
+
the accumulator (pass ``As=scales`` with ``a_global_scale=global_scale``). Per-expert globals
|
| 727 |
+
need each row's expert, which ``expert_index`` gives where the caller already holds that map
|
| 728 |
+
(the batched op's ``expert_ids``, sentinels and all) — it is read only when the globals are
|
| 729 |
+
per expert, and ``mx_act_quant_grouped`` needs none at all, its tiles carrying an expert.
|
| 730 |
+
``None`` = single-level (``g_a = 1``). ``swizzled=True`` emits the scale directly in
|
| 731 |
+
SWIZZLE_32_4_4 for the tcgen05 fast path."""
|
| 732 |
+
return _launch_act_quant(
|
| 733 |
+
x, "nvfp4", NVFP4_SCALE_GROUP_K, torch.float8_e4m3fn, swizzled, global_scale, expert_index
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
|
| 737 |
+
def nvfp4_quantize_two_level(
|
| 738 |
+
weight: torch.Tensor, swizzled: bool = False
|
| 739 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 740 |
+
"""Canonical two-level NVFP4 quant of a ``(N, K)`` (or ``(K,)``-last) tensor. Returns
|
| 741 |
+
``(packed_e2m1 int8, e4m3 group-16 block scales, fp32 per-tensor global)`` — the block scale
|
| 742 |
+
is the op's ``Bs`` and the global its ``b_global_scale`` (the decoupled API keeps the two levels
|
| 743 |
+
as separate arguments).
|
| 744 |
+
|
| 745 |
+
The second level is a per-tensor fp32 global = ``amax / (6 · 448)`` — the smallest global that
|
| 746 |
+
keeps every e4m3 block scale in range (block ``amax/6`` after dividing by the global stays
|
| 747 |
+
``≤ 448``). Two-level quant IS single-level quant of the globally-normalized tensor, so the block
|
| 748 |
+
values + scales come straight from ``nvfp4_act_quant(weight / global)``. The kernels multiply
|
| 749 |
+
the folded ``g_a · g_b`` onto the accumulator; the e4m3 block scales ride ``dot_scaled`` as
|
| 750 |
+
usual (activations are single-level ⇒ ``g_a = 1``)."""
|
| 751 |
+
global_scale = (weight.abs().amax() / (6.0 * 448.0)).clamp(min=1e-30).float()
|
| 752 |
+
# normalize in fp32: bf16 / 0-dim-fp32 stays bf16 under type promotion, and the extra
|
| 753 |
+
# rounding before the block quant flips E2M1 codes vs the exact normalized tensor
|
| 754 |
+
packed, block = nvfp4_act_quant((weight.float() / global_scale).contiguous(), swizzled)
|
| 755 |
+
return packed.view(torch.int8), block, global_scale.reshape(1)
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
# offline act-quant pass per activation format (keys = ``resolve_activation_format`` results)
|
| 759 |
+
MX_ACT_QUANT = {
|
| 760 |
+
"mxfp8": mxfp8_act_quant,
|
| 761 |
+
"mxfp4": mxfp4_act_quant,
|
| 762 |
+
"nvfp4": nvfp4_act_quant,
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def _launch_act_quant(
|
| 767 |
+
x, activation_format, scale_group, scale_dtype, swizzled=False, global_scale=None,
|
| 768 |
+
expert_index=None,
|
| 769 |
+
):
|
| 770 |
+
"""One-pass activation quant for every format (``mxfp8`` = E4M3 values, else packed E2M1) and
|
| 771 |
+
both scale layouts. ``swizzled=True`` writes the scale straight into the SWIZZLE_32_4_4 buffer
|
| 772 |
+
``(1, cdiv(T, 128), cb, 2, 256)`` (per-element ptr store, dense autotuned ``BLOCK_T`` — same grid
|
| 773 |
+
as the affine path, just the store address flips); ``swizzled=False`` writes row-major
|
| 774 |
+
``(T, K // scale_group)``. ``global_scale`` (NVFP4 two-level, ``(1,)`` fp32) normalizes the
|
| 775 |
+
values before the block quant. Returns ``(values, scales)``."""
|
| 776 |
+
T, K = x.shape
|
| 777 |
+
packed = activation_format != "mxfp8"
|
| 778 |
+
per_expert = is_per_expert_global(global_scale)
|
| 779 |
+
if packed:
|
| 780 |
+
assert K % (2 * scale_group) == 0, (
|
| 781 |
+
f"K (={K}) must be a multiple of {2 * scale_group} to pack E2M1 pairs"
|
| 782 |
+
)
|
| 783 |
+
values = torch.empty(
|
| 784 |
+
T, K // 2 if packed else K, device=x.device,
|
| 785 |
+
dtype=torch.uint8 if packed else FP8_DTYPE,
|
| 786 |
+
)
|
| 787 |
+
if swizzled:
|
| 788 |
+
cb = triton.cdiv(K // scale_group, 4)
|
| 789 |
+
scales = torch.empty(
|
| 790 |
+
1, triton.cdiv(T, 128), cb, 2, 256, device=x.device, dtype=scale_dtype
|
| 791 |
+
)
|
| 792 |
+
else:
|
| 793 |
+
scales = torch.empty(T, K // scale_group, device=x.device, dtype=scale_dtype)
|
| 794 |
+
with device_context(x.device):
|
| 795 |
+
compile_time_only_triton_wrap(_mx_act_quant_kernel)[
|
| 796 |
+
lambda META: (triton.cdiv(T, META["BLOCK_T"]), K // META["BLOCK_K"])
|
| 797 |
+
](
|
| 798 |
+
x,
|
| 799 |
+
values,
|
| 800 |
+
None if swizzled else scales, # S: row-major scales, plain arm only
|
| 801 |
+
scales if swizzled else None, # SOut: SWIZZLE_32_4_4 buffer, swizzled arm only
|
| 802 |
+
None, # GatherIdx: the dense grid reads rows directly
|
| 803 |
+
None, # ExpertStart: no expert-sorted tiles on the dense grid
|
| 804 |
+
global_scale, # fp32 NVFP4 two-level global; None ⇒ single-level (arm folds out)
|
| 805 |
+
expert_index if per_expert else None, # row -> expert map, read iff the globals are per expert
|
| 806 |
+
global_scale_stride(global_scale),
|
| 807 |
+
(global_scale.numel() - 1) if per_expert else 0,
|
| 808 |
+
x.stride(0),
|
| 809 |
+
x.stride(1),
|
| 810 |
+
T,
|
| 811 |
+
T.bit_length(),
|
| 812 |
+
K=K,
|
| 813 |
+
SCALE_GROUP_K=scale_group,
|
| 814 |
+
FORMAT=activation_format,
|
| 815 |
+
SWIZZLED=swizzled,
|
| 816 |
+
GROUPED=False,
|
| 817 |
+
# explicit even though the dense grid never reads it: inductor's wrap_triton path
|
| 818 |
+
# drops constexprs left to their default, and the arity mismatch that produces kills
|
| 819 |
+
# the whole torch.compile launch ("launcher() missing 1 required positional argument")
|
| 820 |
+
NUM_EXPERTS_POW2=1,
|
| 821 |
+
PDL=decode_pdl(),
|
| 822 |
+
launch_pdl=decode_pdl(),
|
| 823 |
+
)
|
| 824 |
+
return (values.view(torch.int8) if packed else values), scales
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
@bayesian_autotune(
|
| 828 |
+
[
|
| 829 |
+
triton.Config({"BLOCK_T": bt}, num_warps=w)
|
| 830 |
+
for bt in (16, 32, 64, 128)
|
| 831 |
+
for w in (1, 2, 4, 8)
|
| 832 |
+
],
|
| 833 |
+
["K", "BLOCK_K", "t_bucket"],
|
| 834 |
+
n_trials=100,
|
| 835 |
+
)
|
| 836 |
+
@triton.jit
|
| 837 |
+
def _fp8_act_quant_block_dynamic_kernel(
|
| 838 |
+
X,
|
| 839 |
+
Y,
|
| 840 |
+
S,
|
| 841 |
+
stride_x_t,
|
| 842 |
+
stride_x_k,
|
| 843 |
+
T,
|
| 844 |
+
t_bucket, # autotune key only (log2 token-count bucket); unused in body
|
| 845 |
+
K: tl.constexpr,
|
| 846 |
+
BLOCK_K: tl.constexpr,
|
| 847 |
+
BLOCK_T: tl.constexpr,
|
| 848 |
+
UE8M0: tl.constexpr = False,
|
| 849 |
+
PDL: tl.constexpr = False,
|
| 850 |
+
):
|
| 851 |
+
"""One-pass block-FP8 activation quant: rows → E4M3 + one ``amax/448`` scale per
|
| 852 |
+
``BLOCK_K`` span (fp32, or a UE8M0 exponent byte under ``UE8M0``). Grid
|
| 853 |
+
``(cdiv(T, BLOCK_T), K // BLOCK_K)`` — each program quantizes a ``[BLOCK_T, BLOCK_K]``
|
| 854 |
+
tile (``BLOCK_T`` rows over one K-block, each row its own scale). The one-row-per-program
|
| 855 |
+
form starved memory (128-byte transactions across ~T*K/BK tiny programs); the row tile
|
| 856 |
+
gives the loads something to coalesce. The span equals the consumer's ``BLOCK_SIZE_K``,
|
| 857 |
+
so results are bit-exact with the inline quant. Arbitrary input strides (no host-side
|
| 858 |
+
copy); ``BLOCK_K`` is fixed by the scale layout, ``BLOCK_T`` and warps are tuned."""
|
| 859 |
+
if PDL:
|
| 860 |
+
gdc_wait()
|
| 861 |
+
pid_t = tl.program_id(0)
|
| 862 |
+
kb = tl.program_id(1)
|
| 863 |
+
rows = (pid_t * BLOCK_T + tl.arange(0, BLOCK_T)).to(tl.int64)
|
| 864 |
+
offs = kb * BLOCK_K + tl.arange(0, BLOCK_K)
|
| 865 |
+
row_mask = rows < T
|
| 866 |
+
x = tl.load(
|
| 867 |
+
X + rows[:, None] * stride_x_t + offs[None, :] * stride_x_k,
|
| 868 |
+
mask=row_mask[:, None],
|
| 869 |
+
other=0.0,
|
| 870 |
+
).to(tl.float32)
|
| 871 |
+
y, s = fp8_act_quant_inline(x, UE8M0=UE8M0)
|
| 872 |
+
tl.store(Y + rows[:, None] * K + offs[None, :], y, mask=row_mask[:, None])
|
| 873 |
+
if PDL:
|
| 874 |
+
gdc_launch_dependents()
|
| 875 |
+
tl.store(S + rows * (K // BLOCK_K) + kb, s, mask=row_mask)
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def fp8_act_quant_block_dynamic(
|
| 879 |
+
x: torch.Tensor, block_k: int, use_ue8m0: bool = False
|
| 880 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 881 |
+
"""Quantize ``(T, K)`` activations to block-FP8 once (E4M3 + per-``block_k`` scales)
|
| 882 |
+
instead of inline per weight-tile — same rationale and layout as ``mxfp8_act_quant`` (a
|
| 883 |
+
GEMM re-reads its activation once per N-tile). Bit-exact with the inline form. Scales are
|
| 884 |
+
fp32 (``amax/448``) or, under ``use_ue8m0``, UE8M0 exponent bytes (power-of-two scales)
|
| 885 |
+
for the tcgen05 ``dot_scaled`` path — the DeepGEMM-Blackwell format."""
|
| 886 |
+
T, K = x.shape
|
| 887 |
+
y = torch.empty(T, K, device=x.device, dtype=FP8_DTYPE)
|
| 888 |
+
s_dtype = torch.uint8 if use_ue8m0 else torch.float32
|
| 889 |
+
s = torch.empty(T, K // block_k, device=x.device, dtype=s_dtype)
|
| 890 |
+
|
| 891 |
+
def grid(META):
|
| 892 |
+
return (triton.cdiv(T, META["BLOCK_T"]), K // block_k)
|
| 893 |
+
|
| 894 |
+
with device_context(x.device):
|
| 895 |
+
compile_time_only_triton_wrap(_fp8_act_quant_block_dynamic_kernel)[grid](
|
| 896 |
+
x, y, s, x.stride(0), x.stride(1), T, T.bit_length(),
|
| 897 |
+
K=K, BLOCK_K=block_k, UE8M0=use_ue8m0,
|
| 898 |
+
PDL=decode_pdl(),
|
| 899 |
+
launch_pdl=decode_pdl(),
|
| 900 |
+
)
|
| 901 |
+
return y, s
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
# ── fp8_act_quant_tensor_wide kernel (used by tensor-mode FP8 wrappers) ───────────────────
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
@triton.jit
|
| 908 |
+
def _fp8_act_quant_kernel(
|
| 909 |
+
x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr, PADDED_BLOCK: tl.constexpr,
|
| 910 |
+
PDL: tl.constexpr = False,
|
| 911 |
+
):
|
| 912 |
+
if PDL:
|
| 913 |
+
gdc_wait()
|
| 914 |
+
# ``tl.arange`` needs a power-of-2 length, so iterate over PADDED_BLOCK (the next
|
| 915 |
+
# power of 2) and mask the tail — lets block_size be non-power-of-2 (e.g. a full
|
| 916 |
+
# row K=14336 in tensor-mode). Masked lanes load 0, which can't affect ``amax``.
|
| 917 |
+
pid = tl.program_id(axis=0)
|
| 918 |
+
cols = tl.arange(0, PADDED_BLOCK)
|
| 919 |
+
mask = cols < BLOCK_SIZE
|
| 920 |
+
offs = pid * BLOCK_SIZE + cols
|
| 921 |
+
x = tl.load(x_ptr + offs, mask=mask, other=0.0).to(tl.float32)
|
| 922 |
+
s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
|
| 923 |
+
y = (x / tl.maximum(s, 1e-12)).to(y_ptr.dtype.element_ty)
|
| 924 |
+
tl.store(y_ptr + offs, y, mask=mask)
|
| 925 |
+
if PDL:
|
| 926 |
+
gdc_launch_dependents()
|
| 927 |
+
tl.store(s_ptr + pid, s)
|
| 928 |
+
|
| 929 |
+
|
| 930 |
+
@compile_time_only_triton_op(
|
| 931 |
+
add_op_namespace_prefix("fp8_act_quant_tensor_wide"), mutates_args=(), opaque=True
|
| 932 |
+
)
|
| 933 |
+
def fp8_act_quant_tensor_wide(
|
| 934 |
+
x: torch.Tensor, block_size: int = 128
|
| 935 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 936 |
+
assert x.is_contiguous()
|
| 937 |
+
assert x.shape[-1] % block_size == 0
|
| 938 |
+
y = torch.empty_like(x, dtype=FP8_DTYPE)
|
| 939 |
+
grid = (triton.cdiv(x.numel(), block_size),)
|
| 940 |
+
s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
|
| 941 |
+
|
| 942 |
+
with device_context(x.device):
|
| 943 |
+
compile_time_only_triton_wrap(_fp8_act_quant_kernel)[grid](
|
| 944 |
+
x,
|
| 945 |
+
y,
|
| 946 |
+
s,
|
| 947 |
+
BLOCK_SIZE=block_size,
|
| 948 |
+
PADDED_BLOCK=triton.next_power_of_2(block_size),
|
| 949 |
+
PDL=decode_pdl(),
|
| 950 |
+
launch_pdl=decode_pdl(),
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
return y, s
|
| 954 |
+
|
| 955 |
+
|
build/torch-xpu/scheduling.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import triton
|
| 19 |
+
import triton.language as tl
|
| 20 |
+
|
| 21 |
+
from .compat import add_op_namespace_prefix, compile_time_only_triton_op, compile_time_only_triton_wrap, device_context, is_sm10x
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@triton.jit
|
| 25 |
+
def build_tile_layout(
|
| 26 |
+
ExpertStart, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE_M: tl.constexpr
|
| 27 |
+
):
|
| 28 |
+
"""Load ``expert_start`` once and derive the per-BM tile layout vectors (kept in
|
| 29 |
+
registers for the whole persistent loop): per-expert first sorted row, token count,
|
| 30 |
+
exclusive tile-start cumsum, and the total M-tile count. ``ExpertStart`` is
|
| 31 |
+
``(NUM_EXPERTS + 1,)`` with a trailing ``S`` sentinel (``expert_start[E] == S``)."""
|
| 32 |
+
e_offs = tl.arange(0, NUM_EXPERTS)
|
| 33 |
+
exp_start = tl.load(ExpertStart + e_offs)
|
| 34 |
+
exp_end = tl.load(ExpertStart + e_offs + 1)
|
| 35 |
+
freqs = exp_end - exp_start
|
| 36 |
+
tiles_per_e = (freqs + BLOCK_SIZE_M - 1) // BLOCK_SIZE_M
|
| 37 |
+
tile_start_excl = (
|
| 38 |
+
tl.cumsum(tiles_per_e, 0) - tiles_per_e
|
| 39 |
+
) # first tile index of expert e
|
| 40 |
+
total_m_tiles = tl.sum(tiles_per_e, 0)
|
| 41 |
+
return exp_start, freqs, tile_start_excl, total_m_tiles, e_offs
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@triton.jit
|
| 45 |
+
def resolve_tile_inline(
|
| 46 |
+
pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M: tl.constexpr
|
| 47 |
+
):
|
| 48 |
+
"""Map an M-tile id to its owning expert + the tile's sorted row range, from the
|
| 49 |
+
register-resident layout (no global loads). Returns ``(expert_id, sorted_indices,
|
| 50 |
+
row_mask)``."""
|
| 51 |
+
# Bucketize via the exclusive tile cumsum: #experts whose tile-start <= pid_m, minus 1.
|
| 52 |
+
expert_id = tl.sum((tile_start_excl <= pid_m).to(tl.int32), 0) - 1
|
| 53 |
+
sel = (
|
| 54 |
+
e_offs == expert_id
|
| 55 |
+
) # scalar-index the E-vectors via mask-sum (no dynamic index)
|
| 56 |
+
e_start = tl.sum(tl.where(sel, exp_start, 0), 0)
|
| 57 |
+
e_tile_start = tl.sum(tl.where(sel, tile_start_excl, 0), 0)
|
| 58 |
+
freq = tl.sum(tl.where(sel, freqs, 0), 0)
|
| 59 |
+
within = pid_m - e_tile_start
|
| 60 |
+
m_start = e_start + within * BLOCK_SIZE_M
|
| 61 |
+
offs = tl.arange(0, BLOCK_SIZE_M)
|
| 62 |
+
row_mask = offs < freq - within * BLOCK_SIZE_M
|
| 63 |
+
sorted_indices = tl.max_contiguous(m_start + offs, BLOCK_SIZE_M)
|
| 64 |
+
return expert_id, sorted_indices, row_mask
|
| 65 |
+
|
| 66 |
+
# Flat-slot tile per program for the O(S) routing kernels (count + scatter). These are small
|
| 67 |
+
# latency-bound atomic kernels that want many programs: a sweep over {256..4096} x prefill shapes
|
| 68 |
+
# put 256 best (or within ~1%) for both, with 1024 up to ~1.5x slower. The grid derives from it
|
| 69 |
+
# so the two can't drift. Power of 2.
|
| 70 |
+
_ROUTING_BLOCK_SIZE = 256
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@triton.jit
|
| 74 |
+
def _exclusive_offsets_kernel(
|
| 75 |
+
ExpertFreq, ExpertStart, Counters, NUM_EXPERTS: tl.constexpr
|
| 76 |
+
):
|
| 77 |
+
"""Exclusive cumsum of per-expert token counts → ``expert_start`` (leading 0, trailing
|
| 78 |
+
S), and zero the scatter counters — one launch."""
|
| 79 |
+
offs = tl.arange(0, NUM_EXPERTS)
|
| 80 |
+
incl = tl.cumsum(tl.load(ExpertFreq + offs), 0)
|
| 81 |
+
tl.store(ExpertStart, 0)
|
| 82 |
+
tl.store(ExpertStart + 1 + offs, incl)
|
| 83 |
+
tl.store(Counters + offs, tl.zeros([NUM_EXPERTS], tl.int32))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@triton.jit
|
| 87 |
+
def _scatter_kernel(
|
| 88 |
+
ExpertIds,
|
| 89 |
+
Perm,
|
| 90 |
+
PermToken,
|
| 91 |
+
ExpertStart,
|
| 92 |
+
Counters,
|
| 93 |
+
S,
|
| 94 |
+
NUM_TOP_K: tl.constexpr,
|
| 95 |
+
NUM_EXPERTS: tl.constexpr,
|
| 96 |
+
BLOCK_SIZE: tl.constexpr,
|
| 97 |
+
):
|
| 98 |
+
"""Counting-sort scatter: each flat slot atomically claims the next slot of its expert
|
| 99 |
+
(``expert_start[e] + counter[e]++``). O(S), replaces an O(S·logS) argsort. Within-expert
|
| 100 |
+
order is arbitrary (atomic race) — fine, the per-token reduce is order-invariant. Slots whose
|
| 101 |
+
expert is non-local (EP sentinel id ``>= NUM_EXPERTS``) are skipped — matches ``_count_kernel``,
|
| 102 |
+
and avoids the atomic/store landing at an out-of-range (invalid) global address."""
|
| 103 |
+
offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
| 104 |
+
expert_id = tl.load(ExpertIds + offs, mask=offs < S, other=NUM_EXPERTS)
|
| 105 |
+
valid = expert_id < NUM_EXPERTS
|
| 106 |
+
dest = tl.load(ExpertStart + expert_id, mask=valid, other=0) + tl.atomic_add(
|
| 107 |
+
Counters + expert_id, 1, mask=valid, sem="relaxed"
|
| 108 |
+
)
|
| 109 |
+
tl.store(Perm + dest, offs, mask=valid)
|
| 110 |
+
tl.store(PermToken + dest, offs // NUM_TOP_K, mask=valid)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@triton.jit
|
| 114 |
+
def _count_kernel(
|
| 115 |
+
ExpertIds, ExpertFreq, S, NUM_EXPERTS: tl.constexpr, BLOCK_SIZE: tl.constexpr
|
| 116 |
+
):
|
| 117 |
+
"""Per-expert token count via atomics — replaces ``torch.histc`` (no float cast), fixed
|
| 118 |
+
``(NUM_EXPERTS,)`` output stays CUDA-graph friendly. ``ExpertFreq`` is pre-zeroed."""
|
| 119 |
+
offs = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
| 120 |
+
mask = offs < S
|
| 121 |
+
expert_id = tl.load(ExpertIds + offs, mask=mask, other=NUM_EXPERTS)
|
| 122 |
+
tl.atomic_add(
|
| 123 |
+
ExpertFreq + expert_id, 1, mask=mask & (expert_id < NUM_EXPERTS), sem="relaxed"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def compute_grouped_scheduling(
|
| 128 |
+
expert_ids: torch.Tensor, num_experts: int, num_top_k: int
|
| 129 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 130 |
+
"""On-device routing: expert-sorted index (no copy of the activations) via two Triton
|
| 131 |
+
launches — exclusive offsets + an atomic counting-sort scatter (replaces host ``argsort``).
|
| 132 |
+
Run it once per layer and pass the results to every grouped GEMM of that layer. Returns
|
| 133 |
+
``(expert_start, gather_idx, scatter_idx)``:
|
| 134 |
+
|
| 135 |
+
- ``expert_start`` — ``(E+1,)`` cumulative sorted-row starts padded with S; the tiling
|
| 136 |
+
schedule the kernels build their register-resident tile layout from.
|
| 137 |
+
- ``gather_idx`` — each sorted position's source row of hidden (``perm // num_top_k``,
|
| 138 |
+
many-to-one for top_k > 1: the gather that reads hidden without replication). Pass as the
|
| 139 |
+
GEMM's input map (``None`` = ``A`` already expert-sorted, e.g. the down projection).
|
| 140 |
+
- ``scatter_idx`` — each sorted position's token-major routed destination row ``(t*K + j)``,
|
| 141 |
+
the ``perm = torch.sort(expert_ids)`` indices (kernels un-permute by SCATTERING at store
|
| 142 |
+
time, never materializing ``inv_perm``). Pass as the output map (``None`` = leave the output
|
| 143 |
+
expert-sorted, e.g. the gate_up projection's intermediate).
|
| 144 |
+
|
| 145 |
+
E must be a power of 2 (the scheduling kernels hold the per-expert vectors in one
|
| 146 |
+
``tl.arange`` block)."""
|
| 147 |
+
# the scheduling kernels hold the (E,) frequency/offset vectors in one tl.arange
|
| 148 |
+
# block, which requires a power of 2 — fail here with a clear message instead of a
|
| 149 |
+
# Triton compile error from an internal kernel
|
| 150 |
+
assert num_experts & (num_experts - 1) == 0, (
|
| 151 |
+
f"num_experts ({num_experts}) must be a power of 2"
|
| 152 |
+
)
|
| 153 |
+
gather_idx, scatter_idx, expert_start = _compute_grouped_scheduling(
|
| 154 |
+
expert_ids, num_experts, num_top_k
|
| 155 |
+
)
|
| 156 |
+
return expert_start, gather_idx, scatter_idx
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
@compile_time_only_triton_op(
|
| 160 |
+
add_op_namespace_prefix("compute_grouped_scheduling"), mutates_args=(), opaque=True
|
| 161 |
+
)
|
| 162 |
+
def _compute_grouped_scheduling(
|
| 163 |
+
expert_ids: torch.Tensor, num_experts: int, num_top_k: int
|
| 164 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 165 |
+
device = expert_ids.device
|
| 166 |
+
expert_ids = expert_ids.int().contiguous() # routing kernels index with unit stride
|
| 167 |
+
num_routed_tokens = expert_ids.numel() # S = num_tokens * num_top_k
|
| 168 |
+
expert_freq = torch.zeros(num_experts, dtype=torch.int32, device=device)
|
| 169 |
+
expert_start = torch.empty(num_experts + 1, dtype=torch.int32, device=device)
|
| 170 |
+
counters = torch.empty(num_experts, dtype=torch.int32, device=device)
|
| 171 |
+
perm = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
|
| 172 |
+
perm_token = torch.empty(num_routed_tokens, dtype=torch.int32, device=device)
|
| 173 |
+
with device_context(device):
|
| 174 |
+
compile_time_only_triton_wrap(_count_kernel)[
|
| 175 |
+
(triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)
|
| 176 |
+
](
|
| 177 |
+
expert_ids,
|
| 178 |
+
expert_freq,
|
| 179 |
+
num_routed_tokens,
|
| 180 |
+
NUM_EXPERTS=num_experts,
|
| 181 |
+
BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
|
| 182 |
+
)
|
| 183 |
+
compile_time_only_triton_wrap(_exclusive_offsets_kernel)[(1,)](
|
| 184 |
+
expert_freq,
|
| 185 |
+
expert_start,
|
| 186 |
+
counters,
|
| 187 |
+
NUM_EXPERTS=num_experts,
|
| 188 |
+
)
|
| 189 |
+
compile_time_only_triton_wrap(_scatter_kernel)[
|
| 190 |
+
(triton.cdiv(num_routed_tokens, _ROUTING_BLOCK_SIZE),)
|
| 191 |
+
](
|
| 192 |
+
expert_ids,
|
| 193 |
+
perm,
|
| 194 |
+
perm_token,
|
| 195 |
+
expert_start,
|
| 196 |
+
counters,
|
| 197 |
+
num_routed_tokens,
|
| 198 |
+
NUM_TOP_K=num_top_k,
|
| 199 |
+
NUM_EXPERTS=num_experts,
|
| 200 |
+
BLOCK_SIZE=_ROUTING_BLOCK_SIZE,
|
| 201 |
+
)
|
| 202 |
+
return perm_token, perm, expert_start
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# The packed schedule exists for ONE tile size: BM=128, the only region the resolve trade
|
| 206 |
+
# wins (packed_schedule_scope_pruner) — so the table is a single section: header slot 0 =
|
| 207 |
+
# total_m_tiles, then (expert_id, m_start, row_end) per tile.
|
| 208 |
+
PACKED_SCHEDULE_BM = 128
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def packed_schedule_capacity(num_routed_tokens: int, num_experts: int) -> int:
|
| 212 |
+
"""Worst-case M-tile count: every expert pads its own last tile, so
|
| 213 |
+
``cdiv(S, BM) + E`` bounds ``sum_e(cdiv(freq_e, BM))``. Fixed shape — graph-capturable."""
|
| 214 |
+
return (num_routed_tokens + PACKED_SCHEDULE_BM - 1) // PACKED_SCHEDULE_BM + num_experts
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def build_packed_schedule(
|
| 218 |
+
expert_start: torch.Tensor, num_routed_tokens: int, num_experts: int
|
| 219 |
+
) -> torch.Tensor:
|
| 220 |
+
"""Allocate + fill the packed tile schedule (one launch, fixed shape — graph-safe)."""
|
| 221 |
+
schedule = torch.empty(
|
| 222 |
+
1 + 3 * packed_schedule_capacity(num_routed_tokens, num_experts),
|
| 223 |
+
dtype=torch.int32, device=expert_start.device,
|
| 224 |
+
)
|
| 225 |
+
pow2 = triton.next_power_of_2(num_experts)
|
| 226 |
+
with device_context(expert_start.device):
|
| 227 |
+
compile_time_only_triton_wrap(_packed_schedule_kernel)[(pow2,)](
|
| 228 |
+
expert_start, schedule, NUM_EXPERTS=pow2
|
| 229 |
+
)
|
| 230 |
+
return schedule
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@triton.jit
|
| 234 |
+
def _packed_schedule_kernel(ExpertStart, Schedule, NUM_EXPERTS: tl.constexpr):
|
| 235 |
+
"""Build the packed tile schedule in one launch: one program per expert writes its
|
| 236 |
+
tiles' ``(expert_id, m_start, row_end)`` triples (header slot 0 = total tile count,
|
| 237 |
+
written by expert 0 from the full cumsum). Replaces the kernels' O(E)
|
| 238 |
+
register-resident tile layout with O(1) loads — the packed-schedule tuner axis
|
| 239 |
+
(register relief; wins at the pressure-walled BM=128 configs)."""
|
| 240 |
+
BM: tl.constexpr = 128 # PACKED_SCHEDULE_BM
|
| 241 |
+
e = tl.program_id(0)
|
| 242 |
+
e_start = tl.load(ExpertStart + e)
|
| 243 |
+
e_end = tl.load(ExpertStart + e + 1)
|
| 244 |
+
offs = tl.arange(0, NUM_EXPERTS)
|
| 245 |
+
starts = tl.load(ExpertStart + offs)
|
| 246 |
+
ends = tl.load(ExpertStart + offs + 1)
|
| 247 |
+
tiles_per = (ends - starts + BM - 1) // BM
|
| 248 |
+
before = tl.sum(tl.where(offs < e, tiles_per, 0), 0)
|
| 249 |
+
if e == 0:
|
| 250 |
+
tl.store(Schedule, tl.sum(tiles_per, 0)) # header: total_m_tiles
|
| 251 |
+
n_tiles = (e_end - e_start + BM - 1) // BM
|
| 252 |
+
for t in range(n_tiles):
|
| 253 |
+
base = 1 + (before + t) * 3
|
| 254 |
+
tl.store(Schedule + base, e)
|
| 255 |
+
tl.store(Schedule + base + 1, e_start + t * BM)
|
| 256 |
+
tl.store(Schedule + base + 2, e_end)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
@triton.jit
|
| 260 |
+
def load_packed_schedule(Schedule, BLOCK_SIZE_M: tl.constexpr):
|
| 261 |
+
"""The packed counterpart of ``build_tile_layout``: read the header (total_m_tiles).
|
| 262 |
+
The schedule is built for BM=128 only — the sole region the packed resolve wins (see
|
| 263 |
+
``packed_schedule_scope_pruner``); the static_assert is the self-fence."""
|
| 264 |
+
tl.static_assert(BLOCK_SIZE_M == 128, "the packed schedule is built for BM=128")
|
| 265 |
+
return tl.load(Schedule)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@triton.jit
|
| 269 |
+
def prefetch_packed_entry(Schedule, pid_m, total_m_tiles):
|
| 270 |
+
"""Issue a future M-tile's ``(expert_id, m_start, row_end)`` loads (masked past the
|
| 271 |
+
schedule end). Consumed by ``resolve_grouped_tile_packed`` one persistent iteration
|
| 272 |
+
later, so the entry's dependent-load latency hides under the current tile's K-loop
|
| 273 |
+
instead of serializing into every tile (~300ns/tile, the packed arm's whole overhead)."""
|
| 274 |
+
m = pid_m < total_m_tiles
|
| 275 |
+
entry = Schedule + 1 + pid_m * 3
|
| 276 |
+
return (tl.load(entry, mask=m, other=0),
|
| 277 |
+
tl.load(entry + 1, mask=m, other=0),
|
| 278 |
+
tl.load(entry + 2, mask=m, other=0))
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@triton.jit
|
| 282 |
+
def resolve_grouped_tile_packed(
|
| 283 |
+
tile_id,
|
| 284 |
+
num_n_tiles,
|
| 285 |
+
prefetched,
|
| 286 |
+
GatherIdx,
|
| 287 |
+
ScatterIdx,
|
| 288 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 289 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 290 |
+
GATE: tl.constexpr = False,
|
| 291 |
+
):
|
| 292 |
+
"""``resolve_grouped_tile`` with the M-tile resolved from the packed schedule —
|
| 293 |
+
consuming the triple ``prefetch_packed_entry`` issued last iteration (zero dependent
|
| 294 |
+
loads on this tile's critical path).
|
| 295 |
+
Same return contract; bit-identical tiles."""
|
| 296 |
+
pid_n = tile_id % num_n_tiles
|
| 297 |
+
expert_id, tile_row0, row_end = prefetched
|
| 298 |
+
offs_global_m = tl.max_contiguous(tile_row0 + tl.arange(0, BLOCK_SIZE_M), BLOCK_SIZE_M)
|
| 299 |
+
row_mask = offs_global_m < row_end
|
| 300 |
+
if GatherIdx is not None:
|
| 301 |
+
in_row = tl.load(GatherIdx + offs_global_m, mask=row_mask, other=0)
|
| 302 |
+
else:
|
| 303 |
+
in_row = offs_global_m
|
| 304 |
+
if ScatterIdx is not None:
|
| 305 |
+
out_row = tl.load(ScatterIdx + offs_global_m, mask=row_mask, other=0)
|
| 306 |
+
else:
|
| 307 |
+
out_row = offs_global_m
|
| 308 |
+
expert_id64 = expert_id.to(tl.int64)
|
| 309 |
+
WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 310 |
+
offs_bn = pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)
|
| 311 |
+
row0 = expert_id64.to(tl.int32)
|
| 312 |
+
n_off = pid_n * WEIGHT_ROWS
|
| 313 |
+
m_start = tl.min(in_row).to(tl.int32)
|
| 314 |
+
return pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off, m_start
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@triton.jit
|
| 318 |
+
def resolve_grouped_tile(
|
| 319 |
+
tile_id,
|
| 320 |
+
num_n_tiles,
|
| 321 |
+
exp_start,
|
| 322 |
+
freqs,
|
| 323 |
+
tile_start_excl,
|
| 324 |
+
e_offs,
|
| 325 |
+
GatherIdx,
|
| 326 |
+
ScatterIdx,
|
| 327 |
+
BLOCK_SIZE_N: tl.constexpr,
|
| 328 |
+
BLOCK_SIZE_M: tl.constexpr,
|
| 329 |
+
GATE: tl.constexpr = False,
|
| 330 |
+
):
|
| 331 |
+
"""One persistent grouped tile: split the flat ``tile_id`` into (M-tile, N-tile), map
|
| 332 |
+
the M-tile to its expert + rows via ``resolve_tile_inline`` (on the register-resident
|
| 333 |
+
layout ``build_tile_layout`` builds once per program, passed in), and apply the virtual
|
| 334 |
+
sort — rows load from ``in_row`` and store to ``out_row``, mapped by ``GatherIdx`` /
|
| 335 |
+
``ScatterIdx`` when present (``None`` = expert-sorted, the position itself; the ``None``
|
| 336 |
+
check folds at trace time, so no separate has-gather/has-scatter flag is needed).
|
| 337 |
+
|
| 338 |
+
Returns ``(pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off,
|
| 339 |
+
m_start)`` — both expert-id widths (``expert_id`` int32 for TMA descriptor row indices,
|
| 340 |
+
``expert_id64`` int64 for byte-offset pointer arithmetic that overflows int32 at full expert
|
| 341 |
+
counts), plus the tile's derived load coordinates every grouped K-loop needs: ``row0`` (the
|
| 342 |
+
weight descriptor's expert-slab row base, ×2 under GATE for the gate|up stack), ``n_off`` (the
|
| 343 |
+
N-tile byte offset), and ``m_start`` (the contiguous sorted-row box start — ``min(in_row)``,
|
| 344 |
+
exact because a no-gather tile's rows are contiguous). Shared by the base grouped GEMMs and the
|
| 345 |
+
fused kernels; callers ``_``-ignore whichever fields they don't use."""
|
| 346 |
+
pid_m = tile_id // num_n_tiles
|
| 347 |
+
pid_n = tile_id % num_n_tiles
|
| 348 |
+
expert_id, offs_global_m, row_mask = resolve_tile_inline(
|
| 349 |
+
pid_m, exp_start, freqs, tile_start_excl, e_offs, BLOCK_SIZE_M
|
| 350 |
+
)
|
| 351 |
+
if GatherIdx is not None:
|
| 352 |
+
in_row = tl.load(GatherIdx + offs_global_m, mask=row_mask, other=0)
|
| 353 |
+
else:
|
| 354 |
+
in_row = offs_global_m
|
| 355 |
+
if ScatterIdx is not None:
|
| 356 |
+
out_row = tl.load(ScatterIdx + offs_global_m, mask=row_mask, other=0)
|
| 357 |
+
else:
|
| 358 |
+
out_row = offs_global_m
|
| 359 |
+
expert_id64 = expert_id.to(tl.int64)
|
| 360 |
+
WEIGHT_ROWS: tl.constexpr = 2 * BLOCK_SIZE_N if GATE else BLOCK_SIZE_N
|
| 361 |
+
offs_bn = pid_n * WEIGHT_ROWS + tl.arange(0, WEIGHT_ROWS)
|
| 362 |
+
row0 = expert_id64.to(tl.int32)
|
| 363 |
+
n_off = pid_n * WEIGHT_ROWS
|
| 364 |
+
m_start = tl.min(in_row).to(tl.int32)
|
| 365 |
+
return pid_n, expert_id, expert_id64, in_row, out_row, row_mask, offs_bn, row0, n_off, m_start
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def expand_gather_below_parity(vals, scales, gather_idx, num_experts):
|
| 369 |
+
"""Trade an in-kernel activation gather for one contiguous copy of the (packed) rows +
|
| 370 |
+
scales, when the copy beats what gathering can offer: on sm_10x the gathered descriptor
|
| 371 |
+
arm is tma gather4, MONOTONIC in tokens/expert (2x loss at 32/expert, parity ~1024 — the
|
| 372 |
+
measured law in ``smem_pruner``'s gather clause), so expand below parity; every other
|
| 373 |
+
arch has no gather TMA and forfeits descriptor-A entirely under a gather, so any large-S
|
| 374 |
+
gather expands. Small S (decode) keeps the gather — nothing to amortize, pointer-A wins.
|
| 375 |
+
Call AFTER the activation quant: expanding packed rows costs a fraction of raw bf16 and
|
| 376 |
+
leaves the quant at (num_tokens, K) instead of top_k times that.
|
| 377 |
+
Returns ``(vals, scales, gather_idx)`` with ``gather_idx=None`` when expanded."""
|
| 378 |
+
if gather_idx is None:
|
| 379 |
+
return vals, scales, gather_idx
|
| 380 |
+
if expand_regime(gather_idx.shape[0], num_experts):
|
| 381 |
+
g = gather_idx.to(torch.long)
|
| 382 |
+
vals = vals[g].contiguous()
|
| 383 |
+
if scales is not None and scales.ndim == 2: # row-major per-row scales follow the rows
|
| 384 |
+
scales = scales[g].contiguous()
|
| 385 |
+
gather_idx = None
|
| 386 |
+
return vals, scales, gather_idx
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def expand_regime(S_rows: int, num_experts: int) -> bool:
|
| 390 |
+
"""Where one contiguous copy of the routed rows beats in-kernel gathering: prefill row
|
| 391 |
+
counts (S >= 4096 amortizes the copy) below the sm_10x tma-gather4 parity (~1024
|
| 392 |
+
tokens/expert — gather4's 1-row box is MONOTONIC in tokens/expert; every other arch has
|
| 393 |
+
no gather TMA at all, so any large-S gather expands).
|
| 394 |
+
|
| 395 |
+
Raw bf16 activations (the full-precision grouped path) were A/B'd against keeping the
|
| 396 |
+
in-kernel gather, 200-trial tunes each side, 2026-08-28: DeepSeek-V4 3455 -> 3344us (-3.2%),
|
| 397 |
+
DeepSeek-V3 7267 -> 7290 (wash), GLM-5.2 6039 -> 6439 (+6.6%). The copy (150-370us) is
|
| 398 |
+
offset by what the gather costs the GEMM, and which side wins is shape-specific within the
|
| 399 |
+
tune's roll variance — so the law is NOT split on ``act_itemsize``; it stays as measured.
|
| 400 |
+
``FINEGRAINED_FORCE_GATHER=1`` is the A/B knob (never expand)."""
|
| 401 |
+
if os.environ.get("FINEGRAINED_FORCE_GATHER"):
|
| 402 |
+
return False
|
| 403 |
+
return S_rows >= 4096 and (not is_sm10x() or S_rows < 1024 * num_experts)
|
build/torch-xpu/swizzle.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import triton
|
| 17 |
+
import triton.language as tl
|
| 18 |
+
from triton.tools.tensor_descriptor import TensorDescriptor
|
| 19 |
+
|
| 20 |
+
from .compat import compile_time_only_triton_wrap, device_context
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@triton.jit
|
| 24 |
+
def swizzle_store_block(DST, s, blk, cb, NCB):
|
| 25 |
+
"""Pack one row-major ``(128, 4)`` scale block ``s`` into its SWIZZLE_32_4_4 ``(32, 16)`` block
|
| 26 |
+
and store it at flat offset ``(blk * NCB + cb) * 512`` — the inverse of the un-swizzle in
|
| 27 |
+
``load_swizzled_scale``. Shared by every scale-swizzle kernel below."""
|
| 28 |
+
sw = s.reshape(4, 32, 4).trans(1, 0, 2).reshape(32, 16)
|
| 29 |
+
r = tl.arange(0, 32)
|
| 30 |
+
c = tl.arange(0, 16)
|
| 31 |
+
tl.store(DST + (blk * NCB + cb) * 512 + r[:, None] * 16 + c[None, :], sw)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@triton.jit
|
| 35 |
+
def _swizzle_scales_kernel(
|
| 36 |
+
SRC, # (rows, cols) row-major block scales (uint8 / e8m0 / e4m3 — 1 byte)
|
| 37 |
+
DST, # flat SWIZZLE_32_4_4 output, (n_row_blocks * n_col_blocks * 512,)
|
| 38 |
+
GatherIdx, # (padded_rows,) output-row -> source row (-1 = pad); read only when not None
|
| 39 |
+
ROWS,
|
| 40 |
+
COLS,
|
| 41 |
+
NCB, # number of 4-wide column blocks
|
| 42 |
+
stride_src_m,
|
| 43 |
+
):
|
| 44 |
+
"""One 128x4 SWIZZLE_32_4_4 block per (row-block, col-block) program: gather+pad+swizzle in
|
| 45 |
+
a single launch (replaces the torch view/permute/transpose chain — several kernels + two
|
| 46 |
+
full-tensor copies). Per block: ``(128, 4) -> (4, 32, 4) -> trans(1,0,2) -> (32, 16)`` — the
|
| 47 |
+
exact reorder the torch packer did, so bit-identical."""
|
| 48 |
+
rb = tl.program_id(0)
|
| 49 |
+
cb = tl.program_id(1)
|
| 50 |
+
ri = rb * 128 + tl.arange(0, 128)
|
| 51 |
+
if GatherIdx is not None:
|
| 52 |
+
src = tl.load(GatherIdx + ri, mask=ri < ROWS, other=-1)
|
| 53 |
+
valid = src >= 0
|
| 54 |
+
src = tl.where(valid, src, 0)
|
| 55 |
+
else:
|
| 56 |
+
src = ri
|
| 57 |
+
valid = ri < ROWS
|
| 58 |
+
cj = cb * 4 + tl.arange(0, 4)
|
| 59 |
+
s = tl.load(
|
| 60 |
+
SRC + src[:, None] * stride_src_m + cj[None, :],
|
| 61 |
+
mask=valid[:, None] & (cj[None, :] < COLS),
|
| 62 |
+
other=0,
|
| 63 |
+
)
|
| 64 |
+
swizzle_store_block(DST, s, rb, cb, NCB)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def unswizzle_mx_scales(
|
| 68 |
+
swizzled: torch.Tensor, rows: int, cols: int, *, num_experts: int | None = None
|
| 69 |
+
) -> torch.Tensor:
|
| 70 |
+
"""Inverse of ``swizzle_mx_scales``: a SWIZZLE_32_4_4 artifact back to the plain row-major
|
| 71 |
+
``(rows, cols)`` grid — or ``(num_experts, rows, cols)`` for an expert stack — with the
|
| 72 |
+
(128, 4) padding trimmed. Pure reshape/permute on the byte view (no kernel); it runs at
|
| 73 |
+
save/dequantize time, never on a forward, so the torch-level cost is irrelevant.
|
| 74 |
+
|
| 75 |
+
``rows``/``cols`` are the LOGICAL dims of ONE matrix (for a gate|up slab, the full ``2N``);
|
| 76 |
+
both are trimmed back out of the (128, 4) padding. ``num_experts`` selects the stacked
|
| 77 |
+
``(E, rows, cols)`` result and must match the artifact's leading dim."""
|
| 78 |
+
nrb, ncb = -(-rows // 128), -(-cols // 4)
|
| 79 |
+
n_matrices = num_experts if num_experts is not None else 1
|
| 80 |
+
assert swizzled.shape == (n_matrices, nrb, ncb, 2, 256), (
|
| 81 |
+
f"swizzled artifact is {tuple(swizzled.shape)}, expected {(n_matrices, nrb, ncb, 2, 256)} "
|
| 82 |
+
f"for {n_matrices}x({rows}, {cols})"
|
| 83 |
+
)
|
| 84 |
+
plain = (
|
| 85 |
+
swizzled.reshape(n_matrices, nrb * ncb, 32, 4, 4)
|
| 86 |
+
.transpose(2, 3)
|
| 87 |
+
.reshape(n_matrices, nrb, ncb, 128, 4)
|
| 88 |
+
.permute(0, 1, 3, 2, 4)
|
| 89 |
+
.reshape(n_matrices, nrb * 128, ncb * 4)[:, :rows, :cols]
|
| 90 |
+
)
|
| 91 |
+
return (plain if num_experts is not None else plain[0]).contiguous()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def swizzled_scale_descriptor(scale_u8: torch.Tensor) -> TensorDescriptor:
|
| 95 |
+
"""TMA descriptor over a SWIZZLE_32_4_4 artifact ``(G, row_blocks, cols // 4, 2, 256)`` addressed
|
| 96 |
+
as ``(1, G * row_blocks, cols // 4, 2, 256)``: the kernels index 128-row blocks flat
|
| 97 |
+
(``group * row_blocks + block``), so the expert axis folds into the block axis."""
|
| 98 |
+
assert scale_u8.ndim == 5 and scale_u8.is_contiguous(), (
|
| 99 |
+
f"swizzled scale must be a contiguous 5D artifact, got {tuple(scale_u8.shape)}"
|
| 100 |
+
)
|
| 101 |
+
groups, row_blocks, cols4 = scale_u8.shape[:3]
|
| 102 |
+
blocks = groups * row_blocks
|
| 103 |
+
return TensorDescriptor(
|
| 104 |
+
scale_u8, [1, blocks, cols4, 2, 256], [blocks * cols4 * 512, cols4 * 512, 512, 256, 1], [1, 1, 1, 2, 256]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _swizzle_to_blocks(
|
| 109 |
+
scale: torch.Tensor, gather_idx: torch.Tensor | None
|
| 110 |
+
) -> torch.Tensor:
|
| 111 |
+
"""One ``(rows, cols)`` scale matrix -> its ``(row_blocks, cols//4, 2, 256)``
|
| 112 |
+
SWIZZLE_32_4_4 block stack (single kernel launch)."""
|
| 113 |
+
cols = scale.shape[1]
|
| 114 |
+
rows = gather_idx.shape[0] if gather_idx is not None else scale.shape[0]
|
| 115 |
+
nrb = triton.cdiv(rows, 128)
|
| 116 |
+
ncb = triton.cdiv(cols, 4)
|
| 117 |
+
# the reorder is byte-level; view as uint8 so the triton binder accepts e8m0/e4m3 scales
|
| 118 |
+
src = scale.view(torch.uint8)
|
| 119 |
+
out = torch.empty(nrb * ncb * 512, device=scale.device, dtype=torch.uint8)
|
| 120 |
+
with device_context(scale.device):
|
| 121 |
+
compile_time_only_triton_wrap(_swizzle_scales_kernel)[(nrb, ncb)](
|
| 122 |
+
src,
|
| 123 |
+
out,
|
| 124 |
+
gather_idx, # None = no gather (the is-not-None guard folds the load out)
|
| 125 |
+
rows,
|
| 126 |
+
cols,
|
| 127 |
+
ncb,
|
| 128 |
+
src.stride(0),
|
| 129 |
+
)
|
| 130 |
+
return out.view(scale.dtype).reshape(nrb, ncb, 2, 256)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def swizzle_mx_scales(
|
| 134 |
+
scale: torch.Tensor, gather_idx: torch.Tensor | None = None
|
| 135 |
+
) -> torch.Tensor:
|
| 136 |
+
"""Reorder a block-scale tensor into the ``SWIZZLE_32_4_4`` layout the Blackwell tcgen05
|
| 137 |
+
scaled-MMA consumes, one triton launch per matrix (``_swizzle_scales_kernel`` — no torch
|
| 138 |
+
permute/transpose copies). The scale values are unchanged — this only rearranges them from
|
| 139 |
+
plain row-major into the swizzled order the tensor core reads directly (the same layout
|
| 140 |
+
cuBLAS/CUTLASS require for MXFP8/NVFP4 ``scaled_mm``). Plain row-major forces a gather that
|
| 141 |
+
caps the scaled dot below the fp8/fp4 peak; the swizzle removes it. Apply to weight scales
|
| 142 |
+
once at model load; the ops accept the result directly.
|
| 143 |
+
|
| 144 |
+
``scale`` is the per-block scale grid — UE8M0 (``float8_e8m0fnu``, group-32 MXFP8/MXFP4) or
|
| 145 |
+
E4M3 (``float8_e4m3fn``, group-16 NVFP4); the reorder is dtype-agnostic (moves bytes):
|
| 146 |
+
|
| 147 |
+
- 2D ``(rows, K // group)``: one matrix (a dense weight's scale).
|
| 148 |
+
- 3D ``(E, rows, K // group)``: an expert stack, each expert swizzled independently so its
|
| 149 |
+
blocks stay ``ceil(rows/128)``-aligned (the reader indexes expert ``e`` at
|
| 150 |
+
``e * ceil(rows/128)``; byte-identical to a flat swizzle when ``rows % 128 == 0``).
|
| 151 |
+
|
| 152 |
+
``gather_idx`` (2D only): a 1-D ``(padded_rows,)`` index mapping each output (sorted) row to
|
| 153 |
+
its source row in ``scale`` (``-1`` = padding → zero row), folded into the kernel's load — the
|
| 154 |
+
routed/expert-sorted, per-tile-padded layout a grouped GEMM reads affine per BM=128 tile.
|
| 155 |
+
``padded_rows`` must be a multiple of 128.
|
| 156 |
+
|
| 157 |
+
``rows``/``cols`` are zero-padded to (128, 4) multiples; returns the 5D
|
| 158 |
+
``(G, row_blocks, ceil(cols/4), 2, 256)`` artifact the ops read — ``G`` the expert count (1 for
|
| 159 |
+
a matrix), so an expert stack shards/gathers on its leading dim. Bit-identical to CUTLASS's
|
| 160 |
+
packer (verified)."""
|
| 161 |
+
assert gather_idx is None or gather_idx.shape[0] % 128 == 0, (
|
| 162 |
+
f"gather_idx rows must be 128-padded, got {None if gather_idx is None else gather_idx.shape[0]}"
|
| 163 |
+
)
|
| 164 |
+
if scale.ndim == 3:
|
| 165 |
+
assert gather_idx is None, "gather_idx applies to a single 2D scale, not an expert stack"
|
| 166 |
+
return torch.stack([_swizzle_to_blocks(e, None) for e in scale])
|
| 167 |
+
assert scale.ndim == 2, (
|
| 168 |
+
f"expected a 2D (rows, K//group) or 3D (E, rows, K//group) scale, got {tuple(scale.shape)}"
|
| 169 |
+
)
|
| 170 |
+
return _swizzle_to_blocks(scale, gather_idx).unsqueeze(0)
|