Add RestoreKV press 🤖🤖🤖 - #259
Conversation
|
Hi @higokri, Thanks for your contribution ! Important refactoring will be needed before we can merge this PR (e.g. don't edit the
I will provide more detailed feedback in the coming days |
SimJeg
left a comment
There was a problem hiding this comment.
I left a few first comments. Goal is to limit the changes to other presses
0957afc to
d03d456
Compare
|
Thanks for the detailed review! I pushed a refactor that keeps everything inside
Net: base_press.py / kvzip_press.py have zero changes; pipeline.py is +1 import, Your inference summary is exactly right. One detail worth adding: to ensure a fair budget comparison with the other KVpress baselines, the method is budget-matched. When pruning Happy to adjust the pipeline isinstance if you find a cleaner approach. I've also submitted results to the Hugging Face leaderboard Space, in case they're useful for reference. Thanks again! |
|
Hi @higokri, thanks for this first batch of updates. A second batch of updates to make the code more readable would be to use the PEFT library (and add Please find below a proposed implementation (
# SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
import logging
from dataclasses import dataclass, field
import torch
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
from torch import nn
from transformers import PreTrainedModel
from kvpress.presses.kvzip_press import KVzipPress
logger = logging.getLogger(__name__)
@dataclass
class RestoreKVPress(KVzipPress):
"""RestoreKV (https://arxiv.org/abs/2608.01247): learned restoration on top of KVzip.
Before eviction, n=8 restore tokens attend to the full KV cache in one
LoRA-adapted pass, producing a context-conditioned restore cache that fills
n*L*H slots of the same budget while the base evictor fills the rest
(budget-matched). LoRA is active only for this one-time pass. Only the restore
embeddings and LoRA (~0.4%) are trained, by self-distillation from the
full-cache teacher.
"""
restore_embeddings: torch.Tensor | None = field(init=False, default=None, repr=False)
restore_model_name: str | None = field(init=False, default=None)
encoding_restore_tokens: bool = field(init=False, default=False, repr=False)
@property
def num_restore_tokens(self) -> int:
if self.restore_embeddings is None:
return 0
return self.restore_embeddings.shape[0]
def post_init_from_model(self, model: PreTrainedModel):
model_name = model.config.name_or_path.split("/")[-1]
variant = "_plus" if self.kvzip_plus_normalization else ""
adapter_name = f"restorekv{variant}"
restore_model_name = f"higokri/RestoreKV-{model_name}{variant}"
if restore_model_name == self.restore_model_name:
return
weights_path = hf_hub_download(restore_model_name, "adapter_model.safetensors")
adapter_state = load_file(weights_path)
self.restore_embeddings = adapter_state.pop("restore_embeddings").to(model.device, dtype=model.dtype)
if adapter_name not in getattr(model, "peft_config", {}):
model.load_adapter(
restore_model_name,
adapter_name=adapter_name,
adapter_state_dict=adapter_state,
)
model.disable_adapters()
self.restore_model_name = restore_model_name
logger.info("Loaded %s with %d restore tokens", restore_model_name, self.num_restore_tokens)
def forward_hook(self, module: nn.Module, input: list[torch.Tensor], kwargs: dict, output: list):
# Skip KVzip scoring while the restore tokens are being encoded (see append_restore_tokens).
if self.encoding_restore_tokens:
return output
return super().forward_hook(module, input, kwargs, output)
def append_restore_tokens(self, model: PreTrainedModel):
"""Single LoRA-adapted restore pass over the full cache (before eviction);
appends the n restore tokens' K/V in place. Scoring hooks are skipped."""
if self._cache is None or self.restore_embeddings is None:
raise RuntimeError("Restore append requires a populated context cache")
before = self._cache.get_seq_length()
restore_embeds = self.restore_embeddings.unsqueeze(0)
cache_position = torch.arange(
before,
before + self.num_restore_tokens,
device=restore_embeds.device,
)
self.encoding_restore_tokens = True
model.set_adapter("restorekv_plus" if self.kvzip_plus_normalization else "restorekv")
model.enable_adapters()
try:
with torch.inference_mode():
model.model(
inputs_embeds=restore_embeds,
past_key_values=self._cache,
position_ids=cache_position.unsqueeze(0),
cache_position=cache_position,
use_cache=True,
)
finally:
model.disable_adapters()
self.encoding_restore_tokens = False
after = self._cache.get_seq_length()
if after - before != self.num_restore_tokens:
raise RuntimeError(
"Restore append length mismatch: " f"expected {self.num_restore_tokens}, observed {after - before}"
)
def compress_post(self, model: PreTrainedModel):
# KVzip has finished scoring the context; append the restore tokens to the
# full cache (budget-matched), then let KVzip evict the lowest-scored pairs.
self.append_restore_tokens(model)
requested_ratio = self.compression_ratio
if self.context_length > 0:
restore_overhead = self.num_restore_tokens / self.context_length
self.compression_ratio = min(1.0, requested_ratio + restore_overhead)
try:
super().compress_post(model)
finally:
self.compression_ratio = requested_ratio |
|
Hi @SimJeg, thanks for the proposed implementation! This is a great suggestion. I'll rework the press with PEFT following your snippet, re-publish the checkpoints in the adapter format, and let you know once everything is tested and pushed. |
|
Follow-up on the PEFT rework:
The refactored code runs end to end. I'm currently re-running the RULER benchmark to confirm the scores match the previous version, and will report back once it finishes. One design question before I push: where to store the 8 restore-token embeddings. Both options work, so it's purely a matter of cleanliness:
Do you prefer the separate file or everything in one? Either works for me. |
|
@higokri I think you did not push. Also you can remove updates in |
RestoreKV (https://arxiv.org/abs/2608.01247) is a budget-matched, single-pass plug-in on top of KVzip. Before eviction, n=8 learned restore tokens attend to the full KV cache in one LoRA-adapted pass, producing a context-conditioned restore cache; only the restore-token embeddings and rank-8 LoRA (~0.4% of params) are trained, by self-distillation from the full-cache teacher. RestoreKV_plus = RestoreKV+ (KVzip+ / WoV-norm scoring). The press uses the PEFT library: the LoRA adapter and the restore-token embeddings are published as PEFT adapters at higokri/RestoreKV-<model>[_plus] and loaded automatically from the model name. BasePress and the evaluation harness are unchanged; KVzipPress only gains a post_init_from_model call in __call__ (as in BasePress). Adds: - kvpress/presses/restorekv_press.py (+ self-contained tests) - RestoreKV / RestoreKV_plus in the evaluation registry - README entry and a TestRestoreKVPress mock in tests/default_presses.py - peft>=0.20.0 dependency 🤖🤖🤖 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: higokri <higok318@gmail.com>
|
@SimJeg Pushed ( |
|
/ok to test 0a56c43 |
There was a problem hiding this comment.
Here is a final batch of review. Once fixed we're ready for a merge :) thanks for your reactivity. Please notify us if you need to update the predictions for the leadeboard.
Also you can simply push on your branch instead of push force so that we can keep commit history of this PR
- reword the README one-liner and list the available models in the docstring - drop the cache/embedding guards and the length-mismatch check - inline the restore embeddings and use self.context_length - remove the standalone test (covered by tests/default_presses.py) 🤖🤖🤖 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: higokri <higok318@gmail.com>
|
Applied the final batch, pushed as a new commit (no force-push) to keep the history:
I'll also clean up the old For the leaderboard: our RULER results are submitted at https://huggingface.co/spaces/nvidia/kvpress-leaderboard/discussions/18. Could you take a look and update the leaderboard with those when you get a chance? Thanks! |
|
/ok to test ce3cbc4 |
|
Thanks for your contribution @higokri. LB has been updated too |
PR description
RestoreKV (https://arxiv.org/abs/2608.01247) is a budget-matched, single-pass
learned restoration plug-in on top of KVzip. Before eviction, 8 restore tokens
attend to the full KV cache in one LoRA-adapted pass, producing a
context-conditioned restore cache that fills part of the budget while the base
evictor fills the rest (budget-matched). Only the restore-token embeddings and
rank-8 LoRA (~0.4% of params) are trained (~2 h), by self-distillation from the
full-cache teacher.
RestoreKV_plus= RestoreKV+ (KVzip+ / WoV-norm scoring).Additive only (no upstream lines removed):
kvpress/presses/restorekv_press.py+tests/presses/test_restorekv_press.pybase_press.py/pipeline.py(default no-op forexisting presses) and a
_skip_scoring_hooksguard inkvzip_press.py(KVzipbehavior unchanged) — needed because RestoreKV appends KV entries before eviction
RestoreKV/RestoreKV_plusin the evaluation registryTestRestoreKVPressmock intests/default_presses.pyResults
RULER-4096 average (13 tasks).
compression_ratio= fraction of KV removed;RestoreKV/RestoreKV+columns.Checkpoints are hosted on the HF Hub and loaded automatically — pass an
<org>/<repo>/<file>reference ascheckpoint_path(e.g.
higokri/RestoreKV/llama3.1-8b_restorekv.pt) and the press fetches it viahf_hub_download: https://huggingface.co/higokri/RestoreKVChecklist
make test) — fulltests/suite run locally: 624 passed, 53 skipped, 0 failed (skips are flash-attn / resource-gated cases)make style) —black,isort,flake8,mypy --check-untyped-defs, and the SPDX-header check pass on the changed filesgit commit -srestorekv_press.pyis in thepressesdirectoryRestoreKVPressis in__init__.pyREADME.mdis updated with a 1 liner about the new press in the Available presses sectiondefault_presseslist intests/default_presses.py🤖🤖🤖