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