Skip to content

Commit acc3fe2

Browse files
committed
feat(speculative): Improve ngram-map draft selection and accept feedback
- Store accepted draft lengths per key/value and truncate future drafts accordingly - Make key-only mode draft on any key match without applying min_hits - Select k4v continuations by frequency instead of latest occurrence - Skip ambiguous k4v drafts when the top continuation is not dominant - Track fixed-size k4v continuations to keep frequency statistics comparable Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent a27b100 commit acc3fe2

1 file changed

Lines changed: 78 additions & 15 deletions

File tree

llama_cpp/llama_speculative.py

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,23 @@ def __init__(
106106
# key -> {position: continuation}
107107
#
108108
# A dict is used so that recent entries can be refreshed when more continuation
109-
# tokens become available.
109+
# tokens become available. Draft selection is based on continuation frequency,
110+
# not just the most recent continuation.
110111
self._map_k4v: DefaultDict[
111112
Tuple[int, ...], Dict[int, Tuple[int, ...]]
112113
] = collections.defaultdict(dict)
113114

115+
# Acceptance feedback, aligned with llama.cpp's ngram-map behavior:
116+
# accept(n) stores how many tokens were accepted for the key/value used by
117+
# the previous draft and limits the future draft length for that key/value.
118+
self._accepted_k: Dict[Tuple[int, ...], int] = {}
119+
self._accepted_k4v: DefaultDict[
120+
Tuple[int, ...], Dict[Tuple[int, ...], int]
121+
] = collections.defaultdict(dict)
122+
123+
self._last_draft_key: Optional[Tuple[int, ...]] = None
124+
self._last_draft_value: Optional[Tuple[int, ...]] = None
125+
114126
self._closed = False
115127
self._last_draft_len = 0
116128

@@ -124,6 +136,10 @@ def clear(self) -> None:
124136
self._history.clear()
125137
self._map_k.clear()
126138
self._map_k4v.clear()
139+
self._accepted_k.clear()
140+
self._accepted_k4v.clear()
141+
self._last_draft_key = None
142+
self._last_draft_value = None
127143
self._last_draft_len = 0
128144

129145
def close(self) -> None:
@@ -147,13 +163,27 @@ def accept(self, n_accepted: int) -> None:
147163
"""
148164
Notify how many draft tokens were accepted by the target model.
149165
150-
This implementation does not need to update internal state here, because the
151-
next call receives the verified token history through `input_ids`.
152-
153-
The method is kept for API symmetry and future extensions, such as acceptance
154-
statistics, adaptive reset, or low-acceptance fallback.
166+
The accepted length is written back to the key/value used by the previous
167+
draft. Future drafts for the same key/value are truncated to this accepted
168+
length, matching llama.cpp's ngram-map feedback loop.
155169
"""
156-
return
170+
if n_accepted < 0:
171+
raise ValueError("n_accepted must be non-negative")
172+
173+
if self._last_draft_key is None or self._last_draft_len <= 0:
174+
return
175+
176+
accepted = min(int(n_accepted), self._last_draft_len)
177+
178+
if self.mode == "k":
179+
self._accepted_k[self._last_draft_key] = accepted
180+
else:
181+
if self._last_draft_value is not None:
182+
self._accepted_k4v[self._last_draft_key][self._last_draft_value] = accepted
183+
184+
self._last_draft_key = None
185+
self._last_draft_value = None
186+
self._last_draft_len = 0
157187

158188
def _sync_and_index(self, input_ids: npt.NDArray[np.intc]) -> None:
159189
"""
@@ -231,9 +261,11 @@ def _sync_and_index(self, input_ids: npt.NDArray[np.intc]) -> None:
231261
for pos in range(start, end):
232262
key_start = pos
233263
value_start = pos + self.ngram_size
234-
value_end = min(value_start + self.num_pred_tokens, len(self._history))
264+
value_end = value_start + self.num_pred_tokens
235265

236-
if value_start >= value_end:
266+
# K4V tracks fixed-size continuation m-grams. Partial tail values are
267+
# intentionally skipped so frequency statistics remain comparable.
268+
if value_end > len(self._history):
237269
continue
238270

239271
key = tuple(self._history[key_start:value_start])
@@ -267,6 +299,8 @@ def __call__(
267299
_ = kwargs
268300

269301
self._sync_and_index(input_ids)
302+
self._last_draft_key = None
303+
self._last_draft_value = None
270304
self._last_draft_len = 0
271305

272306
if len(self._history) < self.ngram_size:
@@ -276,28 +310,57 @@ def __call__(
276310

277311
if self.mode == "k":
278312
positions = self._map_k.get(search_key)
279-
if not positions or len(positions) < self.min_hits:
313+
if not positions:
280314
return np.array([], dtype=np.intc)
281315

282-
# Use the latest valid match with an available continuation.
316+
# Key-only mode follows llama.cpp's ngram-map-k behavior: once a key
317+
# match is found, draft from the latest valid match. min_hits is not
318+
# used as a confidence gate for key-only mode.
283319
draft: List[int] = []
320+
accepted_limit = self._accepted_k.get(search_key, self.num_pred_tokens)
321+
if accepted_limit <= 0:
322+
return np.array([], dtype=np.intc)
323+
284324
for pos in reversed(positions):
285325
start = pos + self.ngram_size
286326
if start < len(self._history):
287-
end = min(start + self.num_pred_tokens, len(self._history))
327+
end = min(start + accepted_limit, len(self._history))
288328
draft = self._history[start:end]
289329
break
290330

331+
self._last_draft_key = search_key
332+
291333
else:
292334
values = self._map_k4v.get(search_key)
293335
if not values or len(values) < self.min_hits:
294336
return np.array([], dtype=np.intc)
295337

296-
# Use the continuation from the latest historical position.
297-
latest_pos = max(values)
298-
draft = list(values[latest_pos])
338+
# K4V mode chooses the most frequent continuation m-gram rather than the
339+
# latest one. If the strongest continuation is not at least twice as
340+
# frequent as all other continuations combined, skip drafting.
341+
counts = collections.Counter(values.values())
342+
best_value, best_count = counts.most_common(1)[0]
343+
other_count = sum(counts.values()) - best_count
344+
345+
if other_count > 0 and best_count < 2 * other_count:
346+
return np.array([], dtype=np.intc)
347+
348+
accepted_limit = self._accepted_k4v[search_key].get(
349+
best_value, self.num_pred_tokens
350+
)
351+
if accepted_limit <= 0:
352+
return np.array([], dtype=np.intc)
353+
354+
draft = list(best_value[:accepted_limit])
355+
self._last_draft_key = search_key
356+
self._last_draft_value = best_value
299357

300358
self._last_draft_len = len(draft)
359+
if self._last_draft_len <= 0:
360+
self._last_draft_key = None
361+
self._last_draft_value = None
362+
return np.array([], dtype=np.intc)
363+
301364
return np.asarray(draft, dtype=np.intc)
302365

303366

0 commit comments

Comments
 (0)