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