Skip to content

Commit f1aba7a

Browse files
committed
feat(quantization): adapt W8A8 inference to vLLM 0.24
1 parent dc7ba62 commit f1aba7a

13 files changed

Lines changed: 1134 additions & 15 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
from types import SimpleNamespace
15+
16+
from vllm_fl.ops.fused_moe import layer as layer_module
17+
18+
19+
class _FakeRunner:
20+
def __init__(self, quant_method):
21+
self._quant_method = quant_method
22+
self.moe_config = SimpleNamespace()
23+
self.replacements = []
24+
25+
def _replace_quant_method(self, quant_method):
26+
self.replacements.append(quant_method)
27+
self._quant_method = quant_method
28+
29+
30+
def test_fused_moe_factory_preserves_quantized_method(monkeypatch):
31+
quantized_method = object()
32+
runner = _FakeRunner(quantized_method)
33+
monkeypatch.setattr(layer_module, "_OrigFusedMoE", lambda *args, **kwargs: runner)
34+
monkeypatch.setattr(layer_module, "replace_router_with_fl", lambda: None)
35+
36+
result = layer_module.FusedMoEFL()
37+
38+
assert result is runner
39+
assert runner._quant_method is quantized_method
40+
assert runner.replacements == []
41+
42+
43+
def test_fused_moe_factory_still_replaces_unquantized_method(monkeypatch):
44+
upstream_method = object.__new__(layer_module.UnquantizedFusedMoEMethod)
45+
fl_method = object()
46+
runner = _FakeRunner(upstream_method)
47+
monkeypatch.setattr(layer_module, "_OrigFusedMoE", lambda *args, **kwargs: runner)
48+
monkeypatch.setattr(
49+
layer_module,
50+
"UnquantizedFusedMoEMethodFL",
51+
lambda moe_config: fl_method,
52+
)
53+
monkeypatch.setattr(layer_module, "replace_router_with_fl", lambda: None)
54+
55+
layer_module.FusedMoEFL()
56+
57+
assert runner._quant_method is fl_method
58+
assert runner.replacements == [fl_method]
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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_fl.quantization.w8a8 import packed
23+
from vllm_fl.quantization.w8a8.int8_mode import (
24+
INT8_MODE_ENV,
25+
should_use_packed_w8a8,
26+
)
27+
28+
29+
def _weight_args(strategy: QuantizationStrategy) -> QuantizationArgs:
30+
return QuantizationArgs(
31+
num_bits=8,
32+
type=QuantizationType.INT,
33+
strategy=strategy,
34+
symmetric=True,
35+
dynamic=False,
36+
group_size=128 if strategy == QuantizationStrategy.GROUP else None,
37+
)
38+
39+
40+
def test_auto_mode_maps_channelwise_packed_int8_to_w8a8(monkeypatch):
41+
monkeypatch.delenv(INT8_MODE_ENV, raising=False)
42+
assert should_use_packed_w8a8(
43+
_weight_args(QuantizationStrategy.CHANNEL),
44+
None,
45+
"pack-quantized",
46+
)
47+
assert not should_use_packed_w8a8(
48+
_weight_args(QuantizationStrategy.GROUP),
49+
None,
50+
"pack-quantized",
51+
)
52+
53+
54+
def test_w8a16_mode_keeps_channelwise_checkpoint_weight_only(monkeypatch):
55+
monkeypatch.setenv(INT8_MODE_ENV, "w8a16")
56+
assert not should_use_packed_w8a8(
57+
_weight_args(QuantizationStrategy.CHANNEL),
58+
None,
59+
"pack-quantized",
60+
)
61+
62+
63+
def test_w8a8_mode_rejects_groupwise_checkpoint(monkeypatch):
64+
monkeypatch.setenv(INT8_MODE_ENV, "w8a8")
65+
with pytest.raises(ValueError, match="--strategy channel"):
66+
should_use_packed_w8a8(
67+
_weight_args(QuantizationStrategy.GROUP),
68+
None,
69+
"pack-quantized",
70+
)
71+
72+
73+
def test_packed_scheme_matches_vllm_024_layer_contract(monkeypatch):
74+
class FakeKernel:
75+
def process_weights_after_loading(self, layer):
76+
assert layer.weight.dtype == torch.int8
77+
78+
def apply_weights(self, layer, x, bias):
79+
raise AssertionError("not used")
80+
81+
monkeypatch.setattr(
82+
packed,
83+
"init_int8_linear_kernel",
84+
lambda **kwargs: FakeKernel(),
85+
)
86+
monkeypatch.setattr(
87+
"vllm.model_executor.parameter.get_tensor_model_parallel_rank",
88+
lambda: 0,
89+
)
90+
monkeypatch.setattr(
91+
"vllm.model_executor.parameter.get_tensor_model_parallel_world_size",
92+
lambda: 1,
93+
)
94+
95+
scheme = packed.FLPackedW8A8Scheme(layer_name="model.linear")
96+
layer = torch.nn.Module()
97+
scheme.create_weights(
98+
layer,
99+
output_partition_sizes=[4, 4],
100+
input_size_per_partition=8,
101+
params_dtype=torch.bfloat16,
102+
weight_loader=lambda *args, **kwargs: None,
103+
)
104+
105+
assert layer.logical_widths == [4, 4]
106+
assert layer.weight_packed.shape == (8, 2)
107+
assert layer.weight_scale.shape == (8, 1)
108+
assert layer.weight_scale.dtype == torch.float32
109+
110+
values = torch.arange(-32, 32, dtype=torch.int8).reshape(8, 8)
111+
layer.weight_packed.data.copy_(
112+
(values.to(torch.int16) + 128).to(torch.uint8).contiguous().view(torch.int32)
113+
)
114+
scheme.process_weights_after_loading(layer)
115+
116+
assert not hasattr(layer, "weight_packed")
117+
assert torch.equal(layer.weight, values)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 sys
15+
from types import SimpleNamespace
16+
17+
import vllm.platforms as platforms
18+
19+
from vllm_fl.quantization.w8a8 import moe as moe_adapter
20+
21+
22+
def _install_with_fake_modules(monkeypatch, upstream_selector, upstream_builder):
23+
oracle = SimpleNamespace(
24+
Int8MoeBackend=SimpleNamespace(TRITON="triton"),
25+
select_int8_moe_backend=upstream_selector,
26+
)
27+
scheme = SimpleNamespace(
28+
select_int8_moe_backend=upstream_selector,
29+
make_int8_moe_quant_config=upstream_builder,
30+
)
31+
modules = {
32+
moe_adapter._ORACLE_MODULE: oracle,
33+
moe_adapter._SCHEME_MODULE: scheme,
34+
}
35+
monkeypatch.setattr(moe_adapter, "import_module", lambda name: modules[name])
36+
return oracle, scheme
37+
38+
39+
def test_w8a8_moe_builder_preserves_dynamic_per_token_config(monkeypatch):
40+
def upstream_selector(*args, **kwargs):
41+
return "upstream"
42+
43+
def upstream_builder(*args, **kwargs):
44+
return "upstream-config"
45+
46+
oracle, scheme = _install_with_fake_modules(
47+
monkeypatch,
48+
upstream_selector,
49+
upstream_builder,
50+
)
51+
assert moe_adapter.install_fl_w8a8_moe_selector()
52+
assert oracle.select_int8_moe_backend is scheme.select_int8_moe_backend
53+
54+
config_module = SimpleNamespace(
55+
int8_w8a8_moe_quant_config=lambda **kwargs: kwargs,
56+
)
57+
monkeypatch.setitem(
58+
sys.modules,
59+
"vllm.model_executor.layers.fused_moe.config",
60+
config_module,
61+
)
62+
dynamic_config = scheme.make_int8_moe_quant_config(
63+
w1_scale="w1",
64+
w2_scale="w2",
65+
a1_scale=None,
66+
a2_scale=None,
67+
w1_bias="b1",
68+
w2_bias="b2",
69+
per_act_token_quant=True,
70+
)
71+
assert dynamic_config == {
72+
"w1_scale": "w1",
73+
"w2_scale": "w2",
74+
"a1_scale": None,
75+
"a2_scale": None,
76+
"w1_bias": "b1",
77+
"w2_bias": "b2",
78+
"per_act_token_quant": True,
79+
}
80+
assert (
81+
scheme.make_int8_moe_quant_config(
82+
w1_scale="w1",
83+
w2_scale="w2",
84+
per_act_token_quant=False,
85+
)
86+
== "upstream-config"
87+
)
88+
89+
90+
def test_w8a8_moe_selector_uses_vllm_functional_experts(monkeypatch):
91+
upstream_calls = []
92+
93+
def upstream_selector(*args, **kwargs):
94+
upstream_calls.append((args, kwargs))
95+
return "upstream"
96+
97+
oracle, _ = _install_with_fake_modules(
98+
monkeypatch,
99+
upstream_selector,
100+
lambda **kwargs: kwargs,
101+
)
102+
monkeypatch.setattr(
103+
type(platforms.current_platform),
104+
"is_out_of_tree",
105+
lambda self: True,
106+
)
107+
108+
import vllm_fl.utils as fl_utils
109+
110+
monkeypatch.setattr(fl_utils, "is_oot_enabled", lambda: True)
111+
moe_adapter.install_fl_w8a8_moe_selector()
112+
config = SimpleNamespace(
113+
is_lora_enabled=False,
114+
moe_parallel_config=SimpleNamespace(
115+
use_batched_activation_format=False,
116+
),
117+
)
118+
119+
backend, experts_cls = oracle.select_int8_moe_backend(
120+
config,
121+
weight_key=None,
122+
activation_key=None,
123+
)
124+
125+
from vllm_fl.quantization.w8a8.moe_experts import (
126+
VllmFunctionalW8A8Experts,
127+
)
128+
129+
assert backend == "triton"
130+
assert experts_cls is VllmFunctionalW8A8Experts
131+
assert upstream_calls == []
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
from types import SimpleNamespace
15+
16+
import pytest
17+
import torch
18+
19+
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
20+
21+
from vllm_fl.quantization.w8a8 import moe_experts
22+
23+
24+
def _apply_arguments():
25+
hidden_states = torch.ones((2, 4), dtype=torch.bfloat16)
26+
return {
27+
"output": torch.empty_like(hidden_states),
28+
"hidden_states": hidden_states,
29+
"w1": torch.ones((2, 8, 4), dtype=torch.int8),
30+
"w2": torch.ones((2, 4, 4), dtype=torch.int8),
31+
"topk_weights": torch.ones((2, 1), dtype=torch.float32),
32+
"topk_ids": torch.zeros((2, 1), dtype=torch.int64),
33+
"activation": MoEActivation.SILU,
34+
"global_num_experts": 2,
35+
"expert_map": None,
36+
"a1q_scale": None,
37+
"a2_scale": None,
38+
"workspace13": torch.empty(0),
39+
"workspace2": torch.empty(0),
40+
"expert_tokens_meta": None,
41+
"apply_router_weight_on_input": False,
42+
}
43+
44+
45+
def test_functional_experts_defer_activation_quantization():
46+
instance = SimpleNamespace()
47+
assert (
48+
moe_experts.VllmFunctionalW8A8Experts.expects_unquantized_inputs.fget(instance)
49+
is True
50+
)
51+
52+
53+
def test_functional_experts_calls_vllm_024_with_float_input(monkeypatch):
54+
calls = []
55+
56+
def fake_fused_experts(**kwargs):
57+
calls.append(kwargs)
58+
return torch.full_like(kwargs["hidden_states"], 3)
59+
60+
monkeypatch.setattr(moe_experts, "fused_experts", fake_fused_experts)
61+
quant_config = SimpleNamespace(use_int8_w8a8=True)
62+
instance = SimpleNamespace(quant_config=quant_config)
63+
arguments = _apply_arguments()
64+
65+
moe_experts.VllmFunctionalW8A8Experts.apply(instance, **arguments)
66+
67+
assert calls[0]["hidden_states"].dtype == torch.bfloat16
68+
assert calls[0]["quant_config"] is quant_config
69+
assert "inplace" not in calls[0]
70+
assert torch.equal(
71+
arguments["output"],
72+
torch.full_like(arguments["output"], 3),
73+
)
74+
75+
76+
def test_functional_experts_rejects_prequantized_input(monkeypatch):
77+
monkeypatch.setattr(
78+
moe_experts,
79+
"fused_experts",
80+
lambda **kwargs: pytest.fail("fused_experts must not run"),
81+
)
82+
instance = SimpleNamespace(
83+
quant_config=SimpleNamespace(use_int8_w8a8=True),
84+
)
85+
arguments = _apply_arguments()
86+
arguments["hidden_states"] = torch.ones((2, 4), dtype=torch.int8)
87+
arguments["a1q_scale"] = torch.ones((2, 1), dtype=torch.float32)
88+
89+
with pytest.raises(ValueError, match="quantized before"):
90+
moe_experts.VllmFunctionalW8A8Experts.apply(instance, **arguments)

0 commit comments

Comments
 (0)