Skip to content

Commit d5d381f

Browse files
authored
[Hotfix] Fix performance regression introduced in #1261 (#1276)
#1261 added a logit cache, which was being concatenated to in a loop. This PR never caches any logits except for the final one, but if `include_all_uncached_tokens` is `True`, then `get_logits` will return an array of shape `(len(tokens)-num_cached, vocab_size)`. Otherwise, it will return an array of shape `(1, vocab_size)` This "should" include all tokens being fast-forwarded (unless the fast-forwarded tokens are somehow already in the kv, e.g. if a notebook cell is re-run). If any are missing, I just call the probabilities `nan` here, which could in principle cause some artifacts in the widget. Note that an alternative implementation would have been to keep the full logit cache, pre-allocating the buffer in advance with shape `(n_ctx, vocab_size)`, but this feels really unnecessary. Happy to revisit after we get this merged and the big performance issues are gone. @nopdive would you mind re-running the repro? Note that you shouldn't see a speedup for the prompt part (as this PR does not concern pre-filling), but you should see a speedup for the rest of the completion. Side note, but we can get rid of the (relatively small, mind you) prompt slowdown by simply deciding to omit probabilities from "input" tokens. Lmk if you want that, and I can open a follow-up PR. Edit: Intended to fix #1275
1 parent 9346abe commit d5d381f

4 files changed

Lines changed: 68 additions & 72 deletions

File tree

