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