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,126 @@
1
+ """The structure-by-host activation matrix, generated from receipts.
2
+
3
+ Reuse is a claim about receipts, not intentions: a structure counts as
4
+ activated on a host only where a passing, digest-carrying receipt names
5
+ it in the executed chain. This module scans a directory of receipt
6
+ JSONs and renders the matrix — rows are structure families, columns are
7
+ hosts, cells cite the receipt gates — plus the release rule's tally
8
+ (a structure earns its catalog seat with two or more host families).
9
+
10
+ The matcher is deliberately dumb: exact family-name substrings against
11
+ the receipt's ``chain`` and ``gate`` text. A receipt that ran a family
12
+ without naming it is the receipt's defect to fix, not the matcher's to
13
+ guess around.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import pathlib
20
+
21
+ __all__ = ["FAMILIES", "generate"]
22
+
23
+ #: catalog families plus the doors that behave as families in receipts
24
+ FAMILIES = (
25
+ "decode_loop",
26
+ "gated_delta_core",
27
+ "moe_experts",
28
+ "linear_proj",
29
+ "decoder_ffn",
30
+ "qkv_pack",
31
+ "vision_ffn",
32
+ "modnorm_qkv_chain",
33
+ "qkv_rope",
34
+ "qk_norm_rope",
35
+ "per_head_gqa",
36
+ "two_way_fa2",
37
+ "cross_attention",
38
+ "fixed_iter",
39
+ "adopt_prequantized",
40
+ "quantize_on_adopt",
41
+ "mtp",
42
+ )
43
+
44
+
45
+ def _host_key(host: str) -> str:
46
+ """Collapse a receipt's host string to a short column name."""
47
+ h = host.strip()
48
+ for prefix in ("transformers ", "diffusers "):
49
+ if h.startswith(prefix):
50
+ h = h[len(prefix):]
51
+ return h
52
+
53
+
54
+ def generate(evidence_dir) -> str:
55
+ """Render the matrix markdown from a directory of receipt JSONs."""
56
+ cells: dict[str, dict[str, set[str]]] = {}
57
+ hosts: list[str] = []
58
+ scanned = passing = 0
59
+ for path in sorted(pathlib.Path(evidence_dir).glob("*.json")):
60
+ try:
61
+ rec = json.loads(path.read_text())
62
+ except (OSError, json.JSONDecodeError):
63
+ continue
64
+ scanned += 1
65
+ if rec.get("verdict") != "PASS" or "plan_digest" not in rec:
66
+ continue
67
+ host = _host_key(str(rec.get("host", "")))
68
+ if not host:
69
+ continue
70
+ passing += 1
71
+ text = " ".join((str(rec.get("chain", "")),
72
+ str(rec.get("gate", ""))))
73
+ if host not in hosts:
74
+ hosts.append(host)
75
+ for fam in FAMILIES:
76
+ if fam in text:
77
+ cells.setdefault(fam, {}).setdefault(host, set()).add(
78
+ str(rec.get("gate", path.stem)))
79
+ lines = [
80
+ "# Structure-by-host activation matrix",
81
+ "",
82
+ f"Generated from {passing} passing receipts "
83
+ f"({scanned} scanned). A cell cites the receipt gates that "
84
+ "executed the family on that host; empty means no receipt, "
85
+ "not no opinion.",
86
+ "",
87
+ "| structure | " + " | ".join(hosts) + " | hosts |",
88
+ "|---|" + "---|" * (len(hosts) + 1),
89
+ ]
90
+ for fam in FAMILIES:
91
+ row = cells.get(fam, {})
92
+ if not row:
93
+ continue
94
+ parts = []
95
+ for host in hosts:
96
+ gates = sorted(row.get(host, ()))
97
+ parts.append("<br>".join(gates) if gates else "")
98
+ lines.append(f"| {fam} | " + " | ".join(parts)
99
+ + f" | {len(row)} |")
100
+ lines += [
101
+ "",
102
+ "## Release rule tally (a family earns its seat with >=2 hosts)",
103
+ "",
104
+ ]
105
+ for fam in FAMILIES:
106
+ n = len(cells.get(fam, {}))
107
+ if n:
108
+ mark = "meets" if n >= 2 else "single-host"
109
+ lines.append(f"- {fam}: {n} host(s) — {mark}")
110
+ return "\n".join(lines) + "\n"
111
+
112
+
113
+ def main(argv=None):
114
+ import sys
115
+
116
+ args = list(sys.argv[1:] if argv is None else argv)
117
+ if len(args) != 1:
118
+ print("usage: python -m flashrt_structures.matrix "
119
+ "<evidence-dir>")
120
+ return 2
121
+ print(generate(args[0]), end="")
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ raise SystemExit(main())
@@ -0,0 +1,368 @@
1
+ """Calibration points: the spec names them, discovery locates them.
2
+
3
+ A structure's spec already declares what has to be observed to calibrate
4
+ it — ``calibration.points`` in ``catalog/<name>/structure.yaml``, named by
5
+ position in the structure's own dataflow (``x_after_norm``,
6
+ ``act_after_mul``) rather than by any host's module names. This module is
7
+ the small amount of glue that turns those names into hooks on a
8
+ particular host, and collects the statistic the house calibration path
9
+ expects.
10
+
11
+ Why the split lives where it does, for the backends still to come:
12
+
13
+ catalog/<name>/structure.yaml the point's *name*, i.e. its position
14
+ in this structure's dataflow. Backend-
15
+ independent: ``act_after_mul`` means
16
+ the same thing in a GGML graph as in
17
+ a torch module tree.
18
+ here + discovery where that position sits on this host.
19
+ impls/<name>/<backend>.py what statistic to take there and how
20
+ to reduce it — per-tensor amax for
21
+ FP8, something else entirely for a
22
+ backend whose quantisation is
23
+ per-block or driven by an importance
24
+ matrix.
25
+
26
+ Nothing about the *statistic* is decided here, and nothing about the
27
+ *position* is decided in an implementation. That is the whole point: the
28
+ positions change when a structure's definition changes (rarely, with a
29
+ version bump), while the statistic changes with every new quantisation
30
+ format (often, per backend).
31
+
32
+ The reduction is two-level, and both levels are the house's
33
+ (``flash_rt.core.calibration``, ``docs/calibration.md``):
34
+
35
+ within one sample max over every call the host makes to that point.
36
+ Required, not chosen: §4.2 of the calibration doc
37
+ records that per-step scales on a flow-matching host
38
+ gave the compiler inconsistent shapes and crashed
39
+ it. One forward covers every step, and the max
40
+ across them is the sample's amax.
41
+ across samples ``accumulate_amax(per_sample, percentile)``. Kept as
42
+ one vector per sample so the percentile is possible
43
+ at all — a running max across samples destroys the
44
+ per-sample values as it produces them.
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ from dataclasses import dataclass, field
50
+ from typing import Any, Callable, Iterable, Sequence
51
+
52
+ import torch
53
+
54
+ #: how each spec point name is reached from a discovered seam. The name on
55
+ #: the left must appear in that structure's ``calibration.points``; the
56
+ #: right-hand side is resolved from slots discovery already established,
57
+ #: never from a module-name guess.
58
+ _AT_SEAM = ("", "input")
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Point:
63
+ """One observation site on a host: a spec point name, resolved."""
64
+
65
+ name: str
66
+ path: str
67
+ side: str = "input" # "input" | "output"
68
+
69
+ @property
70
+ def key(self) -> tuple[str, str]:
71
+ return (self.path, self.name)
72
+
73
+
74
+ def _child(seam_path: str, attr: str) -> str:
75
+ return f"{seam_path}.{attr}" if attr else seam_path
76
+
77
+
78
+ def resolve(seam: Any, spec_points: Sequence[str]) -> list[Point]:
79
+ """Locate this seam's spec-declared points on its host.
80
+
81
+ Raises if the structure declares a point this module cannot place, so
82
+ a spec that grows a point is a loud failure rather than a silently
83
+ uncalibrated seam.
84
+ """
85
+ structure = seam.structure
86
+ placed: dict[str, Point] = {}
87
+
88
+ def put(name: str, attr: str = "", side: str = "input") -> None:
89
+ placed[name] = Point(name, _child(seam.path, attr), side)
90
+
91
+ if structure == "decoder_ffn":
92
+ put("x_after_norm")
93
+ # the gated activation is the down projection's input; measuring it
94
+ # there is what removes the need to keep the seam's input around
95
+ # and recompute gate/up over it
96
+ put("act_after_mul", "down_proj")
97
+ elif structure == "vision_ffn":
98
+ put("x_after_norm")
99
+ put("hidden_after_act", (seam.fc_attrs or ("fc1", "fc2"))[1])
100
+ elif structure == "qkv_pack":
101
+ # siblings share one input; the first one the host calls sees it
102
+ put("x", (seam.pack_attrs or ("q_proj",))[0])
103
+ elif structure in ("linear_proj", "patch_projection", "norm_fused"):
104
+ put("x")
105
+ elif structure == "adaln_producer":
106
+ # ``cond`` is not an amax point: the step table is captured content,
107
+ # not a statistic, and is collected by the conditioning hook
108
+ put("x")
109
+ elif (structure == "modnorm_qkv_chain"
110
+ and seam.variant.get("modulation") == "per_token_table"):
111
+ # the table form owns the whole block, so the four static scales
112
+ # its composition needs are measured at the block's own sublayer
113
+ # inputs — the same real-distribution sites the sublayers would
114
+ # calibrate at if bound individually
115
+ put("attn_in", "attn1.to_q")
116
+ put("o_in", "attn1.to_out.0")
117
+ put("ffn_in", "ffn")
118
+ put("ffn_hid", "ffn.net.2")
119
+ elif structure in ("decoder_block", "modnorm_qkv_chain"):
120
+ pass # composed; its sublayers carry the points
121
+
122
+ unplaced = [p for p in spec_points if p not in placed
123
+ and not (structure == "adaln_producer" and p == "cond")
124
+ and not (structure == "attention_core")
125
+ and not (structure == "cadence_static")]
126
+ if unplaced:
127
+ raise ValueError(
128
+ f"{structure}: spec declares calibration point(s) {unplaced} "
129
+ "that cannot be located on a host — either the spec grew a "
130
+ "point or this seam's slots were not discovered")
131
+ return [placed[p] for p in spec_points if p in placed]
132
+
133
+
134
+ @dataclass
135
+ class Collector:
136
+ """Per-sample amax vectors for a set of points, house-shaped.
137
+
138
+ ``sample_amax`` is reduced with a max while one sample runs, snapshot
139
+ into ``per_sample`` when it ends, and reduced across samples by the
140
+ house percentile. ``rows`` and ``dtypes`` ride along because they are
141
+ the other two things a bind needs from the same forward, and they are
142
+ observations of one scalar each rather than statistics.
143
+ """
144
+
145
+ points: list[Point] = field(default_factory=list)
146
+ per_sample: list[Any] = field(default_factory=list)
147
+ keys: list[tuple[str, str]] | None = None
148
+ _cur: dict[tuple[str, str], torch.Tensor] = field(default_factory=dict)
149
+ rows: dict[tuple[str, str], list[int]] = field(default_factory=dict)
150
+ widths: dict[tuple[str, str], int] = field(default_factory=dict)
151
+ dtypes: dict[tuple[str, str], set] = field(default_factory=dict)
152
+ final: dict[tuple[str, str], float] = field(default_factory=dict)
153
+ #: per-point extra statistic requests, keyed ``"path|name"`` — objects
154
+ #: with ``.stat`` / ``.granularity`` (a scheme's ``PointStat``). The
155
+ #: scalar amax is always collected regardless (it keeps the per-sample
156
+ #: vectors aligned and costs one float); a channel request adds a
157
+ #: per-channel track beside it, it does not replace it.
158
+ request: dict[str, Any] = field(default_factory=dict)
159
+ _cur_chan: dict[tuple[str, str], torch.Tensor] = field(
160
+ default_factory=dict)
161
+ _cur_sm: dict[tuple[str, str], tuple] = field(default_factory=dict)
162
+ chan_samples: dict[tuple[str, str], list] = field(default_factory=dict)
163
+ sm_samples: dict[tuple[str, str], list] = field(default_factory=dict)
164
+ chan_final: dict[tuple[str, str], Any] = field(default_factory=dict)
165
+ sm_final: dict[tuple[str, str], Any] = field(default_factory=dict)
166
+
167
+ # ---- capture -----------------------------------------------------
168
+
169
+ def _record(self, point: Point, x: torch.Tensor) -> None:
170
+ if not torch.is_tensor(x):
171
+ return
172
+ key = point.key
173
+ req = self.request.get(f"{point.path}|{point.name}")
174
+ if req is not None and getattr(req, "granularity", None) == "channel":
175
+ flat = x.detach().float().reshape(-1, x.shape[-1])
176
+ if req.stat == "amax":
177
+ chan = flat.abs().amax(dim=0)
178
+ prev = self._cur_chan.get(key)
179
+ self._cur_chan[key] = (chan if prev is None
180
+ else torch.maximum(prev, chan))
181
+ elif req.stat == "second_moment":
182
+ # per-sample (sum of squares, token count) so the
183
+ # per-sample values survive to the reduction, same
184
+ # discipline as the amax vectors
185
+ sq = (flat * flat).sum(dim=0)
186
+ prev = self._cur_sm.get(key)
187
+ self._cur_sm[key] = ((sq, flat.shape[0]) if prev is None
188
+ else (prev[0] + sq,
189
+ prev[1] + flat.shape[0]))
190
+ amax = x.detach().float().abs().max()
191
+ prev = self._cur.get(key)
192
+ self._cur[key] = amax if prev is None else torch.maximum(prev, amax)
193
+ self.rows.setdefault(key, []).append(
194
+ int(x.numel() // x.shape[-1]) if x.ndim else 1)
195
+ if x.ndim:
196
+ self.widths.setdefault(key, int(x.shape[-1]))
197
+ self.dtypes.setdefault(key, set()).add(x.dtype)
198
+
199
+ def hooks(self, resolve_module: Callable[[str], torch.nn.Module]) -> list:
200
+ """Install one hook per point; caller removes them."""
201
+ handles = []
202
+ for point in self.points:
203
+ target = resolve_module(point.path)
204
+ if point.side == "output":
205
+ handles.append(target.register_forward_hook(
206
+ lambda m, a, out, p=point: self._record(p, out)))
207
+ else:
208
+ handles.append(target.register_forward_pre_hook(
209
+ lambda m, a, p=point: self._record(
210
+ p, a[0] if a else None)))
211
+ return handles
212
+
213
+ def end_sample(self) -> None:
214
+ """Freeze this sample's amax into one vector, house ordering."""
215
+ import numpy as np
216
+
217
+ if self.keys is None:
218
+ self.keys = sorted(self._cur)
219
+ missing = [k for k in self.keys if k not in self._cur]
220
+ if missing:
221
+ raise ValueError(
222
+ f"calibration sample reached {len(self._cur)} of "
223
+ f"{len(self.keys)} points; {missing[:3]} were not called. "
224
+ "The host took a different path on this sample, so the "
225
+ "per-sample vectors cannot be aligned")
226
+ self.per_sample.append(
227
+ np.array([float(self._cur[k]) for k in self.keys],
228
+ dtype=np.float32))
229
+ self._cur = {}
230
+ for k, chan in self._cur_chan.items():
231
+ self.chan_samples.setdefault(k, []).append(
232
+ chan.cpu().numpy().astype(np.float32))
233
+ self._cur_chan = {}
234
+ for k, (sq, n) in self._cur_sm.items():
235
+ self.sm_samples.setdefault(k, []).append(
236
+ (sq.cpu().numpy().astype(np.float64), int(n)))
237
+ self._cur_sm = {}
238
+
239
+ # ---- reduce ------------------------------------------------------
240
+
241
+ def reduce(self, percentile: float, *, verbose: bool = False,
242
+ label: str = "structures") -> dict[str, Any]:
243
+ """Percentile-reduce across samples using the house helpers."""
244
+ from flash_rt.core.calibration import (accumulate_amax,
245
+ check_scale_ceiling,
246
+ format_summary,
247
+ summarize_amax_dispersion)
248
+
249
+ if not self.per_sample:
250
+ return {"points": 0, "samples": 0}
251
+ final = accumulate_amax(self.per_sample, percentile=percentile)
252
+ self.final = {k: float(v) for k, v in zip(self.keys or [], final)}
253
+ # channel amax reduces with the same house helper, per point —
254
+ # it is elementwise over vectors, the vector is just per-channel
255
+ # instead of per-point
256
+ for k, samples in self.chan_samples.items():
257
+ self.chan_final[k] = accumulate_amax(samples,
258
+ percentile=percentile)
259
+ # a second moment is a mean, not an amax: combine the per-sample
260
+ # (sum, count) pairs. Per-sample values are kept so a robust
261
+ # cross-sample reduction stays possible later.
262
+ for k, samples in self.sm_samples.items():
263
+ total = sum(s for s, _ in samples)
264
+ count = sum(n for _, n in samples)
265
+ self.sm_final[k] = total / max(count, 1)
266
+ note: dict[str, Any] = {
267
+ "points": len(self.final),
268
+ "samples": len(self.per_sample),
269
+ "percentile": percentile,
270
+ "method": ("single_frame" if len(self.per_sample) == 1
271
+ else "percentile"),
272
+ }
273
+ if len(self.per_sample) > 1:
274
+ summary = summarize_amax_dispersion(self.per_sample, final)
275
+ note["dispersion"] = summary
276
+ if verbose:
277
+ print(f"[structures] {format_summary(summary)}", flush=True)
278
+ # the house diagnostic, on the same scales the impls will bake in
279
+ offenders = check_scale_ceiling(
280
+ {f"{p}|{n}": v / 448.0 for (p, n), v in self.final.items()},
281
+ label=label)
282
+ if offenders:
283
+ note["scale_ceiling_offenders"] = [n for n, _ in offenders]
284
+ return note
285
+
286
+ # ---- what a bind asks for ---------------------------------------
287
+
288
+ def amax(self, path: str, name: str) -> float | None:
289
+ return self.final.get((path, name))
290
+
291
+ def channel_amax(self, path: str, name: str):
292
+ """Per-channel amax vector for a point that requested it."""
293
+ return self.chan_final.get((path, name))
294
+
295
+ def second_moment(self, path: str, name: str):
296
+ """Per-channel E[x^2] for a point that requested it (imatrix
297
+ statistic: what each input column contributes, on real data)."""
298
+ return self.sm_final.get((path, name))
299
+
300
+ def scale(self, path: str, name: str, *,
301
+ fp8_max: float = 448.0) -> float | None:
302
+ """The FP8 per-tensor scale for one point, house formula."""
303
+ value = self.final.get((path, name))
304
+ return None if value is None else max(value / fp8_max, 1e-8)
305
+
306
+ def row_profile(self, path: str, name: str) -> list[int]:
307
+ return sorted(self.rows.get((path, name), []))
308
+
309
+ def seen_dtypes(self, path: str, name: str) -> set:
310
+ return self.dtypes.get((path, name), set())
311
+
312
+ def width(self, path: str, name: str) -> int | None:
313
+ """The last-dim this point was observed at — a shape, not a stat."""
314
+ return self.widths.get((path, name))
315
+
316
+
317
+ def measure(run_once: Callable[[], Any], points: Sequence[Point],
318
+ resolve_module: Callable[[str], torch.nn.Module], *,
319
+ samples: int = 1, percentile: float = 99.9,
320
+ verbose: bool = False,
321
+ label: str = "structures") -> tuple[Collector, dict[str, Any]]:
322
+ """Run the host ``samples`` times under point hooks and reduce.
323
+
324
+ The one calibration pass, shaped like every other one in this repo:
325
+ hooks on, one forward per sample with the per-sample vector snapshot
326
+ at the end of each, hooks off in a ``finally`` so a raising thunk
327
+ cannot leave them on the model.
328
+ """
329
+ collector = Collector(points=list(points))
330
+ handles = collector.hooks(resolve_module)
331
+ try:
332
+ with torch.no_grad():
333
+ for _ in range(max(1, samples)):
334
+ run_once()
335
+ collector.end_sample()
336
+ finally:
337
+ for handle in handles:
338
+ handle.remove()
339
+ return collector, collector.reduce(percentile, verbose=verbose,
340
+ label=label)
341
+
342
+
343
+ def precision_spec(collector: Collector, note: dict[str, Any]):
344
+ """Package the reduced scales as the house ``ModelPrecisionSpec``.
345
+
346
+ The receipt format is the repo's, so a structures attachment answers
347
+ ``precision_spec`` the same way a frontend does — same fields, same
348
+ ``calibration_method`` vocabulary, same introspection.
349
+ """
350
+ import numpy as np
351
+ from flash_rt.core.precision_spec import ModelPrecisionSpec, PrecisionSpec
352
+
353
+ method = note.get("method")
354
+ samples = note.get("samples")
355
+ pct = note.get("percentile") if method == "percentile" else None
356
+ specs = {
357
+ f"{path}|{name}": PrecisionSpec(
358
+ dtype="fp8_e4m3", granularity="per_tensor", scheme="symmetric",
359
+ scale_source="calibration",
360
+ scale=np.array([max(value / 448.0, 1e-8)], dtype=np.float32),
361
+ calibration_method=method, calibration_samples=samples,
362
+ calibration_percentile=pct)
363
+ for (path, name), value in collector.final.items()
364
+ }
365
+ # these are activation scales measured at GEMM inputs; weight scales are
366
+ # derived at bind time from the weights themselves and belong to the
367
+ # other bucket, which this layer does not populate
368
+ return ModelPrecisionSpec(activation_specs=specs, source="calibration")
@@ -0,0 +1,131 @@
1
+ """Adopting pre-quantized checkpoints: packed weights in, structures out.
2
+
3
+ The scheme door (``auto_swaps(scheme=...)``) quantizes at runtime from a
4
+ full-precision host — whatever the checkpoint holds, statistics are
5
+ measured and formats are chosen here. This module is the other door: a
6
+ checkpoint that is *already* quantized, in someone else's packed layout,
7
+ adopted by converting each packed projection into a structure impl that
8
+ executes it.
9
+
10
+ The first supported layout is compressed-tensors NVFP4
11
+ (``quant_method="compressed-tensors"``, ``format="nvfp4-pack-quantized"``,
12
+ the ``run_compressed`` load path where every quantized projection is an
13
+ ``nn.Linear`` carrying ``weight_packed``/``weight_scale`` parameters).
14
+ The upstream execution path decompresses those weights to BF16 inside
15
+ ``forward``, which multiplies the footprint back to full precision —
16
+ the 27B host that motivated this door OOMs a 32 GB card exactly that
17
+ way. Adoption converts each packed projection to the Hub NVFP4 layout
18
+ once, at load time, and the model runs in its quantized footprint.
19
+
20
+ Decompression semantics stay upstream's: the layer is unpacked by the
21
+ compressor registered for the checkpoint's own format, never by a
22
+ reimplementation here. Conversion into the Hub layout is a regrid with
23
+ a measurable relative error against those decompressed values; it is
24
+ recorded per layer in the returned report, because "the tokens still
25
+ match" is a claim about a specific conversion loss, not the absence of
26
+ one. Lossless direct transcoding between the two grids is the recorded
27
+ follow-up, not silently assumed.
28
+
29
+ Adoption is a load-time transform, not an attachment: the packed source
30
+ modules cannot execute and holding decompressed copies of every layer
31
+ would defeat the footprint this door exists for. There is no
32
+ ``detach()`` — undoing an adoption is reloading the checkpoint.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from dataclasses import dataclass, field
38
+
39
+ import torch
40
+
41
+ __all__ = ["AdoptionReport", "adopt_prequantized"]
42
+
43
+ _FORMATS = ("ct_nvfp4",)
44
+
45
+
46
+ @dataclass
47
+ class AdoptionReport:
48
+ """What one adoption did, per layer, for the receipt."""
49
+
50
+ fmt: str
51
+ replaced: list[str] = field(default_factory=list)
52
+ conversion_rel_l2: dict[str, float] = field(default_factory=dict)
53
+
54
+ @property
55
+ def worst_conversion(self) -> float:
56
+ return max(self.conversion_rel_l2.values(), default=0.0)
57
+
58
+ def summary(self) -> dict:
59
+ vals = sorted(self.conversion_rel_l2.values())
60
+ return {
61
+ "format": self.fmt,
62
+ "replaced_projections": len(self.replaced),
63
+ "conversion_rel_l2_median": (
64
+ round(vals[len(vals) // 2], 5) if vals else None),
65
+ "conversion_rel_l2_max": (
66
+ round(vals[-1], 5) if vals else None),
67
+ }
68
+
69
+
70
+ def _is_ct_nvfp4_linear(module: torch.nn.Module) -> bool:
71
+ """The ``run_compressed`` load form: a plain Linear that carries the
72
+ packed parameters alongside its (meta or absent) dense weight."""
73
+ return isinstance(module, torch.nn.Linear) and hasattr(
74
+ module, "weight_packed")
75
+
76
+
77
+ @torch.no_grad()
78
+ def adopt_prequantized(model: torch.nn.Module, fmt: str = "ct_nvfp4",
79
+ *, verbose: bool = False) -> AdoptionReport:
80
+ """Convert every packed projection of ``model`` to a structure impl.
81
+
82
+ ``model`` is expected CPU-resident, straight from its loader:
83
+ conversion streams each layer through the GPU one at a time, so the
84
+ packed checkpoint plus one decompressed layer is the peak footprint.
85
+ Move the model to the device *after* adopting.
86
+ """
87
+ if fmt not in _FORMATS:
88
+ raise ValueError(
89
+ f"unknown pre-quantized format {fmt!r}; supported: "
90
+ f"{', '.join(_FORMATS)}")
91
+ try:
92
+ from compressed_tensors.compressors import BaseCompressor
93
+ except ImportError as exc:
94
+ raise ImportError(
95
+ "adopting a compressed-tensors checkpoint requires the "
96
+ "compressed-tensors package (the checkpoint's own "
97
+ "decompression semantics); install it rather than "
98
+ "reimplementing the unpack") from exc
99
+
100
+ from .impls.linear_proj import nvfp4_dynamic
101
+
102
+ compressor = BaseCompressor.load_from_registry("nvfp4-pack-quantized")
103
+ report = AdoptionReport(fmt=fmt)
104
+ for name, module in list(model.named_modules()):
105
+ for child_name, child in list(module.named_children()):
106
+ if not _is_ct_nvfp4_linear(child):
107
+ continue
108
+ path = f"{name}.{child_name}" if name else child_name
109
+ # the registered compressor either mutates the layer's dense
110
+ # weight in place or returns the tensor; take whichever form
111
+ # this version speaks
112
+ ret = compressor.decompress_module(child)
113
+ w = ret if torch.is_tensor(ret) else child.weight.detach()
114
+ bias = getattr(child, "bias", None)
115
+ bound, rel = nvfp4_dynamic.bind_proj_seam(
116
+ {"w": w, "b": None if bias is None else bias.detach()})
117
+ setattr(module, child_name, bound)
118
+ report.replaced.append(path)
119
+ report.conversion_rel_l2[path] = rel
120
+ del w
121
+ if verbose:
122
+ print(f"[prequantized] {path}: relL2={rel:.4f}",
123
+ flush=True)
124
+ if not report.replaced:
125
+ raise ValueError(
126
+ "no packed projections found: this model does not look like "
127
+ f"a {fmt} checkpoint loaded in its compressed form")
128
+ torch.cuda.empty_cache()
129
+ if verbose:
130
+ print(f"[prequantized] {report.summary()}", flush=True)
131
+ return report