[Quantization] Support native Quark W4A16 INT4/UINT4 exports in vLLM (#48606)

Signed-off-by: HongWei Meng <HongWei.Meng@amd.com>

meng-amd committed Sep 18, 2026 at 17:52 UTC 6df2b1a8c64a7956c51410cbfe1b786a69574263
9 files changed +1124 -6
tests/quantization/test_quark.py
+439 -1
@@ -58,12 +58,17 @@ from vllm.model_executor.layers.quantization.quark.quark import ( # noqa: E501
58 from vllm.model_executor.layers.quantization.quark.quark_moe import ( # noqa: E501
59 QuarkMoEMethod,
60 QuarkW4A8Fp8MoEMethod,
61 + QuarkW4A16Int4MoEMethod,
62 QuarkW8A8Fp8MoEMethod,
63 QuarkW8A8Int8MoEMethod,
64 )
64 -from vllm.model_executor.layers.quantization.quark.schemes import QuarkScheme
65 +from vllm.model_executor.layers.quantization.quark.schemes import (
66 + QuarkScheme,
67 + QuarkW4A16Int4,
68 +)
69 from vllm.model_executor.layers.quantization.quark.utils import (
70 QuarkQTensorHint,
71 + canonicalize_quark_packed_int4,
72 should_ignore_layer,
73 )
74 from vllm.model_executor.layers.quantization.utils.mxfp4_utils import (
@@ -97,6 +102,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
102 kNvfp4Static,
103 )
104 from vllm.model_executor.models.llama import LlamaForCausalLM
105 +from vllm.model_executor.models.utils import WeightsMapper
106 from vllm.platforms import current_platform
107 from vllm.transformers_utils.repo_utils import hf_api
108
@@ -1876,3 +1882,435 @@ def test_glm5next_attn_loader_accepts_deepseek_weight_scale_inv():
1882 )
1883 assert target.loaded_shard is None # o_proj is a direct (non-fused) proj
1884 assert torch.equal(target.loaded_weight, _dequant_fp8_block(weight, scale, 128))
1885 +
1886 +
1887 +_REVERSE_AWQ_PACK_ORDER = [0, 4, 1, 5, 2, 6, 3, 7]
1888 +
1889 +
1890 +def _quark_int4_config(
1891 + *,
1892 + pack_method: str = "reorder",
1893 + symmetric: bool = True,
1894 + exclude: list[str] | None = None,
1895 +) -> dict:
1896 + return {
1897 + "quant_method": "quark",
1898 + "export": {"pack_method": pack_method, "kv_cache_group": []},
1899 + "global_quant_config": {
1900 + "weight": {
1901 + "dtype": "int4",
1902 + "group_size": 128,
1903 + "symmetric": symmetric,
1904 + }
1905 + },
1906 + "exclude": exclude or [],
1907 + }
1908 +
1909 +
1910 +def _sign_extend_int4_nibbles(t: torch.Tensor) -> torch.Tensor:
1911 + mask = (t & 0x8).bool()
1912 + t = t.clone()
1913 + t[mask] = t[mask] | 0xF0
1914 + return t
1915 +
1916 +
1917 +def _dequantize_quark_signed_awq_torch(
1918 + qweight: torch.Tensor,
1919 + scales: torch.Tensor,
1920 + qzeros: torch.Tensor,
1921 + group_size: int,
1922 + *,
1923 + pack_reorder: bool = True,
1924 +) -> torch.Tensor:
1925 + bits = 4
1926 + shifts = torch.arange(0, 32, bits, device=qweight.device)
1927 + iweights = ((qweight[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
1928 + iweights = iweights.view(qweight.shape[0], -1)
1929 + zeros = ((qzeros[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
1930 + zeros = zeros.view(qzeros.shape[0], -1)
1931 +
1932 + if pack_reorder:
1933 + order = torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=qweight.device)
1934 + else:
1935 + order = torch.arange(8, device=qweight.device)
1936 + iweights = iweights.view(qweight.shape[0], -1, 8)[:, :, order].reshape(
1937 + qweight.shape[0], -1
1938 + )
1939 + zeros = zeros.view(qzeros.shape[0], -1, 8)[:, :, order].reshape(qzeros.shape[0], -1)
1940 + iweights = _sign_extend_int4_nibbles(iweights & 0xF)
1941 + zeros = _sign_extend_int4_nibbles(zeros & 0xF)
1942 +
1943 + scales = scales.repeat_interleave(group_size, dim=0)
1944 + zeros = zeros.repeat_interleave(group_size, dim=0)
1945 + return (iweights - zeros) * scales
1946 +
1947 +
1948 +def _dequantize_awq_unsigned_torch(
1949 + qweight: torch.Tensor,
1950 + scales: torch.Tensor,
1951 + qzeros: torch.Tensor,
1952 + group_size: int,
1953 + *,
1954 + pack_reorder: bool = True,
1955 +) -> torch.Tensor:
1956 + bits = 4
1957 + shifts = torch.arange(0, 32, bits, device=qweight.device)
1958 + iweights = ((qweight[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
1959 + iweights = iweights.view(qweight.shape[0], -1)
1960 + zeros = ((qzeros[:, :, None] >> shifts[None, None, :]) & 0xF).to(torch.int8)
1961 + zeros = zeros.view(qzeros.shape[0], -1)
1962 +
1963 + if pack_reorder:
1964 + order = torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=qweight.device)
1965 + else:
1966 + order = torch.arange(8, device=qweight.device)
1967 + iweights = iweights.view(qweight.shape[0], -1, 8)[:, :, order].reshape(
1968 + qweight.shape[0], -1
1969 + )
1970 + zeros = zeros.view(qzeros.shape[0], -1, 8)[:, :, order].reshape(qzeros.shape[0], -1)
1971 +
1972 + scales = scales.repeat_interleave(group_size, dim=0)
1973 + zeros = zeros.repeat_interleave(group_size, dim=0)
1974 + return (iweights - zeros) * scales
1975 +
1976 +
1977 +def _pack_int4_nibbles(nibbles: torch.Tensor, *, pack_reorder: bool) -> torch.Tensor:
1978 + pack_order = (
1979 + torch.tensor(_REVERSE_AWQ_PACK_ORDER, device=nibbles.device)
1980 + if pack_reorder
1981 + else torch.arange(8, device=nibbles.device)
1982 + )
1983 + shifts = pack_order * 4
1984 + return ((nibbles.to(torch.int64) & 0xF) << shifts).sum(dim=-1).to(torch.int32)
1985 +
1986 +
1987 +class TestQuarkInt4Format:
1988 + """Tests for Quark INT4 export format compatibility."""
1989 +
1990 + def test_quark_order_pack_method_uses_native_int4_scheme(self):
1991 + quant_config = QuarkConfig.from_config(_quark_int4_config(pack_method="order"))
1992 + weight_config = quant_config.quant_config["global_quant_config"]
1993 + weight_key, act_key, scheme_cls = quant_config._get_scheme_cls_from_config(
1994 + weight_config
1995 + )
1996 + assert scheme_cls is QuarkW4A16Int4
1997 +
1998 + scheme = quant_config.init_scheme(
1999 + scheme_cls,
2000 + weight_quant_key=weight_key,
2001 + activation_quant_key=act_key,
2002 + weight_config=weight_config["weight"],
2003 + )
2004 + assert isinstance(scheme, QuarkW4A16Int4)
2005 + assert not scheme.pack_reorder
2006 +
2007 + def test_quark_int4_scheme_supports_asymmetric_weights(self):
2008 + quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2009 + weight_config = quant_config.quant_config["global_quant_config"]
2010 + weight_key, act_key, scheme_cls = quant_config._get_scheme_cls_from_config(
2011 + weight_config
2012 + )
2013 + assert scheme_cls is QuarkW4A16Int4
2014 +
2015 + scheme = quant_config.init_scheme(
2016 + scheme_cls,
2017 + weight_quant_key=weight_key,
2018 + activation_quant_key=act_key,
2019 + weight_config=weight_config["weight"],
2020 + )
2021 + assert isinstance(scheme, QuarkW4A16Int4)
2022 + assert not scheme.is_symmetric
2023 +
2024 + @pytest.mark.parametrize("missing_field", ["group_size", "symmetric"])
2025 + def test_quark_int4_scheme_requires_weight_config_fields(self, missing_field):
2026 + from vllm.model_executor.layers.quantization.utils.quant_utils import (
2027 + kInt4Static,
2028 + )
2029 +
2030 + weight_config = {
2031 + "dtype": "int4",
2032 + "group_size": 128,
2033 + "symmetric": True,
2034 + }
2035 + weight_config.pop(missing_field)
2036 + quant_config = QuarkConfig.from_config(
2037 + {
2038 + "quant_method": "quark",
2039 + "export": {"pack_method": "reorder", "kv_cache_group": []},
2040 + "global_quant_config": {"weight": weight_config},
2041 + "exclude": [],
2042 + }
2043 + )
2044 +
2045 + with pytest.raises(ValueError, match=missing_field):
2046 + quant_config.init_scheme(
2047 + QuarkW4A16Int4,
2048 + weight_quant_key=kInt4Static,
2049 + activation_quant_key=None,
2050 + weight_config=weight_config,
2051 + )
2052 +
2053 + def test_quark_int4_moe_uses_native_moe_method(self):
2054 + from unittest.mock import MagicMock, patch
2055 +
2056 + from vllm.model_executor.layers.quantization.utils.quant_utils import (
2057 + kInt4StaticAsym,
2058 + )
2059 +
2060 + quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2061 + moe_config = type("MoeConfig", (), {})()
2062 + moe_config.has_bias = False
2063 +
2064 + mock_backend = MagicMock()
2065 + mock_experts_cls = MagicMock()
2066 + with patch(
2067 + "vllm.model_executor.layers.quantization.quark.quark_moe"
2068 + ".select_wna16_moe_backend",
2069 + return_value=(mock_backend, mock_experts_cls),
2070 + ):
2071 + method = QuarkW4A16Int4MoEMethod(
2072 + kInt4StaticAsym,
2073 + None,
2074 + quant_config.quant_config["global_quant_config"]["weight"],
2075 + quant_config.pack_method,
2076 + moe_config,
2077 + )
2078 +
2079 + assert method.group_size == 128
2080 + assert method.pack_reorder
2081 + assert method.use_wna16_backend
2082 +
2083 + def test_triton_wna16_experts_supports_asymmetric_int4(self):
2084 + from vllm.model_executor.layers.fused_moe.experts.triton_moe import (
2085 + TritonWNA16Experts,
2086 + )
2087 + from vllm.model_executor.layers.quantization.utils.quant_utils import (
2088 + kInt4Static,
2089 + kInt4Static32,
2090 + kInt4Static32Asym,
2091 + kInt4StaticAsym,
2092 + )
2093 +
2094 + for key in [kInt4Static, kInt4Static32, kInt4StaticAsym, kInt4Static32Asym]:
2095 + assert TritonWNA16Experts._supports_quant_scheme(key, None), (
2096 + f"{key} should be supported by TritonWNA16Experts"
2097 + )
2098 +
2099 + def test_quark_moe_loader_shards_w2_zero_point_across_tp_ranks(self):
2100 + """w2 zero-points are sharded along the intermediate dim per TP rank.
2101 +
2102 + Loading the same Quark checkpoint tensor at tensor_parallel_size=2 for
2103 + both ranks must jointly reconstruct the tensor_parallel_size=1 (full)
2104 + result, with no overlap or gap. This guards the custom loader's TP
2105 + split for the packed zero-point layout.
2106 + """
2107 +
2108 + class _FakeMoEConfig:
2109 + has_bias = False
2110 +
2111 + def __init__(self, tp_size, tp_rank):
2112 + self.tp_size = tp_size
2113 + self.tp_rank = tp_rank
2114 +
2115 + class _FakeLayer:
2116 + def __init__(self, moe_config):
2117 + self.moe_config = moe_config
2118 + self.intermediate_size_per_partition = 64
2119 + self.group_size_div_factor = 1
2120 +
2121 + def load_w2_zero_point(raw_zp, tp_size, tp_rank):
2122 + from unittest.mock import MagicMock, patch
2123 +
2124 + from vllm.model_executor.layers.quantization.utils.quant_utils import (
2125 + kInt4StaticAsym,
2126 + )
2127 +
2128 + quant_config = QuarkConfig.from_config(_quark_int4_config(symmetric=False))
2129 + moe_config = _FakeMoEConfig(tp_size, tp_rank)
2130 + with patch(
2131 + "vllm.model_executor.layers.quantization.quark.quark_moe"
2132 + ".select_wna16_moe_backend",
2133 + return_value=(MagicMock(), MagicMock()),
2134 + ):
2135 + method = QuarkW4A16Int4MoEMethod(
2136 + kInt4StaticAsym,
2137 + None,
2138 + quant_config.quant_config["global_quant_config"]["weight"],
2139 + quant_config.pack_method,
2140 + moe_config,
2141 + )
2142 +
2143 + layer = _FakeLayer(moe_config)
2144 + # The loader converts the packed zero-point, then writes the
2145 + # per-rank slice into param.data[expert_id]. A single expert dim is
2146 + # enough; the last dim shrinks by tp_size after sharding.
2147 + param = torch.nn.Parameter(
2148 + torch.zeros(1, 16, 8 // tp_size, dtype=torch.uint8),
2149 + requires_grad=False,
2150 + )
2151 + loader = method.get_weight_loader(layer, weight_loader=None)
2152 + loader(
2153 + param,
2154 + raw_zp.clone(),
2155 + weight_name="w2_weight_zero_point",
2156 + shard_id="w2",
2157 + expert_id=0,
2158 + )
2159 + return param.data[0]
2160 +
2161 + # Quark-packed zero-point for one expert: rows = hidden // pack_factor,
2162 + # cols carry the (packed) intermediate dim, split evenly across ranks.
2163 + raw_zp = torch.randint(0, 256, (8, 16), dtype=torch.uint8)
2164 +
2165 + full = load_w2_zero_point(raw_zp, tp_size=1, tp_rank=0)
2166 + shard0 = load_w2_zero_point(raw_zp, tp_size=2, tp_rank=0)
2167 + shard1 = load_w2_zero_point(raw_zp, tp_size=2, tp_rank=1)
2168 +
2169 + expected = full.view(full.size(0), 2, -1)
2170 + torch.testing.assert_close(shard0, expected[:, 0])
2171 + torch.testing.assert_close(shard1, expected[:, 1])
2172 +
2173 + def test_quark_shared_expert_gate_keeps_quantized_tensors(self):
2174 + quant_config = QuarkConfig.from_config(_quark_int4_config())
2175 + mapper = quant_config.get_cache_scale_mapper()
2176 +
2177 + output_names = {
2178 + name
2179 + for name, _ in mapper.apply(
2180 + [
2181 + (
2182 + "model.language_model.layers.0.mlp.shared_expert_gate.weight",
2183 + torch.zeros(1),
2184 + ),
2185 + (
2186 + "model.language_model.layers.0.mlp.shared_expert_gate"
2187 + ".weight_scale",
2188 + torch.zeros(1),
2189 + ),
2190 + (
2191 + "model.language_model.layers.0.mlp.shared_expert_gate"
2192 + ".weight_zero_point",
2193 + torch.zeros(1),
2194 + ),
2195 + ]
2196 + )
2197 + }
2198 +
2199 + assert (
2200 + "model.language_model.layers.0.mlp.shared_expert_gate.weight"
2201 + in output_names
2202 + )
2203 + assert (
2204 + "model.language_model.layers.0.mlp.shared_expert_gate.weight_scale"
2205 + in output_names
2206 + )
2207 + assert (
2208 + "model.language_model.layers.0.mlp.shared_expert_gate.weight_zero_point"
2209 + in output_names
2210 + )
2211 +
2212 + def test_quark_apply_mapper_updates_exclude_and_layer_quant_config(self):
2213 + quant_config = QuarkConfig.from_config(
2214 + {
2215 + **_quark_int4_config(),
2216 + "exclude": ["lm_head"],
2217 + "layer_quant_config": {
2218 + "model.language_model.layers.0.mlp.gate": {
2219 + "weight": {"dtype": "float16"},
2220 + },
2221 + },
2222 + }
2223 + )
2224 + quant_config.apply_vllm_mapper(
2225 + WeightsMapper(
2226 + orig_to_new_prefix={
2227 + "lm_head": "language_model.lm_head",
2228 + "model.language_model.": "language_model.model.",
2229 + },
2230 + )
2231 + )
2232 +
2233 + layer_quant_config = quant_config.quant_config["layer_quant_config"]
2234 + assert "language_model.model.layers.0.mlp.gate" in layer_quant_config
2235 + assert "model.language_model.layers.0.mlp.gate" not in layer_quant_config
2236 + assert quant_config.quant_config["exclude"] == [
2237 + "language_model.lm_head",
2238 + ]
2239 +
2240 + def test_quark_apply_mapper_ignores_non_string_list_entries(self):
2241 + quant_config = QuarkConfig.from_config(
2242 + {
2243 + **_quark_int4_config(),
2244 + "algo_config": [
2245 + {"name": "qronos", "inside_layer_modules": ["self_attn.q_proj"]}
2246 + ],
2247 + }
2248 + )
2249 + quant_config.apply_vllm_mapper(WeightsMapper())
2250 +
2251 + assert quant_config.quant_config["algo_config"] == [
2252 + {"name": "qronos", "inside_layer_modules": ["self_attn.q_proj"]}
2253 + ]
2254 +
2255 + def test_quark_exclude_matches_via_mapper_prefix(self):
2256 + quant_config = QuarkConfig.from_config(_quark_int4_config(exclude=["lm_head"]))
2257 + quant_config.apply_vllm_mapper(
2258 + WeightsMapper(orig_to_new_prefix={"lm_head": "language_model.lm_head"})
2259 + )
2260 +
2261 + exclude_layers = quant_config.quant_config["exclude"]
2262 + assert should_ignore_layer("language_model.lm_head", ignore=exclude_layers)
2263 + assert not should_ignore_layer(
2264 + "language_model.model.layers.0.mlp.gate", ignore=exclude_layers
2265 + )
2266 +
2267 +
2268 +@pytest.mark.parametrize("symmetric", [False, True])
2269 +@pytest.mark.parametrize("pack_method", ["order", "reorder"])
2270 +def test_quark_int4_canonicalizes_pack_for_kernel_layout(pack_method, symmetric):
2271 + """Quark pack order is normalized to the layout expected by awq_* ops.
2272 +
2273 + This reuses the existing AWQ dequant/gemm kernels for compute only; loading
2274 + still goes through the native Quark quantization path, not AutoAWQ.
2275 + """
2276 + pack_reorder = pack_method == "reorder"
2277 + group_size = 2
2278 + packed_values = torch.tensor(
2279 + [
2280 + [0, 1, 7, 8, 9, 15, 2, 14],
2281 + [15, 8, 0, 3, 12, 7, 1, 9],
2282 + ],
2283 + dtype=torch.int32,
2284 + )
2285 + packed_zeros = torch.zeros((1, 8), dtype=torch.int32)
2286 + qweight = _pack_int4_nibbles(packed_values, pack_reorder=pack_reorder).view(2, 1)
2287 + qzeros = _pack_int4_nibbles(packed_zeros, pack_reorder=pack_reorder).view(1, 1)
2288 + scales = torch.ones((1, 8), dtype=torch.float16)
2289 +
2290 + dequantize = (
2291 + _dequantize_quark_signed_awq_torch
2292 + if symmetric
2293 + else _dequantize_awq_unsigned_torch
2294 + )
2295 + expected = dequantize(
2296 + qweight,
2297 + scales,
2298 + qzeros,
2299 + group_size,
2300 + pack_reorder=pack_reorder,
2301 + )
2302 + canonical_weight = canonicalize_quark_packed_int4(
2303 + qweight,
2304 + pack_reorder=pack_reorder,
2305 + is_symmetric=symmetric,
2306 + )
2307 + canonical_zero = canonicalize_quark_packed_int4(
2308 + qzeros,
2309 + pack_reorder=pack_reorder,
2310 + is_symmetric=symmetric,
2311 + )
2312 + actual = _dequantize_awq_unsigned_torch(
2313 + canonical_weight, scales, canonical_zero, group_size
2314 + )
2315 +
2316 + assert torch.equal(actual, expected)
vllm/model_executor/kernels/linear/__init__.py
+1
@@ -864,6 +864,7 @@ def choose_mp_linear_kernel(
864
865 can_implement, failure_reason = kernel.can_implement(config)
866 if can_implement:
867 + logger.info_once("Using %s for mixed-precision linear", kernel.__name__)
868 return kernel
869 else:
870 failure_reasons.append(
vllm/model_executor/layers/fused_moe/experts/triton_moe.py
+1 -1
@@ -596,9 +596,9 @@ class TritonWNA16Experts(TritonExperts):
596 ) -> bool:
597 SUPPORTED_W = [
598 kInt4Static,
599 + kInt4StaticAsym,
600 kInt8Static,
601 kInt4Static32,
601 - kInt4StaticAsym,
602 kInt4Static32Asym,
603 # other group sizes?
604 ]
vllm/model_executor/layers/quantization/auto_awq.py
+5 -1
@@ -93,7 +93,7 @@ def _replace_or_register_parameter(
93 def _convert_awq_to_standard_format(
94 layer: torch.nn.Module,
95 w_q_name: str,
96 - w_zp_name: str,
96 + w_zp_name: str | None,
97 size_bits: int,
98 ) -> None:
99 """Convert AWQ weight and zero-point tensors to standard GPTQ-like format.
@@ -101,6 +101,7 @@ def _convert_awq_to_standard_format(
101 AWQ packs qweight along the output dim with a non-standard bit order.
102 This converts to standard bit order and repacks qweight along the input
103 dim, matching the format expected by the MPLinearKernel framework.
104 + If w_zp_name is None (symmetric quantization), only the weight is converted.
105 """
106 pack_factor = 32 // size_bits
107 mask = (1 << size_bits) - 1
@@ -139,6 +140,9 @@ def _convert_awq_to_standard_format(
140 )
141 setattr(layer, w_q_name, new_param)
142
143 + if w_zp_name is None:
144 + return
145 +
146 # --- Convert qzeros: fix AWQ bit ordering and repack
147 # AWQ qzeros: (G, N // pack) packed along dim 1, AWQ bit order
148 # Target: (N // pack, G) packed along dim 0, standard bit order
vllm/model_executor/layers/quantization/quark/quark.py
+55 -1
@@ -32,6 +32,7 @@ from vllm.model_executor.layers.quantization.quark.schemes import (
32 QuarkOCP_MX,
33 QuarkScheme,
34 QuarkW4A8_MXFP4_FP8,
35 + QuarkW4A16Int4,
36 QuarkW8A8Fp8,
37 QuarkW8A8Fp8PerBlock,
38 QuarkW8A8Int8,
@@ -39,6 +40,7 @@ from vllm.model_executor.layers.quantization.quark.schemes import (
40 from vllm.model_executor.layers.quantization.quark.utils import (
41 QuarkQTensorHint,
42 deep_compare,
43 + parse_w4a16_int4_weight_config,
44 should_ignore_layer,
45 )
46 from vllm.model_executor.layers.quantization.utils.ocp_mx_utils import (
@@ -54,6 +56,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
56 kFp8Static128BlockSym,
57 kFp8StaticChannelSym,
58 kFp8StaticTensorSym,
59 + kInt4Static,
60 + kInt4Static32,
61 + kInt4Static32Asym,
62 + kInt4StaticAsym,
63 kInt4W4A8StaticChannelSym,
64 kInt8DynamicTensorAsym,
65 kInt8DynamicTensorSym,
@@ -172,8 +178,10 @@ class QuarkConfig(QuantizationConfig):
178 quant_config_with_hf_to_vllm_mapper: dict[str, Any] = {}
179
180 for k, v in self.quant_config.items():
175 - if isinstance(v, list):
181 + if isinstance(v, list) and all(isinstance(item, str) for item in v):
182 quant_config_with_hf_to_vllm_mapper[k] = hf_to_vllm_mapper.apply_list(v)
183 + elif isinstance(v, list):
184 + quant_config_with_hf_to_vllm_mapper[k] = v
185 elif isinstance(v, dict):
186 quant_config_with_hf_to_vllm_mapper[k] = hf_to_vllm_mapper.apply_dict(v)
187 else:
@@ -215,6 +223,9 @@ class QuarkConfig(QuantizationConfig):
223 weight_quant_key=weight_quant_key,
224 activation_quant_key=activation_quant_key,
225 dynamic_mxfp4_quant=dynamic_mxfp4_quant,
226 + weight_config=self._find_matched_config(prefix, type(layer)).get(
227 + "weight"
228 + ),
229 )
230 layer.scheme = scheme
231 return QuarkLinearMethod(self)
@@ -227,6 +238,7 @@ class QuarkConfig(QuantizationConfig):
238 method_cls=method_cls,
239 weight_quant_key=weight_quant_key,
240 activation_quant_key=activation_quant_key,
241 + layer_name=prefix,
242 )
243
244 return None
@@ -572,6 +584,26 @@ class QuarkConfig(QuantizationConfig):
584 )
585 return QuantKeyMatch(True, activation_quant_key, weight_quant_key)
586
587 + def _is_w4a16_int4(
588 + self,
589 + weight_quant: dict[str, Any] | None,
590 + input_quant: dict[str, Any] | None,
591 + ) -> QuantKeyMatch:
592 + if weight_quant is None or input_quant is not None:
593 + return QuantKeyMatch(False, None, None)
594 +
595 + is_int4 = weight_quant.get("dtype") in ("int4", "uint4")
596 + is_packed = self.pack_method in ("order", "reorder")
597 + if not (is_int4 and is_packed):
598 + return QuantKeyMatch(False, None, None)
599 +
600 + group_size, is_symmetric = parse_w4a16_int4_weight_config(weight_quant)
601 + if is_symmetric:
602 + weight_key = kInt4Static32 if group_size == 32 else kInt4Static
603 + else:
604 + weight_key = kInt4Static32Asym if group_size == 32 else kInt4StaticAsym
605 + return QuantKeyMatch(True, None, weight_key)
606 +
607 def _is_w4a8_mxfp4_fp8(
608 self,
609 weight_quant: dict[str, Any] | None,
@@ -890,6 +922,12 @@ class QuarkConfig(QuantizationConfig):
922 match.activation_quant_key,
923 QuarkW8A8Int8,
924 )
925 + elif match := self._is_w4a16_int4(weight_config, input_config):
926 + return (
927 + match.weight_quant_key,
928 + match.activation_quant_key,
929 + QuarkW4A16Int4,
930 + )
931 elif match := self._is_w4a8_mxfp4_fp8(weight_config, input_config):
932 return (
933 match.weight_quant_key,
@@ -919,6 +957,7 @@ class QuarkConfig(QuantizationConfig):
957 weight_quant_key: QuantKey | None,
958 activation_quant_key: QuantKey | None,
959 dynamic_mxfp4_quant: bool = False,
960 + weight_config: dict[str, Any] | None = None,
961 ) -> "QuarkScheme":
962 """Construct a Quark scheme selected by get_scheme_cls."""
963 if scheme_cls not in (
@@ -928,9 +967,24 @@ class QuarkConfig(QuantizationConfig):
967 QuarkOCP_MX,
968 QuarkNVFP4,
969 QuarkW4A8_MXFP4_FP8,
970 + QuarkW4A16Int4,
971 ):
972 raise AssertionError(f"Unsupported Quark scheme class: {scheme_cls}")
973
974 + scheme: QuarkScheme
975 + if scheme_cls is QuarkW4A16Int4:
976 + normalized_weight_config = self._unwrap_single_quant_config(weight_config)
977 + assert normalized_weight_config is not None
978 + assert weight_quant_key is not None
979 + scheme = QuarkW4A16Int4(
980 + weight_quant_key=weight_quant_key,
981 + activation_quant_key=activation_quant_key,
982 + pack_method=self.pack_method,
983 + weight_config=normalized_weight_config,
984 + )
985 + self._check_scheme_supported(scheme.get_min_capability())
986 + return scheme
987 +
988 kwargs: dict[str, Any] = {"activation_quant_key": activation_quant_key}
989
990 # These classes handle different possible `weight_quant_key`.
vllm/model_executor/layers/quantization/quark/quark_moe.py
+352 -2
@@ -1,7 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -from typing import TYPE_CHECKING
4 +from typing import TYPE_CHECKING, Any
5
6 import torch
7
@@ -40,6 +40,12 @@ from vllm.model_executor.layers.fused_moe.oracle.int8 import (
40 make_int8_moe_quant_config,
41 select_int8_moe_backend,
42 )
43 +from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
44 + convert_to_wna16_moe_kernel_format,
45 + make_wna16_moe_kernel,
46 + make_wna16_moe_quant_config,
47 + select_wna16_moe_backend,
48 +)
49 from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
50 TRITON_BACKENDS,
51 Mxfp4MoeBackend,
@@ -56,7 +62,10 @@ from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
62 make_nvfp4_moe_quant_config,
63 select_nvfp4_moe_backend,
64 )
59 -from vllm.model_executor.layers.quantization.quark.utils import QuarkQTensorHint
65 +from vllm.model_executor.layers.quantization.quark.utils import (
66 + QuarkQTensorHint,
67 + parse_w4a16_int4_weight_config,
68 +)
69 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
70 validate_fp8_block_shape_moe,
71 )
@@ -75,6 +84,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
84 kFp8Static128BlockSym,
85 kFp8StaticChannelSym,
86 kFp8StaticTensorSym,
87 + kInt4Static,
88 + kInt4Static32,
89 + kInt4Static32Asym,
90 + kInt4StaticAsym,
91 kInt4W4A8StaticChannelSym,
92 kInt8DynamicTensorAsym,
93 kInt8DynamicTensorSym,
@@ -102,6 +115,7 @@ logger = init_logger(__name__)
115
116 __all__ = [
117 "QuarkMoEMethod",
118 + "QuarkW4A16Int4MoEMethod",
119 "QuarkW8A8Fp8MoEMethod",
120 "QuarkOCP_MX_MoEMethod",
121 "QuarkNvfp4MoEMethod",
@@ -179,6 +193,12 @@ class QuarkMoEMethod(FusedMoEMethodBase):
193 match.activation_quant_key,
194 QuarkW8A8Fp8MoEMethod,
195 )
196 + if match := quant_config._is_w4a16_int4(weight_config, input_config):
197 + return (
198 + match.weight_quant_key,
199 + match.activation_quant_key,
200 + QuarkW4A16Int4MoEMethod,
201 + )
202 if match := quant_config._is_w_ocp_mx_a_x(
203 weight_config, input_config, allow_static_fp8=True
204 ):
@@ -202,6 +222,7 @@ class QuarkMoEMethod(FusedMoEMethodBase):
222 method_cls: type["QuarkMoEMethod"],
223 weight_quant_key: QuantKey | None,
224 activation_quant_key: QuantKey | None,
225 + layer_name: str | None = None,
226 ) -> "QuarkMoEMethod":
227 if weight_quant_key is None:
228 raise RuntimeError("Unsupported FusedMoe scheme")
@@ -226,10 +247,339 @@ class QuarkMoEMethod(FusedMoEMethodBase):
247 return QuarkW8A8Int8MoEMethod(
248 module.moe_config, weight_quant_key, activation_quant_key
249 )
250 + elif method_cls is QuarkW4A16Int4MoEMethod:
251 + assert layer_name is not None
252 + layer_quant_config = quant_config._find_matched_config(
253 + layer_name, type(module)
254 + )
255 + weight_config = quant_config._unwrap_single_quant_config(
256 + layer_quant_config.get("weight")
257 + )
258 + if weight_config is None:
259 + raise RuntimeError(
260 + f"Missing weight quantization config for layer {layer_name}"
261 + )
262 + return QuarkW4A16Int4MoEMethod(
263 + weight_quant_key,
264 + activation_quant_key,
265 + weight_config,
266 + quant_config.pack_method,
267 + module.moe_config,
268 + quant_config=quant_config,
269 + )
270 else:
271 raise RuntimeError("Unsupported FusedMoe scheme")
272
273
274 +class QuarkW4A16Int4MoEMethod(QuarkMoEMethod):
275 + """Quark packed INT4 weight-only MoE method."""
276 +
277 + supported_activation_quant_keys: list[QuantKey | None] = [None]
278 + supported_weight_quant_keys: list[QuantKey] = [
279 + kInt4Static,
280 + kInt4Static32,
281 + kInt4StaticAsym,
282 + kInt4Static32Asym,
283 + ]
284 +
285 + def __init__(
286 + self,
287 + weight_quant_key: QuantKey,
288 + activation_quant_key: QuantKey | None,
289 + weight_config: dict[str, Any],
290 + pack_method: str,
291 + moe: FusedMoEConfig,
292 + quant_config: "QuarkConfig | None" = None, # type: ignore # noqa F821
293 + ):
294 + super().__init__(moe, weight_quant_key, activation_quant_key)
295 +
296 + self.quant_config = quant_config
297 + # group_size is read from the Quark config rather than weight_quant_key:
298 + # the int4 QuantKeys only encode group_size 32 or -1, but Quark int4
299 + # checkpoints commonly use group_size 128, which the QuantKey cannot
300 + # represent. Symmetry is unambiguous, so it comes from the key.
301 + self.group_size = parse_w4a16_int4_weight_config(weight_config)[0]
302 + self.is_symmetric = weight_quant_key.symmetric
303 + self.bit8_pack_factor = 2
304 + self.pack_reorder = pack_method == "reorder"
305 +
306 + self.use_wna16_backend = True
307 + self.wna16_backend, self.experts_cls = select_wna16_moe_backend(
308 + config=self.moe,
309 + weight_key=weight_quant_key,
310 + quant_config=self.quant_config,
311 + may_have_zp=not self.is_symmetric,
312 + may_have_bias=False,
313 + )
314 +
315 + def create_weights(
316 + self,
317 + layer: RoutedExperts,
318 + num_experts: int,
319 + hidden_size: int,
320 + intermediate_size_per_partition: int,
321 + params_dtype: torch.dtype,
322 + **extra_weight_attrs,
323 + ):
324 + group_size = self.group_size
325 + group_size_div_factor = 1
326 +
327 + while intermediate_size_per_partition % group_size or hidden_size % group_size:
328 + group_size = group_size // 2
329 + group_size_div_factor *= 2
330 + assert group_size >= 32
331 + layer.group_size = group_size
332 + layer.group_size_div_factor = group_size_div_factor
333 +
334 + strategy = FusedMoeWeightScaleSupported.GROUP.value
335 + extra_weight_attrs.update({"quant_method": strategy, "is_transposed": False})
336 +
337 + assert "weight_loader" in extra_weight_attrs
338 + weight_loader = extra_weight_attrs["weight_loader"]
339 + extra_weight_attrs["weight_loader"] = self.get_weight_loader(
340 + layer, weight_loader
341 + )
342 +
343 + w13_weight = torch.nn.Parameter(
344 + torch.empty(
345 + num_experts,
346 + 2 * intermediate_size_per_partition,
347 + hidden_size // self.bit8_pack_factor,
348 + dtype=torch.uint8,
349 + ),
350 + requires_grad=False,
351 + )
352 + layer.register_parameter("w13_weight", w13_weight)
353 + set_weight_attrs(w13_weight, extra_weight_attrs)
354 +
355 + w2_weight = torch.nn.Parameter(
356 + torch.empty(
357 + num_experts,
358 + hidden_size,
359 + intermediate_size_per_partition // self.bit8_pack_factor,
360 + dtype=torch.uint8,
361 + ),
362 + requires_grad=False,
363 + )
364 + layer.register_parameter("w2_weight", w2_weight)
365 + set_weight_attrs(w2_weight, extra_weight_attrs)
366 +
367 + w13_weight_scale = torch.nn.Parameter(
368 + torch.zeros(
369 + num_experts,
370 + 2 * intermediate_size_per_partition,
371 + hidden_size // group_size,
372 + dtype=params_dtype,
373 + ),
374 + requires_grad=False,
375 + )
376 + layer.register_parameter("w13_weight_scale", w13_weight_scale)
377 + set_weight_attrs(w13_weight_scale, extra_weight_attrs)
378 +
379 + w2_weight_scale = torch.nn.Parameter(
380 + torch.zeros(
381 + num_experts,
382 + hidden_size,
383 + intermediate_size_per_partition // group_size,
384 + dtype=params_dtype,
385 + ),
386 + requires_grad=False,
387 + )
388 + layer.register_parameter("w2_weight_scale", w2_weight_scale)
389 + set_weight_attrs(w2_weight_scale, extra_weight_attrs)
390 +
391 + w13_weight_zero_point = torch.nn.Parameter(
392 + torch.zeros(
393 + num_experts,
394 + 2 * intermediate_size_per_partition // self.bit8_pack_factor,
395 + hidden_size // group_size,
396 + dtype=torch.uint8,
397 + ),
398 + requires_grad=False,
399 + )
400 + layer.register_parameter("w13_weight_zero_point", w13_weight_zero_point)
401 + set_weight_attrs(w13_weight_zero_point, extra_weight_attrs)
402 +
403 + w2_weight_zero_point = torch.nn.Parameter(
404 + torch.zeros(
405 + num_experts,
406 + hidden_size // self.bit8_pack_factor,
407 + intermediate_size_per_partition // group_size,
408 + dtype=torch.uint8,
409 + ),
410 + requires_grad=False,
411 + )
412 + layer.register_parameter("w2_weight_zero_point", w2_weight_zero_point)
413 + set_weight_attrs(w2_weight_zero_point, extra_weight_attrs)
414 +
415 + def process_weights_after_loading(self, layer: RoutedExperts) -> None:
416 + w13_qzeros = layer.w13_weight_zero_point if not self.is_symmetric else None
417 + w2_qzeros = layer.w2_weight_zero_point if not self.is_symmetric else None
418 +
419 + converted = convert_to_wna16_moe_kernel_format(
420 + backend=self.wna16_backend,
421 + layer=layer,
422 + quant_config=self.quant_config,
423 + input_dtype=None,
424 + w13=layer.w13_weight,
425 + w2=layer.w2_weight,
426 + w13_scale=layer.w13_weight_scale,
427 + w2_scale=layer.w2_weight_scale,
428 + w13_qzeros=w13_qzeros,
429 + w2_qzeros=w2_qzeros,
430 + )
431 +
432 + if converted is None:
433 + # Backend rewrote the layer's params in place (e.g. Humming).
434 + self._setup_kernel(layer)
435 + return
436 +
437 + (
438 + w13_qweight,
439 + w2_qweight,
440 + w13_scales,
441 + w2_scales,
442 + _, # w13_qzeros
443 + _, # w2_qzeros
444 + _, # w13_input_global_scale
445 + _, # w2_input_global_scale
446 + _, # w13_bias
447 + _, # w2_bias
448 + ) = converted
449 +
450 + replace_parameter(layer, "w13_weight", w13_qweight)
451 + replace_parameter(layer, "w2_weight", w2_qweight)
452 + replace_parameter(layer, "w13_weight_scale", w13_scales)
453 + replace_parameter(layer, "w2_weight_scale", w2_scales)
454 +
455 + self._setup_kernel(layer)
456 +
457 + def _setup_kernel(self, layer: RoutedExperts) -> None:
458 + assert self.experts_cls is not None
459 + self.moe_quant_config = self.get_fused_moe_quant_config(layer)
460 + assert self.moe_quant_config is not None
461 + self.moe_kernel = make_wna16_moe_kernel(
462 + moe_quant_config=self.moe_quant_config,
463 + moe_config=self.moe,
464 + experts_cls=self.experts_cls,
465 + backend=self.wna16_backend,
466 + routing_tables=layer._expert_routing_tables(),
467 + )
468 +
469 + def get_fused_moe_quant_config(
470 + self, layer: RoutedExperts
471 + ) -> FusedMoEQuantConfig | None:
472 + w1_zp = layer.w13_weight_zero_point if not self.is_symmetric else None
473 + w2_zp = layer.w2_weight_zero_point if not self.is_symmetric else None
474 + return make_wna16_moe_quant_config(
475 + w1_scale=layer.w13_weight_scale,
476 + w2_scale=layer.w2_weight_scale,
477 + w1_zp=w1_zp,
478 + w2_zp=w2_zp,
479 + group_size=layer.group_size,
480 + num_bits=4,
481 + )
482 +
483 + def apply(
484 + self,
485 + layer: RoutedExperts,
486 + x: torch.Tensor,
487 + topk_weights: torch.Tensor,
488 + topk_ids: torch.Tensor,
489 + shared_experts: SharedExperts | None,
490 + shared_experts_input: torch.Tensor | None,
491 + ) -> torch.Tensor:
492 + assert self.moe_kernel is not None
493 + return self.moe_kernel.apply(
494 + hidden_states=x,
495 + w1=layer.w13_weight,
496 + w2=layer.w2_weight,
497 + topk_weights=topk_weights,
498 + topk_ids=topk_ids,
499 + activation=layer.activation,
500 + global_num_experts=layer.global_num_experts,
501 + apply_router_weight_on_input=layer.apply_router_weight_on_input,
502 + expert_map=layer.expert_map,
503 + shared_experts=shared_experts,
504 + shared_experts_input=shared_experts_input,
505 + )
506 +
507 + def get_weight_loader(self, layer: RoutedExperts, weight_loader):
508 + def convert_quark_tensor(tensor: torch.Tensor, tensor_type: str):
509 + size0 = tensor.size(0)
510 + tensor = tensor.view(torch.uint8)
511 +
512 + shifter = torch.tensor([0, 4], dtype=torch.uint8, device=tensor.device)
513 + tensor = (tensor[:, :, None] >> shifter) & 0xF
514 +
515 + if self.pack_reorder:
516 + reverse_awq_pack_order = [0, 4, 1, 5, 2, 6, 3, 7]
517 + tensor = tensor.view(-1, 8)[:, reverse_awq_pack_order]
518 + else:
519 + tensor = tensor.view(-1, 8)
520 + if self.is_symmetric:
521 + tensor = tensor ^ 0x8
522 + tensor = tensor.view(size0, -1)
523 +
524 + tensor = tensor.T.contiguous()
525 + if tensor_type == "weight":
526 + tensor = tensor[:, 1::2] * 16 + tensor[:, ::2]
527 + elif tensor_type == "zero_point":
528 + tensor = tensor[1::2, :] * 16 + tensor[::2, :]
529 + return tensor
530 +
531 + def quark_int4_weight_loader(
532 + param: torch.nn.Parameter,
533 + loaded_weight: torch.Tensor,
534 + weight_name: str,
535 + shard_id: str,
536 + expert_id: int,
537 + return_success: bool = False,
538 + ):
539 + loaded_weight = loaded_weight.to(param.data.device)
540 + shard_size = layer.intermediate_size_per_partition
541 +
542 + if weight_name.endswith("_weight"):
543 + loaded_weight = convert_quark_tensor(loaded_weight, "weight")
544 + elif weight_name.endswith("_weight_zero_point"):
545 + loaded_weight = convert_quark_tensor(loaded_weight, "zero_point")
546 + elif weight_name.endswith("_weight_scale"):
547 + loaded_weight = loaded_weight.T
548 +
549 + if layer.group_size_div_factor > 1 and (
550 + "weight_zero_point" in weight_name or "weight_scale" in weight_name
551 + ):
552 + loaded_weight = loaded_weight.repeat_interleave(
553 + layer.group_size_div_factor, 1
554 + )
555 +
556 + if "w13_weight_zero_point" in weight_name:
557 + tensor = loaded_weight.view(
558 + layer.moe_config.tp_size, -1, loaded_weight.size(1)
559 + )[layer.moe_config.tp_rank]
560 + if shard_id == "w1":
561 + param.data[expert_id, : shard_size // 2] = tensor
562 + else:
563 + param.data[expert_id, shard_size // 2 :] = tensor
564 + return True if return_success else None
565 + elif "w2_weight_zero_point" in weight_name:
566 + param.data[expert_id] = loaded_weight.view(
567 + loaded_weight.size(0), layer.moe_config.tp_size, -1
568 + )[:, layer.moe_config.tp_rank]
569 + return True if return_success else None
570 +
571 + return weight_loader(
572 + param,
573 + loaded_weight,
574 + weight_name,
575 + shard_id,
576 + expert_id,
577 + return_success=return_success,
578 + )
579 +
580 + return quark_int4_weight_loader
581 +
582 +
583 class QuarkW8A8Fp8MoEMethod(QuarkMoEMethod):
584 supported_activation_quant_keys = [
585 kFp8StaticTensorSym,
vllm/model_executor/layers/quantization/quark/schemes/__init__.py
+2
@@ -5,6 +5,7 @@ from .quark_nvfp4 import QuarkNVFP4
5 from .quark_ocp_mx import QuarkOCP_MX
6 from .quark_scheme import QuarkScheme
7 from .quark_w4a8_mxfp4_fp8 import QuarkW4A8_MXFP4_FP8
8 +from .quark_w4a16_int4 import QuarkW4A16Int4
9 from .quark_w8a8_fp8 import QuarkW8A8Fp8, QuarkW8A8Fp8PerBlock
10 from .quark_w8a8_int8 import QuarkW8A8Int8
11
@@ -14,6 +15,7 @@ __all__ = [
15 "QuarkW8A8Fp8PerBlock",
16 "QuarkW8A8Int8",
17 "QuarkOCP_MX",
18 + "QuarkW4A16Int4",
19 "QuarkW4A8_MXFP4_FP8",
20 "QuarkNVFP4",
21 ]
vllm/model_executor/layers/quantization/quark/schemes/quark_w4a16_int4.py new
+212
@@ -0,0 +1,212 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import math
5 +from collections.abc import Callable
6 +from typing import Any
7 +
8 +import torch
9 +
10 +from vllm.model_executor.kernels.linear import (
11 + MPLinearKernel,
12 + MPLinearLayerConfig,
13 + choose_mp_linear_kernel,
14 +)
15 +from vllm.model_executor.layers.quantization.auto_awq import (
16 + _convert_awq_to_standard_format,
17 +)
18 +from vllm.model_executor.layers.quantization.quark.schemes.quark_scheme import (
19 + QuarkScheme,
20 +)
21 +from vllm.model_executor.layers.quantization.quark.utils import (
22 + canonicalize_quark_packed_int4,
23 + parse_w4a16_int4_weight_config,
24 +)
25 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
26 + QuantKey,
27 + kInt4Static,
28 + kInt4Static32,
29 + kInt4Static32Asym,
30 + kInt4StaticAsym,
31 +)
32 +from vllm.model_executor.parameter import (
33 + GroupQuantScaleParameter,
34 + PackedvLLMParameter,
35 +)
36 +from vllm.scalar_type import scalar_types
37 +
38 +
39 +class QuarkW4A16Int4(QuarkScheme):
40 + """Quark packed INT4 weight-only linear scheme via MPLinearKernel."""
41 +
42 + supported_activation_quant_keys: list[QuantKey | None] = [None]
43 + supported_weight_quant_keys: list[QuantKey] = [
44 + kInt4Static,
45 + kInt4Static32,
46 + kInt4StaticAsym,
47 + kInt4Static32Asym,
48 + ]
49 +
50 + def __init__(
51 + self,
52 + weight_quant_key: QuantKey,
53 + activation_quant_key: QuantKey | None,
54 + pack_method: str,
55 + weight_config: dict[str, Any],
56 + ):
57 + super().__init__(weight_quant_key, activation_quant_key)
58 +
59 + # group_size is read from the Quark config rather than weight_quant_key:
60 + # the int4 QuantKeys only encode group_size 32 or -1, but Quark int4
61 + # checkpoints commonly use group_size 128, which the QuantKey cannot
62 + # represent. Symmetry is unambiguous, so it comes from the key.
63 + self.group_size = parse_w4a16_int4_weight_config(weight_config)[0]
64 + self.pack_factor = 8
65 + self.pack_reorder = pack_method == "reorder"
66 + self.is_symmetric = weight_quant_key.symmetric
67 + self.quant_type = (
68 + scalar_types.uint4b8 if self.is_symmetric else scalar_types.uint4
69 + )
70 + self.kernel: MPLinearKernel | None = None
71 +
72 + @classmethod
73 + def get_min_capability(cls) -> int:
74 + return 70
75 +
76 + def create_weights(
77 + self,
78 + layer: torch.nn.Module,
79 + output_partition_sizes: list[int],
80 + input_size_per_partition: int,
81 + params_dtype: torch.dtype,
82 + weight_loader: Callable,
83 + **kwargs,
84 + ):
85 + input_size = kwargs["input_size"]
86 + output_size = kwargs["output_size"]
87 + group_size = (
88 + self.group_size if self.group_size != -1 else input_size_per_partition
89 + )
90 + if input_size_per_partition % group_size != 0:
91 + raise ValueError(
92 + "The input size is not aligned with the quantized weight shape. "
93 + "This can be caused by too large tensor parallel size. "
94 + f"input_size_per_partition={input_size_per_partition}, "
95 + f"group_size={group_size}."
96 + )
97 +
98 + output_size_per_partition = sum(output_partition_sizes)
99 + packed_output_size_per_partition = math.ceil(
100 + output_size_per_partition / self.pack_factor
101 + )
102 + layer.output_size_per_partition = output_size_per_partition
103 + layer.packed_output_size_per_partition = packed_output_size_per_partition
104 +
105 + mp_linear_kernel_config = MPLinearLayerConfig(
106 + full_weight_shape=(input_size, output_size),
107 + partition_weight_shape=(
108 + input_size_per_partition,
109 + output_size_per_partition,
110 + ),
111 + weight_type=self.quant_type,
112 + act_type=params_dtype,
113 + group_size=group_size,
114 + zero_points=not self.is_symmetric,
115 + )
116 + kernel_type = choose_mp_linear_kernel(mp_linear_kernel_config)
117 +
118 + def weight_scale_loader(
119 + param: torch.nn.Parameter,
120 + loaded_weight: torch.Tensor,
121 + *args,
122 + **loader_kwargs,
123 + ) -> None:
124 + if loaded_weight.shape[1] < param.data.shape[1]:
125 + padded_weight = loaded_weight.new_zeros(param.data.shape)
126 + padded_weight[:, : loaded_weight.shape[1]] = loaded_weight
127 + loaded_weight = padded_weight
128 + weight_loader(param, loaded_weight, *args, **loader_kwargs)
129 +
130 + weight = PackedvLLMParameter(
131 + data=torch.empty(
132 + input_size_per_partition,
133 + packed_output_size_per_partition,
134 + dtype=torch.int32,
135 + ),
136 + input_dim=0,
137 + output_dim=1,
138 + packed_dim=1,
139 + packed_factor=self.pack_factor,
140 + weight_loader=weight_loader,
141 + )
142 + num_groups = input_size_per_partition // group_size
143 + weight_zero_point = PackedvLLMParameter(
144 + data=torch.zeros(
145 + num_groups,
146 + packed_output_size_per_partition,
147 + dtype=torch.int32,
148 + ),
149 + input_dim=0,
150 + output_dim=1,
151 + packed_dim=1,
152 + packed_factor=self.pack_factor,
153 + weight_loader=weight_loader,
154 + )
155 + weight_scale = GroupQuantScaleParameter(
156 + data=torch.empty(
157 + num_groups,
158 + packed_output_size_per_partition * self.pack_factor,
159 + dtype=params_dtype,
160 + ),
161 + input_dim=0,
162 + output_dim=1,
163 + weight_loader=weight_scale_loader,
164 + )
165 +
166 + layer.register_parameter("weight", weight)
167 + layer.register_parameter("weight_zero_point", weight_zero_point)
168 + layer.register_parameter("weight_scale", weight_scale)
169 +
170 + self.kernel = kernel_type(
171 + mp_linear_kernel_config,
172 + w_q_param_name="weight",
173 + w_s_param_name="weight_scale",
174 + w_zp_param_name="weight_zero_point" if not self.is_symmetric else None,
175 + )
176 +
177 + def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
178 + layer.weight.data = canonicalize_quark_packed_int4(
179 + layer.weight.data,
180 + pack_reorder=self.pack_reorder,
181 + is_symmetric=self.is_symmetric,
182 + pack_factor=self.pack_factor,
183 + )
184 + if not self.is_symmetric:
185 + layer.weight_zero_point.data = canonicalize_quark_packed_int4(
186 + layer.weight_zero_point.data,
187 + pack_reorder=self.pack_reorder,
188 + is_symmetric=self.is_symmetric,
189 + pack_factor=self.pack_factor,
190 + )
191 + output_size = layer.output_size_per_partition
192 + packed_output_size = layer.packed_output_size_per_partition * self.pack_factor
193 + if output_size < packed_output_size:
194 + layer.weight_scale.data[:, output_size:].zero_()
195 +
196 + _convert_awq_to_standard_format(
197 + layer,
198 + "weight",
199 + "weight_zero_point" if not self.is_symmetric else None,
200 + self.quant_type.size_bits,
201 + )
202 + assert self.kernel is not None
203 + self.kernel.process_weights_after_loading(layer)
204 +
205 + def apply_weights(
206 + self,
207 + layer: torch.nn.Module,
208 + x: torch.Tensor,
209 + bias: torch.Tensor | None = None,
210 + ):
211 + assert self.kernel is not None
212 + return self.kernel.apply_weights(layer, x, bias)
vllm/model_executor/layers/quantization/quark/utils.py
+57
@@ -66,6 +66,63 @@ def should_ignore_layer(
66 return all(shards_ignored)
67
68
69 +def parse_w4a16_int4_weight_config(
70 + weight_config: Mapping[str, Any],
71 +) -> tuple[int, bool]:
72 + """Parse required W4A16 INT4/UINT4 weight fields from Quark config."""
73 + if "group_size" not in weight_config:
74 + raise ValueError(
75 + "Quark W4A16 INT4/UINT4 configs must specify weight.group_size"
76 + )
77 + if "symmetric" not in weight_config:
78 + raise ValueError("Quark W4A16 INT4/UINT4 configs must specify weight.symmetric")
79 +
80 + group_size = weight_config["group_size"]
81 + is_symmetric = weight_config["symmetric"]
82 + if not isinstance(group_size, int) or group_size <= 0:
83 + raise ValueError(
84 + f"Quark W4A16 weight.group_size must be a positive int, got {group_size!r}"
85 + )
86 + if not isinstance(is_symmetric, bool):
87 + raise ValueError(
88 + f"Quark W4A16 weight.symmetric must be a bool, got {is_symmetric!r}"
89 + )
90 + return group_size, is_symmetric
91 +
92 +
93 +def canonicalize_quark_packed_int4(
94 + packed_weight: torch.Tensor,
95 + *,
96 + pack_reorder: bool,
97 + is_symmetric: bool,
98 + pack_factor: int = 8,
99 +) -> torch.Tensor:
100 + """Convert Quark export nibble layout to AWQ checkpoint layout."""
101 + from vllm.model_executor.layers.quantization.auto_awq import (
102 + _REVERSE_AWQ_PACK_ORDER,
103 + )
104 +
105 + if pack_reorder:
106 + source_order = torch.tensor(
107 + _REVERSE_AWQ_PACK_ORDER, device=packed_weight.device, dtype=torch.int32
108 + )
109 + else:
110 + source_order = torch.arange(
111 + pack_factor, device=packed_weight.device, dtype=torch.int32
112 + )
113 + target_order = torch.tensor(
114 + _REVERSE_AWQ_PACK_ORDER, device=packed_weight.device, dtype=torch.int32
115 + )
116 + source_shifts = source_order * 4
117 + target_shifts = target_order * 4
118 +
119 + values = (packed_weight.to(torch.int32)[..., None] >> source_shifts) & 0xF
120 + if is_symmetric:
121 + values = values ^ 0x8
122 + packed = (values.to(torch.int64) << target_shifts.to(torch.int64)).sum(dim=-1)
123 + return packed.to(torch.int32)
124 +
125 +
126 # utility for tensor dims > 2 cases
127 def quark_quantize_weight_to_mxfp4(w: torch.Tensor):
128 assert w.dtype == torch.bfloat16, (