vllm-quantpack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ """vllm-quantpack: serve EXL3 (exllamav3 trellis) checkpoints, and the model-family fixes they need, in stock vLLM.
2
+
3
+ Installed as a vLLM general plugin (entry point `vllm.general_plugins`), so `vllm serve <repo>` picks up any checkpoint
4
+ whose config says `quant_method: "exl3"` with no flag and no patch to vLLM.
5
+
6
+ Layout:
7
+ exl3/ quantization config, linear / embedding / MoE methods, the custom ops, tensor-parallel cuts
8
+ families/ fixes for specific model families (Qwen vision and Flash-Next PLE) and models vLLM lacks or
9
+ maps incompletely (Flint, Spark, Nemotron-3.5-Super-VL)
10
+
11
+ It replaces OrcaSAQ2 (whose code it includes, Apache-2.0), qwen38_exl3_vision, flint_vllm and spark_vllm, which must
12
+ not be installed alongside.
13
+ """
14
+ __version__ = "0.1.0"
15
+
16
+ # Entry-point names of the plugins this package replaces.
17
+ _CONFLICTS = ("orcasaq2", "qwen38_exl3_vision", "tarninfer_exl3", "flint_vllm", "spark_vllm")
18
+
19
+
20
+ def _conflicts():
21
+ """Replaced plugins that vLLM will also load: installed, and not left out by VLLM_PLUGINS."""
22
+ import os
23
+ from importlib.metadata import entry_points
24
+
25
+ allowed = os.environ.get("VLLM_PLUGINS")
26
+ allowed = None if allowed is None else {p.strip() for p in allowed.split(",")}
27
+ return sorted({ep.name for ep in entry_points(group="vllm.general_plugins")
28
+ if ep.name in _CONFLICTS and (allowed is None or ep.name in allowed)})
29
+
30
+
31
+ def register():
32
+ from vllm.logger import init_logger
33
+ from vllm.model_executor.layers.quantization import register_quantization_config
34
+
35
+ logger = init_logger("vllm.quantpack")
36
+ found = _conflicts()
37
+ if found:
38
+ # They register the same quant_method and patch the same classes; which one wins would depend on plugin order.
39
+ raise RuntimeError(f"vllm-quantpack replaces the vLLM plugin(s) {', '.join(found)}: uninstall them from this "
40
+ "environment, or leave them out with VLLM_PLUGINS=quantpack")
41
+
42
+ from .exl3.layers import patch_embedding_layers
43
+ from .exl3.linear import Exl3Config
44
+
45
+ try:
46
+ register_quantization_config("exl3")(Exl3Config)
47
+ except ValueError:
48
+ return # already registered in this process
49
+ patch_embedding_layers(logger)
50
+
51
+ from .exl3.spec_decode import patch_should_share
52
+ from .families import flint, nemotron, qwen, spark
53
+ patch_should_share(logger)
54
+ qwen.register(logger)
55
+ if flint.register(logger):
56
+ logger.info("vllm-quantpack: Flint model registered")
57
+ if spark.register(logger):
58
+ logger.info("vllm-quantpack: Spark model registered")
59
+ if nemotron.register(logger):
60
+ logger.info("vllm-quantpack: Nemotron-3.5-Super-VL model registered")
61
+ logger.info("vllm-quantpack %s: quant_method 'exl3' is loadable (linear, int8 embedding, MoE, TP)", __version__)
File without changes
@@ -0,0 +1,85 @@
1
+ """The embedding table for vLLM's VocabParallelEmbedding.
2
+
3
+ exllamav3 refuses to quantize embed_tokens and keeps it in system RAM, so its native engine never pays VRAM for it. vLLM
4
+ does: VocabParallelEmbedding is a GPU tensor (2.54 GB in bf16 for a 248,320-token vocabulary). Our packs store it as
5
+ int8 rows with a per-row scale (scripts/embed_int8.py in each repo), dequantized on the GATHERED rows so the table itself
6
+ stays int8; the ablation measured that as free (KLD 0.0338 against 0.0341 for bf16).
7
+
8
+ Origin: OrcaSAQ2 heads.py (Apache-2.0) with the published patch (empty draft table), plus the TP cut and host release
9
+ qwen38_exl3_vision 0.4.1 added: each rank keeps its own vocabulary rows (VocabParallelEmbedding masks the ids and
10
+ all-reduces).
11
+ """
12
+ import torch
13
+ from vllm.model_executor.layers.quantization.base_config import QuantizeMethodBase
14
+
15
+ from .linear import Exl3ShardParam, release
16
+
17
+ EMBED_SUFFIXES = ("qweight", "scales", "weight")
18
+
19
+
20
+ class Exl3EmbeddingMethod(QuantizeMethodBase):
21
+ """int8 rows with a per-row scale, or a dense table, whichever the pack carries."""
22
+
23
+ def create_weights(self, layer, input_size_per_partition, output_partition_sizes,
24
+ input_size, output_size, params_dtype, **extra_weight_attrs):
25
+ # `extra_weight_attrs` is deliberately not applied: it carries vLLM's vocab-sharding weight_loader, which would
26
+ # assert against our empty placeholders.
27
+ for suffix in EMBED_SUFFIXES:
28
+ layer.register_parameter(suffix, Exl3ShardParam(suffix=suffix))
29
+ layer.exl3_params_dtype = params_dtype
30
+
31
+ def process_weights_after_loading(self, layer):
32
+ if getattr(layer, "tp_size", 1) > 1:
33
+ _cut_tp(layer)
34
+ q = layer.qweight.shards.get(0)
35
+ s = layer.scales.shards.get(0)
36
+ w = layer.weight.shards.get(0)
37
+ dev = next((t.device for t in (q, w) if t is not None and t.device.type == "cuda"),
38
+ torch.device("cuda", torch.cuda.current_device()))
39
+ release(layer, EMBED_SUFFIXES)
40
+ # vLLM's speculative proposer reads `<embed>.weight` (EAGLE/DFlash width check), so keep the name, as None.
41
+ layer.weight = None
42
+ layer.exl3_embed_q = None
43
+ layer.exl3_embed = None
44
+ if q is None:
45
+ # No table at all: a DFlash/EAGLE draft, whose module vLLM replaces with the target's after loading
46
+ # (_maybe_share_embeddings). embedding() complains if it is ever used.
47
+ if w is not None:
48
+ layer.exl3_embed = torch.nn.Parameter(w.to(dev), requires_grad=False)
49
+ return
50
+ if s is None:
51
+ raise RuntimeError("embed_tokens: qweight without scales")
52
+ layer.exl3_embed_q = torch.nn.Parameter(q.to(dev), requires_grad=False)
53
+ layer.exl3_embed_s = torch.nn.Parameter(s.to(dev, layer.exl3_params_dtype).reshape(-1, 1), requires_grad=False)
54
+
55
+ def embedding(self, layer, input_: torch.Tensor) -> torch.Tensor:
56
+ if layer.exl3_embed_q is None and layer.exl3_embed is None:
57
+ raise RuntimeError("embed_tokens: empty table (a draft whose embeddings were not shared with the target)")
58
+ if layer.exl3_embed_q is None:
59
+ return layer.exl3_embed[input_]
60
+ # Index directly rather than through F.embedding: the table is int8, and the scale is applied to the gathered
61
+ # rows so the table itself is never materialised in bf16.
62
+ rows = layer.exl3_embed_q[input_].to(layer.exl3_embed_s.dtype)
63
+ return rows * layer.exl3_embed_s[input_]
64
+
65
+ def apply(self, layer, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
66
+ if layer.exl3_embed_q is None:
67
+ w = layer.exl3_embed
68
+ else:
69
+ w = layer.exl3_embed_q.to(x.dtype) * layer.exl3_embed_s
70
+ out = torch.nn.functional.linear(x, w.to(x.dtype))
71
+ return out + bias if bias is not None else out
72
+
73
+
74
+ def _cut_tp(layer):
75
+ si = layer.shard_indices
76
+ a, b = si.org_vocab_start_index, si.org_vocab_end_index
77
+ n = layer.num_embeddings_per_partition
78
+ for name in EMBED_SUFFIXES:
79
+ d = getattr(layer, name).shards
80
+ if 0 not in d:
81
+ continue
82
+ full = d[0]
83
+ local = full.new_zeros((n,) + tuple(full.shape[1:]))
84
+ local[: b - a] = full[a:b]
85
+ d[0] = local
@@ -0,0 +1,11 @@
1
+ """Post-load hooks for model families that need to adjust EXL3 layers after their weights are in place.
2
+
3
+ Each hook is called once per layer at the end of process_weights_after_loading:
4
+
5
+ LINEAR: hook(prefix, layer) layer.exl3_shards / exl3_dense are final; may set layer.exl3_out_mul
6
+ MOE: hook(prefix, layer, ex) ex is the layer's _Experts; may set ex.out_scale
7
+
8
+ A hook must return without touching the layer when the prefix or model is not one it handles.
9
+ """
10
+ LINEAR: list = []
11
+ MOE: list = []
@@ -0,0 +1,89 @@
1
+ """Give VocabParallelEmbedding and ParallelLMHead the quant config the model withholds.
2
+
3
+ vLLM consults quant_config only for layers that are constructed with one, and several models (qwen3_next.py among them)
4
+ build `embed_tokens` and `lm_head` without passing it. A pack whose lm_head is an EXL3 tensor, or whose embedding is int8
5
+ rows, then cannot load: the loader looks for `lm_head.weight` / `embed_tokens.weight` and fails on the names it finds.
6
+ The two constructors are wrapped to supply the current config when the caller omitted one:
7
+
8
+ * EXL3: always (layers of other quant methods are untouched);
9
+ * compressed-tensors (GPTQ packs with an int4/int8 embedding, `re:.*embed_tokens$`): only for the embedding a family
10
+ module announced with `embedding_prefix(...)`, because compressed-tensors picks the scheme by layer name and the
11
+ constructor alone does not know it (see families/qwen.py).
12
+
13
+ Origin: OrcaSAQ2 __init__.py (Apache-2.0); the compressed-tensors case replaces qwen3_5-embed-quant.patch from
14
+ syv-ai/qwen38-27b-rtx3090 (Apache-2.0).
15
+ """
16
+ import contextlib
17
+ import contextvars
18
+ import inspect
19
+
20
+ _pending_embed = contextvars.ContextVar("quantpack_pending_embed", default=None)
21
+
22
+
23
+ @contextlib.contextmanager
24
+ def embedding_prefix(prefix: str):
25
+ """While a model builds its layers: the next embedding built without a quant config is `prefix`."""
26
+ token = _pending_embed.set(prefix)
27
+ try:
28
+ yield
29
+ finally:
30
+ _pending_embed.reset(token)
31
+
32
+
33
+ def take_embedding_prefix():
34
+ """The announced embedding prefix, once; None if there is none or it was already used."""
35
+ prefix = _pending_embed.get()
36
+ if prefix is not None:
37
+ _pending_embed.set(None)
38
+ return prefix
39
+
40
+
41
+ def _current_quant_config():
42
+ from vllm.config import get_current_vllm_config
43
+ try:
44
+ cfg = get_current_vllm_config()
45
+ except Exception:
46
+ return None
47
+ return getattr(cfg, "quant_config", None) or getattr(getattr(cfg, "model_config", None), "quant_config", None)
48
+
49
+
50
+ def _current_exl3_config():
51
+ from .linear import Exl3Config
52
+ qc = _current_quant_config()
53
+ return qc if isinstance(qc, Exl3Config) else None
54
+
55
+
56
+ def patch_embedding_layers(logger):
57
+ from vllm.model_executor.layers import vocab_parallel_embedding as vpe
58
+
59
+ if getattr(vpe.VocabParallelEmbedding, "_quantpack_patched", False):
60
+ return
61
+ for cls in (vpe.VocabParallelEmbedding, vpe.ParallelLMHead):
62
+ def make(orig_init, is_head):
63
+ sig = inspect.signature(orig_init)
64
+
65
+ def __init__(self, *args, **kwargs):
66
+ # ParallelLMHead forwards quant_config to its parent POSITIONALLY, and the parent is wrapped too, so
67
+ # injecting a keyword blindly collides. Bind against the real signature and only fill an empty slot.
68
+ try:
69
+ bound = sig.bind_partial(self, *args, **kwargs)
70
+ except TypeError:
71
+ return orig_init(self, *args, **kwargs)
72
+ if bound.arguments.get("quant_config") is None:
73
+ qc = _current_exl3_config()
74
+ if qc is not None:
75
+ bound.arguments["quant_config"] = qc
76
+ return orig_init(*bound.args, **bound.kwargs)
77
+ if not is_head and "prefix" in sig.parameters:
78
+ prefix = take_embedding_prefix()
79
+ qc = _current_quant_config() if prefix is not None else None
80
+ if type(qc).__name__ == "CompressedTensorsConfig":
81
+ bound.arguments["quant_config"] = qc
82
+ bound.arguments["prefix"] = prefix
83
+ return orig_init(*bound.args, **bound.kwargs)
84
+ return orig_init(self, *args, **kwargs)
85
+ return __init__
86
+
87
+ cls.__init__ = make(cls.__init__, cls is vpe.ParallelLMHead)
88
+ vpe.VocabParallelEmbedding._quantpack_patched = True
89
+ logger.info("vllm-quantpack: embed_tokens / lm_head receive the EXL3 (or compressed-tensors embedding) quant config")
@@ -0,0 +1,328 @@
1
+ """Quantization config and linear method for EXL3 checkpoints.
2
+
3
+ An EXL3 checkpoint stores each projection self-contained: a packed tail-biting trellis plus its own incoherence vectors,
4
+ and the bitrate is implied by the trellis width. Two consequences shape this module:
5
+
6
+ * Shards of a fused vLLM layer (q/k/v inside qkv_proj, gate/up inside gate_up_proj) are separate EXL3 tensors with
7
+ different shapes and, under a mixed-precision allocation, different K. Each shard is kept as its own buffer and
8
+ `apply` runs one GEMM per shard and concatenates.
9
+ * Nothing about the shapes is known until the weights arrive, so create_weights registers placeholders and a loader
10
+ that captures each shard as it is loaded.
11
+
12
+ Origin: OrcaSAQ2 config.py (Apache-2.0) with the published patch (prefix normalisation, empty draft lm_head), merged
13
+ with what qwen38_exl3_vision 0.4.1 used to add on top by monkeypatching:
14
+ * padding: EXL3 pads some unfused layers to a multiple of 128; inputs are zero-padded to the stored width and the extra
15
+ output columns are trimmed;
16
+ * dense layers stored in fp16 are converted once to the model dtype (OrcaSAQ2 converted them on every call: several ms
17
+ per token with a 1 GB lm_head);
18
+ * host memory: the captured tensors are views of the safetensors mmap; the shard dicts are emptied once each layer is
19
+ on the GPU, otherwise the whole checkpoint stayed resident in host RAM;
20
+ * tensor parallelism: the placeholders collect the WHOLE checkpoint tensor whatever the rank, and each rank keeps its
21
+ slice before the kernels are built (tp_ranges.py). Slices off the 128-column block keep the whole tensor and select
22
+ this rank's columns (or zero-pad this rank's input for row-parallel layers).
23
+ """
24
+ from typing import Any
25
+
26
+ import torch
27
+ import torch.nn.functional as F
28
+ from vllm.logger import init_logger
29
+ from vllm.model_executor.layers.linear import LinearBase, LinearMethodBase
30
+ from vllm.model_executor.layers.quantization.base_config import QuantizationConfig, QuantizeMethodBase
31
+ from vllm.model_executor.parameter import BasevLLMParameter
32
+
33
+ from . import hooks
34
+ from . import ops # noqa: F401 (registers quantpack::exl3_gemm)
35
+ from .names import expand, manifest_prefixes, norm_prefix, slot, trellis_k
36
+ from .tp_ranges import col_ranges, logical_parts, on_block, row_range
37
+
38
+ logger = init_logger("vllm.quantpack.exl3")
39
+
40
+ LINEAR_SUFFIXES = ("trellis", "suh", "svh", "mcg", "mul1", "weight")
41
+
42
+ EXLLAMAV3_MISSING = (
43
+ "vllm-quantpack needs the exllamav3 CUDA extension to serve EXL3 checkpoints, and importing it failed: {err}\n"
44
+ "It is not on PyPI: the wheel is ABI-linked to both torch and the CUDA major version, so pick the release asset "
45
+ "matching this environment:\n"
46
+ " python -c \"import torch; print(torch.__version__, torch.version.cuda)\"\n"
47
+ " https://github.com/turboderp-org/exllamav3/releases (cu128.* for CUDA 12.x, cu132.* for CUDA 13.x)")
48
+
49
+
50
+ class Exl3ShardParam(BasevLLMParameter):
51
+ """One placeholder parameter standing in for a list of per-shard EXL3 tensors.
52
+
53
+ vLLM wants a Parameter of known shape per weight name; EXL3 shard shapes (and K) are only known once the file is
54
+ read, and differ between shards of a fused layer. So the Parameter is an empty dummy and the real tensors land in
55
+ `shards`, keyed by slot (0 for an unfused layer). `parts` remembers the loader's shard id of each slot, which the
56
+ TP cut needs to know which logical partitions a tensor carries.
57
+
58
+ It carries its OWN weight_loader: vLLM's generic path is `param.weight_loader(param, loaded_weight, shard_id)`, and
59
+ the layer's legacy loader would assert the placeholder's shape against the incoming tensor.
60
+ """
61
+
62
+ def __new__(cls, **kwargs):
63
+ return super().__new__(cls, data=torch.empty(0))
64
+
65
+ def __init__(self, suffix: str):
66
+ super().__init__(data=torch.empty(0), weight_loader=_capture)
67
+ # A dict rather than a fixed list: the number of EXL3 tensors behind a fused layer is not its output-partition
68
+ # count (vLLM builds in_proj_qkvz with four partitions while the pack has two tensors).
69
+ self.shards: dict[int, torch.Tensor] = {}
70
+ self.parts: dict[int, Any] = {}
71
+ self.suffix = suffix
72
+
73
+ def slot(self, shard_id):
74
+ return slot(shard_id)
75
+
76
+ # The BasevLLMParameter hooks, for whichever layers take vLLM's newer path.
77
+ def load_column_parallel_weight(self, loaded_weight, **kw):
78
+ _capture(self, loaded_weight, kw.get("shard_id", kw.get("loaded_shard_id")))
79
+
80
+ load_row_parallel_weight = load_column_parallel_weight
81
+ load_merged_column_weight = load_column_parallel_weight
82
+ load_qkv_weight = load_column_parallel_weight
83
+
84
+
85
+ def _capture(param, loaded_weight, shard_id=None, **kw):
86
+ """Stash a shard; shapes are whatever the checkpoint says."""
87
+ s = slot(shard_id)
88
+ param.shards[s] = loaded_weight
89
+ param.parts[s] = shard_id
90
+
91
+
92
+ def release(layer, names):
93
+ """Empty and drop the placeholders: their tensors are views of the safetensors mmap."""
94
+ for p in names:
95
+ if hasattr(layer, p):
96
+ sh = getattr(getattr(layer, p), "shards", None)
97
+ if sh is not None:
98
+ sh.clear()
99
+ delattr(layer, p)
100
+
101
+
102
+ class Exl3Config(QuantizationConfig):
103
+ """`quant_method: "exl3"`: exllamav3 trellis quantization, any per-tensor bitrate."""
104
+
105
+ def __init__(self, bits: float, codebook: str, head_bits: float, full_config: dict):
106
+ super().__init__()
107
+ self.bits = bits
108
+ self.codebook = codebook
109
+ self.head_bits = head_bits
110
+ self.full_config = full_config
111
+ # which module prefixes actually carry a trellis, from the pack's own manifest (normalised)
112
+ self.quantized = manifest_prefixes(full_config.get("tensor_storage") or {})
113
+
114
+ def __repr__(self):
115
+ return (f"Exl3Config(bits={self.bits}, codebook={self.codebook!r}, "
116
+ f"head_bits={self.head_bits}, quantized={len(self.quantized)})")
117
+
118
+ @classmethod
119
+ def get_name(cls):
120
+ return "exl3"
121
+
122
+ @classmethod
123
+ def get_supported_act_dtypes(cls):
124
+ return [torch.half, torch.bfloat16]
125
+
126
+ @classmethod
127
+ def get_min_capability(cls):
128
+ return 80
129
+
130
+ @staticmethod
131
+ def get_config_filenames():
132
+ return ["quantization_config.json"]
133
+
134
+ @classmethod
135
+ def from_config(cls, config: dict[str, Any]):
136
+ # Fail here, not in the middle of a forward pass: every GEMM reaches for the extension lazily.
137
+ try:
138
+ from exllamav3.ext import exllamav3_ext # noqa: F401
139
+ except Exception as e:
140
+ raise RuntimeError(EXLLAMAV3_MISSING.format(err=f"{type(e).__name__}: {e}")) from e
141
+ return cls(bits=float(config.get("bits", 0.0)),
142
+ codebook=config.get("codebook", "mul1"),
143
+ head_bits=float(config.get("head_bits", 16)),
144
+ full_config=config)
145
+
146
+ def get_quant_method(self, layer: torch.nn.Module, prefix: str) -> QuantizeMethodBase | None:
147
+ from vllm.model_executor.layers.fused_moe import RoutedExperts
148
+ from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead, VocabParallelEmbedding
149
+
150
+ if isinstance(layer, RoutedExperts):
151
+ from .moe import Exl3MoEMethod
152
+ return Exl3MoEMethod(layer.moe_config, prefix)
153
+ if isinstance(layer, ParallelLMHead):
154
+ # A recipe with head_bits below 16 makes lm_head an ordinary EXL3 tensor; ParallelLMHead is exempt from
155
+ # embedding(), so the linear method is correct. It falls back to a dense weight when the pack has one.
156
+ return Exl3LinearMethod(self, prefix)
157
+ if isinstance(layer, VocabParallelEmbedding):
158
+ from .embedding import Exl3EmbeddingMethod
159
+ return Exl3EmbeddingMethod()
160
+ if not isinstance(layer, LinearBase):
161
+ return None
162
+ # A fused layer is quantized iff its constituents are; the manifest is authoritative when present, otherwise
163
+ # every LinearBase is assumed quantized.
164
+ if self.quantized and not any(norm_prefix(c) in self.quantized for c in expand(prefix)):
165
+ from vllm.model_executor.layers.quantization.unquant import UnquantizedLinearMethod
166
+ return UnquantizedLinearMethod()
167
+ return Exl3LinearMethod(self, prefix)
168
+
169
+ def get_scaled_act_names(self):
170
+ return []
171
+
172
+
173
+ class Exl3LinearMethod(LinearMethodBase):
174
+ def __init__(self, quant_config: Exl3Config, prefix: str = ""):
175
+ self.quant_config = quant_config
176
+ self.prefix = prefix
177
+
178
+ def create_weights(self, layer, input_size_per_partition, output_partition_sizes,
179
+ input_size, output_size, params_dtype, **extra_weight_attrs):
180
+ # An EXL3 pack is not uniformly quantized: exllamav3 leaves some projections in bf16 (linear_attn.in_proj_a/b,
181
+ # anything given 16 bits) and they arrive as a plain `weight`. Register placeholders for both, decide later.
182
+ for suffix in LINEAR_SUFFIXES:
183
+ layer.register_parameter(suffix, Exl3ShardParam(suffix=suffix))
184
+ layer.exl3_out_total = sum(output_partition_sizes)
185
+ layer.exl3_tp = None
186
+ layer.exl3_row_pad = None
187
+ layer.exl3_col_sel = None
188
+ layer.exl3_out_trim = None
189
+ layer.exl3_out_mul = None # set by a family hook (see hooks.py)
190
+ if input_size != input_size_per_partition or sum(output_partition_sizes) != output_size:
191
+ layer.exl3_tp = dict(rank=getattr(layer, "tp_rank", 0), tp=getattr(layer, "tp_size", 1),
192
+ row=input_size != input_size_per_partition,
193
+ in_local=input_size_per_partition, out_local=list(output_partition_sizes),
194
+ vocab=getattr(layer, "shard_indices", None))
195
+
196
+ def process_weights_after_loading(self, layer) -> None:
197
+ if layer.exl3_tp is not None:
198
+ sel = self._cut_tp(layer, layer.exl3_tp)
199
+ else:
200
+ sel = None
201
+ tr, suh, svh = layer.trellis.shards, layer.suh.shards, layer.svh.shards
202
+ mcg, mul1 = layer.mcg.shards, layer.mul1.shards
203
+ dense = layer.weight.shards
204
+ # padding of a single unfused layer: the pack stores up to 127 extra output columns
205
+ got = sum(v.numel() for v in svh.values()) if tr else layer.exl3_out_total
206
+ if got != layer.exl3_out_total and len(tr) == 1 and 0 < got - layer.exl3_out_total < 128:
207
+ layer.exl3_out_trim, layer.exl3_out_total = layer.exl3_out_total, got
208
+ slots = sorted(tr) or sorted(dense) # shard order == vLLM's concat order
209
+ # The loader may hand these over on CPU; pin everything to this worker's device.
210
+ dev = next((t.device for t in list(tr.values()) + list(dense.values()) if t.device.type == "cuda"),
211
+ torch.device("cuda", torch.cuda.current_device()))
212
+ layer.exl3_shards = None
213
+ layer.exl3_dense = None
214
+ if not tr:
215
+ if not dense:
216
+ if self.prefix.endswith("lm_head"):
217
+ # A DFlash/EAGLE draft ships no lm_head; vLLM replaces this module with the target's after loading
218
+ # (_maybe_share_lm_head). Leave it empty instead of failing.
219
+ release(layer, LINEAR_SUFFIXES)
220
+ return
221
+ raise RuntimeError(f"{self.prefix}: no trellis and no dense weight loaded")
222
+ w = torch.cat([dense[i] for i in slots], 0) if len(slots) > 1 else dense[slots[0]]
223
+ from vllm.config import get_current_vllm_config
224
+ dt = get_current_vllm_config().model_config.dtype
225
+ layer.exl3_dense = torch.nn.Parameter(w.contiguous().to(dev, dt), requires_grad=False)
226
+ else:
227
+ if dense:
228
+ raise RuntimeError(f"{self.prefix}: {len(tr)} trellis and {len(dense)} dense shards; "
229
+ "a fused layer must be all one kind or all the other")
230
+ layer.exl3_shards = [
231
+ dict(trellis=tr[i].contiguous().to(dev), suh=suh[i].to(dev, torch.half).contiguous(),
232
+ svh=svh[i].to(dev, torch.half).contiguous(),
233
+ mcg=mcg.get(i) is not None, mul1=mul1.get(i) is not None,
234
+ K=trellis_k(tr[i].shape[-1]), ic=suh[i].numel(), oc=svh[i].numel())
235
+ for i in slots
236
+ ]
237
+ got = sum(s["oc"] for s in layer.exl3_shards)
238
+ if got != layer.exl3_out_total:
239
+ raise RuntimeError(f"{self.prefix}: shards give {got} outputs, layer wants {layer.exl3_out_total}")
240
+ release(layer, LINEAR_SUFFIXES)
241
+ if sel is not None:
242
+ layer.exl3_col_sel = torch.tensor(sel, dtype=torch.long, device=dev)
243
+ for hook in hooks.LINEAR:
244
+ hook(self.prefix, layer)
245
+
246
+ def _cut_tp(self, layer, t):
247
+ """Keep this rank's slice of the captured tensors. Returns output columns to select, or None."""
248
+ rank, tp = t["rank"], t["tp"]
249
+ tr, suh, svh = layer.trellis.shards, layer.suh.shards, layer.svh.shards
250
+ dense = layer.weight.shards
251
+ if t["row"]:
252
+ a, b = row_range(rank, t["in_local"])
253
+ if tr:
254
+ if not on_block([(a, b)]):
255
+ # e.g. Flash-Next shared_expert.down_proj (640 -> 320 per GPU): the Hadamard block 256:384 straddles
256
+ # the ranks. Keep the whole matrix and zero-pad this rank's input to full width; the all-reduce of
257
+ # W·x_r sums to W·x exactly.
258
+ layer.exl3_row_pad = (a, t["in_local"] * tp - b)
259
+ return None
260
+ for s in list(tr):
261
+ tr[s] = tr[s][a // 16:b // 16].contiguous()
262
+ suh[s] = suh[s][a:b].contiguous()
263
+ for s in list(dense):
264
+ dense[s] = dense[s][:, a:b].contiguous()
265
+ return None
266
+ parts = (layer.trellis if tr else layer.weight).parts
267
+ vocab = t["vocab"]
268
+ sel, off, cut = [], 0, False
269
+ for s in list(tr or dense):
270
+ total = svh[s].numel() if tr else dense[s].shape[0]
271
+ if vocab is not None: # lm_head: this rank's vocabulary rows
272
+ a, b = vocab.org_vocab_start_index, vocab.org_vocab_end_index
273
+ if b - a != t["out_local"][0]:
274
+ raise NotImplementedError(f"{self.prefix}: padded vocabulary partition "
275
+ f"({b - a} rows for {t['out_local'][0]})")
276
+ ranges = [(a, b)]
277
+ else:
278
+ ranges = col_ranges(total, logical_parts(parts.get(s), len(t["out_local"])), t["out_local"], rank, tp)
279
+ if tr and not on_block(ranges):
280
+ # Not on a 128-column block (exllamav3's TP granularity; the reconstruct kernel rejects an output width
281
+ # that is not a multiple of 128, e.g. Flash-Next shared_expert gate_up: 320 per GPU): keep the whole
282
+ # tensor, compute every output and keep this rank's columns.
283
+ sel.extend(i for a, b in ranges for i in range(off + a, off + b))
284
+ off += total
285
+ continue
286
+ off += total
287
+ cut = True
288
+ if tr:
289
+ tr[s] = torch.cat([tr[s][:, a // 16:b // 16] for a, b in ranges], 1).contiguous()
290
+ svh[s] = torch.cat([svh[s][a:b] for a, b in ranges], 0).contiguous()
291
+ else:
292
+ dense[s] = torch.cat([dense[s][a:b] for a, b in ranges], 0).contiguous()
293
+ if not sel:
294
+ return None
295
+ if cut:
296
+ raise NotImplementedError(f"{self.prefix}: fused layer with some shards off the 128-column block")
297
+ layer.exl3_out_total = sum(v.numel() for v in svh.values()) # the kernels produce every output
298
+ return sel
299
+
300
+ def apply(self, layer, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
301
+ if layer.exl3_row_pad is not None:
302
+ x = F.pad(x, layer.exl3_row_pad)
303
+ if layer.exl3_col_sel is not None:
304
+ out = self._forward(layer, x).index_select(-1, layer.exl3_col_sel)
305
+ elif layer.exl3_out_trim is not None:
306
+ out = self._forward(layer, x)[..., :layer.exl3_out_trim]
307
+ else:
308
+ out = self._forward(layer, x)
309
+ if layer.exl3_out_mul is not None:
310
+ out = out * layer.exl3_out_mul
311
+ return out + bias if bias is not None else out
312
+
313
+ @staticmethod
314
+ def _forward(layer, x):
315
+ if layer.exl3_shards is None:
316
+ return F.linear(x, layer.exl3_dense.to(x.dtype)) # a no-op: converted to the model dtype at load
317
+ sh = layer.exl3_shards
318
+ if x.shape[-1] < sh[0]["ic"]:
319
+ x = F.pad(x, (0, sh[0]["ic"] - x.shape[-1]))
320
+ rows = x.numel() // x.shape[-1]
321
+ xf = x.reshape(rows, x.shape[-1]).half()
322
+ if not xf.is_contiguous():
323
+ xf = xf.contiguous() # the kernels read A as contiguous rows
324
+ # One custom op per shard: the extension calls are invisible to Dynamo (see ops.py).
325
+ outs = [torch.ops.quantpack.exl3_gemm(xf, s["trellis"], s["suh"], s["svh"], s["oc"], float(s["K"]),
326
+ s["mcg"], s["mul1"]) for s in sh]
327
+ out = (outs[0] if len(outs) == 1 else torch.cat(outs, dim=-1)).to(x.dtype)
328
+ return out.reshape(*x.shape[:-1], out.shape[-1])