MXFP4 linear kernel for RDNA3/RDNA3.5 (gfx11) with an int8-activation decode
path.
Routes on the number of activation rows M
1 <= M <= MAX_W4A8_BATCH_SIZE (decode, speculative verify):
HIP W4A8 GEMV (_rocm_C.mxfp4_w4a8_gemv_rdna3). Activations are quantized
to int8 per 32-element group and reduced with int8 dot instructions.
otherwise (prefill, profiling):
dequantize the weight and run a high-precision GEMM on the unquantized
activations (weight-only).
Both paths read the checkpoint layout directly ([N, K/2] uint8 E2M1 codes,
[N, K/32] uint8 E8M0 scales), so there is no weight repack.
Selected when int8 activations are requested, e.g. with
--quantization-config.linear.activation int8_per_group_32. For W4A4
checkpoints this replaces the MXFP4 activation QDQ: decode uses int8
activations and prefill uses the model dtype, which kept accuracy where mixing
MXFP4-QDQ prefill with int8 decode did not.
Classes:
Rdna3W4A8MxFp4LinearKernel
Bases: MxFp4LinearKernel
MXFP4 GEMM for RDNA3/RDNA3.5: W4A8 int8-dot GEMV for M <= 8, dequant +
high-precision GEMM above.
Source code in vllm/model_executor/kernels/linear/mxfp4/rdna3_hip.py
| class Rdna3W4A8MxFp4LinearKernel(MxFp4LinearKernel):
"""MXFP4 GEMM for RDNA3/RDNA3.5: W4A8 int8-dot GEMV for M <= 8, dequant +
high-precision GEMM above."""
@classmethod
def is_supported(
cls, compute_capability: int | None = None
) -> tuple[bool, str | None]:
if not current_platform.is_rocm():
return False, "only targets ROCm"
from vllm.platforms.rocm import on_gfx11
if not on_gfx11():
return False, "requires RDNA3/RDNA3.5 (gfx11)"
if not (
hasattr(torch.ops, "_rocm_C")
and hasattr(torch.ops._rocm_C, "mxfp4_w4a8_gemv_rdna3")
):
return False, "_rocm_C.mxfp4_w4a8_gemv_rdna3 is not built"
return True, None
@classmethod
def _supports_quant_scheme(
cls,
weight_key: QuantKey | None,
activation_key: QuantKey | None,
) -> bool:
return weight_key == kMxfp4Static and activation_key == kInt8Dynamic32Sym
@classmethod
def can_implement(cls, config: MxFp4LinearLayerConfig) -> tuple[bool, str | None]:
if config.activation_quant_key != kInt8Dynamic32Sym:
return False, "only supports dynamic per-32-group int8 activations"
if not has_quark():
return False, "amd-quark package not available (needed for M > 8)"
return True, None
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
weight = layer.weight.data
weight_scale = layer.weight_scale.data
if weight.dtype != torch.uint8 or weight_scale.dtype != torch.uint8:
raise ValueError(
"Rdna3W4A8MxFp4LinearKernel expects uint8 packed weights and "
f"uint8 E8M0 scales, got {weight.dtype} and {weight_scale.dtype}"
)
N, k_half = weight.shape
K = 2 * k_half
if K % _MXFP4_GROUP_SIZE != 0 or weight_scale.shape != (
N,
K // _MXFP4_GROUP_SIZE,
):
raise ValueError(
"Rdna3W4A8MxFp4LinearKernel expects weight [N, K/2] and "
f"weight_scale [N, K/32] with K % 32 == 0, got "
f"{tuple(weight.shape)} and {tuple(weight_scale.shape)}"
)
layer.weight = Parameter(weight.contiguous(), requires_grad=False)
layer.weight_scale = Parameter(weight_scale.contiguous(), requires_grad=False)
def apply_weights(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
x_2d = x.reshape(-1, x.shape[-1])
if not x_2d.is_contiguous():
x_2d = x_2d.contiguous()
out = torch.ops.vllm.rdna3_mxfp4_w4a8_apply(
x_2d, layer.weight, layer.weight_scale, bias
)
return out.reshape(*x.shape[:-1], out.shape[-1])
|