Adding Prompt lookup decoding (#27775)
* MVP * fix ci * more ci * remove redundant kwarg * added and wired up PromptLookupCandidateGenerator * rebased with main, working * removed print * style fixes * fix test * fixed tests * added test for prompt lookup decoding * fixed circleci * fixed test issue * Update src/transformers/generation/candidate_generator.py Co-authored-by: Joao Gante <joaofranciscocardosogante@gmail.com> * Update src/transformers/generation/candidate_generator.py Co-authored-by: Joao Gante <joaofranciscocardosogante@gmail.com> * Update src/transformers/generation/candidate_generator.py * Update src/transformers/generation/candidate_generator.py Co-authored-by: Arthur <48595927+ArthurZucker@users.noreply.github.com> --------- Co-authored-by: Joao Gante <joao@huggingface.co> Co-authored-by: Joao Gante <joaofranciscocardosogante@gmail.com> Co-authored-by: Arthur <48595927+ArthurZucker@users.noreply.github.com>
This commit is contained in:
@@ -226,6 +226,98 @@ class AssistedCandidateGenerator(CandidateGenerator):
|
|||||||
self.num_assistant_tokens = max(1.0, self.num_assistant_tokens - 1.0)
|
self.num_assistant_tokens = max(1.0, self.num_assistant_tokens - 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptLookupCandidateGenerator(CandidateGenerator):
|
||||||
|
"""
|
||||||
|
`CandidateGenerator` class to be used for prompt lookup generation. This class generates candidates by looking up
|
||||||
|
likely continuations in the provided prompt (input_ids) itself.
|
||||||
|
Read the following blog post for more information: https://github.com/apoorvumang/prompt-lookup-decoding
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_matching_ngram_size (`int`):
|
||||||
|
The maximum ngram size to be considered for matching in the prompt
|
||||||
|
num_output_tokens (`int`):
|
||||||
|
The number of tokens to be output as candidate tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_output_tokens: int = 10,
|
||||||
|
max_matching_ngram_size: int = 2,
|
||||||
|
):
|
||||||
|
self.num_output_tokens = num_output_tokens
|
||||||
|
self.max_matching_ngram_size = max_matching_ngram_size
|
||||||
|
|
||||||
|
if self.max_matching_ngram_size <= 0 or self.num_output_tokens <= 0:
|
||||||
|
raise ValueError("Invalid max_matching_ngram_size or num_output_tokens")
|
||||||
|
|
||||||
|
def get_candidates(self, input_ids: torch.LongTensor) -> Tuple[torch.LongTensor, Optional[torch.FloatTensor]]:
|
||||||
|
"""
|
||||||
|
Fetches the candidates to be tried for the current input.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
||||||
|
Indices of input sequence tokens in the vocabulary. [What are input IDs?](../glossary#input-ids)
|
||||||
|
|
||||||
|
Return:
|
||||||
|
`torch.LongTensor` of shape `(num_candidates, candidate_length)`: The candidate sequences to be tried.
|
||||||
|
"""
|
||||||
|
input_length = input_ids.size(1)
|
||||||
|
|
||||||
|
chosen_ids = None
|
||||||
|
match_found = False
|
||||||
|
for ngram_size in range(min(self.max_matching_ngram_size, input_length - 1), 0, -1):
|
||||||
|
# Create sliding windows of size ngram_size
|
||||||
|
windows = input_ids.unfold(dimension=1, size=ngram_size, step=1)
|
||||||
|
|
||||||
|
# Convert ngram to a tensor for comparison
|
||||||
|
ngram_tensor = input_ids[0, -ngram_size:]
|
||||||
|
|
||||||
|
# Find where the windows match the ngram
|
||||||
|
matches = (windows == ngram_tensor).all(dim=2)
|
||||||
|
|
||||||
|
# Get the indices of matches
|
||||||
|
match_indices = matches.nonzero(as_tuple=True)[1]
|
||||||
|
|
||||||
|
# Iterate through match indices to find a valid continuation
|
||||||
|
for idx in match_indices:
|
||||||
|
start_idx = idx + ngram_size
|
||||||
|
end_idx = start_idx + self.num_output_tokens
|
||||||
|
end_idx = min(end_idx, input_length)
|
||||||
|
|
||||||
|
if start_idx < end_idx:
|
||||||
|
chosen_ids = input_ids[0, start_idx:end_idx]
|
||||||
|
match_found = True
|
||||||
|
break
|
||||||
|
if match_found:
|
||||||
|
break
|
||||||
|
|
||||||
|
if chosen_ids is None or len(chosen_ids) == 0:
|
||||||
|
# Need to make a dummy tensor to avoid errors
|
||||||
|
chosen_ids = torch.zeros((1), dtype=torch.long, device=input_ids.device)
|
||||||
|
|
||||||
|
# Now need extend input_ids with chosen_ids
|
||||||
|
chosen_ids = chosen_ids.unsqueeze(0)
|
||||||
|
candidate_input_ids = torch.cat((input_ids, chosen_ids), dim=1)
|
||||||
|
# assisted_generation expects logits as well, but we don't have those here, so returning None
|
||||||
|
return candidate_input_ids, None
|
||||||
|
|
||||||
|
def update_candidate_strategy(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, num_matches: int):
|
||||||
|
"""
|
||||||
|
Updates the candidate generation strategy based on the outcomes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
|
||||||
|
Indices of input sequence tokens in the vocabulary. [What are input IDs?](../glossary#input-ids)
|
||||||
|
scores (`torch.FloatTensor` of shape `(batch_size, candidate_length, config.vocab_size)`):
|
||||||
|
Prediction scores of a language modeling head. These can be logits for each vocabulary when not using
|
||||||
|
beam search or log softmax for each vocabulary token when using beam search
|
||||||
|
num_matches (`int`):
|
||||||
|
The number of matches between the candidate sequences and the model predictions.
|
||||||
|
"""
|
||||||
|
# Currently does nothing
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def _crop_past_key_values(model, past_key_values, maximum_length):
|
def _crop_past_key_values(model, past_key_values, maximum_length):
|
||||||
"""Crops the past key values up to a certain maximum length."""
|
"""Crops the past key values up to a certain maximum length."""
|
||||||
new_past = []
|
new_past = []
|
||||||
|
|||||||
@@ -320,6 +320,9 @@ class GenerationConfig(PushToHubMixin):
|
|||||||
self.num_assistant_tokens = kwargs.pop("num_assistant_tokens", 5)
|
self.num_assistant_tokens = kwargs.pop("num_assistant_tokens", 5)
|
||||||
self.num_assistant_tokens_schedule = kwargs.pop("num_assistant_tokens_schedule", "heuristic")
|
self.num_assistant_tokens_schedule = kwargs.pop("num_assistant_tokens_schedule", "heuristic")
|
||||||
|
|
||||||
|
# Prompt lookup decoding
|
||||||
|
self.prompt_lookup_num_tokens = kwargs.pop("prompt_lookup_num_tokens", None)
|
||||||
|
|
||||||
# Wild card
|
# Wild card
|
||||||
self.generation_kwargs = kwargs.pop("generation_kwargs", {})
|
self.generation_kwargs = kwargs.pop("generation_kwargs", {})
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from .beam_search import BeamScorer, BeamSearchScorer, ConstrainedBeamSearchScor
|
|||||||
from .candidate_generator import (
|
from .candidate_generator import (
|
||||||
AssistedCandidateGenerator,
|
AssistedCandidateGenerator,
|
||||||
CandidateGenerator,
|
CandidateGenerator,
|
||||||
|
PromptLookupCandidateGenerator,
|
||||||
_crop_past_key_values,
|
_crop_past_key_values,
|
||||||
_prepare_attention_mask,
|
_prepare_attention_mask,
|
||||||
_prepare_token_type_ids,
|
_prepare_token_type_ids,
|
||||||
@@ -908,14 +909,19 @@ class GenerationMixin:
|
|||||||
"""
|
"""
|
||||||
Returns the candidate generator to be used in `assisted_generation`
|
Returns the candidate generator to be used in `assisted_generation`
|
||||||
"""
|
"""
|
||||||
candidate_generator = AssistedCandidateGenerator(
|
if generation_config.prompt_lookup_num_tokens is not None:
|
||||||
input_ids=input_ids,
|
candidate_generator = PromptLookupCandidateGenerator(
|
||||||
assistant_model=assistant_model,
|
num_output_tokens=generation_config.prompt_lookup_num_tokens,
|
||||||
generation_config=generation_config,
|
)
|
||||||
logits_processor=logits_processor,
|
else:
|
||||||
model_kwargs=model_kwargs,
|
candidate_generator = AssistedCandidateGenerator(
|
||||||
inputs_tensor=inputs_tensor,
|
input_ids=input_ids,
|
||||||
)
|
assistant_model=assistant_model,
|
||||||
|
generation_config=generation_config,
|
||||||
|
logits_processor=logits_processor,
|
||||||
|
model_kwargs=model_kwargs,
|
||||||
|
inputs_tensor=inputs_tensor,
|
||||||
|
)
|
||||||
return candidate_generator
|
return candidate_generator
|
||||||
|
|
||||||
def _get_logits_warper(
|
def _get_logits_warper(
|
||||||
@@ -995,7 +1001,7 @@ class GenerationMixin:
|
|||||||
generation_mode = GenerationMode.BEAM_SEARCH
|
generation_mode = GenerationMode.BEAM_SEARCH
|
||||||
|
|
||||||
# Assisted generation may extend some generation modes
|
# Assisted generation may extend some generation modes
|
||||||
if assistant_model is not None:
|
if assistant_model is not None or generation_config.prompt_lookup_num_tokens is not None:
|
||||||
if generation_mode in ("greedy_search", "sample"):
|
if generation_mode in ("greedy_search", "sample"):
|
||||||
generation_mode = GenerationMode.ASSISTED_GENERATION
|
generation_mode = GenerationMode.ASSISTED_GENERATION
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1569,6 +1569,66 @@ class GenerationTesterMixin:
|
|||||||
for output in (output_greedy, output_assisted):
|
for output in (output_greedy, output_assisted):
|
||||||
self._check_outputs(output, input_ids, model.config, use_cache=True)
|
self._check_outputs(output, input_ids, model.config, use_cache=True)
|
||||||
|
|
||||||
|
@is_flaky()
|
||||||
|
def test_prompt_lookup_decoding_matches_greedy_search(self):
|
||||||
|
# This test ensures that the prompt lookup generation does not introduce output changes over greedy search.
|
||||||
|
# This test is mostly a copy of test_assisted_decoding_matches_greedy_search
|
||||||
|
|
||||||
|
for model_class in self.all_generative_model_classes:
|
||||||
|
if any(model_name in model_class.__name__.lower() for model_name in ["fsmt", "reformer"]):
|
||||||
|
self.skipTest("Won't fix: old model with different cache format")
|
||||||
|
if any(
|
||||||
|
model_name in model_class.__name__.lower()
|
||||||
|
for model_name in [
|
||||||
|
"bigbirdpegasus",
|
||||||
|
"led",
|
||||||
|
"mega",
|
||||||
|
"speech2text",
|
||||||
|
"git",
|
||||||
|
"prophetnet",
|
||||||
|
"seamlessm4t",
|
||||||
|
"clvp",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
self.skipTest("May fix in the future: need model-specific fixes")
|
||||||
|
|
||||||
|
# enable cache
|
||||||
|
config, input_ids, attention_mask, _ = self._get_input_ids_and_config(batch_size=1)
|
||||||
|
|
||||||
|
# NOTE: assisted generation only works with cache on at the moment.
|
||||||
|
if not hasattr(config, "use_cache"):
|
||||||
|
self.skipTest("This model doesn't support caching")
|
||||||
|
|
||||||
|
config.use_cache = True
|
||||||
|
config.is_decoder = True
|
||||||
|
model = model_class(config).to(torch_device).eval()
|
||||||
|
# Sets assisted generation arguments such that:
|
||||||
|
# a) no EOS is generated, to ensure generation doesn't break early
|
||||||
|
# b) the prompt lookup tries to give the model 2 tokens, to ensure the input preparation of
|
||||||
|
# prompt lookup is correct
|
||||||
|
# c) there are at least two forward passes in the main model, to ensure the input preparation of
|
||||||
|
# the main model is correct
|
||||||
|
generation_kwargs = {
|
||||||
|
"eos_token_id": -1, # see a)
|
||||||
|
"max_new_tokens": 4, # see c)
|
||||||
|
"num_beams": 1,
|
||||||
|
"do_sample": False,
|
||||||
|
"output_scores": True,
|
||||||
|
"output_hidden_states": True,
|
||||||
|
"output_attentions": True,
|
||||||
|
"return_dict_in_generate": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
output_greedy = model.generate(input_ids, attention_mask=attention_mask, **generation_kwargs)
|
||||||
|
|
||||||
|
generation_kwargs.update({"prompt_lookup_num_tokens": 2}) # see b)
|
||||||
|
output_prompt_lookup = model.generate(input_ids, attention_mask=attention_mask, **generation_kwargs)
|
||||||
|
|
||||||
|
# The two outputs must match and their shape must be as expected
|
||||||
|
self.assertListEqual(output_greedy.sequences.tolist(), output_prompt_lookup.sequences.tolist())
|
||||||
|
for output in (output_greedy, output_prompt_lookup):
|
||||||
|
self._check_outputs(output, input_ids, model.config, use_cache=True)
|
||||||
|
|
||||||
def test_assisted_decoding_sample(self):
|
def test_assisted_decoding_sample(self):
|
||||||
# In this test we don't check assisted vs non-assisted output -- seeded assisted decoding with sample will not
|
# In this test we don't check assisted vs non-assisted output -- seeded assisted decoding with sample will not
|
||||||
# match sample for the same seed, as the forward pass does not return the exact same logits (due to matmul with
|
# match sample for the same seed, as the forward pass does not return the exact same logits (due to matmul with
|
||||||
|
|||||||
Reference in New Issue
Block a user