A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 5, 02:15 UTC
[ROCm][FEAT] Integrate aiter gemm w8a8 ptpc (#33773)
Signed-off-by: vllmellm <vllm.ellm@embeddedllm.com>
vllmellm committed
Apr 16, 2026 at 09:55 UTC
5f7fab881a13df2926f50cb8d9f67b0bbc2ee41f
9 files changed
+279
-23
tests/utils.py
+1
@@ -1856,6 +1856,7 @@ class TestFP8Layer(torch.nn.Module):
1856
out_dtype=out_dtype,
1857
force_kernel=force_kernel,
1858
)
1859
+ self.kernel.process_weights_after_loading(self)
1860
1861
def is_quant_fp8_enabled(self) -> bool:
1862
return self.kernel.quant_fp8.enabled()
vllm/_aiter_ops.py
+97
-9
@@ -3,6 +3,7 @@
3
import functools
4
from collections.abc import Callable
5
6
+import pandas as pd
7
import torch
8
from torch._ops import OpOverload
9
@@ -56,6 +57,29 @@ def is_aiter_found_and_supported() -> bool:
57
return False
58
59
60
+@functools.cache
61
+def _load_gemm_tuned_configs(
62
+ q_dtype_w: torch.dtype, csv_path: str
63
+) -> set[tuple[int, int, int]]:
64
+ try:
65
+ df = pd.read_csv(csv_path).drop_duplicates()
66
+ df = df[df["q_dtype_w"] == str(q_dtype_w)]
67
+ return set(zip(df["N"].astype(int), df["K"].astype(int), df["M"].astype(int)))
68
+ except Exception:
69
+ return set()
70
+
71
+
72
+def _check_kernel_tuned(N: int, K: int, q_dtype_w: torch.dtype, csv_path: str) -> bool:
73
+ configs = _load_gemm_tuned_configs(q_dtype_w, csv_path)
74
+ l_m = (
75
+ [1, 2, 4]
76
+ + list(range(8, 513, 8))
77
+ + [1024, 1536]
78
+ + [2**i for i in range(11, 19)]
79
+ )
80
+ return any((N, K, M) in configs for M in l_m)
81
+
82
+
83
def if_aiter_supported(func: Callable) -> Callable:
84
"""Decorator that only executes the function if
85
ROCm AITER package is supported and enabled on gfx9 archs.
@@ -468,7 +492,7 @@ def _rocm_aiter_mla_decode_fwd_fake(
492
pass
493
494
471
-def _rocm_aiter_gemm_a8w8_impl(
495
+def _rocm_aiter_w8a8_gemm_impl(
496
A: torch.Tensor,
497
B: torch.Tensor,
498
As: torch.Tensor,
@@ -485,7 +509,7 @@ def _rocm_aiter_gemm_a8w8_impl(
509
return gemm_a8w8_CK(A, B, As, Bs, bias, output_dtype)
510
511
488
-def _rocm_aiter_gemm_a8w8_fake(
512
+def _rocm_aiter_w8a8_gemm_fake(
513
A: torch.Tensor,
514
B: torch.Tensor,
515
As: torch.Tensor,
@@ -499,6 +523,35 @@ def _rocm_aiter_gemm_a8w8_fake(
523
return Y
524
525
526
+def _rocm_aiter_preshuffled_per_token_w8a8_gemm_impl(
527
+ A: torch.Tensor,
528
+ B: torch.Tensor,
529
+ As: torch.Tensor,
530
+ Bs: torch.Tensor,
531
+ bias: torch.Tensor | None = None,
532
+ output_dtype: torch.dtype = torch.float16,
533
+) -> torch.Tensor:
534
+ from aiter import gemm_a8w8_bpreshuffle
535
+
536
+ output = gemm_a8w8_bpreshuffle(A, B, As, Bs, None, output_dtype)
537
+ if bias is not None:
538
+ output.add_(bias)
539
+ return output
540
+
541
+
542
+def _rocm_aiter_preshuffled_per_token_w8a8_gemm_fake(
543
+ A: torch.Tensor,
544
+ B: torch.Tensor,
545
+ As: torch.Tensor,
546
+ Bs: torch.Tensor,
547
+ bias: torch.Tensor | None = None,
548
+ output_dtype: torch.dtype = torch.float16,
549
+) -> torch.Tensor:
550
+ m = A.shape[0]
551
+ n = B.shape[0]
552
+ return torch.empty(m, n, dtype=output_dtype, device=A.device)
553
+
554
+
555
def _rocm_aiter_triton_gemm_a8w8_blockscale_impl(
556
A: torch.Tensor,
557
B: torch.Tensor,
@@ -1313,11 +1366,15 @@ class rocm_aiter_ops:
1366
)
1367
1368
direct_register_custom_op(
1316
- op_name="rocm_aiter_gemm_a8w8",
1317
- op_func=_rocm_aiter_gemm_a8w8_impl,
1318
- mutates_args=[],
1319
- fake_impl=_rocm_aiter_gemm_a8w8_fake,
1320
- dispatch_key=current_platform.dispatch_key,
1369
+ op_name="rocm_aiter_w8a8_gemm",
1370
+ op_func=_rocm_aiter_w8a8_gemm_impl,
1371
+ fake_impl=_rocm_aiter_w8a8_gemm_fake,
1372
+ )
1373
+
1374
+ direct_register_custom_op(
1375
+ op_name="_rocm_aiter_preshuffled_per_token_w8a8_gemm",
1376
+ op_func=_rocm_aiter_preshuffled_per_token_w8a8_gemm_impl,
1377
+ fake_impl=_rocm_aiter_preshuffled_per_token_w8a8_gemm_fake,
1378
)
1379
1380
direct_register_custom_op(
@@ -1493,7 +1550,7 @@ class rocm_aiter_ops:
1550
)
1551
1552
@staticmethod
1496
- def gemm_a8w8(
1553
+ def w8a8_gemm(
1554
A: torch.Tensor,
1555
B: torch.Tensor,
1556
As: torch.Tensor,
@@ -1501,7 +1558,20 @@ class rocm_aiter_ops:
1558
bias: torch.Tensor | None = None,
1559
output_dtype: torch.dtype = torch.float16,
1560
) -> torch.Tensor:
1504
- return torch.ops.vllm.rocm_aiter_gemm_a8w8(A, B, As, Bs, bias, output_dtype)
1561
+ return torch.ops.vllm.rocm_aiter_w8a8_gemm(A, B, As, Bs, bias, output_dtype)
1562
+
1563
+ @staticmethod
1564
+ def preshuffled_per_token_w8a8_gemm(
1565
+ A: torch.Tensor,
1566
+ B: torch.Tensor,
1567
+ As: torch.Tensor,
1568
+ Bs: torch.Tensor,
1569
+ bias: torch.Tensor | None = None,
1570
+ output_dtype: torch.dtype = torch.float16,
1571
+ ) -> torch.Tensor:
1572
+ return torch.ops.vllm._rocm_aiter_preshuffled_per_token_w8a8_gemm(
1573
+ A, B, As, Bs, bias, output_dtype
1574
+ )
1575
1576
@staticmethod
1577
def triton_gemm_a8w8_blockscale(
@@ -1920,6 +1990,24 @@ class rocm_aiter_ops:
1990
(8192, 3584),
1991
]
1992
1993
+ @staticmethod
1994
+ def is_shuffled_per_token_w8a8_gemm_tuned(
1995
+ N: int, K: int, q_dtype_w: torch.dtype
1996
+ ) -> bool:
1997
+ import aiter.ops.gemm_op_a8w8 as aiter_gemm_a8w8_ops
1998
+
1999
+ csv_path = (
2000
+ aiter_gemm_a8w8_ops.AITER_CONFIGS.AITER_CONFIG_GEMM_A8W8_BPRESHUFFLE_FILE
2001
+ )
2002
+ return _check_kernel_tuned(N, K, q_dtype_w, csv_path)
2003
+
2004
+ @staticmethod
2005
+ def is_per_token_w8a8_gemm_tuned(N: int, K: int, q_dtype_w: torch.dtype) -> bool:
2006
+ import aiter.ops.gemm_op_a8w8 as aiter_gemm_a8w8_ops
2007
+
2008
+ csv_path = aiter_gemm_a8w8_ops.AITER_CONFIGS.AITER_CONFIG_GEMM_A8W8_FILE
2009
+ return _check_kernel_tuned(N, K, q_dtype_w, csv_path)
2010
+
2011
@staticmethod
2012
def shuffle_weight(
2013
tensor: torch.Tensor, layout: tuple[int, int] = (16, 16)
vllm/model_executor/kernels/linear/__init__.py
+9
-3
@@ -106,6 +106,8 @@ from vllm.model_executor.kernels.linear.scaled_mm import (
106
from vllm.model_executor.kernels.linear.scaled_mm.aiter import (
107
AiterFp8BlockScaledMMKernel,
108
AiterInt8ScaledMMLinearKernel,
109
+ AiterPerTokenFp8ScaledMMLinearKernel,
110
+ AiterPreshuffledPerTokenFp8ScaledMMLinearKernel,
111
)
112
from vllm.model_executor.kernels.linear.scaled_mm.cpu import (
113
CPUInt8ScaledMMLinearKernel,
@@ -165,6 +167,8 @@ _POSSIBLE_FP8_KERNELS: dict[PlatformEnum, list[type[FP8ScaledMMLinearKernel]]] =
167
ChannelWiseTorchFP8ScaledMMLinearKernel,
168
],
169
PlatformEnum.ROCM: [
170
+ AiterPreshuffledPerTokenFp8ScaledMMLinearKernel,
171
+ AiterPerTokenFp8ScaledMMLinearKernel,
172
ROCmFP8ScaledMMLinearKernel,
173
PerTensorTorchFP8ScaledMMLinearKernel,
174
RowWiseTorchFP8ScaledMMLinearKernel,
@@ -360,18 +364,18 @@ def choose_scaled_mm_linear_kernel(
364
def init_fp8_linear_kernel(
365
activation_quant_key: QuantKey,
366
weight_quant_key: QuantKey,
363
- weight_shape: tuple[int, int],
367
input_dtype: torch.dtype,
368
out_dtype: torch.dtype,
366
- force_kernel: type[_KernelT] | None = None,
369
+ weight_shape: tuple[int, int],
370
+ force_kernel: type[FP8ScaledMMLinearKernel] | None = None,
371
module_name: str | None = None,
372
) -> FP8ScaledMMLinearKernel | Fp8BlockScaledMMLinearKernel:
373
scaled_mm_linear_kernel_config = FP8ScaledMMLinearLayerConfig(
374
weight_quant_key=weight_quant_key,
375
activation_quant_key=activation_quant_key,
372
- weight_shape=weight_shape,
376
input_dtype=input_dtype,
377
out_dtype=out_dtype,
378
+ weight_shape=weight_shape,
379
)
380
381
if activation_quant_key.scale.group_shape.is_per_group():
@@ -725,6 +729,8 @@ __all__ = [
729
"FP8ScaledMMLinearLayerConfig",
730
"Int8ScaledMMLinearLayerConfig",
731
"ScaledMMLinearLayerConfig",
732
+ "AiterPreshuffledPerTokenFp8ScaledMMLinearKernel",
733
+ "AiterPerTokenFp8ScaledMMLinearKernel",
734
"NvFp4LinearKernel",
735
"NvFp4LinearLayerConfig",
736
"AiterInt8ScaledMMLinearKernel",
vllm/model_executor/kernels/linear/scaled_mm/aiter.py
+160
-4
@@ -5,18 +5,27 @@
5
import torch
6
7
from vllm import _custom_ops as ops
8
-from vllm._aiter_ops import rocm_aiter_ops
8
+from vllm._aiter_ops import (
9
+ rocm_aiter_ops,
10
+)
11
+from vllm.logger import init_logger
12
from vllm.model_executor.layers.quantization.utils.quant_utils import (
13
GroupShape,
14
)
15
+from vllm.model_executor.utils import replace_parameter
16
from vllm.platforms import current_platform
17
18
from .BlockScaledMMLinearKernel import (
19
Fp8BlockScaledMMLinearKernel,
16
- FP8ScaledMMLinearLayerConfig,
20
)
21
from .cutlass import CutlassInt8ScaledMMLinearKernel
19
-from .ScaledMMLinearKernel import Int8ScaledMMLinearLayerConfig
22
+from .ScaledMMLinearKernel import (
23
+ FP8ScaledMMLinearKernel,
24
+ FP8ScaledMMLinearLayerConfig,
25
+ Int8ScaledMMLinearLayerConfig,
26
+)
27
+
28
+logger = init_logger(__name__)
29
30
31
class AiterInt8ScaledMMLinearKernel(CutlassInt8ScaledMMLinearKernel):
@@ -113,7 +122,154 @@ class AiterInt8ScaledMMLinearKernel(CutlassInt8ScaledMMLinearKernel):
122
# a to be [M, K]
123
# b to be [N, K]
124
# CutlassInt8ScaledMMLinearKernel prepare weight `w_q` in [K, N] format
116
- return rocm_aiter_ops.gemm_a8w8(x_q, w_q.t(), x_s, w_s, bias, out_dtype)
125
+ return rocm_aiter_ops.w8a8_gemm(x_q, w_q.t(), x_s, w_s, bias, out_dtype)
126
+
127
+
128
+class AiterPreshuffledPerTokenFp8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
129
+ @classmethod
130
+ def is_supported(
131
+ cls, compute_capability: int | None = None
132
+ ) -> tuple[bool, str | None]:
133
+ if not current_platform.is_rocm():
134
+ return False, "requires ROCm."
135
+ if not rocm_aiter_ops.is_linear_fp8_enabled():
136
+ return (
137
+ False,
138
+ "requires setting `VLLM_ROCM_USE_AITER=1` "
139
+ "and `VLLM_ROCM_USE_AITER_LINEAR=1`. "
140
+ "`VLLM_ROCM_USE_AITER_LINEAR` default is True.",
141
+ )
142
+ try:
143
+ import aiter # noqa: F401
144
+ except Exception:
145
+ return False, "requires aiter library to be installed."
146
+ return True, None
147
+
148
+ @classmethod
149
+ def can_implement(cls, c: FP8ScaledMMLinearLayerConfig) -> tuple[bool, str | None]:
150
+ is_ptpc = (
151
+ c.activation_quant_key.scale.group_shape.is_per_token()
152
+ and c.weight_quant_key.scale.group_shape.is_per_channel()
153
+ )
154
+ if c.weight_shape is None:
155
+ return False, "weight_shape is required for Aiter kernels"
156
+ N, K = c.weight_shape
157
+ fp8_dtype = current_platform.fp8_dtype()
158
+
159
+ if c.out_dtype is not torch.bfloat16:
160
+ return False, "requires bfloat16 output dtype."
161
+
162
+ if not is_ptpc:
163
+ return (
164
+ False,
165
+ "requires per token activation scales and per channel weight scales.",
166
+ )
167
+
168
+ if not (N % 16 == 0 and K % 16 == 0):
169
+ return (
170
+ False,
171
+ f"requires N and K dimensions divisible by 16, received "
172
+ f"N={N} and K={K}.",
173
+ )
174
+
175
+ # Aiter's shuffled per-token Gemm performs better than torch only when its
176
+ # tuned.
177
+ if not rocm_aiter_ops.is_shuffled_per_token_w8a8_gemm_tuned(N, K, fp8_dtype):
178
+ return (
179
+ False,
180
+ f"requires a tuned configuration for N: {N} and K: {K} "
181
+ f"and fp8 dtype {fp8_dtype}.",
182
+ )
183
+
184
+ return True, None
185
+
186
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
187
+ w_name, *_ = self.layer_param_names
188
+ w, *_ = self._get_layer_params(layer)
189
+
190
+ replace_parameter(
191
+ layer,
192
+ w_name,
193
+ torch.nn.Parameter(
194
+ rocm_aiter_ops.shuffle_weight(w.t().contiguous()).data,
195
+ requires_grad=False,
196
+ ),
197
+ )
198
+
199
+ def apply_scaled_mm(
200
+ self,
201
+ *,
202
+ A: torch.Tensor,
203
+ B: torch.Tensor,
204
+ out_dtype: torch.dtype,
205
+ As: torch.Tensor,
206
+ Bs: torch.Tensor,
207
+ bias: torch.Tensor | None,
208
+ output_shape: list,
209
+ ) -> torch.Tensor:
210
+ return rocm_aiter_ops.preshuffled_per_token_w8a8_gemm(
211
+ A, B, As, Bs, bias, out_dtype
212
+ )
213
+
214
+
215
+class AiterPerTokenFp8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
216
+ @classmethod
217
+ def is_supported(
218
+ cls, compute_capability: int | None = None
219
+ ) -> tuple[bool, str | None]:
220
+ return AiterPreshuffledPerTokenFp8ScaledMMLinearKernel.is_supported(
221
+ compute_capability
222
+ )
223
+
224
+ @classmethod
225
+ def can_implement(cls, c: FP8ScaledMMLinearLayerConfig) -> tuple[bool, str | None]:
226
+ is_ptpc = (
227
+ c.activation_quant_key.scale.group_shape.is_per_token()
228
+ and c.weight_quant_key.scale.group_shape.is_per_channel()
229
+ )
230
+ if c.weight_shape is None:
231
+ return False, "weight_shape is required for Aiter kernels"
232
+ N, K = c.weight_shape
233
+ fp8_dtype = current_platform.fp8_dtype()
234
+
235
+ if not is_ptpc:
236
+ return (
237
+ False,
238
+ "requires per token activation scales and per channel weight scales.",
239
+ )
240
+
241
+ # Aiter's per-token Gemm performs better than torch oonly when its
242
+ # tuned.
243
+ if not rocm_aiter_ops.is_per_token_w8a8_gemm_tuned(N, K, fp8_dtype):
244
+ return (
245
+ False,
246
+ f"requires a tuned configuration for N: {N} and K: {K} "
247
+ f"and fp8 dtype {fp8_dtype}.",
248
+ )
249
+ return True, None
250
+
251
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
252
+ w_name, *_ = self.layer_param_names
253
+ w, *_ = self._get_layer_params(layer)
254
+
255
+ replace_parameter(
256
+ layer,
257
+ w_name,
258
+ torch.nn.Parameter(w.t(), requires_grad=False),
259
+ )
260
+
261
+ def apply_scaled_mm(
262
+ self,
263
+ *,
264
+ A: torch.Tensor,
265
+ B: torch.Tensor,
266
+ out_dtype: torch.dtype,
267
+ As: torch.Tensor,
268
+ Bs: torch.Tensor,
269
+ bias: torch.Tensor | None,
270
+ output_shape: list,
271
+ ) -> torch.Tensor:
272
+ return rocm_aiter_ops.w8a8_gemm(A, B, As, Bs, bias, out_dtype)
273
274
275
class AiterFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w8a8_fp8.py
+4
-5
@@ -31,8 +31,8 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
31
GroupShape,
32
create_fp8_quant_key,
33
kFp8DynamicTokenSym,
34
+ kFp8StaticChannelSym,
35
kFp8StaticTensorSym,
35
- kFp8StaticTokenSym,
36
)
37
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
38
cutlass_block_fp8_supported,
@@ -47,7 +47,7 @@ activation_quant_key_mapping = {
47
DYNAMIC_QUANT: kFp8DynamicTokenSym,
48
}
49
weight_quant_key_mapping = {
50
- QuantizationStrategy.CHANNEL: kFp8StaticTokenSym,
50
+ QuantizationStrategy.CHANNEL: kFp8StaticChannelSym,
51
QuantizationStrategy.TENSOR: kFp8StaticTensorSym,
52
}
53
logger = init_logger(__name__)
@@ -67,7 +67,6 @@ class CompressedTensorsW8A8Fp8(CompressedTensorsScheme):
67
self.use_aiter_and_is_supported = rocm_aiter_ops.is_linear_fp8_enabled()
68
assert not self.is_static_input_scheme
69
self.act_q_group_shape = GroupShape(1, self.weight_block_size[0])
70
-
70
self.weight_quant_key = create_fp8_quant_key(
71
static=True, group_shape=GroupShape(*self.weight_block_size)
72
)
@@ -76,7 +75,7 @@ class CompressedTensorsW8A8Fp8(CompressedTensorsScheme):
75
)
76
else:
77
self.activation_quant_key = activation_quant_key_mapping[
79
- is_static_input_scheme
78
+ self.is_static_input_scheme
79
]
80
self.weight_quant_key = weight_quant_key_mapping[self.strategy]
81
@@ -138,9 +137,9 @@ class CompressedTensorsW8A8Fp8(CompressedTensorsScheme):
137
self.fp8_linear = init_fp8_linear_kernel(
138
activation_quant_key=self.activation_quant_key,
139
weight_quant_key=self.weight_quant_key,
141
- weight_shape=layer.weight.shape,
140
input_dtype=self.input_dtype,
141
out_dtype=self.out_dtype,
142
+ weight_shape=(output_size_per_partition, input_size_per_partition),
143
module_name=self.__class__.__name__,
144
)
145
vllm/model_executor/layers/quantization/fbgemm_fp8.py
+2
@@ -175,6 +175,8 @@ class FBGEMMFp8LinearMethod(LinearMethodBase):
175
# Activations not quantized for marlin.
176
del layer.input_scale_ub
177
178
+ self.fp8_linear.process_weights_after_loading(layer)
179
+
180
def apply(
181
self,
182
layer: torch.nn.Module,
vllm/model_executor/layers/quantization/fp8.py
+2
-2
@@ -397,8 +397,6 @@ class Fp8LinearMethod(LinearMethodBase):
397
if self.block_quant:
398
assert not self.act_q_static
399
400
- self.fp8_linear.process_weights_after_loading(layer)
401
-
400
# If checkpoint not serialized fp8, quantize the weights.
401
else:
402
# If checkpoint is fp8 per-tensor, handle that there are N scales for N
@@ -428,6 +426,8 @@ class Fp8LinearMethod(LinearMethodBase):
426
else:
427
layer.input_scale = None
428
429
+ self.fp8_linear.process_weights_after_loading(layer)
430
+
431
def apply(
432
self,
433
layer: torch.nn.Module,
vllm/model_executor/layers/quantization/modelopt.py
+2
@@ -517,6 +517,7 @@ class ModelOptFp8LinearMethod(LinearMethodBase):
517
layer.weight = Parameter(weight.t(), requires_grad=False)
518
layer.weight_scale = Parameter(max_w_scale, requires_grad=False)
519
layer.input_scale = Parameter(layer.input_scale.max(), requires_grad=False)
520
+ self.fp8_linear.process_weights_after_loading(layer)
521
522
def apply(
523
self,
@@ -597,6 +598,7 @@ class ModelOptFp8PcPtLinearMethod(LinearMethodBase):
598
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
599
layer.weight = Parameter(layer.weight.t(), requires_grad=False)
600
layer.weight_scale = Parameter(layer.weight_scale.data, requires_grad=False)
601
+ self.fp8_linear.process_weights_after_loading(layer)
602
603
def apply(
604
self,
vllm/model_executor/layers/quantization/quark/schemes/quark_w8a8_fp8.py
+2
@@ -120,6 +120,8 @@ class QuarkW8A8Fp8(QuarkScheme):
120
if self.is_static_input_scheme:
121
layer.input_scale = Parameter(layer.input_scale.max(), requires_grad=False)
122
123
+ self.fp8_linear.process_weights_after_loading(layer)
124
+
125
def create_weights(
126
self,
127
layer: torch.nn.Module,