main
py 813 lines 26.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5 import math
6
7 import numpy
8 import torch
9
10 import vllm.envs as envs
11 from vllm.distributed.utils import verify_group_size_divides_partition
12 from vllm.logger import init_logger
13 from vllm.model_executor.layers.fused_moe import RoutedExperts
14 from vllm.model_executor.layers.fused_moe.config import FusedMoEConfig
15 from vllm.model_executor.layers.linear import LinearBase
16 from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
17 from vllm.model_executor.layers.quantization.utils.int8_utils import (
18 per_token_quant_int8,
19 )
20 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
21 from vllm.platforms import current_platform
22 from vllm.scalar_type import ScalarType, scalar_types
23 from vllm.utils.math_utils import round_up
24 from vllm.utils.platform_utils import num_compute_units
25 from vllm.utils.torch_utils import direct_register_custom_op
26
27 from .quant_utils import pack_cols, unpack_cols
28
29 logger = init_logger(__name__)
30
31 GPTQ_MARLIN_TILE = 16
32 GPTQ_MARLIN_MIN_THREAD_N = 64
33 GPTQ_MARLIN_MIN_THREAD_K = 128
34 GPTQ_MARLIN_MAX_PARALLEL = 16
35 MARLIN_MAX_BLOCKS_PER_SM = 4
36
37 MARLIN_SUPPORTED_GROUP_SIZES = [-1, 32, 64, 128]
38
39 # In case there is a performance issue with Marlin, the variable below can be
40 # changed to False, which allows Marlin to perform global reductions in fp16
41 # precision (instead of fp32), and therefore, save on some memory movements.
42 USE_FP32_REDUCE_DEFAULT = True
43
44
45 # For binary size and compile time, we don't support the same types for with and
46 # without runtime zero-point. We support common cases, i.e. AWQ and GPTQ.
47 # TODO: we may want to move this into the C++ so its closer to the actual impl
48 def query_marlin_supported_quant_types(
49 has_zp: bool | None = None,
50 include_fp_type: bool = True,
51 device_capability: int | None = None,
52 ):
53 if current_platform.is_cpu():
54 return _query_cpu_marlin_supported_quant_types(has_zp, include_fp_type)
55
56 if current_platform.is_xpu():
57 return [scalar_types.uint4, scalar_types.uint4b8]
58
59 if not current_platform.is_rocm():
60 if device_capability is None:
61 capability_tuple = current_platform.get_device_capability()
62 device_capability = (
63 -1 if capability_tuple is None else capability_tuple.to_int()
64 )
65
66 if device_capability < 75:
67 return []
68
69 # - has_zp is True: return quant_types that has zero points
70 # - has_zp is False: return quant_types that has not zero points
71 # - has_zp is None: both
72 if has_zp is None:
73 types0 = query_marlin_supported_quant_types(
74 False, include_fp_type, device_capability
75 )
76 types1 = query_marlin_supported_quant_types(
77 True, include_fp_type, device_capability
78 )
79 return types0 + types1
80
81 if has_zp:
82 # AWQ style, unsigned + runtime zero-point
83 return [scalar_types.uint4]
84 else:
85 # GPTQ style, unsigned + symmetric bias
86 res = [scalar_types.uint4b8, scalar_types.uint8b128]
87 if include_fp_type:
88 res += [scalar_types.float8_e4m3fn, scalar_types.float4_e2m1f]
89 return res
90
91
92 def _query_cpu_marlin_supported_quant_types(
93 has_zp: bool | None = None,
94 include_fp_type: bool = True,
95 ):
96 # - has_zp is True: return quant_types that has zero points
97 # - has_zp is False: return quant_types that has not zero points
98 # - has_zp is None: both
99 if has_zp is None:
100 types0 = _query_cpu_marlin_supported_quant_types(
101 False,
102 include_fp_type,
103 )
104 types1 = _query_cpu_marlin_supported_quant_types(
105 True,
106 include_fp_type,
107 )
108 return types0 + types1
109
110 if has_zp:
111 # AWQ style, unsigned + runtime zero-point
112 return [scalar_types.uint4]
113 else:
114 # GPTQ style, unsigned + symmetric bias, only supports 4-bits for now
115 res = [scalar_types.uint4b8]
116 return res
117
118
119 def _check_marlin_supported(
120 quant_type: ScalarType,
121 group_size: int | None,
122 has_zp: bool,
123 device_capability: int | None = None,
124 ) -> tuple[bool, str | None]:
125 if device_capability is None:
126 capability_tuple = current_platform.get_device_capability()
127 device_capability = (
128 -1 if capability_tuple is None else capability_tuple.to_int()
129 )
130
131 supported_types = query_marlin_supported_quant_types(
132 has_zp, True, device_capability
133 )
134
135 if quant_type not in supported_types:
136 return (
137 False,
138 f"Marlin does not support weight_bits = {quant_type}. "
139 f"Only types = {supported_types} "
140 f"are supported (for group_size = {group_size}, "
141 f"device_capability = {device_capability}, zp = {has_zp}).",
142 )
143 if group_size is None or group_size not in MARLIN_SUPPORTED_GROUP_SIZES:
144 return (
145 False,
146 f"Marlin does not support group_size = {group_size}. "
147 f"Only group_sizes = {MARLIN_SUPPORTED_GROUP_SIZES} "
148 "are supported.",
149 )
150
151 return True, None
152
153
154 def check_marlin_supported(
155 quant_type: ScalarType,
156 group_size: int,
157 has_zp: bool = False,
158 device_capability: int | None = None,
159 ) -> bool:
160 cond, _ = _check_marlin_supported(quant_type, group_size, has_zp, device_capability)
161 return cond
162
163
164 def verify_marlin_supported(
165 quant_type: ScalarType, group_size: int, has_zp: bool = False
166 ) -> None:
167 cond, err_msg = _check_marlin_supported(quant_type, group_size, has_zp)
168 if not cond:
169 assert err_msg is not None
170 raise ValueError(err_msg)
171
172
173 def verify_marlin_supports_shape(
174 output_size_per_partition: int,
175 input_size_per_partition: int,
176 input_size: int,
177 group_size: int,
178 ) -> None:
179 # Validate output_size_per_partition
180 if output_size_per_partition % GPTQ_MARLIN_MIN_THREAD_N != 0:
181 raise ValueError(
182 f"Weight output_size_per_partition = "
183 f"{output_size_per_partition} is not divisible by "
184 f" min_thread_n = {GPTQ_MARLIN_MIN_THREAD_N}. "
185 "Consider reducing tensor_parallel_size or running "
186 "with --quantization gptq."
187 )
188
189 # Validate input_size_per_partition
190 if input_size_per_partition % GPTQ_MARLIN_MIN_THREAD_K != 0:
191 raise ValueError(
192 f"Weight input_size_per_partition = "
193 f"{input_size_per_partition} is not divisible "
194 f"by min_thread_k = {GPTQ_MARLIN_MIN_THREAD_K}. "
195 "Consider reducing tensor_parallel_size or running "
196 "with --quantization gptq."
197 )
198
199 if group_size < input_size:
200 verify_group_size_divides_partition(
201 input_size_per_partition,
202 group_size,
203 extra_suggestion=" or running with --quantization gptq",
204 )
205
206
207 def check_marlin_supports_shape(
208 output_size_per_partition: int,
209 input_size_per_partition: int,
210 input_size: int,
211 group_size: int,
212 ) -> tuple[bool, str | None]:
213 try:
214 verify_marlin_supports_shape(
215 output_size_per_partition, input_size_per_partition, input_size, group_size
216 )
217 except ValueError as e:
218 return False, e.__str__()
219 return True, None
220
221
222 def marlin_padded_nk(size_n: int, size_k: int, group_size: int = -1) -> tuple[int, int]:
223 """Minimal (padded_n, padded_k) satisfying a Marlin thread-tile family.
224
225 Marlin GEMM and repack require (n % 64, k % 128) or (n % 128, k % 64);
226 shapes satisfying neither are zero-padded up to the cheaper family. K
227 stays divisible by group_size so padded scales keep an integral group
228 count. Padded weight regions contribute nothing to the GEMM output:
229 quantized value 0 decodes to 0.0 (FP4/FP8) or is cancelled by the
230 zero-padded scales/zero-points (INT).
231 """
232 group = group_size if group_size > 0 else 1
233 candidates = (
234 (round_up(size_n, 64), round_up(size_k, math.lcm(128, group))),
235 (round_up(size_n, 128), round_up(size_k, math.lcm(64, group))),
236 )
237 padded_nk = min(candidates, key=lambda nk: (nk[0] * nk[1], nk[0] + nk[1]))
238 if padded_nk != (size_n, size_k):
239 logger.warning_once(
240 "Marlin requires thread-tile padding for some weight shapes in "
241 "this model. Activations and/or outputs of the padded layers are "
242 "padded/sliced on every forward; performance may be degraded."
243 )
244 return padded_nk
245
246
247 def marlin_repacked_nk(qweight: torch.Tensor, num_bits: int) -> tuple[int, int]:
248 """Recover the (size_n, size_k) a Marlin weight was repacked with
249 (including any tile padding) from its packed shape."""
250 pack_factor = 32 // num_bits
251 size_k = qweight.size(0) * GPTQ_MARLIN_TILE
252 size_n = qweight.size(1) * pack_factor // GPTQ_MARLIN_TILE
253 return size_n, size_k
254
255
256 def marlin_pad_qweight(
257 qweight: torch.Tensor, size_n: int, size_k: int, padded_n: int, padded_k: int
258 ) -> torch.Tensor:
259 """Zero-pad a GPTQ-layout packed weight (size_k / pack, size_n) for
260 gptq_marlin_repack."""
261 if (padded_n, padded_k) == (size_n, size_k):
262 return qweight
263 pack_factor = size_k // qweight.size(0)
264 return torch.nn.functional.pad(
265 qweight, (0, padded_n - size_n, 0, (padded_k - size_k) // pack_factor)
266 )
267
268
269 def marlin_pad_scales(
270 scales: torch.Tensor,
271 size_n: int,
272 size_k: int,
273 padded_n: int,
274 padded_k: int,
275 group_size: int,
276 ) -> torch.Tensor:
277 """Zero-pad weight scales (num_groups, size_n); call before
278 marlin_permute_scales and pass the padded extents to it."""
279 if (padded_n, padded_k) == (size_n, size_k):
280 return scales
281 pad_rows = padded_k // group_size - scales.size(0) if group_size > 0 else 0
282 assert pad_rows >= 0
283 return torch.nn.functional.pad(scales, (0, padded_n - size_n, 0, pad_rows))
284
285
286 def marlin_pad_dim(x: torch.Tensor, size: int, padded: int) -> torch.Tensor:
287 """Zero-pad the last dim from size to padded (activations K, bias N)."""
288 if padded == size:
289 return x
290 return torch.nn.functional.pad(x, (0, padded - size))
291
292
293 def marlin_unpad_output(
294 output: torch.Tensor, size_n: int, padded_n: int
295 ) -> torch.Tensor:
296 """Strip padded output columns back to the logical N.
297
298 TODO: marlin_gemm could instead write the un-padded columns directly
299 into a caller-provided `c` buffer so this slice copy disappears.
300 """
301 if padded_n == size_n:
302 return output
303 return output[..., :size_n].contiguous()
304
305
306 def check_marlin_supports_layer(
307 layer: LinearBase, group_size: int, allow_tile_padding: bool = False
308 ) -> bool:
309 output_size_per_partition = (
310 getattr(layer, "output_size_per_partition", None) or layer.output_size
311 )
312 input_size_per_partition = (
313 getattr(layer, "input_size_per_partition", None) or layer.input_size
314 )
315
316 if allow_tile_padding:
317 # Thread-tile misalignment is fixed by zero-padding at weight prep
318 # (see marlin_padded_nk); only a quantization group straddling the
319 # TP shard remains unsupported. Dense layers only - MoE prep does
320 # not pad yet.
321 return (
322 group_size == -1
323 or group_size >= layer.input_size
324 or input_size_per_partition % group_size == 0
325 )
326
327 return check_marlin_supports_shape(
328 output_size_per_partition=output_size_per_partition,
329 input_size_per_partition=input_size_per_partition,
330 input_size=layer.input_size,
331 group_size=group_size,
332 )[0]
333
334
335 def marlin_moe_padded_intermediate(intermediate_size: int, group_size: int = -1) -> int:
336 """Smallest MoE intermediate size satisfying the Marlin MoE thread tiles.
337
338 The kernel needs gate-up ``2 * intermediate % 128 == 0`` and down
339 ``intermediate % 64 == 0``, i.e. ``intermediate % 64 == 0``. A misaligned
340 size is zero-padded to the next valid tile at weight prep, kept a multiple
341 of ``group_size`` so the group count stays integral. The padded region never
342 reaches the MoE output: w13's padded output channels are zeroed by the
343 zero-padded scales, so the padded inputs to w2 are zero.
344 """
345 group = group_size if group_size > 0 else 1
346 padded = round_up(intermediate_size, math.lcm(64, group))
347 if padded != intermediate_size:
348 logger.warning_once(
349 "Marlin requires thread-tile padding for the MoE intermediate size "
350 "of some layers in this model. Padded experts pad/slice activations "
351 "on every forward; performance may be degraded."
352 )
353 return padded
354
355
356 def check_moe_marlin_supports_config(
357 config: FusedMoEConfig,
358 group_size: int,
359 allow_tile_padding: bool = False,
360 ) -> bool:
361 """Whether the fused MoE Marlin kernel supports ``config``.
362
363 With ``allow_tile_padding=True``, a tile-misaligned intermediate size is
364 zero-padded to a valid thread tile at weight prep (see
365 marlin_moe_padded_intermediate), so only a group straddling the padded
366 boundary stays unsupported. hidden_size is the MoE I/O extent and is never
367 padded.
368 """
369 if current_platform.is_rocm():
370 return False
371 hidden_size = config.hidden_dim
372 # The layer has not rounded intermediate_size yet; use the stable unpadded
373 # size. gate-up needs n=2*intermediate % 128, down needs k=intermediate % 64.
374 intermediate_size_per_partition = config.intermediate_size_per_partition_unpadded
375 assert intermediate_size_per_partition is not None
376
377 if allow_tile_padding:
378 supports_shape = hidden_size % 128 == 0 and (
379 group_size <= 0 or intermediate_size_per_partition % group_size == 0
380 )
381 else:
382 supports_shape = (
383 hidden_size % 128 == 0
384 and intermediate_size_per_partition % max(64, group_size) == 0
385 )
386 supports_group_size = group_size in [-1, 32, 64, 128]
387 return supports_shape and supports_group_size
388
389
390 def check_moe_marlin_supports_layer(
391 layer: RoutedExperts,
392 group_size: int,
393 allow_tile_padding: bool = False,
394 ) -> bool:
395 return check_moe_marlin_supports_config(
396 layer.moe_config, group_size, allow_tile_padding
397 )
398
399
400 def marlin_moe_intermediate_size(w1_packed: torch.Tensor, w2_packed: torch.Tensor):
401 """Given Marlin packed weight matrices w1_packed, and w2_packed,
402 return the MoE intermediate size N
403 """
404 marlin_tile_size = 16
405 return w2_packed.size(1) * marlin_tile_size
406
407
408 def marlin_make_workspace_new(
409 device: torch.device,
410 max_blocks_per_sm: int = 1,
411 ) -> torch.Tensor:
412 # In the new marlin kernel, we use the num of threadblocks as workspace
413 # size. The num of threadblocks is sms_count * max_blocks_per_sm.
414 sms = num_compute_units(device.index)
415 return torch.zeros(sms * max_blocks_per_sm, dtype=torch.int, device=device)
416
417
418 def get_marlin_workspace(device: torch.device) -> torch.Tensor:
419 """Get zero-initialized locks for the current stream, ubatch, and lane.
420
421 Marlin resets its locks after each call, so ordered calls can reuse them.
422 """
423 from vllm.utils.torch_utils import current_stream
424 from vllm.v1.worker.workspace import (
425 current_workspace_manager,
426 is_workspace_manager_initialized,
427 )
428
429 if not is_workspace_manager_initialized():
430 return marlin_make_workspace_new(device, MARLIN_MAX_BLOCKS_PER_SM)
431
432 return current_workspace_manager().get_persistent(
433 ("marlin_locks", current_stream()),
434 (num_compute_units(device.index) * MARLIN_MAX_BLOCKS_PER_SM,),
435 torch.int,
436 zero_init=True,
437 )
438
439
440 def _marlin_gemm(
441 a: torch.Tensor,
442 b_q_weight: torch.Tensor,
443 b_bias: torch.Tensor | None,
444 b_scales: torch.Tensor,
445 a_scales: torch.Tensor | None,
446 global_scale: torch.Tensor | None,
447 b_zeros: torch.Tensor | None,
448 workspace: torch.Tensor | None,
449 b_q_type_id: int,
450 size_m: int,
451 size_n: int,
452 size_k: int,
453 use_atomic_add: bool = False,
454 use_fp32_reduce: bool = False,
455 is_zp_float: bool = False,
456 ) -> torch.Tensor:
457 if workspace is None:
458 workspace = get_marlin_workspace(a.device)
459 return torch.ops._C.marlin_gemm(
460 a,
461 None,
462 b_q_weight,
463 b_bias,
464 b_scales,
465 a_scales,
466 global_scale,
467 b_zeros,
468 workspace,
469 b_q_type_id,
470 size_m,
471 size_n,
472 size_k,
473 use_atomic_add,
474 use_fp32_reduce,
475 is_zp_float,
476 )
477
478
479 def _marlin_gemm_fake(
480 a: torch.Tensor,
481 b_q_weight: torch.Tensor,
482 b_bias: torch.Tensor | None,
483 b_scales: torch.Tensor,
484 a_scales: torch.Tensor | None,
485 global_scale: torch.Tensor | None,
486 b_zeros: torch.Tensor | None,
487 workspace: torch.Tensor | None,
488 b_q_type_id: int,
489 size_m: int,
490 size_n: int,
491 size_k: int,
492 use_atomic_add: bool = False,
493 use_fp32_reduce: bool = False,
494 is_zp_float: bool = False,
495 ) -> torch.Tensor:
496 dtype = a.dtype if a.dtype in (torch.float16, torch.bfloat16) else b_scales.dtype
497 return torch.empty((size_m, size_n), device=a.device, dtype=dtype)
498
499
500 direct_register_custom_op("marlin_gemm", _marlin_gemm, fake_impl=_marlin_gemm_fake)
501
502
503 def marlin_repeat_scales_on_all_ranks(group_size: int, is_row_parallel: bool) -> bool:
504 is_channelwise = group_size == -1
505 return is_channelwise and is_row_parallel
506
507
508 def marlin_make_empty(device: torch.device) -> torch.Tensor:
509 return torch.nn.Parameter(
510 torch.empty(0, dtype=torch.int, device=device), requires_grad=False
511 )
512
513
514 def get_scale_perms():
515 scale_perm: list[int] = []
516 for i in range(8):
517 scale_perm.extend([i + 8 * j for j in range(8)])
518 scale_perm_single: list[int] = []
519 for i in range(4):
520 scale_perm_single.extend([2 * i + j for j in [0, 1, 8, 9, 16, 17, 24, 25]])
521 return scale_perm, scale_perm_single
522
523
524 def marlin_permute_scales(
525 s: torch.Tensor, size_k: int, size_n: int, group_size: int, is_a_8bit: bool = False
526 ) -> torch.Tensor:
527 scale_perm, scale_perm_single = get_scale_perms()
528 if group_size < size_k and group_size != -1 and not is_a_8bit:
529 s = s.reshape((-1, len(scale_perm)))[:, scale_perm]
530 else:
531 s = s.reshape((-1, len(scale_perm_single)))[:, scale_perm_single]
532 s = s.reshape((-1, size_n)).contiguous()
533
534 return s
535
536
537 def marlin_permute_bias(s: torch.Tensor) -> torch.Tensor:
538 origin_shape = s.shape
539 _, scale_perm_single = get_scale_perms()
540 s = s.reshape((-1, len(scale_perm_single)))[:, scale_perm_single]
541 return s.reshape(*origin_shape).contiguous()
542
543
544 def marlin_act_int8_process_scales(s: torch.Tensor):
545 a_scales_scale_factor = 1 / 4096 * s.max().float()
546 s = s / s.max() * 4096
547 s = s.round().to(torch.int16).view(s.dtype)
548 return s, a_scales_scale_factor
549
550
551 def marlin_moe_permute_scales(
552 s: torch.Tensor, size_k: int, size_n: int, group_size: int, is_a_8bit: bool = False
553 ):
554 num_experts = s.shape[0]
555 output = torch.empty(
556 (num_experts, s.shape[1], s.shape[2]),
557 device=s.device,
558 dtype=s.dtype,
559 )
560
561 for e in range(num_experts):
562 output[e] = marlin_permute_scales(s[e], size_k, size_n, group_size, is_a_8bit)
563 return output
564
565
566 def marlin_zero_points(
567 zp: torch.Tensor, size_k: int, size_n: int, num_bits: int, is_a_8bit: bool = False
568 ) -> torch.Tensor:
569 # Permute zero-points in a similar way to scales, but do not use the
570 # "single" permutation, since zero-points are applied on every MMA
571 scale_perm, _ = get_scale_perms()
572 zp = zp.reshape((-1, len(scale_perm)))[:, scale_perm]
573
574 # Interleave column dim (for the dequantize code) and pack it to int32
575 if num_bits == 4:
576 interleave = numpy.array([0, 2, 4, 6, 1, 3, 5, 7])
577 elif num_bits == 8:
578 interleave = numpy.array([0, 2, 1, 3])
579 else:
580 raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
581
582 if not is_a_8bit:
583 zp = zp.reshape((-1, len(interleave)))[:, interleave].ravel()
584 zp = zp.reshape((-1, size_n)).contiguous()
585 zp = pack_cols(zp, num_bits, size_k, size_n)
586
587 return zp
588
589
590 def awq_to_marlin_zero_points(
591 q_zp_packed: torch.Tensor,
592 size_k: int,
593 size_n: int,
594 num_bits: int,
595 is_a_8bit: bool = False,
596 ) -> torch.Tensor:
597 # AWQ zero-points are quantized and packed on the column dim.
598 # In addition, the values are permuted based on dequantizer.
599 # Here we undo both of these, and then apply marlin permutation
600 # and pack it back.
601 q_zp = unpack_cols(q_zp_packed, num_bits, size_k, size_n)
602
603 # Undo interleaving (use argsort(..) to get inverse perm)
604 if num_bits == 4:
605 undo_interleave = numpy.argsort(numpy.array([0, 2, 4, 6, 1, 3, 5, 7]))
606 elif num_bits == 8:
607 undo_interleave = numpy.argsort(numpy.array([0, 2, 1, 3]))
608 else:
609 raise Exception("num_bits must be 4 or 8, got {}".format(num_bits))
610
611 q_zp = q_zp.reshape((-1, len(undo_interleave)))[:, undo_interleave].ravel()
612 q_zp = q_zp.reshape((-1, size_n)).contiguous()
613
614 marlin_zp = marlin_zero_points(q_zp, size_k, size_n, num_bits, is_a_8bit)
615 return marlin_zp
616
617
618 def moe_awq_to_marlin_zero_points(
619 q_zp_packed: torch.Tensor,
620 size_k: int,
621 size_n: int,
622 num_bits: int,
623 is_a_8bit: bool = False,
624 ):
625 num_experts = q_zp_packed.shape[0]
626 output = torch.empty(
627 (num_experts, q_zp_packed.shape[1], q_zp_packed.shape[2]),
628 device=q_zp_packed.device,
629 dtype=q_zp_packed.dtype,
630 )
631 for e in range(num_experts):
632 output[e] = awq_to_marlin_zero_points(
633 q_zp_packed[e], size_k, size_n, num_bits, is_a_8bit
634 )
635 return output
636
637
638 def maybe_warn_marlin_atomic_add(device, dtype):
639 if torch.compiler.is_dynamo_compiling():
640 return
641 device_capability = torch.cuda.get_device_capability(device)
642 if device_capability[0] < 9 and dtype == torch.bfloat16:
643 logger.info_once(
644 "You are running Marlin kernel with bf16 on GPUs before SM90. "
645 "You can consider change to fp16 to achieve better performance "
646 "if possible."
647 )
648
649
650 def moe_packed_to_marlin_zero_points(
651 q_zp_packed: torch.Tensor,
652 size_k: int,
653 size_n: int,
654 num_bits: int,
655 is_a_8bit: bool = False,
656 ):
657 """Convert compressed-tensors packed zero points to Marlin format.
658
659 Unlike AWQ, compressed-tensors uses standard bit packing without
660 interleaving, so we just unpack and apply Marlin permutation directly.
661 """
662 num_experts = q_zp_packed.shape[0]
663 output = torch.empty(
664 (num_experts, q_zp_packed.shape[1], q_zp_packed.shape[2]),
665 device=q_zp_packed.device,
666 dtype=q_zp_packed.dtype,
667 )
668 for e in range(num_experts):
669 q_zp = unpack_cols(q_zp_packed[e], num_bits, size_k, size_n)
670 output[e] = marlin_zero_points(q_zp, size_k, size_n, num_bits, is_a_8bit)
671 return output
672
673
674 def maybe_warn_marlin_atomic_add_env():
675 if torch.compiler.is_dynamo_compiling():
676 return
677 if envs.VLLM_MARLIN_USE_ATOMIC_ADD:
678 return
679 logger.info_once(
680 "Marlin kernel can achieve better performance for small size_n "
681 "with experimental use_atomic_add feature. "
682 "You can consider set environment variable "
683 "VLLM_MARLIN_USE_ATOMIC_ADD to 1 if possible."
684 )
685
686
687 def should_use_atomic_add_reduce(
688 m: int, n: int, k: int, device: torch.device, dtype: torch.dtype
689 ) -> bool:
690 # the performance of atomicAdd is better than global reduce
691 # only when m*n is small and k is large
692 if n >= 2048 or k < 2048 or device.type != "cuda":
693 return False
694
695 # disable atomicAdd reduce by default,
696 # one can enable it with VLLM_MARLIN_USE_ATOMIC_ADD=1
697 if not envs.VLLM_MARLIN_USE_ATOMIC_ADD:
698 maybe_warn_marlin_atomic_add_env()
699 return False
700
701 # sm8x doesn't support atomicAdd + bfloat16 natively
702 device_capability = torch.cuda.get_device_capability(device)
703 if device_capability[0] < 9 and dtype == torch.bfloat16:
704 maybe_warn_marlin_atomic_add(device, dtype)
705 return False
706
707 return True
708
709
710 _quant_fp8_method: QuantFP8 | None = None
711
712
713 def get__quant_fp8_method() -> QuantFP8:
714 global _quant_fp8_method
715 if _quant_fp8_method is None:
716 _quant_fp8_method = QuantFP8(False, GroupShape.PER_TOKEN)
717 return _quant_fp8_method
718
719
720 def get_marlin_input_dtype(prefix: str | None = None):
721 if envs.VLLM_MARLIN_INPUT_DTYPE is None:
722 return
723 elif envs.VLLM_MARLIN_INPUT_DTYPE.lower() == "int8":
724 return torch.int8
725 elif envs.VLLM_MARLIN_INPUT_DTYPE.lower() == "fp8":
726 if not current_platform.is_device_capability(
727 89
728 ) and not current_platform.is_device_capability_family(120):
729 raise ValueError(
730 "Marlin W4A8-FP8 only support SM89 or SM12x device "
731 "(It is slower than Marlin W4A16 on other devices). "
732 "You can consider using W4A8-INT8 instead"
733 "(set VLLM_MARLIN_INPUT_DTYPE=int8)."
734 )
735
736 _ = get__quant_fp8_method()
737 return torch.float8_e4m3fn
738 else:
739 return
740
741
742 def marlin_quant_input(x: torch.Tensor, quant_dtype: torch.dtype):
743 x = x.reshape(-1, x.shape[-1])
744 if quant_dtype == torch.int8:
745 return per_token_quant_int8(x)
746 elif quant_dtype == torch.float8_e4m3fn:
747 return get__quant_fp8_method()(x)
748 else:
749 raise ValueError(f"unsupported quant_dtype {quant_dtype}")
750
751
752 def apply_gptq_marlin_linear(
753 input: torch.Tensor,
754 weight: torch.Tensor,
755 weight_scale: torch.Tensor,
756 weight_zp: torch.Tensor,
757 workspace: torch.Tensor | None,
758 wtype: ScalarType,
759 output_size_per_partition: int,
760 input_size_per_partition: int,
761 input_global_scale: torch.Tensor | None = None,
762 bias: torch.Tensor | None = None,
763 use_fp32_reduce: bool = USE_FP32_REDUCE_DEFAULT,
764 input_dtype: torch.dtype | None = None,
765 ) -> torch.Tensor:
766 reshaped_x = input.reshape(-1, input.shape[-1])
767 out_shape = input.shape[:-1] + (output_size_per_partition,)
768
769 padded_n, padded_k = marlin_repacked_nk(weight, wtype.size_bits)
770 reshaped_x = marlin_pad_dim(reshaped_x, input_size_per_partition, padded_k)
771
772 use_atomic_add = should_use_atomic_add_reduce(
773 m=reshaped_x.size(0),
774 n=padded_n,
775 k=padded_k,
776 device=input.device,
777 dtype=input.dtype,
778 )
779
780 a_scales = None
781 if input_dtype == torch.int8:
782 assert wtype == scalar_types.uint4b8, (
783 "W8A8-INT8 is not supported by marlin kernel."
784 )
785 reshaped_x, a_scales = marlin_quant_input(reshaped_x, input_dtype)
786 a_scales = a_scales * input_global_scale
787 elif input_dtype == torch.float8_e4m3fn:
788 assert wtype == scalar_types.uint4b8, (
789 "INT8 weight + FP8 activation is not supported."
790 )
791
792 reshaped_x, a_scales = marlin_quant_input(reshaped_x, input_dtype)
793
794 output = torch.ops.vllm.marlin_gemm(
795 reshaped_x,
796 weight,
797 bias,
798 weight_scale,
799 a_scales,
800 None,
801 weight_zp,
802 workspace,
803 wtype.id,
804 size_m=reshaped_x.shape[0],
805 size_n=padded_n,
806 size_k=padded_k,
807 use_atomic_add=use_atomic_add,
808 use_fp32_reduce=use_fp32_reduce,
809 is_zp_float=False,
810 )
811
812 output = marlin_unpad_output(output, output_size_per_partition, padded_n)
813 return output.reshape(out_shape)