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