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.
Files changed (122) hide show
  1. flashrt_structures/__init__.py +174 -0
  2. flashrt_structures/adapters/__init__.py +55 -0
  3. flashrt_structures/adapters/diffusers_attention.py +237 -0
  4. flashrt_structures/adapters/diffusers_rotary_attention.py +239 -0
  5. flashrt_structures/adapters/factored_qk_norm_rope.py +252 -0
  6. flashrt_structures/adapters/factored_two_way_attention.py +99 -0
  7. flashrt_structures/adapters/gemma_attention.py +226 -0
  8. flashrt_structures/adapters/packed_qkv_rope.py +342 -0
  9. flashrt_structures/adapters/packed_stream_qk_norm_rope.py +376 -0
  10. flashrt_structures/adapters/qwen_per_head_qk_norm_rope.py +231 -0
  11. flashrt_structures/adapters/sglang_engine.py +207 -0
  12. flashrt_structures/adapters/transformers_attention_interface.py +73 -0
  13. flashrt_structures/adapters/transformers_gated_delta.py +152 -0
  14. flashrt_structures/adapters/transformers_gated_delta_fused.py +96 -0
  15. flashrt_structures/adapters/vllm_engine.py +424 -0
  16. flashrt_structures/adjudicate.py +85 -0
  17. flashrt_structures/aot.py +191 -0
  18. flashrt_structures/autobuild.py +2052 -0
  19. flashrt_structures/beta/__init__.py +43 -0
  20. flashrt_structures/beta/conform.py +94 -0
  21. flashrt_structures/beta/joins.py +113 -0
  22. flashrt_structures/beta/negotiate.py +84 -0
  23. flashrt_structures/beta/ports.py +140 -0
  24. flashrt_structures/decisions.py +80 -0
  25. flashrt_structures/discover.py +623 -0
  26. flashrt_structures/explain.py +70 -0
  27. flashrt_structures/frontdoor.py +572 -0
  28. flashrt_structures/gates.py +465 -0
  29. flashrt_structures/guard.py +421 -0
  30. flashrt_structures/handle.py +189 -0
  31. flashrt_structures/impls/__init__.py +219 -0
  32. flashrt_structures/impls/adaln_producer/__init__.py +8 -0
  33. flashrt_structures/impls/adaln_producer/broker.py +116 -0
  34. flashrt_structures/impls/adaln_producer/fused.py +388 -0
  35. flashrt_structures/impls/adarms_stack/__init__.py +8 -0
  36. flashrt_structures/impls/adarms_stack/fp8_chain.py +832 -0
  37. flashrt_structures/impls/adarms_stack/region.py +102 -0
  38. flashrt_structures/impls/attention_core/__init__.py +132 -0
  39. flashrt_structures/impls/attention_core/fa2_seqused.py +458 -0
  40. flashrt_structures/impls/attention_core/fa4_cute.py +154 -0
  41. flashrt_structures/impls/attention_core/fa4_fp8.py +178 -0
  42. flashrt_structures/impls/attention_core/masked_mha.py +158 -0
  43. flashrt_structures/impls/attention_core/two_way_fa2.py +220 -0
  44. flashrt_structures/impls/cadence_static/__init__.py +18 -0
  45. flashrt_structures/impls/cadence_static/buffers.py +122 -0
  46. flashrt_structures/impls/cadence_static/cross_attention.py +187 -0
  47. flashrt_structures/impls/chain_elements.py +89 -0
  48. flashrt_structures/impls/decode_loop/__init__.py +0 -0
  49. flashrt_structures/impls/decode_loop/fp8_kv.py +206 -0
  50. flashrt_structures/impls/decode_loop/mtp_speculative.py +245 -0
  51. flashrt_structures/impls/decode_loop/whole_step.py +852 -0
  52. flashrt_structures/impls/decoder_block/__init__.py +6 -0
  53. flashrt_structures/impls/decoder_block/attn_sublayer.py +110 -0
  54. flashrt_structures/impls/decoder_block/fused.py +167 -0
  55. flashrt_structures/impls/decoder_ffn/__init__.py +0 -0
  56. flashrt_structures/impls/decoder_ffn/fp8_static.py +310 -0
  57. flashrt_structures/impls/decoder_ffn/fp8_static.yaml +22 -0
  58. flashrt_structures/impls/decoder_ffn/w4a16_static.py +221 -0
  59. flashrt_structures/impls/decoder_ffn/w8a16_static.py +183 -0
  60. flashrt_structures/impls/dit_stack/__init__.py +11 -0
  61. flashrt_structures/impls/dit_stack/fp4_chain.py +417 -0
  62. flashrt_structures/impls/dit_stack/region.py +86 -0
  63. flashrt_structures/impls/fixed_iter/__init__.py +29 -0
  64. flashrt_structures/impls/fixed_iter/openpi.py +264 -0
  65. flashrt_structures/impls/fixed_iter/protocol.py +94 -0
  66. flashrt_structures/impls/gated_delta_core/__init__.py +3 -0
  67. flashrt_structures/impls/gated_delta_core/fused_layer.py +545 -0
  68. flashrt_structures/impls/gated_delta_core/hub_v3.py +152 -0
  69. flashrt_structures/impls/graph_lowering/__init__.py +27 -0
  70. flashrt_structures/impls/graph_lowering/pi052_denoise.py +179 -0
  71. flashrt_structures/impls/graph_lowering/protocol.py +76 -0
  72. flashrt_structures/impls/graph_lowering/qwen3_vl.py +364 -0
  73. flashrt_structures/impls/linear_proj/__init__.py +0 -0
  74. flashrt_structures/impls/linear_proj/fp8_static.py +270 -0
  75. flashrt_structures/impls/linear_proj/nvfp4_balance.py +131 -0
  76. flashrt_structures/impls/linear_proj/nvfp4_dynamic.py +182 -0
  77. flashrt_structures/impls/linear_proj/w8a16_static.py +230 -0
  78. flashrt_structures/impls/modnorm_qkv_chain/__init__.py +0 -0
  79. flashrt_structures/impls/modnorm_qkv_chain/fp8_ptok_table.py +291 -0
  80. flashrt_structures/impls/moe_experts/__init__.py +9 -0
  81. flashrt_structures/impls/moe_experts/nvfp4_dynamic.py +208 -0
  82. flashrt_structures/impls/moe_experts/nvfp4_w4a16.py +129 -0
  83. flashrt_structures/impls/norm_fused/__init__.py +3 -0
  84. flashrt_structures/impls/norm_fused/bf16.py +94 -0
  85. flashrt_structures/impls/norm_fused/fp8_producer.py +84 -0
  86. flashrt_structures/impls/patch_projection/__init__.py +3 -0
  87. flashrt_structures/impls/patch_projection/bf16_flat.py +139 -0
  88. flashrt_structures/impls/prefill_tower/__init__.py +10 -0
  89. flashrt_structures/impls/prefill_tower/fp8_chain.py +955 -0
  90. flashrt_structures/impls/prefill_tower/region.py +99 -0
  91. flashrt_structures/impls/qk_norm_rope/__init__.py +12 -0
  92. flashrt_structures/impls/qk_norm_rope/per_head_gqa.py +199 -0
  93. flashrt_structures/impls/qk_norm_rope/projection_bf16.py +165 -0
  94. flashrt_structures/impls/qkv_pack/__init__.py +5 -0
  95. flashrt_structures/impls/qkv_pack/bf16.py +110 -0
  96. flashrt_structures/impls/qkv_pack/fp8_static.py +435 -0
  97. flashrt_structures/impls/qkv_pack/nvfp4_balance.py +218 -0
  98. flashrt_structures/impls/qkv_rope/__init__.py +3 -0
  99. flashrt_structures/impls/qkv_rope/packed_bias_bf16.py +143 -0
  100. flashrt_structures/impls/step_table.py +113 -0
  101. flashrt_structures/impls/vision_ffn/__init__.py +0 -0
  102. flashrt_structures/impls/vision_ffn/fp8_static.py +261 -0
  103. flashrt_structures/impls/vision_ffn/nvfp4_balance.py +211 -0
  104. flashrt_structures/impls/vision_tower/__init__.py +7 -0
  105. flashrt_structures/impls/vision_tower/fp8_chain.py +533 -0
  106. flashrt_structures/impls/vision_tower/region.py +91 -0
  107. flashrt_structures/matrix.py +126 -0
  108. flashrt_structures/points.py +368 -0
  109. flashrt_structures/prequantized.py +131 -0
  110. flashrt_structures/quantize_on_adopt.py +94 -0
  111. flashrt_structures/recipe.py +438 -0
  112. flashrt_structures/regions.py +208 -0
  113. flashrt_structures/schemes.py +490 -0
  114. flashrt_structures/stages.py +298 -0
  115. flashrt_structures/storage.py +255 -0
  116. flashrt_structures/swap.py +398 -0
  117. flashrt_structures/workspace.py +110 -0
  118. flashrt_structures-0.2.0.dist-info/METADATA +140 -0
  119. flashrt_structures-0.2.0.dist-info/RECORD +122 -0
  120. flashrt_structures-0.2.0.dist-info/WHEEL +5 -0
  121. flashrt_structures-0.2.0.dist-info/licenses/LICENSE +202 -0
  122. 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"]