flashrt-structures 0.2.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.
- flashrt_structures/__init__.py +174 -0
- flashrt_structures/adapters/__init__.py +55 -0
- flashrt_structures/adapters/diffusers_attention.py +237 -0
- flashrt_structures/adapters/diffusers_rotary_attention.py +239 -0
- flashrt_structures/adapters/factored_qk_norm_rope.py +252 -0
- flashrt_structures/adapters/factored_two_way_attention.py +99 -0
- flashrt_structures/adapters/gemma_attention.py +226 -0
- flashrt_structures/adapters/packed_qkv_rope.py +342 -0
- flashrt_structures/adapters/packed_stream_qk_norm_rope.py +376 -0
- flashrt_structures/adapters/qwen_per_head_qk_norm_rope.py +231 -0
- flashrt_structures/adapters/sglang_engine.py +207 -0
- flashrt_structures/adapters/transformers_attention_interface.py +73 -0
- flashrt_structures/adapters/transformers_gated_delta.py +152 -0
- flashrt_structures/adapters/transformers_gated_delta_fused.py +96 -0
- flashrt_structures/adapters/vllm_engine.py +424 -0
- flashrt_structures/adjudicate.py +85 -0
- flashrt_structures/aot.py +191 -0
- flashrt_structures/autobuild.py +2052 -0
- flashrt_structures/beta/__init__.py +43 -0
- flashrt_structures/beta/conform.py +94 -0
- flashrt_structures/beta/joins.py +113 -0
- flashrt_structures/beta/negotiate.py +84 -0
- flashrt_structures/beta/ports.py +140 -0
- flashrt_structures/decisions.py +80 -0
- flashrt_structures/discover.py +623 -0
- flashrt_structures/explain.py +70 -0
- flashrt_structures/frontdoor.py +572 -0
- flashrt_structures/gates.py +465 -0
- flashrt_structures/guard.py +421 -0
- flashrt_structures/handle.py +189 -0
- flashrt_structures/impls/__init__.py +219 -0
- flashrt_structures/impls/adaln_producer/__init__.py +8 -0
- flashrt_structures/impls/adaln_producer/broker.py +116 -0
- flashrt_structures/impls/adaln_producer/fused.py +388 -0
- flashrt_structures/impls/adarms_stack/__init__.py +8 -0
- flashrt_structures/impls/adarms_stack/fp8_chain.py +832 -0
- flashrt_structures/impls/adarms_stack/region.py +102 -0
- flashrt_structures/impls/attention_core/__init__.py +132 -0
- flashrt_structures/impls/attention_core/fa2_seqused.py +458 -0
- flashrt_structures/impls/attention_core/fa4_cute.py +154 -0
- flashrt_structures/impls/attention_core/fa4_fp8.py +178 -0
- flashrt_structures/impls/attention_core/masked_mha.py +158 -0
- flashrt_structures/impls/attention_core/two_way_fa2.py +220 -0
- flashrt_structures/impls/cadence_static/__init__.py +18 -0
- flashrt_structures/impls/cadence_static/buffers.py +122 -0
- flashrt_structures/impls/cadence_static/cross_attention.py +187 -0
- flashrt_structures/impls/chain_elements.py +89 -0
- flashrt_structures/impls/decode_loop/__init__.py +0 -0
- flashrt_structures/impls/decode_loop/fp8_kv.py +206 -0
- flashrt_structures/impls/decode_loop/mtp_speculative.py +245 -0
- flashrt_structures/impls/decode_loop/whole_step.py +852 -0
- flashrt_structures/impls/decoder_block/__init__.py +6 -0
- flashrt_structures/impls/decoder_block/attn_sublayer.py +110 -0
- flashrt_structures/impls/decoder_block/fused.py +167 -0
- flashrt_structures/impls/decoder_ffn/__init__.py +0 -0
- flashrt_structures/impls/decoder_ffn/fp8_static.py +310 -0
- flashrt_structures/impls/decoder_ffn/fp8_static.yaml +22 -0
- flashrt_structures/impls/decoder_ffn/w4a16_static.py +221 -0
- flashrt_structures/impls/decoder_ffn/w8a16_static.py +183 -0
- flashrt_structures/impls/dit_stack/__init__.py +11 -0
- flashrt_structures/impls/dit_stack/fp4_chain.py +417 -0
- flashrt_structures/impls/dit_stack/region.py +86 -0
- flashrt_structures/impls/fixed_iter/__init__.py +29 -0
- flashrt_structures/impls/fixed_iter/openpi.py +264 -0
- flashrt_structures/impls/fixed_iter/protocol.py +94 -0
- flashrt_structures/impls/gated_delta_core/__init__.py +3 -0
- flashrt_structures/impls/gated_delta_core/fused_layer.py +545 -0
- flashrt_structures/impls/gated_delta_core/hub_v3.py +152 -0
- flashrt_structures/impls/graph_lowering/__init__.py +27 -0
- flashrt_structures/impls/graph_lowering/pi052_denoise.py +179 -0
- flashrt_structures/impls/graph_lowering/protocol.py +76 -0
- flashrt_structures/impls/graph_lowering/qwen3_vl.py +364 -0
- flashrt_structures/impls/linear_proj/__init__.py +0 -0
- flashrt_structures/impls/linear_proj/fp8_static.py +270 -0
- flashrt_structures/impls/linear_proj/nvfp4_balance.py +131 -0
- flashrt_structures/impls/linear_proj/nvfp4_dynamic.py +182 -0
- flashrt_structures/impls/linear_proj/w8a16_static.py +230 -0
- flashrt_structures/impls/modnorm_qkv_chain/__init__.py +0 -0
- flashrt_structures/impls/modnorm_qkv_chain/fp8_ptok_table.py +291 -0
- flashrt_structures/impls/moe_experts/__init__.py +9 -0
- flashrt_structures/impls/moe_experts/nvfp4_dynamic.py +208 -0
- flashrt_structures/impls/moe_experts/nvfp4_w4a16.py +129 -0
- flashrt_structures/impls/norm_fused/__init__.py +3 -0
- flashrt_structures/impls/norm_fused/bf16.py +94 -0
- flashrt_structures/impls/norm_fused/fp8_producer.py +84 -0
- flashrt_structures/impls/patch_projection/__init__.py +3 -0
- flashrt_structures/impls/patch_projection/bf16_flat.py +139 -0
- flashrt_structures/impls/prefill_tower/__init__.py +10 -0
- flashrt_structures/impls/prefill_tower/fp8_chain.py +955 -0
- flashrt_structures/impls/prefill_tower/region.py +99 -0
- flashrt_structures/impls/qk_norm_rope/__init__.py +12 -0
- flashrt_structures/impls/qk_norm_rope/per_head_gqa.py +199 -0
- flashrt_structures/impls/qk_norm_rope/projection_bf16.py +165 -0
- flashrt_structures/impls/qkv_pack/__init__.py +5 -0
- flashrt_structures/impls/qkv_pack/bf16.py +110 -0
- flashrt_structures/impls/qkv_pack/fp8_static.py +435 -0
- flashrt_structures/impls/qkv_pack/nvfp4_balance.py +218 -0
- flashrt_structures/impls/qkv_rope/__init__.py +3 -0
- flashrt_structures/impls/qkv_rope/packed_bias_bf16.py +143 -0
- flashrt_structures/impls/step_table.py +113 -0
- flashrt_structures/impls/vision_ffn/__init__.py +0 -0
- flashrt_structures/impls/vision_ffn/fp8_static.py +261 -0
- flashrt_structures/impls/vision_ffn/nvfp4_balance.py +211 -0
- flashrt_structures/impls/vision_tower/__init__.py +7 -0
- flashrt_structures/impls/vision_tower/fp8_chain.py +533 -0
- flashrt_structures/impls/vision_tower/region.py +91 -0
- flashrt_structures/matrix.py +126 -0
- flashrt_structures/points.py +368 -0
- flashrt_structures/prequantized.py +131 -0
- flashrt_structures/quantize_on_adopt.py +94 -0
- flashrt_structures/recipe.py +438 -0
- flashrt_structures/regions.py +208 -0
- flashrt_structures/schemes.py +490 -0
- flashrt_structures/stages.py +298 -0
- flashrt_structures/storage.py +255 -0
- flashrt_structures/swap.py +398 -0
- flashrt_structures/workspace.py +110 -0
- flashrt_structures-0.2.0.dist-info/METADATA +140 -0
- flashrt_structures-0.2.0.dist-info/RECORD +122 -0
- flashrt_structures-0.2.0.dist-info/WHEEL +5 -0
- flashrt_structures-0.2.0.dist-info/licenses/LICENSE +202 -0
- flashrt_structures-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""vLLM engine family: explicit assembly onto a serving host's model.
|
|
2
|
+
|
|
3
|
+
A vLLM model is an ``nn.Module`` tree living inside the engine process,
|
|
4
|
+
but its seams do not match the static module patterns discovery reads:
|
|
5
|
+
projections are merged/parallel classes whose ``forward`` returns
|
|
6
|
+
``(out, bias)``, expert weights live stacked on a routed-experts child,
|
|
7
|
+
and the LM head is consulted through ``quant_method.apply`` rather than
|
|
8
|
+
module forward. This adapter recognises those seams by structure —
|
|
9
|
+
2-D ``weight`` plus a ``quant_method`` slot for projections, a
|
|
10
|
+
``w13_weight``/``w2_weight`` pair for an expert bank, a ``lm_head``
|
|
11
|
+
whose vocabulary row count may exceed a quantize entry's grid limit —
|
|
12
|
+
and never by class or model name.
|
|
13
|
+
|
|
14
|
+
Three engine facts shape the assembly, each carried here so callers do
|
|
15
|
+
not rediscover them:
|
|
16
|
+
|
|
17
|
+
- **Seats must be installed after weights load and before the engine's
|
|
18
|
+
first trace.** vLLM's compiled artifact resolves parameters by tree
|
|
19
|
+
path; a post-compile swap either raises ``KeyError`` or is silently
|
|
20
|
+
bypassed. ``install_load_hook`` patches the model runner's
|
|
21
|
+
``load_model`` for exactly this window.
|
|
22
|
+
- **A Python shape branch dies in the compiled form.** vLLM traces with
|
|
23
|
+
guard evaluation off, so band dispatch (decode rows to the packed
|
|
24
|
+
bank, prefill rows to the retained host) lives inside a custom op:
|
|
25
|
+
its body re-runs at capture (decode sizes bake the seam branch into
|
|
26
|
+
the graphs) and eagerly at prefill.
|
|
27
|
+
- **The head is intercepted at ``quant_method``**, and binds as row
|
|
28
|
+
slabs when the vocabulary exceeds the quantize entry's row support.
|
|
29
|
+
|
|
30
|
+
Everything installed through :func:`attach_engine` goes through
|
|
31
|
+
``swap.attach``; the returned handle detaches bit-exactly. The expert
|
|
32
|
+
bank and head interceptions are host mutations recorded as ``revert``
|
|
33
|
+
callables on the same handle, so one ``detach`` restores all of it.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import os
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
import torch
|
|
42
|
+
from torch import nn
|
|
43
|
+
|
|
44
|
+
from .. import swap as _swap
|
|
45
|
+
from ..impls.linear_proj import nvfp4_dynamic as _linear
|
|
46
|
+
from ..impls.moe_experts import nvfp4_w4a16 as _experts_w4a16
|
|
47
|
+
from ..impls.moe_experts import nvfp4_dynamic as _experts_w4a4
|
|
48
|
+
|
|
49
|
+
#: dense projection seams, by dataflow position suffix. These are
|
|
50
|
+
#: positions in the qwen3_5 family's dataflow, not module identities;
|
|
51
|
+
#: a host that lacks one simply contributes no seat.
|
|
52
|
+
DENSE_SEAT_SUFFIXES = (
|
|
53
|
+
"linear_attn.out_proj", "linear_attn.in_proj_qkvz",
|
|
54
|
+
"self_attn.qkv_proj", "self_attn.o_proj",
|
|
55
|
+
"shared_expert.gate_up_proj", "shared_expert.down_proj",
|
|
56
|
+
"mlp.gate_up_proj", "mlp.down_proj",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_SEATS_BY_IDX: dict[int, Any] = {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Registered at import: the registration itself must never sit on a
|
|
63
|
+
# traced path — a lazy first call lands inside dynamo and the schema
|
|
64
|
+
# inference graph-breaks the host's compiled forward.
|
|
65
|
+
@torch.library.custom_op("flash_rt_structures::vllm_moe_seat",
|
|
66
|
+
mutates_args=())
|
|
67
|
+
def _vllm_moe_seat_op(hidden: torch.Tensor, router_logits: torch.Tensor,
|
|
68
|
+
top_idx: torch.Tensor, top_w: torch.Tensor,
|
|
69
|
+
idx: int) -> torch.Tensor:
|
|
70
|
+
return _SEATS_BY_IDX[idx].run(hidden, router_logits, top_idx, top_w)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@_vllm_moe_seat_op.register_fake
|
|
74
|
+
def _(hidden, router_logits, top_idx, top_w, idx):
|
|
75
|
+
return torch.empty_like(hidden)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _ProjSeat(nn.Module):
|
|
79
|
+
"""Preserves the engine's ``(out, bias)`` projection contract."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, seam):
|
|
82
|
+
super().__init__()
|
|
83
|
+
self.seam = seam
|
|
84
|
+
|
|
85
|
+
def forward(self, x, *args, **kwargs):
|
|
86
|
+
return self.seam(x), None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _MoESeat(nn.Module):
|
|
90
|
+
"""Stands where the fused-MoE module stood: routing here, bank in
|
|
91
|
+
the seam, the host's own shared-expert module added back (it owned
|
|
92
|
+
it too, and its projections may themselves carry seats), and a
|
|
93
|
+
declared band — decode rows walk the packed bank, prefill rows go
|
|
94
|
+
to the retained host module."""
|
|
95
|
+
|
|
96
|
+
#: rows above which the retained host module serves the batch.
|
|
97
|
+
#: Measured, not assumed. A routed-MoE decode does not amortise the
|
|
98
|
+
#: way a dense one does — eight tokens pick their own top-8 experts,
|
|
99
|
+
#: so expert traffic grows with the batch instead of being shared,
|
|
100
|
+
#: and a packed bank keeps paying well past batch one. Measured on
|
|
101
|
+
#: Thor against vLLM 0.26 (35B-A3B, 128-token generations): 2.10x at
|
|
102
|
+
#: batch 1, 2.45x at 4, 2.51x at 8, 1.65x at 16. The earlier value
|
|
103
|
+
#: of 8 handed batch-16 traffic back to the host and threw that
|
|
104
|
+
#: 1.65x away — the arm measured 0.98x, because at that batch the
|
|
105
|
+
#: dense seats alone are worth nothing while the MoE seat is worth
|
|
106
|
+
#: everything. 16 is the largest batch measured to pay; sweep with
|
|
107
|
+
#: ``FRT_MOE_BAND_T`` before raising it further.
|
|
108
|
+
BAND_T = int(os.environ.get("FRT_MOE_BAND_T", "16"))
|
|
109
|
+
|
|
110
|
+
def __init__(self, seam, top_k, renormalize, shared, host):
|
|
111
|
+
super().__init__()
|
|
112
|
+
self.seam = seam
|
|
113
|
+
self.top_k = top_k
|
|
114
|
+
self.renormalize = renormalize
|
|
115
|
+
self.shared = shared
|
|
116
|
+
self.host = host
|
|
117
|
+
self.host_internal = bool(getattr(host, "is_internal_router", False))
|
|
118
|
+
self.is_internal_router = False # the host block branches on this
|
|
119
|
+
self._frt_host_serving = True # prefill band runs through host
|
|
120
|
+
self.idx = len(_SEATS_BY_IDX)
|
|
121
|
+
_SEATS_BY_IDX[self.idx] = self
|
|
122
|
+
|
|
123
|
+
def run(self, hidden_states, router_logits, top_idx, top_w):
|
|
124
|
+
if hidden_states.shape[0] > self.BAND_T and self.host is not None:
|
|
125
|
+
logits = (hidden_states if self.host_internal else router_logits)
|
|
126
|
+
return self.host(hidden_states=hidden_states,
|
|
127
|
+
router_logits=logits)
|
|
128
|
+
out = self.seam(hidden_states, top_idx, top_w)
|
|
129
|
+
if self.shared is not None:
|
|
130
|
+
out = out + self.shared(hidden_states)
|
|
131
|
+
return out.to(hidden_states.dtype)
|
|
132
|
+
|
|
133
|
+
def forward(self, hidden_states, router_logits):
|
|
134
|
+
# routing stays in the traced region so inductor fuses the
|
|
135
|
+
# softmax/topk/renormalize chain; the opaque op keeps only what
|
|
136
|
+
# tracing would freeze — the band branch and the bank walk
|
|
137
|
+
w = torch.softmax(router_logits.float(), dim=-1)
|
|
138
|
+
tw, ti = torch.topk(w, self.top_k, dim=-1)
|
|
139
|
+
if self.renormalize:
|
|
140
|
+
tw = tw / tw.sum(dim=-1, keepdim=True)
|
|
141
|
+
return _vllm_moe_seat_op(hidden_states, router_logits, ti, tw,
|
|
142
|
+
self.idx)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _SlabbedHeadMethod:
|
|
146
|
+
"""Stands in for the LM head's quant method: the engine computes
|
|
147
|
+
logits through ``quant_method.apply``, never module forward."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, seams, orig):
|
|
150
|
+
self.seams = seams
|
|
151
|
+
self.orig = orig
|
|
152
|
+
|
|
153
|
+
def apply(self, layer, x, bias=None):
|
|
154
|
+
xb = x.to(torch.bfloat16)
|
|
155
|
+
y = torch.cat([s(xb) for s in self.seams], dim=-1)
|
|
156
|
+
if bias is not None:
|
|
157
|
+
y = y + bias
|
|
158
|
+
return y.to(x.dtype)
|
|
159
|
+
|
|
160
|
+
def __getattr__(self, name):
|
|
161
|
+
return getattr(self.orig, name)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_projection(module) -> bool:
|
|
165
|
+
w = getattr(module, "weight", None)
|
|
166
|
+
return (isinstance(w, torch.Tensor) and w.dim() == 2
|
|
167
|
+
and hasattr(module, "quant_method"))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _expert_holder(module):
|
|
171
|
+
for _, child in module.named_modules():
|
|
172
|
+
if torch.is_tensor(getattr(child, "w13_weight", None)):
|
|
173
|
+
return child
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class _NoSeats:
|
|
178
|
+
"""The handle shape for a host where nothing could be seated.
|
|
179
|
+
|
|
180
|
+
Every seat refusing is a normal outcome, not an error: the hub may be
|
|
181
|
+
unreachable, this architecture may have no build, the memory budget
|
|
182
|
+
may leave no room. What must not happen is the engine dying because
|
|
183
|
+
its accelerator was absent — a server that fails to start is worse
|
|
184
|
+
than one that starts unaccelerated. So the refusals are reported and
|
|
185
|
+
the host is handed back untouched, with a handle of the same shape so
|
|
186
|
+
callers need no special case.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, refused, reverts):
|
|
190
|
+
self.notes = {"refused": refused, "head_slabs": 0, "seated": 0}
|
|
191
|
+
self._reverts = list(reverts)
|
|
192
|
+
|
|
193
|
+
def detach(self):
|
|
194
|
+
for fn in reversed(self._reverts):
|
|
195
|
+
fn()
|
|
196
|
+
self._reverts.clear()
|
|
197
|
+
|
|
198
|
+
def report(self):
|
|
199
|
+
return {}
|
|
200
|
+
|
|
201
|
+
def summary(self):
|
|
202
|
+
return {"seams": 0, "guarded_calls": 0, "fallbacks": 0,
|
|
203
|
+
"seams_fell_back": [], "seams_self_detached": [],
|
|
204
|
+
"seams_never_called": [], "clean": True,
|
|
205
|
+
"refused": len(self.notes["refused"])}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def attach_engine(model, *, seats=DENSE_SEAT_SUFFIXES, experts=True,
|
|
209
|
+
head=True, use_gemv=None, verbose=True, strict=False):
|
|
210
|
+
"""Seat a vLLM model: dense projections, expert banks, LM head.
|
|
211
|
+
|
|
212
|
+
Call between weight load and the engine's first trace (see
|
|
213
|
+
:func:`install_load_hook`). Returns the ``swap.attach`` handle;
|
|
214
|
+
``handle.detach()`` restores the module tree, the expert modules
|
|
215
|
+
and the head's quant method.
|
|
216
|
+
"""
|
|
217
|
+
if use_gemv is None:
|
|
218
|
+
cc = torch.cuda.get_device_capability()
|
|
219
|
+
use_gemv = cc >= (12, 0) # the warp-split GEMV entry's own arch
|
|
220
|
+
if not use_gemv:
|
|
221
|
+
orig_init = _linear.LinearProjNvfp4Dynamic.__init__
|
|
222
|
+
|
|
223
|
+
def _init(self, *a, **kw):
|
|
224
|
+
orig_init(self, *a, **kw)
|
|
225
|
+
self._gemv = None
|
|
226
|
+
_linear.LinearProjNvfp4Dynamic.__init__ = _init
|
|
227
|
+
|
|
228
|
+
swaps: dict[str, nn.Module] = {}
|
|
229
|
+
reverts: list = []
|
|
230
|
+
refused: list = []
|
|
231
|
+
modules = dict(model.named_modules())
|
|
232
|
+
|
|
233
|
+
# dense projections, smallest first: on tight cards early frees
|
|
234
|
+
# make room for the big binds
|
|
235
|
+
targets = [(n, m) for n, m in modules.items()
|
|
236
|
+
if any(n.endswith(s) for s in seats) and _is_projection(m)]
|
|
237
|
+
targets.sort(key=lambda t: t[1].weight.numel())
|
|
238
|
+
for name, mod in targets:
|
|
239
|
+
try:
|
|
240
|
+
seam, _ = _linear.bind_proj_seam({"w": mod.weight.data})
|
|
241
|
+
swaps[name] = _ProjSeat(seam)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
refused.append((name, repr(e)[:120]))
|
|
244
|
+
|
|
245
|
+
if experts:
|
|
246
|
+
impl = (_experts_w4a4 if use_gemv else _experts_w4a16)
|
|
247
|
+
for name, mod in modules.items():
|
|
248
|
+
if not name.endswith("mlp.experts"):
|
|
249
|
+
continue
|
|
250
|
+
holder = _expert_holder(mod)
|
|
251
|
+
if holder is None:
|
|
252
|
+
continue
|
|
253
|
+
try:
|
|
254
|
+
seam, _ = impl.bind_experts_seam(
|
|
255
|
+
{"gate_up_proj": holder.w13_weight.data,
|
|
256
|
+
"down_proj": holder.w2_weight.data},
|
|
257
|
+
act_fn=torch.nn.functional.silu)
|
|
258
|
+
top_k = (getattr(mod, "top_k", None)
|
|
259
|
+
or getattr(getattr(mod, "moe_config", None),
|
|
260
|
+
"experts_per_token", None) or 8)
|
|
261
|
+
renorm = getattr(mod, "renormalize", None)
|
|
262
|
+
parent = modules[name.rsplit(".experts", 1)[0]]
|
|
263
|
+
swaps[name] = _MoESeat(
|
|
264
|
+
seam, int(top_k),
|
|
265
|
+
True if renorm is None else bool(renorm),
|
|
266
|
+
getattr(parent, "shared_expert", None), mod)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
refused.append((name, repr(e)[:120]))
|
|
269
|
+
|
|
270
|
+
head_slabs = 0
|
|
271
|
+
if head:
|
|
272
|
+
lm = next((m for n, m in modules.items()
|
|
273
|
+
if n.endswith("lm_head")
|
|
274
|
+
and isinstance(getattr(m, "weight", None), torch.Tensor)),
|
|
275
|
+
None)
|
|
276
|
+
if lm is not None:
|
|
277
|
+
try:
|
|
278
|
+
rows = lm.weight.shape[0]
|
|
279
|
+
slab = -(-rows // 4) // 64 * 64
|
|
280
|
+
seams = [
|
|
281
|
+
_linear.bind_proj_seam(
|
|
282
|
+
{"w": lm.weight.data[lo:lo + slab]})[0]
|
|
283
|
+
for lo in range(0, rows, slab)]
|
|
284
|
+
orig_method = lm.quant_method
|
|
285
|
+
lm.quant_method = _SlabbedHeadMethod(seams, orig_method)
|
|
286
|
+
reverts.append(
|
|
287
|
+
lambda lm=lm, m=orig_method: setattr(
|
|
288
|
+
lm, "quant_method", m))
|
|
289
|
+
head_slabs = len(seams)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
refused.append(("lm_head", repr(e)[:120]))
|
|
292
|
+
|
|
293
|
+
model.eval()
|
|
294
|
+
if not swaps:
|
|
295
|
+
if strict:
|
|
296
|
+
raise RuntimeError(
|
|
297
|
+
"refused: no seat could be bound on this host (%d refusals; "
|
|
298
|
+
"first: %s). Pass strict=False to let the engine start "
|
|
299
|
+
"unaccelerated." % (len(refused),
|
|
300
|
+
refused[0][1] if refused else "none"))
|
|
301
|
+
if verbose:
|
|
302
|
+
print(f"[structures.vllm] 0 seats, {len(refused)} refused — "
|
|
303
|
+
f"host runs unmodified", flush=True)
|
|
304
|
+
for name, why in refused[:3]:
|
|
305
|
+
print(f"[structures.vllm] {name}: {why}", flush=True)
|
|
306
|
+
handle = _NoSeats(refused, reverts)
|
|
307
|
+
handle.notes["refused"] = refused
|
|
308
|
+
return handle
|
|
309
|
+
handle = _swap.attach(model, swaps, revert=reverts)
|
|
310
|
+
if verbose:
|
|
311
|
+
print(f"[structures.vllm] {len(swaps)} seats "
|
|
312
|
+
f"({head_slabs} head slabs), {len(refused)} refused",
|
|
313
|
+
flush=True)
|
|
314
|
+
handle.notes = {"refused": refused, "head_slabs": head_slabs}
|
|
315
|
+
return handle
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
#: every handle :func:`install_load_hook` has seated, in order. A caller
|
|
319
|
+
#: that wants certainty rather than a log line asserts on this after the
|
|
320
|
+
#: engine is up: empty means the hook never fired.
|
|
321
|
+
_ATTACHED: list = []
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def attached() -> list:
|
|
325
|
+
"""The handles seated so far. Empty after an engine came up means the
|
|
326
|
+
patch never reached the process that loaded the model — see
|
|
327
|
+
:func:`install_load_hook` on the start method."""
|
|
328
|
+
return list(_ATTACHED)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _patch_would_not_survive() -> str | None:
|
|
332
|
+
"""Why a patch made here would not exist in the worker process.
|
|
333
|
+
|
|
334
|
+
The engine starts its worker with ``fork`` by default, which
|
|
335
|
+
inherits this patch, and that is why the four-line integration
|
|
336
|
+
works at all. But it switches to ``spawn`` under conditions the
|
|
337
|
+
caller can walk into without noticing — a spawned worker re-imports
|
|
338
|
+
the engine from scratch and the patch is simply not there.
|
|
339
|
+
|
|
340
|
+
The failure is silent: patching succeeds here, the hook never fires,
|
|
341
|
+
and the run comes out at baseline speed with no error anywhere. That
|
|
342
|
+
is the one outcome this layer refuses to produce, so it is checked
|
|
343
|
+
before the caller builds an engine rather than discovered afterwards
|
|
344
|
+
from a missing log line.
|
|
345
|
+
"""
|
|
346
|
+
if os.environ.get("VLLM_ENABLE_V1_MULTIPROCESSING", "1") == "0":
|
|
347
|
+
return None # the engine runs in this process
|
|
348
|
+
if os.environ.get("VLLM_WORKER_MULTIPROC_METHOD") == "spawn":
|
|
349
|
+
return "VLLM_WORKER_MULTIPROC_METHOD is set to 'spawn'"
|
|
350
|
+
if torch.cuda.is_initialized():
|
|
351
|
+
return ("CUDA is already initialized in this process, and the "
|
|
352
|
+
"engine forces 'spawn' when it is")
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def install_load_hook(*, on_attached=None, allow_spawn=False,
|
|
357
|
+
**attach_kwargs):
|
|
358
|
+
"""Patch every importable vLLM model-runner so :func:`attach_engine`
|
|
359
|
+
runs after weights load and before the engine's first trace. Set
|
|
360
|
+
``VLLM_DISABLE_COMPILE_CACHE=1``: the engine's compile cache key
|
|
361
|
+
does not see the module tree, and a stale artifact resolves
|
|
362
|
+
parameters that the seats replaced.
|
|
363
|
+
|
|
364
|
+
Call this before touching CUDA. The engine forks its worker by
|
|
365
|
+
default, which is what carries this patch into the process that
|
|
366
|
+
loads the model, but it switches to spawning one the moment CUDA is
|
|
367
|
+
already initialized here — and a spawned worker re-imports the
|
|
368
|
+
engine without the patch. Nothing raises in that case: the seats
|
|
369
|
+
simply never go in and the run comes out at baseline. So the
|
|
370
|
+
condition is refused here instead, with the two ways out. Pass
|
|
371
|
+
``allow_spawn=True`` to proceed anyway.
|
|
372
|
+
"""
|
|
373
|
+
import importlib
|
|
374
|
+
import os
|
|
375
|
+
|
|
376
|
+
# Find the runners before changing anything: "there is no host here"
|
|
377
|
+
# is the more fundamental refusal, and a caller without vLLM should
|
|
378
|
+
# hear that rather than a lecture about start methods. Nothing is
|
|
379
|
+
# mutated until both questions have been answered.
|
|
380
|
+
found = []
|
|
381
|
+
for modname in ("vllm.v1.worker.gpu.model_runner",
|
|
382
|
+
"vllm.v1.worker.gpu_model_runner",
|
|
383
|
+
"vllm.v2.worker.gpu_model_runner"):
|
|
384
|
+
try:
|
|
385
|
+
module = importlib.import_module(modname)
|
|
386
|
+
except ImportError:
|
|
387
|
+
continue
|
|
388
|
+
runner = getattr(module, "GPUModelRunner", None)
|
|
389
|
+
if runner is None or not hasattr(runner, "load_model"):
|
|
390
|
+
continue
|
|
391
|
+
found.append((modname, runner))
|
|
392
|
+
if not found:
|
|
393
|
+
raise RuntimeError(
|
|
394
|
+
"refused: no vLLM model runner found to hook; the engine "
|
|
395
|
+
"layout is outside this adapter's profile")
|
|
396
|
+
|
|
397
|
+
lost = None if allow_spawn else _patch_would_not_survive()
|
|
398
|
+
if lost is not None:
|
|
399
|
+
raise RuntimeError(
|
|
400
|
+
"refused: this patch would not reach the process that loads "
|
|
401
|
+
"the model — " + lost + ".\n"
|
|
402
|
+
"The engine would start its worker with 'spawn', which "
|
|
403
|
+
"re-imports it from scratch, so the seats would never be "
|
|
404
|
+
"installed and the run would come out at baseline speed "
|
|
405
|
+
"with nothing raised anywhere.\n"
|
|
406
|
+
"Either call install_load_hook() before anything touches "
|
|
407
|
+
"CUDA, or run the engine in this process with "
|
|
408
|
+
"VLLM_ENABLE_V1_MULTIPROCESSING=0. Pass allow_spawn=True to "
|
|
409
|
+
"proceed regardless.")
|
|
410
|
+
|
|
411
|
+
os.environ.setdefault("VLLM_DISABLE_COMPILE_CACHE", "1")
|
|
412
|
+
patched = []
|
|
413
|
+
for modname, runner in found:
|
|
414
|
+
orig = runner.load_model
|
|
415
|
+
|
|
416
|
+
def load_model(self, *a, __orig=orig, **kw):
|
|
417
|
+
__orig(self, *a, **kw)
|
|
418
|
+
handle = attach_engine(self.model, **attach_kwargs)
|
|
419
|
+
_ATTACHED.append(handle)
|
|
420
|
+
if on_attached is not None:
|
|
421
|
+
on_attached(handle)
|
|
422
|
+
runner.load_model = load_model
|
|
423
|
+
patched.append(modname)
|
|
424
|
+
return patched
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Form adjudication: candidates declared, facts qualify, measurement seats.
|
|
2
|
+
|
|
3
|
+
No form is ever hard-coded to a shape or a device. A seat family
|
|
4
|
+
declares its candidates; qualification predicates read device-neutral
|
|
5
|
+
facts; the qualified candidates are timed on the calibrated shapes and
|
|
6
|
+
the measurement seats the winner. Precision precedes speed twice over:
|
|
7
|
+
a candidate enters only above the parity floor (0.99 — the house
|
|
8
|
+
tradeoff-and-calibrate line), and within the win margin the
|
|
9
|
+
higher-precision form takes the seat, so noise never flips one.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Callable, Sequence
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
#: the house parity floor: at or above this, a form is inside the
|
|
20
|
+
#: tradeoff-and-calibration band and speed may decide
|
|
21
|
+
PARITY_FLOOR = 0.99
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FormCandidate:
|
|
26
|
+
name: str
|
|
27
|
+
build: Callable[[], Any]
|
|
28
|
+
run: Callable[[Any], Any]
|
|
29
|
+
qualify: Callable[[], bool] = lambda: True
|
|
30
|
+
#: tie-break inside the win margin: lower = higher precision
|
|
31
|
+
precision_rank: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def adjudicate(seat: str, candidates: Sequence[FormCandidate], *,
|
|
35
|
+
margin: float = 0.03, iters: int = 30,
|
|
36
|
+
notes: dict | None = None):
|
|
37
|
+
"""Measure the qualified candidates; return (winner_name, built).
|
|
38
|
+
|
|
39
|
+
Every candidate's outcome — unqualified, refused (with reason), or
|
|
40
|
+
its measured milliseconds — lands in ``notes["adjudication"]``.
|
|
41
|
+
A candidate that cannot build or run loses by default. Returns
|
|
42
|
+
``(None, None)`` when nothing survives.
|
|
43
|
+
"""
|
|
44
|
+
entries, built = [], {}
|
|
45
|
+
for cand in candidates:
|
|
46
|
+
try:
|
|
47
|
+
if not cand.qualify():
|
|
48
|
+
entries.append({"name": cand.name,
|
|
49
|
+
"outcome": "unqualified"})
|
|
50
|
+
continue
|
|
51
|
+
built[cand.name] = cand.build()
|
|
52
|
+
except Exception as exc: # noqa: BLE001 — loses
|
|
53
|
+
entries.append({"name": cand.name,
|
|
54
|
+
"outcome": f"refused: {str(exc)[:80]}"})
|
|
55
|
+
timed = []
|
|
56
|
+
for cand in candidates:
|
|
57
|
+
if cand.name not in built:
|
|
58
|
+
continue
|
|
59
|
+
b = built[cand.name]
|
|
60
|
+
with torch.no_grad():
|
|
61
|
+
for _ in range(5):
|
|
62
|
+
cand.run(b)
|
|
63
|
+
torch.cuda.synchronize()
|
|
64
|
+
start = torch.cuda.Event(True)
|
|
65
|
+
end = torch.cuda.Event(True)
|
|
66
|
+
start.record()
|
|
67
|
+
for _ in range(iters):
|
|
68
|
+
cand.run(b)
|
|
69
|
+
end.record()
|
|
70
|
+
torch.cuda.synchronize()
|
|
71
|
+
ms = start.elapsed_time(end) / iters
|
|
72
|
+
timed.append((ms, cand.precision_rank, cand.name))
|
|
73
|
+
entries.append({"name": cand.name, "ms": round(ms, 4)})
|
|
74
|
+
winner = None
|
|
75
|
+
if timed:
|
|
76
|
+
timed.sort()
|
|
77
|
+
best = timed[0][0]
|
|
78
|
+
close = [t for t in timed if t[0] <= best * (1.0 + margin)]
|
|
79
|
+
close.sort(key=lambda t: (t[1], t[0]))
|
|
80
|
+
winner = close[0][2]
|
|
81
|
+
if notes is not None:
|
|
82
|
+
notes.setdefault("adjudication", []).append(
|
|
83
|
+
{"seat": seat, "winner": winner, "margin": margin,
|
|
84
|
+
"entries": entries})
|
|
85
|
+
return winner, (built.get(winner) if winner else None)
|