huggingface/transformers · #49269

🚨 Generation cleanup in pipelines

Rocketknight1 · merged Oct 8, 202612 files · 323 + / 153 −
src/transformers/models/dia/generation_dia.py3 + / 1 −
@@ -120,7 +120,9 @@ def _prepare_generation_config(             generation_config.temperature = 1.0         # We allow generation up to max length + max delay pattern         # (will revert back to max length after generation)-        generation_config.max_length += max(self.config.delay_pattern)+        # `max_length` can be unset in favor of `max_new_tokens`, which then defines it+        if generation_config.max_length is not None:+            generation_config.max_length += max(self.config.delay_pattern)          # Internal flag to indicate CFG that needs to prepare unconditioned input         self._uses_cfg = generation_config.guidance_scale is not None and generation_config.guidance_scale != 1
src/transformers/pipelines/any_to_any.py4 + / 5 −
@@ -308,8 +308,9 @@ def __call__(             return_full_text (`bool`, *optional*, defaults to `True`):                 If set to `False` only added text is returned, otherwise the full text is returned. Cannot be                 specified at the same time as `return_text`.-            clean_up_tokenization_spaces (`bool`, *optional*, defaults to `True`):-                Whether or not to clean up the potential extra spaces in the text output.+            clean_up_tokenization_spaces (`bool`, *optional*):+                Whether or not to clean up the potential extra spaces in the text output. Defaults to the tokenizer's+                `clean_up_tokenization_spaces` setting.             continue_final_message( `bool`, *optional*): This indicates that you want the model to continue the                 last message in the input chat rather than starting a new one, allowing you to "prefill" its response.                 By default this is `True` when the final message in the input chat has the `assistant` role and@@ -420,9 +421,7 @@ def _forward(self, model_inputs, generate_kwargs=None):         prompt_text = model_inputs.pop("text")         input_ids = model_inputs.get("input_ids", model_inputs.get("decoder_input_ids")) -        # User-defined `generation_config` passed to the pipeline call take precedence-        if "generation_config" not in generate_kwargs:-            generate_kwargs["generation_config"] = self.generation_config+        generate_kwargs = self._prepare_generate_kwargs(generate_kwargs)          generated_sequence = self.model.generate(**model_inputs, **generate_kwargs)         return {"generated_sequence": generated_sequence, "prompt_text": prompt_text, "input_ids": input_ids}
src/transformers/pipelines/automatic_speech_recognition.py17 + / 13 −
@@ -220,6 +220,13 @@ def __init__(             self.type = "ctc"          super().__init__(model, tokenizer, feature_extractor, device=device, **kwargs)+        if self.type == "tdt":+            # The pipeline defaults (e.g. beam search) target seq2seq models, transducers decode with their own+            # generation config+            self._generation_defaults = {}+        elif self.model.config.model_type == "kyutai_speech_to_text":+            # Kyutai's `generate()` sets the generation length from the audio length+            self._generation_defaults.pop("max_new_tokens", None)      def __call__(self, inputs: np.ndarray | bytes | str | dict, **kwargs: Any) -> list[dict[str, Any]]:         """@@ -338,8 +345,9 @@ def _sanitize_parameters(          # Parameter used in more than one place         # in some models like whisper, the generation config has a `return_timestamps` key-        if hasattr(self, "generation_config") and hasattr(self.generation_config, "return_timestamps"):-            return_timestamps = return_timestamps or self.generation_config.return_timestamps+        generation_config = getattr(self.model, "generation_config", None)+        if getattr(generation_config, "return_timestamps", False):+            return_timestamps = return_timestamps or generation_config.return_timestamps          if return_timestamps is not None:             # Check whether we have a valid setting for return_timestamps and throw an error before we perform a forward pass@@ -538,16 +546,14 @@ def _forward(self, model_inputs, return_timestamps=False, return_language=None,                 )              # custom processing for Whisper timestamps and word-level timestamps-            return_timestamps = return_timestamps or getattr(self.generation_config, "return_timestamps", False)+            return_timestamps = return_timestamps or getattr(self.model.generation_config, "return_timestamps", False)             if return_timestamps and self.type == "seq2seq_whisper":                 generate_kwargs["return_timestamps"] = bool(return_timestamps)                 if return_timestamps == "word":                     generate_kwargs["return_token_timestamps"] = True                     generate_kwargs["return_segments"] = True -            # User-defined `generation_config` passed to the pipeline call take precedence-            if "generation_config" not in generate_kwargs:-                generate_kwargs["generation_config"] = self.generation_config+            generate_kwargs = self._prepare_generate_kwargs(generate_kwargs)              main_input_name = self.model.main_input_name if hasattr(self.model, "main_input_name") else "inputs"             generate_kwargs = {@@ -588,7 +594,7 @@ def _forward(self, model_inputs, return_timestamps=False, return_language=None,                     if segments and segments[0]:                         result = segments[0][0]["result"]                         full_seq = result["sequences"] if isinstance(result, dict) else result-                        gen_config = generate_kwargs.get("generation_config", self.generation_config)+                        gen_config = generate_kwargs.get("generation_config", self.model.generation_config)                         if hasattr(gen_config, "lang_to_id"):                             lang_ids = set(gen_config.lang_to_id.values())                             for token_id in full_seq.tolist():@@ -618,12 +624,10 @@ def _forward(self, model_inputs, return_timestamps=False, return_language=None,                 else:                     out["stride"] = rescale_stride(stride, ratio)         elif self.type == "tdt":-            inputs = {-                self.model.main_input_name: model_inputs.pop(self.model.main_input_name),-            }-            if "attention_mask" in model_inputs:-                inputs["attention_mask"] = model_inputs.pop("attention_mask")-            outputs = self.model.generate(**inputs)+            inputs = {self.model.main_input_name: model_inputs.pop(self.model.main_input_name)}+            if attention_mask is not None:+                inputs["attention_mask"] = attention_mask+            outputs = self.model.generate(**inputs, **self._prepare_generate_kwargs(generate_kwargs))             out = {"tokens": outputs.sequences}         else:             raise ValueError(f"Unsupported model type {self.type}.")
src/transformers/pipelines/base.py66 + / 49 −
@@ -32,6 +32,7 @@ from ..distributed.utils import _is_torch_distributed_initialized from ..dynamic_module_utils import custom_object_save from ..feature_extraction_utils import PreTrainedFeatureExtractor+from ..generation import GenerationConfig from ..image_processing_utils import BaseImageProcessor from ..models.auto import AutoConfig, AutoTokenizer from ..processing_utils import ProcessorMixin@@ -784,6 +785,11 @@ class Pipeline(_ScikitCompat, PushToHubMixin):      # Pipelines that call `generate` have shared logic, e.g. preparing the generation config.     _pipeline_calls_generate = False+    # Defaults for the generation parameters that neither the user nor the model's generation config set+    _default_generation_config: GenerationConfig | None = None+    # Parameters of the pipeline that are named like generation parameters but aren't any (e.g. `top_k` as a number of+    # answers). They are handled like the other pipeline parameters, rather than passed to `generate()`.+    _non_generation_params: tuple[str, ...] = ()      default_input_names = None @@ -877,61 +883,22 @@ def __init__(             self.model.to(self.device)          # If it's a generation pipeline and the model can generate:-        # 1 - create a local generation config. This is done to avoid side-effects on the model as we apply local-        # tweaks to the generation config.+        # 1 - set aside the generation parameters, which apply to every call (see `_prepare_generate_kwargs`).         # 2 - load the assistant model if it is passed.         if self._pipeline_calls_generate and self.model.can_generate():             self.assistant_model, self.assistant_tokenizer = load_assistant_model(                 self.model, kwargs.pop("assistant_model", None), kwargs.pop("assistant_tokenizer", None)             )             self.prefix = self.model.config.prefix if hasattr(self.model.config, "prefix") else None-            # Priority order: kwargs > user_generation_config > model.generation_config > default_pipeline_generation_config-            default_pipeline_generation_config = getattr(self, "_default_generation_config", None)-            user_generation_config = kwargs.pop("generation_config", None)-            if hasattr(self.model, "_prepare_generation_config"):-                base_config = user_generation_config or copy.deepcopy(self.model.generation_config)-                if default_pipeline_generation_config is not None:-                    base_config.update(-                        **default_pipeline_generation_config.to_dict(),-                        defaults_only=True,-                        allow_custom_entries=True,-                    )-                prepared_generation_config, kwargs = self.model._prepare_generation_config(-                    generation_config=base_config, **kwargs-                )-                self.generation_config = prepared_generation_config-                # if the `max_new_tokens` is set to the pipeline default, but `max_length` is set to a non-default-                # value: let's honor `max_length`. E.g. we want Whisper's default `max_length=448` take precedence-                # over over the pipeline's length default.-                if (-                    default_pipeline_generation_config is not None-                    and default_pipeline_generation_config.max_new_tokens is not None  # there's a pipeline default-                    and self.generation_config.max_new_tokens == default_pipeline_generation_config.max_new_tokens-                    and self.generation_config.max_length is not None-                    and self.generation_config.max_length != 20  # global default-                ):-                    self.generation_config.max_new_tokens = None-            else:-                # TODO (joao): no PT model should reach this line. However, some audio models with complex-                # inheritance patterns do. Streamline those models such that this line is no longer needed.-                # In those models, the default generation config is not (yet) used.-                self.generation_config = copy.deepcopy(self.model.generation_config)-            # Update the generation config with task specific params if they exist.-            # NOTE: 1. `prefix` is pipeline-specific and doesn't exist in the generation config.-            #       2. `task_specific_params` is a legacy feature and should be removed in a future version.-            task_specific_params = getattr(self.model.config, "task_specific_params", None)-            if task_specific_params is not None and task in task_specific_params:-                this_task_params = task_specific_params.get(task)-                if "prefix" in this_task_params:-                    self.prefix = this_task_params.pop("prefix")-                self.generation_config.update(**this_task_params)-            # If the tokenizer has a pad token but the model doesn't, set it so that `generate` is aware of it.-            if (-                self.tokenizer is not None-                and self.tokenizer.pad_token_id is not None-                and self.generation_config.pad_token_id is None-            ):-                self.generation_config.pad_token_id = self.tokenizer.pad_token_id+            generation_keys = {key for key in vars(GenerationConfig()) if not key.startswith("_")}+            generation_keys.difference_update(self._non_generation_params)+            generation_keys.add("generation_config")+            self._generate_kwargs = {key: kwargs.pop(key) for key in generation_keys & kwargs.keys()}+            defaults = self._default_generation_config+            self._generation_defaults = {} if defaults is None else defaults.to_diff_dict()+            self._generation_defaults.pop("transformers_version", None)+            if self.tokenizer is not None and self.tokenizer.pad_token_id is not None:+                self._generation_defaults["pad_token_id"] = self.tokenizer.pad_token_id          self.call_count = 0         self._batch_size = kwargs.pop("batch_size", None)@@ -1069,6 +1036,56 @@ def device_placement(self):         else:             yield +    def _prepare_generate_kwargs(self, generate_kwargs: dict, **overrides) -> dict:+        """+        Returns the `generate()` kwargs for this call. They hold the user's generation parameters as passed, so that+        `generate()` handles them as in a direct call: the call's parameters, on top of those passed at init unless the+        call passes a `generation_config`, which then replaces them. The pipeline adds its `overrides`, and its defaults+        for the parameters that neither the user nor the model's generation config set (the sampling ones only when+        sampling).++        What the pipeline adds goes in a copy of the user's `generation_config` if there is one, as `generate()`+        deprecates passing one together with generation parameters.+        """+        # Pipeline defaults for these only apply when sampling, otherwise `generate()` warns about them+        _SAMPLING_PARAMS = {+            "temperature",+            "top_k",+            "top_p",+            "min_p",+            "top_h",+            "typical_p",+            "epsilon_cutoff",+            "eta_cutoff",+        }+        if "generation_config" not in generate_kwargs:+            generate_kwargs = {**self._generate_kwargs, **generate_kwargs}+        set_params = self._get_set_generation_params(generate_kwargs)+        # A default `max_new_tokens` would also override a set `max_length`, e.g. Whisper's 448+        added_params = {+            key: value+            for key, value in self._generation_defaults.items()+            if key not in set_params and not (key == "max_new_tokens" and "max_length" in set_params)+        }+        if not {**set_params, **added_params}.get("do_sample"):+            added_params = {key: value for key, value in added_params.items() if key not in _SAMPLING_PARAMS}+        added_params.update(overrides)+        if (user_config := generate_kwargs.get("generation_config")) is None:+            return {**generate_kwargs, **added_params}+        user_config = copy.deepcopy(user_config)+        user_config.update(**added_params)+        return {**generate_kwargs, "generation_config": user_config}++    def _get_set_generation_params(self, generate_kwargs: dict) -> dict:+        """+        The generation parameters that a `generate()` call with `generate_kwargs` doesn't take from its global+        defaults: the kwargs, on top of their `generation_config` if any, on top of the model's generation config.+        """+        params = self.model.generation_config.to_diff_dict()+        if (generation_config := generate_kwargs.get("generation_config")) is not None:+            params.update(generation_config.to_diff_dict())+        return {**params, **generate_kwargs}+     def ensure_tensor_on_device(self, **inputs):         """         Ensure PyTorch tensors are on the specified device.
src/transformers/pipelines/document_question_answering.py3 + / 3 −
@@ -238,6 +238,8 @@ class DocumentQuestionAnsweringPipeline(ChunkPipeline):     """      _pipeline_calls_generate = True+    # The number of answers, not the sampling parameter+    _non_generation_params = ("top_k",)     _load_processor = False     _load_image_processor = None     _load_feature_extractor = None@@ -564,9 +566,7 @@ def _forward(self, model_inputs, **generate_kwargs):         is_last = model_inputs.pop("is_last", False)          if self.model_type == ModelType.VisionEncoderDecoder:-            # User-defined `generation_config` passed to the pipeline call take precedence-            if "generation_config" not in generate_kwargs:-                generate_kwargs["generation_config"] = self.generation_config+            generate_kwargs = self._prepare_generate_kwargs(generate_kwargs)              model_outputs = self.model.generate(**model_inputs, **generate_kwargs)         else:
src/transformers/pipelines/image_text_to_text.py4 + / 6 −
@@ -249,8 +249,9 @@ def __call__(             return_full_text (`bool`, *optional*, defaults to `True`):                 If set to `False` only added text is returned, otherwise the full text is returned. Cannot be                 specified at the same time as `return_text`.-            clean_up_tokenization_spaces (`bool`, *optional*, defaults to `True`):-                Whether or not to clean up the potential extra spaces in the text output.+            clean_up_tokenization_spaces (`bool`, *optional*):+                Whether or not to clean up the potential extra spaces in the text output. Defaults to the tokenizer's+                `clean_up_tokenization_spaces` setting.             continue_final_message( `bool`, *optional*): This indicates that you want the model to continue the                 last message in the input chat rather than starting a new one, allowing you to "prefill" its response.                 By default this is `True` when the final message in the input chat has the `assistant` role and@@ -388,10 +389,7 @@ def _forward(self, model_inputs, generate_kwargs=None):             model_inputs["input_ids"] if "input_ids" in model_inputs else model_inputs["decoder_input_ids"]         )  # for decoder-only models -        # User-defined `generation_config` passed to the pipeline call take precedence-        if "generation_config" not in generate_kwargs:-            generate_kwargs["generation_config"] = self.generation_config-        generate_kwargs["return_dict_in_generate"] = False+        generate_kwargs = self._prepare_generate_kwargs(generate_kwargs, return_dict_in_generate=False)          generated_sequence = self.model.generate(**model_inputs, **generate_kwargs) 
src/transformers/pipelines/table_question_answering.py1 + / 3 −
@@ -331,9 +331,7 @@ def _forward(self, model_inputs, sequential=False, **generate_kwargs):             else:                 outputs = self.batch_inference(**model_inputs)         else:-            # User-defined `generation_config` passed to the pipeline call take precedence-            if "generation_config" not in generate_kwargs:-                generate_kwargs["generation_config"] = self.generation_config+            generate_kwargs = self._prepare_generate_kwargs(generate_kwargs)              outputs = self.model.generate(**model_inputs, **generate_kwargs)         model_outputs = {"model_inputs": model_inputs, "table": table, "outputs": outputs}
src/transformers/pipelines/text_generation.py20 + / 29 −
@@ -191,8 +191,10 @@ def _sanitize_parameters(          # forward kwargs         if stop_sequence is not None:-            stop_sequence_ids = self.tokenizer.encode(stop_sequence, add_special_tokens=False)-            generate_kwargs["eos_token_id"] = stop_sequence_ids+            if isinstance(stop_sequence, str):+                stop_sequence = [stop_sequence]+            generate_kwargs["stop_strings"] = stop_sequence+            generate_kwargs["tokenizer"] = self.tokenizer         forward_params = generate_kwargs         if self.assistant_model is not None:             forward_params["assistant_model"] = self.assistant_model@@ -264,14 +266,17 @@ def __call__(self, text_inputs, **kwargs):             return_full_text (`bool`, *optional*, defaults to `True`):                 If set to `False` only added text is returned, otherwise the full text is returned. Cannot be                 specified at the same time as `return_text`.-            clean_up_tokenization_spaces (`bool`, *optional*, defaults to `True`):-                Whether or not to clean up the potential extra spaces in the text output.+            clean_up_tokenization_spaces (`bool`, *optional*):+                Whether or not to clean up the potential extra spaces in the text output. Defaults to the tokenizer's+                `clean_up_tokenization_spaces` setting.             continue_final_message( `bool`, *optional*): This indicates that you want the model to continue the                 last message in the input chat rather than starting a new one, allowing you to "prefill" its response.                 By default this is `True` when the final message in the input chat has the `assistant` role and                 `False` otherwise, but you can manually override that behaviour by setting this flag.             prefix (`str`, *optional*):                 Prefix added to prompt.+            stop_sequence (`str` or `list[str]`, *optional*):+                One or more strings that stop generation once generated. The stop string is included in the output.             handle_long_generation (`str`, *optional*):                 By default, this pipelines does not handle long generation (ones that exceed in one form or the other                 the model maximum length). There is no perfect way to address this (more info@@ -346,10 +351,10 @@ def preprocess(          if handle_long_generation == "hole":             cur_len = inputs["input_ids"].shape[-1]-            if "max_new_tokens" in generate_kwargs:-                new_tokens = generate_kwargs["max_new_tokens"]-            else:-                new_tokens = generate_kwargs.get("max_length", self.generation_config.max_length) - cur_len+            generation_params = self._get_set_generation_params(self._prepare_generate_kwargs(generate_kwargs))+            new_tokens = generation_params.get("max_new_tokens")+            if new_tokens is None:+                new_tokens = generation_params["max_length"] - cur_len                 if new_tokens < 0:                     raise ValueError("We cannot infer how many new tokens are expected")             if cur_len + new_tokens > self.tokenizer.model_max_length:@@ -378,27 +383,13 @@ def _forward(self, model_inputs, **generate_kwargs):             in_b = input_ids.shape[0]         prompt_text = model_inputs.pop("prompt_text") -        # If there is a prefix, we may need to adjust the generation length. Do so without permanently modifying-        # generate_kwargs, as some of the parameterization may come from the initialization of the pipeline.         prefix_length = generate_kwargs.pop("prefix_length", 0)-        if prefix_length > 0:-            has_max_new_tokens = "max_new_tokens" in generate_kwargs or (-                "generation_config" in generate_kwargs-                and generate_kwargs["generation_config"].max_new_tokens is not None-            )-            if not has_max_new_tokens:-                generate_kwargs["max_length"] = generate_kwargs.get("max_length") or self.generation_config.max_length-                generate_kwargs["max_length"] += prefix_length-            has_min_new_tokens = "min_new_tokens" in generate_kwargs or (-                "generation_config" in generate_kwargs-                and generate_kwargs["generation_config"].min_new_tokens is not None-            )-            if not has_min_new_tokens and "min_length" in generate_kwargs:-                generate_kwargs["min_length"] += prefix_length--        # User-defined `generation_config` passed to the pipeline call take precedence-        if "generation_config" not in generate_kwargs:-            generate_kwargs["generation_config"] = self.generation_config+        generate_kwargs = self._prepare_generate_kwargs(generate_kwargs)+        if prefix_length:  # a total length (as opposed to a number of new tokens) has to include the prefix+            generation_params = self._get_set_generation_params(generate_kwargs)+            for length, new_tokens in (("max_length", "max_new_tokens"), ("min_length", "min_new_tokens")):+                if length in generation_params and new_tokens not in generation_params:+                    generate_kwargs[length] = generation_params[length] + prefix_length          output = self.model.generate(input_ids=input_ids, attention_mask=attention_mask, **generate_kwargs) @@ -433,7 +424,7 @@ def postprocess(         self,         model_outputs,         return_type=ReturnType.FULL_TEXT,-        clean_up_tokenization_spaces=True,+        clean_up_tokenization_spaces=None,         continue_final_message=None,         skip_special_tokens=None,     ):
src/transformers/pipelines/text_to_audio.py20 + / 31 −
@@ -10,7 +10,7 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and-# limitations under the License.from typing import List, Union+# limitations under the License.  import copy from typing import Any, TypedDict, overload@@ -107,12 +107,18 @@ class TextToAudioPipeline(Pipeline):     _default_generation_config = GenerationConfig(max_new_tokens=256)      def __init__(self, *args, vocoder=None, sampling_rate=None, noise_scheduler=None, **kwargs):-        # Some models (e.g., VibeVoice) require noise_scheduler during initialization because `_prepare_generation_config` is called in super().__init__-        if noise_scheduler is not None:-            kwargs["noise_scheduler"] = noise_scheduler         self.noise_scheduler = noise_scheduler          super().__init__(*args, **kwargs)+        # These models pass generation parameters to each of their sub-models, which have their own generation configs:+        # the pipeline doesn't add any+        self._passes_generation_params_to_submodels = self.model.config.model_type in (+            "bark",+            "seamless_m4t",+            "seamless_m4t_v2",+        )+        if self._passes_generation_params_to_submodels:+            self._generation_defaults = {}          self.vocoder = None         if self.model.__class__ in MODEL_FOR_TEXT_TO_SPECTROGRAM_MAPPING.values():@@ -133,7 +139,7 @@ def __init__(self, *args, vocoder=None, sampling_rate=None, noise_scheduler=None         if self.sampling_rate is None:             # get sampling_rate from config and generation config -            config = self.model.config+            config = copy.deepcopy(self.model.config)             gen_config = self.model.__dict__.get("generation_config", None)             if gen_config is not None:                 config.update({k: v for k, v in gen_config.to_dict().items() if v is not None})@@ -159,8 +165,8 @@ def preprocess(self, text, **kwargs):             # bark Tokenizer is called with BarkProcessor which uses those kwargs             # Check if generation_config has semantic_config (BarkGenerationConfig) or use default             max_length = 256-            if hasattr(self.generation_config, "semantic_config"):-                max_length = getattr(self.generation_config.semantic_config, "max_input_semantic_length", 256)+            if hasattr(self.model.generation_config, "semantic_config"):+                max_length = getattr(self.model.generation_config.semantic_config, "max_input_semantic_length", 256)             new_kwargs = {                 "max_length": max_length,                 "add_special_tokens": False,@@ -211,29 +217,9 @@ def _forward(self, model_inputs, **kwargs):             # we expect some kwargs to be additional tensors which need to be on the right device             generate_kwargs = self._ensure_tensor_on_device(generate_kwargs, device=self.device) -            # User-defined `generation_config` passed to the pipeline call take precedence-            if "generation_config" in generate_kwargs:-                generation_config = generate_kwargs.pop("generation_config")-            else:-                generation_config = copy.deepcopy(self.generation_config)--            # ensure dict output to facilitate postprocessing-            generation_config.return_dict_in_generate = True--            # priotiize max_new_tokens to avoid max_length warning-            if generation_config.max_new_tokens is not None:-                generation_config.max_length = None--            # Merge generate_kwargs into generation_config where possible to avoid the-            # deprecation warning about passing generation_config alongside generation params-            model_specific_kwargs = {}-            for k, v in generate_kwargs.items():-                if hasattr(generation_config, k):-                    setattr(generation_config, k, v)-                else:-                    model_specific_kwargs[k] = v-            forward_params["generation_config"] = generation_config-            forward_params.update(model_specific_kwargs)+            # `generate_kwargs` take precedence over `forward_params`. Dict output facilitates postprocessing.+            overrides = {} if self._passes_generation_params_to_submodels else {"return_dict_in_generate": True}+            forward_params = self._prepare_generate_kwargs({**forward_params, **generate_kwargs}, **overrides)              if self.model.config.model_type in ["csm"]:                 # NOTE (ebezzam): CSM does not have the audio tokenizer in the processor therefore `output_audio=True`@@ -289,7 +275,8 @@ def __call__(self, text_inputs, **forward_params):                 The dictionary of ad-hoc parametrization of `generate_config` to be used for the generation call. For a                 complete overview of generate, check the [following                 guide](https://huggingface.co/docs/transformers/en/main_classes/text_generation). `generate_kwargs` are-                only passed to the underlying model if the latter is a generative model.+                only passed to the underlying model if the latter is a generative model, and take precedence over+                `forward_params`.          Return:             `AudioOutput` or a list of `AudioOutput`, which is a `TypedDict` with two keys:@@ -330,6 +317,8 @@ def postprocess(self, audio):         if isinstance(audio, dict):             if "audio" in audio:                 audio = audio["audio"]+            elif "waveform" in audio:  # e.g. SeamlessM4T with `return_intermediate_token_ids=True`+                audio = audio["waveform"]             else:                 needs_decoding = True                 audio = audio["sequences"]
tests/pipelines/test_pipelines_automatic_speech_recognition.py51 + / 0 −
@@ -13,6 +13,7 @@ # limitations under the License.  import unittest+from unittest.mock import patch  import numpy as np import pytest@@ -27,6 +28,9 @@     AutoModelForSpeechSeq2Seq,     AutoProcessor,     AutoTokenizer,+    ParakeetEncoderConfig,+    ParakeetForTDT,+    ParakeetTDTConfig,     Speech2TextForConditionalGeneration,     Wav2Vec2ForCTC,     WhisperForConditionalGeneration,@@ -40,6 +44,7 @@     is_pipeline_test,     is_torch_available,     nested_simplify,+    require_librosa,     require_pyctcdecode,     require_torch,     require_torch_accelerator,@@ -567,6 +572,52 @@ def test_return_timestamps_in_preprocess_longform(self):         )         # fmt: on +    @require_torch+    def test_whisper_generation_parameters_passed_as_kwargs(self):+        # Whisper's `generate()` takes `temperature` as an argument and ignores the one in a `generation_config`+        speech_recognizer = pipeline(task="automatic-speech-recognition", model="openai/whisper-tiny")+        model = speech_recognizer.model+        waveform = np.tile(np.arange(1000, dtype=np.float32), 34)+        with patch.object(model, "generate", wraps=model.generate) as generate:+            _ = speech_recognizer(waveform, generate_kwargs={"temperature": 0.8, "max_new_tokens": 2})+        generate_kwargs = generate.call_args.kwargs+        self.assertNotIn("generation_config", generate_kwargs)+        self.assertEqual(generate_kwargs["temperature"], 0.8)++    @require_torch+    @require_librosa+    def test_transducer_generation_parameters_passed_at_creation(self):+        # Transducers get the generation parameters passed at creation, but not the pipeline's (seq2seq) defaults+        tokenizer = AutoTokenizer.from_pretrained("nvidia/parakeet-tdt-0.6b-v3")+        feature_extractor = AutoFeatureExtractor.from_pretrained("nvidia/parakeet-tdt-0.6b-v3")+        encoder = ParakeetEncoderConfig(+            hidden_size=16,+            num_hidden_layers=1,+            num_attention_heads=2,+            intermediate_size=32,+            subsampling_conv_channels=8,+            num_mel_bins=feature_extractor.feature_size,+        )+        config = ParakeetTDTConfig(+            vocab_size=len(tokenizer) + 1,+            decoder_hidden_size=16,+            encoder_config=encoder.to_dict(),+            blank_token_id=len(tokenizer),+        )+        model = ParakeetForTDT(config)+        model.generation_config.decoder_start_token_id = config.blank_token_id+        speech_recognizer = pipeline(+            task="automatic-speech-recognition",+            model=model,+            tokenizer=tokenizer,+            feature_extractor=feature_extractor,+            max_new_tokens=3,+        )+        with patch.object(model, "generate", wraps=model.generate) as generate:+            _ = speech_recognizer(np.zeros(16000, dtype=np.float32))+        self.assertEqual(generate.call_args.kwargs["max_new_tokens"], 3)+        self.assertNotIn("num_beams", generate.call_args.kwargs)+     @require_torch     def test_return_timestamps_in_init(self):         # segment-level timestamps are accepted
tests/pipelines/test_pipelines_document_question_answering.py38 + / 0 −
@@ -13,10 +13,16 @@ # limitations under the License.  import unittest+from unittest.mock import patch  from transformers import (     MODEL_FOR_DOCUMENT_QUESTION_ANSWERING_MAPPING,     AutoTokenizer,+    BartConfig,+    DonutImageProcessor,+    DonutSwinConfig,+    VisionEncoderDecoderConfig,+    VisionEncoderDecoderModel,     is_torch_available,     is_vision_available, )@@ -200,6 +206,38 @@ def test_small_model_pt_donut(self):             nested_simplify(outputs, decimals=4), [{"score": 0.8799, "answer": "2"}, {"score": 0.296, "answer": "1"}]         ) +    @require_torch+    @require_vision+    def test_top_k_is_not_a_generation_parameter(self):+        # With a generative (Donut) model, `top_k` is still the number of answers, not the sampling parameter+        tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/tiny-random-bart")+        encoder = DonutSwinConfig(image_size=32, patch_size=4, embed_dim=8, depths=[1], num_heads=[1], window_size=2)+        decoder = BartConfig(+            vocab_size=len(tokenizer),+            d_model=8,+            encoder_layers=1,+            decoder_layers=1,+            encoder_attention_heads=1,+            decoder_attention_heads=1,+            encoder_ffn_dim=8,+            decoder_ffn_dim=8,+        )+        model = VisionEncoderDecoderModel(VisionEncoderDecoderConfig.from_encoder_decoder_configs(encoder, decoder))+        model.generation_config.decoder_start_token_id = tokenizer.bos_token_id+        dqa_pipeline = pipeline(+            "document-question-answering",+            model=model,+            tokenizer=tokenizer,+            image_processor=DonutImageProcessor(size={"height": 32, "width": 32}),+            top_k=2,+            max_new_tokens=3,+        )+        with patch.object(model, "generate", wraps=model.generate) as generate:+            _ = dqa_pipeline(image=Image.new("RGB", (32, 32)), question="What is this?")+        self.assertNotIn("top_k", generate.call_args.kwargs)+        self.assertEqual(generate.call_args.kwargs["max_new_tokens"], 3)+        self.assertEqual(dqa_pipeline._postprocess_params["top_k"], 2)+     @slow     @require_torch     @require_detectron2
tests/pipelines/test_pipelines_text_generation.py96 + / 13 −
@@ -373,6 +373,12 @@ def test_stop_sequence_stopping_criteria(self):         output = text_generator(prompt, stop_sequence=" fe")         self.assertEqual(output, [{"generated_text": "Hello I believe in fe"}]) +        # Multi-token stop sequences only stop generation once the whole sequence is generated+        output = text_generator(prompt, stop_sequence=" fe banana")+        self.assertEqual(output, [{"generated_text": "Hello I believe in fe fe fe fe fe"}])+        output = text_generator(prompt, stop_sequence=[" banana", " fe fe"])+        self.assertEqual(output, [{"generated_text": "Hello I believe in fe fe"}])+     def run_pipeline_test(self, text_generator, _):         model = text_generator.model         tokenizer = text_generator.tokenizer@@ -562,6 +568,77 @@ def test_pipeline_length_setting_warning(self):             _ = text_generator(prompt, max_length=10, max_new_tokens=None)         self.assertNotIn(logger_msg, cl.out) +    def test_pipeline_no_spurious_generation_warnings(self):+        # The pipeline must not warn about generation parameters it sets itself (generation config defaults or+        # parameters passed alongside its generation config)+        text_generator = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2")+        logger = logging.get_logger("transformers.generation.utils")+        for kwargs in ({}, {"max_new_tokens": 1}, {"max_length": 10, "min_new_tokens": 1}):+            logger.warning_once.cache_clear()+            with CaptureLogger(logger) as cl:+                _ = text_generator("Hello world", **kwargs)+            self.assertEqual(cl.out, "", f"Unexpected warning with {kwargs}")++    def test_pipeline_length_kwarg_takes_precedence(self):+        # `max_length` passed at call time must override the pipeline's default `max_new_tokens`+        text_generator = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2")+        input_length = len(text_generator.tokenizer("Hello world").input_ids)+        out = text_generator("Hello world", max_length=input_length + 2, do_sample=False, return_tensors=True)+        self.assertEqual(len(out[0]["generated_token_ids"]), input_length + 2)++    def test_pipeline_generation_parameters_passed_as_kwargs(self):+        # `generate()` receives the generation parameters as kwargs: those passed at call time on top of those passed+        # at creation, plus the pipeline's defaults for the parameters they don't set+        text_generator = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2", max_new_tokens=3)+        model = text_generator.model+        with patch.object(model, "generate", wraps=model.generate) as generate:+            _ = text_generator("Hello world", do_sample=False)+        generate_kwargs = generate.call_args.kwargs+        self.assertNotIn("generation_config", generate_kwargs)+        self.assertEqual(generate_kwargs["max_new_tokens"], 3)+        self.assertFalse(generate_kwargs["do_sample"])+        # The pipeline's default `temperature` only applies to sampling+        self.assertNotIn("temperature", generate_kwargs)++    def test_pipeline_call_generation_config_replaces_creation_parameters(self):+        # A `generation_config` passed at call time replaces the generation parameters passed at creation. Like in+        # `generate()`, passing one together with generation parameters is deprecated (the parameters take precedence).+        text_generator = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2", max_new_tokens=3)+        model = text_generator.model+        logger = logging.get_logger("transformers.generation.utils")+        for kwargs, deprecated in (({}, False), ({"max_new_tokens": 2}, True)):+            logger.warning_once.cache_clear()+            with CaptureLogger(logger) as cl, patch.object(model, "generate", wraps=model.generate) as generate:+                _ = text_generator("Hello world", generation_config=GenerationConfig(do_sample=False), **kwargs)+            self.assertEqual("generation-related arguments=({'max_new_tokens'})" in cl.out, deprecated)+            self.assertEqual(generate.call_args.kwargs.get("max_new_tokens"), kwargs.get("max_new_tokens"))+            # The pipeline's default length goes in the `generation_config`, unless a length is passed+            self.assertEqual(generate.call_args.kwargs["generation_config"].max_new_tokens, None if kwargs else 256)++    def test_pipeline_generation_config_with_custom_entries(self):+        # Entries that the model's generation config doesn't have (e.g. Whisper's `no_timestamps_token_id`, when fixing+        # an incomplete config) can only reach `generate()` inside a `generation_config`, at call time or at creation+        generation_config = GenerationConfig(do_sample=False, custom_entry=1)+        call_time = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2", max_new_tokens=3)+        creation_time = pipeline(+            "text-generation",+            model=call_time.model,+            tokenizer=call_time.tokenizer,+            generation_config=generation_config,+        )+        logger = logging.get_logger("transformers.generation.utils")+        for text_generator, kwargs in ((call_time, {"generation_config": generation_config}), (creation_time, {})):+            logger.warning_once.cache_clear()+            model = text_generator.model+            with CaptureLogger(logger) as cl, patch.object(model, "generate", wraps=model.generate) as generate:+                _ = text_generator("Hello world", **kwargs)+            self.assertEqual(cl.out, "")+            passed_generation_config = generate.call_args.kwargs["generation_config"]+            self.assertEqual(passed_generation_config.custom_entry, 1)+            self.assertFalse(passed_generation_config.do_sample)+            # The pipeline's default length is added+            self.assertEqual(passed_generation_config.max_new_tokens, 256)+     def test_return_dict_in_generate(self):         text_generator = pipeline("text-generation", model="hf-internal-testing/tiny-random-gpt2", max_new_tokens=2)         out = text_generator(@@ -640,30 +717,36 @@ def test_pipeline_respects_model_generation_config(self):         model = AutoModelForCausalLM.from_pretrained(model_id)         tokenizer = AutoTokenizer.from_pretrained(model_id) +        def generation_param(pipe, key):+            # The value that `generate()` uses+            return pipe._get_set_generation_params(pipe._prepare_generate_kwargs({})).get(key)+         # 1. Modify model.generation_config directly (model_config > pipeline_default)         model.generation_config.max_new_tokens = 500-        model.generation_config.temperature = 0.7+        model.generation_config.temperature = 0.5          # Instantiate pipeline without extra generation kwargs         pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)          # Assert user settings on model.generation_config were respected over pipeline defaults-        self.assertEqual(pipe.generation_config.max_new_tokens, 500)-        self.assertEqual(pipe.generation_config.temperature, 0.7)+        self.assertEqual(generation_param(pipe, "max_new_tokens"), 500)+        self.assertEqual(generation_param(pipe, "temperature"), 0.5)          # 2. Explicit generation_config object overrides model_config and pipeline_default         custom_gc = GenerationConfig(max_new_tokens=250, temperature=0.9)         pipe_gc = pipeline("text-generation", model=model, tokenizer=tokenizer, generation_config=custom_gc)-        self.assertEqual(pipe_gc.generation_config.max_new_tokens, 250)-        self.assertEqual(pipe_gc.generation_config.temperature, 0.9)+        self.assertEqual(generation_param(pipe_gc, "max_new_tokens"), 250)+        self.assertEqual(generation_param(pipe_gc, "temperature"), 0.9) -        # 3. Explicit kwargs override user_generation_config, model_config, and pipeline_default+        # 3. Explicit kwargs override user_generation_config, model_config, and pipeline_default. Like in `generate()`,+        # passing both is deprecated         pipe_kwargs = pipeline(-            "text-generation",-            model=model,-            tokenizer=tokenizer,-            generation_config=custom_gc,-            max_new_tokens=100,+            "text-generation", model=model, tokenizer=tokenizer, generation_config=custom_gc, max_new_tokens=2         )-        self.assertEqual(pipe_kwargs.generation_config.max_new_tokens, 100)-        self.assertEqual(pipe_kwargs.generation_config.temperature, 0.9)+        self.assertEqual(generation_param(pipe_kwargs, "max_new_tokens"), 2)+        self.assertEqual(generation_param(pipe_kwargs, "temperature"), 0.9)+        logger = logging.get_logger("transformers.generation.utils")+        logger.warning_once.cache_clear()+        with CaptureLogger(logger) as cl:+            _ = pipe_kwargs("Hello world")+        self.assertIn("Passing `generation_config` together with generation-related arguments", cl.out)