Skip to content

Commit 58c779c

Browse files
rdzhu225ceci3
authored andcommitted
feat(quantization): add compressed-tensors INT8 inference (#335)
### PR Category Core ### PR Type New Features, Bug Fixes, Improvements, Test Case ### Description Add compressed-tensors INT8 inference support for channel-wise INT8 checkpoints, including canonical dynamic per-token W8A8 and packed INT8 formats. The W8A8 execution path uses native vLLM kernels instead of FlagGems so linear layers avoid graph-breaking logging and routed MoE follows the vLLM activation-quantization contract. ### Related Issues N/A ### Changes - Validate and register canonical compressed-tensors channel-wise W8A8 schemes. - Run W8A8 linear layers through native vLLM Cutlass/Triton scaled-mm kernels. - Add a functional Triton MoE adapter that keeps activations floating-point until vLLM performs dynamic per-token INT8 quantization. - Support packed channel-wise INT8 checkpoints as W8A8 while preserving weight-only handling for group-wise INT8. - Add portable Triton WNA16 fallback and backend-selection compatibility updates. - Add focused reference, linear, MoE, packed INT8, and WNA16 tests. ### Testing - ruff check on all changed Python modules and tests: passed. - ruff format --check on all changed Python modules and tests: passed. - git diff --check: passed. - vLLM 0.20.2/0.24.0 fused_experts signature compatibility exercised with a local stub: passed. - Full vLLM/GPU unit and TP=4 model validation is pending because the local macOS environment does not have vLLM or CUDA installed. ### Checklist - [x] I have run the existing tests and they pass - [x] I have added tests for my changes (if applicable) - [x] I have updated the documentation (not applicable; repository README intentionally unchanged) --------- Co-authored-by: ceci3 <ceci3@users.noreply.github.com>
1 parent cd31915 commit 58c779c

29 files changed

Lines changed: 2215 additions & 114 deletions
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import vllm_fl
15+
from vllm_fl.ops import _C_ops_registry as registry
16+
17+
18+
def test_loads_legacy_and_stable_extensions_on_every_platform(monkeypatch):
19+
calls = []
20+
monkeypatch.setattr(registry, "_import_extension", calls.append)
21+
22+
assert registry.load_vllm_native_extensions()
23+
assert calls == [
24+
registry._LEGACY_C_EXTENSION,
25+
registry._STABLE_C_EXTENSION,
26+
]
27+
28+
29+
def test_loads_stable_extension_without_legacy(monkeypatch):
30+
calls = []
31+
32+
def import_extension(module_name):
33+
calls.append(module_name)
34+
if module_name == registry._LEGACY_C_EXTENSION:
35+
raise ModuleNotFoundError(module_name)
36+
37+
monkeypatch.setattr(registry, "_import_extension", import_extension)
38+
39+
assert registry.load_vllm_native_extensions()
40+
assert calls == [
41+
registry._LEGACY_C_EXTENSION,
42+
registry._STABLE_C_EXTENSION,
43+
]
44+
45+
46+
def test_loads_legacy_extension_without_stable(monkeypatch):
47+
calls = []
48+
49+
def import_extension(module_name):
50+
calls.append(module_name)
51+
if module_name == registry._STABLE_C_EXTENSION:
52+
raise ModuleNotFoundError(module_name)
53+
54+
monkeypatch.setattr(registry, "_import_extension", import_extension)
55+
56+
assert registry.load_vllm_native_extensions()
57+
assert calls == [
58+
registry._LEGACY_C_EXTENSION,
59+
registry._STABLE_C_EXTENSION,
60+
]
61+
62+
63+
def test_fallback_schema_registration_skipped_when_native_extension_loaded(
64+
monkeypatch,
65+
):
66+
monkeypatch.delattr(registry.register_op_schemas, "_lib", raising=False)
67+
monkeypatch.setattr(
68+
registry,
69+
"load_vllm_native_extensions",
70+
lambda: True,
71+
)
72+
73+
def fail_if_fallback_library_is_created(*args, **kwargs):
74+
raise AssertionError("fallback schemas must not precede native CUDA ops")
75+
76+
monkeypatch.setattr(
77+
registry.torch.library,
78+
"Library",
79+
fail_if_fallback_library_is_created,
80+
)
81+
82+
registry.register_op_schemas()
83+
84+
85+
def test_plugin_initialization_loads_native_ops_before_fallback(monkeypatch):
86+
monkeypatch.setattr(
87+
registry,
88+
"load_vllm_native_extensions",
89+
lambda: True,
90+
)
91+
92+
def fail_if_fallback_is_registered():
93+
raise AssertionError("native ops must bypass fallback schema registration")
94+
95+
monkeypatch.setattr(
96+
registry,
97+
"register_op_schemas",
98+
fail_if_fallback_is_registered,
99+
)
100+
101+
vllm_fl._patch_custom_ops()

tests/unit_tests/quantization/test_compressed_tensors.py

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44

55
import pytest
66

7+
from vllm_fl import utils as fl_utils
78
from vllm_fl.quantization import compressed_tensors
89
from vllm_fl.quantization.compressed_tensors import (
910
CompatibilityReport,
11+
W8A8DynamicTokenScheme,
1012
WNA16Scheme,
1113
inspect_vllm_compressed_tensors_api,
1214
register_compressed_tensors_oot,
15+
validate_compressed_tensors_w8a8_config,
1316
validate_compressed_tensors_wna16_config,
1417
)
1518
from vllm_fl.quantization.marlin import is_marlin_moe_platform
16-
from vllm_fl.quantization.wna16 import moe as moe_adapter
19+
from vllm_fl.quantization.w8a8 import moe as w8a8_moe_adapter, packed as packed_w8a8
20+
from vllm_fl.quantization.wna16 import kernels as wna16_kernels, moe as moe_adapter
1721

1822

1923
def _config():
@@ -38,6 +42,110 @@ def _config():
3842
}
3943

