A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Transform] [Quantization] Add transforms to compressed tensors (#22486)
Kyle Sayers committed
Aug 28, 2025 at 02:43 UTC
22feac8e957a2f9787eb721c685269afc15bb3b1
9 files changed
+661
-36
tests/conftest.py
+37
-6
@@ -1,10 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import json
4
+import math
5
import os
6
import tempfile
7
from enum import Enum
7
-from typing import Any, Callable, Optional, TypedDict, TypeVar, Union
8
+from typing import Any, Callable, Optional, TypedDict, TypeVar, Union, cast
9
10
import numpy as np
11
import pytest
@@ -33,6 +34,7 @@ from vllm.inputs import (ExplicitEncoderDecoderPrompt, TextPrompt,
34
from vllm.logger import init_logger
35
from vllm.outputs import RequestOutput
36
from vllm.sampling_params import BeamSearchParams
37
+from vllm.sequence import Logprob
38
from vllm.transformers_utils.utils import maybe_model_redirect
39
40
logger = init_logger(__name__)
@@ -602,7 +604,7 @@ class HfRunner:
604
def _hidden_states_to_logprobs(
605
self,
606
hidden_states: tuple[tuple[torch.Tensor, ...], ...],
605
- num_logprobs: int,
607
+ num_logprobs: Optional[int],
608
) -> tuple[list[dict[int, float]], int]:
609
seq_logprobs = self._hidden_states_to_seq_logprobs(hidden_states)
610
output_len = len(hidden_states)
@@ -630,7 +632,7 @@ class HfRunner:
632
self,
633
prompts: list[str],
634
max_tokens: int,
633
- num_logprobs: int,
635
+ num_logprobs: Optional[int],
636
images: Optional[PromptImageInput] = None,
637
audios: Optional[PromptAudioInput] = None,
638
videos: Optional[PromptVideoInput] = None,
@@ -677,7 +679,7 @@ class HfRunner:
679
self,
680
encoder_decoder_prompts: list[ExplicitEncoderDecoderPrompt[str, str]],
681
max_tokens: int,
680
- num_logprobs: int,
682
+ num_logprobs: Optional[int],
683
images: Optional[PromptImageInput] = None,
684
**kwargs: Any,
685
) -> list[TokensTextLogprobs]:
@@ -966,7 +968,7 @@ class VllmRunner:
968
self,
969
prompts: list[str],
970
max_tokens: int,
969
- num_logprobs: int,
971
+ num_logprobs: Optional[int],
972
num_prompt_logprobs: Optional[int] = None,
973
images: Optional[PromptImageInput] = None,
974
audios: Optional[PromptAudioInput] = None,
@@ -991,11 +993,40 @@ class VllmRunner:
993
videos=videos,
994
**kwargs)
995
996
+ def generate_prompt_perplexity(self, prompts: list[str]) -> list[float]:
997
+ """
998
+ Return the perplexity score associated with generating the prompts
999
+
1000
+ :param prompts: list of prompts to score
1001
+ :return: perplexity score of each prompt
1002
+ """
1003
+ outputs = self.generate_greedy_logprobs(prompts,
1004
+ max_tokens=1,
1005
+ num_logprobs=None,
1006
+ num_prompt_logprobs=0)
1007
+
1008
+ perplexities = []
1009
+ for output in outputs:
1010
+ output = cast(TokensTextLogprobsPromptLogprobs, output)
1011
+ token_datas = cast(list[Optional[dict[int, Logprob]]], output[3])
1012
+ assert token_datas[0] is None
1013
+ token_log_probs = []
1014
+ for token_data in token_datas[1:]:
1015
+ assert token_data is not None
1016
+ assert len(token_data) == 1
1017
+ token_log_prob = list(token_data.values())[0].logprob
1018
+ token_log_probs.append(token_log_prob)
1019
+
1020
+ perplexity = math.exp(-sum(token_log_probs) / len(token_log_probs))
1021
+ perplexities.append(perplexity)
1022
+
1023
+ return perplexities
1024
+
1025
def generate_encoder_decoder_greedy_logprobs(
1026
self,
1027
encoder_decoder_prompts: list[ExplicitEncoderDecoderPrompt[str, str]],
1028
max_tokens: int,
998
- num_logprobs: int,
1029
+ num_logprobs: Optional[int],
1030
num_prompt_logprobs: Optional[int] = None,
1031
skip_special_tokens: bool = True,
1032
) -> Union[list[TokensTextLogprobs],
tests/quantization/test_compressed_tensors.py
+22
@@ -719,3 +719,25 @@ def test_compressed_tensors_w4a8_fp8(vllm_runner, args):
719
output = llm.generate_greedy("Hello my name is", max_tokens=20)
720
print(output)
721
assert output
722
+
723
+
724
+@pytest.mark.skipif(not current_platform.is_cuda(),
725
+ reason="This test is skipped on non-CUDA platform.")
726
+@pytest.mark.parametrize("model,prompt,exp_perplexity", [
727
+ (
728
+ "nm-testing/Llama-3.2-1B-Instruct-spinquantR1R2R4-w4a16",
729
+ "Flat is better than nested.\nSparse is better than dense.",
730
+ 150.0,
731
+ ),
732
+ (
733
+ "nm-testing/Llama-3.2-1B-Instruct-quip-w4a16",
734
+ "Flat is better than nested.\nSparse is better than dense.",
735
+ 150.0,
736
+ ),
737
+])
738
+def test_compressed_tensors_transforms_perplexity(vllm_runner, model, prompt,
739
+ exp_perplexity):
740
+ with vllm_runner(model, enforce_eager=True) as llm:
741
+ perplexity = llm.generate_prompt_perplexity([prompt])[0]
742
+ print(perplexity)
743
+ assert perplexity <= exp_perplexity
\ No newline at end of file
vllm/model_executor/layers/linear.py
+15
-1
@@ -35,6 +35,7 @@ logger = init_logger(__name__)
35
36
WEIGHT_LOADER_V2_SUPPORTED = [
37
"CompressedTensorsLinearMethod",
38
+ "CompressedTensorsLinearTransformMethod",
39
"BitBLASLinearMethod",
40
"GPTQBitBLASLinearMethod",
41
"AWQMarlinLinearMethod",
@@ -199,6 +200,7 @@ class UnquantizedLinearMethod(LinearMethodBase):
200
set_weight_attrs(weight, extra_weight_attrs)
201
202
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
203
+ # special postprocessing for CPU SGL
204
if current_platform.is_cpu() and envs.VLLM_CPU_SGL_KERNEL:
205
from vllm.model_executor.layers.utils import check_cpu_sgl_kernel
206
N, K = layer.weight.size()
@@ -1470,7 +1472,7 @@ class QKVCrossParallelLinear(LinearBase):
1472
self.bias = torch.nn.Parameter()
1473
set_weight_attrs(self.bias, {
1474
"output_dim": 0,
1473
- "weight_loader": self.weight_loader,
1475
+ "weight_loader": self.weight_loader_v1,
1476
})
1477
else:
1478
self.bias = None
@@ -1580,6 +1582,18 @@ class QKVCrossParallelLinear(LinearBase):
1582
k, v = kv_enc.split(self.kv_size, dim=-1)
1583
return q, k, v
1584
1585
+ def weight_loader_v1(self,
1586
+ param: torch.nn.Parameter,
1587
+ loaded_weight: torch.Tensor,
1588
+ loaded_shard_id: Optional[str] = None):
1589
+ # just like all other parameters, does not yet
1590
+ # support loading bias with weight_loader_v2
1591
+ layer = (self.q_proj_decoder
1592
+ if loaded_shard_id == "q" else self.kv_proj_encoder)
1593
+ target_param = self.select_proj_params(layer, param)
1594
+ shard_id_args = (loaded_shard_id, ) if loaded_shard_id != "q" else ()
1595
+ layer.weight_loader(target_param, loaded_weight, *shard_id_args)
1596
+
1597
def weight_loader(self,
1598
param: torch.nn.Parameter,
1599
loaded_weight: torch.Tensor,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors.py
+37
-15
@@ -11,6 +11,7 @@ from compressed_tensors.config import (CompressionFormat,
11
from compressed_tensors.quantization import (QuantizationArgs,
12
QuantizationStrategy,
13
QuantizationType)
14
+from compressed_tensors.transform import TransformConfig
15
from pydantic import BaseModel
16
17
import vllm.envs as envs
@@ -30,6 +31,8 @@ from vllm.model_executor.layers.quantization.compressed_tensors.schemes import (
31
CompressedTensorsW4A16Fp4, CompressedTensorsW4A16Sparse24,
32
CompressedTensorsW8A8Fp8, CompressedTensorsW8A8Int8,
33
CompressedTensorsW8A16Fp8, CompressedTensorsWNA16)
34
+from vllm.model_executor.layers.quantization.compressed_tensors.transform.linear import ( # noqa: E501
35
+ CompressedTensorsLinearTransformMethod, get_linear_transform_schemes)
36
from vllm.model_executor.layers.quantization.compressed_tensors.utils import (
37
find_matched_target, is_activation_quantization_format,
38
should_ignore_layer)
@@ -60,6 +63,7 @@ class CompressedTensorsConfig(QuantizationConfig):
63
sparsity_ignore_list: list[str],
64
kv_cache_scheme: Optional[dict[str, Any]] = None,
65
config: Optional[dict[str, Any]] = None,
66
+ transform_config: Optional[TransformConfig] = None,
67
):
68
super().__init__()
69
self.ignore = ignore
@@ -71,6 +75,12 @@ class CompressedTensorsConfig(QuantizationConfig):
75
self.sparsity_ignore_list = sparsity_ignore_list
76
self.config = config
77
78
+ if transform_config is not None:
79
+ self.transform_config = TransformConfig.model_validate(
80
+ transform_config)
81
+ else:
82
+ self.transform_config = None
83
+
84
def get_linear_method(self) -> "CompressedTensorsLinearMethod":
85
return CompressedTensorsLinearMethod(self)
86
@@ -103,18 +113,27 @@ class CompressedTensorsConfig(QuantizationConfig):
113
) -> Optional["QuantizeMethodBase"]:
114
from vllm.attention.layer import Attention # Avoid circular import
115
106
- # Check if the layer is skipped for quantization.
107
- # TODO (@robertgshaw2): support module names
108
- if should_ignore_layer(prefix,
109
- ignore=self.ignore,
110
- fused_mapping=self.packed_modules_mapping):
111
- return UnquantizedLinearMethod()
116
if isinstance(layer, LinearBase):
113
- scheme = self.get_scheme(layer=layer, layer_name=prefix)
114
- if scheme is None:
115
- return UnquantizedLinearMethod()
116
- layer.scheme = scheme
117
- return CompressedTensorsLinearMethod(self)
117
+ # collect schemes
118
+ quant_scheme = self.get_scheme(layer=layer, layer_name=prefix)
119
+ input_tfms, output_tfms = get_linear_transform_schemes(
120
+ layer, prefix, self.transform_config,
121
+ self.packed_modules_mapping)
122
+
123
+ # choose quantization method
124
+ quant_method: LinearMethodBase = UnquantizedLinearMethod()
125
+ if quant_scheme is not None:
126
+ layer.scheme = quant_scheme
127
+ quant_method = CompressedTensorsLinearMethod(self)
128
+
129
+ # choose transform method
130
+ if any((input_tfms, output_tfms)):
131
+ return CompressedTensorsLinearTransformMethod.from_schemes(
132
+ quant_method, input_tfms, output_tfms)
133
+
134
+ else:
135
+ return quant_method
136
+
137
if isinstance(layer, Attention):
138
return CompressedTensorsKVCacheMethod(self)
139
if isinstance(layer, FusedMoE):
@@ -129,6 +148,7 @@ class CompressedTensorsConfig(QuantizationConfig):
148
config=config)
149
sparsity_scheme_map, sparsity_ignore_list = cls._parse_sparsity_config(
150
config=config)
151
+ transform_config = config.get("transform_config")
152
153
return cls(
154
target_scheme_map=target_scheme_map,
@@ -137,6 +157,7 @@ class CompressedTensorsConfig(QuantizationConfig):
157
sparsity_scheme_map=sparsity_scheme_map,
158
sparsity_ignore_list=sparsity_ignore_list,
159
config=config,
160
+ transform_config=transform_config,
161
)
162
163
@classmethod
@@ -537,9 +558,11 @@ class CompressedTensorsConfig(QuantizationConfig):
558
559
# Find the "target" in the compressed-tensors config
560
# that our layer conforms to.
540
- # TODO (@robertgshaw): add compressed-tensors as dep
541
- # so we do not have to re-write these functions
542
- # need to make accelerate optional in ct to do this
561
+ # TODO (@kylesayrs): support ignore module names with ct matching utils
562
+ if should_ignore_layer(layer_name,
563
+ ignore=self.ignore,
564
+ fused_mapping=self.packed_modules_mapping):
565
+ return None
566
567
# Will be empty for models with only sparsity
568
weight_quant = input_quant = None
@@ -722,7 +745,6 @@ class CompressedTensorsLinearMethod(LinearMethodBase):
745
layer input. See LinearMethodBase for param details
746
747
"""
725
-
748
scheme = layer.scheme
749
if scheme is None:
750
raise ValueError("A scheme must be defined for each layer")
vllm/model_executor/layers/quantization/compressed_tensors/transform/linear.py
new
+227
@@ -0,0 +1,227 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from collections.abc import Generator
4
+from itertools import accumulate
5
+from typing import Callable, Optional
6
+
7
+import torch
8
+from compressed_tensors.transform import (TransformArgs, TransformConfig,
9
+ TransformLocation, TransformScheme)
10
+from compressed_tensors.utils import is_match
11
+
12
+from vllm.model_executor.layers.linear import (WEIGHT_LOADER_V2_SUPPORTED,
13
+ LinearMethodBase,
14
+ QKVCrossParallelLinear)
15
+from vllm.model_executor.layers.quantization.compressed_tensors.transform.module import ( # noqa: E501
16
+ HadamardTransform)
17
+from vllm.model_executor.layers.quantization.compressed_tensors.transform.utils import ( # noqa: E501
18
+ TransformTuple)
19
+
20
+
21
+class CompressedTensorsLinearTransformMethod(LinearMethodBase):
22
+ """
23
+ Wraps `CompressedTensorsLinearMethod` or `UnquantizedLinearMethod` and adds
24
+ input and output transforms to either side of the original apply method
25
+ """
26
+
27
+ @classmethod
28
+ def from_schemes(
29
+ cls, quant_method: LinearMethodBase, input_tfms: dict[int,
30
+ TransformTuple],
31
+ output_tfms: dict[int, TransformTuple]
32
+ ) -> "CompressedTensorsLinearTransformMethod":
33
+ assert input_tfms or output_tfms
34
+
35
+ # TODO (@ksayers): implement QutlassLinearMethodNvFP4
36
+ # hadacore and fwht can be selected by Transform module
37
+
38
+ return cls(quant_method, input_tfms, output_tfms)
39
+
40
+ def __init__(self, quant_method: LinearMethodBase,
41
+ input_tfms: dict[int, TransformTuple],
42
+ output_tfms: dict[int, TransformTuple]):
43
+ self.quant_method = quant_method
44
+ self.input_tfms = input_tfms
45
+ self.output_tfms = output_tfms
46
+
47
+ self.input_transform: Optional[HadamardTransform] = None
48
+ self.output_transform: Optional[HadamardTransform] = None
49
+
50
+ def create_weights(self, layer: torch.nn.Module,
51
+ input_size_per_partition: int,
52
+ output_partition_sizes: list[int], input_size: int,
53
+ output_size: int, params_dtype: torch.dtype,
54
+ **extra_weight_attrs):
55
+
56
+ # get weight loader for transforms
57
+ weight_loader: Callable = extra_weight_attrs.get(
58
+ "weight_loader") # type: ignore[assignment]
59
+
60
+ # HACK: UnquantizedLinearMethod does not support weight loader v2, but
61
+ # transforms (specifically SharedWeightParameter) requires
62
+ # weight loader v2. Until UnquantizedLinearMethod supports v2, we must
63
+ # hack around this by getting weight loader v1 so ULM can load correctly
64
+ quant_method_name = self.quant_method.__class__.__name__
65
+ if quant_method_name not in WEIGHT_LOADER_V2_SUPPORTED:
66
+ if isinstance(layer, QKVCrossParallelLinear):
67
+ weight_loader_v1 = layer.weight_loader_v1
68
+ else:
69
+ weight_loader_v1 = layer.weight_loader
70
+ extra_weight_attrs["weight_loader"] = weight_loader_v1
71
+
72
+ self.quant_method.create_weights(
73
+ layer=layer,
74
+ input_size_per_partition=input_size_per_partition,
75
+ output_partition_sizes=output_partition_sizes,
76
+ input_size=input_size,
77
+ output_size=output_size,
78
+ params_dtype=params_dtype,
79
+ **extra_weight_attrs)
80
+
81
+ # validate schemes
82
+ num_partitions = len(output_partition_sizes)
83
+ self._validate_tfm_schemes(num_partitions)
84
+
85
+ # create submodules for weight loading
86
+ if len(self.input_tfms) > 0:
87
+ scheme_name = list(self.input_tfms.values())[0].scheme_name
88
+ location = list(self.input_tfms.values())[0].args.location
89
+ transform_name = f"{scheme_name}_{location}"
90
+
91
+ transform = HadamardTransform(self.input_tfms, layer,
92
+ weight_loader,
93
+ input_size_per_partition,
94
+ output_partition_sizes)
95
+ layer.register_module(transform_name, transform)
96
+ self.input_transform = transform
97
+
98
+ if len(self.output_tfms) > 0:
99
+ scheme_name = list(self.output_tfms.values())[0].scheme_name
100
+ location = list(self.output_tfms.values())[0].args.location
101
+ transform_name = f"{scheme_name}_{location}"
102
+
103
+ transform = HadamardTransform(self.output_tfms, layer,
104
+ weight_loader,
105
+ input_size_per_partition,
106
+ output_partition_sizes)
107
+ layer.register_module(transform_name, transform)
108
+ self.output_transform = transform
109
+
110
+ # compute partition ranges for slicing activations
111
+ starts = [0] + list(accumulate(output_partition_sizes))[:-1]
112
+ self.partition_ranges = list(zip(starts, output_partition_sizes))
113
+
114
+ def process_weights_after_loading(self, layer):
115
+ self.quant_method.process_weights_after_loading(layer)
116
+
117
+ for submodule in layer.children():
118
+ if isinstance(submodule, HadamardTransform):
119
+ submodule.process_weights_after_loading()
120
+
121
+ def apply(self,
122
+ layer: torch.nn.Module,
123
+ x: torch.Tensor,
124
+ bias: Optional[torch.Tensor] = None) -> torch.Tensor:
125
+
126
+ if self.input_transform is not None:
127
+ x = self.input_transform(x)
128
+
129
+ assert bias is None
130
+ x = self.quant_method.apply(layer, x, bias)
131
+
132
+ # TODO (@ksayers): Write a triton kernel to do this in parallel
133
+ if self.output_transform is not None:
134
+ for part_id, (start, length) in enumerate(self.partition_ranges):
135
+ x[:, start:start + length] = self.output_transform(
136
+ x[:, start:start + length], part_id=part_id)
137
+
138
+ return x
139
+
140
+ def _validate_tfm_schemes(self, num_partitions: int):
141
+ if len(self.input_tfms) > 0:
142
+ if 0 not in self.input_tfms:
143
+ raise ValueError("Must have same input")
144
+
145
+ for part_index in range(num_partitions):
146
+ if self.input_tfms[part_index] != self.input_tfms[0]:
147
+ raise ValueError("Must have same input")
148
+
149
+ if len(self.output_tfms) > 0:
150
+ scheme_name = list(self.output_tfms.values())[0].scheme_name
151
+ location = list(self.output_tfms.values())[0].args.location
152
+
153
+ for tfm in self.output_tfms.values():
154
+ if tfm.scheme_name != scheme_name:
155
+ raise ValueError("Must have same scheme name")
156
+ if tfm.args.location != location:
157
+ raise ValueError("Must have same location")
158
+
159
+ return self.input_tfms, self.output_tfms
160
+
161
+
162
+def get_linear_transform_schemes(
163
+ layer: torch.nn.Module, layer_name: str,
164
+ transform_config: Optional[TransformConfig],
165
+ packed_modules_mapping: dict[str, list[str]]
166
+) -> tuple[dict[int, TransformTuple], dict[
167
+ int, TransformTuple]]: # [input_transform, [output_transform, ...]]
168
+ # there can only be one transform input scheme per (fused) module
169
+ input_tfms = {}
170
+ output_tfms = {}
171
+
172
+ partition_names = get_layer_partition_names(layer_name,
173
+ packed_modules_mapping)
174
+
175
+ for scheme_name, scheme, args in get_schemes_args(transform_config):
176
+ for part_index, part_name in enumerate(partition_names):
177
+ if is_match(part_name, layer, args.targets,
178
+ args.ignore) and args.is_online():
179
+ if args.location == TransformLocation.INPUT:
180
+ input_tfms[part_index] = TransformTuple(
181
+ scheme_name, scheme, args)
182
+
183
+ elif args.location == TransformLocation.OUTPUT:
184
+ output_tfms[part_index] = TransformTuple(
185
+ scheme_name, scheme, args)
186
+
187
+ else:
188
+ raise ValueError(f"Cannot apply `{args.location}` "
189
+ f"transform to `{layer_name}`")
190
+
191
+ return (input_tfms, output_tfms)
192
+
193
+
194
+def get_schemes_args(
195
+ transform_config: Optional[TransformConfig]
196
+) -> Generator[tuple[str, TransformScheme, TransformArgs]]:
197
+ if transform_config is None:
198
+ return
199
+
200
+ for scheme_name, scheme in transform_config.config_groups.items():
201
+ for args in scheme.apply:
202
+ yield (scheme_name, scheme, args)
203
+
204
+
205
+def get_layer_partition_names(
206
+ layer_name: str, packed_modules_mapping: dict[str,
207
+ list[str]]) -> list[str]:
208
+ """
209
+ Get all partition names associated with this layer.
210
+ Names are returned in order of their partition indices.
211
+
212
+ ```python
213
+ mapping = {"gate_up_proj", "gate_proj", "up_proj"}
214
+
215
+ assert get_layer_partition_names(
216
+ "mlp.gate_up_proj", mapping) == ["gate_proj", "up_proj"]
217
+ assert get_layer_partition_names(
218
+ "mlp.down_proj", mapping) == ["down_proj"]
219
+ """
220
+ for fused_suffix, part_suffixes in packed_modules_mapping.items():
221
+ if layer_name.endswith(fused_suffix):
222
+ return [
223
+ layer_name.removesuffix(fused_suffix) + part_suffix
224
+ for part_suffix in part_suffixes
225
+ ]
226
+
227
+ return [layer_name]
vllm/model_executor/layers/quantization/compressed_tensors/transform/module.py
new
+135
@@ -0,0 +1,135 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import math
4
+from collections.abc import Hashable
5
+from typing import Callable, Optional
6
+
7
+import torch
8
+from compressed_tensors.transform import TransformLocation, TransformScheme
9
+from torch import Tensor
10
+
11
+from vllm.distributed.parallel_state import (
12
+ get_tensor_model_parallel_world_size)
13
+from vllm.model_executor.layers.linear import LinearBase
14
+from vllm.model_executor.layers.quantization.compressed_tensors.transform.utils import ( # noqa: E501
15
+ TransformTuple)
16
+from vllm.model_executor.layers.utils import dispatch_unquantized_gemm
17
+from vllm.model_executor.layers.vocab_parallel_embedding import (
18
+ VocabParallelEmbedding)
19
+from vllm.model_executor.parameter import SharedWeightParameter
20
+
21
+
22
+class HadamardTransform(torch.nn.Module):
23
+ """
24
+ Class which handles weight loading, postprocessing, and application of
25
+ transforms. Meant to be used with `CompressedTensorsLinearTransformMethod`
26
+ and attention transforms method (not implemented yet)
27
+ """
28
+ transforms: dict[int, TransformTuple] # info parsed from transforms config
29
+ weight: SharedWeightParameter # container for shared tensors
30
+
31
+ kernel: Callable # function used during application
32
+ scales: dict[int, float] # hadamard scale, usually sqrt(matrix.size(0))
33
+
34
+ def __init__(self,
35
+ transforms: dict[int, TransformTuple],
36
+ layer: torch.nn.Module,
37
+ weight_loader: Callable,
38
+ input_size_per_partition: int,
39
+ output_partition_sizes: list[int],
40
+ kernel: Optional[Callable] = None):
41
+ super().__init__()
42
+ self.transforms = transforms
43
+ self.scales = {}
44
+
45
+ if get_tensor_model_parallel_world_size() > 1:
46
+ raise NotImplementedError("Online transforms with tensor "
47
+ "parallelism is not supported")
48
+
49
+ # Similar to row/col parallel params, but tensors are separate
50
+ # to allow for loading with shared memory
51
+ self.weight = SharedWeightParameter(weight_loader=weight_loader)
52
+
53
+ # create shared partition data for each partition of the original weight
54
+ input_size = input_size_per_partition
55
+ for part_index, (_scheme_name, scheme,
56
+ args) in self.transforms.items():
57
+ output_size = output_partition_sizes[part_index]
58
+ weight_size = self._get_weight_size(layer, args.location,
59
+ input_size, output_size)
60
+
61
+ data_key = self._get_data_key(scheme, weight_size)
62
+ self.weight.add_partition(
63
+ part_index,
64
+ data_key,
65
+ size=(weight_size, weight_size),
66
+ dtype=scheme.precision,
67
+ )
68
+
69
+ # validate that shared tensors and schemes are correct
70
+ self._validate_input_transforms()
71
+
72
+ # select kernel based on transform schemes
73
+ self.kernel = self._infer_kernel() if kernel is None else kernel
74
+
75
+ def process_weights_after_loading(self):
76
+ for part_id in self.weight.partitions:
77
+ data = self.weight.partitions[part_id].data
78
+
79
+ # required by torch.compile
80
+ self.weight.process_weights_after_loading()
81
+
82
+ # precompute scale as a runtime multiply, not division
83
+ # do not fold into weight in order to utilize FWHT
84
+ self.scales[part_id] = 1 / math.sqrt(data.size(0))
85
+
86
+ # FUTURE: avoid runtime tranpose by processing weights
87
+ # prior to apply
88
+
89
+ def forward(self, value: Tensor, part_id: int = 0) -> Tensor:
90
+ if part_id not in self.weight.partitions:
91
+ return value
92
+
93
+ weight = self.weight.partitions[part_id]
94
+ weight = weight if self.transforms[
95
+ part_id].args.inverse else weight.T # linear := x(W.T)
96
+ scale = self.scales[part_id]
97
+ return self.kernel(self, value.to(weight.dtype), weight, None).to(
98
+ value.dtype) * scale
99
+
100
+ def _get_data_key(self, scheme: TransformScheme,
101
+ weight_size: int) -> Hashable:
102
+ return (id(scheme), weight_size)
103
+
104
+ def _get_weight_size(self, layer: torch.nn.Module,
105
+ location: TransformLocation, input_size: int,
106
+ output_size: int) -> int:
107
+ if isinstance(layer, LinearBase):
108
+ if location == TransformLocation.INPUT:
109
+ return input_size
110
+
111
+ elif location == TransformLocation.OUTPUT:
112
+ return output_size
113
+
114
+ elif isinstance(layer, VocabParallelEmbedding):
115
+ if location == TransformLocation.INPUT:
116
+ return output_size
117
+
118
+ elif location == TransformLocation.OUTPUT:
119
+ return input_size
120
+
121
+ raise ValueError()
122
+
123
+ def _validate_input_transforms(self):
124
+ assert len(self.transforms) > 0
125
+ location = list(self.transforms.values())[0].args.location
126
+
127
+ if location == TransformLocation.INPUT:
128
+ first_data = self.weight.partitions[0].data
129
+ for partition in self.weight.partitions.values():
130
+ if partition.data.data_ptr() != first_data.data_ptr():
131
+ raise ValueError("")
132
+
133
+ def _infer_kernel(self) -> Callable:
134
+ # TODO (@ksayers): use fwht, hadacore
135
+ return dispatch_unquantized_gemm()
vllm/model_executor/layers/quantization/compressed_tensors/transform/schemes/linear_qutlass_nvfp4.py
new
+21
@@ -0,0 +1,21 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from typing import Optional
4
+
5
+import torch
6
+
7
+from vllm.model_executor.layers.quantization.compressed_tensors.transform.linear import ( # noqa: E501
8
+ CompressedTensorsLinearTransformMethod)
9
+
10
+
11
+# Because qutlass fuses hadamard with quantization, it cannot automatically be
12
+# composed with kernels in the way CompressedTensorsLinearTransformMethod does.
13
+# Therefore, a separate scheme must be created for each quantized dtype
14
+class QutlassLinearMethodNvFP4(CompressedTensorsLinearTransformMethod):
15
+
16
+ def apply(self,
17
+ layer: torch.nn.Module,
18
+ x: torch.Tensor,
19
+ bias: Optional[torch.Tensor] = None) -> torch.Tensor:
20
+ # fused hadamard quant linear method
21
+ raise NotImplementedError()
vllm/model_executor/layers/quantization/compressed_tensors/transform/utils.py
new
+13
@@ -0,0 +1,13 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from typing import NamedTuple
4
+
5
+from compressed_tensors.transform import TransformArgs, TransformScheme
6
+
7
+__all__ = ["TransformTuple"]
8
+
9
+
10
+class TransformTuple(NamedTuple):
11
+ scheme_name: str
12
+ scheme: TransformScheme
13
+ args: TransformArgs
vllm/model_executor/parameter.py
+154
-14
@@ -1,13 +1,16 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from collections.abc import Hashable
5
from fractions import Fraction
6
from typing import Callable, Optional, Union
7
+from weakref import WeakValueDictionary
8
9
import torch
10
from torch.nn import Parameter
11
10
-from vllm.distributed import get_tensor_model_parallel_rank
12
+from vllm.distributed import (get_tensor_model_parallel_rank,
13
+ get_tensor_model_parallel_world_size)
14
from vllm.logger import init_logger
15
from vllm.model_executor.utils import _make_synced_weight_loader
16
@@ -27,7 +30,7 @@ class BasevLLMParameter(Parameter):
30
into the parameter when the provided weight loader is called.
31
"""
32
30
- def __new__(cls, data: torch.Tensor, **kwargs):
33
+ def __new__(cls, data: Optional[torch.Tensor], **kwargs):
34
35
return super().__new__(cls, data=data, requires_grad=False)
36
@@ -81,6 +84,17 @@ class BasevLLMParameter(Parameter):
84
def load_qkv_weight(self, loaded_weight: torch.Tensor, **kwargs):
85
self._assert_and_load(loaded_weight)
86
87
+ def _shard_id_as_int(self, shard_id: Union[str, int]) -> int:
88
+ if isinstance(shard_id, int):
89
+ return shard_id
90
+
91
+ # if not int, assume shard_id for qkv
92
+ # map to int and return
93
+ qkv_idxs = {"q": 0, "k": 1, "v": 2}
94
+ assert isinstance(shard_id, str)
95
+ assert shard_id in qkv_idxs
96
+ return qkv_idxs[shard_id]
97
+
98
99
class _ColumnvLLMParameter(BasevLLMParameter):
100
"""
@@ -113,6 +127,7 @@ class _ColumnvLLMParameter(BasevLLMParameter):
127
128
shard_offset = kwargs.get("shard_offset")
129
shard_size = kwargs.get("shard_size")
130
+ # TODO: move these to PackedColumnParameter and PackedvLLMParameter
131
if isinstance(
132
self,
133
(PackedColumnParameter,
@@ -137,6 +152,7 @@ class _ColumnvLLMParameter(BasevLLMParameter):
152
shard_id = kwargs.get("shard_id")
153
num_heads = kwargs.get("num_heads")
154
155
+ # TODO: move these to PackedColumnParameter and PackedvLLMParameter
156
if isinstance(
157
self,
158
(PackedColumnParameter,
@@ -224,19 +240,8 @@ class PerTensorScaleParameter(BasevLLMParameter):
240
"""
241
242
def __init__(self, **kwargs):
227
- self.qkv_idxs = {"q": 0, "k": 1, "v": 2}
243
super().__init__(**kwargs)
244
230
- def _shard_id_as_int(self, shard_id: Union[str, int]) -> int:
231
- if isinstance(shard_id, int):
232
- return shard_id
233
-
234
- # if not int, assume shard_id for qkv
235
- # map to int and return
236
- assert isinstance(shard_id, str)
237
- assert shard_id in self.qkv_idxs
238
- return self.qkv_idxs[shard_id]
239
-
245
# For row parallel layers, no sharding needed
246
# load weight into parameter as is
247
def load_row_parallel_weight(self, *args, **kwargs):
@@ -373,6 +378,141 @@ class BlockQuantScaleParameter(_ColumnvLLMParameter, RowvLLMParameter):
378
pass
379
380
381
+class SharedWeightParameter(BasevLLMParameter):
382
+ """
383
+ Parameter for weights with many shared tensors across a model
384
+
385
+ For example, when applying transforms to the "gate" and "up" partitions of
386
+ `MergedColumnParallelLinear`, the transform weights must stay separate
387
+ tensors in order to allow for tensor memory sharing between layers.
388
+ """
389
+ # global registry for sharing tensors based on passed `data_key`
390
+ # this dict holds weaksrefs to avoid memory leak after model cleanup
391
+ tensors_registry: WeakValueDictionary = WeakValueDictionary()
392
+
393
+ # local container for strong references to shared tensors
394
+ # this set compensates for the fact that torch.nn.Parameter
395
+ # and Parameter subclasses do not hold reliable references to tensors
396
+ local_tensors: set[torch.Tensor]
397
+
398
+ # dictionary mapping partition indices to associated parameters
399
+ partitions: dict[int, Union[ModelWeightParameter, Parameter]]
400
+
401
+ def __new__(cls, **kwargs):
402
+ return super().__new__(cls, data=None, **kwargs)
403
+
404
+ def __init__(self, input_dim: int = 1, output_dim: int = 0, **kwargs):
405
+ weight_loader: Callable = kwargs.get(
406
+ "weight_loader") # type: ignore[assignment]
407
+ super().__init__(data=None, weight_loader=weight_loader)
408
+
409
+ self.local_tensors = set()
410
+ self.partitions = {}
411
+ self.kwargs = {
412
+ "input_dim": input_dim,
413
+ "output_dim": output_dim,
414
+ "weight_loader": self._fake_weight_loader
415
+ }
416
+
417
+ self.tp_rank = get_tensor_model_parallel_rank()
418
+ self.tp_size = get_tensor_model_parallel_world_size()
419
+
420
+ if self.tp_size > 1:
421
+ raise NotImplementedError(f"{self.__class__.__name__} does not "
422
+ "currently support tensor parallelism")
423
+
424
+ def add_partition(self, index: int, data_key: Hashable, *args, **kwargs):
425
+ """
426
+ Add a partition to the weight parameter. Partitions whose `data_key`
427
+ is the same will share tensor data
428
+
429
+ :param index: index of partition to add
430
+ :param data_key: hashable key used to key shared tensors
431
+ :param *args: arguments for `torch.empty`
432
+ :param **kwargs: keyword arguments for `torch.empty`
433
+ """
434
+ # load (shared) tensor using `data_key`
435
+ if data_key not in self.tensors_registry:
436
+ data = torch.empty(*args, **kwargs)
437
+ self.tensors_registry[data_key] = data
438
+ else:
439
+ data = self.tensors_registry[data_key]
440
+
441
+ # create associated model parameter
442
+ self.partitions[index] = ModelWeightParameter(
443
+ data=data, **self.kwargs) # type: ignore[arg-type]
444
+
445
+ # hold local reference, since ModelWeightParameter does not
446
+ # see https://github.com/pytorch/pytorch/issues/75932
447
+ self.local_tensors.add(data)
448
+
449
+ def load_column_parallel_weight(self, loaded_weight: torch.Tensor):
450
+ assert len(self.partitions) == 1 and 0 in self.partitions
451
+ partition = self.partitions[0]
452
+
453
+ ModelWeightParameter.load_column_parallel_weight(
454
+ partition, loaded_weight)
455
+
456
+ def load_row_parallel_weight(self, loaded_weight: torch.Tensor):
457
+ assert len(self.partitions) == 1 and 0 in self.partitions
458
+ partition = self.partitions[0]
459
+
460
+ ModelWeightParameter.load_row_parallel_weight(partition, loaded_weight)
461
+
462
+ def load_merged_column_weight(self, loaded_weight: torch.Tensor, **kwargs):
463
+ partition_id = kwargs.pop("shard_id")
464
+ partition_id = self._shard_id_as_int(partition_id)
465
+ partition = self.partitions[partition_id]
466
+
467
+ input_dim = self.kwargs.get("input_dim")
468
+ shard_size = partition.data.size(input_dim) // self.tp_size
469
+ shard_offset = self.tp_rank * shard_size
470
+
471
+ ModelWeightParameter.load_merged_column_weight(
472
+ partition,
473
+ loaded_weight,
474
+ shard_offset=shard_offset,
475
+ shard_size=shard_size)
476
+
477
+ def load_qkv_weight(self, loaded_weight: torch.Tensor, **kwargs):
478
+ partition_id = self._shard_id_as_int(kwargs.pop("shard_id"))
479
+ partition = self.partitions[partition_id]
480
+
481
+ input_dim = self.kwargs.get("input_dim")
482
+ shard_size = partition.data.size(input_dim) // self.tp_size
483
+ shard_offset = self.tp_rank * shard_size
484
+ shard_id = "q" # fake first partition
485
+ num_heads = kwargs.get("num_heads")
486
+
487
+ ModelWeightParameter.load_qkv_weight(
488
+ partition,
489
+ loaded_weight,
490
+ shard_offset=shard_offset,
491
+ shard_size=shard_size,
492
+ shard_id=shard_id,
493
+ num_heads=num_heads,
494
+ )
495
+
496
+ def process_weights_after_loading(self):
497
+ for key in self.partitions:
498
+ self.partitions[key] = torch.nn.Parameter(
499
+ data=self.partitions[key].data, requires_grad=False)
500
+
501
+ @property
502
+ def data(self):
503
+ raise ValueError("Accessing `data` of a "
504
+ "`PartitionedModelWeightParameter` is not allowed. "
505
+ "Instead, use `get_partition` to get the weight of "
506
+ "the particular partition you want to access")
507
+
508
+ def _fake_weight_loader(self, param: BasevLLMParameter,
509
+ loaded_weight: torch.Tensor,
510
+ loaded_weight_shard_id: Optional[Union[str, int]]):
511
+ raise ValueError("When loading partition weights of "
512
+ f"{self.__class__.__name__}, use methods provided by "
513
+ f"{self.__class__.__name__}, not partition loader")
514
+
515
+
516
def permute_param_layout_(param: BasevLLMParameter, input_dim: int,
517
output_dim: int, **kwargs) -> BasevLLMParameter:
518
"""
@@ -456,4 +596,4 @@ def _adjust_shard_indexes_for_packing(shard_size, shard_offset, packed_factor,
596
shard_offset=shard_offset,
597
bitblas_tile_size=bitblas_tile_size)
598
459
- return shard_size, shard_offset
\ No newline at end of file
599
+ return shard_size, shard_offset