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,6 @@
|
|
|
1
|
+
from .attn_sublayer import (PackedAttnSublayer, bind_attn_sublayer,
|
|
2
|
+
rotate_half)
|
|
3
|
+
from .fused import FusedDecoderBlock, bind_decoder_block, qualify
|
|
4
|
+
|
|
5
|
+
__all__ = ["FusedDecoderBlock", "PackedAttnSublayer", "bind_attn_sublayer",
|
|
6
|
+
"bind_decoder_block", "qualify", "rotate_half"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The attention sublayer with no layout churn between its parts.
|
|
2
|
+
|
|
3
|
+
Three bound structures already sit inside a host attention module: the
|
|
4
|
+
packed projections, the rotary embedding, the fused attention core. Each
|
|
5
|
+
is faster than what it replaced, and between them the host still pays for
|
|
6
|
+
a layout it does not need. The host lays q/k/v out as ``(B, H, S, D)``
|
|
7
|
+
because that is what eager SDPA wants; the fused kernel wants
|
|
8
|
+
``(B, S, H, D)``, which is exactly what the projections' own output view
|
|
9
|
+
already is. So the host transposes, the rotary embedding runs on the
|
|
10
|
+
transposed layout, and the core transposes back and makes it
|
|
11
|
+
contiguous — two cancelling transposes plus the copies around them, per
|
|
12
|
+
projection, per layer, per step.
|
|
13
|
+
|
|
14
|
+
None of the three seams can see that, because each is bound inside the
|
|
15
|
+
module that owns the layout. The sublayer boundary can: run the packed
|
|
16
|
+
projections, view their output as ``(B, S, H, D)``, apply the rotary
|
|
17
|
+
embedding on that layout (the ``unsqueeze`` axis moves, the arithmetic
|
|
18
|
+
does not), and hand it straight to the kernel.
|
|
19
|
+
|
|
20
|
+
The rotary form is family-specific (half-split against interleaved), so
|
|
21
|
+
it enters as a callable from the host-family adapter; everything else
|
|
22
|
+
here is the generic pre-norm attention sublayer.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
from torch import nn
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
|
32
|
+
"""Half-split rotation (Llama/Gemma/Qwen convention)."""
|
|
33
|
+
half = x.shape[-1] // 2
|
|
34
|
+
return torch.cat((-x[..., half:], x[..., :half]), dim=-1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PackedAttnSublayer(nn.Module):
|
|
38
|
+
"""Packed projections -> rotary -> fused core -> output projection."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, attn: nn.Module, core, *, scale: float,
|
|
41
|
+
rotate=rotate_half, q_heads: int = 0):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.attn = attn
|
|
44
|
+
self.core = core
|
|
45
|
+
self.scale = scale
|
|
46
|
+
self.rotate = rotate
|
|
47
|
+
self.q_heads = q_heads
|
|
48
|
+
|
|
49
|
+
def forward(self, x: torch.Tensor, position_embeddings=None, **kw):
|
|
50
|
+
a = self.attn
|
|
51
|
+
bsz, seq, _ = x.shape
|
|
52
|
+
hd = a.head_dim
|
|
53
|
+
pack = a.q_proj if getattr(a.q_proj, "joint_slots", 0) else None
|
|
54
|
+
if pack is not None:
|
|
55
|
+
# q and k are one contiguous run of the packed output and
|
|
56
|
+
# the rotary embedding is the same arithmetic on both, so it
|
|
57
|
+
# runs once over the pair. Splitting them first would cost a
|
|
58
|
+
# kernel and the copies that separate them.
|
|
59
|
+
qk = pack.joint(x).view(bsz, seq, -1, hd)
|
|
60
|
+
qk = self._rope(qk, position_embeddings)
|
|
61
|
+
q, k = qk[:, :, :self.q_heads], qk[:, :, self.q_heads:]
|
|
62
|
+
v = a.v_proj(x).view(bsz, seq, -1, hd)
|
|
63
|
+
else:
|
|
64
|
+
# the host's own call order is the data dependency the
|
|
65
|
+
# packed projection relies on: the first call runs the GEMM,
|
|
66
|
+
# the others read its stash
|
|
67
|
+
q = a.q_proj(x).view(bsz, seq, -1, hd)
|
|
68
|
+
k = a.k_proj(x).view(bsz, seq, -1, hd)
|
|
69
|
+
v = a.v_proj(x).view(bsz, seq, -1, hd)
|
|
70
|
+
q = self._rope(q, position_embeddings)
|
|
71
|
+
k = self._rope(k, position_embeddings)
|
|
72
|
+
out = self.core.forward_suffix(q, k, v, scale=self.scale)
|
|
73
|
+
return a.o_proj(out.reshape(bsz, seq, -1))
|
|
74
|
+
|
|
75
|
+
def _rope(self, t: torch.Tensor, position_embeddings):
|
|
76
|
+
if position_embeddings is None:
|
|
77
|
+
return t
|
|
78
|
+
cos, sin = position_embeddings
|
|
79
|
+
cos = cos.unsqueeze(2).to(t.dtype)
|
|
80
|
+
sin = sin.unsqueeze(2).to(t.dtype)
|
|
81
|
+
return t * cos + self.rotate(t) * sin
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bind_attn_sublayer(attn: nn.Module, core, *, rotate=rotate_half):
|
|
85
|
+
"""Compose one attention sublayer around an already-bound core.
|
|
86
|
+
|
|
87
|
+
Returns ``None`` rather than raising when the host module or the core
|
|
88
|
+
is missing a part: the block then keeps the host's own attention, so
|
|
89
|
+
this can only add coverage.
|
|
90
|
+
"""
|
|
91
|
+
if core is None or not hasattr(core, "forward_suffix"):
|
|
92
|
+
return None
|
|
93
|
+
for attr in ("q_proj", "k_proj", "v_proj", "o_proj", "head_dim"):
|
|
94
|
+
if not hasattr(attn, attr):
|
|
95
|
+
return None
|
|
96
|
+
# the sublayer's projections produce the new tokens only, so the
|
|
97
|
+
# packed plan's suffix has to be exactly those. A host that carries
|
|
98
|
+
# its own KV cache into attention breaks that equality, and there the
|
|
99
|
+
# host's attention module stays.
|
|
100
|
+
plan = getattr(core, "plan", None)
|
|
101
|
+
if plan is None or plan.suffix_len != getattr(core, "seq_q", -1):
|
|
102
|
+
return None
|
|
103
|
+
scale = getattr(attn, "scaling", None)
|
|
104
|
+
if scale is None:
|
|
105
|
+
scale = getattr(attn, "scale", None)
|
|
106
|
+
if scale is None:
|
|
107
|
+
return None
|
|
108
|
+
q_heads = attn.q_proj.out_features // attn.head_dim
|
|
109
|
+
return PackedAttnSublayer(attn, core, scale=float(scale),
|
|
110
|
+
rotate=rotate, q_heads=q_heads)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""decoder_block — the pre-norm transformer block as one boundary.
|
|
2
|
+
|
|
3
|
+
Every region structure in this library binds inside a block: the norm,
|
|
4
|
+
the packed projections, the attention, the MLP. What none of them can
|
|
5
|
+
see is the *dataflow between them* — and that is where the residual
|
|
6
|
+
adds, the gate broadcasts and the dtype round trips live. A host block
|
|
7
|
+
is a fixed shape:
|
|
8
|
+
|
|
9
|
+
r = h; h, g = norm_in(h, cond); h = attn(h); h = r + h * g
|
|
10
|
+
r = h; h, g = norm_out(h, cond); h = mlp(h); h = r + h * g
|
|
11
|
+
|
|
12
|
+
so the second norm's real input is not a hidden state, it is a pending
|
|
13
|
+
``residual + attn_out * gate``. The adaptive-norm kernel already takes
|
|
14
|
+
exactly that (it computes the gated residual, norms it, modulates and
|
|
15
|
+
quantizes in one pass) — bound at the norm boundary there is nothing to
|
|
16
|
+
hand it, so the residual argument gets zeros, the host keeps its own
|
|
17
|
+
elementwise add, and the fused producer looks like dead weight. It was
|
|
18
|
+
measured as such and refused. The boundary was wrong, not the kernel.
|
|
19
|
+
|
|
20
|
+
Binding the block puts the residual back in the producer's hands. Three
|
|
21
|
+
things follow, none of which is available one seam at a time:
|
|
22
|
+
|
|
23
|
+
- the pending residual add disappears into the producer's kernel;
|
|
24
|
+
- the producer emits FP8, so the MLP takes the FP8 entry and its own
|
|
25
|
+
input quantization goes away;
|
|
26
|
+
- the step lookup is resolved once and shared by both producers instead
|
|
27
|
+
of being recomputed per norm.
|
|
28
|
+
|
|
29
|
+
The block owns composition, not kernels: every slot is a module bound by
|
|
30
|
+
its own structure, and a slot that did not bind keeps the host's child.
|
|
31
|
+
It can therefore only add to what the region structures already do.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import torch
|
|
37
|
+
from torch import nn
|
|
38
|
+
|
|
39
|
+
from ...guard import CAST_OK, PROCEED, GuardedSeam
|
|
40
|
+
|
|
41
|
+
_BLOCK_ATTRS = ("self_attn", "mlp", "input_layernorm",
|
|
42
|
+
"post_attention_layernorm")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parameterised_children(module: nn.Module) -> set[str]:
|
|
46
|
+
return {name for name, child in module.named_children()
|
|
47
|
+
if any(True for _ in child.parameters())}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FusedDecoderBlock(GuardedSeam, nn.Module):
|
|
51
|
+
"""Pre-norm block whose sublayer dataflow runs through the producers.
|
|
52
|
+
|
|
53
|
+
The host block is retained whole, which makes this the widest way back
|
|
54
|
+
in the library: a call outside the calibrated form runs the entire
|
|
55
|
+
original block. That is exact rather than approximate, because the
|
|
56
|
+
block never swapped the host's own children — it holds separately
|
|
57
|
+
bound copies and leaves the host's sublayers where they were.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
_frt_host_attr = "host"
|
|
61
|
+
_frt_can_fallback = True
|
|
62
|
+
|
|
63
|
+
def __init__(self, host: nn.Module, producer_in, producer_out,
|
|
64
|
+
ffn: nn.Module, *, cond_kw: str = "adarms_cond",
|
|
65
|
+
returns_tuple: bool = False, attn=None):
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.host = host
|
|
68
|
+
self.producer_in = producer_in
|
|
69
|
+
self.producer_out = producer_out
|
|
70
|
+
self.ffn = ffn
|
|
71
|
+
self.cond_kw = cond_kw
|
|
72
|
+
self.returns_tuple = returns_tuple
|
|
73
|
+
# an attention sublayer that owns its own layout, or None to keep
|
|
74
|
+
# the host's attention module (which owns the layout itself)
|
|
75
|
+
self.attn = attn
|
|
76
|
+
self.own_attn = attn is not None
|
|
77
|
+
rows, dim = producer_in.resid.shape
|
|
78
|
+
self._frt_arm(dtypes=CAST_OK, device=producer_in.resid.device,
|
|
79
|
+
k=int(dim), rows=int(rows))
|
|
80
|
+
|
|
81
|
+
def forward(self, hidden_states: torch.Tensor, *args, **kwargs):
|
|
82
|
+
# before the conditioning is taken out of kwargs: the host block
|
|
83
|
+
# expects its own signature back if this call has to go to it
|
|
84
|
+
admitted = self._frt_admit(hidden_states, *args, **kwargs)
|
|
85
|
+
if admitted is not PROCEED:
|
|
86
|
+
return admitted
|
|
87
|
+
cond = kwargs.pop(self.cond_kw, None)
|
|
88
|
+
idx = self.producer_in.resolve(cond)
|
|
89
|
+
|
|
90
|
+
y, gate = self.producer_in.produce(hidden_states, idx)
|
|
91
|
+
y = y.reshape(hidden_states.shape)
|
|
92
|
+
if self.own_attn:
|
|
93
|
+
attn_out = self.attn(
|
|
94
|
+
y, position_embeddings=kwargs.get("position_embeddings"))
|
|
95
|
+
else:
|
|
96
|
+
attn_out = self.host.self_attn(y, *args, **kwargs)
|
|
97
|
+
if isinstance(attn_out, tuple):
|
|
98
|
+
attn_out = attn_out[0]
|
|
99
|
+
|
|
100
|
+
# the pending "hidden_states + attn_out * gate" is the second
|
|
101
|
+
# norm's input; the kernel takes it whole
|
|
102
|
+
resid, y, gate = self.producer_out.absorb(
|
|
103
|
+
hidden_states, attn_out, gate, idx)
|
|
104
|
+
|
|
105
|
+
out = resid + self.ffn(y) * gate
|
|
106
|
+
out = out.reshape(hidden_states.shape)
|
|
107
|
+
return (out,) if self.returns_tuple else out
|
|
108
|
+
|
|
109
|
+
def __getattr__(self, name):
|
|
110
|
+
try:
|
|
111
|
+
return super().__getattr__(name)
|
|
112
|
+
except AttributeError:
|
|
113
|
+
return getattr(super().__getattr__("host"), name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def qualify(host: nn.Module) -> None:
|
|
117
|
+
"""Refuse a host block whose dataflow this structure does not model.
|
|
118
|
+
|
|
119
|
+
The check is structural, not behavioural: the four sublayer slots
|
|
120
|
+
must be there and nothing else that carries weights may be, because
|
|
121
|
+
a fifth parameterised child (a second pair of norms, a cross
|
|
122
|
+
attention) is a sublayer this block would silently drop. What the
|
|
123
|
+
forward then does with those slots is adjudicated by the parity gate
|
|
124
|
+
at the tick boundary, which is where this structure's reference is
|
|
125
|
+
declared.
|
|
126
|
+
"""
|
|
127
|
+
missing = [a for a in _BLOCK_ATTRS
|
|
128
|
+
if not isinstance(getattr(host, a, None), nn.Module)]
|
|
129
|
+
if missing:
|
|
130
|
+
raise ValueError(f"decoder_block: host lacks {missing}")
|
|
131
|
+
extra = _parameterised_children(host) - set(_BLOCK_ATTRS)
|
|
132
|
+
if extra:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"decoder_block: host carries sublayers this structure does "
|
|
135
|
+
f"not model ({sorted(extra)}) — keeping the host block")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def bind_decoder_block(host: nn.Module, producer_in, producer_out,
|
|
139
|
+
ffn: nn.Module, *, cond_kw: str = "adarms_cond",
|
|
140
|
+
returns_tuple: bool = False,
|
|
141
|
+
attn=None) -> FusedDecoderBlock:
|
|
142
|
+
"""Compose bound sublayer structures into one block.
|
|
143
|
+
|
|
144
|
+
``producer_in`` / ``producer_out`` are bound ``adaln_producer``
|
|
145
|
+
modules on the same conditioning stream (they must share a locator
|
|
146
|
+
for the step lookup to be shared); ``ffn`` is a bound
|
|
147
|
+
``decoder_ffn`` on the FP8 entry, which is what makes the producer's
|
|
148
|
+
quantize load-bearing.
|
|
149
|
+
"""
|
|
150
|
+
qualify(host)
|
|
151
|
+
for name, prod in (("producer_in", producer_in),
|
|
152
|
+
("producer_out", producer_out)):
|
|
153
|
+
if not hasattr(prod, "resolve"):
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"decoder_block: {name} is not an adaln_producer")
|
|
156
|
+
if not producer_out.can_absorb:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"decoder_block: the second producer cannot absorb a residual "
|
|
159
|
+
"(it is not the rms form with fp8 output) — without that "
|
|
160
|
+
"fold the block boundary buys nothing over the region seams")
|
|
161
|
+
if producer_in.locator is not producer_out.locator:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
"decoder_block: the two producers do not share a step "
|
|
164
|
+
"locator, so the lookup would still run twice")
|
|
165
|
+
return FusedDecoderBlock(host, producer_in, producer_out, ffn,
|
|
166
|
+
cond_kw=cond_kw,
|
|
167
|
+
returns_tuple=returns_tuple, attn=attn)
|
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""FP8-static implementation of the ``decoder_ffn`` structure.
|
|
2
|
+
|
|
3
|
+
Composes the fused FP8 gate/up -> activation -> down block from the
|
|
4
|
+
``flashrt/flashrt-fp8-swiglu-ffn`` Hub kernel behind the structure
|
|
5
|
+
boundary. Two bind entrypoints share the packing and calibration code:
|
|
6
|
+
``bind`` covers the full structure (norm and AdaLN modulation run in
|
|
7
|
+
torch ahead of the fused block); ``bind_mlp_seam`` covers the
|
|
8
|
+
normed-input -> ffn-output slice for hosts whose replaceable module
|
|
9
|
+
boundary is the MLP. Activation scales are static per-tensor,
|
|
10
|
+
calibrated from caller-provided representative inputs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from typing import Callable, Mapping, Sequence
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
from ...guard import CAST_OK, FP8_ONLY, PROCEED, GuardedSeam
|
|
22
|
+
|
|
23
|
+
KERNEL_DEP = {
|
|
24
|
+
"provider": "hf",
|
|
25
|
+
"repo": "flashrt/flashrt-fp8-swiglu-ffn",
|
|
26
|
+
"version": ">=1",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_FP8 = torch.float8_e4m3fn
|
|
30
|
+
_FP8_MAX = 448.0
|
|
31
|
+
_ENTRYPOINTS = {"gelu": "bf16_fp8_geglu_mlp_bf16",
|
|
32
|
+
"silu": "bf16_fp8_swiglu_mlp_bf16"}
|
|
33
|
+
# fp8 entry: the upstream producer already emitted fp8 with the shared
|
|
34
|
+
# activation scale, so the kernel's own input quantization is dead work.
|
|
35
|
+
# Same math, one less kernel per call.
|
|
36
|
+
_ENTRYPOINTS_FP8 = {"gelu": "fp8_geglu_mlp_bf16",
|
|
37
|
+
"silu": "fp8_swiglu_mlp_bf16"}
|
|
38
|
+
|
|
39
|
+
SUPPORT = {
|
|
40
|
+
"D": {"min": 512, "max": 16384},
|
|
41
|
+
"F": {"min": 1024, "max": 16384},
|
|
42
|
+
"m_classes": ("micro", "small", "medium"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@lru_cache(maxsize=1)
|
|
47
|
+
def _kernel():
|
|
48
|
+
from flashrt_structures.impls import hub_kernel
|
|
49
|
+
|
|
50
|
+
return hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _activation(variant: Mapping[str, str]) -> tuple[str, Callable]:
|
|
54
|
+
name = variant.get("activation", "gelu")
|
|
55
|
+
if name not in _ENTRYPOINTS:
|
|
56
|
+
raise ValueError(f"unsupported activation: {name!r}")
|
|
57
|
+
if name == "gelu":
|
|
58
|
+
return name, lambda t: torch.nn.functional.gelu(t, approximate="tanh")
|
|
59
|
+
return name, torch.nn.functional.silu
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _amax_scale(tensor: torch.Tensor) -> torch.Tensor:
|
|
63
|
+
return (tensor.float().abs().max() / _FP8_MAX).clamp(min=1e-8)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _quantize(tensor: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
|
67
|
+
return (tensor.float() / scale).clamp(-_FP8_MAX, _FP8_MAX).to(_FP8)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize(
|
|
71
|
+
x: torch.Tensor,
|
|
72
|
+
w_norm: torch.Tensor,
|
|
73
|
+
mode: str,
|
|
74
|
+
cond_scale: torch.Tensor | None,
|
|
75
|
+
cond_shift: torch.Tensor | None,
|
|
76
|
+
eps: float,
|
|
77
|
+
) -> torch.Tensor:
|
|
78
|
+
h = x.float()
|
|
79
|
+
h = h * torch.rsqrt(h.pow(2).mean(dim=-1, keepdim=True) + eps)
|
|
80
|
+
if mode == "offset":
|
|
81
|
+
h = h * (1.0 + w_norm.float())
|
|
82
|
+
elif mode == "direct":
|
|
83
|
+
h = h * w_norm.float()
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"unknown norm_weight_mode: {mode!r}")
|
|
86
|
+
if cond_scale is not None:
|
|
87
|
+
h = h * (1.0 + cond_scale.float())
|
|
88
|
+
if cond_shift is not None:
|
|
89
|
+
h = h + cond_shift.float()
|
|
90
|
+
return h.to(torch.bfloat16)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check_and_pack(weights: Mapping[str, torch.Tensor]):
|
|
94
|
+
"""Validate dims against the support envelope; pack FP8 weights."""
|
|
95
|
+
w_gate, w_up, w_down = weights["w_gate"], weights["w_up"], weights["w_down"]
|
|
96
|
+
dim_d, dim_f = w_gate.shape
|
|
97
|
+
if w_up.shape != (dim_d, dim_f) or w_down.shape != (dim_f, dim_d):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"inconsistent weight dims: gate {tuple(w_gate.shape)}, "
|
|
100
|
+
f"up {tuple(w_up.shape)}, down {tuple(w_down.shape)}"
|
|
101
|
+
)
|
|
102
|
+
for name, dim in (("D", dim_d), ("F", dim_f)):
|
|
103
|
+
bounds = SUPPORT[name]
|
|
104
|
+
if not bounds["min"] <= dim <= bounds["max"]:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"{name}={dim} outside support envelope "
|
|
107
|
+
f"[{bounds['min']}, {bounds['max']}]"
|
|
108
|
+
)
|
|
109
|
+
if not (w_gate.is_cuda and w_up.is_cuda and w_down.is_cuda):
|
|
110
|
+
raise ValueError("fp8_static requires CUDA-resident weights")
|
|
111
|
+
gate_up = torch.cat([w_gate.t(), w_up.t()], dim=0).contiguous()
|
|
112
|
+
down = w_down.t().contiguous()
|
|
113
|
+
return gate_up, down, _amax_scale(gate_up), _amax_scale(down)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _calibrate_scales(
|
|
117
|
+
normed_samples: Sequence[torch.Tensor],
|
|
118
|
+
w_gate: torch.Tensor,
|
|
119
|
+
w_up: torch.Tensor,
|
|
120
|
+
act: Callable[[torch.Tensor], torch.Tensor],
|
|
121
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
122
|
+
"""Static per-tensor input/hidden scales from normed activations."""
|
|
123
|
+
if not normed_samples:
|
|
124
|
+
raise ValueError("calibration samples must be non-empty")
|
|
125
|
+
device = w_gate.device
|
|
126
|
+
input_amax = torch.zeros((), device=device)
|
|
127
|
+
hidden_amax = torch.zeros((), device=device)
|
|
128
|
+
with torch.no_grad():
|
|
129
|
+
for h in normed_samples:
|
|
130
|
+
flat = h.reshape(-1, h.shape[-1]).float().to(device)
|
|
131
|
+
hidden = act(flat @ w_gate.float()) * (flat @ w_up.float())
|
|
132
|
+
input_amax = torch.maximum(input_amax, flat.abs().max())
|
|
133
|
+
hidden_amax = torch.maximum(hidden_amax, hidden.abs().max())
|
|
134
|
+
return ((input_amax / _FP8_MAX).clamp(min=1e-8),
|
|
135
|
+
(hidden_amax / _FP8_MAX).clamp(min=1e-8))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class BoundDecoderFfnFp8:
|
|
140
|
+
"""Bound callable for the full structure boundary."""
|
|
141
|
+
|
|
142
|
+
fused_mlp: Callable[..., torch.Tensor]
|
|
143
|
+
w_norm: torch.Tensor
|
|
144
|
+
gate_up_fp8: torch.Tensor
|
|
145
|
+
down_fp8: torch.Tensor
|
|
146
|
+
input_scale: torch.Tensor
|
|
147
|
+
gate_up_scale: torch.Tensor
|
|
148
|
+
hidden_scale: torch.Tensor
|
|
149
|
+
down_scale: torch.Tensor
|
|
150
|
+
norm_weight_mode: str
|
|
151
|
+
eps: float
|
|
152
|
+
in_dtype: str = "bf16"
|
|
153
|
+
|
|
154
|
+
def ffn(self, normed: torch.Tensor) -> torch.Tensor:
|
|
155
|
+
"""The normed-input -> ffn-output slice (no norm, no residual).
|
|
156
|
+
|
|
157
|
+
On the BF16 entry the kernel quantizes the input itself; on the
|
|
158
|
+
FP8 entry the producer already did, and the input passes
|
|
159
|
+
straight through."""
|
|
160
|
+
shape = normed.shape
|
|
161
|
+
if getattr(self, "in_dtype", "bf16") == "fp8_static":
|
|
162
|
+
out = self.fused_mlp(
|
|
163
|
+
normed.reshape(-1, shape[-1]),
|
|
164
|
+
self.gate_up_fp8, self.down_fp8,
|
|
165
|
+
self.input_scale.view(1), self.gate_up_scale.view(1),
|
|
166
|
+
self.hidden_scale.view(1), self.down_scale.view(1))
|
|
167
|
+
return out.reshape(*shape[:-1], out.shape[-1])
|
|
168
|
+
out = self.fused_mlp(
|
|
169
|
+
normed.reshape(-1, shape[-1]).to(torch.bfloat16).contiguous(),
|
|
170
|
+
self.gate_up_fp8,
|
|
171
|
+
self.down_fp8,
|
|
172
|
+
self.input_scale.view(1),
|
|
173
|
+
self.gate_up_scale.view(1),
|
|
174
|
+
self.hidden_scale.view(1),
|
|
175
|
+
self.down_scale.view(1),
|
|
176
|
+
)
|
|
177
|
+
return out.reshape(shape).to(normed.dtype)
|
|
178
|
+
|
|
179
|
+
def __call__(
|
|
180
|
+
self,
|
|
181
|
+
x: torch.Tensor,
|
|
182
|
+
*,
|
|
183
|
+
cond_scale: torch.Tensor | None = None,
|
|
184
|
+
cond_shift: torch.Tensor | None = None,
|
|
185
|
+
cond_gate: torch.Tensor | None = None,
|
|
186
|
+
) -> torch.Tensor:
|
|
187
|
+
h = _normalize(x, self.w_norm, self.norm_weight_mode,
|
|
188
|
+
cond_scale, cond_shift, self.eps)
|
|
189
|
+
out = self.ffn(h)
|
|
190
|
+
if cond_gate is not None:
|
|
191
|
+
out = out * cond_gate
|
|
192
|
+
return x + out.to(x.dtype)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class FusedGeGluMlp(GuardedSeam, torch.nn.Module):
|
|
196
|
+
"""MLP-seam module for hosts whose replaceable boundary is the MLP.
|
|
197
|
+
|
|
198
|
+
The host keeps its own norm, AdaLN gate, and residual. ``original``
|
|
199
|
+
is retained whole (host MLP naming varies across model families), and
|
|
200
|
+
attribute lookups fall through to it so hosts that introspect the
|
|
201
|
+
projection attributes of the module they call keep working. Retaining
|
|
202
|
+
it is also what makes the seam reversible per call: an input outside
|
|
203
|
+
the calibrated form runs the host MLP instead of this kernel.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
_frt_host_attr = "host_mlp"
|
|
207
|
+
_frt_can_fallback = True
|
|
208
|
+
|
|
209
|
+
def __init__(self, bound: BoundDecoderFfnFp8,
|
|
210
|
+
original: torch.nn.Module | None = None):
|
|
211
|
+
super().__init__()
|
|
212
|
+
self._bound = bound
|
|
213
|
+
if original is not None:
|
|
214
|
+
self.host_mlp = original
|
|
215
|
+
self._frt_arm(
|
|
216
|
+
dtypes=(FP8_ONLY if bound.in_dtype == "fp8_static" else CAST_OK),
|
|
217
|
+
device=bound.gate_up_fp8.device,
|
|
218
|
+
k=int(bound.gate_up_fp8.shape[1]))
|
|
219
|
+
|
|
220
|
+
def __getattr__(self, name):
|
|
221
|
+
try:
|
|
222
|
+
return super().__getattr__(name)
|
|
223
|
+
except AttributeError:
|
|
224
|
+
if name == "host_mlp":
|
|
225
|
+
raise
|
|
226
|
+
return getattr(super().__getattr__("host_mlp"), name)
|
|
227
|
+
|
|
228
|
+
def forward(self, hidden: torch.Tensor) -> torch.Tensor:
|
|
229
|
+
admitted = self._frt_admit(hidden)
|
|
230
|
+
if admitted is not PROCEED:
|
|
231
|
+
return admitted
|
|
232
|
+
return self._bound.ffn(hidden)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _build(weights, variant, input_scale, hidden_scale, eps):
|
|
236
|
+
name, _ = _activation(variant)
|
|
237
|
+
gate_up, down, gate_up_scale, down_scale = _check_and_pack(weights)
|
|
238
|
+
in_dtype = variant.get("in_dtype", "bf16")
|
|
239
|
+
table = (_ENTRYPOINTS_FP8 if in_dtype == "fp8_static"
|
|
240
|
+
else _ENTRYPOINTS)
|
|
241
|
+
return BoundDecoderFfnFp8(
|
|
242
|
+
fused_mlp=getattr(_kernel(), table[name]),
|
|
243
|
+
w_norm=weights["w_norm"],
|
|
244
|
+
gate_up_fp8=_quantize(gate_up, gate_up_scale),
|
|
245
|
+
down_fp8=_quantize(down, down_scale),
|
|
246
|
+
input_scale=input_scale,
|
|
247
|
+
gate_up_scale=gate_up_scale,
|
|
248
|
+
hidden_scale=hidden_scale,
|
|
249
|
+
down_scale=down_scale,
|
|
250
|
+
in_dtype=in_dtype,
|
|
251
|
+
norm_weight_mode=variant.get("norm_weight_mode", "offset"),
|
|
252
|
+
eps=eps,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@torch.no_grad()
|
|
257
|
+
def bind(
|
|
258
|
+
weights: Mapping[str, torch.Tensor],
|
|
259
|
+
*,
|
|
260
|
+
variant: Mapping[str, str],
|
|
261
|
+
calibration_inputs: Sequence[Mapping[str, torch.Tensor]],
|
|
262
|
+
eps: float = 1e-6,
|
|
263
|
+
) -> BoundDecoderFfnFp8:
|
|
264
|
+
"""Bind the full structure: calibration inputs are boundary inputs.
|
|
265
|
+
|
|
266
|
+
``calibration_inputs`` must be drawn from the real input distribution
|
|
267
|
+
of the target binding; static FP8 scales are only as trustworthy as
|
|
268
|
+
the data they were measured on.
|
|
269
|
+
"""
|
|
270
|
+
if not calibration_inputs:
|
|
271
|
+
raise ValueError("calibration_inputs must be non-empty")
|
|
272
|
+
_, act = _activation(variant)
|
|
273
|
+
mode = variant.get("norm_weight_mode", "offset")
|
|
274
|
+
normed = [
|
|
275
|
+
_normalize(sample["x"], weights["w_norm"], mode,
|
|
276
|
+
sample.get("cond_scale"), sample.get("cond_shift"), eps)
|
|
277
|
+
for sample in calibration_inputs
|
|
278
|
+
]
|
|
279
|
+
input_scale, hidden_scale = _calibrate_scales(
|
|
280
|
+
normed, weights["w_gate"], weights["w_up"], act)
|
|
281
|
+
return _build(weights, variant, input_scale, hidden_scale, eps)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@torch.no_grad()
|
|
285
|
+
def bind_mlp_seam(
|
|
286
|
+
weights: Mapping[str, torch.Tensor],
|
|
287
|
+
*,
|
|
288
|
+
variant: Mapping[str, str],
|
|
289
|
+
input_scale: float,
|
|
290
|
+
hidden_scale: float,
|
|
291
|
+
original: torch.nn.Module | None = None,
|
|
292
|
+
eps: float = 1e-6,
|
|
293
|
+
) -> FusedGeGluMlp:
|
|
294
|
+
"""Bind the MLP-seam slice from two already-calibrated scales.
|
|
295
|
+
|
|
296
|
+
The scales arrive measured, not derived: ``input_scale`` is the amax at
|
|
297
|
+
this MLP's input and ``hidden_scale`` the amax at its down
|
|
298
|
+
projection's input — which is exactly the gated activation this kernel
|
|
299
|
+
quantises. Recomputing the second one here would mean keeping the
|
|
300
|
+
seam's inputs alive to run gate/up over them again, and the amax it
|
|
301
|
+
would arrive at is the one the host already produced.
|
|
302
|
+
|
|
303
|
+
Both are per-tensor FP8 scales (amax/448), reduced across calibration
|
|
304
|
+
samples by the caller through ``flash_rt.core.calibration``.
|
|
305
|
+
"""
|
|
306
|
+
dev = weights["w_gate"].device
|
|
307
|
+
bound = _build(weights, variant,
|
|
308
|
+
torch.tensor(float(input_scale), device=dev),
|
|
309
|
+
torch.tensor(float(hidden_scale), device=dev), eps)
|
|
310
|
+
return FusedGeGluMlp(bound, original=original)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
impl: fp8_static
|
|
2
|
+
structure: decoder_ffn
|
|
3
|
+
version: 1
|
|
4
|
+
recipe: fp8_static
|
|
5
|
+
backends: [cuda, rocm]
|
|
6
|
+
form: hub
|
|
7
|
+
module: fp8_static
|
|
8
|
+
entrypoint: bind
|
|
9
|
+
|
|
10
|
+
kernels:
|
|
11
|
+
- {provider: hf, repo: flashrt/flashrt-fp8-swiglu-ffn, version: ">=1"}
|
|
12
|
+
|
|
13
|
+
envelope:
|
|
14
|
+
support:
|
|
15
|
+
D: {min: 512, max: 16384}
|
|
16
|
+
F: {min: 1024, max: 16384}
|
|
17
|
+
align: {}
|
|
18
|
+
m_classes: [micro, small, medium]
|
|
19
|
+
notes: >
|
|
20
|
+
Support bounds mirror the checks enforced in bind(); a kernel-side
|
|
21
|
+
can_implement probe should replace them once exposed. Activation
|
|
22
|
+
scales are static per-tensor and require calibration inputs at bind.
|