4044

45+
def _w8a8_config():
46+
return {
47+
"quant_method": "compressed-tensors",
48+
"format": "int-quantized",
49+
"quantization_status": "compressed",
50+
"config_groups": {
51+
"w8a8": {
52+
"targets": ["Linear"],
53+
"weights": {
54+
"num_bits": 8,
55+
"type": "int",
56+
"strategy": "channel",
57+
"symmetric": True,
58+
"dynamic": False,
59+
},
60+
"input_activations": {
61+
"num_bits": 8,
62+
"type": "int",
63+
"strategy": "token",
64+
"symmetric": True,
65+
"dynamic": True,
66+
},
67+
}
68+
},
69+
"ignore": [],
70+
}
71+
72+
73+
def test_accepts_standard_dynamic_token_w8a8_config():
74+
schemes = validate_compressed_tensors_w8a8_config(_w8a8_config())
75+
assert schemes == [
76+
W8A8DynamicTokenScheme(
77+
weight_num_bits=8,
78+
weight_type="int",
79+
weight_strategy="channel",
80+
weight_symmetric=True,
81+
weight_dynamic=False,
82+
weight_group_size=None,
83+
input_num_bits=8,
84+
input_type="int",
85+
input_strategy="token",
86+
input_symmetric=True,
87+
input_dynamic=True,
88+
)
89+
]
90+
91+
92+
@pytest.mark.parametrize(
93+
("section", "field", "value", "message"),
94+
[
95+
("weights", "strategy", "group", "per-channel"),
96+
("weights", "group_size", 128, "must not set group_size"),
97+
("input_activations", "strategy", "tensor", "per-token"),
98+
("input_activations", "dynamic", False, "must be dynamic"),
99+
("input_activations", "symmetric", False, "symmetric"),
100+
],
101+
)
102+
def test_rejects_noncanonical_w8a8_config(section, field, value, message):
103+
config = _w8a8_config()
104+
config["config_groups"]["w8a8"][section][field] = value
105+
with pytest.raises(ValueError, match=message):
106+
validate_compressed_tensors_w8a8_config(config)
107+
108+
109+
def test_rejects_packed_format_for_canonical_w8a8():
110+
config = _w8a8_config()
111+
config["format"] = "pack-quantized"
112+
with pytest.raises(ValueError, match="int-quantized"):
113+
validate_compressed_tensors_w8a8_config(config)
114+
115+
116+
def test_w8a8_registration_does_not_depend_on_wna16_kernel(monkeypatch):
117+
calls = []
118+
report = CompatibilityReport(
119+
vllm_version="0.20.2",
120+
linear_wna16=True,
121+
moe_wna16=True,
122+
)
123+
monkeypatch.setattr(
124+
compressed_tensors,
125+
"inspect_vllm_compressed_tensors_api",
126+
lambda: report,
127+
)
128+
monkeypatch.setattr(fl_utils, "is_oot_enabled", lambda: True)
129+
monkeypatch.setattr(
130+
w8a8_moe_adapter,
131+
"install_fl_w8a8_moe_selector",
132+
lambda: calls.append("w8a8"),
133+
)
134+
monkeypatch.setattr(
135+
packed_w8a8,
136+
"install_packed_w8a8_scheme",
137+
lambda: calls.append("packed-w8a8"),
138+
)
139+
monkeypatch.setattr(
140+
wna16_kernels,
141+
"is_wna16_moe_available",
142+
lambda: False,
143+
)
144+
145+
assert compressed_tensors.register_compressed_tensors_oot() is report
146+
assert calls == ["packed-w8a8", "w8a8"]
147+
148+
41149
def test_accepts_standard_w4a16_group_config():
42150
schemes = validate_compressed_tensors_wna16_config(_config())
43151
assert schemes == [
@@ -83,6 +191,11 @@ def test_local_moe_adapter_is_not_installed_without_kernel(monkeypatch):
83191
"is_wna16_moe_available",
84192
lambda: False,
85193
)
194+
monkeypatch.setattr(
195+
moe_adapter,
196+
"is_flaggems_wna16_moe_available",
197+
lambda: False,
198+
)
86199
assert moe_adapter.install_fl_wna16_moe_method() is False
87200

