main
py 241 lines 8.65 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import torch
6 from torch import nn
7
8 from vllm.model_executor.layers.quantization.modelopt import (
9 ModelOptLinearMethod,
10 resolve,
11 )
12 from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
13 dequantize_to_dtype,
14 nvfp4_gathered_bias,
15 )
16 from vllm.model_executor.models.qwen3_dspark import DSparkMarkovHead
17 from vllm.platforms import current_platform
18
19
20 def _markov_head(weight: torch.Tensor) -> DSparkMarkovHead:
21 head = DSparkMarkovHead.__new__(DSparkMarkovHead)
22 nn.Module.__init__(head)
23 head.markov_w2 = nn.Linear(
24 weight.shape[1], weight.shape[0], bias=False, dtype=weight.dtype
25 )
26 head.markov_w2.weight.data.copy_(weight)
27 head.markov_w2._retain_weight_for_gather = False
28 head.markov_w2.is_w4a16_nvfp4 = False
29 return head
30
31
32 def test_gathered_markov_bias_overwrites_dense_logits():
33 weight = torch.arange(21, dtype=torch.float32).view(7, 3) / 10
34 markov_embed = torch.tensor([[0.5, -1.0, 0.25], [1.0, 0.5, -0.5]])
35 logits = torch.tensor(
36 [
37 [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7],
38 [0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1],
39 ]
40 )
41 values, index = logits.topk(3, dim=-1)
42 values = torch.stack((values, torch.zeros_like(values)), dim=1)[:, 0]
43 expected = values + torch.bmm(weight[index], markov_embed.unsqueeze(-1)).squeeze(-1)
44 logits.fill_(float("-inf"))
45
46 result = _markov_head(weight).apply_bias_gathered(
47 markov_embed, logits, values, index
48 )
49
50 assert result is logits
51 torch.testing.assert_close(result.gather(1, index), expected)
52 selected = torch.zeros_like(result, dtype=torch.bool).scatter_(1, index, True)
53 assert torch.isneginf(result.masked_select(~selected)).all()
54
55
56 def test_gathered_markov_bias_matches_dense_at_full_vocab():
57 weight = torch.arange(15, dtype=torch.float32).view(5, 3) / 10
58 markov_embed = torch.tensor([[0.5, -1.0, 0.25]])
59 logits = torch.tensor([[0.1, 0.4, -0.2, 0.3, 0.0]])
60 original = logits.clone()
61 values, index = logits.topk(logits.shape[-1], dim=-1)
62 scale = 0.5
63 logits.fill_(float("-inf"))
64
65 result = _markov_head(weight).apply_bias_gathered(
66 markov_embed, logits, values, index, scale
67 )
68
69 expected = original + markov_embed @ weight.T * scale
70 torch.testing.assert_close(result, expected)
71
72
73 @pytest.mark.parametrize("cuda_alike_platform", [False, True])
74 def test_gathered_markov_bias_dequantizes_selected_w4a16_rows(
75 monkeypatch, cuda_alike_platform
76 ):
77 packed_weight = torch.tensor(
78 [[0x00] * 8, [0x22] * 8, [0xAA] * 8, [0x31] * 8], dtype=torch.uint8
79 )
80 weight_scale = torch.ones((4, 1), dtype=torch.float8_e4m3fn)
81
82 layer = nn.Module()
83 layer.weight = nn.Parameter(packed_weight, requires_grad=False)
84 layer.weight_scale = nn.Parameter(weight_scale, requires_grad=False)
85 layer.weight_scale_2 = nn.Parameter(torch.ones(1), requires_grad=False)
86 layer._retain_weight_for_gather = True
87
88 spec, ctx, fmt = resolve("W4A16_NVFP4", type("Config", (), {"group_size": 16}), "")
89 method = ModelOptLinearMethod(spec, ctx, fmt)
90
91 class RepackingKernel:
92 @staticmethod
93 def process_weights_after_loading(layer):
94 layer.weight = nn.Parameter(
95 torch.zeros((1, 1), dtype=torch.int32), requires_grad=False
96 )
97 layer.weight_scale = nn.Parameter(
98 torch.zeros((1, 1), dtype=torch.float32), requires_grad=False
99 )
100 layer.weight_global_scale = nn.Parameter(
101 torch.zeros(1, dtype=torch.float32), requires_grad=False
102 )
103
104 method.kernel = RepackingKernel()
105 method.process_weights_after_loading(layer)
106 assert layer.is_w4a16_nvfp4
107
108 head = DSparkMarkovHead.__new__(DSparkMarkovHead)
109 nn.Module.__init__(head)
110 head.markov_w2 = layer
111 markov_embed = torch.tensor([[1.0] * 16, [0.5, -0.5] * 8], dtype=torch.float32)
112 index = torch.tensor([[1, 3], [2, 0]])
113 values = torch.tensor([[0.25, -0.5], [1.0, 2.0]])
114 original_values = values.clone()
115 logits = torch.full((2, 4), float("-inf"))
116
117 # CPU tensors must use the fallback even on CUDA/ROCm hosts.
118 monkeypatch.setattr(current_platform, "is_cuda_alike", lambda: cuda_alike_platform)
119 result = head.apply_bias_gathered(markov_embed, logits, values, index)
120
121 dense_weight = torch.tensor([[0.0] * 16, [1.0] * 16, [-1.0] * 16, [0.5, 1.5] * 8])
122 expected = original_values + torch.bmm(
123 dense_weight[index], markov_embed.unsqueeze(-1)
124 ).squeeze(-1)
125 torch.testing.assert_close(result.gather(1, index), expected)
126 torch.testing.assert_close(values, original_values)
127
128
129 @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
130 def test_nvfp4_gathered_bias_rejects_non_16_group_size():
131 device = torch.device("cuda")
132 rank = 32
133 vocab_size = 2
134
135 with pytest.raises(ValueError, match="group size of 16"):
136 nvfp4_gathered_bias(
137 torch.zeros((1, rank), dtype=torch.bfloat16, device=device),
138 torch.zeros((vocab_size, rank // 2), dtype=torch.uint8, device=device),
139 torch.ones(
140 (vocab_size, rank // 32), dtype=torch.float8_e4m3fn, device=device
141 ),
142 torch.ones(1, dtype=torch.float32, device=device),
143 torch.zeros((1, 1), dtype=torch.bfloat16, device=device),
144 torch.zeros((1, 1), dtype=torch.int64, device=device),
145 torch.full(
146 (1, vocab_size), float("-inf"), dtype=torch.bfloat16, device=device
147 ),
148 1.0,
149 )
150
151
152 @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
153 @pytest.mark.parametrize("batch_size", [1, 2, 4, 8])
154 def test_nvfp4_gathered_markov_bias_kernel_matches_reference(batch_size: int):
155 torch.manual_seed(0)
156 device = torch.device("cuda")
157 topk = 256
158 rank = 512
159 vocab_size = 512
160 alpha = 0.75
161
162 packed_weight = torch.randint(
163 0, 256, (vocab_size, rank // 2), dtype=torch.uint8, device=device
164 )
165 weight_scale = torch.full(
166 (vocab_size, rank // 16),
167 0.5,
168 dtype=torch.float8_e4m3fn,
169 device=device,
170 )
171 global_scale = torch.tensor([0.25], dtype=torch.float32, device=device)
172
173 markov_storage = torch.randn(
174 batch_size, 2, rank, dtype=torch.bfloat16, device=device
175 )
176 values_storage = torch.randn(
177 batch_size, 2, topk, dtype=torch.bfloat16, device=device
178 )
179 index_storage = torch.empty(batch_size, 2, topk, dtype=torch.int64, device=device)
180 for batch_idx in range(batch_size):
181 index_storage[batch_idx, 0] = (
182 torch.arange(topk, device=device) + batch_idx
183 ) % vocab_size
184 logits_storage = torch.full(
185 (batch_size, 2, vocab_size),
186 float("-inf"),
187 dtype=torch.bfloat16,
188 device=device,
189 )
190
191 markov_embed = markov_storage[:, 0]
192 values = values_storage[:, 0]
193 index = index_storage[:, 0]
194 logits = logits_storage[:, 0]
195
196 layer = nn.Module()
197 layer.register_buffer("_nvfp4_weight_for_gather", packed_weight)
198 layer.register_buffer("_nvfp4_weight_scale_for_gather", weight_scale)
199 layer.register_buffer("_nvfp4_weight_global_scale_for_gather", global_scale)
200 layer._nvfp4_group_size_for_gather = 16
201 layer._retain_weight_for_gather = True
202 layer.is_w4a16_nvfp4 = True
203 head = DSparkMarkovHead.__new__(DSparkMarkovHead)
204 nn.Module.__init__(head)
205 head.markov_w2 = layer
206
207 flat_index = index.reshape(-1)
208 selected_weight = dequantize_to_dtype(
209 packed_weight.index_select(0, flat_index),
210 weight_scale.index_select(0, flat_index),
211 global_scale,
212 dtype=torch.bfloat16,
213 block_size=16,
214 swizzle=False,
215 ).view(batch_size, topk, rank)
216 expected = (
217 values
218 + torch.bmm(selected_weight, markov_embed.unsqueeze(-1)).squeeze(-1) * alpha
219 )
220
221 result = head.apply_bias_gathered(markov_embed, logits, values, index, alpha)
222
223 torch.testing.assert_close(
224 result.gather(1, index), expected, rtol=1e-2, atol=6.25e-2
225 )
226 selected = torch.zeros_like(result, dtype=torch.bool).scatter_(1, index, True)
227 assert torch.isneginf(result.masked_select(~selected)).all()
228
229 if batch_size == 1:
230 logits.fill_(float("-inf"))
231 graph = torch.cuda.CUDAGraph()
232 with torch.cuda.graph(graph):
233 captured = head.apply_bias_gathered(
234 markov_embed, logits, values, index, alpha
235 )
236 logits.fill_(float("-inf"))
237 graph.replay()
238 assert captured is logits
239 torch.testing.assert_close(
240 logits.gather(1, index), expected, rtol=1e-2, atol=6.25e-2
241 )