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,298 @@
|
|
|
1
|
+
"""Capture door: ``structures.capture(fn, ...)`` — graph a hot stage.
|
|
2
|
+
|
|
3
|
+
The schedule-structure counterpart of the region doors. ``fn`` is the
|
|
4
|
+
hot stage of a cond_iter pipeline (a denoise loop, a decode step chain);
|
|
5
|
+
``capture`` warms it, records it into a CUDA graph on a side stream, and
|
|
6
|
+
returns a runtime whose ``replay()`` re-executes the whole stage as one
|
|
7
|
+
launch. Declared ``windows`` are the tensors you may rewrite between
|
|
8
|
+
replays (noise, observations, condition buffers) — replay reads their
|
|
9
|
+
current contents in place, which is what makes the graph a reusable
|
|
10
|
+
stage rather than a frozen trace.
|
|
11
|
+
|
|
12
|
+
Gates are built in: ``reference`` (an eager thunk producing the same
|
|
13
|
+
output under the same window contents) certifies parity, and the replay
|
|
14
|
+
is timed against eager ``fn``; a capture that is not both accurate and
|
|
15
|
+
net-faster raises ``CaptureRefused`` — the host keeps its eager path,
|
|
16
|
+
never a silently wrong or slower graph.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Callable, Mapping
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
from .gates import parity_metrics
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CaptureRefused(RuntimeError):
|
|
30
|
+
"""The captured stage failed its parity or net-win gate."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _first_tensor(value: Any) -> torch.Tensor | None:
|
|
34
|
+
"""Pick one tensor deterministically for schedule-equivalence preflight."""
|
|
35
|
+
if torch.is_tensor(value):
|
|
36
|
+
return value
|
|
37
|
+
logits = getattr(value, "logits", None)
|
|
38
|
+
if torch.is_tensor(logits):
|
|
39
|
+
return logits
|
|
40
|
+
if isinstance(value, Mapping):
|
|
41
|
+
for key in sorted(value):
|
|
42
|
+
found = _first_tensor(value[key])
|
|
43
|
+
if found is not None:
|
|
44
|
+
return found
|
|
45
|
+
return None
|
|
46
|
+
if isinstance(value, (tuple, list)):
|
|
47
|
+
for item in value:
|
|
48
|
+
found = _first_tensor(item)
|
|
49
|
+
if found is not None:
|
|
50
|
+
return found
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _time_ms(fn: Callable[[], Any], warmup: int = 5, iters: int = 20) -> float:
|
|
55
|
+
for _ in range(warmup):
|
|
56
|
+
fn()
|
|
57
|
+
torch.cuda.synchronize()
|
|
58
|
+
start, end = torch.cuda.Event(True), torch.cuda.Event(True)
|
|
59
|
+
start.record()
|
|
60
|
+
for _ in range(iters):
|
|
61
|
+
fn()
|
|
62
|
+
end.record()
|
|
63
|
+
torch.cuda.synchronize()
|
|
64
|
+
return start.elapsed_time(end) / iters
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CapturedStage:
|
|
69
|
+
"""A replayable hot stage with declared swap windows."""
|
|
70
|
+
|
|
71
|
+
graph: torch.cuda.CUDAGraph
|
|
72
|
+
stream: torch.cuda.Stream
|
|
73
|
+
output: Any
|
|
74
|
+
windows: Mapping[str, torch.Tensor]
|
|
75
|
+
certification: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
#: family shape-lowerings applied to the host before capture. Replay
|
|
77
|
+
#: does not need them (a graph replays kernels, not Python), but the
|
|
78
|
+
#: host object stays pinned for any eager use until they are undone.
|
|
79
|
+
lowerings: tuple = ()
|
|
80
|
+
|
|
81
|
+
def restore_host(self) -> None:
|
|
82
|
+
"""Undo every family lowering; the host runs eager as loaded."""
|
|
83
|
+
for lowering in reversed(self.lowerings):
|
|
84
|
+
lowering.undo()
|
|
85
|
+
self.lowerings = ()
|
|
86
|
+
|
|
87
|
+
def replay(self, sync: bool = True) -> Any:
|
|
88
|
+
with torch.cuda.stream(self.stream):
|
|
89
|
+
self.graph.replay()
|
|
90
|
+
if sync:
|
|
91
|
+
torch.cuda.synchronize()
|
|
92
|
+
return self.output
|
|
93
|
+
|
|
94
|
+
def write(self, name: str, value: torch.Tensor) -> None:
|
|
95
|
+
"""Rewrite one declared window in place.
|
|
96
|
+
|
|
97
|
+
The window's form is the graph's contract: ``copy_`` would
|
|
98
|
+
silently broadcast a wrong shape or cast a wrong dtype, and the
|
|
99
|
+
replay would then run baked kernels over coerced data with no
|
|
100
|
+
guard anywhere to notice — the captured tier's ledger blindness
|
|
101
|
+
is by design, so the door has to check.
|
|
102
|
+
"""
|
|
103
|
+
window = self.windows[name]
|
|
104
|
+
if (value.shape != window.shape or value.dtype != window.dtype
|
|
105
|
+
or value.device != window.device):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"captured stage: window {name!r} expects "
|
|
108
|
+
f"{tuple(window.shape)}/{window.dtype}/{window.device}, "
|
|
109
|
+
f"got {tuple(value.shape)}/{value.dtype}/{value.device}"
|
|
110
|
+
" — a replay over a coerced write is silently wrong")
|
|
111
|
+
window.copy_(value)
|
|
112
|
+
|
|
113
|
+
def export(self, *, ports, regions=(), stages=None, roles=None,
|
|
114
|
+
identity=None, manifest_extra=None):
|
|
115
|
+
"""One call from this captured stage to ``frt_model_runtime_v1``.
|
|
116
|
+
|
|
117
|
+
The declared windows become the contract's boundary windows
|
|
118
|
+
(name -> device pointer + bytes); ``ports`` reference them by
|
|
119
|
+
name exactly as in
|
|
120
|
+
:func:`flash_rt.runtime.provider.export_captured_runtime`.
|
|
121
|
+
This is the absorption edge: a stage captured out of any torch
|
|
122
|
+
host becomes a runtime the FlashRT serving mechanisms (Nexus
|
|
123
|
+
tick, capsule snapshot/restore) consume like a whitebox one.
|
|
124
|
+
"""
|
|
125
|
+
from flash_rt.runtime.provider import export_captured_runtime
|
|
126
|
+
|
|
127
|
+
window_map = {
|
|
128
|
+
name: (t.data_ptr(), t.numel() * t.element_size())
|
|
129
|
+
for name, t in self.windows.items()}
|
|
130
|
+
return export_captured_runtime(
|
|
131
|
+
stream_handle=self.stream.cuda_stream,
|
|
132
|
+
graphs=[("infer", self.graph.raw_cuda_graph_exec())],
|
|
133
|
+
windows=window_map,
|
|
134
|
+
ports=ports, regions=regions,
|
|
135
|
+
stages=tuple(stages) if stages else ("infer",),
|
|
136
|
+
roles=roles, identity=identity,
|
|
137
|
+
manifest_extra=manifest_extra)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def capture(
|
|
141
|
+
fn: Callable[[], Any],
|
|
142
|
+
*,
|
|
143
|
+
model: torch.nn.Module | None = None,
|
|
144
|
+
windows: Mapping[str, torch.Tensor] | None = None,
|
|
145
|
+
reference: Callable[[], torch.Tensor] | None = None,
|
|
146
|
+
output_of: Callable[[Any], torch.Tensor] | None = None,
|
|
147
|
+
warmup: int = 3,
|
|
148
|
+
gate_cos: float = 0.999,
|
|
149
|
+
min_speedup: float = 1.02,
|
|
150
|
+
verbose: bool = True,
|
|
151
|
+
) -> CapturedStage:
|
|
152
|
+
"""Capture ``fn`` into a replayable, gated stage.
|
|
153
|
+
|
|
154
|
+
``fn`` must be graph-safe: fixed shapes, no host-side branching on
|
|
155
|
+
tensor values, all varying inputs read from the declared ``windows``
|
|
156
|
+
buffers. When ``model`` is supplied, a registered fixed-iteration
|
|
157
|
+
schedule adapter may normalize a recognized graph-unsafe host loop; the
|
|
158
|
+
original and normalized callables must pass an exact preflight before
|
|
159
|
+
capture. ``reference`` runs the host's own eager path under the same
|
|
160
|
+
window contents; parity is judged between its output and the
|
|
161
|
+
replayed stage output. Pass ``gate_cos=0`` / ``min_speedup=0`` to
|
|
162
|
+
skip a gate explicitly (recorded in the certification).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def say(msg: str) -> None:
|
|
166
|
+
if verbose:
|
|
167
|
+
print(f"[structures] {msg}", flush=True)
|
|
168
|
+
|
|
169
|
+
from .impls.fixed_iter import (
|
|
170
|
+
FixedIterationRefused,
|
|
171
|
+
normalize_fixed_iteration,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
schedule = None
|
|
175
|
+
try:
|
|
176
|
+
schedule = normalize_fixed_iteration(fn, model)
|
|
177
|
+
except FixedIterationRefused as exc:
|
|
178
|
+
raise CaptureRefused(str(exc)) from exc
|
|
179
|
+
if schedule is not None:
|
|
180
|
+
want_schedule = _first_tensor(schedule.reference_output)
|
|
181
|
+
with torch.no_grad():
|
|
182
|
+
got_schedule = _first_tensor(schedule.forward())
|
|
183
|
+
if want_schedule is None or got_schedule is None:
|
|
184
|
+
raise CaptureRefused(
|
|
185
|
+
f"{schedule.family}: schedule parity needs one tensor output")
|
|
186
|
+
metrics = parity_metrics(
|
|
187
|
+
got_schedule.detach().float().cpu(),
|
|
188
|
+
want_schedule.detach().float().cpu(),
|
|
189
|
+
)
|
|
190
|
+
exact = torch.equal(got_schedule, want_schedule)
|
|
191
|
+
if schedule.exact and not exact:
|
|
192
|
+
raise CaptureRefused(
|
|
193
|
+
f"{schedule.family}: fixed-iteration lowering is not exact "
|
|
194
|
+
f"(cos={metrics['cosine']:.7f}, "
|
|
195
|
+
f"max_abs={metrics['max_abs']:.7g})")
|
|
196
|
+
fn = schedule.forward
|
|
197
|
+
if schedule.compile_before_capture:
|
|
198
|
+
fn = torch.compile(fn)
|
|
199
|
+
say(f"schedule normalized: {schedule.family}, "
|
|
200
|
+
f"{schedule.steps} fixed step(s), exact={exact}")
|
|
201
|
+
|
|
202
|
+
lowerings: list = []
|
|
203
|
+
if model is not None:
|
|
204
|
+
from .impls.graph_lowering import lower_for_capture
|
|
205
|
+
lowerings = lower_for_capture(model, fn)
|
|
206
|
+
for lowering in lowerings:
|
|
207
|
+
say(f"graph lowering applied: {lowering.family} "
|
|
208
|
+
f"[{', '.join(lowering.pins)}]")
|
|
209
|
+
|
|
210
|
+
declared_windows = dict(schedule.windows) if schedule is not None else {}
|
|
211
|
+
for name, tensor in dict(windows or {}).items():
|
|
212
|
+
if name in declared_windows \
|
|
213
|
+
and declared_windows[name] is not tensor:
|
|
214
|
+
raise CaptureRefused(
|
|
215
|
+
f"window {name!r} conflicts with the tensor discovered by "
|
|
216
|
+
"the fixed-iteration schedule")
|
|
217
|
+
declared_windows[name] = tensor
|
|
218
|
+
windows = declared_windows
|
|
219
|
+
stream = torch.cuda.Stream()
|
|
220
|
+
try:
|
|
221
|
+
with torch.no_grad(), torch.cuda.stream(stream):
|
|
222
|
+
for _ in range(max(1, warmup)):
|
|
223
|
+
fn()
|
|
224
|
+
torch.cuda.synchronize()
|
|
225
|
+
graph = torch.cuda.CUDAGraph()
|
|
226
|
+
# same grad mode as the warmups: a mode switch here changes the
|
|
227
|
+
# dynamo guards and a compiled fn recompiles inside the capture,
|
|
228
|
+
# which runs CUDA work no capture may record
|
|
229
|
+
with torch.no_grad(), torch.cuda.graph(graph, stream=stream):
|
|
230
|
+
output = fn()
|
|
231
|
+
torch.cuda.synchronize()
|
|
232
|
+
except Exception:
|
|
233
|
+
for lowering in reversed(lowerings):
|
|
234
|
+
lowering.undo()
|
|
235
|
+
raise
|
|
236
|
+
say(f"stage captured ({len(windows)} window(s))")
|
|
237
|
+
|
|
238
|
+
stage = CapturedStage(graph=graph, stream=stream, output=output,
|
|
239
|
+
windows=windows, lowerings=tuple(lowerings))
|
|
240
|
+
pick = output_of or (lambda out: out)
|
|
241
|
+
|
|
242
|
+
cert: dict[str, Any] = {"windows": sorted(windows),
|
|
243
|
+
"gate_cos": gate_cos,
|
|
244
|
+
"min_speedup": min_speedup}
|
|
245
|
+
if lowerings:
|
|
246
|
+
cert["graph_lowering"] = [
|
|
247
|
+
{"family": low.family, "pins": list(low.pins),
|
|
248
|
+
**dict(low.details)} for low in lowerings]
|
|
249
|
+
if schedule is not None:
|
|
250
|
+
cert["schedule"] = {
|
|
251
|
+
"family": schedule.family,
|
|
252
|
+
"steps": schedule.steps,
|
|
253
|
+
"exact": exact,
|
|
254
|
+
"parity_cos": round(metrics["cosine"], 7),
|
|
255
|
+
"max_abs": metrics["max_abs"],
|
|
256
|
+
"compiled": schedule.compile_before_capture,
|
|
257
|
+
**dict(schedule.details),
|
|
258
|
+
}
|
|
259
|
+
if reference is not None and gate_cos:
|
|
260
|
+
with torch.no_grad():
|
|
261
|
+
want = reference().detach().float().cpu()
|
|
262
|
+
got = pick(stage.replay()).detach().float().cpu()
|
|
263
|
+
cos = parity_metrics(got, want)["cosine"]
|
|
264
|
+
cert["parity_cos"] = round(cos, 7)
|
|
265
|
+
if cos < gate_cos:
|
|
266
|
+
raise CaptureRefused(
|
|
267
|
+
f"captured stage parity cos {cos:.6f} < {gate_cos} vs "
|
|
268
|
+
"eager reference — check that every varying input is a "
|
|
269
|
+
"declared window (in-graph RNG never matches eager)")
|
|
270
|
+
say(f"parity vs eager: cos={cos:.6f}")
|
|
271
|
+
if min_speedup:
|
|
272
|
+
eager_ms = _time_ms(lambda: fn())
|
|
273
|
+
# replays run on the side stream: the timing events must be
|
|
274
|
+
# recorded on that same stream or they only measure enqueue time
|
|
275
|
+
for _ in range(5):
|
|
276
|
+
stage.replay(sync=False)
|
|
277
|
+
torch.cuda.synchronize()
|
|
278
|
+
start, end = torch.cuda.Event(True), torch.cuda.Event(True)
|
|
279
|
+
with torch.cuda.stream(stream):
|
|
280
|
+
start.record()
|
|
281
|
+
for _ in range(20):
|
|
282
|
+
stage.replay(sync=False)
|
|
283
|
+
with torch.cuda.stream(stream):
|
|
284
|
+
end.record()
|
|
285
|
+
torch.cuda.synchronize()
|
|
286
|
+
replay_ms = start.elapsed_time(end) / 20
|
|
287
|
+
cert["eager_ms"] = round(eager_ms, 3)
|
|
288
|
+
cert["replay_ms"] = round(replay_ms, 3)
|
|
289
|
+
cert["speedup"] = round(eager_ms / replay_ms, 4)
|
|
290
|
+
if eager_ms / replay_ms < min_speedup:
|
|
291
|
+
raise CaptureRefused(
|
|
292
|
+
f"captured stage is not a net win "
|
|
293
|
+
f"({eager_ms / replay_ms:.3f}x vs eager, margin "
|
|
294
|
+
f"{min_speedup}) — host keeps its eager path")
|
|
295
|
+
say(f"net win: {eager_ms:.2f} -> {replay_ms:.2f} ms "
|
|
296
|
+
f"({eager_ms / replay_ms:.3f}x)")
|
|
297
|
+
stage.certification = cert
|
|
298
|
+
return stage
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Weight residency: the fallback store lives off the GPU.
|
|
2
|
+
|
|
3
|
+
The attachment used to keep every replaced module's parameters resident
|
|
4
|
+
in device memory so that per-call fallback and ``detach`` were instant.
|
|
5
|
+
That is a double weight bill paid exactly when memory is scarcest — at
|
|
6
|
+
bind time on a large host — and the only thing it bought was speed of a
|
|
7
|
+
path that production (captured, fixed-shape) never takes.
|
|
8
|
+
|
|
9
|
+
This module removes the resident tier. When an attachment consumes its
|
|
10
|
+
originals, each parameter's truth moves to one of two stores and the
|
|
11
|
+
device storage is freed:
|
|
12
|
+
|
|
13
|
+
``DISK``
|
|
14
|
+
The parameter verifiably came from a checkpoint file: its
|
|
15
|
+
safetensors shard and key are recorded, and a sampled-block
|
|
16
|
+
comparison against the live tensor proved the mapping before
|
|
17
|
+
anything was released. Restore reloads from the file and re-applies
|
|
18
|
+
the load transform (a dtype cast). Zero extra copies held.
|
|
19
|
+
|
|
20
|
+
``HOST_RAM``
|
|
21
|
+
No verifiable provenance (the host mutated the weight after
|
|
22
|
+
loading, or the mapping could not be proven). The tensor is spilled
|
|
23
|
+
to pinned CPU memory; restore is one host-to-device copy. Costs
|
|
24
|
+
host RAM, never device memory.
|
|
25
|
+
|
|
26
|
+
Correctness never depends on the mapping heuristics: a provenance miss
|
|
27
|
+
costs RAM, not accuracy. Tied parameters (shared storage) are stashed
|
|
28
|
+
once and restored to every holder.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import glob
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
import torch
|
|
40
|
+
|
|
41
|
+
_SAMPLE_BYTES = 65536
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _sample_equal(a: torch.Tensor, b: torch.Tensor) -> bool:
|
|
45
|
+
"""Compare shape, dtype and the first/last sample blocks."""
|
|
46
|
+
if a.shape != b.shape or a.dtype != b.dtype:
|
|
47
|
+
return False
|
|
48
|
+
fa = a.reshape(-1)
|
|
49
|
+
fb = b.reshape(-1)
|
|
50
|
+
n = min(fa.numel(), _SAMPLE_BYTES // max(1, fa.element_size()))
|
|
51
|
+
if n == 0:
|
|
52
|
+
return True
|
|
53
|
+
return (torch.equal(fa[:n].cpu(), fb[:n].cpu())
|
|
54
|
+
and torch.equal(fa[-n:].cpu(), fb[-n:].cpu()))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class _Ticket:
|
|
59
|
+
tier: str # "disk" | "ram"
|
|
60
|
+
shape: torch.Size
|
|
61
|
+
dtype: torch.dtype
|
|
62
|
+
device: torch.device
|
|
63
|
+
shard: str | None = None # disk tier
|
|
64
|
+
key: str | None = None
|
|
65
|
+
spill: torch.Tensor | None = None # ram tier
|
|
66
|
+
storage_key: int = 0 # data_ptr at stash time (tied weights)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class WeightStore:
|
|
71
|
+
"""Tiered off-device store for consumed host weights."""
|
|
72
|
+
|
|
73
|
+
checkpoint: str | None = None
|
|
74
|
+
_index: dict[str, str] | None = field(default=None, repr=False)
|
|
75
|
+
_by_storage: dict[int, _Ticket] = field(default_factory=dict,
|
|
76
|
+
repr=False)
|
|
77
|
+
_stashed: list = field(default_factory=list, repr=False)
|
|
78
|
+
stats: dict[str, Any] = field(default_factory=lambda: {
|
|
79
|
+
"disk": 0, "ram": 0, "freed_bytes": 0, "restored": 0})
|
|
80
|
+
|
|
81
|
+
# ---- provenance -------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def _load_index(self) -> dict[str, str]:
|
|
84
|
+
if self._index is not None:
|
|
85
|
+
return self._index
|
|
86
|
+
index: dict[str, str] = {}
|
|
87
|
+
if self.checkpoint and os.path.isdir(self.checkpoint):
|
|
88
|
+
idx_path = os.path.join(
|
|
89
|
+
self.checkpoint, "model.safetensors.index.json")
|
|
90
|
+
if os.path.exists(idx_path):
|
|
91
|
+
with open(idx_path) as f:
|
|
92
|
+
weight_map = json.load(f).get("weight_map", {})
|
|
93
|
+
index = {k: os.path.join(self.checkpoint, v)
|
|
94
|
+
for k, v in weight_map.items()}
|
|
95
|
+
else:
|
|
96
|
+
for shard in glob.glob(
|
|
97
|
+
os.path.join(self.checkpoint, "*.safetensors")):
|
|
98
|
+
from safetensors import safe_open
|
|
99
|
+
with safe_open(shard, framework="pt") as f:
|
|
100
|
+
for k in f.keys():
|
|
101
|
+
index[k] = shard
|
|
102
|
+
self._index = index
|
|
103
|
+
return index
|
|
104
|
+
|
|
105
|
+
def _disk_source(self, name: str,
|
|
106
|
+
tensor: torch.Tensor) -> tuple[str, str] | None:
|
|
107
|
+
"""A verified (shard, key) for this parameter, or None."""
|
|
108
|
+
index = self._load_index()
|
|
109
|
+
if not index:
|
|
110
|
+
return None
|
|
111
|
+
candidates = [name]
|
|
112
|
+
# hosts commonly hang the checkpoint tree under one wrapper
|
|
113
|
+
# attribute; try progressively stripped prefixes
|
|
114
|
+
parts = name.split(".")
|
|
115
|
+
for i in range(1, min(4, len(parts))):
|
|
116
|
+
candidates.append(".".join(parts[i:]))
|
|
117
|
+
from safetensors import safe_open
|
|
118
|
+
for key in candidates:
|
|
119
|
+
shard = index.get(key)
|
|
120
|
+
if shard is None:
|
|
121
|
+
continue
|
|
122
|
+
with safe_open(shard, framework="pt") as f:
|
|
123
|
+
disk = f.get_tensor(key)
|
|
124
|
+
if _sample_equal(disk.to(tensor.dtype), tensor):
|
|
125
|
+
return shard, key
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# ---- stash / restore -------------------------------------------
|
|
129
|
+
|
|
130
|
+
@torch.no_grad()
|
|
131
|
+
def stash_module(self, name: str, module: torch.nn.Module) -> int:
|
|
132
|
+
"""Move every parameter's truth off the device and free it.
|
|
133
|
+
|
|
134
|
+
Returns the number of device bytes freed. Safe to call twice —
|
|
135
|
+
already-emptied parameters are skipped. The module object stays
|
|
136
|
+
whole (shapes, dtypes and attributes remain introspectable);
|
|
137
|
+
only the storage leaves.
|
|
138
|
+
"""
|
|
139
|
+
freed = 0
|
|
140
|
+
for mpath, sub in module.named_modules():
|
|
141
|
+
for leaf, par in list(sub._parameters.items()):
|
|
142
|
+
if par is None or par.is_meta or par.numel() == 0:
|
|
143
|
+
continue
|
|
144
|
+
pname = f"{mpath}.{leaf}" if mpath else leaf
|
|
145
|
+
skey = par.data.data_ptr()
|
|
146
|
+
ticket = self._by_storage.get(skey)
|
|
147
|
+
if ticket is None:
|
|
148
|
+
full = f"{name}.{pname}" if name else pname
|
|
149
|
+
source = self._disk_source(full, par.data)
|
|
150
|
+
if source is not None:
|
|
151
|
+
ticket = _Ticket(
|
|
152
|
+
tier="disk", shape=par.shape, dtype=par.dtype,
|
|
153
|
+
device=par.device, shard=source[0],
|
|
154
|
+
key=source[1], storage_key=skey)
|
|
155
|
+
self.stats["disk"] += 1
|
|
156
|
+
else:
|
|
157
|
+
spill = torch.empty(
|
|
158
|
+
par.shape, dtype=par.dtype, device="cpu",
|
|
159
|
+
pin_memory=torch.cuda.is_available())
|
|
160
|
+
spill.copy_(par.data)
|
|
161
|
+
ticket = _Ticket(
|
|
162
|
+
tier="ram", shape=par.shape, dtype=par.dtype,
|
|
163
|
+
device=par.device, spill=spill, storage_key=skey)
|
|
164
|
+
self.stats["ram"] += 1
|
|
165
|
+
self._by_storage[skey] = ticket
|
|
166
|
+
tickets = getattr(module, "_frt_tickets", None)
|
|
167
|
+
if tickets is None:
|
|
168
|
+
tickets = {}
|
|
169
|
+
module._frt_tickets = tickets
|
|
170
|
+
module._frt_store = self
|
|
171
|
+
self._stashed.append(module)
|
|
172
|
+
tickets[pname] = ticket
|
|
173
|
+
freed += par.numel() * par.element_size()
|
|
174
|
+
# release to a meta parameter of the SAME shape: the
|
|
175
|
+
# storage is gone, but shape probes (an attention family
|
|
176
|
+
# deriving head counts from a weight) still see the truth
|
|
177
|
+
# — compute on it fails loudly and per-site, instead of
|
|
178
|
+
# collapsing a whole discovery stage on a 1-D empty
|
|
179
|
+
sub._parameters[leaf] = torch.nn.Parameter(
|
|
180
|
+
torch.empty(par.shape, dtype=par.dtype,
|
|
181
|
+
device="meta"), requires_grad=False)
|
|
182
|
+
self.stats["freed_bytes"] += freed
|
|
183
|
+
return freed
|
|
184
|
+
|
|
185
|
+
@torch.no_grad()
|
|
186
|
+
def restore_module(self, module: torch.nn.Module) -> bool:
|
|
187
|
+
"""Put every stashed parameter back on its device. Idempotent."""
|
|
188
|
+
tickets = getattr(module, "_frt_tickets", None)
|
|
189
|
+
if not tickets:
|
|
190
|
+
return False
|
|
191
|
+
subs = dict(module.named_modules())
|
|
192
|
+
for pname, ticket in tickets.items():
|
|
193
|
+
mpath, _, leaf = pname.rpartition(".")
|
|
194
|
+
sub = subs.get(mpath)
|
|
195
|
+
par = None if sub is None else sub._parameters.get(leaf)
|
|
196
|
+
if par is None or not par.is_meta:
|
|
197
|
+
continue
|
|
198
|
+
if ticket.tier == "disk":
|
|
199
|
+
from safetensors import safe_open
|
|
200
|
+
with safe_open(ticket.shard, framework="pt") as f:
|
|
201
|
+
data = f.get_tensor(ticket.key)
|
|
202
|
+
data = data.to(ticket.device, ticket.dtype)
|
|
203
|
+
else:
|
|
204
|
+
data = ticket.spill.to(ticket.device, non_blocking=False)
|
|
205
|
+
if data.shape != ticket.shape:
|
|
206
|
+
raise RuntimeError(
|
|
207
|
+
f"weight store: {pname} restored to {tuple(data.shape)}"
|
|
208
|
+
f", expected {tuple(ticket.shape)}")
|
|
209
|
+
sub._parameters[leaf] = torch.nn.Parameter(
|
|
210
|
+
data, requires_grad=False)
|
|
211
|
+
self.stats["restored"] += 1
|
|
212
|
+
del module._frt_tickets
|
|
213
|
+
del module._frt_store
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
def restore_all(self) -> int:
|
|
217
|
+
"""Abort path: put every stashed module's weights back.
|
|
218
|
+
|
|
219
|
+
For a bind or attach that dies midway — the model returns to
|
|
220
|
+
runnable, whatever the failure was.
|
|
221
|
+
"""
|
|
222
|
+
return _restore_all_of(self)
|
|
223
|
+
|
|
224
|
+
def drop_module(self, module: torch.nn.Module) -> None:
|
|
225
|
+
"""Forget a module's tickets — its consumption becomes final."""
|
|
226
|
+
if hasattr(module, "_frt_tickets"):
|
|
227
|
+
del module._frt_tickets
|
|
228
|
+
if hasattr(module, "_frt_store"):
|
|
229
|
+
del module._frt_store
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _restore_all_of(store: "WeightStore") -> int:
|
|
233
|
+
count = 0
|
|
234
|
+
for module in list(store._stashed):
|
|
235
|
+
if store.restore_module(module):
|
|
236
|
+
count += 1
|
|
237
|
+
store._stashed.clear()
|
|
238
|
+
return count
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def restore_for_fallback(host: torch.nn.Module) -> bool:
|
|
242
|
+
"""Bring a consumed host module back for a live fallback.
|
|
243
|
+
|
|
244
|
+
Called from the guard path when a seam outside its contract needs
|
|
245
|
+
the host and the host's weights were consumed. Returns True when a
|
|
246
|
+
restore happened. Consumption made final (tickets dropped) leaves
|
|
247
|
+
nothing to restore — the caller refuses instead.
|
|
248
|
+
"""
|
|
249
|
+
store = getattr(host, "_frt_store", None)
|
|
250
|
+
if store is None:
|
|
251
|
+
return False
|
|
252
|
+
return store.restore_module(host)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = ["WeightStore", "restore_for_fallback"]
|