Skip to content

Commit c0d9a9b

Browse files
committed
Refactor EntropyGatedChunkKVPress to subclass ChunkKVPress; test/registry cleanup
- kvpress/presses/entropy_gated_chunkkv_press.py: subclass ChunkKVPress instead of BasePress, dropping the inherited press field, __post_init__, post_init_from_model, and compression_ratio property/setter; keep chunk_length default at 10. Remove the redundant explanatory comments and hoist the epsilon to a module-level EPSILON constant. - tests/presses/test_entropy_gated_chunkkv_press.py: deleted. The dedicated test duplicated coverage already provided by the test_presses_run wrapper matrix. - tests/presses/test_presses.py: removed the redundant test_entropy_gated_chunkkv_press function for the same reason; the press stays covered via the EntropyGatedChunkKVPress entry in the wrapper_press matrix. - evaluation/evaluate_registry.py: dropped the explicit chunk_length=10, rescue_size=4 from the registry entry; both are defaults on the press now, so EntropyGatedChunkKVPress(press=SnapKVPress()) is enough. - README.md: tightened the one-line description to match the other press entries.
1 parent 7797071 commit c0d9a9b

5 files changed

Lines changed: 10 additions & 184 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Finally we provide wrapper presses that can be combined with other presses:
140140
- `ComposedPress` ([source](kvpress/presses/composed_press.py)): compose multiple presses together by chaining their forward hooks
141141
- `KeyRerotationPress` ([source](kvpress/presses/key_rerotation_press.py)): rerotate pruned keys to have continuous RoPE embeddings
142142
- `ChunkKVPress` ([source](kvpress/presses/chunkkv_press.py), [paper](https://arxiv.org/abs/2502.00299)): compress by selecting important chunks, preserving semantic coherence
143-
- `EntropyGatedChunkKVPress` ([source](kvpress/presses/entropy_gated_chunkkv_press.py)): like `ChunkKVPress`, but reduces important chunks whose score mass is concentrated (using entropy from information theory) in a few tokens to their top-`rescue_size` tokens, reallocating the freed budget to more chunks
143+
- `EntropyGatedChunkKVPress` ([source](kvpress/presses/entropy_gated_chunkkv_press.py)): similar to `ChunkKVPress` but reduces the chunk length for chunks with high scores but low entropy
144144
- `ChunkPress` ([source](kvpress/presses/chunk_press.py), [paper](https://direct.mit.edu/tacl/article/doi/10.1162/tacl_a_00716/125280)): compress the KV cache on each sequence chunk separately. This can yield to more uniform compression across long sequences
145145
- `CriticalKVPress` and `CriticalAdaKVPress` ([source](kvpress/presses/criticalkv_press.py), [paper](https://arxiv.org/abs/2502.03805)): refine the scores using the L1 norm of Wo @ values, coupled with a two-stage selection.
146146
- `BlockPress` ([source](kvpress/presses/block_press.py), [paper](https://arxiv.org/abs/2504.15364)): segment input sequence into non-overlapping blocks and compress iteratively (⚠️ not a true chunked-prefill implementation)

evaluation/evaluate_registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"cur": CURPress(),
8989
"duo_attention": DuoAttentionPress(),
9090
"duo_attention_on_the_fly": DuoAttentionPress(on_the_fly_scoring=True),
91-
"entropy_gated_chunkkv": EntropyGatedChunkKVPress(press=SnapKVPress(), chunk_length=10, rescue_size=4),
91+
"entropy_gated_chunkkv": EntropyGatedChunkKVPress(press=SnapKVPress()),
9292
"expected_attention": AdaKVPress(ExpectedAttentionPress(epsilon=1e-2)),
9393
"fastkvzip": FastKVzipPress(),
9494
"finch": FinchPress(),

kvpress/presses/entropy_gated_chunkkv_press.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import torch
99
from torch import nn
1010

11-
from kvpress.presses.base_press import BasePress
12-
from kvpress.presses.scorer_press import ScorerPress
11+
from kvpress.presses.chunkkv_press import ChunkKVPress
12+
13+
EPSILON = 1e-8
1314

1415

1516
@dataclass
16-
class EntropyGatedChunkKVPress(BasePress):
17+
class EntropyGatedChunkKVPress(ChunkKVPress):
1718
"""
1819
EntropyGatedChunkKV: chunk selection gated by within-chunk score entropy.
1920
@@ -52,25 +53,10 @@ class EntropyGatedChunkKVPress(BasePress):
5253
is computed.
5354
"""
5455

55-
press: ScorerPress
5656
chunk_length: int = 10
5757
rescue_size: int = 4
5858
entropy_threshold: Optional[float] = None
5959

60-
def __post_init__(self):
61-
assert isinstance(self.press, ScorerPress), "EntropyGatedChunkKVPress requires a ScorerPress as input"
62-
63-
def post_init_from_model(self, model):
64-
self.press.post_init_from_model(model)
65-
66-
@property
67-
def compression_ratio(self):
68-
return self.press.compression_ratio
69-
70-
@compression_ratio.setter
71-
def compression_ratio(self, value):
72-
self.press.compression_ratio = value
73-
7460
def compress(
7561
self,
7662
module: nn.Module,
@@ -84,7 +70,6 @@ def compress(
8470
return keys, values
8571
assert attentions is None, "EntropyGatedChunkKVPress does not support attentions."
8672

87-
eps = 1e-8
8873
kv_len = keys.shape[2]
8974
c = self.chunk_length
9075

@@ -102,30 +87,22 @@ def compress(
10287
n_complete = kv_len // c
10388
remaining_tokens = kv_len % c
10489

105-
# Per-chunk statistics are computed vectorized rather than in a Python loop, which
106-
# would launch O(n_chunks) tiny kernels per forward pass. Complete chunks all hold
107-
# exactly c tokens, so reshaping to (n_complete, c) makes each row one chunk and the
108-
# row-wise reductions give its mean and normalized Shannon entropy.
10990
X = tok[: n_complete * c].view(n_complete, c)
11091
s_scores = X.mean(dim=1)
11192
if c > 1:
112-
p = X / (X.sum(dim=1, keepdim=True) + eps)
113-
h = -(p * (p + eps).log()).sum(dim=1)
93+
p = X / (X.sum(dim=1, keepdim=True) + EPSILON)
94+
h = -(p * (p + EPSILON).log()).sum(dim=1)
11495
ht = (h / math.log(c)).clamp(0.0, 1.0)
11596
else:
116-
# Entropy is undefined for a single token, so such a chunk is treated as spiky,
117-
# as for a length-1 trailing chunk below. Normalizing by log(1) = 0 would divide
118-
# by zero here, which is why this case is handled separately.
11997
ht = torch.zeros(n_complete, device=tok.device)
12098

12199
# The trailing partial chunk does not fit the reshape and is handled separately.
122-
# Entropy is undefined for a single token, so such a chunk is treated as spiky.
123100
if remaining_tokens > 0:
124101
ts = tok[n_complete * c :]
125102
s_tail = ts.mean().unsqueeze(0)
126103
if remaining_tokens >= 2:
127-
pr = ts / (ts.sum() + eps)
128-
hr = -(pr * (pr + eps).log()).sum()
104+
pr = ts / (ts.sum() + EPSILON)
105+
hr = -(pr * (pr + EPSILON).log()).sum()
129106
ht_tail = (hr / math.log(remaining_tokens)).clamp(0.0, 1.0).unsqueeze(0)
130107
else:
131108
ht_tail = torch.zeros(1, device=tok.device)
@@ -139,10 +116,6 @@ def compress(
139116
tau = torch.tensor(float(self.entropy_threshold), device=tok.device)
140117

141118
# 2. Greedy pass over chunks in decreasing semantic score.
142-
# The loop is inherently sequential because the budget is consumed in order. Both
143-
# gating masks are therefore computed vectorized and moved to CPU lists once: reading
144-
# a GPU scalar per iteration would force a synchronize and serialize the loop. The
145-
# topk calls stay on the GPU tensor so their tie-breaking is unchanged.
146119
important_all = (s_scores >= med).tolist()
147120
spiky_all = (ht < tau).tolist()
148121
keep = torch.zeros(kv_len, dtype=torch.bool, device=tok.device)

tests/presses/test_entropy_gated_chunkkv_press.py

Lines changed: 0 additions & 135 deletions
This file was deleted.

tests/presses/test_presses.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,6 @@ def test_chunkkv_press(unit_test_model): # noqa: F811
6262
assert cache.get_seq_length() == 128
6363

6464

65-
def test_entropy_gated_chunkkv_press(unit_test_model): # noqa: F811
66-
press = SnapKVPress(compression_ratio=0.5)
67-
for chunk_length in [2, 4, 8, 128]:
68-
for rescue_size in [1, 4]:
69-
composed_press = EntropyGatedChunkKVPress(press=press, chunk_length=chunk_length, rescue_size=rescue_size)
70-
with composed_press(unit_test_model):
71-
input_ids = torch.randint(0, 1024, (1, 256), device=unit_test_model.device)
72-
cache = DynamicCache()
73-
unit_test_model(input_ids, past_key_values=cache).past_key_values
74-
assert cache.get_seq_length() == 128
75-
76-
7765
@pytest.mark.parametrize("press_dict", default_presses)
7866
@pytest.mark.parametrize(
7967
"wrapper_press",

0 commit comments

Comments
 (0)