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,102 @@
1
+ """The adarms_stack region family: structural identification, candidates.
2
+
3
+ The identifier matches shape, never names: a module carrying a
4
+ ``layers`` list whose blocks each hold a bias-free attention group
5
+ (``q_proj``/``k_proj``/``v_proj``/``o_proj``), a bias-free gated FFN
6
+ (``gate_proj``/``up_proj``/``down_proj``), and *conditioned* norms — a
7
+ ``dense`` projection emitting three modulation vectors per norm is
8
+ what separates this stack from an ordinary decoder tower — plus the
9
+ stack-level tail (a conditioned final norm and a rotary table). The
10
+ key width must be narrower than the query width: the fused chain's
11
+ cache layout is written for the single-KV band, and a stack outside
12
+ it is exactly what the candidate's own fact checks exist to refuse.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import torch
18
+
19
+ from . import fp8_chain
20
+ from ... import regions
21
+
22
+
23
+ def _plain_linear(mod) -> bool:
24
+ return isinstance(mod, torch.nn.Linear) and mod.bias is None
25
+
26
+
27
+ def _ada_norm(mod, dim: int) -> bool:
28
+ dense = getattr(mod, "dense", None)
29
+ return (isinstance(dense, torch.nn.Linear)
30
+ and dense.out_features == 3 * dim)
31
+
32
+
33
+ def _block_ok(block, dim: int) -> bool:
34
+ attn = getattr(block, "self_attn", None)
35
+ mlp = getattr(block, "mlp", None)
36
+ if attn is None or mlp is None:
37
+ return False
38
+ if not all(_plain_linear(getattr(attn, a, None))
39
+ for a in ("q_proj", "k_proj", "v_proj", "o_proj")):
40
+ return False
41
+ if not all(_plain_linear(getattr(mlp, a, None))
42
+ for a in ("gate_proj", "up_proj", "down_proj")):
43
+ return False
44
+ if mlp.gate_proj.out_features != mlp.up_proj.out_features:
45
+ return False
46
+ if not _ada_norm(getattr(block, "input_layernorm", None), dim):
47
+ return False
48
+ return _ada_norm(getattr(block, "post_attention_layernorm", None),
49
+ dim)
50
+
51
+
52
+ def identify(model) -> list[str]:
53
+ roots = []
54
+ for path, mod in model.named_modules():
55
+ layers = getattr(mod, "layers", None)
56
+ if not isinstance(layers, torch.nn.ModuleList) or len(layers) < 2:
57
+ continue
58
+ if not callable(getattr(mod, "rotary_emb", None)):
59
+ continue
60
+ head = layers[0]
61
+ attn = getattr(head, "self_attn", None)
62
+ if attn is None or not isinstance(
63
+ getattr(attn, "q_proj", None), torch.nn.Linear):
64
+ continue
65
+ dim = attn.q_proj.in_features
66
+ if not _ada_norm(getattr(mod, "norm", None), dim):
67
+ continue
68
+ if not all(_block_ok(b, dim) for b in layers):
69
+ continue
70
+ if attn.k_proj.out_features >= attn.q_proj.out_features:
71
+ continue
72
+ roots.append(path)
73
+ return roots
74
+
75
+
76
+ def _band_candidate(band: str, row: dict) -> regions.RegionCandidate:
77
+ return regions.RegionCandidate(
78
+ name=f"{band}_chain",
79
+ missing=lambda band=band: fp8_chain.missing_symbols(band=band),
80
+ bind=lambda model, root, probe, band=band:
81
+ fp8_chain.bind_adarms_fp8_chain(model, root, probe,
82
+ band=band),
83
+ precision_rank=row["precision_rank"],
84
+ )
85
+
86
+
87
+ #: the candidate list is generated from the band table — adding a
88
+ #: precision band to the family is a table row, not new wiring here
89
+ FAMILY = regions.RegionFamily(
90
+ family="adarms_stack",
91
+ identify=identify,
92
+ candidates=[_band_candidate(band, row)
93
+ for band, row in fp8_chain.BANDS.items()],
94
+ )
95
+
96
+
97
+ def register() -> None:
98
+ """(Re-)register the family — idempotent, import calls it once."""
99
+ regions.register_region_family(FAMILY)
100
+
101
+
102
+ register()
@@ -0,0 +1,132 @@
1
+ import torch
2
+
3
+ from .fa2_seqused import (DenseAttention, PackedKVAttention,
4
+ bind_attention_core,
5
+ bind_dense_attention, plan_packed_kv)
6
+ from .two_way_fa2 import FactoredTwoWayAttention, bind_two_way_attention
7
+
8
+
9
+ def bind_dense_attention_best(captures):
10
+ """Dense attention across the variant family, precision-descending.
11
+
12
+ One structure, parallel executable forms, and no second hardware
13
+ table: each variant's kernel package declares its own archs (or
14
+ ships build variants only for the hosts it serves), so the device
15
+ split *is* the refusal machinery. The order is precision first:
16
+ FA2 (BF16, numerics preserving) binds wherever its runtime
17
+ executes — every current receipt keeps its exact form. Then the
18
+ FA4 CuTe DSL form (BF16, the SM100-family hot path), the
19
+ allocation-free masked MHA (BF16/FP16, batch-of-one sites), and
20
+ last the FP8 FA4 form. All are judged by the same downstream
21
+ gates; a device none serves keeps the host's own attention.
22
+
23
+ The winner carries the trail: ``_frt_variant`` names the bound
24
+ form and ``_frt_variant_trail`` holds what each preferred variant
25
+ said when it stepped aside. Without that, a host whose preferred
26
+ package is merely absent looks identical to one where it was
27
+ weighed and rejected — the two need different fixes, and only the
28
+ trail tells them apart.
29
+ """
30
+ # a package can refuse a device three ways: its arch declaration
31
+ # (ValueError from the loader's metadata check), the kernels
32
+ # library finding no build variant for the host (OSError), or a
33
+ # bind smoke the runtime cannot execute (RuntimeError) — each
34
+ # means "not this variant here", never an error for the family
35
+ def _fa2(caps):
36
+ return bind_dense_attention(caps)
37
+
38
+ def _fa4_cute(caps):
39
+ from . import fa4_cute
40
+ return fa4_cute.bind_dense_attention(caps)
41
+
42
+ def _masked_mha(caps):
43
+ from . import masked_mha
44
+ return masked_mha.bind_dense_attention(caps)
45
+
46
+ def _fa4_fp8(caps):
47
+ from . import fa4_fp8
48
+ return fa4_fp8.bind_dense_attention(caps)
49
+
50
+ refusals, declined = [], 0
51
+ for name, binder in (("fa2", _fa2), ("fa4_cute", _fa4_cute),
52
+ ("masked_mha", _masked_mha),
53
+ ("fa4_fp8", _fa4_fp8)):
54
+ try:
55
+ core = binder(captures)
56
+ except (ValueError, RuntimeError, OSError) as refusal:
57
+ refusals.append(f"{name}: {str(refusal)[:120]}")
58
+ continue
59
+ if core is not None and not _beats_host(core, captures[0]):
60
+ refusals.append(
61
+ f"{name}: bound but measured slower than the host "
62
+ "attention at the captured shape — stepped aside")
63
+ declined += 1
64
+ core = None
65
+ if core is not None:
66
+ # the seam is served, but which variant served it and what
67
+ # the preferred ones said are both load-bearing facts: a
68
+ # host silently falling back to a lower-precision or slower
69
+ # form is exactly the failure the ordering exists to make
70
+ # visible, and it is invisible unless the superseded
71
+ # refusals travel with the bound module
72
+ core._frt_variant = name
73
+ core._frt_variant_trail = tuple(refusals)
74
+ return core
75
+ declined += 1
76
+ refusals.append(f"{name}: declined the captured shape form")
77
+ if declined:
78
+ # at least one variant executed its qualification and declined
79
+ # the shape form — a site-level refusal the adapter records,
80
+ # same contract as a single binder returning None
81
+ return None
82
+ raise ValueError(
83
+ "attention_core: no variant serves this device — "
84
+ + "; ".join(refusals))
85
+
86
+
87
+ def _beats_host(core, capture, margin: float = 0.02, iters: int = 20):
88
+ """The family's speed gate: a variant seats only if it measures at
89
+ least as fast as the host's own attention on the captured shape.
90
+ Availability and precision order decide who gets weighed first;
91
+ this decides whether the winner actually serves — bands are
92
+ measured, not conceded, in this family too."""
93
+ import torch.nn.functional as F
94
+
95
+ query = capture.get("q")
96
+ key = capture.get("key", capture.get("k"))
97
+ value = capture.get("value", capture.get("v"))
98
+ mask = capture.get("mask")
99
+ if query is None or key is None or value is None:
100
+ return True
101
+ if not query.is_cuda:
102
+ return True
103
+
104
+ def _time(fn):
105
+ with torch.no_grad():
106
+ for _ in range(4):
107
+ fn()
108
+ torch.cuda.synchronize()
109
+ start = torch.cuda.Event(True)
110
+ end = torch.cuda.Event(True)
111
+ start.record()
112
+ for _ in range(iters):
113
+ fn()
114
+ end.record()
115
+ torch.cuda.synchronize()
116
+ return start.elapsed_time(end) / iters
117
+
118
+ try:
119
+ ours = _time(lambda: core(query, key, value))
120
+ host = _time(lambda: F.scaled_dot_product_attention(
121
+ query, key, value, attn_mask=mask))
122
+ except (RuntimeError, ValueError):
123
+ return False
124
+ return ours <= host * (1.0 + margin)
125
+
126
+
127
+ __all__ = ["DenseAttention", "PackedKVAttention",
128
+ "bind_attention_core", "bind_dense_attention",
129
+ "bind_dense_attention_best", "plan_packed_kv",
130
+ "FactoredTwoWayAttention", "bind_two_way_attention"]
131
+ # variant modules (fa4_fp8, fa4_cute, masked_mha) import lazily inside
132
+ # the family binder: loading one must not require the others' runtimes
@@ -0,0 +1,458 @@
1
+ """attention_core — run attention on the FlashRT FA2 kernel.
2
+
3
+ A fused attention kernel wants contiguous keys and values. This module
4
+ provides two executable forms: a cadence-aware packed-KV form for decoder
5
+ loops, and a stateless dense form for hosts that supply complete Q/K/V on
6
+ every call. Both reduce supported masks to contiguous allowed key ranges
7
+ before dispatching one dense attention call.
8
+
9
+ The packed prefix (everything before the blocked run) belongs to a
10
+ slower cadence — it is the encoder's output for the current
11
+ observation, unchanged across the denoise loop — so it is filled once
12
+ at bind time and refreshed through an update function, exactly as
13
+ :mod:`..cadence_static` does for whole modules. The suffix is written
14
+ per call.
15
+
16
+ Three qualifications, all decided from real captures:
17
+
18
+ - ``head_dim`` must be one the kernel supports; otherwise the caller
19
+ keeps its own path (``bind_attention_core`` returns ``None`` so the
20
+ host can fall back to a community kernel rather than fail),
21
+ - every query row must see the same mask, and the blocked positions
22
+ must form a single contiguous run — anything else is not expressible
23
+ as a packed dense attention,
24
+ - the prefix keys must not move across the loop; a moving prefix means
25
+ the split is wrong and the parity gate would catch it downstream, so
26
+ it is rejected here where the reason is still legible.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass
32
+ from functools import lru_cache
33
+
34
+ import torch
35
+
36
+ from .. import hub_kernel
37
+ from ...guard import PROCEED, GuardedSeam
38
+
39
+ @lru_cache(maxsize=1)
40
+ def supported_head_dims() -> tuple[int, ...]:
41
+ """Read the executable envelope from the installed Hub artifact."""
42
+ package = hub_kernel("flashrt/fa2-seqused-runtime", ">=1")
43
+ advertised = getattr(package, "SUPPORTED_HEAD_DIMS", None)
44
+ if advertised is None:
45
+ raise ValueError(
46
+ "attention_core: FA2 Hub artifact does not advertise "
47
+ "SUPPORTED_HEAD_DIMS; refusing to duplicate backend capability "
48
+ "inside the structure layer")
49
+ dims = tuple(sorted({int(dim) for dim in advertised}))
50
+ if not dims or any(dim <= 0 for dim in dims):
51
+ raise ValueError(
52
+ "attention_core: FA2 Hub artifact advertised invalid head dims")
53
+ return dims
54
+
55
+
56
+ @dataclass
57
+ class _Scratch:
58
+ """Output/LSE/workspace shared by same-shaped attention sites."""
59
+
60
+ out: torch.Tensor
61
+ lse: torch.Tensor
62
+ workspace: object
63
+
64
+
65
+ @dataclass
66
+ class PackedKVPlan:
67
+ """How the host's masked attention maps onto a dense one."""
68
+
69
+ prefix: int
70
+ suffix_start: int
71
+ suffix_len: int
72
+ seq_kv: int
73
+
74
+ @property
75
+ def packed(self) -> int:
76
+ return self.prefix + self.suffix_len
77
+
78
+
79
+ def plan_packed_kv(mask: torch.Tensor | None, seq_kv: int) -> PackedKVPlan:
80
+ """Derive the packing plan from one captured attention mask."""
81
+ if mask is None:
82
+ return PackedKVPlan(seq_kv, seq_kv, 0, seq_kv)
83
+ if mask.dim() < 3:
84
+ raise ValueError("attention_core: unexpected mask rank")
85
+ rows = mask.reshape(-1, mask.shape[-2], mask.shape[-1])[0]
86
+ if not bool((rows == rows[0]).all()):
87
+ raise ValueError("attention_core: mask differs per query row")
88
+ row = rows[0].float()
89
+ blocked = ((row < -1e5) | row.isneginf()).nonzero().flatten()
90
+ if blocked.numel() == 0:
91
+ return PackedKVPlan(seq_kv, seq_kv, 0, seq_kv)
92
+ runs, start, prev = [], None, None
93
+ for i in blocked.tolist():
94
+ if start is None:
95
+ start = prev = i
96
+ elif i == prev + 1:
97
+ prev = i
98
+ else:
99
+ runs.append((start, prev))
100
+ start = prev = i
101
+ runs.append((start, prev))
102
+ if len(runs) != 1:
103
+ raise ValueError(
104
+ f"attention_core: mask blocks {len(runs)} separate runs — "
105
+ "not expressible as one packed dense attention")
106
+ lo, hi = runs[0]
107
+ return PackedKVPlan(lo, hi + 1, seq_kv - (hi + 1), seq_kv)
108
+
109
+
110
+ class PackedKVAttention(GuardedSeam, torch.nn.Module):
111
+ """Attention over packed keys/values, on the FlashRT FA2 kernel.
112
+
113
+ Holds one host module's packed buffers. Call it in place of the
114
+ host's attention body; the prefix half is refreshed by the update
115
+ function returned from :func:`bind_attention_core`.
116
+
117
+ The packed region, the output and the split-KV workspace are all
118
+ allocated for one query length, and this module is reached through a
119
+ routed call rather than a module path, so there is no host module to
120
+ revert to per call: a query outside the calibrated form raises. That
121
+ is the whole reason to check — handing the kernel a scratch buffer
122
+ sized for a different sequence is the failure that does not announce
123
+ itself.
124
+ """
125
+
126
+ def __init__(self, plan: PackedKVPlan, q_shape, kv_heads: int,
127
+ dtype: torch.dtype, device, prefix_kv=None,
128
+ scratch: "_Scratch | None" = None):
129
+ super().__init__()
130
+ self.plan = plan
131
+ b, heads, seq_q, head_dim = q_shape
132
+ self.seq_q = seq_q
133
+ # set by alias_suffix when a producer writes that side in place
134
+ self._alias_k = False
135
+ self._alias_v = False
136
+ self._kfa = hub_kernel("flashrt/fa2-seqused-runtime", ">=1")
137
+ self.register_buffer("packed_k", torch.zeros(
138
+ b, plan.packed, kv_heads, head_dim, device=device,
139
+ dtype=dtype))
140
+ self.register_buffer("packed_v", torch.zeros_like(self.packed_k))
141
+ if prefix_kv is not None:
142
+ k0, v0 = prefix_kv
143
+ self.packed_k[:, :plan.prefix] = k0
144
+ self.packed_v[:, :plan.prefix] = v0
145
+ # Output, LSE and split-KV workspace are scratch: each site
146
+ # consumes its result before the next one runs, so sites with
147
+ # the same shapes share one set. Per-site copies cost real
148
+ # latency (pi05 r16: 18 private scratches ran 1.5% slower than
149
+ # one shared set) and buy nothing.
150
+ if scratch is None:
151
+ q_sample = torch.empty(b, seq_q, heads, head_dim,
152
+ device=device, dtype=dtype)
153
+ out, lse = self._kfa.allocate_outputs(q_sample)
154
+ scratch = _Scratch(out, lse, self._kfa.allocate_workspace(
155
+ q_sample, self.packed_k))
156
+ self._scratch = scratch
157
+ self._frt_arm(dtypes=(dtype,), device=self.packed_k.device,
158
+ k=int(head_dim), rows=int(b * heads * seq_q))
159
+
160
+ def forward(self, query, key, value, *, scale=None):
161
+ """``query``/``key``/``value`` in the host's (B, H, S, D)."""
162
+ admitted = self._frt_admit(query)
163
+ if admitted is not PROCEED: # unreachable: no host path
164
+ return admitted # to revert to, so it raises
165
+ plan = self.plan
166
+ q = query.transpose(1, 2).contiguous()
167
+ if plan.suffix_len:
168
+ self.packed_k[:, plan.prefix:].copy_(
169
+ key.transpose(1, 2)[:, plan.suffix_start:])
170
+ self.packed_v[:, plan.prefix:].copy_(
171
+ value.transpose(1, 2)[:, plan.suffix_start:])
172
+ sc = self._scratch
173
+ return self._kfa.forward_static(
174
+ q, self.packed_k, self.packed_v, out=sc.out,
175
+ softmax_lse=sc.lse, workspace=sc.workspace,
176
+ softmax_scale=scale)
177
+
178
+ def alias_suffix(self, *, key: bool = False, value: bool = False):
179
+ """Hand out the packed suffix rows for a producer to write into.
180
+
181
+ The declarative form of what a hand-written runtime does when it
182
+ gives the next stage a pointer: instead of the producer filling
183
+ its own buffer and this module copying it in, the producer's
184
+ output *is* the region.
185
+
186
+ Only legal where nothing transforms the tensor between the two —
187
+ a rotary embedding applied after the projection would leave the
188
+ untransformed values here. Callers therefore alias key and value
189
+ independently, and take ``None`` for whatever does not qualify.
190
+ """
191
+ plan = self.plan
192
+ if not plan.suffix_len or self.packed_k.shape[0] != 1:
193
+ return None, None # only a single batch row slices
194
+ regions = [] # into a contiguous suffix
195
+ for want, packed, flag in ((key, self.packed_k, "_alias_k"),
196
+ (value, self.packed_v, "_alias_v")):
197
+ if not want:
198
+ regions.append(None)
199
+ continue
200
+ region = packed[0, plan.prefix:]
201
+ if not region.is_contiguous():
202
+ regions.append(None)
203
+ continue
204
+ setattr(self, flag, True)
205
+ regions.append(region)
206
+ return tuple(regions)
207
+
208
+ def forward_suffix(self, query, key, value, *, scale=None):
209
+ """Kernel layout (B, S, H, D), with **suffix-only** keys/values.
210
+
211
+ Two things separate this from :meth:`forward`. The layout: the
212
+ host builds (B, H, S, D) because that is what eager SDPA wants
213
+ and :meth:`forward` transposes it back — two cancelling
214
+ transposes plus the copies that make each of them contiguous. A
215
+ caller that owns the whole attention sublayer never builds the
216
+ host layout at all; the projections' own output view *is* this
217
+ one. And the extent: the host hands attention the full
218
+ cache-concatenated KV and the suffix gets sliced back out here,
219
+ while a sublayer's projections produce exactly the new tokens —
220
+ which is the suffix already. Binding checks that equality rather
221
+ than assuming it.
222
+ """
223
+ # the contract is the same one ``forward`` checks: it counts rows
224
+ # against the head dim, which is what both layouts agree on
225
+ admitted = self._frt_admit(query)
226
+ if admitted is not PROCEED:
227
+ return admitted
228
+ plan = self.plan
229
+ q = query if query.is_contiguous() else query.contiguous()
230
+ if plan.suffix_len:
231
+ # an aliased side already wrote itself here
232
+ if not self._alias_k:
233
+ self.packed_k[:, plan.prefix:].copy_(key)
234
+ if not self._alias_v:
235
+ self.packed_v[:, plan.prefix:].copy_(value)
236
+ sc = self._scratch
237
+ return self._kfa.forward_static(
238
+ q, self.packed_k, self.packed_v, out=sc.out,
239
+ softmax_lse=sc.lse, workspace=sc.workspace,
240
+ softmax_scale=scale)
241
+
242
+
243
+ class DenseAttention(GuardedSeam, torch.nn.Module):
244
+ """FA2 replacement for an ordinary dense SDPA call.
245
+
246
+ Unlike :class:`PackedKVAttention`, this form owns no observation-cadence
247
+ state. Every call receives the complete Q/K/V tensors, so it is suitable
248
+ for Diffusers self- and cross-attention and remains correct when the
249
+ conditioning changes between graph replays.
250
+
251
+ Inputs and outputs use the host SDPA layout ``[B, H, S, D]``. The package
252
+ consumes ``[B, S, H, D]``; reversing the host's projection view is normally
253
+ already contiguous and therefore does not materialise a transpose.
254
+ """
255
+
256
+ def __init__(
257
+ self, q_shape, kv_shape, dtype: torch.dtype, device,
258
+ allowed_ranges=None, scratch: "_Scratch | None" = None,
259
+ ):
260
+ super().__init__()
261
+ b, heads, seq_q, head_dim = q_shape
262
+ kb, kv_heads, seq_kv, kv_dim = kv_shape
263
+ if kb != b or kv_dim != head_dim:
264
+ raise ValueError(
265
+ "attention_core dense: Q and KV batch/head dimensions differ")
266
+ if heads % kv_heads:
267
+ raise ValueError(
268
+ "attention_core dense: query heads must be divisible by "
269
+ "KV heads")
270
+ self.q_shape = tuple(q_shape)
271
+ self.kv_shape = tuple(kv_shape)
272
+ self.allowed_ranges = tuple(allowed_ranges or ())
273
+ self._kfa = hub_kernel("flashrt/fa2-seqused-runtime", ">=1")
274
+ q_sample = torch.empty(
275
+ b, seq_q, heads, head_dim, device=device, dtype=dtype)
276
+ packed_seq = (
277
+ sum(hi - lo for lo, hi in self.allowed_ranges)
278
+ if self.allowed_ranges else seq_kv)
279
+ kv_sample = torch.empty(
280
+ b, packed_seq, kv_heads, head_dim, device=device, dtype=dtype)
281
+ if self.allowed_ranges:
282
+ self.register_buffer("packed_k", torch.empty_like(kv_sample))
283
+ self.register_buffer("packed_v", torch.empty_like(kv_sample))
284
+ if scratch is None:
285
+ out, lse = self._kfa.allocate_outputs(q_sample)
286
+ scratch = _Scratch(
287
+ out, lse, self._kfa.allocate_workspace(q_sample, kv_sample))
288
+ elif (
289
+ scratch.out.shape != q_sample.shape
290
+ or scratch.out.dtype != dtype
291
+ or scratch.out.device != q_sample.device
292
+ or scratch.lse.shape != (b, heads, seq_q)
293
+ or scratch.lse.device != q_sample.device
294
+ ):
295
+ raise ValueError(
296
+ "attention_core dense: shared scratch does not match "
297
+ "the bound attention form"
298
+ )
299
+ self._scratch = scratch
300
+ self._frt_arm(
301
+ dtypes=(dtype,), device=q_sample.device, k=int(head_dim),
302
+ rows=int(b * heads * seq_q))
303
+
304
+ def forward(self, query, key, value, *, scale=None):
305
+ admitted = self._frt_admit(query)
306
+ if admitted is not PROCEED:
307
+ return admitted
308
+ if tuple(query.shape) != self.q_shape:
309
+ raise ValueError(
310
+ "attention_core dense: query shape moved from "
311
+ f"{self.q_shape} to {tuple(query.shape)}")
312
+ if tuple(key.shape) != self.kv_shape or value.shape != key.shape:
313
+ raise ValueError(
314
+ "attention_core dense: key/value shape moved from "
315
+ f"{self.kv_shape} to {tuple(key.shape)}/{tuple(value.shape)}")
316
+ q = query.transpose(1, 2)
317
+ k = key.transpose(1, 2)
318
+ v = value.transpose(1, 2)
319
+ if not q.is_contiguous():
320
+ q = q.contiguous()
321
+ if self.allowed_ranges:
322
+ offset = 0
323
+ for lo, hi in self.allowed_ranges:
324
+ length = hi - lo
325
+ self.packed_k[:, offset:offset + length].copy_(k[:, lo:hi])
326
+ self.packed_v[:, offset:offset + length].copy_(v[:, lo:hi])
327
+ offset += length
328
+ k, v = self.packed_k, self.packed_v
329
+ else:
330
+ if not k.is_contiguous():
331
+ k = k.contiguous()
332
+ if not v.is_contiguous():
333
+ v = v.contiguous()
334
+ sc = self._scratch
335
+ out = self._kfa.forward_static(
336
+ q, k, v, out=sc.out, softmax_lse=sc.lse,
337
+ workspace=sc.workspace, softmax_scale=scale)
338
+ return out.transpose(1, 2)
339
+
340
+
341
+ def _allowed_ranges(mask):
342
+ if mask is None:
343
+ return ()
344
+ rows = mask.reshape(-1, mask.shape[-1])
345
+ first = rows[0]
346
+ if mask.dtype == torch.bool:
347
+ allowed = first
348
+ if not bool((rows == first).all()):
349
+ raise ValueError(
350
+ "attention_core dense: mask differs per query row")
351
+ else:
352
+ allowed = ~((first.float() < -1e5) | first.float().isneginf())
353
+ other = ~((rows.float() < -1e5) | rows.float().isneginf())
354
+ if not bool((other == allowed).all()):
355
+ raise ValueError(
356
+ "attention_core dense: mask differs per query row")
357
+ indices = allowed.nonzero().flatten().tolist()
358
+ if not indices:
359
+ raise ValueError("attention_core dense: mask permits no keys")
360
+ ranges = []
361
+ for index in indices:
362
+ if not ranges or index != ranges[-1][1]:
363
+ ranges.append([index, index + 1])
364
+ else:
365
+ ranges[-1][1] += 1
366
+ if len(ranges) > 8:
367
+ # the packed-copy loop is linear in segments; past a handful
368
+ # the copies outweigh the masked-out keys and the masked
369
+ # executable form serves better
370
+ return None
371
+ if len(ranges) == 1 and ranges[0] == [0, mask.shape[-1]]:
372
+ return ()
373
+ return tuple((lo, hi) for lo, hi in ranges)
374
+
375
+
376
+ def bind_dense_attention(captures):
377
+ """Bind one stateless dense FA2 core from repeated host captures."""
378
+ if not captures:
379
+ raise ValueError("attention_core dense: no captures")
380
+ first = captures[0]
381
+ query, key, value = first["q"], first["key"], first["value"]
382
+ head_dim = query.shape[-1]
383
+ if head_dim not in supported_head_dims():
384
+ return None
385
+ allowed_ranges = _allowed_ranges(first.get("mask"))
386
+ if allowed_ranges is None:
387
+ return None
388
+ expected = (tuple(query.shape), tuple(key.shape), tuple(value.shape),
389
+ query.dtype, key.dtype, value.dtype)
390
+ for capture in captures[1:]:
391
+ got = (
392
+ tuple(capture["q"].shape),
393
+ tuple(capture["key"].shape),
394
+ tuple(capture["value"].shape),
395
+ capture["q"].dtype,
396
+ capture["key"].dtype,
397
+ capture["value"].dtype,
398
+ )
399
+ if got != expected:
400
+ raise ValueError(
401
+ "attention_core dense: shape, dtype, or mask moved within "
402
+ f"one calibration call: {expected} -> {got}")
403
+ if _allowed_ranges(capture.get("mask")) != allowed_ranges:
404
+ raise ValueError(
405
+ "attention_core dense: mask pattern moved within one "
406
+ "calibration call")
407
+ if not (query.dtype == key.dtype == value.dtype):
408
+ raise ValueError("attention_core dense: Q/K/V dtypes differ")
409
+ return DenseAttention(
410
+ query.shape, key.shape, query.dtype, query.device,
411
+ allowed_ranges=allowed_ranges)
412
+
413
+
414
+ def bind_attention_core(captures, *, prefix_static_rtol: float = 1e-3):
415
+ """Bind one packed-KV attention per site from real captures.
416
+
417
+ ``captures`` is a sequence of per-site dicts holding ``q`` (one
418
+ captured query, host layout), ``keys``/``values`` (the tensors that
419
+ site produced across the hot loop, oldest first) and ``mask``.
420
+ Returns ``(modules, update)``, or ``None`` when the head dim is
421
+ unsupported so the caller can keep a fallback path.
422
+ """
423
+ if not captures:
424
+ raise ValueError("attention_core: no captures")
425
+ head_dim = captures[0]["q"].shape[-1]
426
+ if head_dim not in supported_head_dims():
427
+ return None
428
+
429
+ modules, scratch = [], None
430
+ for site, cap in enumerate(captures):
431
+ keys = cap["keys"]
432
+ plan = plan_packed_kv(cap.get("mask"), keys[0].shape[2])
433
+ first = keys[0][:, :, :plan.prefix]
434
+ for other in keys[1:]:
435
+ if not torch.allclose(first, other[:, :, :plan.prefix],
436
+ rtol=prefix_static_rtol,
437
+ atol=prefix_static_rtol):
438
+ raise ValueError(
439
+ f"attention_core: site {site} prefix keys move "
440
+ "across the loop — the cadence split is wrong")
441
+ q = cap["q"]
442
+ core = PackedKVAttention(
443
+ plan, q.shape, keys[0].shape[1], q.dtype, q.device,
444
+ prefix_kv=(keys[0].transpose(1, 2)[:, :plan.prefix],
445
+ cap["values"][0].transpose(1, 2)[:, :plan.prefix]),
446
+ scratch=scratch)
447
+ scratch = scratch or core._scratch
448
+ modules.append(core)
449
+
450
+ def update(fresh_kv) -> None:
451
+ """Refresh every site's prefix from freshly computed K/V."""
452
+ with torch.no_grad():
453
+ for mod, (k, v) in zip(modules, fresh_kv):
454
+ p = mod.plan.prefix
455
+ mod.packed_k[:, :p].copy_(k.transpose(1, 2)[:, :p])
456
+ mod.packed_v[:, :p].copy_(v.transpose(1, 2)[:, :p])
457
+
458
+ return modules, update