flashcodec-kv 1.0.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.
@@ -0,0 +1,159 @@
1
+ """JPEG-style lossy frequency-domain compression: blockwise orthonormal 1D
2
+ DCT-II.
3
+
4
+ Physical intuition: the hidden vectors of adjacent tokens form a strongly
5
+ autocorrelated time-series signal whose energy concentrates in low
6
+ frequencies. Each block is projected onto the first K orthonormal DCT-II basis
7
+ vectors (DC mean + AC linear trend, ...), filtering out high-frequency random
8
+ noise and yielding a geometric r/K compression ratio. Basis rows are
9
+ orthonormal, so keep=r is lossless per block.
10
+
11
+ Tensor contract: input (..., S, D); the compression/filtering axis is the
12
+ second-to-last dimension (seq axis, dim=-2). Everything is batched matmul
13
+ (GEMM), with no explicit Python loops.
14
+ """
15
+
16
+ import math
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+
21
+ _BASIS_CACHE = {}
22
+
23
+
24
+ def dct_basis(r, k, device=None, dtype=None):
25
+ """Precompute the orthonormal DCT-II basis M_enc (k, r):
26
+ M[j, n] = c_j · cos(π/r · (n+½)·j), c_0 = sqrt(1/r), c_j = sqrt(2/r).
27
+ Rows are orthonormal (M @ M.T = I); k == r gives the complete orthonormal
28
+ basis (lossless reconstruction).
29
+ Module-level constant cache, keyed by (r, k)."""
30
+ key = (r, k)
31
+ if key not in _BASIS_CACHE:
32
+ n = torch.arange(r, dtype=torch.float32)
33
+ j = torch.arange(k, dtype=torch.float32)
34
+ c = torch.where(j == 0, math.sqrt(1.0 / r), math.sqrt(2.0 / r))
35
+ m = c[:, None] * torch.cos(math.pi / r * (n[None, :] + 0.5) * j[:, None])
36
+ _BASIS_CACHE[key] = m
37
+ m = _BASIS_CACHE[key]
38
+ if device is not None or dtype is not None:
39
+ m = m.to(device=device, dtype=dtype or m.dtype)
40
+ return m
41
+
42
+
43
+ def _pad_to_blocks(x, r):
44
+ """Zero-pad the seq axis (-2) to a multiple of r; returns (block tensor,
45
+ original length)."""
46
+ S = x.shape[-2]
47
+ n = (S + r - 1) // r
48
+ pad = n * r - S
49
+ if pad:
50
+ x = F.pad(x, (0, 0, 0, pad)) # (..., S, D) pads the -2 axis only
51
+ blocks = x.reshape(*x.shape[:-2], n, r, x.shape[-1])
52
+ return blocks, S
53
+
54
+
55
+ def dct_compress(x, r, k):
56
+ """Blockwise DCT projection: x (..., S, D) -> coeffs (..., n_blocks, k, D)
57
+ (x dtype).
58
+ The seq axis is split into blocks of r (zero-padded at the tail); each
59
+ block is projected along the within-block axis onto the first k DCT-II
60
+ basis vectors (computed in fp32, then cast back to the input dtype — lossy
61
+ modes need no higher precision, and the storage ratio stays honest).
62
+ Compression storage ratio = k/r (same dtype, plus tail padding overhead)."""
63
+ blocks, _ = _pad_to_blocks(x, r)
64
+ basis = dct_basis(r, k, device=x.device, dtype=torch.float32)
65
+ hp = blocks.float().transpose(-2, -1) # (..., n, D, r)
66
+ coef = torch.matmul(hp, basis.t()) # (..., n, D, k)
67
+ return coef.transpose(-2, -1).to(x.dtype) # (..., n, k, D)
68
+
69
+
70
+ def dct_decompress(coeffs, r, out_len=None):
71
+ """Blockwise DCT reconstruction: coeffs (..., n_blocks, k, D) ->
72
+ x (..., n_blocks*r, D).
73
+ Low-frequency coefficients are multiplied by the basis to reconstruct the
74
+ within-block waveform; when out_len is given, the tail padding is trimmed."""
75
+ n, k = coeffs.shape[-3], coeffs.shape[-2]
76
+ basis = dct_basis(r, k, device=coeffs.device, dtype=torch.float32)
77
+ hp = torch.matmul(coeffs.transpose(-2, -1).float(), basis) # (...,n,D,r)
78
+ rec = hp.transpose(-2, -1).reshape(
79
+ *coeffs.shape[:-3], n * r, coeffs.shape[-1])
80
+ if out_len is not None:
81
+ rec = rec[..., :out_len, :]
82
+ return rec
83
+
84
+
85
+ def quantize_coeffs(coeffs, bits=8):
86
+ """Symmetric per-(block, frequency) quantization of DCT coefficients.
87
+
88
+ coeffs (..., n, k, D) -> (q int8, scale fp32 of shape (..., n, k, 1)).
89
+ One scale per block **and** frequency row: the DC row and the AC rows have
90
+ very different magnitudes, and a row-wise scale costs only 4 bytes per row
91
+ (vs D*1 byte for the int8 row itself).
92
+ """
93
+ if bits != 8:
94
+ raise ValueError(f"only 8-bit coefficient quantization is supported, "
95
+ f"got bits={bits}")
96
+ amax = coeffs.abs().amax(dim=-1, keepdim=True) # (...,n,k,1)
97
+ scale = amax / 127.0
98
+ scale = torch.where(scale > 0, scale, torch.ones_like(scale))
99
+ q = torch.round(coeffs.float() / scale).clamp_(-127, 127).to(torch.int8)
100
+ return q, scale.to(torch.float32)
101
+
102
+
103
+ def dequantize_coeffs(q, scale, dtype=torch.float32):
104
+ """int8 coefficients + per-row scale -> coefficients in ``dtype``."""
105
+ return q.to(dtype) * scale.to(dtype)
106
+
107
+
108
+ class JPEGCodec:
109
+ """JPEG-style lossy frequency-domain codec (stateless, shareable across
110
+ caches). ``quant_bits=8`` additionally stores the DCT coefficients as int8
111
+ with a per-(block, frequency) scale — a real byte-level saving on top of the
112
+ k/r geometric ratio (the scales cost 4 bytes per row)."""
113
+
114
+ name = "jpeg"
115
+
116
+ def __init__(self, block_size=8, keep_freqs=2, quant_bits=0):
117
+ assert 1 <= keep_freqs <= block_size, \
118
+ f"keep_freqs must be within [1, {block_size}]"
119
+ assert quant_bits in (0, 8), \
120
+ f"quant_bits must be 0 (store in input dtype) or 8, got {quant_bits}"
121
+ self.block_size = block_size
122
+ self.keep_freqs = keep_freqs
123
+ self.quant_bits = quant_bits
124
+
125
+ @property
126
+ def quantized(self):
127
+ return self.quant_bits > 0
128
+
129
+ def compress(self, x):
130
+ return dct_compress(x, self.block_size, self.keep_freqs)
131
+
132
+ def quantize(self, coeffs):
133
+ """Coefficients -> (int8, scale) when quantized, else (None, None)."""
134
+ if not self.quantized:
135
+ return None, None
136
+ return quantize_coeffs(coeffs, self.quant_bits)
137
+
138
+ def decompress(self, coeffs, out_len=None, scales=None):
139
+ if self.quantized:
140
+ assert scales is not None, "quantized codec needs the scales"
141
+ coeffs = dequantize_coeffs(coeffs, scales)
142
+ return dct_decompress(coeffs, self.block_size, out_len)
143
+
144
+ def storage_ratio(self):
145
+ """Archive bytes / original bytes **assuming fp32 input** (excluding the
146
+ raw tail). Non-quantized: k/r. Quantized: (k*D + k*4) / (r*D*4)."""
147
+ if not self.quantized:
148
+ return self.keep_freqs / self.block_size
149
+ # D cancels out only in the element-count sense; report per fp32 element
150
+ # with D=1 for the generic bound and make the byte count exact in
151
+ # storage_bytes(); here we use the k/r geometric ratio scaled by the
152
+ # int8-vs-fp32 element ratio, ignoring the (tiny) scale overhead.
153
+ return (self.keep_freqs / self.block_size) / 4.0
154
+
155
+ def __repr__(self):
156
+ q = f", quant_bits={self.quant_bits}" if self.quantized else ""
157
+ return (f"JPEGCodec(block_size={self.block_size}, "
158
+ f"keep_freqs={self.keep_freqs}{q})")
159
+
@@ -0,0 +1,107 @@
1
+ """PNG-style lossless/near-lossless time-domain compression: causal blockwise
2
+ prediction delta filtering.
3
+
4
+ Physical intuition: the hidden vectors of adjacent tokens are highly
5
+ autocorrelated; after a first-order delta Δ = x_i - x_{i-1} the variance drops
6
+ sharply and the values cluster near 0 (very low information entropy). Each
7
+ block stores a full-precision anchor at its head (I-Token) and deltas
8
+ elsewhere; cumsum gives causal reconstruction that is bitwise invertible at
9
+ keep_ratio=1.0 (up to fp rounding).
10
+ When keep_ratio<1.0, the Top-M steps by |Δ| magnitude are kept per
11
+ (block, feature), emulating a near-lossless mode.
12
+
13
+ Tensor contract: input (..., S, D); the operating axis is the second-to-last
14
+ dimension (seq axis, dim=-2). Purely linear operators + discrete selection, so
15
+ autograd works naturally (gradients flow only to the retained delta steps).
16
+ """
17
+
18
+ import math
19
+
20
+ import torch
21
+ import torch.nn.functional as F
22
+
23
+
24
+ def _pad_to_blocks(x, r):
25
+ """Zero-pad the seq axis (-2) to a multiple of r; returns (block tensor,
26
+ original length)."""
27
+ S = x.shape[-2]
28
+ n = (S + r - 1) // r
29
+ pad = n * r - S
30
+ if pad:
31
+ x = F.pad(x, (0, 0, 0, pad))
32
+ blocks = x.reshape(*x.shape[:-2], n, r, x.shape[-1])
33
+ return blocks, S
34
+
35
+
36
+ def png_filter(x, block_size, keep_ratio=1.0):
37
+ """Delta filtering: x (..., S, D) -> (deltas (..., n, r-1, float32),
38
+ anchor (..., n, D, input dtype)).
39
+ Position 0 of every block is forced to a full-precision anchor; position
40
+ i>=1 stores Δ_i = x_i - x_{i-1}.
41
+ When keep_ratio<1.0, the Top-M steps by |Δ| magnitude are kept per
42
+ (block, feature) (m = ceil((r-1)*keep_ratio), m>=1), the rest are set to 0.
43
+ dtype contract: **deltas are always fp32** — computing the deltas of bf16
44
+ inputs in bf16 loses ~0.4% precision and breaks the bitwise exactness of
45
+ lossless reconstruction; Δ is a "portable representation" whose precision
46
+ must not be lower than that of the original signal (bf16 deltas are exactly
47
+ representable in fp32, so cumsum restores them bitwise).
48
+ The anchor keeps the input dtype."""
49
+ assert 0 < keep_ratio <= 1.0, "keep_ratio must be within (0, 1]"
50
+ blocks, _ = _pad_to_blocks(x, block_size)
51
+ anchor = blocks[..., 0, :] # (..., n, D)
52
+ bf = blocks.float()
53
+ delta = bf[..., 1:, :] - bf[..., :-1, :] # (..., n, r-1, D)
54
+ if keep_ratio < 1.0:
55
+ m = max(1, math.ceil(delta.shape[-2] * keep_ratio))
56
+ topi = delta.abs().topk(m, dim=-2).indices
57
+ mask = torch.zeros_like(delta, dtype=torch.bool)
58
+ mask.scatter_(-2, topi, True)
59
+ delta = delta * mask
60
+ return delta, anchor
61
+
62
+
63
+ def png_reconstruct(deltas, anchor):
64
+ """Causal reconstruction: anchor + cumsum (accumulated in fp32, then cast
65
+ back to the anchor dtype).
66
+ Position 0 = anchor, position i>=1 = anchor + the first i delta steps.
67
+ deltas (..., n, r-1, D), anchor (..., n, D) -> x (..., n*r, D).
68
+ With bf16 input + keep_ratio=1.0 the result is bitwise lossless (deltas are
69
+ exact in fp32)."""
70
+ base = anchor.float().unsqueeze(-2) # (..., n, 1, D)
71
+ rec = torch.cat([base, base + torch.cumsum(deltas.float(), dim=-2)], dim=-2)
72
+ return rec.to(anchor.dtype).reshape(*rec.shape[:-3],
73
+ rec.shape[-3] * rec.shape[-2],
74
+ rec.shape[-1])
75
+
76
+
77
+ class PNGCodec:
78
+ """PNG-style prediction-filter codec (stateless, shareable across caches)."""
79
+
80
+ name = "png"
81
+
82
+ def __init__(self, block_size=8, keep_ratio=1.0):
83
+ assert 0 < keep_ratio <= 1.0, "keep_ratio must be within (0, 1]"
84
+ self.block_size = block_size
85
+ self.keep_ratio = keep_ratio
86
+
87
+ def filter(self, x):
88
+ return png_filter(x, self.block_size, self.keep_ratio)
89
+
90
+ def reconstruct(self, deltas, anchor):
91
+ return png_reconstruct(deltas, anchor)
92
+
93
+ def storage_ratio(self):
94
+ """Dense tensor storage ratio: 1.0 for fp32 input (anchor + (r-1) delta
95
+ steps are the same size).
96
+ With bf16 input, deltas are stored as fp32 (the dtype contract for
97
+ bitwise losslessness), so the archive is about 1.9× — bandwidth traded
98
+ for exactness, with the compression headroom left to an entropy coder.
99
+ PNG's value lies at the information-entropy level: delta variance drops
100
+ ~30×, and after Top-M truncation the residual is highly sparse and
101
+ near zero — that is the headroom for an entropy coder
102
+ (run-length/arithmetic coding)."""
103
+ return 1.0
104
+
105
+ def __repr__(self):
106
+ return (f"PNGCodec(block_size={self.block_size}, "
107
+ f"keep_ratio={self.keep_ratio})")
flashcodec/patch.py ADDED
@@ -0,0 +1,136 @@
1
+ """One-step hot patch: make any HuggingFace CausalLM use FlashCodecCache.
2
+
3
+ Mechanism (zero intrusion): in standard HF models, KV writes/reads are all
4
+ funnelled through the Cache interface — after the attention layer computes
5
+ key/value it calls ``cache.update(k, v, layer_idx, ...)`` and gets the
6
+ full-history KV back. Swapping the Cache object for FlashCodecCache (a native
7
+ transformers 5.x Cache subclass) therefore completes the "replacement of the
8
+ KV write and read logic" without model surgery for every attention
9
+ implementation (GQA/MQA/RoPE variants).
10
+
11
+ - `apply_flash_codec(model, ...)`: wraps `model.generate` and `model.forward`;
12
+ when `use_cache` is requested and no `past_key_values` is passed explicitly,
13
+ it injects the configured FlashCodecCache; it still walks model.modules() to
14
+ find standard KV-projection attention modules (with k_proj/v_proj) for
15
+ validation and reporting;
16
+ - `remove_flash_codec(model)`: fully restores both wrappers.
17
+ """
18
+
19
+ import functools
20
+ import warnings
21
+
22
+ from .cache import HAS_HF_CACHE, CodecKVCache, FlashCodecCache
23
+
24
+ __all__ = ["apply_flash_codec", "remove_flash_codec", "undo_flash_codec",
25
+ "find_attention_modules"]
26
+
27
+
28
+ def find_attention_modules(model):
29
+ """Find standard Self-Attention / KV projection modules (with k_proj and
30
+ v_proj)."""
31
+ return [m for m in model.modules()
32
+ if hasattr(m, "k_proj") and hasattr(m, "v_proj")]
33
+
34
+
35
+ def _make_factory(mode, block_size, kwargs, sink, config=None):
36
+ codec_kw = dict(keep_freqs=kwargs.get("keep_freqs", 2),
37
+ keep_ratio=kwargs.get("keep_ratio", 1.0),
38
+ quant_bits=kwargs.get("quant_bits", 0))
39
+ per_layer = kwargs.get("per_layer")
40
+ if HAS_HF_CACHE:
41
+ def factory():
42
+ c = None
43
+ if config is not None: # hybrid architecture (e.g. Qwen3.5):
44
+ try: # only full-attention layers get a codec
45
+ c = FlashCodecCache.from_config( # per layer_types
46
+ config, mode=mode, block_size=block_size,
47
+ per_layer=per_layer, **codec_kw)
48
+ except Exception:
49
+ c = None
50
+ if c is None:
51
+ c = FlashCodecCache(mode=mode, block_size=block_size,
52
+ **codec_kw)
53
+ sink.append(c)
54
+ return c
55
+ else: # fallback without transformers
56
+ def factory():
57
+ c = CodecKVCache(mode=mode, block_size=block_size, **codec_kw)
58
+ sink.append(c)
59
+ return c
60
+ return factory
61
+
62
+
63
+ def apply_flash_codec(model, mode="jpeg", block_size=8,
64
+ materialization_free=False, **kwargs):
65
+ """One-line hot patch: inject a codec KV cache into model (native
66
+ transformers 5.x).
67
+
68
+ Supports mode="jpeg" (kwargs: keep_freqs, default 2; quant_bits, default 0
69
+ or 8 for int8 coefficients) and mode="png" (kwargs: keep_ratio, default
70
+ 1.0).
71
+ Afterwards every `model.generate(..., use_cache=True)` call that does not
72
+ pass past_key_values explicitly uses FlashCodecCache. With
73
+ ``materialization_free=True`` the forward of standard Llama/Qwen-style
74
+ attention modules is patched too, so decode steps attend on the stored rows
75
+ directly (no reconstructed KV view). Returns model;
76
+ `remove_flash_codec(model)` restores it at any time."""
77
+ if getattr(model, "_flashcodec", None) is not None:
78
+ warnings.warn("model is already patched with flashcodec, "
79
+ "call remove_flash_codec first")
80
+ return model
81
+
82
+ attns = find_attention_modules(model)
83
+ caches = [] # lets tests/diagnostics inspect injected caches
84
+ factory = _make_factory(mode, block_size, kwargs, caches,
85
+ config=getattr(model, "config", None))
86
+
87
+ base_generate = model.generate
88
+ base_forward = model.forward
89
+
90
+ @functools.wraps(base_generate)
91
+ def wrapped_generate(*args, **kw):
92
+ use_cache = kw.get("use_cache")
93
+ if (use_cache is None or use_cache) and kw.get("past_key_values") is None:
94
+ kw["past_key_values"] = factory()
95
+ return base_generate(*args, **kw)
96
+
97
+ @functools.wraps(base_forward)
98
+ def wrapped_forward(*args, **kw):
99
+ if kw.get("use_cache") and kw.get("past_key_values") is None:
100
+ kw["past_key_values"] = factory()
101
+ return base_forward(*args, **kw)
102
+
103
+ model.generate = wrapped_generate
104
+ model.forward = wrapped_forward
105
+ n_wrapped = 0
106
+ if materialization_free:
107
+ from .patch_attention import apply_codec_attention
108
+ n_wrapped = apply_codec_attention(model)
109
+ model._flashcodec = {
110
+ "mode": mode, "caches": caches, "attention_modules": attns,
111
+ "codec_attention_modules": n_wrapped,
112
+ "undo": lambda: _restore(model, base_generate, base_forward)}
113
+ if not attns:
114
+ warnings.warn("no attention module with k_proj/v_proj found — if the "
115
+ "model uses a non-standard KV interface, the patch may "
116
+ "have no effect")
117
+ return model
118
+
119
+
120
+ def _restore(model, base_generate, base_forward):
121
+ from .patch_attention import remove_codec_attention
122
+ remove_codec_attention(model)
123
+ model.generate = base_generate
124
+ model.forward = base_forward
125
+ model._flashcodec = None
126
+
127
+
128
+ def remove_flash_codec(model):
129
+ """Undo the patch applied by apply_flash_codec (restores both generate and
130
+ forward)."""
131
+ info = getattr(model, "_flashcodec", None)
132
+ if info is not None:
133
+ info["undo"]()
134
+
135
+
136
+ undo_flash_codec = remove_flash_codec # legacy alias
@@ -0,0 +1,151 @@
1
+ """Route decode steps through the coefficient-domain attention kernel.
2
+
3
+ `apply_flash_codec` swaps the cache, but transformers still computes attention
4
+ on the reconstructed KV view. This patch replaces the forward of *standard*
5
+ Llama/Qwen-style attention modules (q_proj / k_proj / v_proj / o_proj, optional
6
+ q_norm / k_norm) so that, for a single-token decode step on a codec layer, the
7
+ projection + RoPE part runs as usual and the attention itself is computed by
8
+ `codec_decode_attention` — the (S, D) reconstructed view is never built.
9
+
10
+ Scope / guards (anything else falls back to the original forward):
11
+ - single-token query, batch size 1, eval mode
12
+ - the cache layer must be a ``FlashCodecLayer`` (i.e. a full-attention layer)
13
+ - no sliding-window attention
14
+ - ``position_embeddings`` must be provided (5.x standard signature)
15
+
16
+ The fallback is silent by design (mixed models have linear-attention layers that
17
+ never hit this path); `codec_attention_stats` reports how many layers are wrapped.
18
+ """
19
+
20
+ import importlib
21
+
22
+ import torch
23
+
24
+ from .attention import codec_decode_attention
25
+ from .cache import FlashCodecLayer
26
+
27
+ _ROPE_CACHE = {}
28
+
29
+
30
+ def _rope_fn(module):
31
+ """The `apply_rotary_pos_emb` of the model file that defines this module."""
32
+ mod_name = type(module).__module__
33
+ if mod_name not in _ROPE_CACHE:
34
+ mod = importlib.import_module(mod_name)
35
+ _ROPE_CACHE[mod_name] = getattr(mod, "apply_rotary_pos_emb", False)
36
+ return _ROPE_CACHE[mod_name] or None
37
+
38
+
39
+ def _attention_forward(module, orig, hidden_states,
40
+ position_embeddings=None, attention_mask=None,
41
+ past_key_values=None, **kwargs):
42
+ layer = None
43
+ if past_key_values is not None and hasattr(past_key_values, "layers"):
44
+ try:
45
+ layer = past_key_values.layers[module.layer_idx]
46
+ except (IndexError, TypeError):
47
+ layer = None
48
+
49
+ usable = (
50
+ isinstance(layer, FlashCodecLayer)
51
+ and not module.training
52
+ and hidden_states.shape[0] == 1 # batch 1: no padding mask
53
+ and hidden_states.shape[-2] == 1 # decode step only
54
+ and position_embeddings is not None
55
+ and getattr(module, "sliding_window", None) is None
56
+ and _rope_fn(module) is not None
57
+ )
58
+ if not usable:
59
+ return orig(hidden_states, position_embeddings=position_embeddings,
60
+ attention_mask=attention_mask,
61
+ past_key_values=past_key_values, **kwargs)
62
+
63
+ input_shape = hidden_states.shape[:-1]
64
+ hidden_shape = (*input_shape, -1, module.head_dim)
65
+ o_in = module.o_proj.in_features
66
+ gated = module.q_proj.out_features == 2 * o_in # Qwen3.5-style Q gate
67
+ q_raw = module.q_proj(hidden_states)
68
+ gate = None
69
+ if gated:
70
+ q, gate = torch.chunk(
71
+ q_raw.view(*input_shape, -1, module.head_dim * 2), 2, dim=-1)
72
+ gate = gate.reshape(*input_shape, -1)
73
+ else:
74
+ q = q_raw
75
+ q = q.view(hidden_shape)
76
+ k = module.k_proj(hidden_states).view(hidden_shape)
77
+ v = module.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
78
+ # Qwen3-style QK-norm is applied on (heads, head_dim) before the transpose
79
+ if hasattr(module, "q_norm"):
80
+ q = module.q_norm(q)
81
+ if hasattr(module, "k_norm"):
82
+ k = module.k_norm(k)
83
+ q, k = q.transpose(1, 2), k.transpose(1, 2)
84
+
85
+ cos, sin = position_embeddings
86
+ q, k = _rope_fn(module)(q, k, cos, sin)
87
+
88
+ if not layer.is_initialized:
89
+ layer.lazy_initialization(k, v)
90
+ # prefill left a full reconstructed view behind; decode does not need it
91
+ for st in (layer._key_stream, layer._value_stream):
92
+ if st._rec is not None:
93
+ st.discard_view()
94
+ layer.keys = layer.values = None # drop the prefill view reference
95
+ layer._key_stream.append(k) # append only: no read()
96
+ layer._value_stream.append(v)
97
+ out = codec_decode_attention(q, layer._key_stream, layer._value_stream,
98
+ scale=module.scaling)
99
+ out = out.transpose(1, 2).reshape(*input_shape, -1).contiguous()
100
+ if gated:
101
+ out = out * torch.sigmoid(gate)
102
+ return module.o_proj(out), None
103
+
104
+
105
+ def apply_codec_attention(model):
106
+ """Patch every supported attention module so decode steps use the
107
+ coefficient-domain kernel. Idempotent; returns the number of wrapped
108
+ modules."""
109
+ wrapped = 0
110
+ for module in model.modules():
111
+ if not (hasattr(module, "q_proj") and hasattr(module, "k_proj")
112
+ and hasattr(module, "v_proj") and hasattr(module, "o_proj")
113
+ and hasattr(module, "layer_idx") and hasattr(module, "head_dim")):
114
+ continue
115
+ # only the standard layout: head_dim divides the output width, and the
116
+ # query projection is either plain (Llama/Qwen3) or gated (Qwen3.5)
117
+ o_in = module.o_proj.in_features
118
+ if module.head_dim <= 0 or o_in % module.head_dim:
119
+ continue
120
+ if module.q_proj.out_features not in (o_in, 2 * o_in):
121
+ continue
122
+ if getattr(module, "_flashcodec_orig_forward", None) is not None:
123
+ wrapped += 1
124
+ continue
125
+ orig = module.forward
126
+
127
+ def forward(*args, _module=module, _orig=orig, **kw):
128
+ return _attention_forward(_module, _orig, *args, **kw)
129
+
130
+ module._flashcodec_orig_forward = orig
131
+ module.forward = forward
132
+ wrapped += 1
133
+ return wrapped
134
+
135
+
136
+ def remove_codec_attention(model):
137
+ """Restore the original forwards. Returns the number restored."""
138
+ restored = 0
139
+ for module in model.modules():
140
+ orig = getattr(module, "_flashcodec_orig_forward", None)
141
+ if orig is not None:
142
+ module.forward = orig
143
+ del module._flashcodec_orig_forward
144
+ restored += 1
145
+ return restored
146
+
147
+
148
+ def codec_attention_stats(model):
149
+ """Number of attention modules currently wrapped."""
150
+ return sum(1 for m in model.modules()
151
+ if getattr(m, "_flashcodec_orig_forward", None) is not None)