| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import pytest |
| 5 | import torch |
| 6 | |
| 7 | from tests.kernels.utils import to_int8 |
| 8 | from vllm import _custom_ops as ops |
| 9 | from vllm.platforms import current_platform |
| 10 | |
| 11 | if not current_platform.is_cpu(): |
| 12 | pytest.skip("skipping CPU-only tests", allow_module_level=True) |
| 13 | |
| 14 | NK_FACTORS = [ |
| 15 | (256, 128), |
| 16 | (4096, 4096), |
| 17 | (16384, 4096), |
| 18 | (64128, 4096), |
| 19 | (1023, 491), |
| 20 | (1001, 15), |
| 21 | ] |
| 22 | M_FACTORS = [ |
| 23 | (16, 1, 32, 128, 64), |
| 24 | (1, 17, 1, 31, 17), |
| 25 | ] |
| 26 | CACHE_SIZES = [2] |
| 27 | DTYPE = [torch.bfloat16] |
| 28 | |
| 29 | |
| 30 | def rand_int8(shape: tuple, device: str = "cpu"): |
| 31 | return to_int8(torch.rand(shape, device=device) * 255 - 128) |
| 32 | |
| 33 | |
| 34 | def ref_int8_scaled_mm( |
| 35 | a: torch.Tensor, |
| 36 | b: torch.Tensor, |
| 37 | scale_a: torch.Tensor, |
| 38 | scale_b: torch.Tensor, |
| 39 | azp: torch.Tensor | None, |
| 40 | bias: torch.Tensor | None, |
| 41 | output_type: torch.dtype, |
| 42 | ): |
| 43 | if azp is not None: |
| 44 | a = a.to(dtype=torch.float32) - azp.to(dtype=torch.float32) |
| 45 | output = torch.mm( |
| 46 | (scale_a * a.to(dtype=torch.float32)), (scale_b * b.to(dtype=torch.float32)) |
| 47 | ) |
| 48 | if bias is not None: |
| 49 | output += bias.float() |
| 50 | |
| 51 | return output.to(dtype=output_type) |
| 52 | |
| 53 | |
| 54 | def onednn_int8_gemm_test_helper( |
| 55 | primitive_cache_size: int, |
| 56 | m: int, |
| 57 | n: int, |
| 58 | k: int, |
| 59 | per_tensor_a_quant: bool, |
| 60 | per_tensor_b_quant: bool, |
| 61 | use_azp: bool, |
| 62 | use_bias: bool, |
| 63 | out_dtype: torch.dtype = torch.bfloat16, |
| 64 | device: str = "cpu", |
| 65 | ): |
| 66 | # Test for a oneDNN kernel with per-tensor / per-token activation |
| 67 | # quantization and per-tensor / per-output channel weight quantization. |
| 68 | a = to_int8(torch.randn((m, k), device=device) * 5) |
| 69 | b = to_int8(torch.randn((n, k), device=device).t() * 5) |
| 70 | |
| 71 | a_scales_shape = (1, 1) if per_tensor_a_quant else (m, 1) |
| 72 | b_scales_shape = (1, 1) if per_tensor_b_quant else (1, n) |
| 73 | |
| 74 | scale_a = torch.randn(a_scales_shape, device=device, dtype=torch.float32) |
| 75 | scale_b = torch.randn(b_scales_shape, device=device, dtype=torch.float32) |
| 76 | |
| 77 | if use_azp: |
| 78 | azp = torch.rand(a_scales_shape, dtype=torch.float32) * 10 + 1.5 |
| 79 | azp = (azp / scale_a).round().to(dtype=torch.int32) |
| 80 | azp_adj = scale_b * b.sum(dim=0, keepdim=True, dtype=torch.float32) |
| 81 | else: |
| 82 | azp = None |
| 83 | azp_adj = None |
| 84 | |
| 85 | bias = torch.rand((n,), device=device, dtype=out_dtype) * 10 if use_bias else None |
| 86 | |
| 87 | handler = ops.create_onednn_scaled_mm( |
| 88 | b, |
| 89 | scale_b, |
| 90 | out_dtype, |
| 91 | not per_tensor_a_quant, |
| 92 | use_azp, |
| 93 | primitive_cache_size, |
| 94 | ) |
| 95 | |
| 96 | out = torch.zeros((m, n), dtype=out_dtype) |
| 97 | ops.onednn_scaled_mm(handler, a, out, scale_a, azp, azp_adj, bias) |
| 98 | baseline = ref_int8_scaled_mm(a, b, scale_a, scale_b, azp, bias, out_dtype) |
| 99 | |
| 100 | torch.testing.assert_close(out, baseline, rtol=1e-1, atol=1e0) |
| 101 | |
| 102 | if use_bias: |
| 103 | # To test runtime bias setting |
| 104 | out = torch.zeros((m, n), dtype=out_dtype) |
| 105 | ops.onednn_scaled_mm(handler, a, out, scale_a, azp, azp_adj, None) |
| 106 | baseline = ref_int8_scaled_mm(a, b, scale_a, scale_b, azp, None, out_dtype) |
| 107 | |
| 108 | torch.testing.assert_close(out, baseline, rtol=1e-1, atol=1e0) |
| 109 | |
| 110 | |
| 111 | def onednn_gemm_test_helper( |
| 112 | primitive_cache_size: int, |
| 113 | m: int, |
| 114 | n: int, |
| 115 | k: int, |
| 116 | use_bias: bool, |
| 117 | use_stride: bool, |
| 118 | dtype: torch.dtype = torch.bfloat16, |
| 119 | device: str = "cpu", |
| 120 | ): |
| 121 | if use_stride: |
| 122 | a = torch.rand((m, 2 * k), dtype=dtype, device=device) * 1.5 |
| 123 | a = a[:, :k] |
| 124 | else: |
| 125 | a = torch.rand((m, k), dtype=dtype, device=device) * 1.5 |
| 126 | |
| 127 | b = torch.rand((n, k), dtype=dtype, device=device) * 1.5 |
| 128 | |
| 129 | if use_bias: |
| 130 | bias = torch.rand((n,), device=device, dtype=dtype) * 5 |
| 131 | bias_f32 = bias.float() |
| 132 | else: |
| 133 | bias = None |
| 134 | bias_f32 = None |
| 135 | |
| 136 | handler = ops.create_onednn_mm( |
| 137 | b.t(), |
| 138 | primitive_cache_size, |
| 139 | ) |
| 140 | |
| 141 | out = ops.onednn_mm(handler, a, bias) |
| 142 | baseline = torch.nn.functional.linear(a.float(), b.float(), bias_f32).to( |
| 143 | dtype=a.dtype |
| 144 | ) |
| 145 | |
| 146 | torch.testing.assert_close(out, baseline) |
| 147 | |
| 148 | if use_bias: |
| 149 | # To test runtime bias setting |
| 150 | out = ops.onednn_mm(handler, a, None) |
| 151 | baseline = torch.nn.functional.linear(a.float(), b.float(), None).to( |
| 152 | dtype=a.dtype |
| 153 | ) |
| 154 | |
| 155 | torch.testing.assert_close(out, baseline) |
| 156 | |
| 157 | |
| 158 | @pytest.mark.parametrize("n,k", NK_FACTORS) |
| 159 | @pytest.mark.parametrize("m_list", M_FACTORS) |
| 160 | @pytest.mark.parametrize("per_tensor_a_scale", [True, False]) |
| 161 | @pytest.mark.parametrize("per_tensor_b_scale", [True, False]) |
| 162 | @pytest.mark.parametrize("use_bias", [True, False]) |
| 163 | @pytest.mark.parametrize("use_azp", [True, False]) |
| 164 | @pytest.mark.parametrize("output_type", DTYPE) |
| 165 | @pytest.mark.parametrize("primitive_cache_size", CACHE_SIZES) |
| 166 | def test_onednn_int8_scaled_gemm( |
| 167 | n: int, |
| 168 | k: int, |
| 169 | m_list: tuple[int, ...], |
| 170 | per_tensor_a_scale: bool, |
| 171 | per_tensor_b_scale: bool, |
| 172 | use_bias: bool, |
| 173 | use_azp: bool, |
| 174 | output_type: torch.dtype, |
| 175 | primitive_cache_size: int, |
| 176 | ): |
| 177 | for m in m_list: |
| 178 | onednn_int8_gemm_test_helper( |
| 179 | primitive_cache_size=primitive_cache_size, |
| 180 | m=m, |
| 181 | n=n, |
| 182 | k=k, |
| 183 | per_tensor_a_quant=per_tensor_a_scale, |
| 184 | per_tensor_b_quant=per_tensor_b_scale, |
| 185 | use_bias=use_bias, |
| 186 | use_azp=use_azp, |
| 187 | out_dtype=output_type, |
| 188 | ) |
| 189 | |
| 190 | |
| 191 | @pytest.mark.parametrize("n,k", NK_FACTORS) |
| 192 | @pytest.mark.parametrize("m_list", M_FACTORS) |
| 193 | @pytest.mark.parametrize("use_bias", [True, False]) |
| 194 | @pytest.mark.parametrize("use_stride", [True, False]) |
| 195 | @pytest.mark.parametrize("dtype", DTYPE) |
| 196 | @pytest.mark.parametrize("primitive_cache_size", CACHE_SIZES) |
| 197 | def test_onednn_gemm( |
| 198 | n: int, |
| 199 | k: int, |
| 200 | m_list: tuple[int, ...], |
| 201 | use_bias: bool, |
| 202 | use_stride: bool, |
| 203 | dtype: torch.dtype, |
| 204 | primitive_cache_size: int, |
| 205 | ): |
| 206 | for m in m_list: |
| 207 | onednn_gemm_test_helper( |
| 208 | primitive_cache_size=primitive_cache_size, |
| 209 | m=m, |
| 210 | n=n, |
| 211 | k=k, |
| 212 | use_bias=use_bias, |
| 213 | use_stride=use_stride, |
| 214 | dtype=dtype, |
| 215 | ) |