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,131 @@
|
|
|
1
|
+
"""NVFP4 (W4A4) ``linear_proj`` with per-input-channel balance.
|
|
2
|
+
|
|
3
|
+
The W4 recipe as a formal impl variant: the weight is folded with the
|
|
4
|
+
activation-only channel balance fitted on calibrated per-channel amax
|
|
5
|
+
(``fit_input_channel_balance`` — the production formula), packed to
|
|
6
|
+
NVFP4 (E2M1 data plus per-16-element-block scale factors), and the
|
|
7
|
+
activation is quantized dynamically per call after the inverse fold.
|
|
8
|
+
The fold is exact (``x' @ W'.T == x @ W.T``) and only then is either
|
|
9
|
+
side quantized, so the balance costs no arithmetic identity — it moves
|
|
10
|
+
quantization error out of the hot channels.
|
|
11
|
+
|
|
12
|
+
Activations carry dynamic per-block scales computed per call, so there
|
|
13
|
+
is no static activation scale to drift across a denoise schedule; the
|
|
14
|
+
per-channel amax calibration feeds the *balance*, not a scale.
|
|
15
|
+
|
|
16
|
+
Unlike :mod:`.nvfp4_dynamic` (the adoption path for checkpoints that
|
|
17
|
+
are already packed and have no host form to return to), this variant
|
|
18
|
+
retains the host module whole: a call outside the calibrated form runs
|
|
19
|
+
it, and detach restores it bit-exact.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Mapping
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
from ...guard import CAST_OK, PROCEED, GuardedSeam
|
|
29
|
+
from .nvfp4_dynamic import KERNEL_DEP, _check, _kernel # noqa: F401
|
|
30
|
+
|
|
31
|
+
_VARIANT = 2 # the qualified GEMM dispatch across the served shapes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LinearProjNvfp4Balance(GuardedSeam, torch.nn.Module):
|
|
35
|
+
"""Balanced-fold projection: dynamic FP4 quantize + FP4 GEMM."""
|
|
36
|
+
|
|
37
|
+
_frt_host_attr = "host_linear"
|
|
38
|
+
_frt_can_fallback = True
|
|
39
|
+
|
|
40
|
+
def __init__(self, w_packed, w_sfb, inv_s, bias, n, k,
|
|
41
|
+
original: torch.nn.Module | None = None):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.register_buffer("_w_packed", w_packed)
|
|
44
|
+
self.register_buffer("_w_sfb", w_sfb)
|
|
45
|
+
self.register_buffer("_inv_s", inv_s)
|
|
46
|
+
if bias is not None:
|
|
47
|
+
self.register_buffer("_bias", bias)
|
|
48
|
+
else:
|
|
49
|
+
self._bias = None
|
|
50
|
+
self._n = n
|
|
51
|
+
kern = _kernel()
|
|
52
|
+
self._kern = kern
|
|
53
|
+
self._gemm = kern.fp4_w4a16_linear_bf16
|
|
54
|
+
# capability probe: the fused-bias epilogue entry, where the
|
|
55
|
+
# installed package variant ships it — absence is the ordinary
|
|
56
|
+
# two-launch path, never a refusal
|
|
57
|
+
self._gemm_bias = getattr(kern, "nvfp4_gemm_bias_bf16", None)
|
|
58
|
+
if original is not None:
|
|
59
|
+
self.host_linear = original
|
|
60
|
+
self._frt_arm(dtypes=CAST_OK, device=w_packed.device, k=int(k))
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name):
|
|
63
|
+
try:
|
|
64
|
+
return super().__getattr__(name)
|
|
65
|
+
except AttributeError:
|
|
66
|
+
if name == "host_linear":
|
|
67
|
+
raise
|
|
68
|
+
return getattr(super().__getattr__("host_linear"), name)
|
|
69
|
+
|
|
70
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
71
|
+
admitted = self._frt_admit(x)
|
|
72
|
+
if admitted is not PROCEED:
|
|
73
|
+
return admitted
|
|
74
|
+
shape = x.shape
|
|
75
|
+
flat = (x.reshape(-1, shape[-1]).to(torch.float16)
|
|
76
|
+
* self._inv_s).contiguous()
|
|
77
|
+
a_packed, a_sfa = self._kern.quantize_fp4_sfa_fp16(flat)
|
|
78
|
+
if self._bias is not None and self._gemm_bias is not None:
|
|
79
|
+
y = self._gemm_bias(a_packed, self._w_packed, a_sfa,
|
|
80
|
+
self._w_sfb, self._bias)
|
|
81
|
+
else:
|
|
82
|
+
y = self._gemm(a_packed, self._w_packed, a_sfa, self._w_sfb,
|
|
83
|
+
variant=_VARIANT)
|
|
84
|
+
if self._bias is not None:
|
|
85
|
+
y = y + self._bias
|
|
86
|
+
return y.reshape(*shape[:-1], self._n).type_as(x)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@torch.no_grad()
|
|
90
|
+
def bind_proj_seam(
|
|
91
|
+
weights: Mapping[str, torch.Tensor],
|
|
92
|
+
*,
|
|
93
|
+
channel_amax,
|
|
94
|
+
original: torch.nn.Module | None = None,
|
|
95
|
+
alpha: float = 0.5,
|
|
96
|
+
clamp=(0.25, 4.0),
|
|
97
|
+
) -> LinearProjNvfp4Balance:
|
|
98
|
+
"""Bind one projection from a dense ``[N, K]`` weight.
|
|
99
|
+
|
|
100
|
+
``channel_amax`` is the calibrated per-input-channel amax vector at
|
|
101
|
+
this projection's input (``[K]``) — it parameterises the balance
|
|
102
|
+
fold, never a scale. ``alpha``/``clamp`` are the decision's recipe
|
|
103
|
+
payload.
|
|
104
|
+
"""
|
|
105
|
+
from flash_rt.core.quantization import fit_input_channel_balance
|
|
106
|
+
|
|
107
|
+
n, k = _check(weights)
|
|
108
|
+
kern = _kernel()
|
|
109
|
+
w = weights["w"].detach()
|
|
110
|
+
amax = torch.as_tensor(channel_amax, device=w.device,
|
|
111
|
+
dtype=torch.float32)
|
|
112
|
+
w_bal, inv_s = fit_input_channel_balance(
|
|
113
|
+
w.float(), amax, alpha=alpha,
|
|
114
|
+
clamp=(float(clamp[0]), float(clamp[1])),
|
|
115
|
+
out_dtype=torch.float32)
|
|
116
|
+
w_packed, w_sfb = kern.quantize_fp4_sfa_fp16(
|
|
117
|
+
w_bal.to("cuda", torch.float16).contiguous(), is_sfb=True)
|
|
118
|
+
bias = weights.get("b")
|
|
119
|
+
if bias is not None:
|
|
120
|
+
bias = bias.detach().to("cuda", torch.bfloat16)
|
|
121
|
+
bound = LinearProjNvfp4Balance(
|
|
122
|
+
w_packed, w_sfb, inv_s.to("cuda", torch.float16), bias, n, k,
|
|
123
|
+
original=original)
|
|
124
|
+
probe = bound(torch.zeros(1, k, device=w_packed.device,
|
|
125
|
+
dtype=torch.bfloat16))
|
|
126
|
+
if probe.shape != (1, n) or not torch.isfinite(probe).all():
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"refused: nvfp4_balance bind smoke produced shape "
|
|
129
|
+
f"{tuple(probe.shape)}, "
|
|
130
|
+
f"finite={bool(torch.isfinite(probe).all())}")
|
|
131
|
+
return bound
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""NVFP4 (W4A4, dynamic activation scales) ``linear_proj`` implementation.
|
|
2
|
+
|
|
3
|
+
Weights are packed to NVFP4 (E2M1 data plus per-16-element-block scale
|
|
4
|
+
factors) at bind time; activations are quantized to the same format at
|
|
5
|
+
runtime, per call, with dynamically computed block scales — no
|
|
6
|
+
calibration data at either end. This is the execution form behind the
|
|
7
|
+
27B enablement line: checkpoints whose upstream loader decompresses
|
|
8
|
+
4-bit weights to BF16 inside ``forward`` (and therefore cannot fit the
|
|
9
|
+
card) run on the same card once their projections consume the packed
|
|
10
|
+
layout directly.
|
|
11
|
+
|
|
12
|
+
The ``flashrt/fp4-gemm`` entry point ``fp4_w4a16_linear_bf16`` takes the
|
|
13
|
+
pre-quantized activation tensor plus its scale factors — despite the
|
|
14
|
+
``a16`` in its historical name, the GEMM it runs is W4A4. ``variant=2``
|
|
15
|
+
is the qualified dispatch across the decode and short-prefill shapes
|
|
16
|
+
this impl serves (same-token 1.0000 against an exact reference on the
|
|
17
|
+
27B host, decode and prefill both through this path).
|
|
18
|
+
|
|
19
|
+
There is no host fallback: the module this replaces holds packed
|
|
20
|
+
weights the host cannot execute. The guard therefore refuses instead of
|
|
21
|
+
falling back, and adoption of a whole checkpoint is a load-time
|
|
22
|
+
transform, not a reversible attachment.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
from functools import lru_cache
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from ...guard import CAST_OK, PROCEED, GuardedSeam
|
|
33
|
+
|
|
34
|
+
KERNEL_DEP = {
|
|
35
|
+
"provider": "huggingface_kernels",
|
|
36
|
+
"repo": "flashrt/fp4-gemm",
|
|
37
|
+
"version": ">=1",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#: mirrors the kernel's own shape checks (``torch_binding.cpp``: K
|
|
41
|
+
#: divisible by 16 for the per-block scale factors, positive dims) —
|
|
42
|
+
#: no invented size walls: the adoption path serves whatever the
|
|
43
|
+
#: checkpoint author packed, and the 27B host's 17408-wide FFN is a
|
|
44
|
+
#: qualified shape, not an edge case
|
|
45
|
+
SUPPORT = {
|
|
46
|
+
"K": {"min": 16, "multiple_of": 16},
|
|
47
|
+
"N": {"min": 1},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@lru_cache(maxsize=1)
|
|
52
|
+
def _kernel():
|
|
53
|
+
from flashrt_structures.impls import hub_kernel
|
|
54
|
+
|
|
55
|
+
return hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _check(weights: Mapping[str, torch.Tensor]) -> tuple[int, int]:
|
|
59
|
+
w = weights["w"]
|
|
60
|
+
if w.dim() != 2:
|
|
61
|
+
raise ValueError(f"w must be [N, K], got {tuple(w.shape)}")
|
|
62
|
+
n, k = w.shape
|
|
63
|
+
for name, dim in (("K", k), ("N", n)):
|
|
64
|
+
bounds = SUPPORT[name]
|
|
65
|
+
if dim < bounds["min"]:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{name}={dim} outside support envelope "
|
|
68
|
+
f"(min {bounds['min']})")
|
|
69
|
+
if bounds.get("multiple_of") and dim % bounds["multiple_of"]:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"{name}={dim} must be a multiple of "
|
|
72
|
+
f"{bounds['multiple_of']}")
|
|
73
|
+
b = weights.get("b")
|
|
74
|
+
if b is not None and tuple(b.shape) != (n,):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"bias shape {tuple(b.shape)} does not match N={n}")
|
|
77
|
+
return n, k
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _quantize_activation(kern, flat: torch.Tensor):
|
|
81
|
+
"""Use the direct BF16 producer when the installed artifact carries it."""
|
|
82
|
+
if flat.dtype is torch.bfloat16:
|
|
83
|
+
direct = getattr(kern, "quantize_fp4_sfa_bf16", None)
|
|
84
|
+
if direct is not None:
|
|
85
|
+
return direct(flat.contiguous())
|
|
86
|
+
return kern.quantize_fp4_sfa_fp16(
|
|
87
|
+
flat.to(torch.float16).contiguous())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LinearProjNvfp4Dynamic(GuardedSeam, torch.nn.Module):
|
|
91
|
+
"""Packed-weight projection: FP4 GEMM with runtime activation scales."""
|
|
92
|
+
|
|
93
|
+
_frt_can_fallback = False
|
|
94
|
+
|
|
95
|
+
def __init__(self, w_packed, w_sfb, bias, n, k):
|
|
96
|
+
super().__init__()
|
|
97
|
+
self.register_buffer("_w_packed", w_packed)
|
|
98
|
+
self.register_buffer("_w_sfb", w_sfb)
|
|
99
|
+
self._bias = bias
|
|
100
|
+
self._n = n
|
|
101
|
+
self._k = k
|
|
102
|
+
kern = _kernel()
|
|
103
|
+
self._kern = kern
|
|
104
|
+
self._gemm = kern.fp4_w4a16_linear_bf16
|
|
105
|
+
# M=1 decode rows route to the warp-split GEMV where the build
|
|
106
|
+
# carries it and the shape qualifies (its own contract: N%8,
|
|
107
|
+
# K a multiple of 64*warps). Absence is not a refusal - the
|
|
108
|
+
# tiled GEMM serves every shape correctly, the GEMV just fills
|
|
109
|
+
# the SMs it underfills at long-K decode shapes.
|
|
110
|
+
# The entry's presence in the build is not its qualification to
|
|
111
|
+
# run: the aarch64 package carries it and the kernel refuses at
|
|
112
|
+
# call time on anything below SM120, which surfaces as a runtime
|
|
113
|
+
# error on the first M=1 row rather than as a choice made here.
|
|
114
|
+
# The engine adapters already withhold it off SM120; deciding it
|
|
115
|
+
# once, where the arm is selected, means an impl bound directly -
|
|
116
|
+
# through structures.get(), or any hand assembly - behaves the
|
|
117
|
+
# same as one bound through a door.
|
|
118
|
+
gemv = getattr(kern, "fp4_w4a4_gemv_warpsplit_bf16", None)
|
|
119
|
+
cc = (torch.cuda.get_device_capability(w_packed.device)
|
|
120
|
+
if w_packed.is_cuda else (0, 0))
|
|
121
|
+
self._gemv = (gemv if gemv is not None and cc >= (12, 0)
|
|
122
|
+
and n % 8 == 0 and k % (64 * 4) == 0 else None)
|
|
123
|
+
self._frt_arm(dtypes=CAST_OK, device=w_packed.device, k=int(k))
|
|
124
|
+
|
|
125
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
126
|
+
admitted = self._frt_admit(x)
|
|
127
|
+
if admitted is not PROCEED:
|
|
128
|
+
return admitted
|
|
129
|
+
shape = x.shape
|
|
130
|
+
flat = x.reshape(-1, shape[-1])
|
|
131
|
+
a_packed, a_sfa = _quantize_activation(self._kern, flat)
|
|
132
|
+
if flat.shape[0] == 1 and self._gemv is not None:
|
|
133
|
+
y = self._gemv(a_packed, self._w_packed, a_sfa, self._w_sfb)
|
|
134
|
+
else:
|
|
135
|
+
y = self._gemm(a_packed, self._w_packed, a_sfa, self._w_sfb,
|
|
136
|
+
variant=2)
|
|
137
|
+
if self._bias is not None:
|
|
138
|
+
y = y + self._bias
|
|
139
|
+
return y.reshape(*shape[:-1], self._n).type_as(x)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@torch.no_grad()
|
|
143
|
+
def bind_proj_seam(
|
|
144
|
+
weights: Mapping[str, torch.Tensor],
|
|
145
|
+
) -> tuple[LinearProjNvfp4Dynamic, float]:
|
|
146
|
+
"""Bind one projection from a dense ``[N, K]`` weight.
|
|
147
|
+
|
|
148
|
+
The weight is packed to the Hub kernel's NVFP4 layout on the GPU;
|
|
149
|
+
the returned float is the pack-and-unpack relative L2 against the
|
|
150
|
+
input weight — the *conversion* cost of regridding into this layout,
|
|
151
|
+
reported so a caller adopting a whole checkpoint can put it in the
|
|
152
|
+
receipt instead of losing it.
|
|
153
|
+
"""
|
|
154
|
+
n, k = _check(weights)
|
|
155
|
+
kern = _kernel()
|
|
156
|
+
w = weights["w"].to("cuda", torch.float16).contiguous()
|
|
157
|
+
w_packed, w_sfb = kern.quantize_fp4_sfa_fp16(w, is_sfb=True)
|
|
158
|
+
# the conversion check accumulates in row slabs: a whole-tensor
|
|
159
|
+
# FP32 dequant doubles the bind's transient footprint, and on
|
|
160
|
+
# head-class weights that spike is what fails under a tight budget
|
|
161
|
+
deq = kern.dequantize_fp4_sfa_fp16(w_packed, w_sfb)
|
|
162
|
+
num_sq = den_sq = 0.0
|
|
163
|
+
for i in range(0, n, 4096):
|
|
164
|
+
diff = deq[i:i + 4096].float() - w[i:i + 4096].float()
|
|
165
|
+
num_sq += float(diff.square().sum())
|
|
166
|
+
den_sq += float(w[i:i + 4096].float().square().sum())
|
|
167
|
+
del deq
|
|
168
|
+
rel = (num_sq ** 0.5) / max(den_sq ** 0.5, 1e-12)
|
|
169
|
+
bias = weights.get("b")
|
|
170
|
+
if bias is not None:
|
|
171
|
+
bias = bias.detach().to("cuda", torch.bfloat16)
|
|
172
|
+
bound = LinearProjNvfp4Dynamic(w_packed, w_sfb, bias, n, k)
|
|
173
|
+
# bind-time smoke: one M=1 launch through the real entry point before
|
|
174
|
+
# the seam is handed out — a stale build or missing symbol surfaces
|
|
175
|
+
# as a clean bind refusal, not later inside the host's forward
|
|
176
|
+
probe = bound(torch.zeros(1, k, device=w_packed.device,
|
|
177
|
+
dtype=torch.bfloat16))
|
|
178
|
+
if probe.shape != (1, n) or not torch.isfinite(probe).all():
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"refused: nvfp4 bind smoke produced shape "
|
|
181
|
+
f"{tuple(probe.shape)}, finite={bool(torch.isfinite(probe).all())}")
|
|
182
|
+
return bound, rel
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Weight-only INT8 implementation of the ``linear_proj`` structure.
|
|
2
|
+
|
|
3
|
+
The decode-band twin of the FP8 projection impl: weights are quantized
|
|
4
|
+
per output channel to INT8 at bind time, activations stay BF16, so the
|
|
5
|
+
seam needs no calibration data at all. This is the projection-shaped
|
|
6
|
+
slice of the recipe already shipped for ``decoder_ffn`` —
|
|
7
|
+
``w8a16_static`` there covers the gated MLP, this file covers the
|
|
8
|
+
attention Q/K/V/O family and any other single projection the discovery
|
|
9
|
+
qualifies.
|
|
10
|
+
|
|
11
|
+
The ``flashrt/weight-only-ffn`` package's linear entry point qualifies
|
|
12
|
+
its auto dispatch narrowly, and this impl mirrors that table exactly
|
|
13
|
+
rather than stretching it (``torch_binding.cpp``: ``check_variant`` and
|
|
14
|
+
``w8_auto_linear_supported``):
|
|
15
|
+
|
|
16
|
+
- M in [1, 4] — the decode band; and
|
|
17
|
+
- K <= 1024 always qualifies; K <= 4096 needs N >= 1024; larger K needs
|
|
18
|
+
N >= 1024 for M <= 2 and N >= 2048 for M in {3, 4}.
|
|
19
|
+
|
|
20
|
+
Calls outside the band are dispatched to the retained host module by
|
|
21
|
+
declared plan, counted in the ledger — prefill runs the host GEMM,
|
|
22
|
+
decode runs the kernel, and the qualification record states which band
|
|
23
|
+
the kernel serves.
|
|
24
|
+
|
|
25
|
+
The linear entry point carries no bias operand. A projection with a
|
|
26
|
+
bias gets it added in BF16 after the GEMM — one [M<=4, N] elementwise
|
|
27
|
+
add inside the decode band, where the weight read dominates end to end.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Mapping
|
|
33
|
+
from functools import lru_cache
|
|
34
|
+
|
|
35
|
+
import torch
|
|
36
|
+
|
|
37
|
+
from ...guard import CAST_OK, PROCEED, GuardedSeam
|
|
38
|
+
|
|
39
|
+
KERNEL_DEP = {
|
|
40
|
+
"provider": "huggingface_kernels",
|
|
41
|
+
"repo": "flashrt/weight-only-ffn",
|
|
42
|
+
"version": ">=1",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SUPPORT = {
|
|
46
|
+
"K": {"min": 512, "max": 16384},
|
|
47
|
+
"N": {"min": 128, "max": 262144},
|
|
48
|
+
"M": {"min": 1, "max": 4},
|
|
49
|
+
"m_classes": ("micro",),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _qualified(m: int, n: int, k: int) -> bool:
|
|
54
|
+
"""The kernel's own auto-dispatch qualification, mirrored.
|
|
55
|
+
|
|
56
|
+
Copied from ``w8_auto_linear_supported`` plus the ``variant=0``
|
|
57
|
+
M-bound in the package's ``torch_binding.cpp`` — the kernel raises
|
|
58
|
+
outside this table, so the band dispatch must agree with it, not
|
|
59
|
+
rediscover it as runtime errors.
|
|
60
|
+
"""
|
|
61
|
+
if not SUPPORT["M"]["min"] <= m <= SUPPORT["M"]["max"]:
|
|
62
|
+
return False
|
|
63
|
+
if k <= 1024:
|
|
64
|
+
return True
|
|
65
|
+
if k <= 4096:
|
|
66
|
+
return n >= 1024
|
|
67
|
+
return n >= (1024 if m <= 2 else 2048)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@lru_cache(maxsize=1)
|
|
71
|
+
def _kernel():
|
|
72
|
+
from flashrt_structures.impls import hub_kernel
|
|
73
|
+
|
|
74
|
+
return hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _check(weights: Mapping[str, torch.Tensor]) -> tuple[int, int]:
|
|
78
|
+
w = weights["w"]
|
|
79
|
+
if w.dim() != 2:
|
|
80
|
+
raise ValueError(f"w must be [N, K], got {tuple(w.shape)}")
|
|
81
|
+
n, k = w.shape
|
|
82
|
+
for name, dim in (("K", k), ("N", n)):
|
|
83
|
+
bounds = SUPPORT[name]
|
|
84
|
+
if not bounds["min"] <= dim <= bounds["max"]:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"{name}={dim} outside support envelope "
|
|
87
|
+
f"[{bounds['min']}, {bounds['max']}]")
|
|
88
|
+
b = weights.get("b")
|
|
89
|
+
if b is not None and tuple(b.shape) != (n,):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"bias shape {tuple(b.shape)} does not match N={n}")
|
|
92
|
+
return n, k
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BoundLinearProjW8A16:
|
|
96
|
+
"""Projection callable: x[M, K] in, y[M, N] out (BF16)."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, linear_fn, w_q, w_scale, bias, n, k):
|
|
99
|
+
self._linear = linear_fn
|
|
100
|
+
self._w_q = w_q
|
|
101
|
+
self._w_scale = w_scale
|
|
102
|
+
self._bias = bias
|
|
103
|
+
self._n = n
|
|
104
|
+
self._k = k
|
|
105
|
+
|
|
106
|
+
def project(self, x: torch.Tensor) -> torch.Tensor:
|
|
107
|
+
shape = x.shape
|
|
108
|
+
flat = x.reshape(-1, shape[-1])
|
|
109
|
+
m = flat.shape[0]
|
|
110
|
+
if not _qualified(m, self._n, self._k):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"M={m} outside the W8A16 auto-dispatch qualification "
|
|
113
|
+
f"for N={self._n}, K={self._k} (decode band M in "
|
|
114
|
+
f"[1, {SUPPORT['M']['max']}])")
|
|
115
|
+
y = self._linear(flat.to(torch.bfloat16).contiguous(),
|
|
116
|
+
self._w_q, self._w_scale)
|
|
117
|
+
if self._bias is not None:
|
|
118
|
+
y = y + self._bias
|
|
119
|
+
return y.reshape(*shape[:-1], self._n).to(x.dtype)
|
|
120
|
+
|
|
121
|
+
__call__ = project
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class LinearProjW8A16(GuardedSeam, torch.nn.Module):
|
|
125
|
+
"""Drop-in projection module with declared M-dispatch.
|
|
126
|
+
|
|
127
|
+
The weight-only kernel covers the decode band; calls with larger M
|
|
128
|
+
(prefill) are dispatched to the retained host module. This is part
|
|
129
|
+
of the declared plan — per-M dispatch on the real workload — not a
|
|
130
|
+
fallback: both paths are first-class, and the ledger counts the
|
|
131
|
+
dispatch so neither path's share of the calls is ever unknown.
|
|
132
|
+
|
|
133
|
+
``original`` is retained whole and attribute lookups fall through to
|
|
134
|
+
it, so host code that introspects ``weight``/``in_features`` keeps
|
|
135
|
+
working.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
_frt_host_attr = "host_linear"
|
|
139
|
+
_frt_can_fallback = True
|
|
140
|
+
|
|
141
|
+
def __init__(self, bound: BoundLinearProjW8A16,
|
|
142
|
+
original: torch.nn.Module | None = None):
|
|
143
|
+
super().__init__()
|
|
144
|
+
self._bound = bound
|
|
145
|
+
# the same tensors, reachable through *module* attributes: an
|
|
146
|
+
# exporter attributes a tensor by its access path, and a tensor
|
|
147
|
+
# reached only through a plain object gets lifted as an
|
|
148
|
+
# anonymous immutable constant — unnameable in a
|
|
149
|
+
# weights-external package. Identity is unchanged.
|
|
150
|
+
self.register_buffer("_frt_w_q", bound._w_q)
|
|
151
|
+
self.register_buffer("_frt_w_scale", bound._w_scale)
|
|
152
|
+
if bound._bias is not None:
|
|
153
|
+
self.register_buffer("_frt_bias", bound._bias)
|
|
154
|
+
else:
|
|
155
|
+
self._frt_bias = None
|
|
156
|
+
if original is not None:
|
|
157
|
+
self.host_linear = original
|
|
158
|
+
guard = self._frt_arm(dtypes=CAST_OK,
|
|
159
|
+
device=bound._w_q.device,
|
|
160
|
+
k=int(bound._k))
|
|
161
|
+
guard.notes["dispatched_by_band"] = 0
|
|
162
|
+
|
|
163
|
+
def __getattr__(self, name):
|
|
164
|
+
try:
|
|
165
|
+
return super().__getattr__(name)
|
|
166
|
+
except AttributeError:
|
|
167
|
+
if name == "host_linear":
|
|
168
|
+
raise
|
|
169
|
+
return getattr(super().__getattr__("host_linear"), name)
|
|
170
|
+
|
|
171
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
172
|
+
admitted = self._frt_admit(x)
|
|
173
|
+
if admitted is not PROCEED:
|
|
174
|
+
return admitted
|
|
175
|
+
m = x.numel() // x.shape[-1]
|
|
176
|
+
if not _qualified(m, self._bound._n, self._bound._k):
|
|
177
|
+
host = self._frt_host()
|
|
178
|
+
if host is not None:
|
|
179
|
+
guard = self._frt_guard
|
|
180
|
+
if guard is not None and not torch.compiler.is_compiling():
|
|
181
|
+
guard.notes["dispatched_by_band"] += 1
|
|
182
|
+
return host(x)
|
|
183
|
+
return self._bound.project(x) # states the refusal
|
|
184
|
+
shape = x.shape
|
|
185
|
+
flat = x.reshape(-1, shape[-1])
|
|
186
|
+
y = self._bound._linear(flat.to(torch.bfloat16).contiguous(),
|
|
187
|
+
self._frt_w_q, self._frt_w_scale)
|
|
188
|
+
if self._frt_bias is not None:
|
|
189
|
+
y = y + self._frt_bias
|
|
190
|
+
return y.reshape(*shape[:-1], self._bound._n).to(x.dtype)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@torch.no_grad()
|
|
194
|
+
def bind_proj_seam(
|
|
195
|
+
weights: Mapping[str, torch.Tensor],
|
|
196
|
+
*,
|
|
197
|
+
original: torch.nn.Module | None = None,
|
|
198
|
+
) -> LinearProjW8A16:
|
|
199
|
+
"""Bind one projection with weight-only INT8.
|
|
200
|
+
|
|
201
|
+
``weights['w']`` is checkpoint-layout ``[N, K]``, exactly what the
|
|
202
|
+
kernel consumes — no transpose. No calibration data is required:
|
|
203
|
+
quantization is per-output-channel on weights only, and the optional
|
|
204
|
+
``weights['b']`` is kept in BF16.
|
|
205
|
+
"""
|
|
206
|
+
n, k = _check(weights)
|
|
207
|
+
if not _qualified(1, n, k):
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"refused: N={n}, K={k} has no qualified fast path even at "
|
|
210
|
+
f"M=1; the W8A16 projection cannot serve this seam at any M")
|
|
211
|
+
kern = _kernel()
|
|
212
|
+
w = weights["w"].to("cuda", torch.bfloat16).contiguous()
|
|
213
|
+
w_q, w_scale = kern.quantize_w8_weight_bf16(w)
|
|
214
|
+
bias = weights.get("b")
|
|
215
|
+
if bias is not None:
|
|
216
|
+
bias = bias.detach().to("cuda", torch.bfloat16)
|
|
217
|
+
bound = BoundLinearProjW8A16(kern.w8a16_linear_bf16, w_q, w_scale,
|
|
218
|
+
bias, n, k)
|
|
219
|
+
# bind-time smoke: one M=1 launch through the real entry point before
|
|
220
|
+
# the seam is handed out. A stale build or missing symbol must
|
|
221
|
+
# surface here as a clean bind refusal, not later inside the host's
|
|
222
|
+
# forward — identical output cannot catch it there, because the
|
|
223
|
+
# fallback path is numerically exact.
|
|
224
|
+
probe = bound.project(torch.zeros(1, k, device=w_q.device,
|
|
225
|
+
dtype=torch.bfloat16))
|
|
226
|
+
if probe.shape != (1, n) or not torch.isfinite(probe).all():
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"refused: w8a16 bind smoke produced shape "
|
|
229
|
+
f"{tuple(probe.shape)}, finite={bool(torch.isfinite(probe).all())}")
|
|
230
|
+
return LinearProjW8A16(bound, original=original)
|
|
File without changes
|