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,435 @@
1
+ """qkv_pack — pack sibling linears that share one input into one GEMM.
2
+
3
+ Sibling projections consumed in a fixed call order (q/k/v of an
4
+ attention block, gate/up of an MLP) each pay a small-M GEMM whose cost
5
+ is launch/latency floor, not bandwidth. Packing their weights into one
6
+ ``[sum(N_i), K]`` matrix turns the group into a single GEMM; the later
7
+ siblings become buffer reads. Two bind forms cover the hosts seen so
8
+ far:
9
+
10
+ - **leaf**: the host calls the sibling modules separately and there is
11
+ no enclosing attention-module boundary. The first sibling's slot gets
12
+ a :class:`PackedLinear` (runs the packed GEMM, writes the other
13
+ outputs into preallocated buffers); the later slots get
14
+ :class:`StashReader` (return the buffer). The host's own call order
15
+ is the data dependency — functionalization keeps copy/read ordered
16
+ inside compiled and captured graphs.
17
+ - **module**: the host has an attention module with
18
+ ``q_proj/k_proj/v_proj/out_proj`` attributes and a standard
19
+ projections → attention → out_proj forward. :class:`AttnBlockPacked`
20
+ replaces the whole module: packed GEMM, SDPA at a declared compute
21
+ dtype, original ``out_proj``.
22
+
23
+ Both forms quantize the packed weight to FP8 with one joint per-tensor
24
+ scale (the joint-scale rounding difference is covered by the parity
25
+ gate). Inputs enter either as FP8 (a producer seam supplies the shared
26
+ ``act_scale``) or as BF16 through the fused-quantize entry with a
27
+ calibrated ``act_scale``.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Sequence
33
+
34
+ import torch
35
+
36
+ from .. import hub_kernel
37
+ from ...workspace import lease
38
+ from ...guard import CAST_OK, FP8_ONLY, PROCEED, GuardRefused, GuardedSeam
39
+
40
+ _FP8 = torch.float8_e4m3fn
41
+
42
+
43
+ def _all_zero(t: torch.Tensor) -> bool:
44
+ return not bool(t.any())
45
+
46
+
47
+ def _pack_weights(mods: Sequence[torch.nn.Module]):
48
+ ws = [m.weight.detach() for m in mods]
49
+ w = torch.cat(ws, 0)
50
+ scale = (w.float().abs().max() / 448.0).clamp(min=1e-8).view(1)
51
+ w8 = (w.float() / scale).clamp(-448, 448).to(_FP8)
52
+ splits = [wi.shape[0] for wi in ws]
53
+ biases = []
54
+ for m in mods:
55
+ b = getattr(m, "bias", None)
56
+ biases.append(b.detach().to(torch.bfloat16) if b is not None
57
+ else torch.zeros(m.weight.shape[0],
58
+ device=w.device,
59
+ dtype=torch.bfloat16))
60
+ return w8, scale, torch.cat(biases), splits
61
+
62
+
63
+ class PackedLinear(GuardedSeam, torch.nn.Module):
64
+ """Leaf-form head: one packed GEMM, later siblings stashed.
65
+
66
+ The stash and quantize buffers are allocated once at the largest row
67
+ count observed during calibration. The Hub entry accepts a logical M
68
+ no larger than those buffers and returns only the logical rows, so the
69
+ contract is a row *capacity*, not one exact row count. Calls above the
70
+ capacity fall back before they can hand the kernel a short buffer.
71
+ """
72
+
73
+ _frt_host_attr = "host_linear"
74
+ _frt_can_fallback = True
75
+
76
+ def __init__(self, mods: Sequence[torch.nn.Module],
77
+ act_scale: torch.Tensor, rows: int,
78
+ in_dtype: str = "fp8_static", joint_slots: int = 0):
79
+ super().__init__()
80
+ self.host_linear = mods[0]
81
+ kf = hub_kernel("flashrt/flashrt-fp8-ffn", ">=1")
82
+ self.in_dtype = in_dtype
83
+ w8, w_scale, bias, splits = _pack_weights(mods)
84
+ self.splits = splits
85
+ self.rows = rows
86
+ self.register_buffer("w8", w8)
87
+ self.register_buffer("w_scale", w_scale)
88
+ self.register_buffer("bias_cat", bias)
89
+ self.register_buffer("act_scale", act_scale)
90
+ dev = w8.device
91
+ for i, n in enumerate(splits[1:], 1):
92
+ setattr(self, f"stash{i}", lease(
93
+ (rows, n), torch.bfloat16, dev,
94
+ tag=f"qkv_stash{i}",
95
+ # state, not scratch: the host may retain the
96
+ # reader's view (a KV cache did, and the shared
97
+ # slab clobbered every cached slice) — sharing
98
+ # needs immediacy-of-consumption as a fact
99
+ exclusive=True))
100
+ # a bias-add is its own kernel. Hosts whose projections carry no
101
+ # bias (the whole Gemma family) would otherwise pay a launch per
102
+ # call to add zeros — measured 3 kernels/call with the bias
103
+ # entry against 1 without it at the same shapes.
104
+ self.no_bias = _all_zero(bias)
105
+ # A caller that consumes the first `joint_slots` siblings
106
+ # together takes the packed output whole: they are one
107
+ # contiguous run in it, and splitting them apart only to apply
108
+ # the same elementwise transform to each half costs a kernel and
109
+ # two copies. Zero keeps the sibling-by-sibling contract.
110
+ self.joint_slots = joint_slots
111
+ if joint_slots:
112
+ self.packed = lease((rows, sum(splits)), torch.bfloat16,
113
+ w8.device, tag="qkv_joint")
114
+ if in_dtype == "fp8_static":
115
+ self._fn = (kf.fp8_gemm_bf16 if self.no_bias
116
+ else kf.fp8_linear_bias_bf16)
117
+ else:
118
+ self._fn = kf.bf16_fp8_linear_bias_bf16
119
+ k = mods[0].weight.shape[1]
120
+ # Call-lifetime scratch, so the pool owns it: the quantize
121
+ # scratch is written and read inside the kernel call, and
122
+ # the packed output is fully consumed before forward returns
123
+ # (q sliced out by copy, later siblings copied to stashes).
124
+ # Layers run sequentially, so every same-shape pack shares
125
+ # one allocation instead of paying ~900 MiB per layer — the
126
+ # difference between binding a 52-layer host and refusing
127
+ # most of it on budget.
128
+ self.x8_buf = lease((rows, k), _FP8, dev, tag="qkv_x8")
129
+ self.y_buf = lease((rows, sum(splits)), torch.bfloat16, dev,
130
+ tag="qkv_y")
131
+ self._frt_arm(
132
+ dtypes=FP8_ONLY if in_dtype == "fp8_static" else CAST_OK,
133
+ device=dev, k=int(mods[0].weight.shape[1]),
134
+ row_capacity=rows)
135
+
136
+ def alias_stash(self, index: int, region: torch.Tensor) -> None:
137
+ """Write sibling ``index`` straight into a buffer someone else owns.
138
+
139
+ The stash exists because the later siblings' outputs have to live
140
+ somewhere until the host asks for them. When the consumer of that
141
+ output already owns a region of the right shape, that region can
142
+ *be* the stash and the consumer's own copy disappears — one
143
+ buffer instead of two, which is the join the two structures could
144
+ never see from inside either of them.
145
+ """
146
+ if not 1 <= index < len(self.splits):
147
+ raise ValueError(f"qkv_pack: no sibling {index} to alias")
148
+ want = (self.rows, self.splits[index])
149
+ if region.dtype is not torch.bfloat16:
150
+ raise ValueError(
151
+ f"qkv_pack: aliased region is {region.dtype}, the packed "
152
+ "output is bfloat16")
153
+ # An alias has to be checked for actually aliasing, not for a
154
+ # property that usually comes with it. Two ways to lose it, both
155
+ # silent: reshape *copies* when it cannot view, leaving a
156
+ # detached buffer that looks right and is connected to nothing;
157
+ # and a view that does succeed can still be strided, so the
158
+ # writes would land on every other row of the consumer's region.
159
+ try:
160
+ buf = region.view(want)
161
+ except RuntimeError as exc:
162
+ raise ValueError(
163
+ f"qkv_pack: aliased region is not viewable at {want} "
164
+ "without a copy") from exc
165
+ if buf.data_ptr() != region.data_ptr():
166
+ raise ValueError(
167
+ "qkv_pack: aliased view does not start at the region")
168
+ if not buf.is_contiguous():
169
+ raise ValueError(
170
+ f"qkv_pack: aliased region is strided at {want} — the "
171
+ "stash write would skip rows of the consumer's buffer")
172
+ setattr(self, f"stash{index}", buf)
173
+
174
+ def enable_joint(self, slots: int) -> None:
175
+ """Let a caller take the first ``slots`` siblings together.
176
+
177
+ Enabled after binding, because whether anyone consumes them
178
+ jointly is a property of the composition around this pack, not
179
+ of the pack. Refused when the siblings do not divide evenly by
180
+ the head dim they would be viewed at — then they are not one
181
+ run of equal-width heads and the caller cannot treat them alike.
182
+ """
183
+ if not 2 <= slots <= len(self.splits):
184
+ raise ValueError(f"qkv_pack: cannot join {slots} sibling(s)")
185
+ self.joint_slots = slots
186
+ if not hasattr(self, "packed"):
187
+ self.packed = lease((self.rows, sum(self.splits)),
188
+ torch.bfloat16, self.w8.device,
189
+ tag="qkv_joint")
190
+
191
+ def disable_joint(self) -> None:
192
+ """Restore the sibling-by-sibling stash contract."""
193
+ self.joint_slots = 0
194
+
195
+ def joint(self, x):
196
+ """Run the pack and return the first ``joint_slots`` siblings whole.
197
+
198
+ They are one contiguous run of the packed output, so a caller
199
+ that applies the same transform to all of them (a rotary
200
+ embedding over q and k, whose head dims match by construction)
201
+ can do it in one pass instead of splitting them apart first.
202
+ """
203
+ if not self.joint_slots:
204
+ raise ValueError("qkv_pack: this pack has no joint slots")
205
+ flat = x.reshape(-1, x.shape[-1])
206
+ if not torch.compiler.is_compiling():
207
+ reason = self._frt_guard.admit(flat)
208
+ if reason is not None:
209
+ # Joint consumption has no q-only host fallback: all sibling
210
+ # projections have already been claimed by the composition.
211
+ # Still write the refusal into the ordinary seam ledger.
212
+ self._frt_guard.refuse(reason)
213
+ raise GuardRefused(f"qkv_pack: joint refused — {reason}")
214
+ self._run(flat, stash_all=False)
215
+ width = sum(self.splits[:self.joint_slots])
216
+ return self.packed[:flat.shape[0], :width]
217
+
218
+ def _run(self, flat, stash_all: bool = True):
219
+ logical_rows = flat.shape[0]
220
+ out = (self.packed[:logical_rows]
221
+ if self.joint_slots and self.in_dtype == "fp8_static"
222
+ else self.packed if self.joint_slots else None)
223
+ if self.in_dtype == "fp8_static":
224
+ y = (self._fn(flat, self.w8, self.act_scale, self.w_scale,
225
+ out=out)
226
+ if self.no_bias else
227
+ self._fn(flat, self.w8, self.bias_cat, self.act_scale,
228
+ self.w_scale, out=out))
229
+ else:
230
+ y = self._fn(flat.to(torch.bfloat16).contiguous(),
231
+ self.w8, self.bias_cat, self.act_scale,
232
+ self.w_scale, input_fp8=self.x8_buf,
233
+ out=out if out is not None else self.y_buf)
234
+ # siblings the caller takes jointly are read straight out of the
235
+ # packed buffer; only the rest need stashing. A plain forward
236
+ # always stashes — a host-form call on a module whose joint
237
+ # consumer is enabled but not routed must leave fresh stashes,
238
+ # not silently stale ones (measured cos ~1e-5 on the sibling
239
+ # read when this was skipped).
240
+ if not torch.compiler.is_compiling():
241
+ self._stash_epoch = getattr(self, "_stash_epoch", 0) + 1
242
+ skip = 0 if stash_all else self.joint_slots
243
+ off = sum(self.splits[:max(1, skip)]) if skip else self.splits[0]
244
+ for i, n in enumerate(self.splits[1:], 1):
245
+ if i < skip:
246
+ continue
247
+ getattr(self, f"stash{i}")[:logical_rows].copy_(
248
+ y[:, off:off + n])
249
+ if not torch.compiler.is_compiling():
250
+ epochs = getattr(self, "_stash_epochs", None)
251
+ if epochs is None:
252
+ epochs = {}
253
+ self._stash_epochs = epochs
254
+ epochs[i] = self._stash_epoch
255
+ off += n
256
+ return y
257
+
258
+ def forward(self, x):
259
+ admitted = self._frt_admit(x)
260
+ if admitted is not PROCEED:
261
+ return admitted
262
+ flat = x.reshape(-1, x.shape[-1])
263
+ y = self._run(flat)
264
+ out = y[:, :self.splits[0]].contiguous()
265
+ out = out.reshape(*x.shape[:-1], self.splits[0])
266
+ # the kernel's output dtype is BF16 by contract; only cast back
267
+ # when the host boundary itself is a compute dtype. On the
268
+ # fp8_static entry the input is FP8 (a producer seam supplies
269
+ # it) and casting to it would hand FP8 activations to the
270
+ # host's next op.
271
+ return out if x.dtype is _FP8 else out.to(x.dtype)
272
+
273
+ def __getattr__(self, name):
274
+ try:
275
+ return super().__getattr__(name)
276
+ except AttributeError:
277
+ return getattr(super().__getattr__("host_linear"), name)
278
+
279
+
280
+ class StashReader(GuardedSeam, torch.nn.Module):
281
+ """Leaf-form tail: return the packed head's stashed output.
282
+
283
+ Shares the head's contract, because it shares the head's input: the
284
+ host hands the same activation to every sibling, so head and tails
285
+ admit or refuse a call together and the group never half-runs.
286
+ """
287
+
288
+ _frt_host_attr = "host_linear"
289
+ _frt_can_fallback = True
290
+ # Its value is valid only after the packed head ran on the same input.
291
+ # A slower-cadence updater calls one projection independently, so using
292
+ # this replacement there would refresh from a previous sibling call.
293
+ _frt_requires_sibling_order = True
294
+
295
+ def __init__(self, orig: torch.nn.Module, packed: PackedLinear,
296
+ index: int):
297
+ super().__init__()
298
+ self.host_linear = orig
299
+ self._packed = (packed,)
300
+ self.index = index
301
+ head = packed._frt_guard
302
+ self._frt_arm(dtypes=head.dtypes, device=head.device, k=head.k,
303
+ row_capacity=head.row_capacity)
304
+
305
+ def forward(self, x):
306
+ admitted = self._frt_admit(x)
307
+ if admitted is not PROCEED:
308
+ return admitted
309
+ head = self._packed[0]
310
+ if not torch.compiler.is_compiling():
311
+ epoch = getattr(head, "_stash_epoch", 0)
312
+ written = getattr(head, "_stash_epochs", {}).get(self.index)
313
+ if epoch and written != epoch:
314
+ raise GuardRefused(
315
+ "qkv_pack: sibling stash is stale — the head's last "
316
+ "run did not write this slot (a joint consumer "
317
+ "skipped it); reading it would be silently wrong")
318
+ logical_rows = x.numel() // x.shape[-1]
319
+ buf = getattr(self._packed[0], f"stash{self.index}")[:logical_rows]
320
+ out = buf.reshape(*x.shape[:-1], buf.shape[-1])
321
+ return out if x.dtype is _FP8 else out.to(x.dtype)
322
+
323
+ def __getattr__(self, name):
324
+ try:
325
+ return super().__getattr__(name)
326
+ except AttributeError:
327
+ return getattr(super().__getattr__("host_linear"), name)
328
+
329
+
330
+ def bind_qkv_pack(mods: Sequence[torch.nn.Module],
331
+ act_scale: torch.Tensor, rows: int,
332
+ in_dtype: str = "fp8_static"):
333
+ """Bind a sibling group; returns replacements in sibling order.
334
+
335
+ Each host weight is checkpoint-native ``[out_features, in_features]``;
336
+ the binder concatenates along the output axis and packs once. ``rows``
337
+ is the preallocated row capacity, not an exact runtime M.
338
+ """
339
+ if len(mods) < 2:
340
+ raise ValueError("qkv_pack: need at least two siblings")
341
+ kdims = {m.weight.shape[1] for m in mods}
342
+ if len(kdims) != 1:
343
+ raise ValueError(f"qkv_pack: sibling K dims differ {kdims}")
344
+ packed = PackedLinear(mods, act_scale, rows, in_dtype=in_dtype)
345
+ out = [packed]
346
+ for i, m in enumerate(mods[1:], 1):
347
+ out.append(StashReader(m, packed, i))
348
+ return out
349
+
350
+
351
+ class AttnBlockPacked(GuardedSeam, torch.nn.Module):
352
+ """Module-form: packed QKV + SDPA at a declared dtype + out_proj.
353
+
354
+ Fits attention modules exposing ``q_proj/k_proj/v_proj/out_proj``,
355
+ ``head_dim`` and ``scale`` with the standard block forward
356
+ (SigLIP/CLIP-family vision towers and friends).
357
+ """
358
+
359
+ _frt_host_attr = "host_attn"
360
+ _frt_can_fallback = True
361
+
362
+ def __init__(self, orig: torch.nn.Module, act_scale: torch.Tensor,
363
+ rows: int, sdpa_dtype: torch.dtype = torch.bfloat16):
364
+ super().__init__()
365
+ self.host_attn = orig
366
+ kf = hub_kernel("flashrt/flashrt-fp8-ffn", ">=1")
367
+ self._fn = kf.bf16_fp8_linear_bias_bf16
368
+ w8, w_scale, bias, splits = _pack_weights(
369
+ [orig.q_proj, orig.k_proj, orig.v_proj])
370
+ if len(set(splits)) != 1:
371
+ raise ValueError("attn_block: q/k/v widths differ")
372
+ self.e = splits[0]
373
+ self.register_buffer("w8", w8)
374
+ self.register_buffer("w_scale", w_scale)
375
+ self.register_buffer("bias_cat", bias)
376
+ self.register_buffer("in_scale", act_scale)
377
+ k = orig.q_proj.weight.shape[1]
378
+ dev = w8.device
379
+ self.register_buffer("x8_buf", torch.empty(
380
+ rows, k, device=dev, dtype=_FP8))
381
+ self.register_buffer("y_buf", torch.empty(
382
+ rows, 3 * self.e, device=dev, dtype=torch.bfloat16))
383
+ self.sdpa_dtype = sdpa_dtype
384
+ self._frt_arm(dtypes=CAST_OK, device=dev, k=int(k),
385
+ row_capacity=rows)
386
+
387
+ def forward(self, hidden_states, attention_mask=None, **kw):
388
+ admitted = self._frt_admit(hidden_states, attention_mask, **kw)
389
+ if admitted is not PROCEED:
390
+ return admitted
391
+ a = self.host_attn
392
+ bsz, seq, dim = hidden_states.shape
393
+ flat = hidden_states.reshape(-1, dim).to(
394
+ torch.bfloat16).contiguous()
395
+ y = self._fn(flat, self.w8, self.bias_cat, self.in_scale,
396
+ self.w_scale, input_fp8=self.x8_buf,
397
+ out=self.y_buf)
398
+ hd = a.head_dim
399
+
400
+ def split(t):
401
+ return t.contiguous().view(bsz, seq, -1, hd).transpose(
402
+ 1, 2).to(self.sdpa_dtype)
403
+
404
+ e = self.e
405
+ mask = (attention_mask.to(self.sdpa_dtype)
406
+ if attention_mask is not None else None)
407
+ o = torch.nn.functional.scaled_dot_product_attention(
408
+ split(y[:, :e]), split(y[:, e:2 * e]), split(y[:, 2 * e:]),
409
+ attn_mask=mask, scale=a.scale)
410
+ o = o.to(hidden_states.dtype).transpose(1, 2).reshape(
411
+ bsz, seq, dim).contiguous()
412
+ return a.out_proj(o), None
413
+
414
+ def __getattr__(self, name):
415
+ try:
416
+ return super().__getattr__(name)
417
+ except AttributeError:
418
+ return getattr(super().__getattr__("host_attn"), name)
419
+
420
+
421
+ def bind_attn_block(orig: torch.nn.Module, act_scale: torch.Tensor,
422
+ rows: int,
423
+ sdpa_dtype: torch.dtype = torch.bfloat16
424
+ ) -> AttnBlockPacked:
425
+ """Bind the module form with checkpoint-native Q/K/V weights.
426
+
427
+ ``orig.{q,k,v}_proj.weight`` are ``[out_features, in_features]`` and
428
+ ``rows`` is the maximum logical row count covered by the preallocated
429
+ quantize/output buffers.
430
+ """
431
+ for attr in ("q_proj", "k_proj", "v_proj", "out_proj", "head_dim",
432
+ "scale"):
433
+ if not hasattr(orig, attr):
434
+ raise ValueError(f"attn_block: host lacks {attr!r}")
435
+ return AttnBlockPacked(orig, act_scale, rows, sdpa_dtype=sdpa_dtype)
@@ -0,0 +1,218 @@
1
+ """qkv_pack in NVFP4 W4A4 with a shared channel balance.
2
+
3
+ Sibling projections share one input, so under an activation-only
4
+ balance they share one balance vector, one dynamic FP4 quantization,
5
+ and one packed ``[sum(N_i), K]`` NVFP4 GEMM per call — the structural
6
+ form of what a hand-written chain does with a per-forward quantization
7
+ memo, held by construction instead of keyed by data pointer. Later
8
+ siblings read stashed outputs exactly as the FP8 pack does (the
9
+ :class:`~.fp8_static.StashReader` tail is shared; it only needs the
10
+ head's stash buffers and guard).
11
+
12
+ Activation scales are per-block and computed per call, so nothing is
13
+ static enough to drift; the calibrated per-channel amax feeds the
14
+ balance fold only.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Sequence
20
+
21
+ import torch
22
+
23
+ from .. import hub_kernel
24
+ from ...guard import CAST_OK, PROCEED, GuardedSeam
25
+ from ...workspace import lease
26
+ from .fp8_static import StashReader
27
+
28
+
29
+ class WireStashReader(StashReader):
30
+ """Stash tail for a wire-fed pack: the packed uint8 input is the
31
+ negotiated form itself, admitted at negotiation time exactly as the
32
+ head's wire path is — the per-call dtype contract only applies to
33
+ the fallback (BF16) form. Rows come from the packed width, which is
34
+ half the logical feature width."""
35
+
36
+ def forward(self, x):
37
+ head = self._packed[0]
38
+ if (x.dtype is torch.uint8
39
+ and getattr(head, "_wire_sfa", None) is not None):
40
+ self._frt_touch()
41
+ logical_rows = x.numel() // x.shape[-1]
42
+ buf = getattr(head, f"stash{self.index}")[:logical_rows]
43
+ return buf.reshape(*x.shape[:-1], buf.shape[-1])
44
+ return super().forward(x)
45
+
46
+ KERNEL_DEP = {
47
+ "provider": "huggingface_kernels",
48
+ "repo": "flashrt/fp4-gemm",
49
+ "version": ">=1",
50
+ }
51
+
52
+ _VARIANT = 2
53
+
54
+
55
+ class PackedLinearNvfp4(GuardedSeam, torch.nn.Module):
56
+ """Leaf-form head: one balanced FP4 GEMM, later siblings stashed."""
57
+
58
+ _frt_host_attr = "host_linear"
59
+ _frt_can_fallback = True
60
+
61
+ def __init__(self, mods: Sequence[torch.nn.Module], w_packed,
62
+ w_sfb, inv_s, bias_cat, splits, rows: int):
63
+ super().__init__()
64
+ self.host_linear = mods[0]
65
+ kern = hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
66
+ self._kern = kern
67
+ self._gemm = kern.fp4_w4a16_linear_bf16
68
+ # capability probe: fused-bias epilogue where the installed
69
+ # variant ships it; absence keeps the two-launch path.
70
+ # Presence is not qualification: the same artifact can carry an
71
+ # entry built for another arch (an SM110-specialized bias op in
72
+ # a package whose base ops serve SM120). One smoke launch at
73
+ # bind is the fact; a refusal keeps the two-launch path.
74
+ self._gemm_bias = getattr(kern, "nvfp4_gemm_bias_bf16", None)
75
+ if self._gemm_bias is not None:
76
+ try:
77
+ z = torch.zeros(1, mods[0].weight.shape[1],
78
+ dtype=torch.float16,
79
+ device=w_packed.device)
80
+ zp, zsf = kern.quantize_fp4_sfa_fp16(z)
81
+ self._gemm_bias(zp, w_packed, zsf, w_sfb,
82
+ bias_cat.to(torch.bfloat16))
83
+ except (RuntimeError, ValueError):
84
+ self._gemm_bias = None
85
+ self.splits = splits
86
+ self.rows = rows
87
+ self.register_buffer("wp", w_packed)
88
+ self.register_buffer("wsfb", w_sfb)
89
+ self.register_buffer("inv_s", inv_s)
90
+ self.no_bias = not bool(bias_cat.any())
91
+ self.register_buffer("bias_cat", bias_cat)
92
+ dev = w_packed.device
93
+ for i, n in enumerate(splits[1:], 1):
94
+ setattr(self, f"stash{i}", lease(
95
+ (rows, n), torch.bfloat16, dev,
96
+ tag=f"qkv_stash{i}",
97
+ # state, not scratch: the host may retain the
98
+ # reader's view (a KV cache did, and the shared
99
+ # slab clobbered every cached slice) — sharing
100
+ # needs immediacy-of-consumption as a fact
101
+ exclusive=True))
102
+ self._frt_arm(dtypes=CAST_OK, device=dev,
103
+ k=int(mods[0].weight.shape[1]), row_capacity=rows)
104
+
105
+ def accept_wire(self, sfa) -> None:
106
+ """Accept a producer's FP4 wire.
107
+
108
+ A negotiated producer (a fused norm emitting packed FP4 + SFA)
109
+ hands its packed tensor through the host glue and its scale
110
+ factors through this alias — calls arriving as the packed
111
+ uint8 tensor then skip this pack's own quantization entirely.
112
+ Wire admission was decided at negotiation time, which is why
113
+ the wire path sits ahead of the per-call guard; a pack bound
114
+ for the wire must have been packed unbalanced (``wire=True``
115
+ at bind), because no activation-side inverse can be applied to
116
+ an input that is already quantized.
117
+ """
118
+ self._wire_sfa = sfa
119
+
120
+ def forward(self, x):
121
+ wire_sfa = getattr(self, "_wire_sfa", None)
122
+ if x.dtype is torch.uint8 and wire_sfa is not None:
123
+ a_packed = x.reshape(-1, x.shape[-1])
124
+ y = self._gemm(a_packed, self.wp, wire_sfa, self.wsfb,
125
+ variant=_VARIANT)
126
+ if not self.no_bias:
127
+ y = y + self.bias_cat
128
+ rows = a_packed.shape[0]
129
+ off = self.splits[0]
130
+ for i, n in enumerate(self.splits[1:], 1):
131
+ getattr(self, f"stash{i}")[:rows].copy_(
132
+ y[:, off:off + n])
133
+ off += n
134
+ out = y[:, :self.splits[0]].contiguous()
135
+ return out.reshape(*x.shape[:-1], self.splits[0])
136
+ admitted = self._frt_admit(x)
137
+ if admitted is not PROCEED:
138
+ return admitted
139
+ flat = x.reshape(-1, x.shape[-1])
140
+ src = (flat.to(torch.float16) * self.inv_s).contiguous()
141
+ a_packed, a_sfa = self._kern.quantize_fp4_sfa_fp16(src)
142
+ if not self.no_bias and self._gemm_bias is not None:
143
+ y = self._gemm_bias(a_packed, self.wp, a_sfa, self.wsfb,
144
+ self.bias_cat)
145
+ else:
146
+ y = self._gemm(a_packed, self.wp, a_sfa, self.wsfb,
147
+ variant=_VARIANT)
148
+ if not self.no_bias:
149
+ y = y + self.bias_cat
150
+ logical_rows = flat.shape[0]
151
+ off = self.splits[0]
152
+ for i, n in enumerate(self.splits[1:], 1):
153
+ getattr(self, f"stash{i}")[:logical_rows].copy_(
154
+ y[:, off:off + n])
155
+ off += n
156
+ out = y[:, :self.splits[0]].contiguous()
157
+ out = out.reshape(*x.shape[:-1], self.splits[0])
158
+ return out.to(x.dtype)
159
+
160
+ def __getattr__(self, name):
161
+ try:
162
+ return super().__getattr__(name)
163
+ except AttributeError:
164
+ return getattr(super().__getattr__("host_linear"), name)
165
+
166
+
167
+ @torch.no_grad()
168
+ def bind_qkv_pack(mods: Sequence[torch.nn.Module], *, channel_amax,
169
+ rows: int, alpha: float = 0.5, clamp=(0.25, 4.0),
170
+ wire: bool = False):
171
+ """Bind a sibling group; returns replacements in sibling order.
172
+
173
+ ``channel_amax`` is the per-input-channel amax at the shared input
174
+ (``[K]``); the balance it fits folds into the concatenated weight
175
+ once — the vector depends on the activation alone, so it is the
176
+ same for every sibling by construction. ``wire=True`` packs the
177
+ weight unbalanced for a pack that will consume a producer's FP4
178
+ wire (see :meth:`PackedLinearNvfp4.accept_wire`): an input that
179
+ arrives quantized can carry no activation-side inverse.
180
+ """
181
+ if len(mods) < 2:
182
+ raise ValueError("qkv_pack: need at least two siblings")
183
+ kdims = {m.weight.shape[1] for m in mods}
184
+ if len(kdims) != 1:
185
+ raise ValueError(f"qkv_pack: sibling K dims differ {kdims}")
186
+ from flash_rt.core.quantization import fit_input_channel_balance
187
+
188
+ kern = hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
189
+ w = torch.cat([m.weight.detach() for m in mods], 0)
190
+ if wire:
191
+ w_bal = w.float()
192
+ inv_s = torch.ones(w.shape[1], device=w.device,
193
+ dtype=torch.float32)
194
+ else:
195
+ amax = torch.as_tensor(channel_amax, device=w.device,
196
+ dtype=torch.float32)
197
+ w_bal, inv_s = fit_input_channel_balance(
198
+ w.float(), amax, alpha=alpha,
199
+ clamp=(float(clamp[0]), float(clamp[1])),
200
+ out_dtype=torch.float32)
201
+ w_packed, w_sfb = kern.quantize_fp4_sfa_fp16(
202
+ w_bal.to(torch.float16).contiguous(), is_sfb=True)
203
+ splits = [m.weight.shape[0] for m in mods]
204
+ biases = []
205
+ for m in mods:
206
+ b = getattr(m, "bias", None)
207
+ biases.append(b.detach().to(torch.bfloat16) if b is not None
208
+ else torch.zeros(m.weight.shape[0],
209
+ device=w.device,
210
+ dtype=torch.bfloat16))
211
+ packed = PackedLinearNvfp4(
212
+ mods, w_packed, w_sfb, inv_s.to(torch.float16),
213
+ torch.cat(biases), splits, rows)
214
+ out = [packed]
215
+ reader = WireStashReader if wire else StashReader
216
+ for i, m in enumerate(mods[1:], 1):
217
+ out.append(reader(m, packed, i))
218
+ return out
@@ -0,0 +1,3 @@
1
+ from .packed_bias_bf16 import PackedBiasQkvRope, bind_packed_bias_qkv_rope
2
+
3
+ __all__ = ["PackedBiasQkvRope", "bind_packed_bias_qkv_rope"]