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
flashcodec/cache.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""Unified codec KV cache manager.
|
|
2
|
+
|
|
3
|
+
Design:
|
|
4
|
+
- Each KV stream (key/value x layer) keeps internally a ``raw_buffer``
|
|
5
|
+
(tail tokens that do not fill a full block, held at full fidelity) and a
|
|
6
|
+
``compressed_storage`` (compressed archive of completed blocks: jpeg keeps
|
|
7
|
+
low-frequency coefficients / png keeps anchors + deltas);
|
|
8
|
+
- Once block_size tokens have accumulated, batched archive compression is
|
|
9
|
+
triggered (a single GEMM, no loops);
|
|
10
|
+
- Causal safety: only "complete blocks" are archived, the current partial
|
|
11
|
+
block always stays raw — future perturbations at any point of incremental
|
|
12
|
+
decoding never rewrite already-archived tensors;
|
|
13
|
+
- Dual interface: ``forward(key_seq, value_seq)`` tiles the full sequence
|
|
14
|
+
block by block; ``decode_step(new_k, new_v)`` appends a single step and
|
|
15
|
+
archives automatically across block boundaries;
|
|
16
|
+
- Duck-typed compatible with HuggingFace ``Cache`` (update / get_seq_length /
|
|
17
|
+
key_cache / value_cache), so it can be injected directly into generate.
|
|
18
|
+
|
|
19
|
+
Tensor contract: KV layout (batch, num_heads, seq_len, head_dim), compression
|
|
20
|
+
axis dim=-2.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from .codecs import JPEGCodec, PNGCodec, dct_decompress, png_reconstruct
|
|
26
|
+
|
|
27
|
+
try: # native transformers 5.x Cache ecosystem (layer container mode)
|
|
28
|
+
from functools import partial
|
|
29
|
+
|
|
30
|
+
from transformers.cache_utils import Cache as _HFCache
|
|
31
|
+
from transformers.cache_utils import DynamicLayer as _HFDynamicLayer
|
|
32
|
+
HAS_HF_CACHE = True
|
|
33
|
+
except Exception: # fallback when transformers is absent
|
|
34
|
+
_HFCache = object
|
|
35
|
+
_HFDynamicLayer = object
|
|
36
|
+
HAS_HF_CACHE = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_codec(mode="jpeg", block_size=8, keep_freqs=2, keep_ratio=1.0,
|
|
40
|
+
quant_bits=0):
|
|
41
|
+
"""Build a codec instance from a mode name.
|
|
42
|
+
|
|
43
|
+
``quant_bits=8`` switches the jpeg archive to int8 coefficients with a
|
|
44
|
+
per-(block, frequency) scale (real byte-level saving); png ignores it (its
|
|
45
|
+
lossless tier stores fp32 deltas on purpose)."""
|
|
46
|
+
if mode == "jpeg":
|
|
47
|
+
return JPEGCodec(block_size=block_size, keep_freqs=keep_freqs,
|
|
48
|
+
quant_bits=quant_bits)
|
|
49
|
+
if mode == "png":
|
|
50
|
+
if quant_bits:
|
|
51
|
+
raise ValueError("quant_bits is only supported for mode='jpeg' "
|
|
52
|
+
"(the png lossless tier must keep fp32 deltas)")
|
|
53
|
+
return PNGCodec(block_size=block_size, keep_ratio=keep_ratio)
|
|
54
|
+
raise ValueError(f"unknown mode: {mode!r} (expected 'jpeg' | 'png')")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _CodecStream:
|
|
58
|
+
"""Single (B,H,S,D) stream: raw tail + capacity-doubling preallocated archive.
|
|
59
|
+
|
|
60
|
+
Archive storage uses std::vector-style geometric capacity doubling
|
|
61
|
+
(starting at 16 blocks, doubling when full): each full block is written
|
|
62
|
+
once as an in-place slice (no reallocation), and read() runs a single
|
|
63
|
+
large GEMM / cumsum over the valid region — eliminating per-archive
|
|
64
|
+
Python loops and fragmented kernel launches entirely.
|
|
65
|
+
jpeg: ``_coeffs (B,H,capacity,k,D)`` (input dtype), or int8 +
|
|
66
|
+
``_scales (B,H,capacity,k,1)`` fp32 when the codec is quantized;
|
|
67
|
+
png: ``_deltas (B,H,capacity,r-1,D)`` (fp32) + ``_anchors (B,H,capacity,D)``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
_INITIAL_BLOCKS = 16
|
|
71
|
+
|
|
72
|
+
def __init__(self, codec):
|
|
73
|
+
self.codec = codec
|
|
74
|
+
self.raw = None # (B,H,t,D), t < block_size
|
|
75
|
+
self.n_archived = 0 # total number of archived tokens
|
|
76
|
+
self.n_blocks = 0 # blocks used in the archive region
|
|
77
|
+
self.meta = None # (B, H, D, elem_size)
|
|
78
|
+
self.dtype = None # stream input dtype (restored; SDPA dtype contract)
|
|
79
|
+
self._capacity = 0 # blocks allocated in the archive (geometric growth)
|
|
80
|
+
self._coeffs = None # jpeg: (B,H,capacity,k,D)
|
|
81
|
+
self._scales = None # jpeg+int8: (B,H,capacity,k,1) fp32
|
|
82
|
+
self._deltas = None # png: (B,H,capacity,r-1,D) fp32
|
|
83
|
+
self._anchors = None # png: (B,H,capacity,D)
|
|
84
|
+
# Incrementally decoded archive (append-only, one decode per new
|
|
85
|
+
# block instead of re-decoding the whole history on every read).
|
|
86
|
+
self._rec = None # (B,H,_rec_capacity,D) input dtype
|
|
87
|
+
self._rec_blocks = 0 # blocks already materialized into _rec
|
|
88
|
+
self._rec_capacity = 0 # tokens allocated in _rec
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def block_size(self):
|
|
92
|
+
return self.codec.block_size
|
|
93
|
+
|
|
94
|
+
def _archive_spec(self, cap, B, H, D):
|
|
95
|
+
"""(attr, shape, dtype) for every archive tensor at capacity `cap`."""
|
|
96
|
+
if self.codec.name == "jpeg":
|
|
97
|
+
k = self.codec.keep_freqs
|
|
98
|
+
if self.codec.quantized:
|
|
99
|
+
return [("_coeffs", (B, H, cap, k, D), torch.int8),
|
|
100
|
+
("_scales", (B, H, cap, k, 1), torch.float32)]
|
|
101
|
+
return [("_coeffs", (B, H, cap, k, D), self.dtype)]
|
|
102
|
+
r = self.block_size
|
|
103
|
+
return [("_deltas", (B, H, cap, r - 1, D), torch.float32),
|
|
104
|
+
("_anchors", (B, H, cap, D), self.dtype)]
|
|
105
|
+
|
|
106
|
+
def _grow(self, need_blocks, like):
|
|
107
|
+
"""Geometric growth: when capacity is insufficient, double until
|
|
108
|
+
>= need_blocks and copy the old data over verbatim."""
|
|
109
|
+
if need_blocks <= self._capacity:
|
|
110
|
+
return
|
|
111
|
+
cap = max(self._INITIAL_BLOCKS, 1)
|
|
112
|
+
while cap < need_blocks:
|
|
113
|
+
cap *= 2
|
|
114
|
+
B, H, D = like.shape[0], like.shape[1], like.shape[-1]
|
|
115
|
+
for attr, shape, dtype in self._archive_spec(cap, B, H, D):
|
|
116
|
+
new = torch.empty(shape, dtype=dtype, device=like.device)
|
|
117
|
+
if self.n_blocks:
|
|
118
|
+
new[:, :, :self.n_blocks].copy_(
|
|
119
|
+
getattr(self, attr)[:, :, :self.n_blocks])
|
|
120
|
+
setattr(self, attr, new)
|
|
121
|
+
self._capacity = cap
|
|
122
|
+
|
|
123
|
+
def _archive(self, head):
|
|
124
|
+
"""Compress full blocks head (..., n_full*r, D) into the archive
|
|
125
|
+
region (in-place slice)."""
|
|
126
|
+
n_full = head.shape[-2] // self.block_size
|
|
127
|
+
self._grow(self.n_blocks + n_full, head)
|
|
128
|
+
sl = slice(self.n_blocks, self.n_blocks + n_full)
|
|
129
|
+
if self.codec.name == "jpeg":
|
|
130
|
+
coeffs = self.codec.compress(head)
|
|
131
|
+
q, s = self.codec.quantize(coeffs)
|
|
132
|
+
if self.codec.quantized:
|
|
133
|
+
self._coeffs[:, :, sl].copy_(q)
|
|
134
|
+
self._scales[:, :, sl].copy_(s)
|
|
135
|
+
else:
|
|
136
|
+
self._coeffs[:, :, sl].copy_(coeffs)
|
|
137
|
+
else:
|
|
138
|
+
delta, anchor = self.codec.filter(head)
|
|
139
|
+
self._deltas[:, :, sl].copy_(delta)
|
|
140
|
+
self._anchors[:, :, sl].copy_(anchor)
|
|
141
|
+
self.n_blocks += n_full
|
|
142
|
+
|
|
143
|
+
def append(self, tokens):
|
|
144
|
+
"""Append (B,H,m,D); full blocks are batch-compressed into the archive
|
|
145
|
+
immediately (a single batched GEMM)."""
|
|
146
|
+
r = self.block_size
|
|
147
|
+
if self.meta is None:
|
|
148
|
+
self.meta = (tokens.shape[0], tokens.shape[1], tokens.shape[-1],
|
|
149
|
+
tokens.element_size())
|
|
150
|
+
self.dtype = tokens.dtype # archive/read restore the input dtype
|
|
151
|
+
buf = tokens if self.raw is None else torch.cat(
|
|
152
|
+
[self.raw, tokens], dim=-2)
|
|
153
|
+
total = buf.shape[-2]
|
|
154
|
+
n_full = total // r
|
|
155
|
+
if n_full > 0:
|
|
156
|
+
self._archive(buf[..., :n_full * r, :])
|
|
157
|
+
self.n_archived += n_full * r
|
|
158
|
+
self.raw = (buf[..., n_full * r:, :].contiguous()
|
|
159
|
+
if total > n_full * r else None)
|
|
160
|
+
|
|
161
|
+
def _decode_pending(self):
|
|
162
|
+
"""Materialize archive blocks that were added since the last read.
|
|
163
|
+
|
|
164
|
+
The archive is append-only (causal-safety invariant), so the decoded
|
|
165
|
+
prefix never changes: decoding only the new blocks makes `read()` O(new
|
|
166
|
+
blocks) amortized instead of O(history) on every decode step."""
|
|
167
|
+
if self._rec_blocks >= self.n_blocks:
|
|
168
|
+
return
|
|
169
|
+
r = self.block_size
|
|
170
|
+
a, b = self._rec_blocks, self.n_blocks
|
|
171
|
+
like = self._coeffs if self.codec.name == "jpeg" else self._deltas
|
|
172
|
+
if self._rec is None or self._rec.shape[-2] < b * r:
|
|
173
|
+
cap = max(b * r, self._rec_capacity * 2,
|
|
174
|
+
self._INITIAL_BLOCKS * r)
|
|
175
|
+
new = torch.empty((like.shape[0], like.shape[1], cap,
|
|
176
|
+
like.shape[-1]),
|
|
177
|
+
dtype=self.dtype, device=like.device)
|
|
178
|
+
if self._rec is not None and self._rec_blocks:
|
|
179
|
+
done = self._rec_blocks * r
|
|
180
|
+
new[:, :, :done].copy_(self._rec[:, :, :done])
|
|
181
|
+
self._rec, self._rec_capacity = new, cap
|
|
182
|
+
if self.codec.name == "jpeg":
|
|
183
|
+
rec = self.codec.decompress(
|
|
184
|
+
self._coeffs[:, :, a:b],
|
|
185
|
+
scales=(self._scales[:, :, a:b]
|
|
186
|
+
if self.codec.quantized else None))
|
|
187
|
+
if rec.dtype != self.dtype:
|
|
188
|
+
rec = rec.to(self.dtype)
|
|
189
|
+
else:
|
|
190
|
+
rec = png_reconstruct(self._deltas[:, :, a:b],
|
|
191
|
+
self._anchors[:, :, a:b])
|
|
192
|
+
self._rec[:, :, a * r:b * r].copy_(rec)
|
|
193
|
+
self._rec_blocks = b
|
|
194
|
+
|
|
195
|
+
def discard_view(self):
|
|
196
|
+
"""Drop the memoized reconstruction buffer (it is rebuilt on demand by
|
|
197
|
+
`read()`). Used by the materialization-free decode path: once prefill is
|
|
198
|
+
over the view is dead weight, and decode attends on the stored rows."""
|
|
199
|
+
self._rec = None
|
|
200
|
+
self._rec_blocks = 0
|
|
201
|
+
self._rec_capacity = 0
|
|
202
|
+
|
|
203
|
+
def read(self):
|
|
204
|
+
"""Reconstructed view (B,H,S,D): one decode of the newly archived
|
|
205
|
+
blocks (memoized, append-only) + concatenation of the raw tail.
|
|
206
|
+
Everything is restored to the stream input dtype (attention q/k/v dtype
|
|
207
|
+
contract).
|
|
208
|
+
|
|
209
|
+
When the raw tail is empty the result is a view into the internal
|
|
210
|
+
reconstruction buffer; appends only ever write past `seq_len`, and
|
|
211
|
+
structural ops (crop / beam gather) never write into the live region
|
|
212
|
+
in place, so a tensor handed out earlier stays valid (until an
|
|
213
|
+
explicit `reset()`)."""
|
|
214
|
+
if self.n_blocks == 0:
|
|
215
|
+
return self.raw
|
|
216
|
+
self._decode_pending()
|
|
217
|
+
rec = self._rec[:, :, :self.n_archived]
|
|
218
|
+
if self.raw is not None:
|
|
219
|
+
rec = torch.cat([rec, self.raw], dim=-2)
|
|
220
|
+
return rec
|
|
221
|
+
|
|
222
|
+
def reset(self):
|
|
223
|
+
"""Zero the contents but keep the capacity: crop / refill reuse the
|
|
224
|
+
allocated VRAM instead of re-allocating it over and over."""
|
|
225
|
+
self.raw = None
|
|
226
|
+
self.n_archived = 0
|
|
227
|
+
self.n_blocks = 0
|
|
228
|
+
self._rec_blocks = 0
|
|
229
|
+
|
|
230
|
+
def crop(self, tokens_to_remove):
|
|
231
|
+
"""Remove trailing tokens exactly (negative count, HF semantics).
|
|
232
|
+
|
|
233
|
+
Already-archived blocks are kept verbatim; only the partial block at
|
|
234
|
+
the new boundary is re-exposed as a raw tail, taking its values from
|
|
235
|
+
the current reconstruction. So a crop never re-compresses the history
|
|
236
|
+
and never adds loss — unlike a rebuild-from-view crop, repeated crops
|
|
237
|
+
are idempotent."""
|
|
238
|
+
if tokens_to_remove > 0:
|
|
239
|
+
raise ValueError("crop only supports negative values "
|
|
240
|
+
"(remove trailing tokens)")
|
|
241
|
+
if tokens_to_remove == 0:
|
|
242
|
+
return
|
|
243
|
+
r = self.block_size
|
|
244
|
+
keep = max(0, self.seq_len - abs(tokens_to_remove))
|
|
245
|
+
keep_blocks = keep // r
|
|
246
|
+
tail = keep - keep_blocks * r
|
|
247
|
+
if keep_blocks < self.n_blocks:
|
|
248
|
+
self._decode_pending() # boundary tokens come from the view
|
|
249
|
+
self.n_blocks = keep_blocks
|
|
250
|
+
self.n_archived = keep_blocks * r
|
|
251
|
+
self._rec_blocks = min(self._rec_blocks, keep_blocks)
|
|
252
|
+
self.raw = (self._rec[:, :, keep_blocks * r:keep, :].clone()
|
|
253
|
+
if tail else None)
|
|
254
|
+
elif tail == 0:
|
|
255
|
+
self.raw = None
|
|
256
|
+
else:
|
|
257
|
+
self.raw = self.raw[..., :tail, :].contiguous()
|
|
258
|
+
|
|
259
|
+
def select_batch(self, idx):
|
|
260
|
+
"""Gather rows of the batch dim (beam-search reorder / batch ops)."""
|
|
261
|
+
if self.raw is None and self.n_blocks == 0 and self._rec is None:
|
|
262
|
+
return
|
|
263
|
+
like = (self.raw if self.raw is not None else
|
|
264
|
+
(self._coeffs if self.codec.name == "jpeg" else self._deltas))
|
|
265
|
+
idx = idx.to(like.device)
|
|
266
|
+
r = self.block_size
|
|
267
|
+
if idx.numel() == like.shape[0]: # same batch size: in-place
|
|
268
|
+
if self.raw is not None:
|
|
269
|
+
self.raw = self.raw.index_select(0, idx)
|
|
270
|
+
if self.n_blocks:
|
|
271
|
+
for t in self._archive_tensors():
|
|
272
|
+
t[:, :, :self.n_blocks] = t[:, :, :self.n_blocks].index_select(
|
|
273
|
+
0, idx)
|
|
274
|
+
if self._rec is not None and self._rec_blocks:
|
|
275
|
+
done = self._rec_blocks * r
|
|
276
|
+
# allocate instead of writing in place: read() may have handed
|
|
277
|
+
# out views into the current reconstruction buffer
|
|
278
|
+
new_r = torch.empty_like(self._rec)
|
|
279
|
+
new_r[:, :, :done] = self._rec[:, :, :done].index_select(
|
|
280
|
+
0, idx)
|
|
281
|
+
self._rec = new_r
|
|
282
|
+
return
|
|
283
|
+
B, H, D = idx.numel(), like.shape[1], like.shape[-1]
|
|
284
|
+
for attr, shape, dtype in self._archive_spec(self._capacity, B, H, D):
|
|
285
|
+
old = getattr(self, attr)
|
|
286
|
+
new = torch.empty(shape, dtype=dtype, device=like.device)
|
|
287
|
+
if self.n_blocks:
|
|
288
|
+
new[:, :, :self.n_blocks] = old[
|
|
289
|
+
:, :, :self.n_blocks].index_select(0, idx)
|
|
290
|
+
setattr(self, attr, new)
|
|
291
|
+
if self._rec is not None:
|
|
292
|
+
new_r = torch.empty((B, H, self._rec_capacity, D),
|
|
293
|
+
dtype=self._rec.dtype, device=like.device)
|
|
294
|
+
if self._rec_blocks:
|
|
295
|
+
done = self._rec_blocks * r
|
|
296
|
+
new_r[:, :, :done] = self._rec[:, :, :done].index_select(0, idx)
|
|
297
|
+
self._rec = new_r
|
|
298
|
+
if self.raw is not None:
|
|
299
|
+
self.raw = self.raw.index_select(0, idx)
|
|
300
|
+
if self.meta is not None:
|
|
301
|
+
self.meta = (B,) + self.meta[1:]
|
|
302
|
+
|
|
303
|
+
def _archive_tensors(self):
|
|
304
|
+
"""Live archive tensors (shared by batch ops / storage accounting)."""
|
|
305
|
+
if self.codec.name == "jpeg":
|
|
306
|
+
return ((self._coeffs, self._scales) if self.codec.quantized
|
|
307
|
+
else (self._coeffs,))
|
|
308
|
+
return (self._deltas, self._anchors)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def seq_len(self):
|
|
312
|
+
return self.n_archived + (0 if self.raw is None else self.raw.shape[-2])
|
|
313
|
+
|
|
314
|
+
def storage_bytes(self):
|
|
315
|
+
"""Logical storage (valid region + raw tail), excluding the idle
|
|
316
|
+
capacity reserved by the doubling preallocation and the persistent
|
|
317
|
+
reconstruction view (`_rec`)."""
|
|
318
|
+
total = 0
|
|
319
|
+
if self.n_blocks:
|
|
320
|
+
for t in self._archive_tensors():
|
|
321
|
+
total += (t[:, :, :self.n_blocks].numel()
|
|
322
|
+
* t.element_size())
|
|
323
|
+
if self.raw is not None:
|
|
324
|
+
total += self.raw.numel() * self.raw.element_size()
|
|
325
|
+
return total
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class CodecKVCache:
|
|
329
|
+
"""Codec KV cache (multi-layer, duck-typed as an HF Cache).
|
|
330
|
+
|
|
331
|
+
Usage::
|
|
332
|
+
|
|
333
|
+
cache = CodecKVCache(mode="jpeg", block_size=8, keep_freqs=2)
|
|
334
|
+
k, v = cache.update(k_states, v_states, layer_idx=0) # incremental
|
|
335
|
+
k, v = cache.forward(k_seq, v_seq, layer_idx=0) # full sequence
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
|
|
339
|
+
keep_ratio=1.0, quant_bits=0):
|
|
340
|
+
self.mode = mode
|
|
341
|
+
self.codec = make_codec(mode, block_size, keep_freqs, keep_ratio,
|
|
342
|
+
quant_bits=quant_bits)
|
|
343
|
+
self._keys = {}
|
|
344
|
+
self._values = {}
|
|
345
|
+
|
|
346
|
+
# ---------- internals ----------
|
|
347
|
+
def _stream(self, store, layer_idx):
|
|
348
|
+
st = store.get(layer_idx)
|
|
349
|
+
if st is None:
|
|
350
|
+
st = _CodecStream(self.codec)
|
|
351
|
+
store[layer_idx] = st
|
|
352
|
+
return st
|
|
353
|
+
|
|
354
|
+
# ---------- dual interface ----------
|
|
355
|
+
def update(self, key_states, value_states, layer_idx=0):
|
|
356
|
+
"""Incremental autoregressive interface: append new (B,H,m,D) and
|
|
357
|
+
archive automatically across block boundaries; returns the layer's
|
|
358
|
+
reconstructed (key_view, value_view)."""
|
|
359
|
+
self._stream(self._keys, layer_idx).append(key_states)
|
|
360
|
+
self._stream(self._values, layer_idx).append(value_states)
|
|
361
|
+
return (self._keys[layer_idx].read(),
|
|
362
|
+
self._values[layer_idx].read())
|
|
363
|
+
|
|
364
|
+
decode_step = update
|
|
365
|
+
|
|
366
|
+
def forward(self, key_seq, value_seq, layer_idx=0):
|
|
367
|
+
"""Full-sequence forward interface: tile the sequence dimension block
|
|
368
|
+
by block directly (reset the layer, then fill it in one shot); returns
|
|
369
|
+
the reconstructed (key_view, value_view)."""
|
|
370
|
+
self.reset(layer_idx)
|
|
371
|
+
return self.update(key_seq, value_seq, layer_idx)
|
|
372
|
+
|
|
373
|
+
def reset(self, layer_idx=None):
|
|
374
|
+
if layer_idx is None:
|
|
375
|
+
self._keys.clear()
|
|
376
|
+
self._values.clear()
|
|
377
|
+
else:
|
|
378
|
+
self._keys.pop(layer_idx, None)
|
|
379
|
+
self._values.pop(layer_idx, None)
|
|
380
|
+
|
|
381
|
+
def crop(self, tokens_to_remove, layer_idx=None):
|
|
382
|
+
"""Exact trailing-token removal (negative count) for one or all layers."""
|
|
383
|
+
for store in (self._keys, self._values):
|
|
384
|
+
for i, st in store.items():
|
|
385
|
+
if layer_idx is None or i == layer_idx:
|
|
386
|
+
st.crop(tokens_to_remove)
|
|
387
|
+
|
|
388
|
+
def reorder_cache(self, beam_idx, layer_idx=None):
|
|
389
|
+
"""Beam search: gather the batch dim of one or all layers."""
|
|
390
|
+
for store in (self._keys, self._values):
|
|
391
|
+
for i, st in store.items():
|
|
392
|
+
if layer_idx is None or i == layer_idx:
|
|
393
|
+
st.select_batch(beam_idx)
|
|
394
|
+
|
|
395
|
+
# ---------- HF Cache duck typing ----------
|
|
396
|
+
@property
|
|
397
|
+
def key_cache(self):
|
|
398
|
+
mx = max(self._keys, default=-1)
|
|
399
|
+
return [self._keys[i].read() if i in self._keys else None
|
|
400
|
+
for i in range(mx + 1)]
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def value_cache(self):
|
|
404
|
+
mx = max(self._values, default=-1)
|
|
405
|
+
return [self._values[i].read() if i in self._values else None
|
|
406
|
+
for i in range(mx + 1)]
|
|
407
|
+
|
|
408
|
+
def get_seq_length(self, layer_idx=0):
|
|
409
|
+
st = self._keys.get(layer_idx)
|
|
410
|
+
return 0 if st is None else st.seq_len
|
|
411
|
+
|
|
412
|
+
def get_max_length(self):
|
|
413
|
+
return None # no upper bound
|
|
414
|
+
|
|
415
|
+
def get_usable_length(self, new_seq_length, layer_idx=0):
|
|
416
|
+
return self.get_seq_length(layer_idx)
|
|
417
|
+
|
|
418
|
+
# ---------- diagnostics ----------
|
|
419
|
+
def storage_bytes(self, layer_idx=None):
|
|
420
|
+
layers = ([layer_idx] if layer_idx is not None
|
|
421
|
+
else sorted(set(self._keys) | set(self._values)))
|
|
422
|
+
return sum(self._keys[i].storage_bytes() for i in layers if i in self._keys
|
|
423
|
+
) + sum(self._values[i].storage_bytes()
|
|
424
|
+
for i in layers if i in self._values)
|
|
425
|
+
|
|
426
|
+
def stats(self, layer_idx=0):
|
|
427
|
+
ks = self._keys.get(layer_idx)
|
|
428
|
+
if ks is None or ks.meta is None:
|
|
429
|
+
return {"tokens": 0, "archived": 0, "raw": 0,
|
|
430
|
+
"storage_bytes": 0, "raw_bytes": 0}
|
|
431
|
+
B, H, D, elem = ks.meta
|
|
432
|
+
tokens = ks.seq_len
|
|
433
|
+
return {"tokens": tokens, "archived": ks.n_archived,
|
|
434
|
+
"raw": tokens - ks.n_archived,
|
|
435
|
+
"storage_bytes": self.storage_bytes(layer_idx),
|
|
436
|
+
"raw_bytes": tokens * B * H * D * elem * 2}
|
|
437
|
+
|
|
438
|
+
def __repr__(self):
|
|
439
|
+
return (f"CodecKVCache(mode={self.mode!r}, codec={self.codec}, "
|
|
440
|
+
f"layers={sorted(set(self._keys) | set(self._values))})")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ============================================================
|
|
444
|
+
# transformers 5.x native Cache backend (layer container mode)
|
|
445
|
+
# ============================================================
|
|
446
|
+
class FlashCodecLayer(_HFDynamicLayer):
|
|
447
|
+
"""transformers 5.x CacheLayer: replaces DynamicLayer's cat-style growth
|
|
448
|
+
with a codec stream (raw tail + archived full blocks). update returns the
|
|
449
|
+
reconstructed, causally visible KV for the attention layer to consume
|
|
450
|
+
directly — the write and read replacements are both contained in this
|
|
451
|
+
layer, requiring zero model surgery."""
|
|
452
|
+
|
|
453
|
+
def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
|
|
454
|
+
keep_ratio=1.0, quant_bits=0):
|
|
455
|
+
super().__init__()
|
|
456
|
+
codec = make_codec(mode, block_size, keep_freqs, keep_ratio,
|
|
457
|
+
quant_bits=quant_bits)
|
|
458
|
+
self.mode = mode
|
|
459
|
+
self.codec = codec
|
|
460
|
+
self._key_stream = _CodecStream(codec)
|
|
461
|
+
self._value_stream = _CodecStream(codec)
|
|
462
|
+
|
|
463
|
+
def lazy_initialization(self, key_states, value_states):
|
|
464
|
+
self.dtype, self.device = key_states.dtype, key_states.device
|
|
465
|
+
self.keys = torch.tensor([], dtype=self.dtype, device=self.device)
|
|
466
|
+
self.values = torch.tensor([], dtype=self.dtype, device=self.device)
|
|
467
|
+
self.is_initialized = True
|
|
468
|
+
|
|
469
|
+
def update(self, key_states, value_states, *args, **kwargs):
|
|
470
|
+
if not self.is_initialized:
|
|
471
|
+
self.lazy_initialization(key_states, value_states)
|
|
472
|
+
self._key_stream.append(key_states)
|
|
473
|
+
self._value_stream.append(value_states)
|
|
474
|
+
self.keys = self._key_stream.read()
|
|
475
|
+
self.values = self._value_stream.read()
|
|
476
|
+
return self.keys, self.values
|
|
477
|
+
|
|
478
|
+
def get_seq_length(self):
|
|
479
|
+
return self._key_stream.seq_len
|
|
480
|
+
|
|
481
|
+
def get_max_length(self):
|
|
482
|
+
return -1 # dynamic cache, no upper bound
|
|
483
|
+
|
|
484
|
+
def crop(self, tokens_to_remove):
|
|
485
|
+
"""Exact trailing-token removal (speculative decoding). Archived blocks
|
|
486
|
+
are kept verbatim; only the boundary partial block falls back into the
|
|
487
|
+
raw tail, so cropping never re-compresses and never adds loss."""
|
|
488
|
+
self._key_stream.crop(tokens_to_remove)
|
|
489
|
+
self._value_stream.crop(tokens_to_remove)
|
|
490
|
+
self._rebuild_views()
|
|
491
|
+
|
|
492
|
+
def reorder_cache(self, beam_idx):
|
|
493
|
+
"""Beam search: gather the batch dim of both streams. The archive and
|
|
494
|
+
the raw tail have to be reordered too — reordering only the
|
|
495
|
+
materialized view would silently corrupt the next read."""
|
|
496
|
+
self._key_stream.select_batch(beam_idx)
|
|
497
|
+
self._value_stream.select_batch(beam_idx)
|
|
498
|
+
self._rebuild_views()
|
|
499
|
+
|
|
500
|
+
def batch_repeat_interleave(self, repeats):
|
|
501
|
+
"""Contrastive search: replicate batch rows (archives included)."""
|
|
502
|
+
if not self.is_initialized:
|
|
503
|
+
return
|
|
504
|
+
idx = torch.arange(self._key_stream.meta[0],
|
|
505
|
+
device=self.keys.device).repeat_interleave(repeats)
|
|
506
|
+
self.select_batch(idx)
|
|
507
|
+
|
|
508
|
+
def batch_select_indices(self, indices):
|
|
509
|
+
"""Contrastive search: keep only the given batch rows."""
|
|
510
|
+
if not self.is_initialized:
|
|
511
|
+
return
|
|
512
|
+
self.select_batch(indices)
|
|
513
|
+
|
|
514
|
+
def select_batch(self, idx):
|
|
515
|
+
self._key_stream.select_batch(idx)
|
|
516
|
+
self._value_stream.select_batch(idx)
|
|
517
|
+
self._rebuild_views()
|
|
518
|
+
|
|
519
|
+
def _rebuild_views(self):
|
|
520
|
+
"""Re-materialize the attention-facing views after a structural
|
|
521
|
+
change (crop / batch gather)."""
|
|
522
|
+
if not self.is_initialized:
|
|
523
|
+
return
|
|
524
|
+
if self._key_stream.seq_len > 0:
|
|
525
|
+
self.keys = self._key_stream.read()
|
|
526
|
+
self.values = self._value_stream.read()
|
|
527
|
+
else:
|
|
528
|
+
self.keys = torch.empty(0, dtype=self.dtype, device=self.device)
|
|
529
|
+
self.values = torch.empty(0, dtype=self.dtype, device=self.device)
|
|
530
|
+
|
|
531
|
+
def reset(self):
|
|
532
|
+
self._key_stream.reset()
|
|
533
|
+
self._value_stream.reset()
|
|
534
|
+
super().reset()
|
|
535
|
+
|
|
536
|
+
def offload(self):
|
|
537
|
+
pass # prototype has no layer offload (archives are off-CPU)
|
|
538
|
+
|
|
539
|
+
def prefetch(self):
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
def storage_bytes(self):
|
|
543
|
+
return (self._key_stream.storage_bytes()
|
|
544
|
+
+ self._value_stream.storage_bytes())
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class FlashCodecCache(_HFCache):
|
|
548
|
+
"""transformers 5.x native codec KV cache (first-class backend).
|
|
549
|
+
|
|
550
|
+
Usage 1 (explicit)::
|
|
551
|
+
|
|
552
|
+
cache = FlashCodecCache(mode="jpeg", block_size=8, keep_freqs=2)
|
|
553
|
+
model.generate(input_ids, past_key_values=cache, use_cache=True)
|
|
554
|
+
|
|
555
|
+
Usage 2 (automatic injection via the apply_flash_codec hot patch).
|
|
556
|
+
The Cache base class lazily creates layers through
|
|
557
|
+
layer_class_to_replicate and forwards update to FlashCodecLayer;
|
|
558
|
+
get_seq_length / get_max_length / crop and friends come for free by
|
|
559
|
+
inheritance.
|
|
560
|
+
"""
|
|
561
|
+
|
|
562
|
+
def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
|
|
563
|
+
keep_ratio=1.0, quant_bits=0):
|
|
564
|
+
if not HAS_HF_CACHE:
|
|
565
|
+
raise ImportError(
|
|
566
|
+
"FlashCodecCache requires transformers 5.x (this project only "
|
|
567
|
+
"supports the installed 5.20 Cache ecosystem); use "
|
|
568
|
+
"CodecKVCache when transformers is unavailable.")
|
|
569
|
+
super().__init__(
|
|
570
|
+
layers=None,
|
|
571
|
+
layer_class_to_replicate=partial(
|
|
572
|
+
FlashCodecLayer, mode=mode, block_size=block_size,
|
|
573
|
+
keep_freqs=keep_freqs, keep_ratio=keep_ratio,
|
|
574
|
+
quant_bits=quant_bits))
|
|
575
|
+
self.mode = mode
|
|
576
|
+
self.codec_cfg = dict(block_size=block_size, keep_freqs=keep_freqs,
|
|
577
|
+
keep_ratio=keep_ratio, quant_bits=quant_bits)
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def _seen_tokens(self):
|
|
581
|
+
"""Counter attribute familiar from older transformers versions (unused
|
|
582
|
+
internally by 5.x, kept for compatibility)."""
|
|
583
|
+
return self.get_seq_length()
|
|
584
|
+
|
|
585
|
+
@classmethod
|
|
586
|
+
def from_config(cls, config, mode="jpeg", block_size=8, keep_freqs=2,
|
|
587
|
+
keep_ratio=1.0, quant_bits=0, per_layer=None):
|
|
588
|
+
"""Build the mixed-layer list from the model config (Qwen3.5-style
|
|
589
|
+
hybrid architectures): only full_attention layers get a codec layer,
|
|
590
|
+
while the remaining layer types (linear_attention / sliding, etc.)
|
|
591
|
+
keep their native dynamic layer — consistent with how
|
|
592
|
+
DynamicCache(config=...) dispatches, so native interfaces such as
|
|
593
|
+
linear-attention recurrent state stay unaffected.
|
|
594
|
+
|
|
595
|
+
``per_layer`` overrides the codec config for individual layer indices,
|
|
596
|
+
e.g. ``{3: {"keep_freqs": 4}, 7: {"quant_bits": 8}}`` — this is how a
|
|
597
|
+
calibrated (non-uniform) budget is applied."""
|
|
598
|
+
from transformers.cache_utils import (DYNAMIC_LAYER_TYPE_MAPPING,
|
|
599
|
+
get_layer_types_and_kwargs)
|
|
600
|
+
decoder = (config.get_text_config(decoder=True)
|
|
601
|
+
if hasattr(config, "get_text_config") else config)
|
|
602
|
+
try:
|
|
603
|
+
layer_types, per_layer_kwargs = get_layer_types_and_kwargs(decoder)
|
|
604
|
+
except Exception: # legacy config, no per_layer_config
|
|
605
|
+
layer_types = list(getattr(decoder, "layer_types",
|
|
606
|
+
["full_attention"] *
|
|
607
|
+
getattr(decoder, "num_hidden_layers", 1)))
|
|
608
|
+
per_layer_kwargs = [{} for _ in layer_types]
|
|
609
|
+
per_layer = per_layer or {}
|
|
610
|
+
layers = []
|
|
611
|
+
for idx, (ltype, lkw) in enumerate(zip(layer_types, per_layer_kwargs)):
|
|
612
|
+
if ltype == "full_attention":
|
|
613
|
+
cfg = dict(mode=mode, block_size=block_size,
|
|
614
|
+
keep_freqs=keep_freqs, keep_ratio=keep_ratio,
|
|
615
|
+
quant_bits=quant_bits)
|
|
616
|
+
cfg.update(per_layer.get(idx, {}))
|
|
617
|
+
layers.append(FlashCodecLayer(**cfg))
|
|
618
|
+
else:
|
|
619
|
+
layers.append(DYNAMIC_LAYER_TYPE_MAPPING[ltype](**lkw))
|
|
620
|
+
obj = cls.__new__(cls)
|
|
621
|
+
_HFCache.__init__(obj, layers=layers, layer_class_to_replicate=None)
|
|
622
|
+
obj.mode = mode
|
|
623
|
+
obj.codec_cfg = dict(block_size=block_size, keep_freqs=keep_freqs,
|
|
624
|
+
keep_ratio=keep_ratio, quant_bits=quant_bits,
|
|
625
|
+
per_layer=per_layer)
|
|
626
|
+
return obj
|
|
627
|
+
|
|
628
|
+
def codec_layer_indices(self):
|
|
629
|
+
"""Indices of the layers that actually carry a codec (i.e. the
|
|
630
|
+
full_attention layers in hybrid architectures)."""
|
|
631
|
+
return [i for i, layer in enumerate(self.layers)
|
|
632
|
+
if isinstance(layer, FlashCodecLayer)]
|
|
633
|
+
|
|
634
|
+
def to_legacy_cache(self):
|
|
635
|
+
"""Export to the legacy {layer_idx: (keys, values)} format
|
|
636
|
+
(reconstructed full views)."""
|
|
637
|
+
return {i: (layer.keys, layer.values)
|
|
638
|
+
for i, layer in enumerate(self.layers)}
|
|
639
|
+
|
|
640
|
+
@classmethod
|
|
641
|
+
def from_legacy_cache(cls, legacy, **cfg):
|
|
642
|
+
"""Rebuild from the legacy {(layer_idx): (k, v)} format (the whole
|
|
643
|
+
span goes through the codec once more)."""
|
|
644
|
+
cache = cls(**cfg)
|
|
645
|
+
for layer_idx in sorted(legacy):
|
|
646
|
+
k, v = legacy[layer_idx]
|
|
647
|
+
cache.update(k, v, layer_idx)
|
|
648
|
+
return cache
|
|
649
|
+
|
|
650
|
+
def storage_bytes(self, layer_idx=None):
|
|
651
|
+
layers = (self.layers if layer_idx is None
|
|
652
|
+
else [self.layers[layer_idx]])
|
|
653
|
+
return sum(layer.storage_bytes() for layer in layers
|
|
654
|
+
if isinstance(layer, FlashCodecLayer))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""flashcodec codecs: jpeg (lossy frequency domain) and
|
|
2
|
+
png (lossless/near-lossless time domain)."""
|
|
3
|
+
|
|
4
|
+
from .jpeg import (JPEGCodec, dct_basis, dct_compress, dct_decompress,
|
|
5
|
+
dequantize_coeffs, quantize_coeffs)
|
|
6
|
+
from .png import PNGCodec, png_filter, png_reconstruct
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"JPEGCodec", "PNGCodec",
|
|
10
|
+
"dct_basis", "dct_compress", "dct_decompress",
|
|
11
|
+
"quantize_coeffs", "dequantize_coeffs",
|
|
12
|
+
"png_filter", "png_reconstruct",
|
|
13
|
+
]
|