Skip to content

Commit ee88c86

Browse files
authored
Add probabilities back in for ff_tokens (#1261)
Computes the probabilities of fast-forwarded tokens (in addition to generated tokens, for which we already had probabilities). To implement this, I implemented a "logit cache" for the `TransformersEngine` and `LlamacppEngine` classes, which maintains a cached version of all `(seq_len, vocab_size)` logits that have been computed thus far. Each call into the underlying model object will (possibly) truncate and then append to the cache (i.e. it has the same semantics as the KV cache, but its size should be pretty negligable in comparison). There is a slight downside -- we need to call the model one extra time at the end of generation IF the generation ended with fast-forwarded tokens. We need to do this in order to get their probabilities. Another note here, but I'm currently caching the logits as numpy arrays. In principle, it might be nice to keep torch tensors on their original device... All of the round trips might be kind of expensive. That being said, this issue was already present in the previous implementation. It would be great to follow this PR up with something that gets the top-k tokens for ff tokens too.
1 parent 960d613 commit ee88c86

8 files changed

Lines changed: 228 additions & 424 deletions

File tree

guidance/models/_engine/_engine.py

Lines changed: 53 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TYPE_CHECKING, Callable, Iterator, Optional
88

99
import numpy as np
10+
from numpy.typing import NDArray
1011

1112
from ..._parser import TokenParser
1213
from ..._schema import (
@@ -76,12 +77,11 @@ class Engine(ABC):
7677
Server so a single server can serve many clients' model objects through a single Engine object.
7778
"""
7879

79-
def __init__(self, tokenizer: Tokenizer, compute_log_probs=False, enable_backtrack=True, enable_ff_tokens=True,
80+
def __init__(self, tokenizer: Tokenizer, enable_backtrack=True, enable_ff_tokens=True,
8081
enable_monitoring=True, **kwargs):
8182
from ...registry import get_monitor
8283

8384
self.tokenizer = tokenizer
84-
self.compute_log_probs = compute_log_probs
8585
self._enable_backtrack = enable_backtrack
8686
self._enable_ff_tokens = enable_ff_tokens
8787
self._enable_monitoring = enable_monitoring
@@ -165,8 +165,8 @@ def __call__(
165165
enable_ff_tokens=self.enable_ff_tokens,
166166
)
167167

168+
last_temperature = 1.0
168169
engine_output = None
169-
logits_lat_ms = 0
170170
while not parser.done():
171171
t0 = time.time()
172172

@@ -197,18 +197,26 @@ def __call__(
197197
# (model + prompt) + grammar == model + (prompt + grammar)
198198
tokens = self.tokenizer.recode(tokens)
199199

200-
# Note that has_pending_stop implies that the response is a stop response,
201-
# but the converse is not true. We can therefore avoid some (but not all)
202-
# unnecessary calls to get_logits on the final iteration.
203-
has_pending_stop = parser.has_pending_stop()
204-
205-
if not has_pending_stop:
200+
# We can avoid a final get_logits call in the case that:
201+
# 1. The parser has a pending stop
202+
# 2. There are no ff_tokens (except for our last generated token)
203+
# TODO: allow avoiding final forward pass if metrics are disabled
204+
# and we have a pending stop
205+
if parser.has_pending_stop() and (
206+
(not ff_tokens)
207+
or (
208+
len(ff_tokens) == 1
209+
and engine_output is not None
210+
and ff_tokens[0] == engine_output.issued_token.token_id
211+
)
212+
):
213+
logits = None
214+
logits_lat_ms = 0.0
215+
else:
206216
t1 = time.time()
207-
logits = self.get_logits(token_ids=tokens)
217+
logits = self.get_logits(token_ids=tokens, full_sequence=True)
208218
logits_lat_ms = (time.time() - t1) * 1000
209-
else:
210-
# Avoid calling get_logits if we know we won't use it
211-
logits = None
219+
212220

213221
# Important: don't wait on this future until after getting the logits;
214222
# this allows the mask to be built concurrently with model inference
@@ -221,13 +229,31 @@ def __call__(
221229
# if it exists
222230
ff_lat_ms -= logits_lat_ms
223231

232+
if logits is not None:
233+
# Not the last one -- that's for the *next* token.
234+
ff_logits = logits[max(len(logits)-len(ff_tokens)-1, 0):-1]
235+
ff_probs = (
236+
softmax(np.array(ff_logits))
237+
if last_temperature < 0.0001
238+
else softmax(np.array(ff_logits) / last_temperature)
239+
)
240+
if ff_probs.shape[0] != len(ff_tokens):
241+
assert ff_probs.shape[0] == len(ff_tokens) - 1
242+
# We didn't have a BOS token, so we need to fake the first token prob (say... 1?)
243+
ff_probs = np.concatenate(
244+
(np.ones((1, ff_probs.shape[1])), ff_probs), axis=0
245+
)
246+
else:
247+
ff_probs = np.empty(shape=())
248+
224249
gen_tokens = []
225250
if engine_output is None:
226-
for token_id in ff_tokens:
251+
for i, token_id in enumerate(ff_tokens):
227252
gen_tokens.append(
228253
GenToken(
229254
token_id=token_id,
230255
bytes=self.tokenizer.decode([token_id]),
256+
prob=ff_probs[i, token_id],
231257
# amortize latency
232258
latency_ms=ff_lat_ms/len(ff_tokens),
233259
is_input=True,
@@ -240,11 +266,12 @@ def __call__(
240266
ff_start_index = 0
241267
else:
242268
ff_start_index = 1
243-
for token_id in ff_tokens[ff_start_index:]:
269+
for i, token_id in enumerate(ff_tokens[ff_start_index:], start=ff_start_index):
244270
gen_tokens.append(
245271
GenToken(
246272
token_id=token_id,
247273
bytes=self.tokenizer.decode([token_id]),
274+
prob=ff_probs[i, token_id],
248275
# amortize latency
249276
latency_ms=ff_lat_ms/len(ff_tokens[ff_start_index:]),
250277
is_force_forwarded=True,
@@ -272,10 +299,8 @@ def __call__(
272299
# Ensure we break AFTER yielding the final response
273300
break
274301

275-
# If there was a pending stop, we should have broken out of the loop
276-
assert not has_pending_stop
277-
278302
# Help the type checker: assert that everything we need to get the next token is not None
303+
assert logits is not None
279304
assert mask is not None
280305
assert ll_response.temperature is not None
281306

@@ -294,14 +319,15 @@ def __call__(
294319
mask_for_sampling = mask
295320

296321
engine_output = self.get_next_token_with_top_k(
297-
logits=logits,
322+
logits=logits[-1, :],
298323
logits_lat_ms=logits_lat_ms,
299324
token_ids=tokens,
300325
mask=mask_for_sampling,
301326
temperature=ll_response.temperature,
302327
k=self._top_k,
303328
force_return_unmasked_probs=echo,
304329
)
330+
last_temperature = ll_response.temperature
305331

306332
if can_finish_early and not mask[engine_output.issued_token.token_id]:
307333
# Type checker needs some help
@@ -310,8 +336,8 @@ def __call__(
310336

311337
def get_next_token_with_top_k(
312338
self,
313-
logits: Optional[np.ndarray],
314-
logits_lat_ms: Optional[float],
339+
logits: NDArray,
340+
logits_lat_ms: float,
315341
token_ids: list[int],
316342
mask: Optional[bytes],
317343
temperature: float,
@@ -322,7 +348,7 @@ def get_next_token_with_top_k(
322348
323349
Parameters
324350
-------
325-
logits : Optional[np.ndarray]
351+
logits : Optional[NDArray]
326352
The logits for the current token ids in the sequence.
327353
If None, the model will call get_logits to get the logits.
328354
logits_lat_ms: Optional[float]
@@ -345,38 +371,7 @@ def get_next_token_with_top_k(
345371
The output from the model.
346372
"""
347373

348-
if logits is None:
349-
t0 = time.time()
350-
try:
351-
logits = self.get_logits(token_ids=token_ids)
352-
except NotImplementedError:
353-
# fallback to orignal get_next_token method
354-
_t0 = time.time()
355-
token_id = self.get_next_token(
356-
token_ids=token_ids,
357-
mask=mask,
358-
temperature=temperature,
359-
)
360-
_lat = (time.time() - _t0) * 1000
361-
362-
_issued_token = GenToken(
363-
token_id=token_id,
364-
prob=1.0,
365-
bytes=self.tokenizer.decode([token_id]),
366-
latency_ms=_lat,
367-
is_generated=True,
368-
)
369-
370-
return EngineOutput(
371-
issued_token=_issued_token,
372-
top_k=[_issued_token],
373-
masked_top_k=[_issued_token] if mask is not None else None,
374-
)
375-
lat_ms = (time.time() - t0) * 1000
376-
else:
377-
lat_ms = logits_lat_ms
378-
379-
def get_top_k(_probs: np.ndarray, _k: int = 5) -> list[GenToken]:
374+
def get_top_k(_probs: NDArray, _k: int = 5) -> list[GenToken]:
380375
top_k_indices = np.argsort(_probs)[::-1][:_k]
381376
top_k_probs = _probs[top_k_indices]
382377

@@ -385,7 +380,7 @@ def get_top_k(_probs: np.ndarray, _k: int = 5) -> list[GenToken]:
385380
token_id=token,
386381
prob=prob,
387382
bytes=self.tokenizer.decode([token]),
388-
latency_ms=lat_ms,
383+
latency_ms=logits_lat_ms,
389384
is_generated=True,
390385
)
391386
for token, prob in zip(top_k_indices, top_k_probs)
@@ -435,7 +430,7 @@ def get_top_k(_probs: np.ndarray, _k: int = 5) -> list[GenToken]:
435430
token_id=sampled_index,
436431
prob=sampled_prob,
437432
bytes=self.tokenizer.decode([sampled_index]),
438-
latency_ms=lat_ms,
433+
latency_ms=logits_lat_ms,
439434
is_generated=True,
440435
)
441436

@@ -449,15 +444,11 @@ def get_top_k(_probs: np.ndarray, _k: int = 5) -> list[GenToken]:
449444
return output
450445

451446
@abstractmethod
452-
def get_logits(self, token_ids: list[int]) -> np.ndarray:
447+
def get_logits(self, token_ids: list[int], full_sequence: bool = False) -> NDArray:
453448
pass
454449

455-
def get_per_token_topk_probs(self, token_ids: list[int], top_k: int = 5) -> list[GenToken]:
456-
"""Get the top-k probabilities for each token in the sequence."""
457-
raise NotImplementedError
458-
459450
def sample_with_temperature(
460-
self, logits: np.ndarray, mask: Optional[bytes], temperature: float
451+
self, logits: NDArray, mask: Optional[bytes], temperature: float
461452
) -> int:
462453
if mask is not None:
463454
logits += np.frombuffer(mask, dtype=np.uint8)

0 commit comments

Comments
 (0)