guidance/models/_engine/_engine.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,9 @@ def __call__(
212212
logits_lat_ms = 0.0
213213
else:
214214
t1 = time.time()
215-
logits = self.get_logits(token_ids=tokens, full_sequence=True)
215+
logits = self.get_logits(token_ids=tokens, include_all_uncached_tokens=True)
216216
logits_lat_ms = (time.time() - t1) * 1000
217217

218-
219218
# Important: don't wait on this future until after getting the logits;
220219
# this allows the mask to be built concurrently with model inference
221220
mask, ll_response = mask_fut.result()
@@ -229,19 +228,27 @@ def __call__(
229228

230229
if logits is not None:
231230
# Not the last one -- that's for the *next* token.
232-
ff_logits = logits[max(len(logits)-len(ff_tokens)-1, 0):-1]
231+
ff_logits = logits[-len(ff_tokens)-1:-1]
233232
ff_probs = (
234-
softmax(np.array(ff_logits))
233+
softmax(ff_logits)
235234
if last_temperature < _TEMPERATURE_EPSILON
236-
else softmax(np.array(ff_logits) / last_temperature)
235+
else softmax(ff_logits / last_temperature)
237236
)
238-
if ff_probs.shape[0] != len(ff_tokens):
239-
assert ff_probs.shape[0] == len(ff_tokens) - 1
237+
238+
if len(ff_tokens) == len(tokens) and ff_probs.shape[0] == len(ff_tokens) - 1:
240239
# We didn't have a BOS token, so we need to fake the first token prob (say... 1?)
241-
ff_probs = np.concatenate(
242-
(np.ones((1, ff_probs.shape[1])), ff_probs), axis=0
240+
ff_probs = np.pad(
241+
ff_probs, [(1, 0), (0, 0)], mode='constant', constant_values=1.0
242+
)
243+
elif ff_probs.shape[0] < len(ff_tokens):
244+
# Not enough logits were returned despite include_all_uncached_tokens=True, probably due to
245+
# using a mock model that doesn't bother to return logits for uncached tokens (all are uncached...)
246+
ff_probs = np.pad(
247+
ff_probs, [(len(ff_tokens) - ff_probs.shape[0], 0), (0, 0)],
248+
mode='constant', constant_values=np.nan
243249
)
244250
else:
251+
# really just for mypy -- we shouldn't need this
245252
ff_probs = np.empty(shape=())
246253

247254
gen_tokens = []
@@ -448,5 +455,14 @@ def get_top_k(_probs: NDArray, _k: int = 5) -> list[GenToken]:
448455
return output
449456

450457
@abstractmethod
451-
def get_logits(self, token_ids: list[int], full_sequence: bool = False) -> NDArray:
458+
def get_logits(self, token_ids: list[int], include_all_uncached_tokens: bool = False) -> NDArray:
459+
"""
460+
Get the logits for the given token ids.
461+
If include_all_uncached_tokens is True:
462+
logits for all uncached tokens will be returned, i.e.
463+
the return value's shape will be `(len(tokens)-num_cached, vocab_size)`.
464+
If include_all_uncached_tokens is False:
465+
logits for the last token will be returned, i.e.
466+
the return value's shape will be `(1, vocab_size)`.
467+
"""
452468
pass

guidance/models/_llama_cpp.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def __init__(
148148
compute_log_probs=compute_log_probs, enable_backtrack=enable_backtrack,
149149
enable_ff_tokens=enable_ff_tokens, enable_monitoring=enable_monitoring, **kwargs)
150150

151-
def get_logits(self, token_ids, full_sequence=False):
151+
def get_logits(self, token_ids: list[int], include_all_uncached_tokens: bool = False) -> np.ndarray:
152152
"""Computes the logits for the given token state.
153153
154154
This overrides a method from the LocalEngine class that is used to get
@@ -167,20 +167,17 @@ def get_logits(self, token_ids, full_sequence=False):
167167
# check what we have already cached
168168
num_cached = sum(takewhile(operator.truth, map(operator.eq, token_ids, self._cached_token_ids)))
169169
if num_cached == len(token_ids):
170-
if full_sequence:
171-
return self._cached_logits[:num_cached, :]
172-
else:
173-
return self._cached_logits[[num_cached - 1], :]
170+
if num_cached == len(self._cached_token_ids):
171+
# last token input is the same as the last cached token, so return the last cached logits
172+
return self._cached_logits
173+
# we need to pass at least one new token
174+
num_cached = num_cached - 1
174175

175176
# clear obsolete parts of kv cache
176177
llama_cpp.llama_kv_cache_seq_rm(self.model_obj.ctx, -1, num_cached, -1)
177178

178-
# clear obsolete parts of logit cache
179-
if self._cached_logits is not None and len(self._cached_logits) > num_cached:
180-
self._cached_logits = self._cached_logits[:num_cached, :]
181-
182179
# eval the model
183-
logits_for_each_batch = []
180+
logits_for_each_batch: list[np.ndarray] = []
184181
n_batch = self.model_obj.n_batch
185182
batch = self._context.batch
186183
for i in range(num_cached, len(token_ids), n_batch):
@@ -199,31 +196,30 @@ def get_logits(self, token_ids, full_sequence=False):
199196

200197
# get the logits for *this* batch
201198
llama_logits = llama_cpp.llama_get_logits(self.model_obj.ctx)
202-
logits = np.ctypeslib.as_array(
199+
logits_for_this_batch = np.ctypeslib.as_array(
203200
llama_logits,
204201
shape=(n_tokens, self._n_vocab),
205202
).copy()
206-
logits_for_each_batch.append(logits)
203+
logits_for_each_batch.append(logits_for_this_batch)
207204

208205
# update the metrics
209206
self.metrics.engine_output_tokens += 1
210207
self.metrics.engine_input_tokens += len(token_ids) - num_cached
211208

212209
# save the results
213210
self._cached_token_ids = token_ids.copy()
211+
self._cached_logits = logits_for_each_batch[-1][[-1], :]
214212

215-
if self._cached_logits is not None:
216-
logits_for_each_batch = [self._cached_logits] + logits_for_each_batch
217-
218-
self._cached_logits = np.concatenate(
219-
logits_for_each_batch,
220-
axis=0
221-
)
222-
223-
if full_sequence:
224-
return self._cached_logits
213+
if include_all_uncached_tokens:
214+
logits = np.concatenate(
215+
logits_for_each_batch,
216+
axis=0
217+
)
218+
assert logits.shape[0] == len(token_ids) - num_cached
219+
return logits
225220
else:
226-
return self._cached_logits[[-1], :]
221+
return self._cached_logits
222+
227223

228224
class LlamaCpp(Model):
229225
def __init__(

guidance/models/_mock.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,7 @@ def get_next_token_with_top_k(
112112
logits, logits_lat_ms, token_ids, mask, temperature, k, force_return_unmasked_probs
113113
)
114114

115-
def get_logits(self, token_ids: list[int], full_sequence: bool = False) -> np.ndarray:
116-
"""Get the logits for the given token state."""
117-
if not full_sequence:
118-
return self._get_logits(token_ids).reshape(1, -1)
119-
else:
120-
# TODO: is it worth it to add a prefix cache here?
121-
l0 = self._get_logits([token_ids[0]])
122-
# if we are getting the full sequence then we need to compute the logits for all tokens
123-
logits = np.zeros((len(token_ids), len(l0)))
124-
logits[0] = l0
125-
for i in range(1, len(token_ids)):
126-
logits[i] = self._get_logits(token_ids[: i + 1])
127-
return logits
128-
129-
def _get_logits(self, token_ids: list[int]) -> np.ndarray:
115+
def get_logits(self, token_ids: list[int], include_all_uncached_tokens: bool = False) -> np.ndarray:
130116
"""Pretends to compute the logits for the given token state."""
131117
if len(token_ids) == 0:
132118
raise ValueError("token_ids must not be empty")
@@ -154,7 +140,7 @@ def _get_logits(self, token_ids: list[int]) -> np.ndarray:
154140
logits[i] += bias
155141
bias /= 2 # if we have multiple matches then they apply with decreasing bias
156142

157-
return logits
143+
return logits.reshape(1, -1)
158144

159145
def _get_next_tokens(self, byte_string):
160146
special_tokens = [

guidance/models/_transformers.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ def _model(self, model, **kwargs) -> "PreTrainedModel":
459459
model = transformers_package.AutoModelForCausalLM.from_pretrained(model, **kwargs)
460460
return model
461461

462-
def get_logits(self, token_ids: list[int], full_sequence: bool = False):
462+
def get_logits(self, token_ids: list[int], include_all_uncached_tokens: bool = False) -> np.ndarray:
463463
"""Computes the logits for the given token state.
464464
465465
This overrides a method from the LocalEngine class that is used to get
@@ -478,10 +478,11 @@ def get_logits(self, token_ids: list[int], full_sequence: bool = False):
478478
# check what we have already cached
479479
num_cached = sum(takewhile(operator.truth, map(operator.eq, token_ids, self._cached_token_ids)))
480480
if num_cached == len(token_ids):
481-
if full_sequence:
482-
return self._cached_logits[:num_cached, :]
483-
else:
484-
return self._cached_logits[[num_cached - 1], :]
481+
if num_cached == len(self._cached_token_ids):
482+
# last token input is the same as the last cached token, so return the last cached logits
483+
return self._cached_logits
484+
# we need to pass at least one new token
485+
num_cached = num_cached - 1
485486

486487
# check how many tokens are in the kv cache and what the max size of the cache is
487488
past_key_values = self._past_key_values
@@ -558,17 +559,16 @@ def get_logits(self, token_ids: list[int], full_sequence: bool = False):
558559
else:
559560
self._past_key_values = None
560561
past_length = 0
561-
562-
# clear obsolete parts of logit cache
563-
if self._cached_logits is not None and len(self._cached_logits) > num_cached:
564-
self._cached_logits = self._cached_logits[:num_cached, :]
562+
else:
563+
# Should never happen, but just in case -- we're not using all of the cached tokens
564+
# (because they're not really in the kv cache somehow)
565+
num_cached = past_length
565566

566567
# eval the model
567-
assert past_length <= num_cached
568-
assert num_cached <= len(token_ids)
568+
assert past_length <= len(token_ids)
569569
new_token_ids = token_ids[past_length:]
570570
assert len(new_token_ids) > 0
571-
logits_for_each_batch = []
571+
logits_for_each_batch: list[np.ndarray] = []
572572
with torch.no_grad():
573573
# Not all models support batched tokens for some reason
574574
try:
@@ -628,19 +628,17 @@ def get_logits(self, token_ids: list[int], full_sequence: bool = False):
628628
# save the results
629629
self._past_key_values = model_out.past_key_values
630630
self._cached_token_ids = token_ids.copy()
631+
self._cached_logits = logits_for_each_batch[-1][[-1], :]
631632

632-
if self._cached_logits is not None:
633-
logits_for_each_batch = [self._cached_logits] + logits_for_each_batch
634-
635-
self._cached_logits = np.concatenate(
636-
logits_for_each_batch,
637-
axis=0
638-
)
639-
640-
if full_sequence:
641-
return self._cached_logits
633+
if include_all_uncached_tokens:
634+
logits = np.concatenate(
635+
logits_for_each_batch,
636+
axis=0
637+
)
638+
assert logits.shape[0] == len(token_ids) - num_cached
639+
return logits
642640
else:
643-
return self._cached_logits[[-1], :]
641+
return self._cached_logits
644642

645643
class Transformers(Model):
646644
def __init__(

0 commit comments

Comments
 (0)