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