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,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
|