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,421 @@
|
|
|
1
|
+
"""Runtime seam contract: check it, fall back, and leave a receipt.
|
|
2
|
+
|
|
3
|
+
A bound structure is calibrated for one execution form — a device, an
|
|
4
|
+
input dtype, a width, and for the implementations that preallocate, a row
|
|
5
|
+
count. Hand it something else and the honest outcomes are two: refuse, or
|
|
6
|
+
run the host's own module instead. The outcome this module exists to
|
|
7
|
+
prevent is the third one, where the kernel accepts a buffer it should not
|
|
8
|
+
have and the host keeps going with a wrong answer.
|
|
9
|
+
|
|
10
|
+
Falling back is allowed. Falling back *quietly* is not, because a seam
|
|
11
|
+
that always falls back reads exactly like a seam that works: the plan
|
|
12
|
+
still lists it, the receipt still counts it, and the latency it was
|
|
13
|
+
supposed to buy is simply absent. So every fallback is counted, the first
|
|
14
|
+
one per seam says so out loud, and a seam that never stops falling back
|
|
15
|
+
takes itself out of the model rather than staying on as a lie.
|
|
16
|
+
|
|
17
|
+
Three layers, one per timescale:
|
|
18
|
+
|
|
19
|
+
per call the contract is checked; a violation runs the host module
|
|
20
|
+
and increments the seam's ledger
|
|
21
|
+
first time that seam warns once, naming the path and the reason
|
|
22
|
+
persistent after ``SELF_DETACH_AFTER`` consecutive fallbacks the seam
|
|
23
|
+
restores the host module permanently and says so
|
|
24
|
+
|
|
25
|
+
``attach`` collects every guard it swaps in, so ``handle.report()`` is the
|
|
26
|
+
one place to ask what actually ran. Tests assert on it: a probe that does
|
|
27
|
+
not check its ledger has not checked that the thing it measured was on.
|
|
28
|
+
|
|
29
|
+
The check runs in Python, so it costs a call in an eager host and nothing
|
|
30
|
+
in a captured one — a graph replays the kernel it traced and never
|
|
31
|
+
re-enters this code. That is the right way round: the eager host is the
|
|
32
|
+
one whose shapes and devices can still change under it.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import threading
|
|
38
|
+
import warnings
|
|
39
|
+
from typing import Any, Callable, Iterable
|
|
40
|
+
|
|
41
|
+
import torch
|
|
42
|
+
|
|
43
|
+
GUARD_ATTR = "_frt_guard"
|
|
44
|
+
SELF_DETACH_AFTER = 32
|
|
45
|
+
|
|
46
|
+
#: returned by :meth:`GuardedSeam._frt_admit` when the call may proceed
|
|
47
|
+
PROCEED = object()
|
|
48
|
+
|
|
49
|
+
_FP8 = torch.float8_e4m3fn
|
|
50
|
+
#: what a bf16-entry implementation will cast for itself. FP8 is
|
|
51
|
+
#: deliberately absent: an fp8 tensor handed to a bf16 entry is a
|
|
52
|
+
#: negotiation that did not happen, not an input to convert.
|
|
53
|
+
CAST_OK = frozenset({torch.bfloat16, torch.float16, torch.float32})
|
|
54
|
+
#: what an fp8-entry implementation requires, exactly
|
|
55
|
+
FP8_ONLY = frozenset({_FP8})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GuardRefused(RuntimeError):
|
|
59
|
+
"""A seam was called outside the form it was bound for."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _concrete(device: torch.device | str) -> torch.device:
|
|
63
|
+
"""Resolve a device to the indexed form a tensor will report.
|
|
64
|
+
|
|
65
|
+
``torch.device("cuda")`` carries no index while every tensor on it
|
|
66
|
+
reports ``cuda:0``, so an index-less contract would refuse every call
|
|
67
|
+
it was meant to admit — a guard that fails closed on correct input is
|
|
68
|
+
worse than no guard, because it reverts a working seam and blames the
|
|
69
|
+
input. Normalising once at bind time keeps the per-call test an exact
|
|
70
|
+
comparison.
|
|
71
|
+
"""
|
|
72
|
+
device = torch.device(device)
|
|
73
|
+
if device.type == "cuda" and device.index is None:
|
|
74
|
+
return torch.device("cuda", torch.cuda.current_device())
|
|
75
|
+
return device
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SeamGuard:
|
|
79
|
+
"""One seam's runtime contract, its ledger, and its own exit.
|
|
80
|
+
|
|
81
|
+
Created at bind time by the implementation, which knows the form it
|
|
82
|
+
was calibrated for. Given a site by :func:`~flashrt_structures.swap.attach`,
|
|
83
|
+
which knows where in the model it ended up.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
__slots__ = ("dtypes", "device", "k", "rows", "row_capacity", "kind",
|
|
87
|
+
"can_fallback", "calls", "fallbacks", "consecutive",
|
|
88
|
+
"last_reason", "detached", "site", "mode", "notes",
|
|
89
|
+
"thread", "_restore", "_warned", "pair")
|
|
90
|
+
|
|
91
|
+
def __init__(self, *, kind: str, dtypes: Iterable[torch.dtype] | None,
|
|
92
|
+
device: torch.device, k: int | None, rows: int | None,
|
|
93
|
+
row_capacity: int | None, can_fallback: bool) -> None:
|
|
94
|
+
self.kind = kind
|
|
95
|
+
#: structure-specific counters an implementation keeps about
|
|
96
|
+
#: itself, reported alongside the contract ones
|
|
97
|
+
self.notes: dict[str, Any] = {}
|
|
98
|
+
self.dtypes = None if dtypes is None else frozenset(dtypes)
|
|
99
|
+
self.device = _concrete(device)
|
|
100
|
+
self.k = k
|
|
101
|
+
self.rows = rows
|
|
102
|
+
self.row_capacity = row_capacity
|
|
103
|
+
self.can_fallback = can_fallback
|
|
104
|
+
self.calls = 0
|
|
105
|
+
self.fallbacks = 0
|
|
106
|
+
self.consecutive = 0
|
|
107
|
+
self.last_reason: str | None = None
|
|
108
|
+
self.detached = False
|
|
109
|
+
self.site: str | None = None
|
|
110
|
+
self.mode = "fallback"
|
|
111
|
+
#: the thread that first ran this seam. These implementations keep
|
|
112
|
+
#: preallocated stash, quantize and attention scratch buffers whose
|
|
113
|
+
#: whole economy is that one call consumes a result before the next
|
|
114
|
+
#: one writes it; a second thread in the same seam interleaves the
|
|
115
|
+
#: writes and the corruption is silent and shape-correct. Single
|
|
116
|
+
#: stream per attachment is the contract, and this is where it is
|
|
117
|
+
#: enforced rather than assumed.
|
|
118
|
+
self.thread: int | None = None
|
|
119
|
+
self._restore: Callable[[], None] | None = None
|
|
120
|
+
self._warned: set[str] = set()
|
|
121
|
+
#: the other half of a negotiated pair (a producer whose only
|
|
122
|
+
#: consumer this seam is, or vice versa). A pair is
|
|
123
|
+
#: all-or-nothing: one out-of-contract call falsifies the
|
|
124
|
+
#: premise for both seats, and a producer left seated alone
|
|
125
|
+
#: keeps feeding a host that cannot read its format.
|
|
126
|
+
self.pair: "SeamGuard | None" = None
|
|
127
|
+
|
|
128
|
+
# ---- site binding (attach time) --------------------------------
|
|
129
|
+
|
|
130
|
+
def bind_site(self, site: str, *, restore: Callable[[], None] | None,
|
|
131
|
+
mode: str) -> None:
|
|
132
|
+
self.site = site
|
|
133
|
+
self._restore = restore
|
|
134
|
+
self.mode = mode
|
|
135
|
+
|
|
136
|
+
def release_site(self) -> None:
|
|
137
|
+
self.site = None
|
|
138
|
+
self._restore = None
|
|
139
|
+
|
|
140
|
+
# ---- per call --------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def admit(self, x: torch.Tensor) -> str | None:
|
|
143
|
+
"""Return ``None`` to proceed, else why this call cannot.
|
|
144
|
+
|
|
145
|
+
The fast path is the whole point: one dtype test, one width test,
|
|
146
|
+
one device test, and a row test only where the implementation
|
|
147
|
+
preallocated for a fixed row count.
|
|
148
|
+
"""
|
|
149
|
+
self.calls += 1
|
|
150
|
+
if self.dtypes is not None and x.dtype not in self.dtypes:
|
|
151
|
+
return f"input dtype {x.dtype} (bound for {self._dtype_note()})"
|
|
152
|
+
if self.k is not None and x.shape[-1] != self.k:
|
|
153
|
+
return f"input width {x.shape[-1]} (bound for {self.k})"
|
|
154
|
+
if x.device != self.device:
|
|
155
|
+
return f"input on {x.device} (bound on {self.device})"
|
|
156
|
+
if self.rows is not None:
|
|
157
|
+
rows = x.numel() // x.shape[-1]
|
|
158
|
+
if rows != self.rows:
|
|
159
|
+
return f"{rows} row(s) (bound for {self.rows})"
|
|
160
|
+
if self.row_capacity is not None:
|
|
161
|
+
rows = x.numel() // x.shape[-1]
|
|
162
|
+
if rows > self.row_capacity:
|
|
163
|
+
return (f"{rows} row(s) (buffer capacity "
|
|
164
|
+
f"{self.row_capacity})")
|
|
165
|
+
tid = threading.get_ident()
|
|
166
|
+
if self.thread is None:
|
|
167
|
+
self.thread = tid
|
|
168
|
+
elif tid != self.thread:
|
|
169
|
+
return ("called from a second thread; this seam's stash and "
|
|
170
|
+
"scratch buffers are shared across its calls, so "
|
|
171
|
+
"concurrent use would interleave writes into them")
|
|
172
|
+
self.consecutive = 0
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _dtype_note(self) -> str:
|
|
176
|
+
if self.dtypes == FP8_ONLY:
|
|
177
|
+
return "fp8 from a producer seam"
|
|
178
|
+
return "/".join(sorted(str(d).replace("torch.", "")
|
|
179
|
+
for d in self.dtypes or ()))
|
|
180
|
+
|
|
181
|
+
def refuse(self, reason: str) -> None:
|
|
182
|
+
"""Record a fallback; speak up the first time and at the end."""
|
|
183
|
+
where = self.site or f"<unattached {self.kind}>"
|
|
184
|
+
if self.mode == "raise":
|
|
185
|
+
# before counting: in strict mode nothing fell back, the call
|
|
186
|
+
# was refused, and a ledger claiming otherwise would be one
|
|
187
|
+
# more thing that says something that did not happen
|
|
188
|
+
self.last_reason = reason
|
|
189
|
+
raise GuardRefused(f"{where}: {reason}")
|
|
190
|
+
self.fallbacks += 1
|
|
191
|
+
self.consecutive += 1
|
|
192
|
+
self.last_reason = reason
|
|
193
|
+
if reason not in self._warned:
|
|
194
|
+
self._warned.add(reason)
|
|
195
|
+
warnings.warn(
|
|
196
|
+
f"structures: {where} fell back to the host module — "
|
|
197
|
+
f"{reason}. Further fallbacks of this kind are counted in "
|
|
198
|
+
f"handle.report() and not repeated here.",
|
|
199
|
+
RuntimeWarning, stacklevel=4)
|
|
200
|
+
if self.pair is not None and not self.detached:
|
|
201
|
+
# a negotiated pair collapses on the first out-of-contract
|
|
202
|
+
# call: the runtime path has disproved the bind-time
|
|
203
|
+
# premise, and every call the pair survives past this one
|
|
204
|
+
# is a producer feeding a consumer that is no longer there
|
|
205
|
+
self._self_detach()
|
|
206
|
+
if not self.pair.detached:
|
|
207
|
+
self.pair._self_detach()
|
|
208
|
+
return
|
|
209
|
+
if (self.consecutive >= SELF_DETACH_AFTER and not self.detached):
|
|
210
|
+
self._self_detach()
|
|
211
|
+
|
|
212
|
+
def _self_detach(self) -> None:
|
|
213
|
+
"""Stop pretending: put the host module back for good."""
|
|
214
|
+
self.detached = True
|
|
215
|
+
where = self.site or f"<unattached {self.kind}>"
|
|
216
|
+
if self._restore is None:
|
|
217
|
+
warnings.warn(
|
|
218
|
+
f"structures: {where} has fallen back "
|
|
219
|
+
f"{self.consecutive} times in a row and cannot restore "
|
|
220
|
+
f"itself (it is held inside another structure, not swapped "
|
|
221
|
+
f"in at a path). It is costing a check per call and buying "
|
|
222
|
+
f"nothing — detach the attachment.",
|
|
223
|
+
RuntimeWarning, stacklevel=5)
|
|
224
|
+
return
|
|
225
|
+
self._restore()
|
|
226
|
+
warnings.warn(
|
|
227
|
+
f"structures: {where} fell back {self.consecutive} times in a "
|
|
228
|
+
f"row ({self.last_reason}) — the host module has been restored "
|
|
229
|
+
f"at that path permanently. The rest of the attachment is "
|
|
230
|
+
f"unaffected.",
|
|
231
|
+
RuntimeWarning, stacklevel=5)
|
|
232
|
+
|
|
233
|
+
# ---- receipt ---------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def entry(self) -> dict[str, Any]:
|
|
236
|
+
entry = {"kind": self.kind, "calls": self.calls,
|
|
237
|
+
"fallbacks": self.fallbacks, "detached": self.detached,
|
|
238
|
+
"last_reason": self.last_reason,
|
|
239
|
+
"form": {"dtype": self._dtype_note(), "k": self.k,
|
|
240
|
+
"rows": self.rows,
|
|
241
|
+
"row_capacity": self.row_capacity,
|
|
242
|
+
"device": str(self.device)}}
|
|
243
|
+
if self.notes:
|
|
244
|
+
entry["notes"] = dict(self.notes)
|
|
245
|
+
return entry
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class GuardedSeam:
|
|
249
|
+
"""Mixin: contract check, host fallback, and lifecycle refusals.
|
|
250
|
+
|
|
251
|
+
Mixed in *before* ``torch.nn.Module`` so its ``_apply`` and
|
|
252
|
+
``state_dict`` overrides win. An implementation arms itself at the end
|
|
253
|
+
of ``__init__`` with :meth:`_frt_arm` and opens its ``forward`` with
|
|
254
|
+
:meth:`_frt_admit`.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
#: attribute holding the retained host module, if any
|
|
258
|
+
_frt_host_attr: str | None = None
|
|
259
|
+
#: whether calling that module with this ``forward``'s own arguments
|
|
260
|
+
#: reproduces the host's behaviour. False where the replacement's
|
|
261
|
+
#: boundary is not the retained module's boundary (a composed block,
|
|
262
|
+
#: an attention core called by an adapter): those refuse instead.
|
|
263
|
+
_frt_can_fallback: bool = False
|
|
264
|
+
#: class default so attribute lookup never reaches a host-forwarding
|
|
265
|
+
#: ``__getattr__`` for an implementation that has not armed yet
|
|
266
|
+
_frt_guard: SeamGuard | None = None
|
|
267
|
+
|
|
268
|
+
def _frt_arm(self, *, dtypes: Iterable[torch.dtype] | None,
|
|
269
|
+
device: torch.device, k: int | None = None,
|
|
270
|
+
rows: int | None = None,
|
|
271
|
+
row_capacity: int | None = None) -> SeamGuard:
|
|
272
|
+
"""Declare the form this instance was calibrated for."""
|
|
273
|
+
if rows is not None and row_capacity is not None:
|
|
274
|
+
raise ValueError("rows and row_capacity are mutually exclusive")
|
|
275
|
+
guard = SeamGuard(
|
|
276
|
+
kind=type(self).__name__, dtypes=dtypes, device=device, k=k,
|
|
277
|
+
rows=rows, row_capacity=row_capacity,
|
|
278
|
+
can_fallback=self._frt_can_fallback)
|
|
279
|
+
object.__setattr__(self, GUARD_ATTR, guard)
|
|
280
|
+
return guard
|
|
281
|
+
|
|
282
|
+
def _frt_touch(self) -> None:
|
|
283
|
+
"""Count a call whose form another guard already checked.
|
|
284
|
+
|
|
285
|
+
Two cases need this. A replacement that ignores its inputs (a
|
|
286
|
+
frozen buffer on a slower cadence) has no contract to test but
|
|
287
|
+
still has to be countable. And a structure reached through a wider
|
|
288
|
+
entry than its own ``forward`` — a producer that a composed block
|
|
289
|
+
calls directly — is checked by the block's contract and would
|
|
290
|
+
otherwise report zero calls, which reads as "never ran" when it
|
|
291
|
+
ran every time.
|
|
292
|
+
"""
|
|
293
|
+
guard = self._frt_guard
|
|
294
|
+
if guard is not None and not torch.compiler.is_compiling():
|
|
295
|
+
guard.calls += 1
|
|
296
|
+
|
|
297
|
+
def _frt_host(self) -> torch.nn.Module | None:
|
|
298
|
+
attr = self._frt_host_attr
|
|
299
|
+
if attr is None:
|
|
300
|
+
return None
|
|
301
|
+
# straight out of _modules: going through getattr would hit the
|
|
302
|
+
# host-forwarding __getattr__ these implementations define
|
|
303
|
+
return self.__dict__.get("_modules", {}).get(attr)
|
|
304
|
+
|
|
305
|
+
def _frt_admit(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> Any:
|
|
306
|
+
"""``PROCEED``, or the host's own result for this call.
|
|
307
|
+
|
|
308
|
+
Raises :class:`GuardRefused` when there is no host module to fall
|
|
309
|
+
back to, or when the attachment asked for strict mode.
|
|
310
|
+
|
|
311
|
+
Steps aside entirely while a compiler is tracing, for two reasons
|
|
312
|
+
that point the same way. It has to: the ledger's counter is a
|
|
313
|
+
Python-visible side effect in a hot forward, and dynamo either
|
|
314
|
+
breaks the graph around it or re-derives the region's boundaries
|
|
315
|
+
around it — the surrounding compiled region is worth more than the
|
|
316
|
+
count. And it may: dynamo installs its own guards on exactly the
|
|
317
|
+
dtype, shape and device this contract tests, so a traced region
|
|
318
|
+
handed a different form recompiles rather than running the wrong
|
|
319
|
+
kernel. Inside a compiled region this check is redundant; in an
|
|
320
|
+
eager host it is the only thing there.
|
|
321
|
+
"""
|
|
322
|
+
guard = self._frt_guard
|
|
323
|
+
if guard is None or torch.compiler.is_compiling():
|
|
324
|
+
return PROCEED
|
|
325
|
+
reason = guard.admit(x)
|
|
326
|
+
if reason is None:
|
|
327
|
+
return PROCEED
|
|
328
|
+
host = self._frt_host()
|
|
329
|
+
if host is None or not guard.can_fallback:
|
|
330
|
+
raise GuardRefused(
|
|
331
|
+
f"{guard.site or guard.kind}: {reason}; this structure has "
|
|
332
|
+
"no equivalent host module to fall back to")
|
|
333
|
+
if any(p.is_meta or p.numel() == 0
|
|
334
|
+
for p in host.parameters()) and not \
|
|
335
|
+
getattr(host, "_frt_tickets", None):
|
|
336
|
+
raise GuardRefused(
|
|
337
|
+
f"{guard.site or guard.kind}: {reason}; the host module's "
|
|
338
|
+
"weights were consumed and cannot be restored")
|
|
339
|
+
if getattr(host, "_frt_tickets", None):
|
|
340
|
+
# the host's weights were consumed (their truth lives in the
|
|
341
|
+
# weight store); a live fallback restores them once — slower
|
|
342
|
+
# than a resident copy, never wrong — and the ledger says so
|
|
343
|
+
from .storage import restore_for_fallback
|
|
344
|
+
restore_for_fallback(host)
|
|
345
|
+
guard.notes["restored_for_fallback"] = (
|
|
346
|
+
guard.notes.get("restored_for_fallback", 0) + 1)
|
|
347
|
+
guard.refuse(reason)
|
|
348
|
+
return self._frt_host_call(host, x, *args, **kwargs)
|
|
349
|
+
|
|
350
|
+
def _frt_host_call(self, host: torch.nn.Module, x: torch.Tensor,
|
|
351
|
+
*args: Any, **kwargs: Any) -> Any:
|
|
352
|
+
"""How to reproduce this seam with the host module. Overridable."""
|
|
353
|
+
return host(x, *args, **kwargs)
|
|
354
|
+
|
|
355
|
+
# ---- lifecycle refusals ---------------------------------------
|
|
356
|
+
|
|
357
|
+
def _apply(self, *args: Any, **kwargs: Any):
|
|
358
|
+
"""Refuse device/dtype migration while swapped into a model.
|
|
359
|
+
|
|
360
|
+
The packed weights and static scales of these implementations are
|
|
361
|
+
derived tensors held outside the parameter system, so a migration
|
|
362
|
+
would move the host's copy and leave the kernel's behind. Before
|
|
363
|
+
attach and after detach this is an ordinary module and migrates
|
|
364
|
+
normally.
|
|
365
|
+
"""
|
|
366
|
+
guard = self._frt_guard
|
|
367
|
+
if guard is not None and guard.site is not None:
|
|
368
|
+
raise GuardRefused(
|
|
369
|
+
f"{guard.site}: cannot migrate device or dtype while "
|
|
370
|
+
"structures are attached — detach() first, migrate, then "
|
|
371
|
+
"attach again (the scales are calibrated per form and do "
|
|
372
|
+
"not survive a dtype change anyway)")
|
|
373
|
+
return super()._apply(*args, **kwargs)
|
|
374
|
+
|
|
375
|
+
def state_dict(self, *args: Any, destination: Any = None,
|
|
376
|
+
prefix: str = "", keep_vars: bool = False):
|
|
377
|
+
"""Emit the host module's own state at this path.
|
|
378
|
+
|
|
379
|
+
A checkpoint should not change shape because an optimisation is
|
|
380
|
+
attached: the packed and quantised tensors here are derived from
|
|
381
|
+
the host weights and belong to the bind, not to the model's
|
|
382
|
+
state. Delegating keeps the schema and the values identical to the
|
|
383
|
+
unattached model, so saving while attached is safe.
|
|
384
|
+
"""
|
|
385
|
+
host = self._frt_host()
|
|
386
|
+
if host is None or args:
|
|
387
|
+
return super().state_dict(*args, destination=destination,
|
|
388
|
+
prefix=prefix, keep_vars=keep_vars)
|
|
389
|
+
return host.state_dict(destination=destination, prefix=prefix,
|
|
390
|
+
keep_vars=keep_vars)
|
|
391
|
+
|
|
392
|
+
def _load_from_state_dict(self, state_dict, prefix, local_metadata,
|
|
393
|
+
strict, missing_keys, unexpected_keys,
|
|
394
|
+
error_msgs):
|
|
395
|
+
"""Refuse loading into a swapped model rather than half-doing it.
|
|
396
|
+
|
|
397
|
+
Loading would have to re-derive every packed weight and re-run
|
|
398
|
+
calibration; silently loading the host's copy and leaving the
|
|
399
|
+
kernel's stale is the failure this refuses. ``detach()`` restores
|
|
400
|
+
the real modules, so the honest order is detach, load, attach.
|
|
401
|
+
"""
|
|
402
|
+
guard = self._frt_guard
|
|
403
|
+
if guard is not None and guard.site is not None:
|
|
404
|
+
error_msgs.append(
|
|
405
|
+
f"{guard.site}: cannot load_state_dict while structures "
|
|
406
|
+
"are attached — detach(), load, then attach again so the "
|
|
407
|
+
"packed weights are rebuilt from the loaded ones")
|
|
408
|
+
return
|
|
409
|
+
return super()._load_from_state_dict(
|
|
410
|
+
state_dict, prefix, local_metadata, strict, missing_keys,
|
|
411
|
+
unexpected_keys, error_msgs)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def collect(module: torch.nn.Module) -> list[tuple[str, SeamGuard]]:
|
|
415
|
+
"""Every guard at or under ``module``, by relative module name."""
|
|
416
|
+
found = []
|
|
417
|
+
for name, child in module.named_modules():
|
|
418
|
+
guard = child.__dict__.get(GUARD_ATTR)
|
|
419
|
+
if isinstance(guard, SeamGuard):
|
|
420
|
+
found.append((name, guard))
|
|
421
|
+
return found
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Explicit front door: ``structures.get(name)`` — pull one structure,
|
|
2
|
+
bind it to one host module, plug it in yourself.
|
|
3
|
+
|
|
4
|
+
This is the single-site, fully visible counterpart of ``attach``. The
|
|
5
|
+
consumption mirrors ``kernels.get_kernel``:
|
|
6
|
+
|
|
7
|
+
ffn = structures.get("decoder_ffn")
|
|
8
|
+
new_mlp = ffn.bind(model.model.layers[3].mlp, calibration=[x1, x2])
|
|
9
|
+
model.model.layers[3].mlp = new_mlp # your swap, your call
|
|
10
|
+
|
|
11
|
+
``bind`` extracts the weights from the module you hand it, calibrates on
|
|
12
|
+
the samples you provide, builds the fused replacement, and self-checks
|
|
13
|
+
it against the original module on those same samples. A replacement
|
|
14
|
+
that misses the parity gate raises ``GateRefused`` instead of returning
|
|
15
|
+
— the explicit path fails loudly, it never hands back a bad part.
|
|
16
|
+
``attach`` is this same operation, batched over every discovered site
|
|
17
|
+
with the net-win A/B added.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Sequence
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
from .discover import _VISION_PROJ, activation_for
|
|
27
|
+
from .gates import parity_metrics
|
|
28
|
+
from flash_rt.catalog.registry import StructureSpec, load
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GateRefused(RuntimeError):
|
|
32
|
+
"""The bound replacement did not meet the parity gate."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StructureHandle:
|
|
36
|
+
"""One pulled structure, ready to bind to host modules."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, spec: StructureSpec):
|
|
39
|
+
self.spec = spec
|
|
40
|
+
self.name = spec.name
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"StructureHandle({self.name!r}, v{self.spec.version})"
|
|
44
|
+
|
|
45
|
+
def bind(
|
|
46
|
+
self,
|
|
47
|
+
module: torch.nn.Module,
|
|
48
|
+
calibration: torch.Tensor | Sequence[torch.Tensor],
|
|
49
|
+
*,
|
|
50
|
+
variant: dict[str, str] | None = None,
|
|
51
|
+
norm: torch.nn.Module | None = None,
|
|
52
|
+
residual: torch.Tensor | Sequence[torch.Tensor] | None = None,
|
|
53
|
+
gate_cos: float = 0.999,
|
|
54
|
+
percentile: float = 99.9,
|
|
55
|
+
) -> torch.nn.Module:
|
|
56
|
+
"""Build a gated drop-in replacement for ``module``.
|
|
57
|
+
|
|
58
|
+
``calibration``: one or more real inputs of ``module`` (the
|
|
59
|
+
normed hidden states it actually sees). ``residual``: the
|
|
60
|
+
matching pre-norm hidden states — the structure gate is declared
|
|
61
|
+
at the boundary *including* the residual add, so provide these
|
|
62
|
+
to gate at the declared boundary; without them the self-check
|
|
63
|
+
runs on the bare seam output, which is strictly harsher.
|
|
64
|
+
``norm``: optional sibling norm module, recorded for
|
|
65
|
+
full-structure use; the MLP seam itself leaves the norm in the
|
|
66
|
+
host. ``gate_cos=0`` skips the self-check (not recommended).
|
|
67
|
+
"""
|
|
68
|
+
samples = ([calibration] if torch.is_tensor(calibration)
|
|
69
|
+
else list(calibration))
|
|
70
|
+
if not samples:
|
|
71
|
+
raise ValueError("calibration must contain at least one sample")
|
|
72
|
+
residuals = (None if residual is None else
|
|
73
|
+
[residual] if torch.is_tensor(residual)
|
|
74
|
+
else list(residual))
|
|
75
|
+
if residuals is not None and len(residuals) != len(samples):
|
|
76
|
+
raise ValueError("residual must align 1:1 with calibration")
|
|
77
|
+
|
|
78
|
+
if self.name == "decoder_ffn":
|
|
79
|
+
from .impls.decoder_ffn import fp8_static as impl
|
|
80
|
+
gate = module.gate_proj
|
|
81
|
+
device = gate.weight.device
|
|
82
|
+
dims = {"D": gate.in_features, "F": gate.out_features}
|
|
83
|
+
var = {"activation": activation_for(module, "silu"),
|
|
84
|
+
"norm_weight_mode": "direct"}
|
|
85
|
+
weights = {
|
|
86
|
+
"w_norm": (norm.weight.detach() if norm is not None
|
|
87
|
+
and getattr(norm, "weight", None) is not None
|
|
88
|
+
else torch.ones(dims["D"], device=device)),
|
|
89
|
+
"w_gate": gate.weight.detach().t().contiguous(),
|
|
90
|
+
"w_up": module.up_proj.weight.detach().t().contiguous(),
|
|
91
|
+
"w_down": module.down_proj.weight.detach().t().contiguous(),
|
|
92
|
+
}
|
|
93
|
+
elif self.name == "vision_ffn":
|
|
94
|
+
from .impls.vision_ffn import fp8_static as impl
|
|
95
|
+
fc_attrs = next(
|
|
96
|
+
(pair for pair in _VISION_PROJ
|
|
97
|
+
if all(isinstance(getattr(module, a, None), torch.nn.Linear)
|
|
98
|
+
for a in pair)), None)
|
|
99
|
+
if fc_attrs is None:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"module has no fc1/fc2 (or linear_fc1/linear_fc2) pair")
|
|
102
|
+
fc1 = getattr(module, fc_attrs[0])
|
|
103
|
+
fc2 = getattr(module, fc_attrs[1])
|
|
104
|
+
device = fc1.weight.device
|
|
105
|
+
dims = {"D": fc1.in_features, "F": fc1.out_features}
|
|
106
|
+
var = {"activation": activation_for(module, "gelu")}
|
|
107
|
+
weights = {
|
|
108
|
+
"w_norm": (norm.weight.detach() if norm is not None
|
|
109
|
+
else torch.ones(dims["D"], device=device)),
|
|
110
|
+
"b_norm": (norm.bias.detach() if norm is not None
|
|
111
|
+
else torch.zeros(dims["D"], device=device)),
|
|
112
|
+
"w_fc1": fc1.weight.detach(), "b_fc1": fc1.bias.detach(),
|
|
113
|
+
"w_fc2": fc2.weight.detach(), "b_fc2": fc2.bias.detach(),
|
|
114
|
+
}
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"structure {self.name!r} has no module-seam bind; "
|
|
118
|
+
"stage pipelines go through the capture door")
|
|
119
|
+
var.update(variant or {})
|
|
120
|
+
|
|
121
|
+
# the explicit door owns real samples, so it measures the seam's
|
|
122
|
+
# points by running the host module under the same hooks the
|
|
123
|
+
# distribution layer uses — one path for what a point is and where
|
|
124
|
+
# it lives, whichever door asked
|
|
125
|
+
from .points import Collector, Point
|
|
126
|
+
|
|
127
|
+
points = [Point("x_after_norm", "", "input")]
|
|
128
|
+
second = ("down_proj" if self.name == "decoder_ffn"
|
|
129
|
+
else (fc_attrs[1] if self.name == "vision_ffn" else None))
|
|
130
|
+
second_name = ("act_after_mul" if self.name == "decoder_ffn"
|
|
131
|
+
else "hidden_after_act")
|
|
132
|
+
if second is not None:
|
|
133
|
+
points.append(Point(second_name, second, "input"))
|
|
134
|
+
collector = Collector(points=points)
|
|
135
|
+
handles = collector.hooks(
|
|
136
|
+
lambda path: module if not path else getattr(module, path))
|
|
137
|
+
try:
|
|
138
|
+
with torch.no_grad():
|
|
139
|
+
for sample in samples:
|
|
140
|
+
module(sample.to(device))
|
|
141
|
+
collector.end_sample()
|
|
142
|
+
finally:
|
|
143
|
+
for handle in handles:
|
|
144
|
+
handle.remove()
|
|
145
|
+
collector.reduce(percentile)
|
|
146
|
+
kwargs = {} if self.name == "vision_ffn" else {"variant": var}
|
|
147
|
+
bound = impl.bind_mlp_seam(
|
|
148
|
+
weights,
|
|
149
|
+
input_scale=collector.scale("", "x_after_norm"),
|
|
150
|
+
hidden_scale=collector.scale(second or "", second_name),
|
|
151
|
+
original=module, **kwargs)
|
|
152
|
+
|
|
153
|
+
worst = 1.0
|
|
154
|
+
with torch.no_grad():
|
|
155
|
+
for k, sample in enumerate(samples):
|
|
156
|
+
x = sample.to(device)
|
|
157
|
+
got, want = bound(x), module(x)
|
|
158
|
+
if residuals is not None:
|
|
159
|
+
res = residuals[k].to(device)
|
|
160
|
+
got, want = res + got, res + want
|
|
161
|
+
worst = min(worst, parity_metrics(got, want)["cosine"])
|
|
162
|
+
boundary = ("structure (incl. residual)" if residuals is not None
|
|
163
|
+
else "bare seam")
|
|
164
|
+
if gate_cos and worst < gate_cos:
|
|
165
|
+
raise GateRefused(
|
|
166
|
+
f"{self.name} replacement worst cos {worst:.6f} < "
|
|
167
|
+
f"{gate_cos} at {boundary} boundary on {len(samples)} "
|
|
168
|
+
"calibration sample(s) — not handing back a part that "
|
|
169
|
+
"fails its own gate"
|
|
170
|
+
+ ("" if residuals is not None else
|
|
171
|
+
"; pass residual= to gate at the declared boundary"))
|
|
172
|
+
bound.certification = {
|
|
173
|
+
"structure": self.name, "version": self.spec.version,
|
|
174
|
+
"dims": dims, "variant": var, "worst_cos": round(worst, 7),
|
|
175
|
+
"gate_boundary": boundary, "samples": len(samples),
|
|
176
|
+
"m_profile": sorted({
|
|
177
|
+
int(s.reshape(-1, s.shape[-1]).shape[0]) for s in samples}),
|
|
178
|
+
}
|
|
179
|
+
return bound
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get(name: str) -> StructureHandle:
|
|
183
|
+
"""Pull one structure from the catalog (get_kernel-style)."""
|
|
184
|
+
spec = load(name)
|
|
185
|
+
if spec.kind != "region":
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"{name!r} is a {spec.kind} structure — module-level bind "
|
|
188
|
+
"does not apply; use the capture/provider route")
|
|
189
|
+
return StructureHandle(spec)
|