main
py 441 lines 14.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import torch
5
6 from vllm._custom_ops import scaled_fp4_quant
7 from vllm.model_executor.layers.fusion.quant_activation import (
8 QuantizedActivation,
9 as_quantized_activation,
10 )
11 from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
12 nvfp4_weight_padding_bytes,
13 pad_nvfp4_activation_for_cutlass,
14 pad_nvfp4_weight_for_cutlass,
15 slice_nvfp4_output,
16 swizzle_blockscale,
17 )
18 from vllm.model_executor.layers.quantization.utils.quant_utils import (
19 QuantKey,
20 kNvfp4Dynamic,
21 )
22 from vllm.platforms import current_platform
23 from vllm.utils.flashinfer import (
24 flashinfer_prepare_bf16_fp4_weights,
25 flashinfer_scaled_fp4_mm,
26 has_flashinfer,
27 has_flashinfer_b12x_gemm,
28 has_flashinfer_bf16_fp4,
29 )
30
31 from .base import NvFp4LinearKernel, NvFp4LinearLayerConfig
32
33
34 class FlashInferCuteDslNvFp4W4A16LinearKernel(NvFp4LinearKernel):
35 """BF16 x NVFP4 GEMM via FlashInfer's CuTe-DSL backend."""
36
37 @classmethod
38 def is_supported(
39 cls, compute_capability: int | None = None
40 ) -> tuple[bool, str | None]:
41 if compute_capability is None:
42 if not current_platform.is_cuda():
43 return False, "FlashInfer CuTe-DSL W4A16 requires CUDA"
44 capability = current_platform.get_device_capability()
45 if capability is None:
46 return False, "CUDA compute capability is unavailable"
47 compute_capability = capability.to_int()
48
49 if compute_capability not in (100, 103) and not (
50 compute_capability >= 120 and compute_capability < 130
51 ):
52 return False, "FlashInfer CuTe-DSL W4A16 requires sm_100 or sm_12x"
53 if not has_flashinfer_bf16_fp4():
54 return False, "FlashInfer CuTe-DSL BF16 x FP4 GEMM is unavailable"
55 return True, None
56
57 @classmethod
58 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
59 return True, None
60
61 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
62 padded_weight, weights_padding_bytes = pad_nvfp4_weight_for_cutlass(
63 layer.weight.data, alignment=64
64 )
65 swizzled_scale = swizzle_blockscale(layer.weight_scale.data)
66 weight, weight_scale, global_scale = flashinfer_prepare_bf16_fp4_weights(
67 padded_weight,
68 swizzled_scale,
69 layer.weight_global_scale.data.reshape(1),
70 backend="cute-dsl",
71 )
72 assert global_scale is not None
73
74 layer.weight = torch.nn.Parameter(weight, requires_grad=False)
75 layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False)
76 layer.weight_global_scale = torch.nn.Parameter(
77 global_scale, requires_grad=False
78 )
79 layer.weights_padding_cols = weights_padding_bytes
80
81 def apply_weights(
82 self,
83 layer: torch.nn.Module,
84 x: torch.Tensor,
85 bias: torch.Tensor | None = None,
86 ) -> torch.Tensor:
87 if x.dtype != torch.bfloat16:
88 raise ValueError(
89 f"FlashInfer CuTe-DSL W4A16 requires BF16 input, got {x.dtype}"
90 )
91
92 output_size = layer.output_size_per_partition
93 output_shape = [*x.shape[:-1], output_size]
94 x_2d = x.reshape(-1, x.shape[-1])
95 weights_padding_bytes = getattr(layer, "weights_padding_cols", 0)
96 if weights_padding_bytes:
97 x_2d = torch.nn.functional.pad(x_2d, (0, weights_padding_bytes * 2))
98 x_2d = x_2d.contiguous()
99
100 out = torch.ops.vllm.flashinfer_mm_bf16_fp4(
101 x_2d,
102 layer.weight,
103 layer.weight_scale,
104 layer.weight_global_scale,
105 )
106 out = slice_nvfp4_output(out, output_size)
107 if bias is not None:
108 out = out + bias
109 return out.view(*output_shape)
110
111
112 class FlashInferCuteDslNvFp4LinearKernel(NvFp4LinearKernel):
113 """NVFP4 GEMM via FlashInfer's cutedsl backend."""
114
115 @classmethod
116 def is_supported(
117 cls, compute_capability: int | None = None
118 ) -> tuple[bool, str | None]:
119 if not current_platform.is_device_capability_family(100):
120 return False, "FlashInfer cutedsl requires sm_10x"
121 if not has_flashinfer():
122 return False, "FlashInfer required"
123 return True, None
124
125 @classmethod
126 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
127 return True, None
128
129 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
130 # cutedsl uses the same swizzled + padded layout as cutlass.
131 layer.weight_scale = torch.nn.Parameter(
132 swizzle_blockscale(layer.weight_scale.data), requires_grad=False
133 )
134 padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
135 layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
136
137 def apply_weights(
138 self,
139 layer: torch.nn.Module,
140 x: torch.Tensor,
141 bias: torch.Tensor | None = None,
142 ) -> torch.Tensor:
143 output_size = layer.output_size_per_partition
144 output_dtype = x.dtype
145 output_shape = [*x.shape[:-1], output_size]
146
147 x_fp4, x_blockscale = scaled_fp4_quant(
148 x,
149 layer.input_global_scale_inv,
150 is_sf_swizzled_layout=True,
151 backend="flashinfer-cutedsl",
152 )
153
154 x_fp4 = pad_nvfp4_activation_for_cutlass(
155 x_fp4, nvfp4_weight_padding_bytes(layer)
156 )
157
158 out = flashinfer_scaled_fp4_mm(
159 x_fp4,
160 layer.weight,
161 x_blockscale,
162 layer.weight_scale,
163 layer.alpha,
164 output_dtype,
165 backend="cute-dsl",
166 )
167
168 out = slice_nvfp4_output(out, output_size)
169
170 if bias is not None:
171 out = out + bias
172 return out.view(*output_shape)
173
174
175 class FlashInferCutlassNvFp4LinearKernel(NvFp4LinearKernel):
176 """NVFP4 GEMM via FlashInfer's CUTLASS wrapper."""
177
178 def input_quant_key(self) -> QuantKey | None:
179 """This kernel supports dynamic quantization of the input. By
180 convention, pre-quantized blockscales must use the swizzled layout."""
181 return kNvfp4Dynamic
182
183 @classmethod
184 def is_supported(
185 cls, compute_capability: int | None = None
186 ) -> tuple[bool, str | None]:
187 from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
188 cutlass_fp4_supported,
189 )
190
191 if (
192 cutlass_fp4_supported()
193 and current_platform.has_device_capability(100)
194 and has_flashinfer()
195 ):
196 return True, None
197 return False, "FlashInfer + >=sm_100 required"
198
199 @classmethod
200 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
201 return True, None
202
203 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
204 layer.weight_scale = torch.nn.Parameter(
205 swizzle_blockscale(layer.weight_scale.data), requires_grad=False
206 )
207 padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
208 layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
209
210 def apply_weights(
211 self,
212 layer: torch.nn.Module,
213 x: torch.Tensor | QuantizedActivation,
214 bias: torch.Tensor | None = None,
215 ) -> torch.Tensor:
216 output_size = layer.output_size_per_partition
217 weights_padding_bytes = nvfp4_weight_padding_bytes(layer)
218
219 qa = as_quantized_activation(x, self.input_quant_key())
220 if qa is not None:
221 x_fp4, x_blockscale = qa.data, qa.scale
222 x_fp4 = pad_nvfp4_activation_for_cutlass(x_fp4, weights_padding_bytes)
223 output_dtype = qa.orig_dtype
224 output_shape = [*qa.orig_shape[:-1], output_size]
225 else:
226 assert isinstance(x, torch.Tensor)
227 output_dtype = x.dtype
228 output_shape = [*x.shape[:-1], output_size]
229 x_fp4, x_blockscale = scaled_fp4_quant(
230 x,
231 layer.input_global_scale_inv,
232 is_sf_swizzled_layout=True,
233 backend="flashinfer-cutlass",
234 padded_n=x.shape[-1] + weights_padding_bytes * 2,
235 )
236
237 out = flashinfer_scaled_fp4_mm(
238 x_fp4,
239 layer.weight,
240 x_blockscale,
241 layer.weight_scale,
242 layer.alpha,
243 output_dtype,
244 backend="cutlass",
245 )
246
247 out = slice_nvfp4_output(out, output_size)
248
249 if bias is not None:
250 out = out + bias
251 return out.view(*output_shape)
252
253
254 class FlashInferTrtllmNvFp4LinearKernel(NvFp4LinearKernel):
255 """NVFP4 GEMM via FlashInfer's TensorRT-LLM wrapper."""
256
257 @classmethod
258 def is_supported(
259 cls, compute_capability: int | None = None
260 ) -> tuple[bool, str | None]:
261 if has_flashinfer():
262 return True, None
263 return False, "FlashInfer required"
264
265 @classmethod
266 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
267 return True, None
268
269 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
270 from flashinfer import shuffle_matrix_a, shuffle_matrix_sf_a
271
272 weight = layer.weight.data
273 weight_scale = layer.weight_scale.data
274 epilogue_tile_m = 128
275
276 layer.weight = torch.nn.Parameter(
277 shuffle_matrix_a(weight.view(torch.uint8), epilogue_tile_m),
278 requires_grad=False,
279 )
280 layer.weight_scale = torch.nn.Parameter(
281 shuffle_matrix_sf_a(weight_scale.view(torch.uint8), epilogue_tile_m)
282 .reshape(weight_scale.shape)
283 .view(torch.float8_e4m3fn),
284 requires_grad=False,
285 )
286
287 def apply_weights(
288 self,
289 layer: torch.nn.Module,
290 x: torch.Tensor,
291 bias: torch.Tensor | None = None,
292 ) -> torch.Tensor:
293 output_size = layer.output_size_per_partition
294 output_dtype = x.dtype
295 output_shape = [*x.shape[:-1], output_size]
296
297 x_fp4, x_blockscale = scaled_fp4_quant(
298 x,
299 layer.input_global_scale_inv,
300 is_sf_swizzled_layout=True,
301 backend="flashinfer-trtllm",
302 )
303
304 out = flashinfer_scaled_fp4_mm(
305 x_fp4,
306 layer.weight,
307 x_blockscale,
308 layer.weight_scale,
309 layer.alpha,
310 output_dtype,
311 backend="trtllm",
312 )
313
314 out = slice_nvfp4_output(out, output_size)
315
316 if bias is not None:
317 out = out + bias
318 return out.view(*output_shape)
319
320
321 class FlashInferCudnnNvFp4LinearKernel(NvFp4LinearKernel):
322 """NVFP4 GEMM via FlashInfer's cuDNN wrapper."""
323
324 @classmethod
325 def is_supported(
326 cls, compute_capability: int | None = None
327 ) -> tuple[bool, str | None]:
328 if has_flashinfer():
329 return True, None
330 return False, "FlashInfer required"
331
332 @classmethod
333 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
334 return True, None
335
336 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
337 # cuDNN uses the same swizzled + padded layout as CUTLASS
338 layer.weight_scale = torch.nn.Parameter(
339 swizzle_blockscale(layer.weight_scale.data), requires_grad=False
340 )
341 padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
342 layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
343
344 def apply_weights(
345 self,
346 layer: torch.nn.Module,
347 x: torch.Tensor,
348 bias: torch.Tensor | None = None,
349 ) -> torch.Tensor:
350 output_size = layer.output_size_per_partition
351 output_dtype = x.dtype
352 output_shape = [*x.shape[:-1], output_size]
353 weights_padding_bytes = nvfp4_weight_padding_bytes(layer)
354
355 x_fp4, x_blockscale = scaled_fp4_quant(
356 x,
357 layer.input_global_scale_inv,
358 is_sf_swizzled_layout=True,
359 backend="flashinfer-cudnn",
360 padded_n=x.shape[-1] + weights_padding_bytes * 2,
361 )
362
363 out = flashinfer_scaled_fp4_mm(
364 x_fp4,
365 layer.weight,
366 x_blockscale,
367 layer.weight_scale,
368 layer.alpha,
369 output_dtype,
370 backend="cudnn",
371 )
372
373 out = slice_nvfp4_output(out, output_size)
374
375 if bias is not None:
376 out = out + bias
377 return out.view(*output_shape)
378
379
380 class FlashInferB12xNvFp4LinearKernel(NvFp4LinearKernel):
381 """NVFP4 GEMM via FlashInfer's b12x CuTe DSL warp-level MMA kernel (SM120+)."""
382
383 @classmethod
384 def is_supported(
385 cls, compute_capability: int | None = None
386 ) -> tuple[bool, str | None]:
387 if current_platform.has_device_capability(120) and has_flashinfer_b12x_gemm():
388 return True, None
389 return (
390 False,
391 "FlashInfer b12x requires SM120+ and FlashInfer "
392 "with Sm120BlockScaledDenseGemmKernel",
393 )
394
395 @classmethod
396 def can_implement(cls, config: NvFp4LinearLayerConfig) -> tuple[bool, str | None]:
397 return True, None
398
399 def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
400 layer.weight_scale = torch.nn.Parameter(
401 swizzle_blockscale(layer.weight_scale.data), requires_grad=False
402 )
403 padded_weight, _ = pad_nvfp4_weight_for_cutlass(layer.weight.data)
404 layer.weight = torch.nn.Parameter(padded_weight, requires_grad=False)
405
406 def apply_weights(
407 self,
408 layer: torch.nn.Module,
409 x: torch.Tensor,
410 bias: torch.Tensor | None = None,
411 ) -> torch.Tensor:
412 output_size = layer.output_size_per_partition
413 output_dtype = x.dtype
414 output_shape = [*x.shape[:-1], output_size]
415
416 x_fp4, x_blockscale = scaled_fp4_quant(
417 x,
418 layer.input_global_scale_inv,
419 is_sf_swizzled_layout=True,
420 backend="b12x",
421 )
422
423 x_fp4 = pad_nvfp4_activation_for_cutlass(
424 x_fp4, nvfp4_weight_padding_bytes(layer)
425 )
426
427 out = flashinfer_scaled_fp4_mm(
428 x_fp4,
429 layer.weight,
430 x_blockscale,
431 layer.weight_scale,
432 layer.alpha,
433 output_dtype,
434 backend="b12x",
435 )
436
437 out = slice_nvfp4_output(out, output_size)
438
439 if bias is not None:
440 out = out + bias
441 return out.view(*output_shape)