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,490 @@
1
+ """Quantisation schemes: how statistics become per-seam decisions.
2
+
3
+ A scheme owns exactly two questions and nothing else:
4
+
5
+ 1. **What statistic does each calibration point need?**
6
+ (:meth:`QuantScheme.statistics`) — amax for FP8-style static scales, a
7
+ per-channel second moment for imatrix-style weight quantisation, or
8
+ ``None`` for formats that quantise dynamically in-kernel and need no
9
+ calibration at that point (this repo's NVFP4 activation path computes
10
+ per-block scale factors at runtime). The statistic *discipline* is not
11
+ the scheme's to change: per-sample reduction then a cross-sample
12
+ percentile, one vector held per sample, never activations.
13
+
14
+ 2. **Given the reduced statistics, what happens at each seam?**
15
+ (:meth:`QuantScheme.decide`) — bind with these values, or keep the
16
+ host module ("this layer stays at host precision" is a decision, not
17
+ a failure).
18
+
19
+ What a scheme does **not** own: bytes. Scale-factor memory layouts,
20
+ sub-normal handling in packed formats, kernel selection, M-dispatch
21
+ tables — all execution detail, owned by the impl variant that consumes
22
+ the decision. The same decision can be executed by different kernels;
23
+ that boundary is what keeps schemes portable across backends.
24
+
25
+ Schemes are registered by name and selected at the door::
26
+
27
+ structures.auto_swaps(model, forward, scheme="fp8_static")
28
+
29
+ Registering a scheme adds no calibration entry point: the calibration
30
+ axis (``forward`` / ``samples``) is fixed, and a scheme only declares
31
+ what to measure along it and consumes the result.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import statistics as _stats
37
+ from dataclasses import dataclass, field
38
+ from typing import Mapping, Sequence
39
+
40
+ __all__ = ["PointStat", "Decision", "QuantScheme", "Fp8Static",
41
+ "NoQuant", "Bf16Structural", "W8A16Decode", "W4A16Decode",
42
+ "Nvfp4Awq", "Nvfp4Balance",
43
+ "register", "get", "names", "resolve_auto", "validate_request"]
44
+
45
+ #: statistics the collector can currently execute. Granularities other
46
+ #: than per-tensor (per-channel, per-block16) are part of the declared
47
+ #: interface — NVFP4 weight scale factors are per-16-block, imatrix is
48
+ #: per-channel — but the collector does not measure them yet, so a
49
+ #: scheme requesting one fails loudly at plan time instead of silently
50
+ #: getting per-tensor numbers with the wrong shape.
51
+ _EXECUTABLE = {("amax", "tensor"), (None, "tensor"),
52
+ ("amax", "channel"), ("second_moment", "channel")}
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class PointStat:
57
+ """What one calibration point should measure.
58
+
59
+ ``stat`` is ``"amax"`` (this repo's static-scale statistic),
60
+ ``"second_moment"``, ``"histogram"``, or ``None`` — ``None`` means
61
+ the format quantises this point dynamically at runtime and wants no
62
+ calibration data at all. ``granularity`` is ``"tensor"``,
63
+ ``"channel"`` or ``"block16"``.
64
+ """
65
+
66
+ stat: str | None = "amax"
67
+ granularity: str = "tensor"
68
+
69
+
70
+ @dataclass
71
+ class Decision:
72
+ """What :meth:`QuantScheme.decide` hands back.
73
+
74
+ ``keep_host`` lists seam paths that stay on the host module at host
75
+ precision — a first-class outcome, recorded in the plan notes, not a
76
+ refusal. ``reasons`` says why, per path, so the receipt can print it.
77
+ ``formats`` routes a seam to a named impl variant instead of the
78
+ structure's default (``"w8a16_static"`` on a ``decoder_ffn`` seam
79
+ binds the weight-only path). A seam routed to a non-default format
80
+ is excluded from FP8 seam negotiation — a chain shares one scale and
81
+ one wire dtype, and a member in another format has neither. An
82
+ unknown format fails loudly at bind time.
83
+
84
+ ``params`` carries per-seam recipe parameters for the routed format
85
+ (an algorithm's ``alpha``, clamp bounds, recipe name). They are
86
+ *values of the decision*, handed to the impl at bind and recorded in
87
+ the receipt — never read from environment variables, and never the
88
+ bytes: how a parameterised algorithm is executed stays with the
89
+ impl.
90
+ """
91
+
92
+ keep_host: tuple[str, ...] = ()
93
+ reasons: Mapping[str, str] = field(default_factory=dict)
94
+ formats: Mapping[str, str] = field(default_factory=dict)
95
+ params: Mapping[str, Mapping[str, object]] = field(default_factory=dict)
96
+
97
+
98
+ class QuantScheme:
99
+ """Base scheme: amax everywhere, bind everything.
100
+
101
+ Subclass and override the two methods; do not add entry points.
102
+ """
103
+
104
+ name = "base"
105
+
106
+ #: Optional format for an MTP draft head's expert bank / projections
107
+ #: (``decode_loop.enable_mtp`` consumes this vocabulary). ``None``
108
+ #: keeps the draft weights BF16 — the conservative arm. The draft
109
+ #: answers to acceptance length alone: the verify pass anchors the
110
+ #: output stream, so both measured arms (``"bf16"``,
111
+ #: ``"nvfp4_dynamic"`` — AL-equal on the record) are quality-safe,
112
+ #: and the choice trades memory for nothing else. The draft's
113
+ #: private W8 head view is part of the member's fixed form, not a
114
+ #: scheme decision: the model's own head stays on the step/verify
115
+ #: numeric family in every scheme.
116
+ mtp_projection_format: str | None = None
117
+
118
+ #: Optional format for the gated-delta layer's packed projections.
119
+ #: The fused-layer adapter consults this; ``None`` keeps them at
120
+ #: host precision. This is a scheme attribute, not an impl default —
121
+ #: quantising those projections is a precision decision.
122
+ gdn_projection_format: str | None = None
123
+
124
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
125
+ """Per point key (``"path|name"``): what to measure there."""
126
+ return {f"{p.path}|{p.name}": PointStat() for p in points}
127
+
128
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
129
+ """``report`` is per seam path: its points' reduced statistics."""
130
+ return Decision()
131
+
132
+
133
+ class Fp8Static(QuantScheme):
134
+ """The default: static per-tensor FP8, exactly the shipped behaviour.
135
+
136
+ ``keep_outliers`` turns the house scale-ceiling diagnostic into a
137
+ decision: seams owning a point whose reduced amax sits more than
138
+ ``keep_outliers`` times above the median of all points stay at host
139
+ precision. The criterion is the one ``check_scale_ceiling`` already
140
+ warns with (20.0 there); this consumes it instead of only saying it.
141
+ ``None`` (the default) keeps nothing and binds identically to the
142
+ behaviour before schemes existed.
143
+ """
144
+
145
+ name = "fp8_static"
146
+
147
+ def __init__(self, keep_outliers: float | None = None) -> None:
148
+ self.keep_outliers = keep_outliers
149
+
150
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
151
+ if not self.keep_outliers or not report:
152
+ return Decision()
153
+ values = [v for pts in report.values() for v in pts.values()
154
+ if v is not None and v > 0]
155
+ if not values:
156
+ return Decision()
157
+ median = _stats.median(values)
158
+ keep, reasons = [], {}
159
+ for seam_path, pts in report.items():
160
+ worst = max(((k, v) for k, v in pts.items() if v is not None),
161
+ key=lambda kv: kv[1], default=None)
162
+ if worst is not None and worst[1] > self.keep_outliers * median:
163
+ keep.append(seam_path)
164
+ reasons[seam_path] = (
165
+ f"{worst[0]} amax {worst[1]:.4g} > "
166
+ f"{self.keep_outliers:g}x median {median:.4g}; "
167
+ f"kept at host precision")
168
+ return Decision(keep_host=tuple(keep), reasons=reasons)
169
+
170
+
171
+ class W8A16Decode(QuantScheme):
172
+ """Weight-only INT8, activations untouched — the decode-band recipe.
173
+
174
+ Needs no calibration data at all (quantisation is per-output-channel
175
+ on weights, done at bind time), so every point declares ``None``.
176
+ Routes ``decoder_ffn`` seams to the ``w8a16_static`` impl, whose own
177
+ M-dispatch sends decode shapes to the kernel and prefill back to the
178
+ host, and ``linear_proj`` seams (the attention Q/K/V/O family) to
179
+ its projection twin under the same band contract. Other structures
180
+ stay at host precision: this scheme is the decode recipe, not a
181
+ whole-host FP8 replacement.
182
+
183
+ A ``decoder_ffn`` seam is recognised by the point its spec declares
184
+ (``act_after_mul`` — the gated activation), which is
185
+ backend-independent by construction; a ``linear_proj`` seam by the
186
+ structure name the report entry carries.
187
+ """
188
+
189
+ name = "w8a16_decode"
190
+ _format = "w8a16_static"
191
+ #: format for linear_proj seams, or None to keep them at host
192
+ #: precision (the 4-bit twin: its linear auto band is too narrow to
193
+ #: route blind — M in [1, 2] with strict N/K limits — so it stays
194
+ #: host until a measured table says otherwise)
195
+ _linear_format: str | None = "w8a16_static"
196
+
197
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
198
+ return {f"{p.path}|{p.name}": PointStat(None) for p in points}
199
+
200
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
201
+ formats, keep = {}, []
202
+ for seam_path, pts in report.items():
203
+ if any(k.endswith("|act_after_mul") for k in pts):
204
+ formats[seam_path] = self._format
205
+ elif (self._linear_format is not None
206
+ and getattr(pts, "structure", None) == "linear_proj"):
207
+ formats[seam_path] = self._linear_format
208
+ else:
209
+ keep.append(seam_path)
210
+ return Decision(keep_host=tuple(keep),
211
+ reasons={p: f"{self.name} binds decode-band "
212
+ f"GEMM seams only"
213
+ for p in keep},
214
+ formats=formats)
215
+
216
+
217
+ class W4A16Decode(W8A16Decode):
218
+ """Weight-only NVFP4 (E2M1 packed + block scale factors) twin of
219
+ :class:`W8A16Decode` — same decode band, same M-dispatch, half the
220
+ weight bytes. The ``flashrt/weight-only-ffn`` package quantises
221
+ weights per 16-element block at bind time; activations stay BF16,
222
+ so like the INT8 twin it needs no calibration data.
223
+ """
224
+
225
+ name = "w4a16_decode"
226
+ _format = "w4a16_static"
227
+ _linear_format = None
228
+
229
+
230
+ class Nvfp4Awq(QuantScheme):
231
+ """NVFP4 with activation-aware per-input-channel balance.
232
+
233
+ Requests per-channel amax at every calibration point (the collector
234
+ measures it; :func:`validate_request` admits it), and routes
235
+ ``decoder_ffn`` seams to the ``nvfp4_awq`` impl variant with the
236
+ recipe parameters as the decision's payload. The impl consumes the
237
+ channel statistics and calls the one shared algorithm
238
+ (``flash_rt.core.quantization.fit_input_channel_balance``) at bind;
239
+ this scheme owns the *decision*, not the fold and not the bytes.
240
+
241
+ ``recipe="balance"`` is the production formula validated on Pi0.5
242
+ (Thor FP4) and Motus video FFN — activation-only channel balance.
243
+ ``"smoothquant"`` names the legacy activation/weight ratio and is
244
+ accepted for experiments; there is no environment-variable fork.
245
+ Other structures stay at host precision until their NVFP4 variants
246
+ land — extending the routing is a change to this method, in one
247
+ place.
248
+ """
249
+
250
+ name = "nvfp4_awq"
251
+
252
+ def __init__(self, alpha: float = 0.5,
253
+ clamp: tuple[float, float] = (0.25, 4.0),
254
+ recipe: str = "balance") -> None:
255
+ if recipe not in ("balance", "smoothquant"):
256
+ raise ValueError(f"unknown recipe {recipe!r}; "
257
+ f"known: balance, smoothquant")
258
+ self.alpha = float(alpha)
259
+ self.clamp = (float(clamp[0]), float(clamp[1]))
260
+ self.recipe = recipe
261
+
262
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
263
+ return {f"{p.path}|{p.name}": PointStat("amax", "channel")
264
+ for p in points}
265
+
266
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
267
+ formats, params, keep = {}, {}, []
268
+ payload = {"alpha": self.alpha, "clamp": list(self.clamp),
269
+ "recipe": self.recipe}
270
+ for seam_path, pts in report.items():
271
+ if any(k.endswith("|act_after_mul") for k in pts):
272
+ formats[seam_path] = "nvfp4_awq"
273
+ params[seam_path] = dict(payload)
274
+ else:
275
+ keep.append(seam_path)
276
+ return Decision(keep_host=tuple(keep),
277
+ reasons={p: "nvfp4_awq routes decoder_ffn only "
278
+ "in this version"
279
+ for p in keep},
280
+ formats=formats, params=params)
281
+
282
+
283
+ class Nvfp4Balance(QuantScheme):
284
+ """NVFP4 W4A4 with activation-only channel balance at every GEMM.
285
+
286
+ The recorded W4 chain recipe as a scheme decision: projection and
287
+ FFN seams (``qkv_pack`` / ``vision_ffn`` / ``linear_proj``) route to
288
+ their ``nvfp4_balance`` impl variants — weights folded with the
289
+ balance fitted on calibrated per-channel amax, then packed to NVFP4;
290
+ activations quantized dynamically per call with per-block scale
291
+ factors. The channel statistic feeds the balance, never a scale, so
292
+ nothing static exists to drift across a schedule. Everything else
293
+ stays at host precision: this is the half-weight-bytes showcase
294
+ band, not a whole-host replacement.
295
+ """
296
+
297
+ name = "nvfp4_balance"
298
+
299
+ def __init__(self, alpha: float = 0.5,
300
+ clamp: tuple[float, float] = (0.25, 4.0),
301
+ fuse_ffn_wire: bool = False) -> None:
302
+ self.alpha = float(alpha)
303
+ self.clamp = (float(clamp[0]), float(clamp[1]))
304
+ # the FFN's FP4-wire chain (GEMM emits bias+GELU re-quantized,
305
+ # the second GEMM consumes it) drops fc2's input-side balance —
306
+ # a numerics change, so it is a scheme decision the receipt
307
+ # records, never a silent flip on symbol presence
308
+ self.fuse_ffn_wire = bool(fuse_ffn_wire)
309
+ if fuse_ffn_wire:
310
+ self.name = "nvfp4_balance_wire"
311
+
312
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
313
+ return {f"{p.path}|{p.name}": PointStat("amax", "channel")
314
+ for p in points}
315
+
316
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
317
+ formats, params, keep = {}, {}, []
318
+ payload = {"alpha": self.alpha, "clamp": list(self.clamp)}
319
+ for seam_path, pts in report.items():
320
+ if getattr(pts, "structure", None) in (
321
+ "qkv_pack", "vision_ffn", "linear_proj"):
322
+ formats[seam_path] = "nvfp4_balance"
323
+ params[seam_path] = dict(payload)
324
+ if (self.fuse_ffn_wire
325
+ and pts.structure == "vision_ffn"):
326
+ params[seam_path]["fuse_wire"] = True
327
+ else:
328
+ keep.append(seam_path)
329
+ return Decision(keep_host=tuple(keep),
330
+ reasons={p: "nvfp4_balance binds projection and "
331
+ "FFN GEMM seams only"
332
+ for p in keep},
333
+ formats=formats, params=params)
334
+
335
+
336
+ class NoQuant(QuantScheme):
337
+ """Quantisation off: every quantised seam stays at host precision.
338
+
339
+ This is the explicit off-switch, not a degraded mode. Structures
340
+ that are pure fusion (the attention core, cadence buffers) never
341
+ consult a scheme decision and attach as usual — a BF16/FP16 host
342
+ under this scheme still gets every fusion structure, it just gets
343
+ no quantised GEMMs. Zero calibration, zero kernel dependencies.
344
+ """
345
+
346
+ name = "none"
347
+
348
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
349
+ return {f"{p.path}|{p.name}": PointStat(None) for p in points}
350
+
351
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
352
+ keep = tuple(report)
353
+ return Decision(keep_host=keep,
354
+ reasons={p: "quantisation off (scheme 'none')"
355
+ for p in keep})
356
+
357
+
358
+ class W4A4Decode(NoQuant):
359
+ """Mixed decode band for hosts built around gated-delta layers.
360
+
361
+ Two decisions on top of the ``none`` scheme, both decode-band only.
362
+ The fused gated-delta layer's packed input projection and output
363
+ projection — the bandwidth-dominant GEMVs — go through the dynamic
364
+ NVFP4 path (weights packed at bind time, activations quantised per
365
+ call). The attention/head ``linear_proj`` seams go to the INT8
366
+ weight-only band instead: their output feeds attention scores and
367
+ logits, where the denser grid is the right conservatism. Prefill
368
+ dispatches back to the host either way, and everything else stays
369
+ at host precision.
370
+ """
371
+
372
+ name = "w4a4_decode"
373
+ gdn_projection_format = "nvfp4_dynamic"
374
+ _linear_format: str | None = "w8a16_static"
375
+ #: one-way arm: after the FP4 band binds (and only then), the
376
+ #: layer's BF16 projection weights are released — ~11GB on the 27B
377
+ #: host, trading exact detach for the headroom a draft head and a
378
+ #: W8 lm_head need to coexist. Never a default; the receipt says so.
379
+ gdn_release_host_weights = False
380
+
381
+ def __init__(self, release_host_weights: bool = False) -> None:
382
+ if release_host_weights:
383
+ self.gdn_release_host_weights = True
384
+ self.name = "w4a4_decode_release"
385
+
386
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
387
+ formats, keep = {}, []
388
+ for seam_path, pts in report.items():
389
+ if (self._linear_format is not None
390
+ and getattr(pts, "structure", None) == "linear_proj"):
391
+ formats[seam_path] = self._linear_format
392
+ else:
393
+ keep.append(seam_path)
394
+ return Decision(keep_host=tuple(keep),
395
+ reasons={p: f"{self.name} binds decode-band "
396
+ f"GEMM seams only"
397
+ for p in keep},
398
+ formats=formats)
399
+
400
+
401
+ class Bf16Structural(QuantScheme):
402
+ """No quantisation; retain only BF16 structural fusions."""
403
+
404
+ name = "bf16_structural"
405
+
406
+ def statistics(self, points: Sequence) -> dict[str, PointStat]:
407
+ return {f"{p.path}|{p.name}": PointStat(None) for p in points}
408
+
409
+ def decide(self, report: Mapping[str, Mapping[str, float]]) -> Decision:
410
+ formats, keep, reasons = {}, [], {}
411
+ for seam_path, pts in report.items():
412
+ structure = getattr(pts, "structure", None)
413
+ if structure == "qkv_pack":
414
+ formats[seam_path] = "bf16_pack"
415
+ elif structure in ("decoder_ffn", "vision_ffn", "linear_proj"):
416
+ keep.append(seam_path)
417
+ reasons[seam_path] = "bf16_structural introduces no quantisation"
418
+ return Decision(keep_host=tuple(keep), reasons=reasons,
419
+ formats=formats)
420
+
421
+
422
+ _REGISTRY: dict[str, QuantScheme] = {}
423
+
424
+
425
+ def register(name: str, scheme: QuantScheme) -> None:
426
+ """Register a scheme instance under ``name`` (last write wins)."""
427
+ _REGISTRY[name] = scheme
428
+
429
+
430
+ def get(name: str) -> QuantScheme:
431
+ try:
432
+ return _REGISTRY[name]
433
+ except KeyError:
434
+ raise KeyError(f"unknown quantisation scheme {name!r}; "
435
+ f"registered: {sorted(_REGISTRY)}") from None
436
+
437
+
438
+ def names() -> tuple[str, ...]:
439
+ return tuple(sorted(_REGISTRY))
440
+
441
+
442
+ def validate_request(request: Mapping[str, PointStat]) -> None:
443
+ """Refuse loudly what the collector cannot measure yet.
444
+
445
+ A scheme asking for a per-block or per-channel statistic must not
446
+ silently receive per-tensor numbers — wrong-shaped scales bind and
447
+ run, and the error surfaces as accuracy nobody can trace. The wall
448
+ stays until the collector grows that granularity.
449
+ """
450
+ bad = {key: ps for key, ps in request.items()
451
+ if (ps.stat, ps.granularity) not in _EXECUTABLE}
452
+ if bad:
453
+ k, ps = next(iter(bad.items()))
454
+ raise NotImplementedError(
455
+ f"scheme requests ({ps.stat}, {ps.granularity}) at {k} "
456
+ f"(and {len(bad) - 1} more point(s)); the collector currently "
457
+ f"measures only per-tensor amax. Extending it is the "
458
+ f"supported path — do not fall back to per-tensor silently.")
459
+
460
+
461
+ def resolve_auto() -> str:
462
+ """Resolve the ``"auto"`` profile: highest performance this device
463
+ can execute, from the registered names.
464
+
465
+ FP8-capable hardware (SM >= 89) gets ``fp8_static`` — bit-identical
466
+ to the behaviour before ``auto`` existed. Anything else gets
467
+ ``none``: fusion structures still attach, quantised seams stay at
468
+ host precision, and the receipt records why. The resolution table is
469
+ deliberately one function so a future profile that measures faster
470
+ (an FP4 mix, say) is promoted by editing exactly one line.
471
+ """
472
+ try:
473
+ from flash_rt.core.utils.hardware import supports_fp8
474
+ fp8 = bool(supports_fp8())
475
+ except Exception:
476
+ fp8 = False
477
+ return "fp8_static" if fp8 else "none"
478
+
479
+
480
+ register("fp8_static", Fp8Static())
481
+ register("fp8_static_keep_outliers", Fp8Static(keep_outliers=20.0))
482
+ register("w8a16_decode", W8A16Decode())
483
+ register("w4a16_decode", W4A16Decode())
484
+ register("w4a4_decode", W4A4Decode())
485
+ register("w4a4_decode_release", W4A4Decode(release_host_weights=True))
486
+ register("none", NoQuant())
487
+ register("bf16_structural", Bf16Structural())
488
+ register("nvfp4_awq", Nvfp4Awq())
489
+ register("nvfp4_balance", Nvfp4Balance())
490
+ register("nvfp4_balance_wire", Nvfp4Balance(fuse_ffn_wire=True))