flashrt-structures 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. flashrt_structures/__init__.py +174 -0
  2. flashrt_structures/adapters/__init__.py +55 -0
  3. flashrt_structures/adapters/diffusers_attention.py +237 -0
  4. flashrt_structures/adapters/diffusers_rotary_attention.py +239 -0
  5. flashrt_structures/adapters/factored_qk_norm_rope.py +252 -0
  6. flashrt_structures/adapters/factored_two_way_attention.py +99 -0
  7. flashrt_structures/adapters/gemma_attention.py +226 -0
  8. flashrt_structures/adapters/packed_qkv_rope.py +342 -0
  9. flashrt_structures/adapters/packed_stream_qk_norm_rope.py +376 -0
  10. flashrt_structures/adapters/qwen_per_head_qk_norm_rope.py +231 -0
  11. flashrt_structures/adapters/sglang_engine.py +207 -0
  12. flashrt_structures/adapters/transformers_attention_interface.py +73 -0
  13. flashrt_structures/adapters/transformers_gated_delta.py +152 -0
  14. flashrt_structures/adapters/transformers_gated_delta_fused.py +96 -0
  15. flashrt_structures/adapters/vllm_engine.py +424 -0
  16. flashrt_structures/adjudicate.py +85 -0
  17. flashrt_structures/aot.py +191 -0
  18. flashrt_structures/autobuild.py +2052 -0
  19. flashrt_structures/beta/__init__.py +43 -0
  20. flashrt_structures/beta/conform.py +94 -0
  21. flashrt_structures/beta/joins.py +113 -0
  22. flashrt_structures/beta/negotiate.py +84 -0
  23. flashrt_structures/beta/ports.py +140 -0
  24. flashrt_structures/decisions.py +80 -0
  25. flashrt_structures/discover.py +623 -0
  26. flashrt_structures/explain.py +70 -0
  27. flashrt_structures/frontdoor.py +572 -0
  28. flashrt_structures/gates.py +465 -0
  29. flashrt_structures/guard.py +421 -0
  30. flashrt_structures/handle.py +189 -0
  31. flashrt_structures/impls/__init__.py +219 -0
  32. flashrt_structures/impls/adaln_producer/__init__.py +8 -0
  33. flashrt_structures/impls/adaln_producer/broker.py +116 -0
  34. flashrt_structures/impls/adaln_producer/fused.py +388 -0
  35. flashrt_structures/impls/adarms_stack/__init__.py +8 -0
  36. flashrt_structures/impls/adarms_stack/fp8_chain.py +832 -0
  37. flashrt_structures/impls/adarms_stack/region.py +102 -0
  38. flashrt_structures/impls/attention_core/__init__.py +132 -0
  39. flashrt_structures/impls/attention_core/fa2_seqused.py +458 -0
  40. flashrt_structures/impls/attention_core/fa4_cute.py +154 -0
  41. flashrt_structures/impls/attention_core/fa4_fp8.py +178 -0
  42. flashrt_structures/impls/attention_core/masked_mha.py +158 -0
  43. flashrt_structures/impls/attention_core/two_way_fa2.py +220 -0
  44. flashrt_structures/impls/cadence_static/__init__.py +18 -0
  45. flashrt_structures/impls/cadence_static/buffers.py +122 -0
  46. flashrt_structures/impls/cadence_static/cross_attention.py +187 -0
  47. flashrt_structures/impls/chain_elements.py +89 -0
  48. flashrt_structures/impls/decode_loop/__init__.py +0 -0
  49. flashrt_structures/impls/decode_loop/fp8_kv.py +206 -0
  50. flashrt_structures/impls/decode_loop/mtp_speculative.py +245 -0
  51. flashrt_structures/impls/decode_loop/whole_step.py +852 -0
  52. flashrt_structures/impls/decoder_block/__init__.py +6 -0
  53. flashrt_structures/impls/decoder_block/attn_sublayer.py +110 -0
  54. flashrt_structures/impls/decoder_block/fused.py +167 -0
  55. flashrt_structures/impls/decoder_ffn/__init__.py +0 -0
  56. flashrt_structures/impls/decoder_ffn/fp8_static.py +310 -0
  57. flashrt_structures/impls/decoder_ffn/fp8_static.yaml +22 -0
  58. flashrt_structures/impls/decoder_ffn/w4a16_static.py +221 -0
  59. flashrt_structures/impls/decoder_ffn/w8a16_static.py +183 -0
  60. flashrt_structures/impls/dit_stack/__init__.py +11 -0
  61. flashrt_structures/impls/dit_stack/fp4_chain.py +417 -0
  62. flashrt_structures/impls/dit_stack/region.py +86 -0
  63. flashrt_structures/impls/fixed_iter/__init__.py +29 -0
  64. flashrt_structures/impls/fixed_iter/openpi.py +264 -0
  65. flashrt_structures/impls/fixed_iter/protocol.py +94 -0
  66. flashrt_structures/impls/gated_delta_core/__init__.py +3 -0
  67. flashrt_structures/impls/gated_delta_core/fused_layer.py +545 -0
  68. flashrt_structures/impls/gated_delta_core/hub_v3.py +152 -0
  69. flashrt_structures/impls/graph_lowering/__init__.py +27 -0
  70. flashrt_structures/impls/graph_lowering/pi052_denoise.py +179 -0
  71. flashrt_structures/impls/graph_lowering/protocol.py +76 -0
  72. flashrt_structures/impls/graph_lowering/qwen3_vl.py +364 -0
  73. flashrt_structures/impls/linear_proj/__init__.py +0 -0
  74. flashrt_structures/impls/linear_proj/fp8_static.py +270 -0
  75. flashrt_structures/impls/linear_proj/nvfp4_balance.py +131 -0
  76. flashrt_structures/impls/linear_proj/nvfp4_dynamic.py +182 -0
  77. flashrt_structures/impls/linear_proj/w8a16_static.py +230 -0
  78. flashrt_structures/impls/modnorm_qkv_chain/__init__.py +0 -0
  79. flashrt_structures/impls/modnorm_qkv_chain/fp8_ptok_table.py +291 -0
  80. flashrt_structures/impls/moe_experts/__init__.py +9 -0
  81. flashrt_structures/impls/moe_experts/nvfp4_dynamic.py +208 -0
  82. flashrt_structures/impls/moe_experts/nvfp4_w4a16.py +129 -0
  83. flashrt_structures/impls/norm_fused/__init__.py +3 -0
  84. flashrt_structures/impls/norm_fused/bf16.py +94 -0
  85. flashrt_structures/impls/norm_fused/fp8_producer.py +84 -0
  86. flashrt_structures/impls/patch_projection/__init__.py +3 -0
  87. flashrt_structures/impls/patch_projection/bf16_flat.py +139 -0
  88. flashrt_structures/impls/prefill_tower/__init__.py +10 -0
  89. flashrt_structures/impls/prefill_tower/fp8_chain.py +955 -0
  90. flashrt_structures/impls/prefill_tower/region.py +99 -0
  91. flashrt_structures/impls/qk_norm_rope/__init__.py +12 -0
  92. flashrt_structures/impls/qk_norm_rope/per_head_gqa.py +199 -0
  93. flashrt_structures/impls/qk_norm_rope/projection_bf16.py +165 -0
  94. flashrt_structures/impls/qkv_pack/__init__.py +5 -0
  95. flashrt_structures/impls/qkv_pack/bf16.py +110 -0
  96. flashrt_structures/impls/qkv_pack/fp8_static.py +435 -0
  97. flashrt_structures/impls/qkv_pack/nvfp4_balance.py +218 -0
  98. flashrt_structures/impls/qkv_rope/__init__.py +3 -0
  99. flashrt_structures/impls/qkv_rope/packed_bias_bf16.py +143 -0
  100. flashrt_structures/impls/step_table.py +113 -0
  101. flashrt_structures/impls/vision_ffn/__init__.py +0 -0
  102. flashrt_structures/impls/vision_ffn/fp8_static.py +261 -0
  103. flashrt_structures/impls/vision_ffn/nvfp4_balance.py +211 -0
  104. flashrt_structures/impls/vision_tower/__init__.py +7 -0
  105. flashrt_structures/impls/vision_tower/fp8_chain.py +533 -0
  106. flashrt_structures/impls/vision_tower/region.py +91 -0
  107. flashrt_structures/matrix.py +126 -0
  108. flashrt_structures/points.py +368 -0
  109. flashrt_structures/prequantized.py +131 -0
  110. flashrt_structures/quantize_on_adopt.py +94 -0
  111. flashrt_structures/recipe.py +438 -0
  112. flashrt_structures/regions.py +208 -0
  113. flashrt_structures/schemes.py +490 -0
  114. flashrt_structures/stages.py +298 -0
  115. flashrt_structures/storage.py +255 -0
  116. flashrt_structures/swap.py +398 -0
  117. flashrt_structures/workspace.py +110 -0
  118. flashrt_structures-0.2.0.dist-info/METADATA +140 -0
  119. flashrt_structures-0.2.0.dist-info/RECORD +122 -0
  120. flashrt_structures-0.2.0.dist-info/WHEEL +5 -0
  121. flashrt_structures-0.2.0.dist-info/licenses/LICENSE +202 -0
  122. flashrt_structures-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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.