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,220 @@
|
|
|
1
|
+
"""FA2 implementation for factored two-way attention.
|
|
2
|
+
|
|
3
|
+
This is the common MoT form used by multimodal diffusion transformers:
|
|
4
|
+
the causal/understanding branch attends to itself causally, while the
|
|
5
|
+
full/generation branch attends to the joint causal + full sequence. The
|
|
6
|
+
host has already projected, normalized, and rotated Q/K/V; this structure
|
|
7
|
+
owns only the two attention calls and the factored output boundary.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from .. import hub_kernel
|
|
18
|
+
from ...guard import PROCEED, GuardRefused, GuardedSeam
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _AttentionScratch:
|
|
23
|
+
out: torch.Tensor
|
|
24
|
+
lse: torch.Tensor
|
|
25
|
+
workspace: Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _scratch(kernel, q: torch.Tensor, k: torch.Tensor) -> _AttentionScratch:
|
|
29
|
+
out, lse = kernel.allocate_outputs(q)
|
|
30
|
+
return _AttentionScratch(
|
|
31
|
+
out=out,
|
|
32
|
+
lse=lse,
|
|
33
|
+
workspace=kernel.allocate_workspace(q, k),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FactoredTwoWayAttention(GuardedSeam, torch.nn.Module):
|
|
38
|
+
"""Allocation-free single-sample two-way GQA attention processor."""
|
|
39
|
+
|
|
40
|
+
_frt_can_fallback = False
|
|
41
|
+
|
|
42
|
+
def __init__(self, capture: dict[str, Any]) -> None:
|
|
43
|
+
super().__init__()
|
|
44
|
+
query = capture["query"]
|
|
45
|
+
key = capture["key"]
|
|
46
|
+
value = capture["value"]
|
|
47
|
+
causal_q = query["causal_seq"]
|
|
48
|
+
full_q = query["full_only_seq"]
|
|
49
|
+
causal_k = key["causal_seq"]
|
|
50
|
+
full_k = key["full_only_seq"]
|
|
51
|
+
causal_v = value["causal_seq"]
|
|
52
|
+
full_v = value["full_only_seq"]
|
|
53
|
+
|
|
54
|
+
if query["sample_offsets"].numel() != 2:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"attention_core two_way: only one-sample factored packs "
|
|
57
|
+
"are qualified")
|
|
58
|
+
if causal_q.ndim != 3 or full_q.ndim != 3:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"attention_core two_way: Q must have shape [tokens, heads, dim]")
|
|
61
|
+
if causal_k.shape != causal_v.shape or full_k.shape != full_v.shape:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"attention_core two_way: K and V shapes differ")
|
|
64
|
+
if causal_q.shape[-1] != causal_k.shape[-1]:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"attention_core two_way: Q and KV head dimensions differ")
|
|
67
|
+
if causal_q.dtype != torch.bfloat16:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"attention_core two_way: current Hub FA2 path requires BF16")
|
|
70
|
+
|
|
71
|
+
self.causal_shape = tuple(causal_q.shape)
|
|
72
|
+
self.full_q_shape = tuple(full_q.shape)
|
|
73
|
+
self.causal_kv_shape = tuple(causal_k.shape)
|
|
74
|
+
self.full_kv_shape = tuple(full_k.shape)
|
|
75
|
+
self.total_tokens = int(
|
|
76
|
+
query["_causal_indices"].numel()
|
|
77
|
+
+ query["_full_indices"].numel())
|
|
78
|
+
self.scale = float(causal_q.shape[-1] ** -0.5)
|
|
79
|
+
self._kernel = hub_kernel(
|
|
80
|
+
"flashrt/fa2-seqused-runtime",
|
|
81
|
+
">=1",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
device = causal_q.device
|
|
85
|
+
dtype = causal_q.dtype
|
|
86
|
+
kv_heads = causal_k.shape[1]
|
|
87
|
+
head_dim = causal_k.shape[2]
|
|
88
|
+
self.register_buffer(
|
|
89
|
+
"joint_k",
|
|
90
|
+
torch.empty(
|
|
91
|
+
1,
|
|
92
|
+
self.total_tokens,
|
|
93
|
+
kv_heads,
|
|
94
|
+
head_dim,
|
|
95
|
+
device=device,
|
|
96
|
+
dtype=dtype,
|
|
97
|
+
),
|
|
98
|
+
persistent=False,
|
|
99
|
+
)
|
|
100
|
+
self.register_buffer(
|
|
101
|
+
"joint_v",
|
|
102
|
+
torch.empty_like(self.joint_k),
|
|
103
|
+
persistent=False,
|
|
104
|
+
)
|
|
105
|
+
self.register_buffer(
|
|
106
|
+
"causal_indices",
|
|
107
|
+
query["_causal_indices"].long().detach().clone(),
|
|
108
|
+
persistent=False,
|
|
109
|
+
)
|
|
110
|
+
self.register_buffer(
|
|
111
|
+
"full_indices",
|
|
112
|
+
query["_full_indices"].long().detach().clone(),
|
|
113
|
+
persistent=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
causal_q4 = causal_q.unsqueeze(0)
|
|
117
|
+
causal_k4 = causal_k.unsqueeze(0)
|
|
118
|
+
full_q4 = full_q.unsqueeze(0)
|
|
119
|
+
self._causal = _scratch(
|
|
120
|
+
self._kernel,
|
|
121
|
+
causal_q4,
|
|
122
|
+
causal_k4,
|
|
123
|
+
)
|
|
124
|
+
self._full = _scratch(
|
|
125
|
+
self._kernel,
|
|
126
|
+
full_q4,
|
|
127
|
+
self.joint_k,
|
|
128
|
+
)
|
|
129
|
+
self._frt_arm(
|
|
130
|
+
dtypes=(dtype,),
|
|
131
|
+
device=device,
|
|
132
|
+
k=int(head_dim),
|
|
133
|
+
rows=int(causal_q.shape[0] * causal_q.shape[1]),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _validate(self, query, key, value) -> None:
|
|
137
|
+
shapes = (
|
|
138
|
+
tuple(query["causal_seq"].shape),
|
|
139
|
+
tuple(query["full_only_seq"].shape),
|
|
140
|
+
tuple(key["causal_seq"].shape),
|
|
141
|
+
tuple(key["full_only_seq"].shape),
|
|
142
|
+
tuple(value["causal_seq"].shape),
|
|
143
|
+
tuple(value["full_only_seq"].shape),
|
|
144
|
+
)
|
|
145
|
+
expected = (
|
|
146
|
+
self.causal_shape,
|
|
147
|
+
self.full_q_shape,
|
|
148
|
+
self.causal_kv_shape,
|
|
149
|
+
self.full_kv_shape,
|
|
150
|
+
self.causal_kv_shape,
|
|
151
|
+
self.full_kv_shape,
|
|
152
|
+
)
|
|
153
|
+
if shapes != expected:
|
|
154
|
+
raise GuardRefused(
|
|
155
|
+
f"attention_core two_way: shapes {shapes} "
|
|
156
|
+
f"(bound for {expected})")
|
|
157
|
+
|
|
158
|
+
def forward(self, query, key, value):
|
|
159
|
+
admitted = self._frt_admit(query["causal_seq"])
|
|
160
|
+
if admitted is not PROCEED:
|
|
161
|
+
return admitted
|
|
162
|
+
self._validate(query, key, value)
|
|
163
|
+
|
|
164
|
+
causal_q = query["causal_seq"].unsqueeze(0)
|
|
165
|
+
causal_k = key["causal_seq"].unsqueeze(0)
|
|
166
|
+
causal_v = value["causal_seq"].unsqueeze(0)
|
|
167
|
+
cs = self._causal
|
|
168
|
+
causal_out = self._kernel.forward_static(
|
|
169
|
+
causal_q,
|
|
170
|
+
causal_k,
|
|
171
|
+
causal_v,
|
|
172
|
+
out=cs.out,
|
|
173
|
+
softmax_lse=cs.lse,
|
|
174
|
+
workspace=cs.workspace,
|
|
175
|
+
softmax_scale=self.scale,
|
|
176
|
+
causal=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
self.joint_k[0].index_copy_(
|
|
180
|
+
0,
|
|
181
|
+
self.causal_indices,
|
|
182
|
+
key["causal_seq"],
|
|
183
|
+
)
|
|
184
|
+
self.joint_k[0].index_copy_(
|
|
185
|
+
0,
|
|
186
|
+
self.full_indices,
|
|
187
|
+
key["full_only_seq"],
|
|
188
|
+
)
|
|
189
|
+
self.joint_v[0].index_copy_(
|
|
190
|
+
0,
|
|
191
|
+
self.causal_indices,
|
|
192
|
+
value["causal_seq"],
|
|
193
|
+
)
|
|
194
|
+
self.joint_v[0].index_copy_(
|
|
195
|
+
0,
|
|
196
|
+
self.full_indices,
|
|
197
|
+
value["full_only_seq"],
|
|
198
|
+
)
|
|
199
|
+
fs = self._full
|
|
200
|
+
full_out = self._kernel.forward_static(
|
|
201
|
+
query["full_only_seq"].unsqueeze(0),
|
|
202
|
+
self.joint_k,
|
|
203
|
+
self.joint_v,
|
|
204
|
+
out=fs.out,
|
|
205
|
+
softmax_lse=fs.lse,
|
|
206
|
+
workspace=fs.workspace,
|
|
207
|
+
softmax_scale=self.scale,
|
|
208
|
+
causal=False,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
out = dict(query)
|
|
212
|
+
out["causal_seq"] = causal_out.squeeze(0).flatten(-2, -1)
|
|
213
|
+
out["full_only_seq"] = full_out.squeeze(0).flatten(-2, -1)
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def bind_two_way_attention(capture: dict[str, Any]
|
|
218
|
+
) -> FactoredTwoWayAttention:
|
|
219
|
+
"""Bind a factored two-way attention processor from one real call."""
|
|
220
|
+
return FactoredTwoWayAttention(capture)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .buffers import StaticOutput, bind_cadence_static
|
|
2
|
+
from .cross_attention import (
|
|
3
|
+
CrossKvCandidate,
|
|
4
|
+
bind_cross_attention_kv,
|
|
5
|
+
capture_cross_attention_kv,
|
|
6
|
+
discover_cross_attention_kv,
|
|
7
|
+
refresh_cross_attention_kv,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CrossKvCandidate",
|
|
12
|
+
"StaticOutput",
|
|
13
|
+
"bind_cadence_static",
|
|
14
|
+
"bind_cross_attention_kv",
|
|
15
|
+
"capture_cross_attention_kv",
|
|
16
|
+
"discover_cross_attention_kv",
|
|
17
|
+
"refresh_cross_attention_kv",
|
|
18
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""cadence_static — hold work that changes slower than the hot loop.
|
|
2
|
+
|
|
3
|
+
A tick pipeline usually runs several cadences at once: a denoise loop
|
|
4
|
+
that repeats every tick, and encoder-side work that only changes when a
|
|
5
|
+
new observation or prompt arrives. Modules on the slower cadence still
|
|
6
|
+
sit inside the hot path, so a graph captures them and pays for them
|
|
7
|
+
every tick even though their inputs are unchanged.
|
|
8
|
+
|
|
9
|
+
This structure moves such a module out of the loop: its output becomes
|
|
10
|
+
a static buffer the captured graph reads, and the real computation runs
|
|
11
|
+
in an update function the host calls at the module's own cadence. The
|
|
12
|
+
resulting split is explicit — the update callable is returned to the
|
|
13
|
+
caller so it can be registered as a recipe ``outside_update`` rather
|
|
14
|
+
than hidden inside the replacement.
|
|
15
|
+
|
|
16
|
+
Qualification is empirical: the wrapped output must actually be
|
|
17
|
+
constant across the fast loop. Callers pass calibration captures from
|
|
18
|
+
several iterations of the hot loop and binding refuses when they
|
|
19
|
+
disagree — a module whose output moves per step is not a cadence
|
|
20
|
+
substructure, and freezing it would silently change the model.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Callable, Sequence
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
from ...guard import PROCEED, GuardedSeam
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StaticOutput(GuardedSeam, torch.nn.Module):
|
|
33
|
+
"""Return a buffer the host refreshes at the slower cadence.
|
|
34
|
+
|
|
35
|
+
This structure has no input contract to check — ignoring its inputs
|
|
36
|
+
is the point. Its failure mode is the other one: the host stops
|
|
37
|
+
calling the update function and the buffer goes stale while every
|
|
38
|
+
read still succeeds. So the ledger counts reads against refreshes,
|
|
39
|
+
and a seam read many times and refreshed never is visible as exactly
|
|
40
|
+
that instead of as a seam that worked.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_frt_host_attr = "host_module"
|
|
44
|
+
_frt_can_fallback = True
|
|
45
|
+
# the retained host is not a fallback copy: the cadence refresh
|
|
46
|
+
# projects through it every observation, so its weights must stay
|
|
47
|
+
# resident — consumption would corrupt the banks silently
|
|
48
|
+
_frt_host_serving = True
|
|
49
|
+
|
|
50
|
+
def __init__(self, original: torch.nn.Module, value: torch.Tensor):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.host_module = original
|
|
53
|
+
self.register_buffer("buffer", value.contiguous().clone())
|
|
54
|
+
guard = self._frt_arm(dtypes=None, device=self.buffer.device)
|
|
55
|
+
guard.notes.update(reads=0, refreshes=0)
|
|
56
|
+
|
|
57
|
+
def refreshed(self) -> None:
|
|
58
|
+
"""Record that the slower cadence ran. Called by the updater."""
|
|
59
|
+
guard = self._frt_guard
|
|
60
|
+
if guard is not None and not torch.compiler.is_compiling():
|
|
61
|
+
guard.notes["refreshes"] += 1
|
|
62
|
+
|
|
63
|
+
def forward(self, *args, **kwargs):
|
|
64
|
+
# same rule as the contract check: no Python bookkeeping while a
|
|
65
|
+
# compiler is tracing this forward
|
|
66
|
+
if not torch.compiler.is_compiling():
|
|
67
|
+
self._frt_touch()
|
|
68
|
+
self._frt_guard.notes["reads"] += 1
|
|
69
|
+
return self.buffer
|
|
70
|
+
|
|
71
|
+
def __getattr__(self, name):
|
|
72
|
+
try:
|
|
73
|
+
return super().__getattr__(name)
|
|
74
|
+
except AttributeError:
|
|
75
|
+
return getattr(super().__getattr__("host_module"), name)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bind_cadence_static(
|
|
79
|
+
modules: Sequence[torch.nn.Module],
|
|
80
|
+
captures: Sequence[Sequence[torch.Tensor]],
|
|
81
|
+
*,
|
|
82
|
+
recompute: Callable[[], Sequence[torch.Tensor]] | None = None,
|
|
83
|
+
rtol: float = 1e-3,
|
|
84
|
+
atol: float = 1e-3,
|
|
85
|
+
):
|
|
86
|
+
"""Freeze module outputs into buffers plus one update function.
|
|
87
|
+
|
|
88
|
+
``captures[i]`` holds the outputs module ``i`` produced across
|
|
89
|
+
several iterations of the hot loop; they must agree, or the module
|
|
90
|
+
is not on a slower cadence and binding refuses. ``recompute``
|
|
91
|
+
returns fresh outputs for the same modules — typically by running
|
|
92
|
+
the host's own encoder path — and the returned update callable
|
|
93
|
+
copies them into the buffers. Register that callable as the
|
|
94
|
+
recipe's outside update so the split cadence stays explicit and
|
|
95
|
+
the slower work is still timed.
|
|
96
|
+
"""
|
|
97
|
+
if len(modules) != len(captures):
|
|
98
|
+
raise ValueError("cadence_static: modules/captures mismatch")
|
|
99
|
+
statics = []
|
|
100
|
+
for i, (mod, caps) in enumerate(zip(modules, captures)):
|
|
101
|
+
if not caps:
|
|
102
|
+
raise ValueError(f"cadence_static: module {i} has no captures")
|
|
103
|
+
first = caps[0]
|
|
104
|
+
for other in caps[1:]:
|
|
105
|
+
if not torch.allclose(first, other, rtol=rtol, atol=atol):
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"cadence_static: module {i} output varies within "
|
|
108
|
+
"the hot loop — not a cadence substructure")
|
|
109
|
+
statics.append(StaticOutput(mod, first))
|
|
110
|
+
|
|
111
|
+
def update() -> None:
|
|
112
|
+
if recompute is None:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"cadence_static: no recompute function was supplied, so "
|
|
115
|
+
"the buffers cannot be refreshed for a new observation")
|
|
116
|
+
with torch.no_grad():
|
|
117
|
+
fresh = recompute()
|
|
118
|
+
for static, value in zip(statics, fresh):
|
|
119
|
+
static.buffer.copy_(value)
|
|
120
|
+
static.refreshed()
|
|
121
|
+
|
|
122
|
+
return statics, update
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Cross-attention K/V addressing for the existing cadence-static structure.
|
|
2
|
+
|
|
3
|
+
Cross attention has a stable structural signature: Q consumes the current
|
|
4
|
+
hidden width, while K/V consume a different encoder width. The encoder-side
|
|
5
|
+
K/V projections may therefore be refreshed once at the encoder cadence and
|
|
6
|
+
read from static buffers inside a repeated denoise loop. Self attention has
|
|
7
|
+
equal Q/K/V input widths and is deliberately excluded.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Callable, Mapping, Sequence
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from .buffers import StaticOutput, bind_cadence_static
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CrossKvCandidate:
|
|
22
|
+
path: str
|
|
23
|
+
module: torch.nn.Module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def discover_cross_attention_kv(
|
|
27
|
+
model: torch.nn.Module,
|
|
28
|
+
) -> tuple[CrossKvCandidate, ...]:
|
|
29
|
+
"""Find encoder-side K/V projections without host or model names."""
|
|
30
|
+
found = []
|
|
31
|
+
for path, attention in model.named_modules():
|
|
32
|
+
q_proj = getattr(attention, "to_q", None)
|
|
33
|
+
k_proj = getattr(attention, "to_k", None)
|
|
34
|
+
v_proj = getattr(attention, "to_v", None)
|
|
35
|
+
if not all(
|
|
36
|
+
isinstance(module, torch.nn.Linear)
|
|
37
|
+
for module in (q_proj, k_proj, v_proj)
|
|
38
|
+
):
|
|
39
|
+
continue
|
|
40
|
+
if q_proj.in_features == k_proj.in_features:
|
|
41
|
+
continue
|
|
42
|
+
if (
|
|
43
|
+
k_proj.in_features != v_proj.in_features
|
|
44
|
+
or k_proj.out_features != v_proj.out_features
|
|
45
|
+
):
|
|
46
|
+
continue
|
|
47
|
+
found.extend(
|
|
48
|
+
(
|
|
49
|
+
CrossKvCandidate(f"{path}.to_k", k_proj),
|
|
50
|
+
CrossKvCandidate(f"{path}.to_v", v_proj),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
return tuple(found)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def capture_cross_attention_kv(
|
|
57
|
+
candidates: Sequence[CrossKvCandidate],
|
|
58
|
+
forward: Callable[[], object],
|
|
59
|
+
) -> tuple[tuple[torch.Tensor, ...], ...]:
|
|
60
|
+
"""Capture each candidate across one complete repeated-loop forward."""
|
|
61
|
+
rows = [[] for _ in candidates]
|
|
62
|
+
hooks = []
|
|
63
|
+
for candidate, outputs in zip(candidates, rows):
|
|
64
|
+
hooks.append(
|
|
65
|
+
candidate.module.register_forward_hook(
|
|
66
|
+
lambda _module, _args, output, outputs=outputs:
|
|
67
|
+
outputs.append(output.detach().clone())
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
try:
|
|
71
|
+
with torch.no_grad():
|
|
72
|
+
forward()
|
|
73
|
+
finally:
|
|
74
|
+
for hook in hooks:
|
|
75
|
+
hook.remove()
|
|
76
|
+
return tuple(tuple(outputs) for outputs in rows)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def bind_cross_attention_kv(
|
|
80
|
+
candidates: Sequence[CrossKvCandidate],
|
|
81
|
+
captures: Sequence[Sequence[torch.Tensor]],
|
|
82
|
+
*,
|
|
83
|
+
replacements: Mapping[str, torch.nn.Module] | None = None,
|
|
84
|
+
) -> tuple[dict[str, StaticOutput], tuple[StaticOutput, ...]]:
|
|
85
|
+
"""Bind K/V buffers, optionally consuming already-bound projections."""
|
|
86
|
+
replacements = replacements or {}
|
|
87
|
+
modules = []
|
|
88
|
+
for candidate in candidates:
|
|
89
|
+
replacement = replacements.get(candidate.path, candidate.module)
|
|
90
|
+
if getattr(replacement, "_frt_requires_sibling_order", False):
|
|
91
|
+
# A StashReader (and any equivalent composed tail) is not a
|
|
92
|
+
# projection in isolation: it reads data produced by a sibling.
|
|
93
|
+
# Refresh happens outside that sibling call order, so recompute
|
|
94
|
+
# from the candidate's real projection instead of copying stale
|
|
95
|
+
# stash contents into the cadence buffer.
|
|
96
|
+
replacement = candidate.module
|
|
97
|
+
modules.append(replacement)
|
|
98
|
+
statics, _ = bind_cadence_static(modules, captures)
|
|
99
|
+
return (
|
|
100
|
+
{
|
|
101
|
+
candidate.path: static
|
|
102
|
+
for candidate, static in zip(candidates, statics)
|
|
103
|
+
},
|
|
104
|
+
tuple(statics),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def refresh_cross_attention_kv(
|
|
109
|
+
statics: Sequence[StaticOutput],
|
|
110
|
+
encoder_hidden_states: torch.Tensor,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Refresh all K/V buffers once before the repeated attention loop."""
|
|
113
|
+
with torch.no_grad():
|
|
114
|
+
for static in statics:
|
|
115
|
+
static.buffer.copy_(
|
|
116
|
+
static.host_module(encoder_hidden_states)
|
|
117
|
+
)
|
|
118
|
+
static.refreshed()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def wire_refresh_to_producer(
|
|
122
|
+
model: torch.nn.Module,
|
|
123
|
+
statics: Sequence[StaticOutput],
|
|
124
|
+
forward: Callable[[], object],
|
|
125
|
+
):
|
|
126
|
+
"""Wire the K/V refresh into the producing module's own forward.
|
|
127
|
+
|
|
128
|
+
The manual :func:`refresh_cross_attention_kv` form leaves the
|
|
129
|
+
refresh outside the hot path. That is the right split when only the
|
|
130
|
+
fast loop is captured — but a *whole-pipeline* capture then records
|
|
131
|
+
an encoder whose output feeds nothing: the banks are written outside
|
|
132
|
+
the graph, so replaying on a new observation silently reuses the old
|
|
133
|
+
encoding. This wires the split shut: one probe forward identifies
|
|
134
|
+
the module whose output tensor the statics' host projections consume
|
|
135
|
+
(by object identity), and a forward hook on that producer refreshes
|
|
136
|
+
every bank whenever it runs. Eager, compiled and captured forms all
|
|
137
|
+
carry the observation through; within one call the banks are still
|
|
138
|
+
written once and read every loop step, so the cadence saving stands.
|
|
139
|
+
|
|
140
|
+
Returns ``(producer, handle)``; ``handle.remove()`` unwires.
|
|
141
|
+
Raises ``ValueError`` when no single producer can be identified —
|
|
142
|
+
the caller keeps the explicit-refresh contract in that case.
|
|
143
|
+
"""
|
|
144
|
+
if not statics:
|
|
145
|
+
raise ValueError("cadence_static: no statics to wire")
|
|
146
|
+
consumed: dict[int, None] = {}
|
|
147
|
+
probes = []
|
|
148
|
+
for static in statics:
|
|
149
|
+
def grab(_module, args, _consumed=consumed):
|
|
150
|
+
if args and torch.is_tensor(args[0]):
|
|
151
|
+
_consumed[id(args[0])] = None
|
|
152
|
+
probes.append(static.register_forward_pre_hook(grab))
|
|
153
|
+
produced: dict[int, torch.nn.Module] = {}
|
|
154
|
+
|
|
155
|
+
def note(module, _args, output):
|
|
156
|
+
if torch.is_tensor(output):
|
|
157
|
+
# parents fire after children, so an identity-preserving
|
|
158
|
+
# wrapper chain resolves to its outermost module
|
|
159
|
+
produced[id(output)] = module
|
|
160
|
+
|
|
161
|
+
watchers = [module.register_forward_hook(note)
|
|
162
|
+
for _, module in model.named_modules()]
|
|
163
|
+
try:
|
|
164
|
+
with torch.no_grad():
|
|
165
|
+
forward()
|
|
166
|
+
finally:
|
|
167
|
+
for hook in probes + watchers:
|
|
168
|
+
hook.remove()
|
|
169
|
+
producers = {id(produced[x]): produced[x]
|
|
170
|
+
for x in consumed if x in produced}
|
|
171
|
+
if len(producers) != 1:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"cadence_static: could not identify one producer module for "
|
|
174
|
+
f"the cross-attention statics ({len(producers)} candidate(s) "
|
|
175
|
+
"matched by tensor identity)")
|
|
176
|
+
(producer,) = producers.values()
|
|
177
|
+
|
|
178
|
+
def refresh(_module, _args, output):
|
|
179
|
+
if not torch.is_tensor(output):
|
|
180
|
+
return None
|
|
181
|
+
with torch.no_grad():
|
|
182
|
+
for static in statics:
|
|
183
|
+
static.buffer.copy_(static.host_module(output))
|
|
184
|
+
static.refreshed()
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return producer, producer.register_forward_hook(refresh)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Shared chain elements: the parts every region candidate assembles.
|
|
2
|
+
|
|
3
|
+
A region family owns two things — a structural identifier and an
|
|
4
|
+
assembly recipe. Everything a recipe *uses* that another recipe could
|
|
5
|
+
use too lives here: weight packing, layout equivalences, activation
|
|
6
|
+
checks, cache duck-typing, and the attention ladder. Keeping the
|
|
7
|
+
elements out of any one family is what keeps a family thin enough to
|
|
8
|
+
read as its recipe, and what keeps two hosts' chains assembling the
|
|
9
|
+
same certified parts instead of drifting copies.
|
|
10
|
+
|
|
11
|
+
- :func:`fp8_weight` — per-tensor static FP8 packing with the scale
|
|
12
|
+
returned for alpha folding.
|
|
13
|
+
- :func:`interleave_rows` — the rotate-half ↔ adjacent-pair rotation
|
|
14
|
+
equivalence, applied to projection rows at pack time; attention dot
|
|
15
|
+
products are invariant under a shared head-dim permutation.
|
|
16
|
+
- :func:`gelu_tanh_like` — a numeric activation check: the host's
|
|
17
|
+
callable against tanh-GELU, never a class name.
|
|
18
|
+
- :func:`cache_kv` — per-layer K/V access across cache generations.
|
|
19
|
+
- :data:`ATTN_RUNGS` / :func:`attention_rungs` — the attention
|
|
20
|
+
element ladder: the house CuTe FA4 runtime first (D256 2CTA for
|
|
21
|
+
single-KV stacks), the FA2 used-keys entry after it. A rung that
|
|
22
|
+
loads but cannot execute is eliminated by the binder's functional
|
|
23
|
+
probe at the bound shapes, never by a device list.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Callable
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from . import KernelUnavailable, hub_kernel
|
|
33
|
+
|
|
34
|
+
FP8_MAX = 448.0
|
|
35
|
+
|
|
36
|
+
ATTN_RUNGS = (("fa4_cute", "flashrt/fa4-cute-runtime", ">=1",
|
|
37
|
+
"forward_static"),
|
|
38
|
+
("fa2_seqused", "flashrt/fa2-seqused-runtime", ">=1",
|
|
39
|
+
"forward_seqused_static"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def attention_rungs() -> list[tuple[str, object]]:
|
|
43
|
+
rungs = []
|
|
44
|
+
for mode, repo, version, symbol in ATTN_RUNGS:
|
|
45
|
+
try:
|
|
46
|
+
kern = hub_kernel(repo, version)
|
|
47
|
+
except KernelUnavailable:
|
|
48
|
+
continue
|
|
49
|
+
if hasattr(kern, symbol):
|
|
50
|
+
rungs.append((mode, kern))
|
|
51
|
+
return rungs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fp8_weight(w: torch.Tensor) -> tuple[torch.Tensor, float]:
|
|
55
|
+
w = w.detach().to("cuda", torch.float32)
|
|
56
|
+
scale = float(w.abs().amax()) / FP8_MAX
|
|
57
|
+
if scale <= 0.0:
|
|
58
|
+
scale = 1.0
|
|
59
|
+
packed = (w / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
|
|
60
|
+
return packed.contiguous(), scale
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def interleave_rows(w: torch.Tensor, heads: int,
|
|
64
|
+
head_dim: int) -> torch.Tensor:
|
|
65
|
+
"""Permute projection rows so adjacent-pair rotation carries the
|
|
66
|
+
host's rotate-half convention."""
|
|
67
|
+
half = head_dim // 2
|
|
68
|
+
w = w.reshape(heads, head_dim, w.shape[-1])
|
|
69
|
+
out = torch.empty_like(w)
|
|
70
|
+
out[:, 0::2] = w[:, :half]
|
|
71
|
+
out[:, 1::2] = w[:, half:]
|
|
72
|
+
return out.reshape(heads * head_dim, w.shape[-1])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cache_kv(cache, idx: int):
|
|
76
|
+
layers = getattr(cache, "layers", None)
|
|
77
|
+
if layers is not None:
|
|
78
|
+
return layers[idx].keys, layers[idx].values
|
|
79
|
+
return cache.key_cache[idx], cache.value_cache[idx]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def gelu_tanh_like(act: Callable) -> bool:
|
|
83
|
+
t = torch.linspace(-4, 4, 65, device="cuda", dtype=torch.bfloat16)
|
|
84
|
+
try:
|
|
85
|
+
got = act(t)
|
|
86
|
+
except Exception: # noqa: BLE001 — a weird act refuses, not kills
|
|
87
|
+
return False
|
|
88
|
+
ref = torch.nn.functional.gelu(t.float(), approximate="tanh")
|
|
89
|
+
return bool(torch.allclose(got.float(), ref, atol=2e-2))
|
|
File without changes
|