Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

2024 lines
70 KiB
Python
Executable File

from __future__ import annotations
import logging
from enum import Enum
from functools import lru_cache
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union
import torch
from sglang.srt.layers import deep_gemm_wrapper
from sglang.srt.layers.quantization.fp8_kernel import (
sglang_per_token_group_quant_fp8,
sglang_per_token_group_quant_fp8_row_padded,
)
from sglang.srt.layers.quantization.mxfp4_tensor import MXFP4QuantizeUtil
from sglang.srt.runtime_context import get_parallel
from sglang.srt.utils.common import torch_release
if TYPE_CHECKING:
from sglang.srt.server_args import ServerArgs
from sglang.srt.layers.quantization.fp8_kernel import (
fp8_dtype,
fp8_max,
fp8_min,
is_fp8_fnuz,
mxfp8_block_scaled_matmul_triton,
pack_mxfp8_scales_triton,
per_token_group_quant_fp8,
scaled_fp8_quant,
sglang_per_token_quant_fp8,
static_quant_fp8,
triton_scaled_mm,
w8a8_block_fp8_matmul_deepgemm,
w8a8_block_fp8_matmul_triton,
)
from sglang.srt.runtime_context import get_server_args
from sglang.srt.utils import (
ceil_align,
ceil_div,
get_bool_env_var,
get_cuda_version,
get_device_capability,
get_hip_version,
is_blackwell_supported,
is_cuda,
is_flashinfer_available,
is_gfx95_supported,
is_hip,
is_musa,
is_sm90_supported,
is_sm100_supported,
is_sm120_supported,
offloader,
)
from sglang.srt.utils.custom_op import register_custom_op
logger = logging.getLogger(__name__)
_is_hip = is_hip()
_is_cuda = is_cuda()
_is_fp8_fnuz = is_fp8_fnuz()
_is_sm100_supported = is_sm100_supported()
_is_sm120_supported = is_sm120_supported()
_is_gfx95_supported = is_gfx95_supported()
_is_musa = is_musa()
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and _is_hip
_use_aiter_gfx95 = _use_aiter and _is_gfx95_supported
# ROCm 7.0 hipcc miscompiles gemm_a8w8_blockscale_bpreshuffle on gfx95 (#23319).
_use_aiter_bpreshuffle_gfx95 = _use_aiter_gfx95 and get_hip_version() >= (7, 2, 0)
# gfx95 + ROCm < 7.2: bpreshuffle CK is disabled (above), and the non-bpreshuffle
# fallback ck_gemm_a8w8_blockscale returns NaN above a per-shape M for some shapes
# (measured NaN onset: (2560,4096)@M>=4096, (4096,1024)@M>=8192), corrupting prefill.
# Map each affected (n, k) to the largest M for which CK is confirmed correct
# (conservative = last verified-safe M). Keep the faster CK path at/below that M and
# fall back to the numerically-correct Triton FP8 GEMM above it. Fixed in ROCm 7.2.
_AITER_GFX95_CK_W8A8_MAX_SAFE_M = {
(2560, 4096): 2048,
(4096, 1024): 4096,
}
class _MXFP4QuantizedData(MXFP4QuantizeUtil):
def __init__(
self,
original_shape: torch.Size,
original_dtype: torch.dtype,
quantized_data: torch.Tensor,
):
self.original_shape = original_shape
self.original_dtype = original_dtype
self.quantized_data = quantized_data
# Force CK bpreshuffle (not Triton) for the dense w8a8-block GEMMs (MLA q/kv/o
# projections), to match ATOM (CK preshuffle; Triton FP8 blockscale is slower).
# Default OFF; DeepseekV4 enables it via set_force_ck_w8a8(True). The env var
# SGLANG_FORCE_CK_W8A8=1 still works as an override.
_FORCE_CK_W8A8: bool = False
def set_force_ck_w8a8(enabled: bool = True) -> None:
global _FORCE_CK_W8A8
_FORCE_CK_W8A8 = enabled
def materialize_bpreshuffle_fp8_scale(scale: torch.Tensor) -> torch.Tensor:
"""Materialize the physical scale layout consumed by gfx95 bpreshuffle GEMM."""
return scale.t().contiguous().t() if scale.dim() == 2 else scale
def materialize_bpreshuffle_fp8_scale_tuple(
value: Tuple[torch.Tensor, ...],
) -> Tuple[torch.Tensor, ...]:
"""Materialize the scale slot in FP8 ``(q_input, x_scale, ...)`` tuples."""
return (
value[0],
materialize_bpreshuffle_fp8_scale(value[1]),
*value[2:],
)
def use_aiter_triton_gemm_w8a8_tuned_gfx950(n: int, k: int) -> bool:
if _FORCE_CK_W8A8:
return False
return (n, k) in [
(1024, 8192),
(16384, 1536),
(2112, 7168),
(3072, 1536),
(32768, 8192),
(4096, 7168),
(4608, 7168),
(512, 7168),
(7168, 2048),
(7168, 2304),
(7168, 16384),
(7168, 256),
(8192, 1024),
(8192, 32768),
]
if _use_aiter:
import aiter
from aiter import gemm_a8w8_blockscale as ck_gemm_a8w8_blockscale
from aiter import (
gemm_a8w8_blockscale_bpreshuffle,
gemm_a8w8_bpreshuffle,
get_hip_quant,
)
from aiter.ops.triton.gemm_a8w8_blockscale import (
gemm_a8w8_blockscale as triton_gemm_a8w8_blockscale,
)
aiter_per1x128_quant = get_hip_quant(aiter.QuantType.per_1x128)
if _is_cuda:
from sgl_kernel import fp8_blockwise_scaled_mm, fp8_scaled_mm
from sglang.srt.utils.patch_torch import register_fake_if_exists
@register_fake_if_exists("sgl_kernel::fp8_scaled_mm")
def _fp8_scaled_mm_abstract(mat_a, mat_b, scales_a, scales_b, out_dtype, bias=None):
# mat_a: [M, K], mat_b: [K, N] or [N, K] depending on callsite layout; output is [M, N].
M = mat_a.shape[-2]
N = mat_b.shape[-1]
return mat_a.new_empty((M, N), dtype=out_dtype)
@register_fake_if_exists("sgl_kernel::fp8_blockwise_scaled_mm")
def _fp8_blockwise_scaled_mm_abstract(mat_a, mat_b, scales_a, scales_b, out_dtype):
# mat_a: [M, K], mat_b: [K, N] or [N, K] depending on callsite layout; output is [M, N].
M = mat_a.shape[-2]
N = mat_b.shape[-1]
return mat_a.new_empty((M, N), dtype=out_dtype)
use_triton_w8a8_fp8_kernel = get_bool_env_var("USE_TRITON_W8A8_FP8_KERNEL")
# Input scaling factors are no longer optional in _scaled_mm starting
# from pytorch 2.5. Allocating a dummy tensor to pass as input_scale
TORCH_DEVICE_IDENTITY = None
def use_rowwise_torch_scaled_mm():
if _is_hip:
# The condition to determine if it is on a platform that supports
# torch._scaled_mm rowwise feature.
# The condition is determined once as the operations
# are time consuming.
return get_device_capability() >= (9, 4) and torch_release >= (2, 7)
return False
USE_ROWWISE_TORCH_SCALED_MM = use_rowwise_torch_scaled_mm()
@lru_cache(maxsize=1)
def cutlass_fp8_supported():
if not _is_cuda:
return False
major, minor = get_device_capability()
cuda_version = get_cuda_version()
if major >= 9:
return cuda_version >= (12, 0)
elif major == 8 and minor == 9:
return cuda_version >= (12, 4)
return False
def normalize_e4m3fn_to_e4m3fnuz(
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
assert weight.dtype == torch.float8_e4m3fn
# The bits pattern 10000000(-128) represents zero in e4m3fn
# but NaN in e4m3fnuz. So here we set it to 0.
# https://onnx.ai/onnx/technical/float8.html
weight_as_int8 = weight.view(torch.int8)
ROCM_FP8_NAN_AS_INT = -128
weight_as_int8[weight_as_int8 == ROCM_FP8_NAN_AS_INT] = 0
weight = weight_as_int8.view(torch.float8_e4m3fnuz)
# For the same bits representation, e4m3fnuz value is half of
# the e4m3fn value, so we should double the scaling factor to
# get the same dequantized value.
# https://onnx.ai/onnx/technical/float8.html
weight_scale = weight_scale * 2.0
if input_scale is not None:
input_scale = input_scale * 2.0
return weight, weight_scale, input_scale
class Fp8GemmRunnerBackend(Enum):
"""Enum for FP8 GEMM runner backend selection."""
AUTO = "auto"
FLASHINFER_TRTLLM = "flashinfer_trtllm"
FLASHINFER_CUTLASS = "flashinfer_cutlass"
FLASHINFER_DEEPGEMM = "flashinfer_deepgemm"
CUTLASS = "cutlass"
DEEP_GEMM = "deep_gemm"
TRITON = "triton"
AITER = "aiter"
def is_auto(self) -> bool:
return self == Fp8GemmRunnerBackend.AUTO
def is_flashinfer_trtllm(self) -> bool:
return self == Fp8GemmRunnerBackend.FLASHINFER_TRTLLM
def is_flashinfer_cutlass(self) -> bool:
return self == Fp8GemmRunnerBackend.FLASHINFER_CUTLASS
def is_flashinfer_deepgemm(self) -> bool:
return self == Fp8GemmRunnerBackend.FLASHINFER_DEEPGEMM
def is_cutlass(self) -> bool:
return self == Fp8GemmRunnerBackend.CUTLASS
def is_deep_gemm(self) -> bool:
return self == Fp8GemmRunnerBackend.DEEP_GEMM
def is_triton(self) -> bool:
return self == Fp8GemmRunnerBackend.TRITON
def is_aiter(self) -> bool:
return self == Fp8GemmRunnerBackend.AITER
FP8_GEMM_RUNNER_BACKEND: Fp8GemmRunnerBackend | None = None
def _check_cutlass_block_fp8_hardware_support() -> bool:
"""Return True if CUTLASS block FP8 is supported (Hopper or newer with CUDA 12.0+)."""
return is_sm90_supported() or is_blackwell_supported()
if is_blackwell_supported() and is_flashinfer_available():
from flashinfer import SfLayout
from flashinfer import bmm_fp8 as _raw_flashinfer_bmm_fp8
from flashinfer import mm_mxfp8 as _raw_flashinfer_mm_mxfp8
from flashinfer import mxfp8_quantize as _raw_flashinfer_mxfp8_quantize
from flashinfer.gemm import gemm_fp8_nt_groupwise as _raw_gemm_fp8_nt_groupwise
from sglang.srt.utils.custom_op import register_custom_op
@register_custom_op(
op_name="flashinfer_bmm_fp8",
mutates_args=[],
fake_impl=lambda q_input, weight, x_scale, weight_scale, out_dtype: (
q_input.new_empty((q_input.shape[0], weight.shape[1]), dtype=out_dtype)
),
)
def flashinfer_bmm_fp8(
q_input: torch.Tensor, # [M, K] fp8 e4m3
weight: torch.Tensor, # [K, N] fp8 e4m3, column-major
x_scale: torch.Tensor, # per-tensor scalar
weight_scale: torch.Tensor, # per-tensor scalar
out_dtype: torch.dtype,
) -> torch.Tensor:
m, n = q_input.shape[0], weight.shape[1]
return _raw_flashinfer_bmm_fp8(
q_input.unsqueeze(0),
weight.unsqueeze(0),
x_scale.reshape(1),
weight_scale.reshape(1),
out_dtype,
backend="cublas",
).view(m, n)
@lru_cache(maxsize=1)
def _get_flashinfer_groupwise_backend() -> str:
if get_fp8_gemm_runner_backend().is_flashinfer_cutlass():
return "cutlass"
if get_fp8_gemm_runner_backend().is_flashinfer_trtllm():
return "trtllm"
major, minor = get_device_capability()
# SM120/121: CUTLASS only.
# SM100/103: TRTLLM only.
if major >= 12:
return "cutlass"
return "trtllm"
# Wrap gemm_fp8_nt_groupwise as a custom op so torch.compile does not trace
# into flashinfer's JIT compilation code (pathlib/cubin_loader ops).
@register_custom_op(
op_name="flashinfer_gemm_fp8_nt_groupwise",
mutates_args=[],
fake_impl=lambda q_input, weight, x_scale, weight_scale, out_dtype: (
q_input.new_empty((q_input.shape[0], weight.shape[0]), dtype=out_dtype)
),
)
def gemm_fp8_nt_groupwise(
q_input: torch.Tensor,
weight: torch.Tensor,
x_scale: torch.Tensor,
weight_scale: torch.Tensor,
out_dtype: torch.dtype,
) -> torch.Tensor:
backend = _get_flashinfer_groupwise_backend()
if backend == "cutlass":
# FlashInfer CUTLASS groupwise kernel requires contiguous scale tensors
x_scale = x_scale.contiguous()
weight_scale = weight_scale.contiguous()
return _raw_gemm_fp8_nt_groupwise(
q_input,
weight,
x_scale,
weight_scale,
out_dtype=out_dtype,
backend="cutlass",
scale_major_mode="MN",
)
return _raw_gemm_fp8_nt_groupwise(
q_input,
weight,
x_scale,
weight_scale,
out_dtype=out_dtype,
backend=backend,
)
# Wrap MXFP8 ops as custom ops so torch.compile does not trace into
# flashinfer's JIT compilation path (filesystem checks/cubin loader).
def _fake_flashinfer_mxfp8_quantize(
input: torch.Tensor,
_is_sf_swizzled_layout: bool = True,
alignment: int = 32,
) -> Tuple[torch.Tensor, torch.Tensor]:
# Fake mode only needs dtypes and output rank to propagate compile graph.
# The scale tensor shape is not consumed before the following fake mm op.
k_aligned = ((input.shape[1] + alignment - 1) // alignment) * alignment
q_input = input.new_empty(
(input.shape[0], k_aligned), dtype=torch.float8_e4m3fn
)
scale = input.new_empty((1,), dtype=torch.uint8)
return q_input, scale
@register_custom_op(
op_name="flashinfer_mxfp8_quantize",
mutates_args=[],
fake_impl=_fake_flashinfer_mxfp8_quantize,
)
def flashinfer_mxfp8_quantize(
input: torch.Tensor,
is_sf_swizzled_layout: bool = True,
alignment: int = 32,
) -> Tuple[torch.Tensor, torch.Tensor]:
return _raw_flashinfer_mxfp8_quantize(
input,
is_sf_swizzled_layout=is_sf_swizzled_layout,
alignment=alignment,
sf_swizzle_layout=SfLayout.layout_128x4,
)
@register_custom_op(
op_name="flashinfer_mm_mxfp8",
mutates_args=[],
fake_impl=lambda q_input, weight_t, x_scale_u8, weight_scale_t, out_dtype, use_8x4_sf_layout=False, backend="auto": (
q_input.new_empty((q_input.shape[0], weight_t.shape[1]), dtype=out_dtype)
),
)
def flashinfer_mm_mxfp8(
q_input: torch.Tensor,
weight_t: torch.Tensor,
x_scale_u8: torch.Tensor,
weight_scale_t: torch.Tensor,
out_dtype: torch.dtype,
use_8x4_sf_layout: bool = False,
backend: str = "auto",
) -> torch.Tensor:
return _raw_flashinfer_mm_mxfp8(
q_input,
weight_t,
x_scale_u8,
weight_scale_t,
out_dtype=out_dtype,
use_8x4_sf_layout=use_8x4_sf_layout,
backend=backend,
)
if is_sm90_supported() and is_flashinfer_available():
# FlashInfer SM90 DeepGEMM with automatic swapAB optimization for small M
from flashinfer.gemm import fp8_blockscale_gemm_sm90
def dispatch_w8a8_block_fp8_linear() -> Callable:
"""
Dispatch to the appropriate FP8 block linear implementation.
This function selects the backend based on:
1. The --fp8-gemm-backend server argument (preferred)
2. Auto-detection based on hardware capabilities
"""
backend = get_fp8_gemm_runner_backend()
# Handle explicit backend selection via --fp8-gemm-backend
if not backend.is_auto():
return _dispatch_explicit_backend(backend)
# Auto mode: Select based purely on hardware/backend availability
return _dispatch_auto_backend()
def dispatch_w8a8_mxfp8_linear() -> Callable:
backend = get_fp8_gemm_runner_backend()
if backend.is_deep_gemm():
return _deepgemm_w8a8_mxfp8_linear_with_fallback
elif backend.is_flashinfer_cutlass() or backend.is_flashinfer_trtllm():
return flashinfer_mxfp8_blockscaled_linear
elif backend.is_triton():
return triton_mxfp8_blockscaled_linear
elif _is_hip and _is_gfx95_supported:
from sglang.srt.layers.quantization.mxfp8_amd_gfx95 import (
dot_scaled_mxfp8_blockscaled_linear,
)
return dot_scaled_mxfp8_blockscaled_linear
return triton_mxfp8_blockscaled_linear
def _deepgemm_w8a8_mxfp8_linear_with_fallback(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
weight_scale_fallback: Optional[torch.Tensor] = None,
) -> torch.Tensor:
from sglang.srt.layers.quantization.fp8_kernel import (
sglang_per_token_group_quant_fp8,
w8a8_mxfp8_matmul_deepgemm,
)
assert input_scale is None
output_dtype = input.dtype
shape_supported = weight.shape[0] % 64 == 0 and weight.shape[1] % 128 == 0
dtype_supported = output_dtype == torch.bfloat16
if not (shape_supported and dtype_supported):
return triton_mxfp8_blockscaled_linear(
input, weight, weight_scale_fallback, input_scale, bias
)
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
q_input, x_scale = sglang_per_token_group_quant_fp8(
input_2d,
32,
column_major_scales=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
scale_tma_aligned=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
)
# weight_scale format is set per-backend by _process_mxfp8_linear_weight_scale
# (int32 packed TMA-aligned on Blackwell, float32 on Hopper); NOT uint8 — Triton form is routed to the fallback above.
output = w8a8_mxfp8_matmul_deepgemm(
q_input, weight, x_scale, weight_scale, output_dtype=output_dtype
)
if bias is not None:
output += bias
return output.to(dtype=output_dtype).view(*output_shape)
def _dispatch_explicit_backend(backend: Fp8GemmRunnerBackend) -> Callable:
"""Dispatch based on explicitly selected backend."""
if backend.is_flashinfer_trtllm():
if not (is_sm100_supported() and is_flashinfer_available()):
raise RuntimeError(
"FlashInfer FP8 GEMM requested via --fp8-gemm-backend=flashinfer_trtllm, "
"but FlashInfer is not available or not supported on this hardware. "
"FlashInfer TRTLLM FP8 GEMM requires SM100/SM103 GPUs and FlashInfer."
)
return flashinfer_gemm_w8a8_block_fp8_linear_with_fallback
elif backend.is_flashinfer_cutlass():
if not (is_blackwell_supported() and is_flashinfer_available()):
raise RuntimeError(
"FlashInfer FP8 GEMM requested via --fp8-gemm-backend=flashinfer_cutlass, "
"but FlashInfer is not available or not supported on this hardware. "
"FlashInfer CUTLASS FP8 GEMM requires Blackwell GPUs and FlashInfer."
)
return flashinfer_gemm_w8a8_block_fp8_linear_with_fallback
elif backend.is_flashinfer_deepgemm():
if not (is_sm90_supported() and is_flashinfer_available()):
raise RuntimeError(
"FlashInfer DeepGEMM with swapAB requested via --fp8-gemm-backend=flashinfer_deepgemm, "
"but it's not available. This backend requires Hopper (SM90) GPUs and FlashInfer "
"to be installed."
)
return flashinfer_deepgemm_w8a8_block_fp8_linear_with_fallback
elif backend.is_cutlass():
if not _check_cutlass_block_fp8_hardware_support():
raise RuntimeError(
"CUTLASS block FP8 requested via --fp8-gemm-backend=cutlass, "
"but hardware does not support it. CUTLASS block FP8 requires "
"Hopper (SM90+) GPUs with CUDA 12.0+."
)
return cutlass_w8a8_block_fp8_linear_with_fallback
elif backend.is_aiter():
if not _use_aiter:
raise RuntimeError(
"AITER backend requested via --fp8-gemm-backend=aiter, "
"but AITER is not available. AITER requires AMD GPUs with "
"SGLANG_USE_AITER=1 environment variable set."
)
return aiter_w8a8_block_fp8_linear
elif backend.is_deep_gemm():
if not deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM:
raise RuntimeError(
"DeepGEMM backend requested via --fp8-gemm-backend=deep_gemm, "
"but DeepGEMM is not available. This usually means the deep_gemm package "
"is not installed or has been disabled via SGLANG_ENABLE_JIT_DEEPGEMM=0."
)
return deepgemm_w8a8_block_fp8_linear_with_fallback
elif backend.is_triton():
return triton_w8a8_block_fp8_linear
else:
raise ValueError(f"Unknown FP8 GEMM backend: {backend}")
def _dispatch_auto_backend() -> Callable:
"""Auto-select the best backend based on hardware capabilities."""
# Priority order for auto selection:
# 1. DeepGEMM (if enabled and available)
# 2. FlashInfer TRTLLM (if Blackwell GPU and FlashInfer available)
# 3. CUTLASS (if Hopper+ GPU and CUDA 12.0+)
# 4. AITER (if AMD GPU with AITER enabled)
# 5. Triton (fallback)
if deep_gemm_wrapper.ENABLE_JIT_DEEPGEMM:
return deepgemm_w8a8_block_fp8_linear_with_fallback
elif is_blackwell_supported() and is_flashinfer_available():
return flashinfer_gemm_w8a8_block_fp8_linear_with_fallback
elif _check_cutlass_block_fp8_hardware_support():
return cutlass_w8a8_block_fp8_linear_with_fallback
elif _use_aiter:
return aiter_w8a8_block_fp8_linear
else:
return triton_w8a8_block_fp8_linear
def initialize_fp8_gemm_config(server_args: ServerArgs) -> None:
"""Initialize FP8 GEMM configuration."""
global FP8_GEMM_RUNNER_BACKEND
backend = server_args.fp8_gemm_runner_backend
if backend == "auto" and is_sm120_supported():
# TODO(brayden): Verify if CUTLASS can be set by default once SwapAB is supported
backend = "triton"
backend = Fp8GemmRunnerBackend(backend)
if (
backend.is_auto()
and server_args.quantization == "mxfp8"
and _is_sm100_supported
and is_flashinfer_available()
):
backend = Fp8GemmRunnerBackend.FLASHINFER_CUTLASS
FP8_GEMM_RUNNER_BACKEND = backend
def get_fp8_gemm_runner_backend() -> Fp8GemmRunnerBackend:
"""Get the current FP8 GEMM runner backend."""
global FP8_GEMM_RUNNER_BACKEND
if FP8_GEMM_RUNNER_BACKEND is None:
FP8_GEMM_RUNNER_BACKEND = Fp8GemmRunnerBackend.AUTO
return FP8_GEMM_RUNNER_BACKEND
def flashinfer_gemm_w8a8_block_fp8_linear_with_fallback(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
assert input_scale is None
input_2d = input.view(-1, input.shape[-1])
backend = _get_flashinfer_groupwise_backend()
# Fall back to triton for non-supported formats.
# TODO: Check if flashinfer supports other output dtypes besides bf16.
if backend == "trtllm" and (
input_2d.shape[1] < 256 or input_2d.dtype != torch.bfloat16
):
return triton_w8a8_block_fp8_linear(
input, weight, block_size, weight_scale, input_scale, bias
)
output_shape = [*input.shape[:-1], weight.shape[0]]
# TRTLLM uses the existing SGLang column-major scale layout.
# CUTLASS with scale_major_mode="MN" expects (k//block_k, m), so we normalize below.
q_input, x_scale = sglang_per_token_group_quant_fp8(
input_2d, block_size[1], column_major_scales=(backend == "trtllm")
)
if backend == "cutlass":
block_n, block_k = block_size
m, k = input_2d.shape
n = weight.shape[0]
expected_x_scale_shape = (k // block_k, m)
expected_weight_scale_shape = (k // block_k, n // block_n)
if x_scale.shape == (m, k // block_k):
x_scale = x_scale.transpose(-1, -2).contiguous()
if weight_scale.shape == (n // block_n, k // block_k):
weight_scale = weight_scale.transpose(-1, -2).contiguous()
assert x_scale.shape == expected_x_scale_shape, (
"FlashInfer CUTLASS groupwise FP8 expects A scale layout "
f"(k//block_k, m) for scale_major_mode='MN', got {tuple(x_scale.shape)}; "
f"expected {expected_x_scale_shape}. "
f"strides={x_scale.stride()} is_contiguous={x_scale.is_contiguous()} "
f"m={m} n={n} k={k} block_size={block_size}"
)
assert weight_scale.shape == expected_weight_scale_shape, (
"FlashInfer CUTLASS groupwise FP8 expects B scale layout "
f"(k//block_k, n//block_n) for scale_major_mode='MN', got {tuple(weight_scale.shape)}; "
f"expected {expected_weight_scale_shape}. "
f"strides={weight_scale.stride()} is_contiguous={weight_scale.is_contiguous()} "
f"m={m} n={n} k={k} block_size={block_size}"
)
assert x_scale.dtype == torch.float32, (
"FlashInfer CUTLASS groupwise FP8 expects x_scale dtype float32, "
f"got {x_scale.dtype}."
)
assert weight_scale.dtype == torch.float32, (
"FlashInfer CUTLASS groupwise FP8 expects weight_scale dtype float32, "
f"got {weight_scale.dtype}."
)
# TRTLLM path continues using the original quantized scale layout.
output = gemm_fp8_nt_groupwise(
q_input,
weight,
x_scale,
weight_scale,
out_dtype=input_2d.dtype,
)
if bias is not None:
output += bias
return output.to(dtype=input_2d.dtype).view(*output_shape)
def flashinfer_deepgemm_w8a8_block_fp8_linear_with_fallback(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""
FlashInfer DeepGEMM backend for SM90 (Hopper) with swapAB optimization.
Uses flashinfer.gemm.fp8_blockscale_gemm_sm90 which automatically selects
the swapAB kernel for small M dimensions (M < 32) for better performance
during decoding/low batch size scenarios.
For SM90 (Hopper), this uses the DeepGEMM JIT with automatic swapAB selection.
"""
assert input_scale is None
output_dtype = input.dtype
dtype_supported = output_dtype == torch.bfloat16
# fp8_blockscale_gemm_sm90 requires: N % 64 == 0, K % 128 == 0
shape_supported = weight.shape[0] % 64 == 0 and weight.shape[1] % 128 == 0
if not (shape_supported and dtype_supported):
if weight_scale.dtype == torch.int32:
weight_scale = _unpack_ue8m0_scale_for_triton(
weight_scale, weight.shape, block_size
)
return triton_w8a8_block_fp8_linear(
input, weight, block_size, weight_scale, input_scale, bias
)
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
# - input: (M, K) BF16 or FP8
# - weight: (N, K) FP8 with weight_scale
# - weight_scale: (N, K//128) for per-token or (N//128, K//128) for per-block
output = fp8_blockscale_gemm_sm90(
input_2d,
weight,
input_scale=None, # BF16 input, internal quantization
weight_scale=weight_scale,
out_dtype=output_dtype,
)
if bias is not None:
output += bias
return output.view(*output_shape)
def cutlass_w8a8_block_fp8_linear_with_fallback(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
assert input_scale is None
# TODO: add more robust shape check here
shape_supported = weight.shape[0] % 128 == 0 and weight.shape[1] % 128 == 0
if not shape_supported:
# fallback to triton
return triton_w8a8_block_fp8_linear(
input, weight, block_size, weight_scale, input_scale, bias
)
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
# Quantize into row-padded buffers so the sgl-kernel wrapper's per-call
# pad_tensor() on mat_a / scales_a short-circuits (saves 2x fill + 2x cat
# kernels per GEMM). weight_scale.T is left as a K-major view because the
# kernel requires scales_b.stride(0) == 1 and materializes it internally.
q_input, x_scale = sglang_per_token_group_quant_fp8_row_padded(
input_2d, block_size[1]
)
output = fp8_blockwise_scaled_mm(
q_input, weight.T, x_scale, weight_scale.T, out_dtype=input_2d.dtype
)
if output.shape[0] != input_2d.shape[0]:
# GEMM ran on the row-padded buffer; drop the padding rows.
output = output[: input_2d.shape[0]]
if bias is not None:
output += bias
return output.to(dtype=input_2d.dtype).view(*output_shape)
def deepgemm_w8a8_block_fp8_linear_with_fallback(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
assert input_scale is None
output_dtype = input.dtype
dtype_supported = output_dtype == torch.bfloat16
# TODO: https://github.com/sgl-project/sglang/pull/6890#issuecomment-2943395737
shape_supported = weight.shape[0] % 64 == 0 and weight.shape[1] % 128 == 0
if not (shape_supported and dtype_supported):
# fall back to triton
# If weight_scale is in UE8M0 packed format (int32), convert back to float32
# UE8M0 format has shape (N, K//block_k//4) with dtype int32
# Triton expects shape (N//block_n, K//block_k) with dtype float32
if weight_scale.dtype == torch.int32:
weight_scale = _unpack_ue8m0_scale_for_triton(
weight_scale, weight.shape, block_size
)
return triton_w8a8_block_fp8_linear(
input, weight, block_size, weight_scale, input_scale, bias
)
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
if not _is_musa:
q_input, x_scale = sglang_per_token_group_quant_fp8(
input_2d,
block_size[1],
column_major_scales=True,
scale_tma_aligned=True,
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
)
else:
q_input, x_scale = sglang_per_token_group_quant_fp8(
input_2d,
block_size[1],
)
output = w8a8_block_fp8_matmul_deepgemm(
q_input, weight, x_scale, weight_scale, block_size, output_dtype=output_dtype
)
if bias is not None:
output += bias
return output.to(dtype=output_dtype).view(*output_shape)
def _unpack_ue8m0_scale_for_triton(
sf_packed: torch.Tensor,
weight_shape: Tuple[int, int],
block_size: List[int],
) -> torch.Tensor:
"""
Unpack UE8M0 packed scale tensor back to float32 format for triton kernel.
The UE8M0 format packs scales as:
- Shape: (N, K//block_k//4) with dtype int32
- Each int32 contains 4 uint8 scale values
Triton expects:
- Shape: (N//block_n, K//block_k) with dtype float32
Args:
sf_packed: Packed scale tensor with shape (N, packed_k_groups) and dtype int32
weight_shape: (N, K) shape of the weight tensor
block_size: [block_n, block_k] quantization block size
Returns:
Unpacked scale tensor with shape (n_groups, k_groups) and dtype float32
"""
assert sf_packed.dtype == torch.int32
assert len(sf_packed.shape) == 2
N, K = weight_shape
block_n, block_k = block_size
n_groups = ceil_div(N, block_n)
k_groups = ceil_div(K, block_k)
mn_repeat, k_div_4 = sf_packed.shape
k_packed = k_div_4 * 4
# Unpack int32 -> 4x uint8 -> float32
# Each uint8 represents an exponent in UE8M0 format
sf_u8 = sf_packed.contiguous().view(torch.uint8).view(mn_repeat, k_packed)
sf_fp32 = (sf_u8.to(torch.int32) << 23).view(torch.float32)
# Handle row dimension - may have 128x replication or direct mapping
if mn_repeat == N:
# Rows are replicated 128 times, take every 128th row
# sf_fp32 shape: (N, k_packed) -> (n_groups, k_packed)
# Select representative rows at indices 0, 128, 256, ...
indices = torch.arange(0, N, block_n, device=sf_packed.device)
sf_fp32 = sf_fp32.index_select(0, indices)
elif mn_repeat == n_groups:
# Already in the correct n_groups format
pass
else:
raise ValueError(
f"Unexpected scale shape: sf_packed.shape={sf_packed.shape}, "
f"weight_shape={weight_shape}, block_size={block_size}"
)
# Crop k dimension to expected size (remove padding if any)
sf_fp32 = sf_fp32[:, :k_groups].contiguous()
return sf_fp32
def aiter_w8a8_block_fp8_linear(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
# assert input_scale is None
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
n, k = weight.shape
if _use_aiter_bpreshuffle_gfx95:
use_triton = use_aiter_triton_gemm_w8a8_tuned_gfx950(n, k)
elif _use_aiter_gfx95:
# gfx95 on ROCm < 7.2: keep the (faster) CK path at/below the per-shape
# CK-safe M bound; above it, ck_gemm_a8w8_blockscale returns NaN, so use
# Triton. Unlisted shapes keep their original decision. Fixed in ROCm 7.2.
_ck_safe_m = _AITER_GFX95_CK_W8A8_MAX_SAFE_M.get((n, k))
use_triton = use_aiter_triton_gemm_w8a8_tuned_gfx950(n, k) or (
_ck_safe_m is not None and input_2d.shape[0] > _ck_safe_m
)
else:
use_triton = True
# if input_scale not None, input is quanted
if input_scale is not None:
q_input = input_2d
x_scale = input_scale
if _use_aiter_bpreshuffle_gfx95 and not use_triton:
x_scale = materialize_bpreshuffle_fp8_scale(x_scale)
# On ROCm >= 7.2, scale is in bpreshuffle's transposed layout.
# Triton needs a row-major view, so adjust strides only. No copy.
elif use_triton and _use_aiter_bpreshuffle_gfx95:
x_scale = torch.as_strided(x_scale, x_scale.shape, (1, x_scale.shape[0]))
else:
materialize_bpreshuffle_scale = _use_aiter_bpreshuffle_gfx95 and not use_triton
q_input, x_scale = aiter_per1x128_quant(
input_2d,
quant_dtype=aiter.dtypes.fp8,
transpose_scale=False,
)
if materialize_bpreshuffle_scale:
x_scale = materialize_bpreshuffle_fp8_scale(x_scale)
if use_triton:
gemm_a8w8_blockscale_op = triton_gemm_a8w8_blockscale
elif _use_aiter_bpreshuffle_gfx95:
gemm_a8w8_blockscale_op = gemm_a8w8_blockscale_bpreshuffle
else:
gemm_a8w8_blockscale_op = ck_gemm_a8w8_blockscale
output = gemm_a8w8_blockscale_op(
q_input,
weight,
x_scale,
weight_scale,
dtype=torch.bfloat16 if input_scale is not None else input.dtype,
)
if bias is not None:
output += bias
return output.to(
dtype=torch.bfloat16 if input_scale is not None else input_2d.dtype
).view(*output_shape)
def triton_w8a8_block_fp8_linear(
input: torch.Tensor,
weight: torch.Tensor,
block_size: List[int],
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
assert input_scale is None
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[0]]
q_input, x_scale = per_token_group_quant_fp8(
input_2d, block_size[1], column_major_scales=False
)
output = w8a8_block_fp8_matmul_triton(
q_input, weight, x_scale, weight_scale, block_size, output_dtype=input_2d.dtype
)
if bias is not None:
output += bias
return output.to(dtype=input_2d.dtype).view(*output_shape)
@lru_cache(maxsize=1)
def _get_triton_mxfp8_downcast():
try:
from triton_kernels.numerics_details.mxfp import downcast_to_mxfp
except Exception as err:
raise RuntimeError(
"MXFP8 quantization requires triton_kernels with MXFP8 support."
) from err
return downcast_to_mxfp
def mxfp8_group_quantize(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""Quantize a 2D contiguous tensor to MXFP8 with UE8M0 scales per group (32)."""
assert x.dim() == 2, f"Expected 2D input, got {x.dim()}D"
assert x.is_contiguous(), "MXFP8 quantization requires a contiguous 2D tensor."
_, k = x.shape
assert k % 32 == 0, f"{k=} must be divisible by 32"
downcast_to_mxfp = _get_triton_mxfp8_downcast()
q_input, scale_u8 = downcast_to_mxfp(x, torch.float8_e4m3fn, axis=1)
return q_input.contiguous(), scale_u8.contiguous()
def _pack_mxfp8_scales(scale_u8: torch.Tensor) -> torch.Tensor:
if (
_is_hip
and _is_gfx95_supported
and scale_u8.is_cuda
and scale_u8.shape[0] % 128 != 0
):
return pack_mxfp8_scales_triton(scale_u8)
# Pack (M, K//32) UE8M0 scales into the layout expected by tl.dot_scaled.
assert scale_u8.dim() == 2, f"Expected 2D scale tensor, got {scale_u8.dim()}D"
scale_u8 = scale_u8.contiguous()
m, k_groups = scale_u8.shape
assert (
k_groups % 4 == 0
), f"{k_groups=} must be divisible by 4 (K must be multiple of 128)"
scale_m = ceil_div(m, 128)
if m % 128 != 0:
pad_rows = scale_m * 128 - m
pad = torch.full(
(pad_rows, k_groups),
127,
dtype=scale_u8.dtype,
device=scale_u8.device,
)
scale_u8 = torch.cat([scale_u8, pad], dim=0)
scale_k = k_groups // 4
scale_u8 = scale_u8.view(scale_m, 128, scale_k, 4)
scale_u8 = scale_u8.view(scale_m, 4, 32, scale_k, 4)
packed = scale_u8.permute(0, 3, 2, 1, 4).contiguous()
return packed.view(1, scale_m, scale_k, 2, 256)
@register_custom_op(
op_name="triton_mxfp8_block_scaled_matmul",
mutates_args=[],
fake_impl=lambda a, a_scale, b, b_scale, output_dtype, block_m=128, block_n=256, block_k=128, num_stages=None: ( # noqa: E501
a.new_empty((a.shape[0], b.shape[0]), dtype=output_dtype)
),
)
def triton_mxfp8_block_scaled_matmul(
a: torch.Tensor,
a_scale: torch.Tensor,
b: torch.Tensor,
b_scale: torch.Tensor,
output_dtype: torch.dtype,
*,
block_m: int = 128,
block_n: int = 256,
block_k: int = 128,
num_stages: Optional[int] = None,
) -> torch.Tensor:
"""Opaque custom op wrapper to prevent Dynamo tracing Triton grid math."""
return mxfp8_block_scaled_matmul_triton(
a,
a_scale,
b,
b_scale,
output_dtype=output_dtype,
block_m=block_m,
block_n=block_n,
block_k=block_k,
num_stages=num_stages,
)
def _raw_triton_mxfp8_blockscaled_linear(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
output_dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
if not (
(_is_cuda and (_is_sm100_supported or _is_sm120_supported))
or (_is_hip and _is_gfx95_supported)
):
raise RuntimeError(
"MXFP8 dense linear requires Blackwell GPUs (SM100/SM120) or ROCm gfx95."
)
input_2d = input.view(-1, input.shape[-1]).contiguous()
output_shape = [*input.shape[:-1], weight.shape[0]]
block_m = 128
block_n = 256 if weight.shape[0] % 256 == 0 else 128
block_k = 128
m, k = input_2d.shape
n, k_w = weight.shape
assert k == k_w, f"{k=} does not match {k_w=}"
assert k % 128 == 0, f"{k=} must be divisible by 128 for MXFP8"
assert n % block_n == 0, f"{n=} must be divisible by {block_n}"
assert weight.dtype == torch.float8_e4m3fn, "MXFP8 weight must be FP8 E4M3."
assert weight_scale.dtype == torch.uint8, "MXFP8 weight_scale must be UE8M0 uint8."
assert weight_scale.dim() in (
2,
5,
), (
"MXFP8 weight_scale must be canonical 2D or packed 5D, "
f"got {weight_scale.dim()}D."
)
if input_scale is None:
q_input, x_scale_u8 = mxfp8_group_quantize(input_2d)
else:
q_input = input_2d
x_scale_u8 = input_scale
assert x_scale_u8.dtype == torch.uint8, "MXFP8 input_scale must be UE8M0 uint8."
assert x_scale_u8.shape == (m, k // 32)
if output_dtype is None:
if input_2d.dtype in (torch.float16, torch.bfloat16, torch.float32):
output_dtype = input_2d.dtype
else:
output_dtype = torch.bfloat16
if m % block_m != 0:
pad_rows = ceil_div(m, block_m) * block_m - m
q_input = torch.cat(
[
q_input,
torch.zeros((pad_rows, k), device=q_input.device, dtype=q_input.dtype),
],
dim=0,
)
pad_scale = torch.full(
(pad_rows, k // 32),
127,
device=x_scale_u8.device,
dtype=x_scale_u8.dtype,
)
x_scale_u8 = torch.cat([x_scale_u8, pad_scale], dim=0)
a_scale_packed = _pack_mxfp8_scales(x_scale_u8)
b_scale_packed = (
weight_scale.contiguous()
if weight_scale.dim() == 5
else _pack_mxfp8_scales(weight_scale)
)
num_stages = 1 if _is_sm120_supported else (4 if _is_sm100_supported else 1)
output = triton_mxfp8_block_scaled_matmul(
q_input,
a_scale_packed,
weight.contiguous(),
b_scale_packed,
output_dtype=output_dtype,
block_m=block_m,
block_n=block_n,
block_k=block_k,
num_stages=num_stages,
)
output = output[:m, :]
if bias is not None:
output += bias
return output.to(dtype=output_dtype).view(*output_shape)
@register_custom_op(
op_name="triton_mxfp8_blockscaled_linear",
mutates_args=[],
fake_impl=lambda input, weight, weight_scale, input_scale=None, bias=None, output_dtype=None: (
input.new_empty(
(*input.shape[:-1], weight.shape[0]),
dtype=(output_dtype if output_dtype is not None else input.dtype),
)
),
)
def triton_mxfp8_blockscaled_linear(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
output_dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
"""Opaque custom-op wrapper to prevent Dynamo guards on MXFP8 padding branches."""
return _raw_triton_mxfp8_blockscaled_linear(
input=input,
weight=weight,
weight_scale=weight_scale,
input_scale=input_scale,
bias=bias,
output_dtype=output_dtype,
)
def flashinfer_mxfp8_blockscaled_linear(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
output_dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
"""MXFP8 dense linear via FlashInfer mm_mxfp8."""
input_2d = input.view(-1, input.shape[-1]).contiguous()
output_shape = [*input.shape[:-1], weight.shape[0]]
m, k = input_2d.shape
n, k_w = weight.shape
if k != k_w:
raise ValueError(f"Input K={k} does not match weight K={k_w}.")
if k % 32 != 0:
raise ValueError(f"K={k} must be divisible by 32 for MXFP8.")
if weight.dtype != torch.float8_e4m3fn:
raise TypeError("MXFP8 weight must be FP8 E4M3.")
if input_scale is None:
q_input, x_scale_u8 = flashinfer_mxfp8_quantize(
input_2d, is_sf_swizzled_layout=True, alignment=32
)
else:
q_input = input_2d
x_scale_u8 = input_scale.contiguous()
if output_dtype is None:
if input_2d.dtype in (torch.float16, torch.bfloat16, torch.float32):
output_dtype = input_2d.dtype
else:
output_dtype = torch.bfloat16
# Ensure transposed tensors are contiguous for FlashInfer's internal runner.
weight_t = weight.contiguous().t()
if get_fp8_gemm_runner_backend().is_flashinfer_trtllm():
weight_scale_t = weight_scale.contiguous().view(-1)
output = flashinfer_mm_mxfp8(
q_input,
weight_t,
x_scale_u8,
weight_scale_t,
out_dtype=output_dtype,
use_8x4_sf_layout=False,
backend="trtllm",
)
elif get_fp8_gemm_runner_backend().is_flashinfer_cutlass():
weight_scale_t = (
weight_scale.contiguous().t()
if weight_scale.ndim == 2
else weight_scale.contiguous()
)
output = flashinfer_mm_mxfp8(
q_input,
weight_t,
x_scale_u8,
weight_scale_t,
out_dtype=output_dtype,
use_8x4_sf_layout=False,
backend="cutlass",
)
if bias is not None:
output += bias
return output.to(dtype=output_dtype).view(*output_shape)
def dequant_mxfp4(
w_block: torch.Tensor,
w_scale: torch.Tensor,
out_dtype,
) -> torch.Tensor:
"""
:param w_block: (batch, n, k, 16), uint8, pack two mxfp4 into one byte
:param w_scale: (batch, n, k), uint8
:return: (batch, n, k * 32), float32
"""
assert w_block.dtype == torch.uint8
assert w_scale.dtype == torch.uint8
batch, n, k, pack_dim = w_block.shape
batch_, n_, k_ = w_scale.shape
assert pack_dim == 16
assert batch == batch_
assert n == n_
assert k == k_
out_raw = MXFP4QuantizeUtil.dequantize(
quantized_data=w_block, scale=w_scale, dtype=out_dtype, block_sizes=[32]
)
return out_raw.reshape(batch, n, k * 32)
def input_to_float8(
x: torch.Tensor, dtype: torch.dtype = fp8_dtype
) -> Tuple[torch.Tensor, torch.Tensor]:
"""This function quantizes input values to float8 values with tensor-wise quantization."""
min_val, max_val = x.aminmax()
amax = torch.maximum(min_val.abs(), max_val.abs()).float().clamp(min=1e-12)
if _is_fp8_fnuz:
dtype = fp8_dtype
fp_max = fp8_max
else:
finfo = torch.finfo(dtype)
fp_max = finfo.max
scale = fp_max / amax
x_scl_sat = (x.float() * scale).clamp(min=-fp_max, max=fp_max)
return x_scl_sat.to(dtype).contiguous(), scale.float().reciprocal()
def block_quant_to_tensor_quant(
x_q_block: torch.Tensor,
x_s: torch.Tensor,
block_size: List[int],
) -> Tuple[torch.Tensor, torch.Tensor]:
"""This function converts block-wise quantization to tensor-wise quantization.
The inputs are block-wise quantization tensor `x_q_block`, block-wise quantization scale
and the block size.
The outputs are tensor-wise quantization tensor and tensor-wise quantization scale.
Note only float8 is supported for now.
"""
block_n, block_k = block_size[0], block_size[1]
n, k = x_q_block.shape
n_tiles = (n + block_n - 1) // block_n
k_tiles = (k + block_k - 1) // block_k
assert n_tiles == x_s.shape[0]
assert k_tiles == x_s.shape[1]
x_dq_block = x_q_block.to(torch.float32)
x_dq_block_tiles = [
[
x_dq_block[
j * block_n : min((j + 1) * block_n, n),
i * block_k : min((i + 1) * block_k, k),
]
for i in range(k_tiles)
]
for j in range(n_tiles)
]
for i in range(k_tiles):
for j in range(n_tiles):
x_dq_block_tiles[j][i][:, :] = x_dq_block_tiles[j][i] * x_s[j][i]
x_q_tensor, scale = (
scaled_fp8_quant(x_dq_block)
if _is_cuda
else input_to_float8(x_dq_block, dtype=x_q_block.dtype)
)
return x_q_tensor, scale
def block_quant_dequant(
x_q_block: torch.Tensor,
x_s: torch.Tensor,
block_size: List[int],
dtype: torch.dtype,
) -> torch.Tensor:
"""This function converts block-wise quantization to unquantized.
The inputs are block-wise quantization tensor `x_q_block`, block-wise quantization scale
and the block size.
The output is an unquantized tensor with dtype.
"""
block_n, block_k = block_size[0], block_size[1]
*_, n, k = x_q_block.shape
# ... n_scale k_scale -> ... (n_scale block_n) (k_scale block_k)
x_scale_repeat = x_s.repeat_interleave(block_n, dim=-2).repeat_interleave(
block_k, dim=-1
)
x_scale_repeat = x_scale_repeat[..., :n, :k]
return (x_q_block.to(torch.float32) * x_scale_repeat).to(dtype)
def quantize_block_fp8_weight_to_mxfp4(
fp8_weight: torch.Tensor,
fp8_scale: torch.Tensor,
weight_block_size: List[int],
mxfp4_block_size: int = 32,
) -> Tuple[torch.Tensor, torch.Tensor]:
fp8_weight_dequant = block_quant_dequant(
fp8_weight,
fp8_scale.to(torch.float32),
weight_block_size,
torch.bfloat16,
)
fp4_weight, fp4_scale = _MXFP4QuantizedData.quantize(
fp8_weight_dequant, block_size=mxfp4_block_size
)
fp4_weight = fp4_weight.quantized_data
fp4_weight = fp4_weight.contiguous().view(torch.int8)
fp4_scale = fp4_scale.view(
*fp8_weight_dequant.shape[:-1],
fp8_weight_dequant.shape[-1] // mxfp4_block_size,
)
return fp4_weight, fp4_scale.contiguous().view(torch.float8_e8m0fnu)
def requant_weight_ue8m0_inplace(weight, weight_scale_inv, weight_block_size):
assert isinstance(weight, torch.nn.Parameter)
assert isinstance(weight_scale_inv, torch.nn.Parameter)
new_weight, new_weight_scale_inv = requant_weight_ue8m0(
weight.to(weight_scale_inv.device), weight_scale_inv, weight_block_size
)
offloader.update_param(weight, new_weight)
weight_scale_inv.data = new_weight_scale_inv
def requant_block_scale_ue8m0_for_deepgemm(
weight: torch.nn.Parameter,
weight_scale: torch.nn.Parameter,
weight_block_size: Optional[List[int]],
use_deepgemm_runner: bool,
output_dtype: Optional[torch.dtype] = None,
weight_shape=None,
) -> bool:
"""Requantize block-FP8 weight scales to UE8M0 in place for DeepGEMM.
No-op (returns False) unless the caller selected the DeepGEMM runner, the
block size is 128x128 (the only layout the requant kernel supports), the
scales are not already UE8M0, and DeepGEMM can run the layer (bf16 output,
aligned shape). Returns True when it requantizes.
"""
from sglang.srt.model_loader.utils import (
should_deepgemm_weight_requant_ue8m0,
)
if (
not use_deepgemm_runner
or weight_block_size != [128, 128]
or getattr(weight_scale, "format_ue8m0", False)
or not should_deepgemm_weight_requant_ue8m0(
weight_block_size=weight_block_size,
output_dtype=output_dtype,
weight_shape=weight_shape,
)
):
return False
requant_weight_ue8m0_inplace(weight, weight_scale, weight_block_size)
weight_scale.format_ue8m0 = True
return True
def requant_weight_ue8m0(
weight: torch.Tensor,
weight_scale_inv: torch.Tensor,
weight_block_size: List[int],
):
assert weight_block_size == [128, 128]
*_, n, k = weight.shape
weight_dequant = block_quant_dequant(
weight,
weight_scale_inv,
weight_block_size,
torch.bfloat16,
)
out_w, out_s = quant_weight_ue8m0(
weight_dequant=weight_dequant,
weight_block_size=weight_block_size,
)
out_s = transform_scale_ue8m0(out_s, mn=out_w.shape[-2])
return out_w, out_s
def quant_weight_ue8m0(
weight_dequant: torch.Tensor,
weight_block_size: List[int],
):
assert weight_block_size == [128, 128]
assert (
weight_dequant.dtype == torch.bfloat16
), f"{weight_dequant.dtype=} {weight_dequant.shape=}"
*batch_dims, n, k = weight_dequant.shape
weight_dequant_flat = weight_dequant.view((-1, k))
out_w_flat, out_s_flat = per_block_cast_to_fp8(weight_dequant_flat)
out_w = out_w_flat.view((*batch_dims, n, k))
out_s = out_s_flat.view(
(
*batch_dims,
ceil_div(n, weight_block_size[0]),
ceil_div(k, weight_block_size[1]),
)
)
return out_w, out_s
def transform_scale_ue8m0_inplace(param, mn):
param.data = transform_scale_ue8m0(param.data, mn=mn)
# NOTE copy and modified from DeepGEMM
def transform_scale_ue8m0(sf, mn, use_torch_impl: bool = False):
import deep_gemm.utils.layout
get_mn_major_tma_aligned_packed_ue8m0_tensor = (
_get_mn_major_tma_aligned_packed_ue8m0_tensor_torch_impl
if use_torch_impl
else deep_gemm.utils.layout.get_mn_major_tma_aligned_packed_ue8m0_tensor
)
sf = sf.index_select(-2, torch.arange(mn, device=sf.device) // 128)
sf = get_mn_major_tma_aligned_packed_ue8m0_tensor(sf)
# In sgl-deep-gemm, the C++ deepgemm path returns through DLPack which collapses the stride
# of size-1 trailing dims to 1 (happens when packed_sf_k == 1, i.e.
# K <= block_k * 4). Restore the TMA-aligned stride so the deepgemm
# assertion sf.stride(-1) == get_tma_aligned_size(mn, element_size) holds.
if not use_torch_impl and sf.shape[-1] == 1:
from deep_gemm.utils import get_tma_aligned_size
aligned_mn = get_tma_aligned_size(sf.shape[-2], sf.element_size())
if sf.stride(-1) != aligned_mn:
new_stride = list(sf.stride())
new_stride[-1] = aligned_mn
sf = sf.as_strided(sf.shape, tuple(new_stride))
return sf
# Copied from DeepGEMM tests
def _get_mn_major_tma_aligned_packed_ue8m0_tensor_torch_impl(
x: torch.Tensor,
) -> torch.Tensor:
from deep_gemm.utils import align, get_tma_aligned_size
assert x.dtype == torch.float and x.dim() in (2, 3)
# First, convert into UE8M0 `uint8_t`
ue8m0_tensor = (x.view(torch.int) >> 23).to(torch.uint8)
# Second, make padded packed tensors
mn, k = x.shape[-2], x.shape[-1]
remove_dim = False
if x.dim() == 2:
x, remove_dim = x.unsqueeze(0), True
b = x.shape[0]
aligned_mn = get_tma_aligned_size(mn, 4)
aligned_k = align(k, 4)
padded = torch.zeros((b, aligned_mn, aligned_k), device=x.device, dtype=torch.uint8)
padded[:, :mn, :k] = ue8m0_tensor
padded = padded.view(-1).view(dtype=torch.int).view(b, aligned_mn, aligned_k // 4)
# Finally, transpose
transposed = torch.zeros(
(b, aligned_k // 4, aligned_mn), device=x.device, dtype=torch.int
).mT
transposed[:, :, :] = padded
aligned_x = transposed[:, :mn, :]
return aligned_x.squeeze(0) if remove_dim else aligned_x
def inverse_transform_scale_ue8m0(sf_packed, mn):
sf_fp32 = _inverse_transform_scale_ue8m0_impl(sf_packed)
# Can call consistency check every time since this is only called on startup
sf_packed_recreated = transform_scale_ue8m0(sf_fp32, mn=mn, use_torch_impl=True)
assert torch.all(
sf_packed == sf_packed_recreated
), f"{sf_packed=} {sf_packed_recreated=} {sf_fp32=}"
return sf_fp32
# Inverse impl can refer to DeepGEMM's torch impl in get_mn_major_tma_aligned_packed_ue8m0_tensor_torch_impl
def _inverse_transform_scale_ue8m0_impl(sf_packed):
"""
NOTE: We assume k is aligned
:param sf_packed: (scale_mn, scale_k/4) int32
:return: (scale_mn, scale_k), float32
"""
if len(sf_packed.shape) == 3:
return torch.stack(
[_inverse_transform_scale_ue8m0_impl(x) for x in sf_packed], dim=0
)
block_size = 128
assert len(sf_packed.shape) == 2, f"{sf_packed.shape=}"
assert sf_packed.dtype == torch.int32
mn_repeat_128, k_div_4 = sf_packed.shape
mn = mn_repeat_128 // block_size
k = k_div_4 * 4
# packed u8 -> fp32
sf_u8 = sf_packed.contiguous().flatten().view(torch.uint8).view(mn_repeat_128, k)
sf_fp32 = (sf_u8.to(torch.int32) << 23).view(torch.float32)
# remove repeat
sf_reshaped = sf_fp32.view(mn, block_size, k)
sf_unrepeated = sf_reshaped[:, 0:1, :]
if not torch.all(sf_unrepeated == sf_reshaped):
from sglang.srt.debug_utils.dumper import get_tensor_info
raise AssertionError(
f"sf_unrepeated != sf_reshaped ({get_tensor_info(sf_unrepeated)=} {get_tensor_info(sf_reshaped)=})"
)
sf_unrepeated = sf_unrepeated.squeeze(1).contiguous()
assert sf_unrepeated.shape == (mn, k)
return sf_unrepeated
# COPIED FROM DeepGEMM
def per_block_cast_to_fp8(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
assert x.dim() == 2
m, n = x.shape
x_padded = torch.zeros(
(ceil_align(m, 128), ceil_align(n, 128)), dtype=x.dtype, device=x.device
)
x_padded[:m, :n] = x
x_view = x_padded.view(-1, 128, x_padded.size(1) // 128, 128)
x_amax = x_view.abs().float().amax(dim=(1, 3), keepdim=True).clamp(1e-4)
sf = ceil_to_ue8m0(x_amax / 448.0)
x_scaled = (x_view * (1.0 / sf)).to(torch.float8_e4m3fn)
return x_scaled.view_as(x_padded)[:m, :n].contiguous(), sf.view(
x_view.size(0), x_view.size(2)
)
# COPIED FROM DeepGEMM
def ceil_to_ue8m0(x: torch.Tensor):
bits = x.abs().float().view(torch.int32)
exp = (bits >> 23) & 0xFF
mantissa = bits & 0x7FFFFF
exp = exp + (mantissa != 0).to(torch.int32)
exp = exp.clamp(1, 254)
return (exp << 23).view(torch.float32)
def channel_quant_to_tensor_quant(
x_q_channel: torch.Tensor,
x_s: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
x_dq_channel = x_q_channel.to(torch.float32) * x_s
x_q_tensor, scale = (
scaled_fp8_quant(x_dq_channel)
if _is_cuda
else input_to_float8(x_dq_channel, dtype=x_q_channel.dtype)
)
return x_q_tensor, scale
def _process_scaled_mm_output(output, input_2d_shape, output_shape):
if type(output) is tuple and len(output) == 2:
output = output[0]
return torch.narrow(output, 0, 0, input_2d_shape[0]).view(*output_shape)
def _apply_fallback_scaled_mm(
qinput,
weight,
x_scale,
weight_scale,
input_2d_shape,
output_shape,
bias,
input_dtype,
):
global TORCH_DEVICE_IDENTITY
if TORCH_DEVICE_IDENTITY is None:
TORCH_DEVICE_IDENTITY = torch.ones(1, dtype=torch.float32, device=weight.device)
output = torch._scaled_mm(
qinput,
weight,
scale_a=TORCH_DEVICE_IDENTITY,
scale_b=TORCH_DEVICE_IDENTITY,
out_dtype=torch.float32,
)
output = _process_scaled_mm_output(output, input_2d_shape, output_shape)
x_scale = torch.narrow(x_scale, 0, 0, input_2d_shape[0])
output = output * x_scale * weight_scale.t()
if bias is not None:
output = output + bias
return output.to(dtype=input_dtype)
def apply_fp8_linear_bmm_flashinfer(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Per-tensor static fp8 linear via flashinfer bmm_fp8 (SM10X only)."""
output_shape = [*input.shape[:-1], weight.shape[1]]
input_2d = input.view(-1, input.shape[-1])
qinput, x_scale = static_quant_fp8(input_2d, input_scale, repeat_scale=False)
output = flashinfer_bmm_fp8(qinput, weight, x_scale, weight_scale, input.dtype)
if bias is not None:
output = output + bias
return output.view(*output_shape)
def apply_fp8_linear(
input: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
input_scale_ub: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
cutlass_fp8_supported: bool = cutlass_fp8_supported(),
use_per_token_if_dynamic: bool = False,
pad_output: Optional[bool] = None,
compressed_tensor_quant: bool = False,
) -> torch.Tensor:
# Note: we pad the input because torch._scaled_mm is more performant
# for matrices with batch dimension > 16.
# This could change in the future.
# We also don't pad when using torch.compile,
# as it breaks with dynamic shapes.
if pad_output is None:
pad_output = not cutlass_fp8_supported and not get_bool_env_var(
"SGLANG_ENABLE_TORCH_COMPILE"
)
output_padding = 17 if pad_output else None
# View input as 2D matrix for fp8 methods
input_2d = input.view(-1, input.shape[-1])
output_shape = [*input.shape[:-1], weight.shape[1]]
if compressed_tensor_quant:
# Maybe apply padding to output, see comment in __init__
num_token_padding = output_padding
if cutlass_fp8_supported and weight_scale.numel() == weight.shape[1]:
num_token_padding = None
# For static per-tensor activation scales when using inductor compiler,
# use pure PyTorch ops instead of the opaque sgl_kernel quant kernel.
# Inductor fuses these with surrounding ops (RMSNorm, residual add),
# eliminating a separate kernel launch per linear layer.
# weight_scale shape does not matter here -- it is only used in the
# GEMM epilogue, not in the activation quant fusion. Only activates when
# cuda_graph_config[prefill].tc_compiler=inductor; eager PCG and
# decode both use the faster custom kernel.
if (
input_scale is not None
and input_scale.numel() == 1
and get_server_args().cuda_graph_config.prefill.tc_compiler == "inductor"
):
qinput = (
(input_2d * input_scale.reciprocal())
.clamp(min=fp8_min, max=fp8_max)
.to(fp8_dtype)
)
x_scale = input_scale
else:
qinput, x_scale = scaled_fp8_quant(
input_2d,
input_scale,
num_token_padding=num_token_padding,
use_per_token_if_dynamic=use_per_token_if_dynamic,
)
else:
# cutlass w8a8 fp8 sgl-kernel only supports per-token scale
if input_scale is not None:
assert input_scale.numel() == 1
# broadcast per-tensor scale to per-token scale when supporting cutlass
qinput, x_scale = static_quant_fp8(
input_2d, input_scale, repeat_scale=cutlass_fp8_supported
)
else:
# default use per-token quantization if dynamic
if _is_cuda:
qinput, x_scale = sglang_per_token_quant_fp8(input_2d)
else:
# TODO(kkhuang): temporarily enforce per-tensor activation scaling if weight is per-tensor scaling
# final solution should be: 1. add support to per-tensor activation scaling.
# 2. solve the torch.compile error from weight_scale.numel() == 1 and x_scale.numel() > 1 (below line#308)
if _is_hip and weight_scale.numel() == 1:
qinput, x_scale = scaled_fp8_quant(
input_2d,
input_scale,
use_per_token_if_dynamic=use_per_token_if_dynamic,
)
else:
qinput, x_scale = per_token_group_quant_fp8(
input_2d, group_size=input_2d.shape[1]
)
if cutlass_fp8_supported and weight_scale.numel() == weight.shape[1]:
cutlass_compatible_b = weight.shape[0] % 16 == 0 and weight.shape[1] % 16 == 0
if not cutlass_compatible_b or use_triton_w8a8_fp8_kernel:
# Massage the input to be 2D
qinput = qinput.view(-1, qinput.shape[-1])
output = triton_scaled_mm(
qinput, weight, x_scale, weight_scale, input.dtype, bias
)
else:
output = fp8_scaled_mm(
qinput,
weight,
x_scale,
weight_scale,
out_dtype=input.dtype,
bias=bias,
)
return output.view(*output_shape)
# torch.scaled_mm supports per tensor weights + activations only
# so fallback to naive if per channel or per token
per_tensor_weights = weight_scale.numel() == 1
# When the number of token is 1,
# per-token scale has shape (1, 1), per-tensor scale has shape (1) or ().
per_tensor_activations = (x_scale.numel() == 1) and x_scale.dim() < 2
if (
use_per_token_if_dynamic
and not per_tensor_weights
and not per_tensor_activations
and (USE_ROWWISE_TORCH_SCALED_MM or _use_aiter)
):
# into this sector means use dynamic per-token-per-channel quant
# per-token scale quant for input matrix, every row(one token) have one scale factor
# per-channel scale quant for weight matrix, every col(one channel) have one scale factor
if _use_aiter:
# gemm_a8w8_bpreshuffle(XQ, WQ, x_scale, w_scale, dtype)
# XQ -> input tensor, shape = (m, k)
# WQ -> weight tensor, shape = (n, k), with preshuffe get better perf
# x_scale -> input scale tensor, shape = (m, 1)
# w_scale -> weight scale tensor, shape = (n ,1)
# dtype -> output dtype
output = gemm_a8w8_bpreshuffle(
XQ=qinput,
WQ=weight.T,
x_scale=x_scale,
w_scale=weight_scale,
dtype=input.dtype,
)
if bias is not None:
output += bias
return _process_scaled_mm_output(output, input_2d.shape, output_shape)
else:
# For now validated on ROCm platform
# fp8 rowwise scaling in torch._scaled_mm is introduced in
# https://github.com/pytorch/pytorch/pull/144432 using hipBLASLt
# and ROCm 6.3, which only exists in torch 2.7 and above.
# For CUDA platform please validate if the
# torch._scaled_mm support rowwise scaled GEMM
# Fused GEMM_DQ Rowwise GEMM
output = torch._scaled_mm(
qinput,
weight,
out_dtype=input.dtype,
scale_a=x_scale,
scale_b=weight_scale.t(),
bias=bias,
)
return _process_scaled_mm_output(output, input_2d.shape, output_shape)
if per_tensor_weights and per_tensor_activations:
# Fused GEMM_DQ; _scaled_mm with torch.compile requires len(weight_scale.shape) == len(x_scale.shape)
if weight_scale.ndim == 0 and x_scale.ndim == 1:
weight_scale = weight_scale.unsqueeze(0)
output = torch._scaled_mm(
qinput,
weight,
out_dtype=input.dtype,
scale_a=x_scale,
scale_b=weight_scale,
bias=bias,
)
return _process_scaled_mm_output(output, input_2d.shape, output_shape)
# Fallback for channelwise case, where we use unfused DQ
# due to limitations with scaled_mm
# Symmetric quantized GEMM by definition computes the following:
# C = (s_x * X) (s_w * W) + bias
# This is equivalent to dequantizing the weights and activations
# before applying a GEMM.
#
# In order to compute quantized operands, a quantized kernel
# will rewrite the above like so:
# C = s_w * s_x * (X * W) + bias
#
# For the scaled_mm fallback case, we break this down, since it
# does not support s_w being a vector.
return _apply_fallback_scaled_mm(
qinput,
weight,
x_scale,
weight_scale,
input_2d.shape,
output_shape,
bias,
input.dtype,
)
def can_auto_enable_marlin_fp8() -> bool:
try:
major, minor = get_device_capability()
sm = major * 10 + minor
return 80 <= sm < 89
except Exception:
return False
def apply_fp8_ptpc_linear(
input: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]],
weight: torch.Tensor,
weight_scale: torch.Tensor,
input_scale: Optional[torch.Tensor] = None,
input_scale_ub: Optional[torch.Tensor] = None,
bias: Optional[torch.Tensor] = None,
cutlass_fp8_supported: bool = cutlass_fp8_supported(),
use_per_token_if_dynamic: bool = False,
pad_output: Optional[bool] = None,
compressed_tensor_quant: bool = False,
) -> torch.Tensor:
"""FP8 per-token per-channel linear. Only used with the aiter (ROCm) backend."""
# Handle pre-quantized (fp8_tensor, scale) tuple from fused RMSNorm+Quant
if isinstance(input, tuple):
q_input, x_scale = input
q_input = q_input.view(-1, q_input.shape[-1])
output_shape = [*q_input.shape[:-1], weight.shape[0]]
output = aiter.gemm_a8w8_bpreshuffle(
q_input, weight, x_scale, weight_scale, None, torch.bfloat16
)
if bias is not None:
output = output + bias
return output.view(*output_shape)
# View input as 2D matrix for fp8 methods
input_2d = input.view(-1, input.shape[-1])
# weight is transposed (K, N)
output_shape = [*input.shape[:-1], weight.shape[1]]
q_input, x_scale = aiter.per_token_quant_hip(input_2d, quant_dtype=aiter.dtypes.fp8)
per_tensor_weights = (weight_scale.numel() == 1) and weight_scale.dim() < 2
per_tensor_activations = (x_scale.numel() == 1) and x_scale.dim() < 2
if not (per_tensor_weights and per_tensor_activations):
# weight is in (N, K)
output_shape = [*input.shape[:-1], weight.shape[0]]
output = aiter.gemm_a8w8_bpreshuffle(
q_input, weight, x_scale, weight_scale, None, input.dtype
)
if bias is not None:
output = output + bias
return output.view(*output_shape)
def validate_fp8_block_shape(
layer: torch.nn.Module,
input_size: int,
output_size: int,
input_size_per_partition: int,
output_partition_sizes: list[int],
block_size: list[int],
) -> None:
"""Validate block quantization shapes for tensor parallelism."""
tp_size = getattr(layer, "tp_size", get_parallel().tp_size)
block_n, block_k = block_size[0], block_size[1]
# Required by row parallel
if (
tp_size > 1
and input_size // input_size_per_partition == tp_size
and input_size_per_partition % block_k != 0
):
raise ValueError(
f"Weight input_size_per_partition = {input_size_per_partition} "
f"is not divisible by weight quantization block_k = {block_k}."
)
# Required by column parallel or enabling merged weights
is_tp_split = tp_size > 1 and output_size // sum(output_partition_sizes) == tp_size
is_merged_gemm = len(output_partition_sizes) > 1
if is_tp_split or is_merged_gemm:
sizes_to_check = output_partition_sizes
if not is_tp_split and is_merged_gemm:
# In case of merged matrices, we allow the last
# matrix to not be a multiple of block size
sizes_to_check = output_partition_sizes[:-1]
for output_partition_size in sizes_to_check:
if output_partition_size % block_n != 0:
raise ValueError(
f"Weight output_partition_size = "
f"{output_partition_size} is not divisible by "
f"weight quantization block_n = {block_n}."
)