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.
- flashcodec/__init__.py +37 -0
- flashcodec/attention.py +140 -0
- flashcodec/cache.py +654 -0
- flashcodec/codecs/__init__.py +13 -0
- flashcodec/codecs/jpeg.py +159 -0
- flashcodec/codecs/png.py +107 -0
- flashcodec/patch.py +136 -0
- flashcodec/patch_attention.py +151 -0
- flashcodec/triton_attention.py +222 -0
- flashcodec_kv-1.0.0.dist-info/METADATA +430 -0
- flashcodec_kv-1.0.0.dist-info/RECORD +14 -0
- flashcodec_kv-1.0.0.dist-info/WHEEL +5 -0
- flashcodec_kv-1.0.0.dist-info/licenses/LICENSE +21 -0
- flashcodec_kv-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
|
flashcodec/codecs/png.py
ADDED
|
@@ -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)
|