Skip to content

Commit 780ba37

Browse files
[ROCm][Quantization] Add asymmetric INT8 quantization support to TritonInt8ScaledMMLinearKernel (vllm-project#38501)
Signed-off-by: Andreas Karatzas <akaratza@amd.com>
1 parent 9570654 commit 780ba37

7 files changed

Lines changed: 133 additions & 14 deletions

File tree

.buildkite/lm-eval-harness/configs/Meta-Llama-4-Maverick-17B-128E-Instruct-FP8.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# For hf script, without -t option (tensor parallel size).
22
# bash .buildkite/lm-eval-harness/run-lm-eval-mmlupro-vllm-baseline.sh -m meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8 -l 250 -t 8 -f 5
33
model_name: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
4+
required_gpu_arch:
5+
- gfx942
6+
- gfx950
47
tasks:
58
- name: "mmlu_pro"
69
metrics:

.buildkite/lm-eval-harness/configs/Qwen2.5-VL-3B-Instruct-FP8-dynamic.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# For vllm script, with -t option (tensor parallel size)
22
# bash .buildkite/lm-eval-harness/run-lm-eval-gsm-vllm-baseline.sh -m RedHatAI/Qwen2.5-VL-3B-Instruct-FP8-Dynamic -l 1319 -t 1
33
model_name: "RedHatAI/Qwen2.5-VL-3B-Instruct-FP8-Dynamic"
4+
required_gpu_arch:
5+
- gfx942
6+
- gfx950
47
tasks:
58
- name: "gsm8k"
69
metrics:

.buildkite/lm-eval-harness/configs/Qwen3-235B-A22B-Instruct-2507-FP8.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
model_name: "Qwen/Qwen3-235B-A22B-Instruct-2507-FP8"
2+
required_gpu_arch:
3+
- gfx942
4+
- gfx950
25
tasks:
36
- name: "mmlu_pro"
47
metrics:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Qwen2.5-1.5B-Instruct.yaml
22
Meta-Llama-3.2-1B-Instruct-INT8-compressed-tensors.yaml
3+
Meta-Llama-3-8B-Instruct-INT8-compressed-tensors-asym.yaml
34
Meta-Llama-3-8B-Instruct-nonuniform-compressed-tensors.yaml
45
Qwen2.5-VL-3B-Instruct-FP8-dynamic.yaml
56
Qwen1.5-MoE-W4A16-compressed-tensors.yaml

.buildkite/lm-eval-harness/test_lm_eval_correctness.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from contextlib import contextmanager
1414

1515
import lm_eval
16+
import pytest
1617
import yaml
1718

1819
from vllm.platforms import current_platform
@@ -89,9 +90,40 @@ def launch_lm_eval(eval_config, tp_size):
8990
return results
9091

9192

93+
def _check_rocm_gpu_arch_requirement(eval_config):
94+
"""Skip the test if the model requires a ROCm GPU arch not present.
95+
96+
Model YAML configs can specify::
97+
98+
required_gpu_arch:
99+
- gfx942
100+
- gfx950
101+
102+
The check only applies on ROCm. On other platforms (e.g. CUDA) the
103+
field is ignored so that shared config files work for both NVIDIA and
104+
AMD CI pipelines.
105+
"""
106+
required_archs = eval_config.get("required_gpu_arch")
107+
if not required_archs:
108+
return
109+
110+
if not current_platform.is_rocm():
111+
return
112+
113+
from vllm.platforms.rocm import _GCN_ARCH # noqa: E402
114+
115+
if not any(arch in _GCN_ARCH for arch in required_archs):
116+
pytest.skip(
117+
f"Model requires GPU arch {required_archs}, "
118+
f"but detected arch is '{_GCN_ARCH}'"
119+
)
120+
121+
92122
def test_lm_eval_correctness_param(config_filename, tp_size):
93123
eval_config = yaml.safe_load(config_filename.read_text(encoding="utf-8"))
94124

125+
_check_rocm_gpu_arch_requirement(eval_config)
126+
95127
results = launch_lm_eval(eval_config, tp_size)
96128

97129
rtol = eval_config.get("rtol", DEFAULT_RTOL)

.buildkite/test-amd.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2690,6 +2690,24 @@ steps:
26902690
- pytest -s -v evals/gsm8k/test_gsm8k_correctness.py --config-list-file=configs/models-small.txt
26912691

26922692

2693+
- label: LM Eval Small Models (MI325) # TBD
2694+
timeout_in_minutes: 180
2695+
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi325]
2696+
agent_pool: mi325_1
2697+
working_dir: "/vllm-workspace/.buildkite/lm-eval-harness"
2698+
source_file_dependencies:
2699+
- csrc/
2700+
- vllm/model_executor/layers/quantization
2701+
- vllm/model_executor/models/
2702+
- vllm/model_executor/model_loader/
2703+
- vllm/v1/attention/backends/
2704+
- vllm/v1/attention/selector.py
2705+
- vllm/_aiter_ops.py
2706+
- vllm/platforms/rocm.py
2707+
commands:
2708+
- pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-small-rocm.txt
2709+
2710+
26932711
- label: LM Eval Small Models (B200-MI325) # TBD
26942712
timeout_in_minutes: 180
26952713
mirror_hardwares: [amdexperimental, amdproduction, amdgfx942nightly, amdmi325]

