HumboldtJoker commited on
Commit
4ad5cf6
·
verified ·
1 Parent(s): 98fc512

Clean up nested duplicate: v2/v2/pharos/kv_injector.py

Browse files
Files changed (1) hide show
  1. v2/v2/pharos/kv_injector.py +0 -213
v2/v2/pharos/kv_injector.py DELETED
@@ -1,213 +0,0 @@
1
- """Real KV-cache knowledge injection (the actual Pharos pipeline).
2
-
3
- The zero-prompt-token path: the knowledge pack is run through the model
4
- ONCE, its key/value cache is saved to disk, and every subsequent request
5
- resumes generation from that cache — the model attends over the
6
- knowledge without the knowledge ever being re-tokenized, re-prefilled,
7
- or counted against the request.
8
-
9
- Mechanics (transformers backend):
10
-
11
- prefix_text = chat-template rendering of the system message
12
- (Rivet rules + pack knowledge)
13
- prefix_cache = model(prefix_tokens).past_key_values # computed once
14
- answer = model.generate(suffix_tokens, past_key_values=prefix_cache)
15
-
16
- The suffix (user question + generation prompt) must be the exact textual
17
- continuation of the prefix, so both are rendered from the same chat
18
- template and we assert the prefix is a string prefix of the full render.
19
- Caches are keyed by SHA-256 of (model, prefix_text) and stored under
20
- `cache_dir`, so pack updates automatically invalidate them.
21
-
22
- Ollama exposes no KV handle, which is why OllamaClient falls back to
23
- system-prompt injection. This module is what makes the transformers
24
- backend the real thing.
25
- """
26
-
27
- import hashlib
28
- import logging
29
- from pathlib import Path
30
-
31
- logger = logging.getLogger("pharos.kv")
32
-
33
-
34
- class KVInjector:
35
- def __init__(self, model_path: str, device: str = "auto",
36
- cache_dir: str = ""):
37
- import torch
38
- from transformers import AutoModelForCausalLM, AutoTokenizer
39
-
40
- self.torch = torch
41
- self.model_path = model_path
42
- self.device = self._pick_device(device)
43
- self.cache_dir = Path(cache_dir) if cache_dir else None
44
- if self.cache_dir:
45
- self.cache_dir.mkdir(parents=True, exist_ok=True)
46
-
47
- logger.info("loading %s on %s", model_path, self.device)
48
- self.tokenizer = AutoTokenizer.from_pretrained(model_path)
49
- dtype = torch.float16 if self.device != "cpu" else torch.float32
50
- self.model = AutoModelForCausalLM.from_pretrained(
51
- model_path, torch_dtype=dtype,
52
- ).to(self.device).eval()
53
- self._mem_cache: dict = {} # prefix_hash -> legacy kv tuple
54
-
55
- def _pick_device(self, device: str) -> str:
56
- if device != "auto":
57
- return device
58
- if self.torch.cuda.is_available():
59
- return "cuda"
60
- if getattr(self.torch.backends, "mps", None) and \
61
- self.torch.backends.mps.is_available():
62
- return "mps"
63
- return "cpu"
64
-
65
- # ------------------------------------------------------------------
66
-
67
- def _render(self, system: str, user: str) -> tuple:
68
- """Render (prefix_text, full_text) from the chat template."""
69
- sys_msgs = [{"role": "system", "content": system}]
70
- all_msgs = sys_msgs + [{"role": "user", "content": user}]
71
- prefix = self.tokenizer.apply_chat_template(
72
- sys_msgs, tokenize=False, add_generation_prompt=False,
73
- )
74
- full = self.tokenizer.apply_chat_template(
75
- all_msgs, tokenize=False, add_generation_prompt=True,
76
- )
77
- if not full.startswith(prefix):
78
- # Template interleaves system content non-prefix-wise (rare);
79
- # KV reuse is unsound here — signal the caller to prefill fully.
80
- return "", full
81
- return prefix, full
82
-
83
- def _prefix_cache(self, prefix_text: str):
84
- """Compute (or load) the KV cache for a knowledge prefix."""
85
- key = hashlib.sha256(
86
- f"{self.model_path}||{prefix_text}".encode()
87
- ).hexdigest()[:24]
88
-
89
- if key in self._mem_cache:
90
- return key, self._mem_cache[key]
91
-
92
- disk_path = self.cache_dir / f"{key}.pt" if self.cache_dir else None
93
- if disk_path and disk_path.exists():
94
- legacy = self.torch.load(disk_path, map_location=self.device)
95
- self._mem_cache[key] = legacy
96
- logger.info("loaded pack KV cache %s from disk", key)
97
- return key, legacy
98
-
99
- tokens = self.tokenizer(prefix_text, return_tensors="pt").to(self.device)
100
- with self.torch.no_grad():
101
- out = self.model(**tokens, use_cache=True)
102
- cache = out.past_key_values
103
- legacy = (cache.to_legacy_cache()
104
- if hasattr(cache, "to_legacy_cache") else cache)
105
- self._mem_cache[key] = legacy
106
- if disk_path:
107
- self.torch.save(legacy, disk_path)
108
- logger.info("saved pack KV cache %s (%d tokens)",
109
- key, tokens["input_ids"].shape[1])
110
- return key, legacy
111
-
112
- def _to_cache_obj(self, legacy):
113
- try:
114
- from transformers import DynamicCache
115
- return DynamicCache.from_legacy_cache(legacy)
116
- except (ImportError, AttributeError):
117
- return legacy
118
-
119
- # ------------------------------------------------------------------
120
-
121
- def generate(self, prompt: str, system: str = "", knowledge: str = "",
122
- max_tokens: int = 1024) -> str:
123
- full_system = system
124
- if knowledge:
125
- full_system = (
126
- f"{system}\n\n# INJECTED KNOWLEDGE (Pharos)\n{knowledge}"
127
- ).strip()
128
-
129
- prefix_text, full_text = self._render(full_system, prompt)
130
-
131
- if prefix_text and knowledge:
132
- _, legacy = self._prefix_cache(prefix_text)
133
- cache = self._to_cache_obj(legacy)
134
- prefix_ids = self.tokenizer(prefix_text, return_tensors="pt")
135
- full_ids = self.tokenizer(full_text, return_tensors="pt")
136
- n_prefix = prefix_ids["input_ids"].shape[1]
137
- # tokenization boundary check — resume point must match
138
- if not self.torch.equal(
139
- full_ids["input_ids"][:, :n_prefix], prefix_ids["input_ids"]
140
- ):
141
- return self._plain_generate(full_text, max_tokens)
142
- # Prefix-caching pattern: pass the FULL sequence plus the
143
- # precomputed cache — generate() sees the cache already covers
144
- # the first n_prefix positions and prefills only the suffix.
145
- input_ids = full_ids["input_ids"].to(self.device)
146
- attention = full_ids["attention_mask"].to(self.device)
147
- with self.torch.no_grad():
148
- out = self.model.generate(
149
- input_ids=input_ids,
150
- attention_mask=attention,
151
- past_key_values=cache,
152
- max_new_tokens=max_tokens,
153
- do_sample=False,
154
- use_cache=True,
155
- pad_token_id=self.tokenizer.eos_token_id,
156
- )
157
- new_tokens = out[0][input_ids.shape[1]:]
158
- return self.tokenizer.decode(new_tokens, skip_special_tokens=True)
159
-
160
- return self._plain_generate(full_text, max_tokens)
161
-
162
- def _plain_generate(self, full_text: str, max_tokens: int) -> str:
163
- inputs = self.tokenizer(full_text, return_tensors="pt").to(self.device)
164
- with self.torch.no_grad():
165
- out = self.model.generate(
166
- **inputs, max_new_tokens=max_tokens, do_sample=False,
167
- use_cache=True, pad_token_id=self.tokenizer.eos_token_id,
168
- )
169
- new_tokens = out[0][inputs["input_ids"].shape[1]:]
170
- return self.tokenizer.decode(new_tokens, skip_special_tokens=True)
171
-
172
-
173
- def precompute_pack_caches(model_path: str, packs_dir: str, cache_dir: str,
174
- system: str, device: str = "auto") -> list:
175
- """Warm the disk cache for every pack — run once at deploy time."""
176
- import sys
177
- sys.path.insert(0, str(Path(__file__).parent.parent))
178
- from pharos.pack_loader import PackLibrary
179
-
180
- injector = KVInjector(model_path, device=device, cache_dir=cache_dir)
181
- library = PackLibrary(packs_dir)
182
- warmed = []
183
- for pack in library.packs:
184
- knowledge = pack.triples_text()
185
- full_system = f"{system}\n\n# INJECTED KNOWLEDGE (Pharos)\n{knowledge}"
186
- prefix_text, _ = injector._render(full_system, "warmup")
187
- if prefix_text:
188
- key, _ = injector._prefix_cache(prefix_text)
189
- warmed.append({"pack": pack.name, "cache_key": key})
190
- return warmed
191
-
192
-
193
- if __name__ == "__main__":
194
- import argparse
195
- import json
196
- import sys
197
-
198
- sys.path.insert(0, str(Path(__file__).parent.parent))
199
- from engine.synthesis import RIVET_SYSTEM
200
-
201
- ap = argparse.ArgumentParser(description="Precompute Pharos pack KV caches")
202
- ap.add_argument("--model", required=True,
203
- help="HF model path, e.g. Qwen/Qwen2.5-Coder-32B-Instruct")
204
- ap.add_argument("--packs-dir", default="packs")
205
- ap.add_argument("--cache-dir", default="pack_kv_cache")
206
- ap.add_argument("--device", default="auto")
207
- args = ap.parse_args()
208
-
209
- logging.basicConfig(level=logging.INFO)
210
- warmed = precompute_pack_caches(
211
- args.model, args.packs_dir, args.cache_dir, RIVET_SYSTEM, args.device,
212
- )
213
- print(json.dumps(warmed, indent=2))