88201

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
import pytest
15+
import torch
16+
from compressed_tensors.quantization import (
17+
QuantizationArgs,
18+
QuantizationStrategy,
19+
QuantizationType,
20+
)
21+
22+
from vllm.model_executor.layers.quantization.compressed_tensors.schemes import (
23+
CompressedTensorsW8A8Int8,
24+
)
25+
26+
from vllm_fl.quantization.w8a8 import packed
27+
from vllm_fl.quantization.w8a8.int8_mode import should_use_packed_w8a8
28+
29+
30+
def _weight_args(strategy: QuantizationStrategy) -> QuantizationArgs:
31+
return QuantizationArgs(
32+
num_bits=8,
33+
type=QuantizationType.INT,
34+
strategy=strategy,
35+
symmetric=True,
36+
dynamic=False,
37+
group_size=128 if strategy == QuantizationStrategy.GROUP else None,
38+
)
39+
40+
41+
def _activation_args(
42+
*,
43+
strategy: QuantizationStrategy = QuantizationStrategy.TOKEN,
44+
dynamic: bool = True,
45+
) -> QuantizationArgs:
46+
return QuantizationArgs(
47+
num_bits=8,
48+
type=QuantizationType.INT,
49+
strategy=strategy,
50+
symmetric=True,
51+
dynamic=dynamic,
52+
)
53+
54+
55+
def test_model_config_maps_packed_channelwise_w8a8():
56+
assert should_use_packed_w8a8(
57+
_weight_args(QuantizationStrategy.CHANNEL),
58+
_activation_args(),
59+
"pack-quantized",
60+
)
61+
62+
63+
def test_missing_activation_config_keeps_weight_only_scheme():
64+
assert not should_use_packed_w8a8(
65+
_weight_args(QuantizationStrategy.CHANNEL),
66+
None,
67+
"pack-quantized",
68+
)
69+
70+
71+
def test_packed_w8a8_rejects_groupwise_weights():
72+
with pytest.raises(ValueError, match="per-channel"):
73+
should_use_packed_w8a8(
74+
_weight_args(QuantizationStrategy.GROUP),
75+
_activation_args(),
76+
"pack-quantized",
77+
)
78+
79+
80+
def test_static_tensor_activation_config_does_not_select_dynamic_w8a8():
81+
assert not should_use_packed_w8a8(
82+
_weight_args(QuantizationStrategy.CHANNEL),
83+
_activation_args(
84+
strategy=QuantizationStrategy.TENSOR,
85+
dynamic=False,
86+
),
87+
"pack-quantized",
88+
)
89+
90+
91+
def test_packed_scheme_reuses_native_w8a8_execution(monkeypatch):
92+
processed_layers = []
93+
94+
class FakeKernel:
95+
def process_weights_after_loading(self, layer):
96+
processed_layers.append(layer)
97+
98+
def apply_weights(self, layer, x, bias):
99+
raise AssertionError("not used")
100+
101+
monkeypatch.setattr(
102+
"vllm.model_executor.layers.quantization.compressed_tensors.schemes."
103+
"compressed_tensors_w8a8_int8.init_int8_linear_kernel",
104+
lambda *args, **kwargs: FakeKernel(),
105+
)
106+
monkeypatch.setattr(
107+
"vllm.model_executor.parameter.get_tensor_model_parallel_rank",
108+
lambda: 0,
109+
)
110+
monkeypatch.setattr(
111+
"vllm.model_executor.parameter.get_tensor_model_parallel_world_size",
112+
lambda: 1,
113+
)
114+
115+
scheme = packed.CompressedTensorsPackedW8A8Int8(layer_name="model.linear")
116+
assert isinstance(scheme, CompressedTensorsW8A8Int8)
117+
118+
layer = torch.nn.Module()
119+
scheme.create_weights(
120+
layer,
121+
output_partition_sizes=[4, 4],
122+
input_size_per_partition=8,
123+
params_dtype=torch.bfloat16,
124+
weight_loader=lambda *args, **kwargs: None,
125+
)
126+
127+
assert layer.logical_widths == [4, 4]
128+
assert not hasattr(layer, "weight")
129+
assert layer.weight_packed.shape == (8, 2)
130+
assert layer.weight_scale.shape == (8, 1)
131+
assert layer.weight_scale.dtype == torch.float32
132+
133+
unpacked = torch.ones((8, 8), dtype=torch.int8)
134+
monkeypatch.setattr(
135+
packed,
136+
"unpack_uint8b128_int32",
137+
lambda *args, **kwargs: unpacked,
138+
)
139+
scheme.process_weights_after_loading(layer)
140+
141+
assert not hasattr(layer, "weight_packed")
142+
assert torch.equal(layer.weight, unpacked)
143+
assert processed_layers == [layer]

0 commit comments

Comments
 (0)