kernels-bot's picture
Uploaded using `kernel-builder`.
099314d verified
Raw
History Blame
11.1 kB
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
from contextlib import contextmanager
import torch
import triton
import triton.language as tl
from torch.library import triton_op, wrap_triton
from ._ops import add_op_namespace_prefix, ops
# ── Format constants ──────────────────────────────────────────────────────────
# FP8 (E4M3) is the main format for weights and activations;
FP8_DTYPE = torch.float8_e4m3fn
# FP4 (E2M1) packs two 4-bit nibbles per byte. MX formats (MXFP4 weights, MXFP8
# E4M3 weights/activations) share one UE8M0 scale per 32-element K-group — the OCP
# MX block size, consumed by ``tl.dot_scaled``. Format constants, not tunables.
NIBBLES_PER_BYTE = 2
MX_SCALE_GROUP_K = 32
# ── Host-side helpers ─────────────────────────────────────────────────────────
@contextmanager
def device_context(device: torch.device):
"""Context manager that sets the active device for any backend (cuda, xpu, etc.)."""
backend = getattr(torch, device.type, None)
if backend is not None and hasattr(backend, "device"):
with backend.device(device):
yield
else:
yield
def ue8m0_as_uint8(scale: torch.Tensor) -> torch.Tensor:
"""View UE8M0 (``float8_e8m0fnu``) weight scales as ``uint8`` for the Triton
binder, which doesn't recognize the dtype; kernels decode ``2^(exp-127)``
inline. fp32 (non-UE8M0) scales pass through unchanged."""
return scale.view(torch.uint8) if scale.dtype == torch.float8_e8m0fnu else scale
def is_mxfp8(weight: torch.Tensor, scale: torch.Tensor) -> bool:
"""MXFP8 weight/scale pair: E4M3 weights with UE8M0 group-32 scales — last dim
``scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K``, matching leading dims.
Works for 2D ``(N, K)`` and 3D ``(E, N, K)`` weights. The group-32 layout is what
separates MXFP8 from 128-block FP8 (which may also carry UE8M0 scales)."""
return (
weight.dtype == torch.float8_e4m3fn
and scale.dtype == torch.float8_e8m0fnu
and scale.ndim == weight.ndim
and scale.shape[:-1] == weight.shape[:-1]
and scale.shape[-1] == weight.shape[-1] // MX_SCALE_GROUP_K
)
def is_mxfp4(weight: torch.Tensor, scale: torch.Tensor) -> bool:
"""MXFP4 weight/scale pair: packed E2M1 weights (``int8``, two codes/byte) with
UE8M0 group-32 scales — ``scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE //
MX_SCALE_GROUP_K`` (unpacked K = ``2 * K_half``), matching leading dims. 2D or 3D."""
return (
weight.dtype == torch.int8
and scale.dtype == torch.float8_e8m0fnu
and scale.ndim == weight.ndim
and scale.shape[:-1] == weight.shape[:-1]
and scale.shape[-1] == weight.shape[-1] * NIBBLES_PER_BYTE // MX_SCALE_GROUP_K
)
def adaptive_block_size_m(target_m: int) -> int:
"""Smallest power-of-2 >= ``target_m``, floored at 16 and capped at 128.
Used by all matmul wrappers (single / batched / grouped) to size the M tile
to the workload — small per-expert M wants smaller tiles, large M caps out
at 128 to keep register pressure bounded. Pass ``M`` for single matmul, or
``(S + E - 1) // E`` (avg tokens per expert) for batched/grouped.
"""
return min(max(triton.next_power_of_2(target_m), 16), 128)
@functools.cache
def get_active_device_type() -> str:
"""Active torch device type for the current Triton backend (``"cuda"``, ``"xpu"``, ...).
Falls back to ``"cuda"`` when no driver is loaded — Triton raises
``RuntimeError: 0 active drivers ([])`` on driverless build boxes, and the
autotune-config builder is evaluated at module-import time under the
``@triton.autotune`` decorator (no kernel launches there, so the default is
only used to shape the config list).
"""
try:
return triton.runtime.driver.active.get_active_torch_device().type
except RuntimeError:
return "cuda"
def get_accelerator_autotuning_configs(*, with_block_sizes: bool = False):
"""Autotune search grid for the current accelerator.
``num_warps``, ``num_stages`` and ``blocks`` (the ``(BLOCK_SIZE_N, BLOCK_SIZE_K)``
tile shapes) are fixed up front from ``(is_xpu, with_block_sizes)``, then crossed
into the config list.
``with_block_sizes=True`` sweeps the tile: used by kernels that have no caller
``block_size`` to fix it — the MX ``tl.dot_scaled`` paths AND the tensor-dynamic
FP8 paths. ``with_block_sizes=False`` emits a single empty meta-dict (block-scaled
kernels take the tile from the caller's ``block_size``).
The CUDA tile set is a data-driven prune of a B200 sweep across single (BM=128),
grouped MoE (BM=16/64) and decode (BM=1): winners only ever used these 4 tiles,
num_warps in {4,8,16}, num_stages in {2,3} (warps=2, stages=4 and 256x256 never
won) — 108 → 24. (Tuned on dot_scaled; tensor-dynamic tl.dot reuses it.)
"""
is_xpu = get_active_device_type() == "xpu"
if with_block_sizes:
num_stages = [2, 3]
num_warps = [8, 16] if is_xpu else [4, 8, 16]
tiles = (
[(128, 128)] if is_xpu else [(128, 128), (256, 128), (128, 64), (64, 256)]
)
blocks = [{"BLOCK_SIZE_N": bn, "BLOCK_SIZE_K": bk} for bn, bk in tiles]
else:
num_stages = [2, 3, 4]
num_warps = [8, 16] if is_xpu else [2, 4, 8, 16]
blocks = [{}]
return [
triton.Config(b, num_warps=w, num_stages=s)
for b in blocks
for w in num_warps
for s in num_stages
]
# ── Triton-side helpers (inlined by ``@triton.jit`` callers) ──────────────────
@triton.jit
def fp8_act_quant_inline(a_raw):
"""Inline FP8 (E4M3) activation quant for the W8A8 block-scale path.
Per-row amax → fp32 scale ``amax/448`` (floored at 1e-12 against zero rows)
→ cast values to FP8. Returns ``(a_fp8, a_s)`` with shapes ``(M, K)`` and
``(M,)``.
"""
a_s = tl.max(tl.abs(a_raw), axis=1) / 448.0
a_fp8 = (a_raw / tl.maximum(a_s[:, None], 1e-12)).to(tl.float8e4nv)
return a_fp8, a_s
@triton.jit
def mx_act_quant_inline(
a_raw,
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_K: tl.constexpr,
SCALE_GROUP_K: tl.constexpr,
):
"""Inline E4M3 activation quant for the MX paths (W4A8 MXFP4 / W8A8 MXFP8).
Per-row, per-K-group amax → UE8M0 scale (ceil to next power-of-2 via the
mantissa-nonzero bump trick) → cast values to FP8. Returns ``(a_fp8,
a_scale_u8)`` with shapes ``(M, K)`` and ``(M, K // SCALE_GROUP_K)``.
"""
a_groups = tl.reshape(
a_raw, (BLOCK_SIZE_M, BLOCK_SIZE_K // SCALE_GROUP_K, SCALE_GROUP_K)
)
a_s_fp32 = tl.max(tl.abs(a_groups), axis=2) / 448.0
bits = a_s_fp32.to(tl.int32, bitcast=True)
# ceil_to_ue8m0: bump exponent by 1 when mantissa is non-zero.
exp_ceil = ((bits >> 23) & 0xFF) + ((bits & 0x7FFFFF) != 0).to(tl.int32)
exp_ceil = tl.minimum(tl.maximum(exp_ceil, 1), 254)
a_scale_u8 = exp_ceil.to(tl.uint8)
a_s_pow2 = (exp_ceil << 23).to(tl.float32, bitcast=True)
a_fp8 = tl.reshape(
a_groups / tl.maximum(a_s_pow2[:, :, None], 1e-12),
(BLOCK_SIZE_M, BLOCK_SIZE_K),
).to(tl.float8e4nv)
return a_fp8, a_scale_u8
@triton.jit
def decode_ue8m0_scale(scale):
"""Decode a UE8M0 weight scale to fp32: when it was loaded as ``uint8`` exponent
bits, ``value = 2^(exp - 127)``, built directly as the fp32 bit pattern. fp32
scales (block-dynamic with float scales) pass through. The dtype branch is a
compile-time constant, so only the taken path is emitted (single return — Triton
requires all ``return`` statements to share a type)."""
if scale.dtype == tl.uint8:
scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
return scale
# ── fp8_act_quant kernel (used by tensor-mode FP8 wrappers) ───────────────────
@triton.jit
def _fp8_act_quant_kernel(
x_ptr, y_ptr, s_ptr, BLOCK_SIZE: tl.constexpr, PADDED_BLOCK: tl.constexpr
):
# ``tl.arange`` needs a power-of-2 length, so iterate over PADDED_BLOCK (the next
# power of 2) and mask the tail — lets block_size be non-power-of-2 (e.g. a full
# row K=14336 in tensor-mode). Masked lanes load 0, which can't affect ``amax``.
pid = tl.program_id(axis=0)
cols = tl.arange(0, PADDED_BLOCK)
mask = cols < BLOCK_SIZE
offs = pid * BLOCK_SIZE + cols
x = tl.load(x_ptr + offs, mask=mask, other=0.0).to(tl.float32)
s = tl.max(tl.abs(x)) / 448.0 # float8_e4m3fn max
y = (x / tl.maximum(s, 1e-12)).to(y_ptr.dtype.element_ty)
tl.store(y_ptr + offs, y, mask=mask)
tl.store(s_ptr + pid, s)
@triton_op(add_op_namespace_prefix("fp8_act_quant"), mutates_args=())
def _fp8_act_quant(
x: torch.Tensor, block_size: int = 128
) -> tuple[torch.Tensor, torch.Tensor]:
assert x.is_contiguous()
assert x.shape[-1] % block_size == 0
y = torch.empty_like(x, dtype=FP8_DTYPE)
grid = (triton.cdiv(x.numel(), block_size),)
s = x.new_empty(*x.size()[:-1], x.size(-1) // block_size, dtype=torch.float32)
with device_context(x.device):
wrap_triton(_fp8_act_quant_kernel)[grid](
x,
y,
s,
BLOCK_SIZE=block_size,
PADDED_BLOCK=triton.next_power_of_2(block_size),
)
return y, s
def fp8_act_quant(
x: torch.Tensor, block_size: int = 128
) -> tuple[torch.Tensor, torch.Tensor]:
"""Quantize activations to FP8 with per-block dynamic scaling.
Splits the last dimension of ``x`` into blocks of ``block_size`` elements,
computes ``scale = max(|x_block|) / 448`` per block, and quantizes to
``float8_e4m3fn``.
Args:
x: Input tensor in bf16/fp16/fp32. Last dimension must be divisible by
``block_size`` and the tensor must be contiguous.
block_size: Number of elements per quantization block (default: 128).
Returns:
A tuple ``(quantized, scales)`` where ``quantized`` has dtype
``float8_e4m3fn`` with the same shape as ``x``, and ``scales`` has
shape ``(*x.shape[:-1], x.shape[-1] // block_size)`` in float32.
"""
return ops.fp8_act_quant(x, block_size)