[Bugfix][Quantization] Fix W8A8 int-quantized scheme selection regression (#46860)
Signed-off-by: HDCharles <charlesdavidhernandez@gmail.com>
HDCharles committed
Jun 29, 2026 at 11:55 UTC
379acd4e4fc33c3939556cf3a888f0963ec5c8ce
2 files changed
+197
-1
tests/quantization/test_compressed_tensors.py
+196
@@ -32,6 +32,7 @@ from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tenso
32
CompressedTensorsW8A8Int8,
33
CompressedTensorsW8A8Mxfp8,
34
CompressedTensorsW8A16Fp8,
35
+ CompressedTensorsWNA8O8Int,
36
CompressedTensorsWNA16,
37
)
38
from vllm.model_executor.layers.quantization.compressed_tensors.utils import (
@@ -672,6 +673,201 @@ def test_get_scheme_dict_returns_none_on_no_match():
673
assert result is None
674
675
676
+# Test constants for activation quantization
677
+_STATIC_SYM_INT8_ACT = QuantizationArgs(
678
+ num_bits=8,
679
+ type=QuantizationType.INT,
680
+ strategy=QuantizationStrategy.TENSOR.value,
681
+ symmetric=True,
682
+ dynamic=False,
683
+)
684
+
685
+_STATIC_ASYM_INT8_ACT = QuantizationArgs(
686
+ num_bits=8,
687
+ type=QuantizationType.INT,
688
+ strategy=QuantizationStrategy.TENSOR.value,
689
+ symmetric=False,
690
+ dynamic=False,
691
+)
692
+
693
+_DYNAMIC_INT8_ACT = QuantizationArgs(
694
+ num_bits=8,
695
+ type=QuantizationType.INT,
696
+ strategy=QuantizationStrategy.TOKEN.value,
697
+ symmetric=True,
698
+ dynamic=True,
699
+)
700
+
701
+
702
+@pytest.mark.parametrize(
703
+ "weight_bits,weight_strategy,input_act,output_act,format,expected_scheme",
704
+ [
705
+ # W8A8 int-quantized -> W8A8Int8 (regression test for #46389)
706
+ pytest.param(
707
+ 8,
708
+ QuantizationStrategy.CHANNEL.value,
709
+ _STATIC_SYM_INT8_ACT,
710
+ None,
711
+ "int-quantized",
712
+ CompressedTensorsW8A8Int8,
713
+ id="w8a8_channel_static_sym",
714
+ ),
715
+ pytest.param(
716
+ 8,
717
+ QuantizationStrategy.CHANNEL.value,
718
+ _STATIC_ASYM_INT8_ACT,
719
+ None,
720
+ "int-quantized",
721
+ CompressedTensorsW8A8Int8,
722
+ id="w8a8_channel_static_asym",
723
+ ),
724
+ pytest.param(
725
+ 8,
726
+ QuantizationStrategy.TENSOR.value,
727
+ _STATIC_SYM_INT8_ACT,
728
+ None,
729
+ "int-quantized",
730
+ CompressedTensorsW8A8Int8,
731
+ id="w8a8_tensor_static",
732
+ ),
733
+ pytest.param(
734
+ 8,
735
+ QuantizationStrategy.CHANNEL.value,
736
+ _DYNAMIC_INT8_ACT,
737
+ None,
738
+ "int-quantized",
739
+ CompressedTensorsW8A8Int8,
740
+ id="w8a8_channel_dynamic",
741
+ ),
742
+ # W8A8O8 int-quantized -> WNA8O8Int (both input and output)
743
+ pytest.param(
744
+ 8,
745
+ QuantizationStrategy.CHANNEL.value,
746
+ _STATIC_SYM_INT8_ACT,
747
+ _STATIC_SYM_INT8_ACT,
748
+ "int-quantized",
749
+ CompressedTensorsWNA8O8Int,
750
+ id="w8a8o8_channel",
751
+ ),
752
+ pytest.param(
753
+ 4,
754
+ QuantizationStrategy.GROUP.value,
755
+ _STATIC_SYM_INT8_ACT,
756
+ _STATIC_SYM_INT8_ACT,
757
+ "int-quantized",
758
+ CompressedTensorsWNA8O8Int,
759
+ id="w4a8o8_group",
760
+ ),
761
+ # Weight-only pack-quantized -> WNA16
762
+ pytest.param(
763
+ 8,
764
+ QuantizationStrategy.CHANNEL.value,
765
+ None,
766
+ None,
767
+ "pack-quantized",
768
+ CompressedTensorsWNA16,
769
+ id="w8_pack",
770
+ ),
771
+ pytest.param(
772
+ 4,
773
+ QuantizationStrategy.GROUP.value,
774
+ None,
775
+ None,
776
+ "pack-quantized",
777
+ CompressedTensorsWNA16,
778
+ id="w4_pack",
779
+ ),
780
+ pytest.param(
781
+ 2,
782
+ QuantizationStrategy.GROUP.value,
783
+ None,
784
+ None,
785
+ "pack-quantized",
786
+ CompressedTensorsWNA16,
787
+ id="w2_pack",
788
+ ),
789
+ pytest.param(
790
+ 3,
791
+ QuantizationStrategy.GROUP.value,
792
+ None,
793
+ None,
794
+ "pack-quantized",
795
+ CompressedTensorsWNA16,
796
+ id="w3_pack",
797
+ ),
798
+ pytest.param(
799
+ 5,
800
+ QuantizationStrategy.GROUP.value,
801
+ None,
802
+ None,
803
+ "pack-quantized",
804
+ CompressedTensorsWNA16,
805
+ id="w5_pack",
806
+ ),
807
+ pytest.param(
808
+ 6,
809
+ QuantizationStrategy.GROUP.value,
810
+ None,
811
+ None,
812
+ "pack-quantized",
813
+ CompressedTensorsWNA16,
814
+ id="w6_pack",
815
+ ),
816
+ pytest.param(
817
+ 7,
818
+ QuantizationStrategy.GROUP.value,
819
+ None,
820
+ None,
821
+ "pack-quantized",
822
+ CompressedTensorsWNA16,
823
+ id="w7_pack",
824
+ ),
825
+ ],
826
+)
827
+def test_scheme_selection(
828
+ weight_bits, weight_strategy, input_act, output_act, format, expected_scheme
829
+):
830
+ """Test that _get_scheme_from_parts selects the correct scheme.
831
+
832
+ This parametrized test verifies scheme selection for various combinations
833
+ of weight bits, quantization strategies, input/output activations, and
834
+ compression formats.
835
+
836
+ Key regression test: W8A8 int-quantized models with channel-wise weights
837
+ should use W8A8Int8 (true int8 gemm), not WNA8O8Int (fake-quant).
838
+ WNA8O8Int should only match when BOTH input and output activations are
839
+ present.
840
+ """
841
+ weight_quant = QuantizationArgs(
842
+ num_bits=weight_bits,
843
+ type=QuantizationType.INT,
844
+ strategy=weight_strategy,
845
+ symmetric=True,
846
+ dynamic=False,
847
+ group_size=128 if weight_strategy == QuantizationStrategy.GROUP.value else None,
848
+ )
849
+
850
+ config = CompressedTensorsConfig(
851
+ target_scheme_map={},
852
+ ignore=[],
853
+ quant_format=format,
854
+ )
855
+
856
+ scheme = config._get_scheme_from_parts(
857
+ weight_quant=weight_quant,
858
+ input_quant=input_act,
859
+ output_quant=output_act,
860
+ format=format,
861
+ )
862
+
863
+ assert isinstance(scheme, expected_scheme), (
864
+ f"Expected {expected_scheme.__name__} for "
865
+ f"W{weight_bits} {weight_strategy} + "
866
+ f"input_act={input_act} + output_act={output_act} + "
867
+ f"format={format}, got {type(scheme).__name__}"
868
+ )
869
+
870
+
871
@pytest.mark.skipif(
872
not current_platform.is_cuda() or not current_platform.has_device_capability(75),
873
reason="MXFP8 requires Turing (sm_75+) or newer.",
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors.py
+1
-1
@@ -679,7 +679,7 @@ class CompressedTensorsConfig(QuantizationConfig):
679
and output_quant.num_bits == 8
680
and not output_quant.dynamic
681
)
682
- return is_intN_weight and (is_static_int8_in or is_static_int8_out)
682
+ return is_intN_weight and (is_static_int8_in and is_static_int8_out)
683
684
def _get_scheme_from_parts(
685
self,