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,94 @@
1
+ """Quantize-on-adopt: full-precision checkpoints too big for the card.
2
+
3
+ ``adopt_prequantized`` serves checkpoints that arrive already packed in
4
+ someone else's layout. This module is the door for the opposite
5
+ situation: the checkpoint is full precision and *cannot fit the card at
6
+ all*, but nearly all of its weight mass sits in one structure family —
7
+ a sparse-MoE expert bank. Quantizing that family once, at load time,
8
+ into structure impls brings the whole model into card budget while the
9
+ attention, norms, and router stay in the host's own precision.
10
+
11
+ The calling convention matches the sibling door: the model is expected
12
+ CPU-resident straight from its loader; each expert bank streams through
13
+ the GPU in slabs as it packs, so peak footprint is the dense checkpoint
14
+ plus one slab. Move the model to the device *after* adopting — by then
15
+ the dense banks are gone and the remainder fits.
16
+
17
+ Like adoption of a pre-quantized checkpoint, this is a load-time
18
+ transform, not an attachment: the dense expert weights are released as
19
+ each bank binds (holding them would defeat the footprint the door
20
+ exists for), so there is no ``detach()`` — undoing an adoption is
21
+ reloading the checkpoint. The pack-and-unpack relative L2 of every bank
22
+ is recorded in the returned report; the receipt claims a measured
23
+ conversion loss, not the absence of one.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import torch
29
+
30
+ from .prequantized import AdoptionReport
31
+
32
+ __all__ = ["quantize_on_adopt"]
33
+
34
+ _FORMATS = ("moe_experts_nvfp4",)
35
+
36
+
37
+ def _is_moe_expert_bank(module: torch.nn.Module) -> bool:
38
+ """An expert bank: stacked 3D projections plus the activation, the
39
+ shape contract ``gate_up_proj [E, 2I, H]`` / ``down_proj [E, H, I]``."""
40
+ gu = getattr(module, "gate_up_proj", None)
41
+ dn = getattr(module, "down_proj", None)
42
+ if not (torch.is_tensor(gu) and torch.is_tensor(dn)):
43
+ return False
44
+ if gu.dim() != 3 or dn.dim() != 3 or not hasattr(module, "act_fn"):
45
+ return False
46
+ return (gu.shape[0] == dn.shape[0]
47
+ and gu.shape[2] == dn.shape[1]
48
+ and gu.shape[1] == 2 * dn.shape[2])
49
+
50
+
51
+ @torch.no_grad()
52
+ def quantize_on_adopt(model: torch.nn.Module,
53
+ fmt: str = "moe_experts_nvfp4", *,
54
+ verbose: bool = False) -> AdoptionReport:
55
+ """Quantize every discovered expert bank of ``model`` into a
56
+ structure impl; returns the adoption report for the receipt."""
57
+ if fmt not in _FORMATS:
58
+ raise ValueError(
59
+ f"unknown quantize-on-adopt format {fmt!r}; supported: "
60
+ f"{', '.join(_FORMATS)}")
61
+
62
+ from .impls.moe_experts import nvfp4_dynamic
63
+
64
+ report = AdoptionReport(fmt=fmt)
65
+ for name, module in list(model.named_modules()):
66
+ for child_name, child in list(module.named_children()):
67
+ if not _is_moe_expert_bank(child):
68
+ continue
69
+ path = f"{name}.{child_name}" if name else child_name
70
+ bound, rels = nvfp4_dynamic.bind_experts_seam(
71
+ {"gate_up_proj": child.gate_up_proj.detach(),
72
+ "down_proj": child.down_proj.detach()},
73
+ child.act_fn)
74
+ # release the dense bank before moving on: the streaming
75
+ # bind is only slab-peak if the retired experts actually go
76
+ child.gate_up_proj = None
77
+ child.down_proj = None
78
+ setattr(module, child_name, bound)
79
+ report.replaced.append(path)
80
+ for stack, rel in rels.items():
81
+ report.conversion_rel_l2[f"{path}.{stack}"] = rel
82
+ if verbose:
83
+ print(f"[quantize_on_adopt] {path}: "
84
+ + ", ".join(f"{k} relL2={v:.4f}"
85
+ for k, v in rels.items()),
86
+ flush=True)
87
+ if not report.replaced:
88
+ raise ValueError(
89
+ "no expert banks found: this model does not carry the "
90
+ f"stacked-3D MoE structure {fmt} adopts")
91
+ torch.cuda.empty_cache()
92
+ if verbose:
93
+ print(f"[quantize_on_adopt] {report.summary()}", flush=True)
94
+ return report
@@ -0,0 +1,438 @@
1
+ """Declarative e2e recipes: assemble levers, audit on the graph, certify.
2
+
3
+ A recipe declares how a host pipeline eats structure yield end to end:
4
+ which levers engage (region families, cadence pieces, seam negotiations,
5
+ dtype changes), how the hot stage is built, and under which explicit
6
+ gates the result is judged. ``run_recipe`` assembles the levers
7
+ transactionally, audits baseline vs treated **in the same process**
8
+ (cross-run anchors carry sub-millisecond drift, so sub-2% verdicts are
9
+ unreliable across runs), and emits a receipt recording every switch
10
+ state, every gate number, and every refusal reason.
11
+
12
+ Switch discipline (the lever lifecycle):
13
+
14
+ off declared but not engaged; recorded, never built
15
+ candidate engaged and audited this run; a refused lever stays a
16
+ candidate — refusal is an outcome, not an error
17
+ certified won a same-process audit under the current plan digest;
18
+ any digest change (shapes, weights, environment, gates)
19
+ demotes it back to candidate for re-audit — certification
20
+ never outlives the plan it was earned on
21
+
22
+ Gate thresholds are switches too: they are folded into the plan digest
23
+ and written into the receipt, so a relaxed gate is always visible and
24
+ invalidates prior certifications instead of silently inheriting them.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import hashlib
30
+ import json
31
+ import pathlib
32
+ from dataclasses import dataclass, field
33
+ from typing import Any, Callable, Mapping
34
+
35
+ import torch
36
+
37
+ from .gates import parity_metrics
38
+ from .swap import AttachHandle, attach as _swap_attach
39
+
40
+ _LEVER_KINDS = ("regions", "cadence", "seam_negotiation", "dtype",
41
+ "capture_form")
42
+
43
+
44
+ @dataclass
45
+ class Gates:
46
+ """Explicit gate thresholds. Part of the plan digest: changing one
47
+ is a plan change, not a tweak.
48
+
49
+ ``parity_cos`` is the hard floor — below it the recipe refuses.
50
+ ``parity_warn`` marks the comfort line: results in
51
+ ``[parity_cos, parity_warn)`` pass but the receipt records
52
+ ``parity_band: "warn"`` with a note that calibration owes the
53
+ remaining accuracy. During the performance-assembly phase the floor
54
+ is intentionally loose (collapse-only); tighter banded gates arrive
55
+ with the calibration/fallback design, not before.
56
+ """
57
+
58
+ parity_cos: float = 0.99
59
+ parity_warn: float = 0.999
60
+ min_speedup: float = 1.02
61
+ drift_budget: float = 0.02
62
+
63
+ def as_dict(self) -> dict[str, float]:
64
+ return {"parity_cos": self.parity_cos,
65
+ "parity_warn": self.parity_warn,
66
+ "min_speedup": self.min_speedup,
67
+ "drift_budget": self.drift_budget}
68
+
69
+
70
+ @dataclass
71
+ class Lever:
72
+ """One named yield switch: a group of swaps engaged and judged
73
+ together.
74
+
75
+ ``build(model, ctx)`` returns either a mapping ``path -> module`` of
76
+ swaps, or a ``(swaps, outside_update)`` tuple where
77
+ ``outside_update`` is a callable the host must run at the lever's
78
+ cadence outside the captured stage (e.g. refreshing static KV
79
+ buffers on observation ticks). Outside updates are collected into
80
+ ``ctx["outside_updates"]`` before ``build_stage`` runs.
81
+ """
82
+
83
+ name: str
84
+ kind: str
85
+ build: Callable[[torch.nn.Module, dict], Any] | None = None
86
+ state: str = "candidate"
87
+ notes: str = ""
88
+
89
+ def __post_init__(self) -> None:
90
+ if self.kind not in _LEVER_KINDS:
91
+ raise ValueError(f"lever kind {self.kind!r} not in "
92
+ f"{_LEVER_KINDS}")
93
+ if self.state not in ("off", "candidate", "certified"):
94
+ raise ValueError(f"lever state {self.state!r}")
95
+
96
+
97
+ @dataclass
98
+ class Arm:
99
+ """One executable form of the pipeline under audit.
100
+
101
+ ``refs`` must pin every object whose device memory the arm's graph
102
+ reads (input buffers, static intermediates, closures holding them).
103
+ A captured graph keeps no Python references of its own: if a tensor
104
+ it reads is garbage-collected, the next arm's allocations reuse that
105
+ memory and this arm replays over foreign data — the same-process
106
+ baseline retime is exactly where that corruption surfaces.
107
+ """
108
+
109
+ tick: Callable[[], Any]
110
+ output: Callable[[], torch.Tensor]
111
+ teardown: Callable[[], None] | None = None
112
+ refs: Any = None
113
+ stage: Any = None # the CapturedStage, if the arm graphed one —
114
+ # lets the winning arm feed stage.export()
115
+
116
+
117
+ @dataclass
118
+ class Recipe:
119
+ """The declaration ``run_recipe`` executes.
120
+
121
+ ``build_stage(model, ctx)`` constructs the hot stage for whatever is
122
+ currently attached to the model (it is called once for the untouched
123
+ baseline and once with the levers engaged) and returns an
124
+ :class:`Arm`. ``reference(model, ctx)`` produces the stock eager
125
+ output that anchors parity. ``between_arms`` runs after the baseline
126
+ arm is built and before levers attach — compile hosts pass
127
+ ``torch._dynamo.reset`` here so the treated arm gets a fresh
128
+ compile budget instead of inheriting cache-limit fallout.
129
+ """
130
+
131
+ name: str
132
+ levers: list[Lever]
133
+ build_stage: Callable[[torch.nn.Module, dict], Arm]
134
+ reference: Callable[[torch.nn.Module, dict], torch.Tensor]
135
+ gates: Gates = field(default_factory=Gates)
136
+ between_arms: Callable[[], None] | None = None
137
+
138
+
139
+ @dataclass
140
+ class RecipeRun:
141
+ """Outcome of one audit: verdict, evidence, and the live stage."""
142
+
143
+ verdict: str
144
+ receipt: dict[str, Any]
145
+ arm: Arm | None = None
146
+ _handle: AttachHandle | None = None
147
+
148
+ def report(self) -> str:
149
+ r = self.receipt
150
+ lines = [f"recipe {r['recipe']}: {self.verdict}"
151
+ + (f" [{r['reason']}]" if r.get("reason") else "")]
152
+ for name, stat in r["levers"].items():
153
+ demote = (f" (demoted: {stat['demoted']})"
154
+ if stat.get("demoted") else "")
155
+ lines.append(f" {name} [{stat['kind']}]: "
156
+ f"{stat['state_in']} -> {stat['state_out']}"
157
+ f", {stat.get('seams', 0)} seam(s){demote}")
158
+ base, treat = r.get("baseline"), r.get("treated")
159
+ if base:
160
+ lines.append(f" baseline {base['ms']:.2f} ms "
161
+ f"(retime {base.get('retime_ms')}, "
162
+ f"drift {base.get('drift')})")
163
+ if treat:
164
+ lines.append(f" treated {treat['ms']:.2f} ms "
165
+ f"({treat['speedup']:.3f}x), parity "
166
+ f"{treat['parity_vs_reference']:.6f}")
167
+ return "\n".join(lines)
168
+
169
+ def detach(self) -> None:
170
+ if self.arm is not None and self.arm.teardown is not None:
171
+ self.arm.teardown()
172
+ self.arm = None
173
+ if self._handle is not None:
174
+ self._handle.detach()
175
+ self._handle = None
176
+
177
+ def save_receipt(self, directory) -> pathlib.Path:
178
+ directory = pathlib.Path(directory)
179
+ directory.mkdir(parents=True, exist_ok=True)
180
+ path = directory / f"recipe_{self.receipt['recipe']}.json"
181
+ path.write_text(json.dumps(self.receipt, indent=2, default=str))
182
+ return path
183
+
184
+
185
+ def _plan_digest(recipe: Recipe, model: torch.nn.Module,
186
+ ctx: Mapping[str, Any]) -> str:
187
+ ident = {
188
+ "recipe": recipe.name,
189
+ "levers": sorted((lv.name, lv.kind) for lv in recipe.levers),
190
+ "gates": recipe.gates.as_dict(),
191
+ "torch": torch.__version__,
192
+ "device": (torch.cuda.get_device_name()
193
+ if torch.cuda.is_available() else "cpu"),
194
+ "model": type(model).__name__,
195
+ "shape_sig": str(ctx.get("shape_sig", "")),
196
+ }
197
+ return hashlib.sha256(json.dumps(
198
+ ident, sort_keys=True, default=str).encode()).hexdigest()
199
+
200
+
201
+ def _time_ms(fn: Callable[[], Any], warmup: int, iters: int) -> float:
202
+ for _ in range(warmup):
203
+ fn()
204
+ torch.cuda.synchronize()
205
+ start, end = torch.cuda.Event(True), torch.cuda.Event(True)
206
+ start.record()
207
+ for _ in range(iters):
208
+ fn()
209
+ end.record()
210
+ torch.cuda.synchronize()
211
+ return start.elapsed_time(end) / iters
212
+
213
+
214
+ def run_recipe(
215
+ recipe: Recipe,
216
+ model: torch.nn.Module,
217
+ ctx: dict[str, Any] | None = None,
218
+ *,
219
+ receipts_dir: str | pathlib.Path | None = None,
220
+ reaudit: str = "always",
221
+ warmup: int = 5,
222
+ iters: int = 30,
223
+ verbose: bool = True,
224
+ ) -> RecipeRun:
225
+ """Assemble, audit, and certify a recipe in one call.
226
+
227
+ ``reaudit="always"`` runs the full same-process A/B.
228
+ ``reaudit="on_change"`` skips the timing audit when every engaged
229
+ lever is already certified under the current plan digest in the
230
+ stored receipt — parity is still re-checked, timing numbers carry
231
+ over marked ``cached``. Any digest mismatch forces the full audit.
232
+ """
233
+
234
+ def say(msg: str) -> None:
235
+ if verbose:
236
+ print(f"[recipe] {msg}", flush=True)
237
+
238
+ ctx = ctx if ctx is not None else {}
239
+ gates = recipe.gates
240
+ digest = _plan_digest(recipe, model, ctx)
241
+ say(f"{recipe.name}: digest {digest[:12]}")
242
+
243
+ prior = None
244
+ if receipts_dir is not None:
245
+ prior_path = (pathlib.Path(receipts_dir)
246
+ / f"recipe_{recipe.name}.json")
247
+ if prior_path.is_file():
248
+ prior = json.loads(prior_path.read_text())
249
+
250
+ # ---- lever lifecycle: demote certifications the digest no longer
251
+ # covers, adopt ones the stored receipt still backs ----
252
+ lever_stats: dict[str, dict[str, Any]] = {}
253
+ engaged: list[Lever] = []
254
+ for lever in recipe.levers:
255
+ stat = {"kind": lever.kind, "state_in": lever.state,
256
+ "notes": lever.notes}
257
+ state = lever.state
258
+ prior_rec = (prior or {}).get("levers", {}).get(lever.name)
259
+ prior_certified = (prior is not None
260
+ and prior.get("digest") == digest
261
+ and prior_rec is not None
262
+ and prior_rec.get("state_out") == "certified")
263
+ if state == "certified" and not prior_certified:
264
+ state = "candidate"
265
+ stat["demoted"] = ("plan digest changed"
266
+ if prior is not None else "no stored receipt")
267
+ elif state == "candidate" and prior_certified:
268
+ state = "certified"
269
+ stat["state"] = state
270
+ lever_stats[lever.name] = stat
271
+ if state != "off":
272
+ engaged.append(lever)
273
+ if not engaged:
274
+ say("no engaged levers — nothing to audit")
275
+ receipt = {"recipe": recipe.name, "digest": digest,
276
+ "levers": {}, "verdict": "empty"}
277
+ return RecipeRun("empty", receipt)
278
+
279
+ cached_ok = (reaudit == "on_change" and prior is not None
280
+ and prior.get("digest") == digest
281
+ and prior.get("verdict") == "win"
282
+ and all(lever_stats[lv.name]["state"] == "certified"
283
+ for lv in engaged))
284
+
285
+ # ---- stock reference + baseline arm ----
286
+ with torch.no_grad():
287
+ reference_out = recipe.reference(model, ctx).detach().float().cpu()
288
+
289
+ base_ms = base_retime = drift = None
290
+ base_arm: Arm | None = None
291
+ if not cached_ok:
292
+ base_arm = recipe.build_stage(model, ctx)
293
+ base_arm.tick() # outputs are only defined after a full tick
294
+ base_out = base_arm.output().detach().float().cpu()
295
+ base_parity = parity_metrics(base_out, reference_out)["cosine"]
296
+ if base_parity < gates.parity_cos:
297
+ if base_arm.teardown is not None:
298
+ base_arm.teardown()
299
+ say(f"baseline arm parity {base_parity:.6f} < "
300
+ f"{gates.parity_cos} vs stock eager — audit invalid")
301
+ for stat in lever_stats.values():
302
+ stat["state_out"] = stat.pop("state")
303
+ receipt = {"recipe": recipe.name, "digest": digest,
304
+ "levers": lever_stats,
305
+ "baseline": {"parity_vs_reference": base_parity},
306
+ "verdict": "invalid_baseline",
307
+ "reason": "baseline arm does not match stock eager"}
308
+ return RecipeRun("invalid_baseline", receipt)
309
+ base_ms = _time_ms(base_arm.tick, warmup, iters)
310
+ say(f"baseline arm {base_ms:.2f} ms "
311
+ f"(parity {base_parity:.6f})")
312
+
313
+ if recipe.between_arms is not None:
314
+ recipe.between_arms()
315
+
316
+ # ---- engage levers (one transaction) ----
317
+ swaps: dict[str, torch.nn.Module] = {}
318
+ updates: list[Callable[[], None]] = []
319
+ refused_build: list[Lever] = []
320
+ for lever in engaged:
321
+ try:
322
+ built = lever.build(model, ctx) if lever.build else {}
323
+ except ValueError as refusal:
324
+ # a lever may disqualify itself at build time (calibration
325
+ # shows its precondition does not hold on this host) —
326
+ # that is an outcome to record, not a reason to abort the
327
+ # other levers
328
+ lever_stats[lever.name]["state_out"] = "refused"
329
+ lever_stats[lever.name]["reason"] = str(refusal)[:120]
330
+ lever_stats[lever.name]["seams"] = 0
331
+ refused_build.append(lever)
332
+ say(f"{lever.name}: refused at build "
333
+ f"[{str(refusal)[:80]}]")
334
+ continue
335
+ if isinstance(built, tuple):
336
+ lever_swaps, update = built
337
+ else:
338
+ lever_swaps, update = built, None
339
+ overlap = set(lever_swaps) & set(swaps)
340
+ if overlap:
341
+ raise ValueError(f"lever {lever.name!r} overlaps prior "
342
+ f"levers at {sorted(overlap)[:3]}")
343
+ swaps.update(lever_swaps)
344
+ if update is not None:
345
+ updates.append(update)
346
+ lever_stats[lever.name]["seams"] = len(lever_swaps)
347
+ ctx["outside_updates"] = updates
348
+ handle = _swap_attach(model, swaps) if swaps else None
349
+ say(f"engaged {len(engaged)} lever(s), {len(swaps)} seam(s)")
350
+
351
+ # ---- treated arm: parity gate, then net-win gate ----
352
+ arm = recipe.build_stage(model, ctx)
353
+ arm.tick()
354
+ treated_out = arm.output().detach().float().cpu()
355
+ parity = parity_metrics(treated_out, reference_out)["cosine"]
356
+ parity_ok = parity >= gates.parity_cos
357
+ parity_band = ("ok" if parity >= gates.parity_warn
358
+ else "warn" if parity_ok else "fail")
359
+ say(f"treated parity vs stock eager: {parity:.6f}"
360
+ + (" [WARN: below comfort line — calibration owes the rest]"
361
+ if parity_band == "warn" else ""))
362
+
363
+ treated_ms = speedup = None
364
+ if parity_ok and cached_ok:
365
+ base_ms = prior["baseline"]["ms"]
366
+ treated_ms = prior["treated"]["ms"]
367
+ speedup = prior["treated"]["speedup"]
368
+ win = True
369
+ say(f"digest hit — timings carried from stored receipt "
370
+ f"({treated_ms:.2f} ms)")
371
+ elif parity_ok:
372
+ treated_ms = _time_ms(arm.tick, warmup, iters)
373
+ # baseline retime after the treated arm: same-process drift
374
+ # bound; the win must clear the *faster* of the two baselines
375
+ base_retime = _time_ms(base_arm.tick, warmup, iters)
376
+ drift = round(abs(base_retime - base_ms) / base_ms, 4)
377
+ base_floor = min(base_ms, base_retime)
378
+ speedup = round(base_floor / treated_ms, 4)
379
+ win = speedup >= gates.min_speedup
380
+ say(f"treated {treated_ms:.2f} ms vs baseline floor "
381
+ f"{base_floor:.2f} ({speedup:.3f}x, drift {drift})")
382
+ else:
383
+ win = False
384
+
385
+ if base_arm is not None and base_arm.teardown is not None:
386
+ base_arm.teardown()
387
+
388
+ verdict = "win" if win else "refused"
389
+ reason = None
390
+ if not parity_ok:
391
+ reason = f"parity {parity:.6f} < {gates.parity_cos}"
392
+ elif not win:
393
+ reason = f"no net win ({speedup}x < {gates.min_speedup})"
394
+ for lever in engaged:
395
+ if lever in refused_build:
396
+ continue
397
+ lever_stats[lever.name]["state_out"] = (
398
+ "certified" if win else "candidate")
399
+ for name, stat in lever_stats.items():
400
+ stat.setdefault("state_out", stat["state"])
401
+ stat.pop("state", None)
402
+
403
+ if not win:
404
+ if arm.teardown is not None:
405
+ arm.teardown()
406
+ arm = None
407
+ if handle is not None:
408
+ handle.detach()
409
+ handle = None
410
+ say(f"refused [{reason}] — model restored untouched")
411
+ else:
412
+ say(f"win: {len(engaged)} lever(s) certified under digest "
413
+ f"{digest[:12]}")
414
+
415
+ receipt = {
416
+ "recipe": recipe.name, "digest": digest,
417
+ "model": type(model).__name__,
418
+ "device": (torch.cuda.get_device_name()
419
+ if torch.cuda.is_available() else "cpu"),
420
+ "torch": torch.__version__,
421
+ "gates": gates.as_dict(),
422
+ "reaudit": reaudit, "cached": bool(cached_ok),
423
+ "levers": lever_stats,
424
+ "baseline": {"ms": None if base_ms is None else round(base_ms, 3),
425
+ "retime_ms": (None if base_retime is None
426
+ else round(base_retime, 3)),
427
+ "drift": drift},
428
+ "treated": (None if treated_ms is None else
429
+ {"ms": round(treated_ms, 3), "speedup": speedup,
430
+ "parity_vs_reference": round(parity, 7),
431
+ "parity_band": parity_band}),
432
+ "verdict": verdict, "reason": reason,
433
+ }
434
+ run = RecipeRun(verdict, receipt, arm=arm, _handle=handle)
435
+ if receipts_dir is not None:
436
+ path = run.save_receipt(receipts_dir)
437
+ say(f"receipt -> {path}")
438
+ return run