59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
485 lines
16 KiB
Python
485 lines
16 KiB
Python
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
from typing import Optional
|
|
|
|
import torch
|
|
from tokenspeed_kernel._triton import tl, triton
|
|
from tokenspeed_kernel.platform import CapabilityRequirement, current_platform
|
|
from tokenspeed_kernel.registry import Priority, register_kernel
|
|
from tokenspeed_kernel.signature import format_signatures
|
|
|
|
platform = current_platform()
|
|
|
|
|
|
@triton.jit
|
|
def _fp8_quantize_kernel(
|
|
x_ptr,
|
|
out_ptr,
|
|
scale,
|
|
M,
|
|
N,
|
|
x_row_stride,
|
|
out_row_stride,
|
|
BLOCK_N: tl.constexpr,
|
|
EVEN_N: tl.constexpr,
|
|
FP8_DTYPE: tl.constexpr,
|
|
BLOCK_M: tl.constexpr,
|
|
HAS_SCALE: tl.constexpr,
|
|
HAS_SCALE_TENSOR: tl.constexpr,
|
|
ENABLE_PDL: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0)
|
|
m_idx = pid * BLOCK_M + tl.arange(0, BLOCK_M)
|
|
m_mask = m_idx < M
|
|
n_idx = tl.arange(0, BLOCK_N)
|
|
|
|
# PDL: wait for the producer kernel (e.g., kv_b_proj GEMM) to drain before
|
|
# we read its output. No-op when disabled.
|
|
if ENABLE_PDL:
|
|
tl.extra.cuda.gdc_wait()
|
|
|
|
if EVEN_N:
|
|
load_mask = m_mask[:, None]
|
|
else:
|
|
load_mask = m_mask[:, None] & (n_idx[None, :] < N)
|
|
|
|
x_off = m_idx[:, None] * x_row_stride + n_idx[None, :]
|
|
x = tl.load(x_ptr + x_off, mask=load_mask)
|
|
|
|
x = x.to(tl.float32)
|
|
if HAS_SCALE:
|
|
if HAS_SCALE_TENSOR:
|
|
scale = tl.load(scale)
|
|
x = x * (1.0 / scale)
|
|
x_fp8 = x.to(FP8_DTYPE)
|
|
|
|
out_off = m_idx[:, None] * out_row_stride + n_idx[None, :]
|
|
tl.store(out_ptr + out_off, x_fp8, mask=load_mask)
|
|
|
|
# PDL: signal that dependents (e.g., FMHA) can begin their preamble.
|
|
if ENABLE_PDL:
|
|
tl.extra.cuda.gdc_launch_dependents()
|
|
|
|
|
|
def _flatten_to_2d(x: torch.Tensor):
|
|
"""Flatten leading dims into a single M, returning (M, N, row_stride).
|
|
|
|
Requires stride(-1) == 1 and that all leading dims pack contiguously
|
|
onto the row stride — i.e. ``stride(d) == shape(d+1) * stride(d+1)`` for
|
|
every ``d < ndim - 2``. This holds for fully-contiguous tensors and for
|
|
last-dim slices like ``kv[..., qk_nope:]`` where the leading dims still
|
|
pack onto a uniform row stride.
|
|
"""
|
|
assert x.stride(-1) == 1, f"expected stride-1 inner dim, got stride={x.stride(-1)}"
|
|
N = x.shape[-1]
|
|
if x.ndim == 1:
|
|
return 1, N, N
|
|
M = x.numel() // N
|
|
row_stride = x.stride(-2)
|
|
# Validate that every leading dim packs onto the next.
|
|
for d in range(x.ndim - 2):
|
|
expected = x.shape[d + 1] * x.stride(d + 1)
|
|
if x.stride(d) != expected:
|
|
raise ValueError(
|
|
f"cannot flatten dim {d}: stride={x.stride(d)} but expected "
|
|
f"shape[{d+1}]*stride[{d+1}]={expected}. Tensor shape={tuple(x.shape)}, "
|
|
f"stride={tuple(x.stride())}."
|
|
)
|
|
return M, N, row_stride
|
|
|
|
|
|
def fp8_quantize(
|
|
x: torch.Tensor,
|
|
scale: float | torch.Tensor | None = None,
|
|
out: Optional[torch.Tensor] = None,
|
|
fp8_dtype: torch.dtype = torch.float8_e4m3fn,
|
|
enable_pdl: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Cast a BF16/FP16 tensor to FP8 with an optional per-tensor scale.
|
|
|
|
Computes ``out = saturate((x / scale) -> fp8)`` element-wise when scale is
|
|
provided. When scale is omitted, this is a pure FP8 cast.
|
|
|
|
Args:
|
|
x: BF16 or FP16 tensor. Must have stride(-1) == 1; leading dims must
|
|
pack uniformly onto the row stride (true for contiguous tensors and
|
|
for last-dim slice views like ``kv[..., qk_nope:]``).
|
|
scale: optional scalar divisor applied before the cast. Python values
|
|
are passed as plain kernel args; scalar tensors are loaded on device.
|
|
out: optional pre-allocated FP8 output. Same shape as ``x``. If not
|
|
provided, allocated as contiguous.
|
|
fp8_dtype: ``torch.float8_e4m3fn`` (default), ``torch.float8_e5m2`` or
|
|
``torch.float8_e4m3fnuz`` (the bias-8 e4m3 used on AMD CDNA3).
|
|
enable_pdl: opt into Programmatic Dependent Launch (Hopper+). Caller
|
|
must also pass ``launch_pdl=True`` upstream / downstream as needed.
|
|
|
|
Returns:
|
|
FP8 tensor with the same shape as ``x``.
|
|
"""
|
|
assert x.dtype in (
|
|
torch.bfloat16,
|
|
torch.float16,
|
|
), f"fp8_quantize input must be bf16/fp16, got {x.dtype}"
|
|
assert fp8_dtype in (
|
|
torch.float8_e4m3fn,
|
|
torch.float8_e5m2,
|
|
torch.float8_e4m3fnuz,
|
|
), f"fp8_quantize unsupported fp8 dtype: {fp8_dtype}"
|
|
has_scale = scale is not None
|
|
has_scale_tensor = isinstance(scale, torch.Tensor)
|
|
if has_scale_tensor:
|
|
assert scale.numel() == 1, "scale tensor must be scalar"
|
|
scale = scale.contiguous()
|
|
|
|
M, N, x_row_stride = _flatten_to_2d(x)
|
|
|
|
if out is None:
|
|
out = torch.empty(x.shape, dtype=fp8_dtype, device=x.device)
|
|
else:
|
|
assert out.shape == x.shape and out.dtype == fp8_dtype
|
|
out_M, _, out_row_stride = _flatten_to_2d(out)
|
|
assert out_M == M
|
|
|
|
if fp8_dtype is torch.float8_e4m3fn:
|
|
fp8_dtype_const = tl.float8e4nv
|
|
elif fp8_dtype is torch.float8_e5m2:
|
|
fp8_dtype_const = tl.float8e5
|
|
else:
|
|
fp8_dtype_const = tl.float8e4b8
|
|
|
|
# Block-size heuristic — picked from per-shape best configs in an
|
|
# nsys-driven sweep on B200 (kv_a [s,512] and v [s,h,128] for K2.5).
|
|
# Pattern: num_warps=4, num_stages=2 win universally; BLOCK_M ramps with
|
|
# M to amortize launch as the grid grows.
|
|
# See tasks/k2.5_optimization/{tune_fp8_quantize_nsys,parse_tune_fp8_quantize_nsys}.py
|
|
if M <= 2048:
|
|
block_m = 4
|
|
elif M <= 16384:
|
|
block_m = 16
|
|
else:
|
|
block_m = 32
|
|
num_warps = 4
|
|
num_stages = 2
|
|
|
|
grid = (triton.cdiv(M, block_m),)
|
|
|
|
block_n = max(1, triton.next_power_of_2(N))
|
|
even_n = block_n == N
|
|
|
|
# ``launch_pdl`` is a NVIDIA-only Triton runtime kwarg (Hopper+ Programmatic
|
|
# Dependent Launch). The HIP backend rejects unknown kwargs, so only forward
|
|
# it when PDL is actually requested.
|
|
extra_kwargs = {"launch_pdl": True} if enable_pdl else {}
|
|
|
|
_fp8_quantize_kernel[grid](
|
|
x,
|
|
out,
|
|
1.0 if scale is None else scale,
|
|
M,
|
|
N,
|
|
x_row_stride,
|
|
out_row_stride,
|
|
BLOCK_N=block_n,
|
|
EVEN_N=even_n,
|
|
FP8_DTYPE=fp8_dtype_const,
|
|
BLOCK_M=block_m,
|
|
HAS_SCALE=has_scale,
|
|
HAS_SCALE_TENSOR=has_scale_tensor,
|
|
ENABLE_PDL=enable_pdl,
|
|
num_warps=num_warps,
|
|
num_stages=num_stages,
|
|
**extra_kwargs,
|
|
)
|
|
return out
|
|
|
|
|
|
@triton.jit
|
|
def _fp8_token_group_quantize_kernel(
|
|
x_ptr,
|
|
out_ptr,
|
|
scale_ptr,
|
|
group_size,
|
|
eps,
|
|
bit8_min,
|
|
bit8_max,
|
|
BLOCK: tl.constexpr,
|
|
):
|
|
group_id = tl.program_id(0)
|
|
cols = tl.arange(0, BLOCK)
|
|
mask = cols < group_size
|
|
offsets = group_id.to(tl.int64) * group_size + cols
|
|
|
|
x = tl.load(x_ptr + offsets, mask=mask, other=0.0).to(tl.float32)
|
|
scale = tl.maximum(tl.max(tl.abs(x), axis=0), eps) / bit8_max
|
|
out = tl.clamp(x / scale, bit8_min, bit8_max).to(out_ptr.dtype.element_ty)
|
|
|
|
tl.store(out_ptr + offsets, out, mask=mask)
|
|
tl.store(scale_ptr + group_id, scale)
|
|
|
|
|
|
def _fp8_token_group_quantize(
|
|
x: torch.Tensor,
|
|
group_size: int,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
if x.shape[-1] % group_size != 0:
|
|
raise ValueError(
|
|
f"the last dimension of x must be divisible by group_size, got "
|
|
f"shape={tuple(x.shape)}, group_size={group_size}"
|
|
)
|
|
if not x.is_contiguous():
|
|
raise ValueError("x must be contiguous")
|
|
|
|
out_dtype = platform.fp8e4m3fn.dtype
|
|
out = torch.empty_like(x, device=x.device, dtype=out_dtype)
|
|
scales = torch.empty(
|
|
x.shape[:-1] + (x.shape[-1] // group_size,),
|
|
device=x.device,
|
|
dtype=torch.float32,
|
|
)
|
|
|
|
groups = x.numel() // group_size
|
|
block = triton.next_power_of_2(group_size)
|
|
num_warps = min(max(block // 256, 1), 8)
|
|
bit8_max = platform.fp8e4m3fn.max
|
|
bit8_min = -bit8_max
|
|
|
|
_fp8_token_group_quantize_kernel[(groups,)](
|
|
x,
|
|
out,
|
|
scales,
|
|
group_size,
|
|
1e-10,
|
|
bit8_min=bit8_min,
|
|
bit8_max=bit8_max,
|
|
BLOCK=block,
|
|
num_warps=num_warps,
|
|
num_stages=1,
|
|
)
|
|
return out, scales
|
|
|
|
|
|
@register_kernel(
|
|
"quantization",
|
|
"fp8",
|
|
name="triton_quantize_fp8",
|
|
solution="triton",
|
|
signatures=format_signatures("x", "dense", {torch.bfloat16, torch.float16}),
|
|
traits={"has_scale": frozenset({True, False})},
|
|
priority=Priority.PORTABLE,
|
|
)
|
|
def triton_quantize_fp8(
|
|
x: torch.Tensor,
|
|
scale: float | torch.Tensor | None = None,
|
|
enable_pdl: bool = False,
|
|
) -> torch.Tensor:
|
|
return fp8_quantize(x, scale=scale, enable_pdl=enable_pdl)
|
|
|
|
|
|
@register_kernel(
|
|
"quantization",
|
|
"fp8_with_scale",
|
|
name="triton_quantize_fp8_with_scale",
|
|
solution="triton",
|
|
capability=CapabilityRequirement(vendors=frozenset({"amd", "nvidia"})),
|
|
signatures=format_signatures("x", "dense", {torch.bfloat16, torch.float16}),
|
|
traits={
|
|
"granularity": frozenset({"token_group_128"}),
|
|
"scale_encoding": frozenset({"float32"}),
|
|
},
|
|
priority=Priority.PORTABLE,
|
|
)
|
|
def triton_quantize_fp8_with_scale(
|
|
x: torch.Tensor,
|
|
granularity: str = "tensor",
|
|
group_size: int | None = None,
|
|
scale_encoding: str = "float32",
|
|
enable_pdl: bool = False,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
if granularity != "token_group" or group_size != 128:
|
|
raise ValueError(
|
|
"triton FP8 dynamic quantization currently supports only "
|
|
f"granularity='token_group' with group_size=128, got "
|
|
f"granularity={granularity!r}, group_size={group_size}."
|
|
)
|
|
if scale_encoding != "float32":
|
|
raise ValueError(
|
|
"triton FP8 dynamic quantization currently requires "
|
|
f"scale_encoding='float32', got {scale_encoding!r}."
|
|
)
|
|
return _fp8_token_group_quantize(x.contiguous(), 128)
|
|
|
|
|
|
@triton.jit
|
|
def _mxfp4_quantize_block(x):
|
|
max_normal: tl.constexpr = 6
|
|
min_normal: tl.constexpr = 1
|
|
amax = tl.max(tl.abs(x), axis=0)
|
|
amax = amax.to(tl.int32, bitcast=True)
|
|
amax = (amax + 0x200000).to(tl.uint32, bitcast=True) & 0xFF800000
|
|
amax = amax.to(tl.float32, bitcast=True)
|
|
scale_e8m0_unbiased = tl.log2(amax).floor() - 2
|
|
scale_e8m0_unbiased = tl.clamp(scale_e8m0_unbiased, min=-127, max=127)
|
|
scale_byte = scale_e8m0_unbiased.to(tl.uint8) + 127
|
|
qx = x * tl.exp2(-scale_e8m0_unbiased)
|
|
qx = qx.to(tl.uint32, bitcast=True)
|
|
|
|
sign = qx & 0x80000000
|
|
qx = qx ^ sign
|
|
qx_fp32 = qx.to(tl.float32, bitcast=True)
|
|
saturate_mask = qx_fp32 >= max_normal
|
|
denormal_mask = (not saturate_mask) & (qx_fp32 < min_normal)
|
|
normal_mask = not (saturate_mask | denormal_mask)
|
|
|
|
denorm_exp: tl.constexpr = (127 - 1) + (23 - 1) + 1
|
|
denorm_mask_int: tl.constexpr = denorm_exp << 23
|
|
denorm_mask_float: tl.constexpr = tl.cast(denorm_mask_int, tl.float32, bitcast=True)
|
|
denormal_x = qx_fp32 + denorm_mask_float
|
|
denormal_x = denormal_x.to(tl.uint32, bitcast=True)
|
|
denormal_x -= denorm_mask_int
|
|
denormal_x = denormal_x.to(tl.uint8)
|
|
|
|
normal_x = qx
|
|
mant_odd = (normal_x >> (23 - 1)) & 1
|
|
normal_x += 0xC11FFFFF
|
|
normal_x += mant_odd
|
|
normal_x = normal_x >> (23 - 1)
|
|
normal_x = normal_x.to(tl.uint8)
|
|
|
|
e2m1 = tl.full(x.shape, 0x7, dtype=tl.uint8)
|
|
e2m1 = tl.where(normal_mask, normal_x, e2m1)
|
|
e2m1 = tl.where(denormal_mask, denormal_x, e2m1)
|
|
sign_lp = sign >> (23 + 8 - 1 - 2)
|
|
sign_lp = sign_lp.to(tl.uint8)
|
|
e2m1 = e2m1 | sign_lp
|
|
e2m1 = tl.reshape(e2m1, [16, 2])
|
|
evens, odds = tl.split(e2m1)
|
|
return evens | (odds << 4), scale_byte
|
|
|
|
|
|
@triton.jit
|
|
def _mxfp4_quantize_kernel(
|
|
x_ptr,
|
|
out_ptr,
|
|
scale_ptr,
|
|
M,
|
|
x_row_stride,
|
|
out_row_stride,
|
|
scale_row_stride,
|
|
N: tl.constexpr,
|
|
):
|
|
pid_m = tl.program_id(0)
|
|
pid_g = tl.program_id(1)
|
|
offs_k = pid_g * 32 + tl.arange(0, 32)
|
|
x = tl.load(x_ptr + pid_m * x_row_stride + offs_k).to(tl.float32)
|
|
|
|
packed, scale_byte = _mxfp4_quantize_block(x)
|
|
pack_idx = tl.arange(0, 16)
|
|
tl.store(out_ptr + pid_m * out_row_stride + pid_g * 16 + pack_idx, packed)
|
|
tl.store(scale_ptr + pid_m * scale_row_stride + pid_g, scale_byte)
|
|
|
|
|
|
def mxfp4_quantize(
|
|
x: torch.Tensor,
|
|
out: Optional[torch.Tensor] = None,
|
|
scales: Optional[torch.Tensor] = None,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Quantize a BF16/FP16 tensor to packed MXFP4.
|
|
|
|
The last dimension is quantized in groups of 32 values. The returned data
|
|
packs two E2M1 values per byte and returns one uint8 E8M0 scale per group.
|
|
"""
|
|
assert x.dtype in (
|
|
torch.bfloat16,
|
|
torch.float16,
|
|
), f"mxfp4_quantize input must be bf16/fp16, got {x.dtype}"
|
|
M, N, x_row_stride = _flatten_to_2d(x)
|
|
if N % 32 != 0:
|
|
raise ValueError("mxfp4_quantize requires the last dimension divisible by 32")
|
|
|
|
out_shape = (*x.shape[:-1], N // 2)
|
|
scale_shape = (*x.shape[:-1], N // 32)
|
|
if out is None:
|
|
out = torch.empty(out_shape, dtype=torch.uint8, device=x.device)
|
|
else:
|
|
assert out.shape == out_shape and out.dtype == torch.uint8
|
|
if scales is None:
|
|
scales = torch.empty(scale_shape, dtype=torch.uint8, device=x.device)
|
|
else:
|
|
assert scales.shape == scale_shape and scales.dtype == torch.uint8
|
|
|
|
out_M, _, out_row_stride = _flatten_to_2d(out)
|
|
scales_M, _, scale_row_stride = _flatten_to_2d(scales)
|
|
assert out_M == M and scales_M == M
|
|
|
|
grid = (M, N // 32)
|
|
_mxfp4_quantize_kernel[grid](
|
|
x,
|
|
out,
|
|
scales,
|
|
M,
|
|
x_row_stride,
|
|
out_row_stride,
|
|
scale_row_stride,
|
|
N=N,
|
|
num_warps=1,
|
|
)
|
|
return out, scales
|
|
|
|
|
|
@register_kernel(
|
|
"quantization",
|
|
"mxfp4",
|
|
name="triton_quantize_mxfp4",
|
|
solution="triton",
|
|
capability=CapabilityRequirement(vendors=frozenset({"amd"})),
|
|
signatures=format_signatures("x", "dense", {torch.bfloat16, torch.float16}),
|
|
traits={
|
|
"scale_size": frozenset({32}),
|
|
"scale_layout": frozenset({"linear"}),
|
|
"has_global_scale": frozenset({False}),
|
|
"scale_encoding": frozenset({"ue8m0"}),
|
|
},
|
|
priority=Priority.PORTABLE,
|
|
)
|
|
def triton_quantize_mxfp4(
|
|
x: torch.Tensor,
|
|
global_scale: float | None = None,
|
|
scale_size: int = 32,
|
|
scale_layout: str = "linear",
|
|
enable_pdl: bool = False,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
if global_scale is not None:
|
|
raise ValueError("triton MXFP4 quantization does not support global_scale")
|
|
if scale_size != 32:
|
|
raise ValueError(
|
|
f"triton MXFP4 quantization requires scale_size=32, got {scale_size}"
|
|
)
|
|
if scale_layout != "linear":
|
|
raise ValueError(
|
|
"triton MXFP4 quantization requires scale_layout='linear', "
|
|
f"got {scale_layout!r}"
|
|
)
|
|
return mxfp4_quantize(x)
|
|
|
|
|
|
__all__ = [
|
|
"fp8_quantize",
|
|
"mxfp4_quantize",
|
|
"triton_quantize_mxfp4",
|
|
"triton_quantize_fp8_with_scale",
|
|
]
|