[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,