#!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Tests for the ROCm Triton W4A16 GEMM kernel. Run `pytest tests/kernels/quantization/test_triton_w4a16.py`. """ import importlib import pytest import torch from vllm.platforms import current_platform from vllm.utils.torch_utils import set_random_seed # This test module is ROCm/Triton specific. Avoid import-time failures on # non-ROCm or environments without Triton by skipping early. if not current_platform.is_rocm(): pytest.skip("ROCm only", allow_module_level=True) pytest.importorskip("triton") device = "cuda" triton_w4a16_module = importlib.import_module( "vllm.model_executor.kernels.linear.mixed_precision.triton_w4a16" ) triton_w4a16_gemm = triton_w4a16_module.triton_w4a16_gemm TritonW4A16LinearKernel = triton_w4a16_module.TritonW4A16LinearKernel def _pack_int4_along_n(w_int4_kn: torch.Tensor) -> torch.Tensor: """Pack int4 values along N: [K, N] -> [K, N//8] int32.""" assert w_int4_kn.dtype == torch.int32 K, N = w_int4_kn.shape assert N % 8 == 0 shifts = torch.arange(8, device=w_int4_kn.device, dtype=torch.int32) * 4 return torch.sum( (w_int4_kn.view(K, N // 8, 8) & 0xF) << shifts, dim=2, dtype=torch.int32, ).contiguous() def _unpack_int4_along_n(w_packed_kn8: torch.Tensor) -> torch.Tensor: """Unpack int4 values along N: [K, N//8] -> [K, N] int32.""" assert w_packed_kn8.dtype == torch.int32 K, N8 = w_packed_kn8.shape shifts = torch.arange(8, device=w_packed_kn8.device, dtype=torch.int32) * 4 nibbles = (w_packed_kn8.unsqueeze(-1) >> shifts) & 0xF return nibbles.reshape(K, N8 * 8) def _pack_int4_along_k_to_ckpt(w_int4_kn: torch.Tensor) -> torch.Tensor: """Pack int4 values along K into CT checkpoint layout: [K,N] -> [N, K//8].""" assert w_int4_kn.dtype == torch.int32 K, N = w_int4_kn.shape assert K % 8 == 0 out = torch.zeros((N, K // 8), dtype=torch.int32, device=w_int4_kn.device) for i in range(8): out |= (w_int4_kn[i::8, :].t() & 0xF) << (i * 4) return out.contiguous() def _w4a16_reference( a_mk: torch.Tensor, b_packed_kn8: torch.Tensor, scales_gn: torch.Tensor, *, group_size: int, qzeros_gn8: torch.Tensor | None, zp_bias: int, ) -> torch.Tensor: """Reference implementation for W4A16. a_mk: [M,K] fp16/bf16 b_packed_kn8: [K, N//8] int32, N-packed int4 weights scales_gn: [K//G, N] fp16/bf16 qzeros_gn8: [K//G, N//8] int32, N-packed int4 zeros, or None """ assert a_mk.dtype in (torch.float16, torch.bfloat16) assert b_packed_kn8.dtype == torch.int32 assert scales_gn.dtype == a_mk.dtype M, K = a_mk.shape N = b_packed_kn8.shape[1] * 8 assert b_packed_kn8.shape[0] == K assert group_size > 0 and K % group_size == 0 G = group_size num_groups = K // G assert scales_gn.shape == (num_groups, N) w_int4 = _unpack_int4_along_n(b_packed_kn8) # [K,N] if qzeros_gn8 is None: z_full = torch.full((K, N), zp_bias, dtype=torch.int32, device=a_mk.device) else: assert qzeros_gn8.shape == (num_groups, N // 8) z_gn = _unpack_int4_along_n(qzeros_gn8) # [G,N] in groups z_full = z_gn.repeat_interleave(G, dim=0) # [K,N] s_full = scales_gn.repeat_interleave(G, dim=0).to(torch.float32) # [K,N] w_fp = (w_int4 - z_full).to(torch.float32) * s_full # [K,N] out = a_mk.to(torch.float32) @ w_fp # [M,N] return out.to(a_mk.dtype) @pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm only") @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16]) @pytest.mark.parametrize( "M,K,N,G,has_zp", [ (1, 256, 256, 32, False), (17, 256, 512, 32, False), (32, 512, 256, 64, False), (33, 512, 512, 128, False), (64, 1024, 256, 256, False), (128, 256, 1024, 32, True), (64, 512, 512, 64, True), ], ) def test_triton_w4a16_gemm_matches_reference(dtype, M, K, N, G, has_zp): if not torch.cuda.is_available(): pytest.skip("CUDA/HIP device not available") if N % 8 != 0 or K % G != 0: pytest.skip("Invalid test shape") set_random_seed(0) a = (0.25 * torch.randn((M, K), device=device, dtype=torch.float32)).to(dtype) w_int4 = torch.randint(0, 16, (K, N), device=device, dtype=torch.int32) b_packed = _pack_int4_along_n(w_int4) scales = (0.05 * torch.rand((K // G, N), device=device, dtype=torch.float32)).to( dtype ) qzeros = None if has_zp: zeros_int4 = torch.randint(0, 16, (K // G, N), device=device, dtype=torch.int32) qzeros = _pack_int4_along_n(zeros_int4) out = triton_w4a16_gemm( a=a, b_q=b_packed, scales=scales, qzeros=qzeros, group_size=G, zp_bias=8, ) ref = _w4a16_reference( a, b_packed, scales, group_size=G, qzeros_gn8=qzeros, zp_bias=8, ) torch.testing.assert_close(out, ref, rtol=1e-2, atol=1e-2) @pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm only") def test_triton_w4a16_gemm_requires_contiguous_inputs(): if not torch.cuda.is_available(): pytest.skip("CUDA/HIP device not available") set_random_seed(0) M, K, N, G = 32, 256, 256, 32 a = torch.randn((K, M), device=device, dtype=torch.float16).t() # non-contiguous w_int4 = torch.randint(0, 16, (K, N), device=device, dtype=torch.int32) b_packed = _pack_int4_along_n(w_int4) scales = torch.rand((K // G, N), device=device, dtype=torch.float16) with pytest.raises(AssertionError): triton_w4a16_gemm( a=a, b_q=b_packed, scales=scales, qzeros=None, group_size=G, zp_bias=8, ) @pytest.mark.skipif(not current_platform.is_rocm(), reason="ROCm only") def test_triton_w4a16_process_weights_after_loading_repacks_layout(): if not torch.cuda.is_available(): pytest.skip("CUDA/HIP device not available") from vllm.config import VllmConfig, set_current_vllm_config from vllm.distributed import ( ensure_model_parallel_initialized, init_distributed_environment, ) from vllm.model_executor.kernels.linear.mixed_precision.MPLinearKernel import ( MPLinearLayerConfig, ) from vllm.model_executor.parameter import ( GroupQuantScaleParameter, PackedColumnParameter, PackedvLLMParameter, ) from vllm.scalar_type import scalar_types with set_current_vllm_config(VllmConfig()): init_distributed_environment( world_size=1, rank=0, distributed_init_method="tcp://127.0.0.1:0", local_rank=0, ) ensure_model_parallel_initialized(1, 1) set_random_seed(0) # Small-but-nontrivial shapes. K, N = 256, 256 G = 32 assert K % 8 == 0 and N % 8 == 0 and K % G == 0 # Build a canonical int4 weight grid then pack into the CT checkpoint layout. w_int4_kn = torch.randint(0, 16, (K, N), device=device, dtype=torch.int32) w_ckpt_nk8 = _pack_int4_along_k_to_ckpt(w_int4_kn) # [N, K//8] # Scales in CT checkpoint layout for WNA16: [N, K//G] scales_ckpt_nkg = 0.05 * torch.rand((N, K // G), device=device, dtype=torch.float16) # Asymmetric case: zero points in CT checkpoint layout [N//8, K//G] (N-packed) zeros_int4_gn = torch.randint(0, 16, (K // G, N), device=device, dtype=torch.int32) zeros_packed_gn8 = _pack_int4_along_n(zeros_int4_gn) # [K//G, N//8] zeros_ckpt_n8kg = zeros_packed_gn8.t().contiguous() # [N//8, K//G] config = MPLinearLayerConfig( full_weight_shape=(K, N), partition_weight_shape=(K, N), weight_type=scalar_types.uint4, # asymmetric act_type=torch.float16, group_size=G, zero_points=True, has_g_idx=False, ) kernel = TritonW4A16LinearKernel( config, w_q_param_name="weight_packed", w_s_param_name="weight_scale", w_zp_param_name="weight_zero_point", w_gidx_param_name=None, ) # Build dummy layer with vLLM parameter wrappers. weight_loader = lambda *args, **kwargs: None class DummyLayer(torch.nn.Module): pass layer = DummyLayer() layer.register_parameter( "weight_packed", PackedvLLMParameter( data=w_ckpt_nk8, weight_loader=weight_loader, input_dim=1, output_dim=0, packed_factor=8, packed_dim=1, ), ) layer.register_parameter( "weight_scale", GroupQuantScaleParameter( data=scales_ckpt_nkg, weight_loader=weight_loader, input_dim=1, output_dim=0, ), ) layer.register_parameter( "weight_zero_point", PackedColumnParameter( data=zeros_ckpt_n8kg, weight_loader=weight_loader, output_dim=0, packed_factor=8, packed_dim=0, ), ) kernel.process_weights_after_loading(layer) # Expected transformed layouts. expected_w_kn8 = _pack_int4_along_n(w_int4_kn) # [K, N//8] expected_scales_gn = scales_ckpt_nkg.t().contiguous() # [K//G, N] expected_zeros_gn8 = zeros_ckpt_n8kg.t().contiguous() # [K//G, N//8] assert tuple(layer.weight_packed.shape) == (K, N // 8) assert tuple(layer.weight_scale.shape) == (K // G, N) assert tuple(layer.weight_zero_point.shape) == (K // G, N // 8) torch.testing.assert_close(layer.weight_packed, expected_w_kn8) torch.testing.assert_close(layer.weight_scale, expected_scales_gn) torch.testing.assert_close(layer.weight_zero_point, expected_zeros_gn8)