vllm/model_executor/kernels/linear/scaled_mm/triton.py

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ def is_supported(
3131

3232
@classmethod
3333
def can_implement(cls, c: Int8ScaledMMLinearLayerConfig) -> tuple[bool, str | None]:
34-
if not c.input_symmetric:
35-
return False, "supports symmetric input only."
3634
return True, None
3735

3836
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
@@ -62,32 +60,93 @@ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
6260
# INPUT SCALE
6361
if self.config.is_static_input_scheme:
6462
assert i_s is not None
65-
replace_parameter(
66-
layer,
67-
i_s_name,
68-
torch.nn.Parameter(i_s.max(), requires_grad=False),
69-
)
70-
setattr(layer, i_zp_name, None)
63+
64+
if self.config.input_symmetric:
65+
replace_parameter(
66+
layer,
67+
i_s_name,
68+
torch.nn.Parameter(i_s.max(), requires_grad=False),
69+
)
70+
setattr(layer, i_zp_name, None)
71+
else:
72+
input_zero_point = getattr(layer, i_zp_name)
73+
74+
# Reconstruct the ranges to find a single scale and azp
75+
int8_traits = torch.iinfo(torch.int8)
76+
azps = input_zero_point.to(dtype=torch.int32)
77+
range_max = (i_s * (int8_traits.max - azps)).max()
78+
range_min = (i_s * (int8_traits.min - azps)).min()
79+
80+
scale = (range_max - range_min) / (int8_traits.max - int8_traits.min)
81+
replace_parameter(
82+
layer,
83+
i_s_name,
84+
torch.nn.Parameter(scale, requires_grad=False),
85+
)
86+
87+
# AZP loaded as int8 but used as int32
88+
azp = (int8_traits.min - range_min / scale).to(dtype=torch.int32)
89+
replace_parameter(
90+
layer,
91+
i_zp_name,
92+
torch.nn.Parameter(azp, requires_grad=False),
93+
)
7194
else:
7295
setattr(layer, i_s_name, None)
7396
setattr(layer, i_zp_name, None)
7497

75-
setattr(layer, azp_adj_name, None)
98+
# azp_adj is the AZP adjustment term, used to account for weights.
99+
# It does not depend on scales or azp, so it is the same for
100+
# static and dynamic quantization.
101+
# See csrc/quantization/w8a8/cutlass/Epilogues.md for the math.
102+
if not self.config.input_symmetric:
103+
weight = getattr(layer, w_q_name)
104+
# weight is already transposed to [K, N], sum over K (dim=0)
105+
azp_adj = weight.sum(dim=0, keepdim=True, dtype=torch.int32)
106+
if self.config.is_static_input_scheme:
107+
# Fold azp into azp_adj for the per-tensor case
108+
azp_adj = getattr(layer, i_zp_name) * azp_adj
109+
setattr(
110+
layer,
111+
azp_adj_name,
112+
torch.nn.Parameter(azp_adj, requires_grad=False),
113+
)
114+
else:
115+
setattr(layer, azp_adj_name, None)
76116

77117
def apply_weights(
78118
self,
79119
layer: torch.nn.Module,
80120
x: torch.Tensor,
81121
bias: torch.Tensor | None = None,
82122
) -> torch.Tensor:
83-
w_q, w_s, i_s, i_zp, _ = self._get_layer_params(layer)
123+
w_q, w_s, i_s, i_zp, azp_adj = self._get_layer_params(layer)
84124

125+
symmetric = azp_adj is None
85126
x_q, x_s, x_zp = ops.scaled_int8_quant(
86-
x.contiguous(), i_s, i_zp, symmetric=True
127+
x.contiguous(), i_s, i_zp, symmetric=symmetric
87128
)
88129

89-
assert x_zp is None, "Triton kernel only supports symmetric quantization"
90-
91-
return triton_scaled_mm(
130+
out = triton_scaled_mm(
92131
x_q, w_q, scale_a=x_s, scale_b=w_s, out_dtype=x.dtype, bias=bias
93132
)
133+
134+
if azp_adj is not None:
135+
# Asymmetric quantization: subtract the zero-point correction.
136+
# D = scale_a * scale_b * (A_q @ B_q - azp * azp_adj) + bias
137+
# triton_scaled_mm already computed scale_a * scale_b * (A_q @ B_q) + bias
138+
# so we subtract scale_a * scale_b * azp * azp_adj
139+
#
140+
# x_s: [M, 1] or scalar, w_s: [N, 1] or scalar, azp_adj: [1, N]
141+
# Reshape w_s from [N, 1] to [1, N] for proper broadcasting.
142+
w_s_row = w_s.view(1, -1) if w_s.dim() > 0 else w_s
143+
static = i_zp is not None
144+
if not static and x_zp is not None:
145+
# Dynamic per-token: azp is per-token, azp_adj is per-channel
146+
# x_zp: [M, 1], azp_adj: [1, N]
147+
out -= x_s * w_s_row * (x_zp * azp_adj).to(x.dtype)
148+
else:
149+
# Static per-tensor: azp already folded into azp_adj
150+
out -= (x_s * w_s_row * azp_adj).to(x.dtype)
151+
152+
return out

0 commit comments

Comments
 (0)