Optimize group beam search by sharing the initial prefill across beams

#6
by lavrenko - opened
Files changed (1) hide show
  1. custom_generate/generate.py +75 -7
custom_generate/generate.py CHANGED
@@ -1,5 +1,6 @@
1
  import torch
2
  from transformers import LogitsProcessor, LogitsProcessorList, StoppingCriteriaList, GenerationConfig
 
3
  from transformers.generation.utils import (
4
  GenerationMixin,
5
  GenerateBeamDecoderOnlyOutput,
@@ -241,6 +242,34 @@ def _group_beam_search(
241
  device = input_ids.device
242
 
243
  batch_beam_size, cur_len = input_ids.shape
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  # Does not exist anymore in recent versions!
245
  if hasattr(model, "_get_initial_cache_position"):
246
  model_kwargs = model._get_initial_cache_position(cur_len, input_ids.device, model_kwargs)
@@ -313,12 +342,37 @@ def _group_beam_search(
313
  else 1
314
  )
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  # do one decoder step on all beams of all sentences in batch
317
  model_inputs = model.prepare_inputs_for_generation(
318
- input_ids,
319
  next_sequence_length=next_sequence_length,
320
  is_first_iteration=is_first_iteration,
321
- **model_kwargs,
322
  )
323
 
324
  # prepare variable output controls (note: some models won't accept all output controls)
@@ -344,7 +398,10 @@ def _group_beam_search(
344
  continue
345
 
346
  if output_scores:
347
- processed_score = torch.zeros_like(outputs.logits[:, -1, :])
 
 
 
348
  if output_logits:
349
  # Copy is needed to avoid keeping a hanging ref to outputs.logits which may be very large for first iteration
350
  # (the clone itself is always small)
@@ -372,9 +429,12 @@ def _group_beam_search(
372
  # select outputs of beams of current group only
373
  # No need to clone() the logits here as they will not retain outputs.logits at the end of the loop
374
  # .float() is needed to retain precision for later logits manipulations
375
- next_token_logits = outputs.logits[batch_group_indices, -1, :].to(
376
- dtype=torch.float32, device=input_ids.device
377
- )
 
 
 
378
 
379
  next_token_scores = nn.functional.log_softmax(
380
  next_token_logits, dim=-1
@@ -483,8 +543,16 @@ def _group_beam_search(
483
  # (that way the memory peak does not include outputs.logits)
484
  del outputs
485
 
486
- # NOTE: we need to check if `model._reorder_cache` exists for special models like RAG, RecurrentGemma etc.
 
487
  if model_kwargs.get("past_key_values", None) is not None:
 
 
 
 
 
 
 
488
  if hasattr(model, "_reorder_cache"):
489
  model_kwargs["past_key_values"] = model._reorder_cache(
490
  model_kwargs["past_key_values"], reordering_indices
 
1
  import torch
2
  from transformers import LogitsProcessor, LogitsProcessorList, StoppingCriteriaList, GenerationConfig
3
+ from transformers.cache_utils import DynamicCache
4
  from transformers.generation.utils import (
5
  GenerationMixin,
6
  GenerateBeamDecoderOnlyOutput,
 
242
  device = input_ids.device
243
 
244
  batch_beam_size, cur_len = input_ids.shape
245
+ cache = model_kwargs.get("past_key_values")
246
+ use_cache = model_kwargs.get("use_cache", True)
247
+
248
+ # Shared prefill is safe only when the generation state follows the
249
+ # standard contracts that this optimization knows how to reverse.
250
+ can_share_prefill = (
251
+ not model.training
252
+ and type(model)._expand_inputs_for_generation
253
+ is GenerationMixin._expand_inputs_for_generation
254
+ and type(model)._update_model_kwargs_for_generation
255
+ is GenerationMixin._update_model_kwargs_for_generation
256
+ and (
257
+ (not use_cache and cache is None)
258
+ or (
259
+ use_cache
260
+ and isinstance(cache, DynamicCache)
261
+ and not cache.is_compileable
262
+ and cache.get_seq_length() == 0
263
+ )
264
+ )
265
+ and all(
266
+ not isinstance(value, torch.Tensor)
267
+ or value.ndim == 0
268
+ or value.shape[0] == batch_beam_size
269
+ for value in model_kwargs.values()
270
+ )
271
+ )
272
+
273
  # Does not exist anymore in recent versions!
274
  if hasattr(model, "_get_initial_cache_position"):
275
  model_kwargs = model._get_initial_cache_position(cur_len, input_ids.device, model_kwargs)
 
342
  else 1
343
  )
344
 
345
+ # prefill one prompt per batch item before beams diverge
346
+ # keep the original path when beam expansion cannot be safely undone
347
+ shared_prefill = (
348
+ is_first_iteration
349
+ and can_share_prefill
350
+ and not model.config.is_encoder_decoder
351
+ and not (output_attentions or output_hidden_states or output_logits)
352
+ )
353
+
354
+ # keep the beam-shaped search state; reduce only the first model forward
355
+ forward_input_ids = input_ids
356
+ forward_model_kwargs = model_kwargs
357
+
358
+ if shared_prefill:
359
+ # undo generate()'s beam expansion for model inputs
360
+ forward_input_ids = input_ids[::num_beams]
361
+ forward_model_kwargs = {
362
+ key: value[::num_beams]
363
+ if isinstance(value, torch.Tensor)
364
+ and value.ndim > 0
365
+ and value.shape[0] == batch_beam_size
366
+ else value
367
+ for key, value in model_kwargs.items()
368
+ }
369
+
370
  # do one decoder step on all beams of all sentences in batch
371
  model_inputs = model.prepare_inputs_for_generation(
372
+ forward_input_ids,
373
  next_sequence_length=next_sequence_length,
374
  is_first_iteration=is_first_iteration,
375
+ **forward_model_kwargs,
376
  )
377
 
378
  # prepare variable output controls (note: some models won't accept all output controls)
 
398
  continue
399
 
400
  if output_scores:
401
+ # scores remain beam-shaped although the first model output is shared
402
+ processed_score = outputs.logits.new_zeros(
403
+ (batch_beam_size, outputs.logits.shape[-1])
404
+ )
405
  if output_logits:
406
  # Copy is needed to avoid keeping a hanging ref to outputs.logits which may be very large for first iteration
407
  # (the clone itself is always small)
 
429
  # select outputs of beams of current group only
430
  # No need to clone() the logits here as they will not retain outputs.logits at the end of the loop
431
  # .float() is needed to retain precision for later logits manipulations
432
+ # reuse the common raw distribution; diversity is still applied below per group
433
+ next_token_logits = (
434
+ outputs.logits[:, -1, :].repeat_interleave(group_size, dim=0)
435
+ if shared_prefill
436
+ else outputs.logits[batch_group_indices, -1, :]
437
+ ).to(dtype=torch.float32, device=input_ids.device)
438
 
439
  next_token_scores = nn.functional.log_softmax(
440
  next_token_logits, dim=-1
 
543
  # (that way the memory peak does not include outputs.logits)
544
  del outputs
545
 
546
+ # NOTE: we need to check if `model._reorder_cache` exists for special
547
+ # models like RAG, RecurrentGemma etc.
548
  if model_kwargs.get("past_key_values", None) is not None:
549
+ if shared_prefill:
550
+ # Expand the shared prefill cache to the normal beam-shaped
551
+ # state before applying the standard beam reordering.
552
+ model_kwargs["past_key_values"].batch_repeat_interleave(
553
+ num_beams
554
+ )
555
+
556
  if hasattr(model, "_reorder_cache"):
557
  model_kwargs["past_key_values"] = model._reorder_cache(
558
  model_kwargs["past_key_values"], reordering_indices