base.py 47.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# coding=utf-8
# Copyright 2018 The HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# 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.
15
import collections
16
import csv
17
import importlib
18
19
20
21
import json
import os
import pickle
import sys
22
import types
23
import warnings
24
from abc import ABC, abstractmethod
25
from collections import UserDict
26
27
from contextlib import contextmanager
from os.path import abspath, exists
28
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
29

30
31
from packaging import version

Sylvain Gugger's avatar
Sylvain Gugger committed
32
from ..dynamic_module_utils import custom_object_save
33
from ..feature_extraction_utils import PreTrainedFeatureExtractor
34
from ..modelcard import ModelCard
35
from ..models.auto.configuration_auto import AutoConfig
36
from ..tokenization_utils import PreTrainedTokenizer
37
from ..utils import ModelOutput, add_end_docstrings, is_tf_available, is_torch_available, logging
38
39


40
41
GenericTensor = Union[List["GenericTensor"], "torch.Tensor", "tf.Tensor"]

42
43
44
45
46
47
48
if is_tf_available():
    import tensorflow as tf

    from ..models.auto.modeling_tf_auto import TFAutoModel

if is_torch_available():
    import torch
49
    from torch.utils.data import DataLoader, Dataset
50
51

    from ..models.auto.modeling_auto import AutoModel
52
53
54

    # Re-export for backward compatibility
    from .pt_utils import KeyDataset
55
56
57
else:
    Dataset = None
    KeyDataset = None
58
59
60
61
62
63
64
65
66

if TYPE_CHECKING:
    from ..modeling_tf_utils import TFPreTrainedModel
    from ..modeling_utils import PreTrainedModel


logger = logging.get_logger(__name__)


67
def no_collate_fn(items):
68
69
70
71
72
    if len(items) != 1:
        raise ValueError("This collate_fn is meant to be used with batch_size=1")
    return items[0]


73
74
75
76
77
78
def _pad(items, key, padding_value, padding_side):
    batch_size = len(items)
    if isinstance(items[0][key], torch.Tensor):
        # Others include `attention_mask` etc...
        shape = items[0][key].shape
        dim = len(shape)
79
        if key == "pixel_values":
80
81
82
83
            # This is probable image so padding shouldn't be necessary
            # B, C, H, W
            return torch.cat([item[key] for item in items], dim=0)
        max_length = max(item[key].shape[1] for item in items)
84
        min_length = min(item[key].shape[1] for item in items)
85
86
87
        dtype = items[0][key].dtype

        if dim == 2:
88
89
90
91
            if max_length == min_length:
                # Bypass for `ImageGPT` which doesn't provide a padding value, yet
                # we can consistently pad since the size should be matching
                return torch.cat([item[key] for item in items], dim=0)
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
            tensor = torch.zeros((batch_size, max_length), dtype=dtype) + padding_value
        elif dim == 3:
            tensor = torch.zeros((batch_size, max_length, shape[-1]), dtype=dtype) + padding_value

        for i, item in enumerate(items):
            if dim == 2:
                if padding_side == "left":
                    tensor[i, -len(item[key][0]) :] = item[key][0].clone()
                else:
                    tensor[i, : len(item[key][0])] = item[key][0].clone()
            elif dim == 3:
                if padding_side == "left":
                    tensor[i, -len(item[key][0]) :, :] = item[key][0].clone()
                else:
                    tensor[i, : len(item[key][0]), :] = item[key][0].clone()
        return tensor
    else:
        return [item[key] for item in items]


def pad_collate_fn(tokenizer, feature_extractor):
113
114
115
116
    # Tokenizer
    t_padding_side = None
    # Feature extractor
    f_padding_side = None
117
118
119
120
121
122
123
124
125
    if tokenizer is None and feature_extractor is None:
        raise ValueError("Pipeline without tokenizer or feature_extractor cannot do batching")
    if tokenizer is not None:
        if tokenizer.pad_token_id is None:
            raise ValueError(
                "Pipeline with tokenizer without pad_token cannot do batching. You can try to set it with "
                "`pipe.tokenizer.pad_token_id = model.config.eos_token_id`."
            )
        else:
126
127
            t_padding_value = tokenizer.pad_token_id
            t_padding_side = tokenizer.padding_side
128
129
    if feature_extractor is not None:
        # Feature extractor can be images, where no padding is expected
130
131
132
133
134
135
136
137
138
139
140
141
        f_padding_value = getattr(feature_extractor, "padding_value", None)
        f_padding_side = getattr(feature_extractor, "padding_side", None)

    if t_padding_side is not None and f_padding_side is not None and t_padding_side != f_padding_side:
        raise ValueError(
            f"The feature extractor, and tokenizer don't agree on padding side {t_padding_side} != {f_padding_side}"
        )
    padding_side = "right"
    if t_padding_side is not None:
        padding_side = t_padding_side
    if f_padding_side is not None:
        padding_side = f_padding_side
142
143
144
145
146
147

    def inner(items):
        keys = set(items[0].keys())
        for item in items:
            if set(item.keys()) != keys:
                raise ValueError(
Sylvain Gugger's avatar
Sylvain Gugger committed
148
149
                    f"The elements of the batch contain different keys. Cannot batch them ({set(item.keys())} !="
                    f" {keys})"
150
151
                )
        # input_values, input_pixels, input_ids, ...
152
153
        padded = {}
        for key in keys:
154
            if key in {"input_ids"}:
155
156
157
158
159
                # ImageGPT uses a feature extractor
                if feature_extractor is not None:
                    _padding_value = f_padding_value
                else:
                    _padding_value = t_padding_value
160
161
            elif key in {"input_values", "pixel_values", "input_features"}:
                _padding_value = f_padding_value
162
            elif key in {"p_mask", "special_tokens_mask"}:
163
                _padding_value = 1
164
165
            elif key in {"attention_mask", "token_type_ids"}:
                _padding_value = 0
166
            else:
167
                # This is likely another random key maybe even user provided
168
169
                _padding_value = 0
            padded[key] = _pad(items, key, _padding_value, padding_side)
170
171
172
173
174
        return padded

    return inner


175
176
177
178
179
180
def infer_framework_load_model(
    model,
    config: AutoConfig,
    model_classes: Optional[Dict[str, Tuple[type]]] = None,
    task: Optional[str] = None,
    framework: Optional[str] = None,
181
    **model_kwargs,
182
):
183
    """
184
    Select framework (TensorFlow or PyTorch) to use from the `model` passed. Returns a tuple (framework, model).
185

Sylvain Gugger's avatar
Sylvain Gugger committed
186
187
188
    If `model` is instantiated, this function will just infer the framework from the model class. Otherwise `model` is
    actually a checkpoint name and this method will try to instantiate it using `model_classes`. Since we don't want to
    instantiate the model twice, this model is returned for use by the pipeline.
189

190
    If both frameworks are installed and available for `model`, PyTorch is selected.
191
192

    Args:
193
        model (`str`, [`PreTrainedModel`] or [`TFPreTrainedModel`]):
Sylvain Gugger's avatar
Sylvain Gugger committed
194
            The model to infer the framework from. If `str`, a checkpoint name. The model to infer the framewrok from.
195
        config ([`AutoConfig`]):
196
            The config associated with the model to help using the correct class
197
        model_classes (dictionary `str` to `type`, *optional*):
198
            A mapping framework to class.
199
        task (`str`):
200
201
            The task defining which pipeline will be returned.
        model_kwargs:
Sylvain Gugger's avatar
Sylvain Gugger committed
202
203
            Additional dictionary of keyword arguments passed along to the model's `from_pretrained(...,
            **model_kwargs)` function.
204
205

    Returns:
206
        `Tuple`: A tuple framework, model.
207
208
209
210
211
212
213
214
    """
    if not is_tf_available() and not is_torch_available():
        raise RuntimeError(
            "At least one of TensorFlow 2.0 or PyTorch should be installed. "
            "To install TensorFlow 2.0, read the instructions at https://www.tensorflow.org/install/ "
            "To install PyTorch, read the instructions at https://pytorch.org/."
        )
    if isinstance(model, str):
215
        model_kwargs["_from_pipeline"] = task
216
217
218
219
220
221
222
223
224
225
226
227
        class_tuple = ()
        look_pt = is_torch_available() and framework in {"pt", None}
        look_tf = is_tf_available() and framework in {"tf", None}
        if model_classes:
            if look_pt:
                class_tuple = class_tuple + model_classes.get("pt", (AutoModel,))
            if look_tf:
                class_tuple = class_tuple + model_classes.get("tf", (TFAutoModel,))
        if config.architectures:
            classes = []
            for architecture in config.architectures:
                transformers_module = importlib.import_module("transformers")
228
                if look_pt:
229
230
231
                    _class = getattr(transformers_module, architecture, None)
                    if _class is not None:
                        classes.append(_class)
232
                if look_tf:
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
                    _class = getattr(transformers_module, f"TF{architecture}", None)
                    if _class is not None:
                        classes.append(_class)
            class_tuple = class_tuple + tuple(classes)

        if len(class_tuple) == 0:
            raise ValueError(f"Pipeline cannot infer suitable model classes from {model}")

        for model_class in class_tuple:
            kwargs = model_kwargs.copy()
            if framework == "pt" and model.endswith(".h5"):
                kwargs["from_tf"] = True
                logger.warning(
                    "Model might be a TensorFlow model (ending with `.h5`) but TensorFlow is not available. "
                    "Trying to load the model with PyTorch."
                )
            elif framework == "tf" and model.endswith(".bin"):
                kwargs["from_pt"] = True
                logger.warning(
                    "Model might be a PyTorch model (ending with `.bin`) but PyTorch is not available. "
                    "Trying to load the model with Tensorflow."
                )

256
            try:
257
                model = model_class.from_pretrained(model, **kwargs)
258
259
                if hasattr(model, "eval"):
                    model = model.eval()
260
261
262
263
264
265
266
                # Stop loading on the first successful load.
                break
            except (OSError, ValueError):
                continue

        if isinstance(model, str):
            raise ValueError(f"Could not load model {model} with any of the following classes: {class_tuple}.")
267
268
269
270
271

    framework = "tf" if model.__class__.__name__.startswith("TF") else "pt"
    return framework, model


272
273
274
275
276
def infer_framework_from_model(
    model,
    model_classes: Optional[Dict[str, Tuple[type]]] = None,
    task: Optional[str] = None,
    framework: Optional[str] = None,
277
    **model_kwargs,
278
279
):
    """
280
    Select framework (TensorFlow or PyTorch) to use from the `model` passed. Returns a tuple (framework, model).
281

Sylvain Gugger's avatar
Sylvain Gugger committed
282
283
284
    If `model` is instantiated, this function will just infer the framework from the model class. Otherwise `model` is
    actually a checkpoint name and this method will try to instantiate it using `model_classes`. Since we don't want to
    instantiate the model twice, this model is returned for use by the pipeline.
285

286
    If both frameworks are installed and available for `model`, PyTorch is selected.
287
288

    Args:
289
        model (`str`, [`PreTrainedModel`] or [`TFPreTrainedModel`]):
Sylvain Gugger's avatar
Sylvain Gugger committed
290
            The model to infer the framework from. If `str`, a checkpoint name. The model to infer the framewrok from.
291
        model_classes (dictionary `str` to `type`, *optional*):
292
            A mapping framework to class.
293
        task (`str`):
294
295
            The task defining which pipeline will be returned.
        model_kwargs:
Sylvain Gugger's avatar
Sylvain Gugger committed
296
297
            Additional dictionary of keyword arguments passed along to the model's `from_pretrained(...,
            **model_kwargs)` function.
298
299

    Returns:
300
        `Tuple`: A tuple framework, model.
301
302
303
304
305
306
307
308
309
310
    """
    if isinstance(model, str):
        config = AutoConfig.from_pretrained(model, _from_pipeline=task, **model_kwargs)
    else:
        config = model.config
    return infer_framework_load_model(
        model, config, model_classes=model_classes, _from_pipeline=task, task=task, framework=framework, **model_kwargs
    )


311
312
313
314
315
def get_framework(model, revision: Optional[str] = None):
    """
    Select framework (TensorFlow or PyTorch) to use.

    Args:
316
        model (`str`, [`PreTrainedModel`] or [`TFPreTrainedModel`]):
317
318
319
            If both frameworks are installed, picks the one corresponding to the model passed (either a model class or
            the model name). If no specific model is provided, defaults to using PyTorch.
    """
320
321
322
323
    warnings.warn(
        "`get_framework` is deprecated and will be removed in v5, use `infer_framework_from_model` instead.",
        FutureWarning,
    )
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
    if not is_tf_available() and not is_torch_available():
        raise RuntimeError(
            "At least one of TensorFlow 2.0 or PyTorch should be installed. "
            "To install TensorFlow 2.0, read the instructions at https://www.tensorflow.org/install/ "
            "To install PyTorch, read the instructions at https://pytorch.org/."
        )
    if isinstance(model, str):
        if is_torch_available() and not is_tf_available():
            model = AutoModel.from_pretrained(model, revision=revision)
        elif is_tf_available() and not is_torch_available():
            model = TFAutoModel.from_pretrained(model, revision=revision)
        else:
            try:
                model = AutoModel.from_pretrained(model, revision=revision)
            except OSError:
                model = TFAutoModel.from_pretrained(model, revision=revision)

    framework = "tf" if model.__class__.__name__.startswith("TF") else "pt"
    return framework


345
346
347
def get_default_model_and_revision(
    targeted_task: Dict, framework: Optional[str], task_options: Optional[Any]
) -> Union[str, Tuple[str, str]]:
348
349
350
351
    """
    Select a default model to use for a given task. Defaults to pytorch if ambiguous.

    Args:
352
        targeted_task (`Dict` ):
353
354
           Dictionary representing the given task, that should contain default models

355
        framework (`str`, None)
356
357
           "pt", "tf" or None, representing a specific framework if it was specified, or None if we don't know yet.

358
        task_options (`Any`, None)
359
360
361
362
363
           Any further value required by the task to get fully specified, for instance (SRC, TGT) languages for
           translation task.

    Returns

364
        `str` The model string representing the default model for this pipeline
365
366
367
368
369
370
371
372
373
    """
    if is_torch_available() and not is_tf_available():
        framework = "pt"
    elif is_tf_available() and not is_torch_available():
        framework = "tf"

    defaults = targeted_task["default"]
    if task_options:
        if task_options not in defaults:
374
            raise ValueError(f"The task does not provide any default models for options {task_options}")
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
        default_models = defaults[task_options]["model"]
    elif "model" in defaults:
        default_models = targeted_task["default"]["model"]
    else:
        # XXX This error message needs to be updated to be more generic if more tasks are going to become
        # parametrized
        raise ValueError('The task defaults can\'t be correctly selected. You probably meant "translation_XX_to_YY"')

    if framework is None:
        framework = "pt"

    return default_models[framework]


class PipelineException(Exception):
    """
391
    Raised by a [`Pipeline`] when handling __call__.
392
393

    Args:
394
395
396
        task (`str`): The task of the pipeline.
        model (`str`): The model used by the pipeline.
        reason (`str`): The error message to display.
397
398
399
400
401
402
403
404
405
406
407
    """

    def __init__(self, task: str, model: str, reason: str):
        super().__init__(reason)

        self.task = task
        self.model = model


class ArgumentHandler(ABC):
    """
408
    Base interface for handling arguments for each [`~pipelines.Pipeline`].
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
    """

    @abstractmethod
    def __call__(self, *args, **kwargs):
        raise NotImplementedError()


class PipelineDataFormat:
    """
    Base class for all the pipeline supported data format both for reading and writing. Supported data formats
    currently includes:

    - JSON
    - CSV
    - stdin/stdout (pipe)

Sylvain Gugger's avatar
Sylvain Gugger committed
425
426
    `PipelineDataFormat` also includes some utilities to work with multi-columns like mapping from datasets columns to
    pipelines keyword arguments through the `dataset_kwarg_1=dataset_column_1` format.
427
428

    Args:
429
430
431
432
433
        output_path (`str`, *optional*): Where to save the outgoing data.
        input_path (`str`, *optional*): Where to look for the input data.
        column (`str`, *optional*): The column to read.
        overwrite (`bool`, *optional*, defaults to `False`):
            Whether or not to overwrite the `output_path`.
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
    """

    SUPPORTED_FORMATS = ["json", "csv", "pipe"]

    def __init__(
        self,
        output_path: Optional[str],
        input_path: Optional[str],
        column: Optional[str],
        overwrite: bool = False,
    ):
        self.output_path = output_path
        self.input_path = input_path
        self.column = column.split(",") if column is not None else [""]
        self.is_multi_columns = len(self.column) > 1

        if self.is_multi_columns:
            self.column = [tuple(c.split("=")) if "=" in c else (c, c) for c in self.column]

        if output_path is not None and not overwrite:
            if exists(abspath(self.output_path)):
455
                raise OSError(f"{self.output_path} already exists on disk")
456
457
458

        if input_path is not None:
            if not exists(abspath(self.input_path)):
459
                raise OSError(f"{self.input_path} doesnt exist on disk")
460
461
462
463
464
465
466
467

    @abstractmethod
    def __iter__(self):
        raise NotImplementedError()

    @abstractmethod
    def save(self, data: Union[dict, List[dict]]):
        """
Sylvain Gugger's avatar
Sylvain Gugger committed
468
        Save the provided data object with the representation for the current [`~pipelines.PipelineDataFormat`].
469
470

        Args:
471
            data (`dict` or list of `dict`): The data to store.
472
473
474
475
476
477
478
479
        """
        raise NotImplementedError()

    def save_binary(self, data: Union[dict, List[dict]]) -> str:
        """
        Save the provided data object as a pickle-formatted binary data on the disk.

        Args:
480
            data (`dict` or list of `dict`): The data to store.
481
482

        Returns:
483
            `str`: Path where the data has been saved.
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
        """
        path, _ = os.path.splitext(self.output_path)
        binary_path = os.path.extsep.join((path, "pickle"))

        with open(binary_path, "wb+") as f_output:
            pickle.dump(data, f_output)

        return binary_path

    @staticmethod
    def from_str(
        format: str,
        output_path: Optional[str],
        input_path: Optional[str],
        column: Optional[str],
        overwrite=False,
    ) -> "PipelineDataFormat":
        """
Sylvain Gugger's avatar
Sylvain Gugger committed
502
        Creates an instance of the right subclass of [`~pipelines.PipelineDataFormat`] depending on `format`.
503
504

        Args:
505
506
507
            format: (`str`):
                The format of the desired pipeline. Acceptable values are `"json"`, `"csv"` or `"pipe"`.
            output_path (`str`, *optional*):
508
                Where to save the outgoing data.
509
            input_path (`str`, *optional*):
510
                Where to look for the input data.
511
            column (`str`, *optional*):
512
                The column to read.
513
514
            overwrite (`bool`, *optional*, defaults to `False`):
                Whether or not to overwrite the `output_path`.
515
516

        Returns:
517
            [`~pipelines.PipelineDataFormat`]: The proper data format.
518
519
520
521
522
523
524
525
        """
        if format == "json":
            return JsonPipelineDataFormat(output_path, input_path, column, overwrite=overwrite)
        elif format == "csv":
            return CsvPipelineDataFormat(output_path, input_path, column, overwrite=overwrite)
        elif format == "pipe":
            return PipedPipelineDataFormat(output_path, input_path, column, overwrite=overwrite)
        else:
526
            raise KeyError(f"Unknown reader {format} (Available reader are json/csv/pipe)")
527
528
529
530
531
532
533


class CsvPipelineDataFormat(PipelineDataFormat):
    """
    Support for pipelines using CSV data format.

    Args:
534
535
536
537
538
        output_path (`str`, *optional*): Where to save the outgoing data.
        input_path (`str`, *optional*): Where to look for the input data.
        column (`str`, *optional*): The column to read.
        overwrite (`bool`, *optional*, defaults to `False`):
            Whether or not to overwrite the `output_path`.
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
    """

    def __init__(
        self,
        output_path: Optional[str],
        input_path: Optional[str],
        column: Optional[str],
        overwrite=False,
    ):
        super().__init__(output_path, input_path, column, overwrite=overwrite)

    def __iter__(self):
        with open(self.input_path, "r") as f:
            reader = csv.DictReader(f)
            for row in reader:
                if self.is_multi_columns:
                    yield {k: row[c] for k, c in self.column}
                else:
                    yield row[self.column[0]]

    def save(self, data: List[dict]):
        """
Sylvain Gugger's avatar
Sylvain Gugger committed
561
        Save the provided data object with the representation for the current [`~pipelines.PipelineDataFormat`].
562
563

        Args:
564
            data (`List[dict]`): The data to store.
565
566
567
568
569
570
571
572
573
574
575
576
577
        """
        with open(self.output_path, "w") as f:
            if len(data) > 0:
                writer = csv.DictWriter(f, list(data[0].keys()))
                writer.writeheader()
                writer.writerows(data)


class JsonPipelineDataFormat(PipelineDataFormat):
    """
    Support for pipelines using JSON file format.

    Args:
578
579
580
581
582
        output_path (`str`, *optional*): Where to save the outgoing data.
        input_path (`str`, *optional*): Where to look for the input data.
        column (`str`, *optional*): The column to read.
        overwrite (`bool`, *optional*, defaults to `False`):
            Whether or not to overwrite the `output_path`.
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
    """

    def __init__(
        self,
        output_path: Optional[str],
        input_path: Optional[str],
        column: Optional[str],
        overwrite=False,
    ):
        super().__init__(output_path, input_path, column, overwrite=overwrite)

        with open(input_path, "r") as f:
            self._entries = json.load(f)

    def __iter__(self):
        for entry in self._entries:
            if self.is_multi_columns:
                yield {k: entry[c] for k, c in self.column}
            else:
                yield entry[self.column[0]]

    def save(self, data: dict):
        """
        Save the provided data object in a json file.

        Args:
609
            data (`dict`): The data to store.
610
611
612
613
614
615
616
617
618
619
620
621
        """
        with open(self.output_path, "w") as f:
            json.dump(data, f)


class PipedPipelineDataFormat(PipelineDataFormat):
    """
    Read data from piped input to the python process. For multi columns data, columns should separated by \t

    If columns are provided, then the output will be a dictionary with {column_x: value_x}

    Args:
622
623
624
625
626
        output_path (`str`, *optional*): Where to save the outgoing data.
        input_path (`str`, *optional*): Where to look for the input data.
        column (`str`, *optional*): The column to read.
        overwrite (`bool`, *optional*, defaults to `False`):
            Whether or not to overwrite the `output_path`.
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
    """

    def __iter__(self):
        for line in sys.stdin:
            # Split for multi-columns
            if "\t" in line:
                line = line.split("\t")
                if self.column:
                    # Dictionary to map arguments
                    yield {kwargs: l for (kwargs, _), l in zip(self.column, line)}
                else:
                    yield tuple(line)

            # No dictionary to map arguments
            else:
                yield line

    def save(self, data: dict):
        """
        Print the data.

        Args:
649
            data (`dict`): The data to store.
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
        """
        print(data)

    def save_binary(self, data: Union[dict, List[dict]]) -> str:
        if self.output_path is None:
            raise KeyError(
                "When using piped input on pipeline outputting large object requires an output file path. "
                "Please provide such output path through --output argument."
            )

        return super().save_binary(data)


class _ScikitCompat(ABC):
    """
    Interface layer for the Scikit and Keras compatibility.
    """

    @abstractmethod
    def transform(self, X):
        raise NotImplementedError()

    @abstractmethod
    def predict(self, X):
        raise NotImplementedError()


PIPELINE_INIT_ARGS = r"""
    Arguments:
679
        model ([`PreTrainedModel`] or [`TFPreTrainedModel`]):
680
            The model that will be used by the pipeline to make predictions. This needs to be a model inheriting from
Sylvain Gugger's avatar
Sylvain Gugger committed
681
            [`PreTrainedModel`] for PyTorch and [`TFPreTrainedModel`] for TensorFlow.
682
        tokenizer ([`PreTrainedTokenizer`]):
683
            The tokenizer that will be used by the pipeline to encode data for the model. This object inherits from
684
685
            [`PreTrainedTokenizer`].
        modelcard (`str` or [`ModelCard`], *optional*):
686
            Model card attributed to the model for this pipeline.
687
        framework (`str`, *optional*):
Sylvain Gugger's avatar
Sylvain Gugger committed
688
689
            The framework to use, either `"pt"` for PyTorch or `"tf"` for TensorFlow. The specified framework must be
            installed.
690
691

            If no framework is specified, will default to the one currently installed. If no framework is specified and
Sylvain Gugger's avatar
Sylvain Gugger committed
692
693
            both frameworks are installed, will default to the framework of the `model`, or to PyTorch if no model is
            provided.
694
        task (`str`, defaults to `""`):
695
            A task-identifier for the pipeline.
696
697
        num_workers (`int`, *optional*, defaults to 8):
            When the pipeline will use *DataLoader* (when passing a dataset, on GPU for a Pytorch model), the number of
698
            workers to be used.
699
700
        batch_size (`int`, *optional*, defaults to 1):
            When the pipeline will use *DataLoader* (when passing a dataset, on GPU for a Pytorch model), the size of
Sylvain Gugger's avatar
Sylvain Gugger committed
701
702
            the batch to use, for inference this is not always beneficial, please read [Batching with
            pipelines](https://huggingface.co/transformers/main_classes/pipelines.html#pipeline-batching) .
703
        args_parser ([`~pipelines.ArgumentHandler`], *optional*):
704
            Reference to the object in charge of parsing supplied pipeline parameters.
705
        device (`int`, *optional*, defaults to -1):
706
            Device ordinal for CPU/GPU supports. Setting this to -1 will leverage CPU, a positive will run the model on
707
            the associated CUDA device id. You can pass native `torch.device` or a `str` too.
708
        binary_output (`bool`, *optional*, defaults to `False`):
709
710
711
            Flag indicating if the output the pipeline should happen in a binary format (i.e., pickle) or as raw text.
"""

712
if is_torch_available():
713
714
715
716
717
718
    from transformers.pipelines.pt_utils import (
        PipelineChunkIterator,
        PipelineDataset,
        PipelineIterator,
        PipelinePackIterator,
    )
719

720
721
722
723
724
725
726
727
728
729
730
731
732
733

@add_end_docstrings(PIPELINE_INIT_ARGS)
class Pipeline(_ScikitCompat):
    """
    The Pipeline class is the class from which all pipelines inherit. Refer to this class for methods shared across
    different pipelines.

    Base class implementing pipelined operations. Pipeline workflow is defined as a sequence of the following
    operations:

        Input -> Tokenization -> Model Inference -> Post-Processing (task dependent) -> Output

    Pipeline supports running on CPU or GPU through the device argument (see below).

Sylvain Gugger's avatar
Sylvain Gugger committed
734
735
736
    Some pipeline, like for instance [`FeatureExtractionPipeline`] (`'feature-extraction'`) output large tensor object
    as nested-lists. In order to avoid dumping such large structure as textual data we provide the `binary_output`
    constructor argument. If set to `True`, the output will be stored in the pickle format.
737
738
739
740
741
742
743
    """

    default_input_names = None

    def __init__(
        self,
        model: Union["PreTrainedModel", "TFPreTrainedModel"],
744
745
        tokenizer: Optional[PreTrainedTokenizer] = None,
        feature_extractor: Optional[PreTrainedFeatureExtractor] = None,
746
747
748
749
        modelcard: Optional[ModelCard] = None,
        framework: Optional[str] = None,
        task: str = "",
        args_parser: ArgumentHandler = None,
750
        device: Union[int, str, "torch.device"] = -1,
751
        torch_dtype: Optional[Union[str, "torch.dtype"]] = None,
752
        binary_output: bool = False,
753
        **kwargs,
754
755
    ):
        if framework is None:
756
            framework, model = infer_framework_load_model(model, config=model.config)
757
758
759
760

        self.task = task
        self.model = model
        self.tokenizer = tokenizer
761
        self.feature_extractor = feature_extractor
762
763
        self.modelcard = modelcard
        self.framework = framework
764
765
766
767
768
769
770
771
        if is_torch_available() and self.framework == "pt":
            if isinstance(device, torch.device):
                self.device = device
            elif isinstance(device, str):
                self.device = torch.device(device)
            elif device < 0:
                self.device = torch.device("cpu")
            else:
Michael Wyatt's avatar
Michael Wyatt committed
772
                self.device = torch.device(f"cuda:{device}")
773
        else:
774
            self.device = device
775
        self.torch_dtype = torch_dtype
776
777
778
        self.binary_output = binary_output

        # Special handling
779
        if self.framework == "pt" and self.device.type != "cpu":
780
781
782
783
784
785
786
            self.model = self.model.to(self.device)

        # Update config with task specific parameters
        task_specific_params = self.model.config.task_specific_params
        if task_specific_params is not None and task in task_specific_params:
            self.model.config.update(task_specific_params.get(task))

787
        self.call_count = 0
788
789
        self._batch_size = kwargs.pop("batch_size", None)
        self._num_workers = kwargs.pop("num_workers", None)
790
791
        self._preprocess_params, self._forward_params, self._postprocess_params = self._sanitize_parameters(**kwargs)

792
793
794
795
796
    def save_pretrained(self, save_directory: str):
        """
        Save the pipeline's model and tokenizer.

        Args:
797
            save_directory (`str`):
798
799
800
                A path to the directory where to saved. It will be created if it doesn't exist.
        """
        if os.path.isfile(save_directory):
801
            logger.error(f"Provided path ({save_directory}) should be a directory, not a file")
802
803
804
            return
        os.makedirs(save_directory, exist_ok=True)

Sylvain Gugger's avatar
Sylvain Gugger committed
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
        if hasattr(self, "_registered_impl"):
            # Add info to the config
            pipeline_info = self._registered_impl.copy()
            custom_pipelines = {}
            for task, info in pipeline_info.items():
                if info["impl"] != self.__class__:
                    continue

                info = info.copy()
                module_name = info["impl"].__module__
                last_module = module_name.split(".")[-1]
                # Change classes into their names/full names
                info["impl"] = f"{last_module}.{info['impl'].__name__}"
                info["pt"] = tuple(c.__name__ for c in info["pt"])
                info["tf"] = tuple(c.__name__ for c in info["tf"])

                custom_pipelines[task] = info
            self.model.config.custom_pipelines = custom_pipelines
            # Save the pipeline custom code
            custom_object_save(self, save_directory)

826
        self.model.save_pretrained(save_directory)
827
828
829
830
831
832
833

        if self.tokenizer is not None:
            self.tokenizer.save_pretrained(save_directory)

        if self.feature_extractor is not None:
            self.feature_extractor.save_pretrained(save_directory)

834
835
836
837
838
839
840
        if self.modelcard is not None:
            self.modelcard.save_pretrained(save_directory)

    def transform(self, X):
        """
        Scikit / Keras interface to transformers' pipelines. This method will forward to __call__().
        """
841
        return self(X)
842
843
844
845
846

    def predict(self, X):
        """
        Scikit / Keras interface to transformers' pipelines. This method will forward to __call__().
        """
847
        return self(X)
848
849
850
851
852
853
854
855
856

    @contextmanager
    def device_placement(self):
        """
        Context Manager allowing tensor allocation on the user-specified device in framework agnostic way.

        Returns:
            Context manager

857
        Examples:
858

859
860
861
862
863
864
865
        ```python
        # Explicitly ask for tensor allocation on CUDA device :0
        pipe = pipeline(..., device=0)
        with pipe.device_placement():
            # Every framework specific tensor allocation will be done on the request device
            output = pipe(...)
        ```"""
866
        if self.framework == "tf":
867
            with tf.device("/CPU:0" if self.device == -1 else f"/device:GPU:{self.device}"):
868
869
870
871
872
873
874
875
876
877
878
879
                yield
        else:
            if self.device.type == "cuda":
                torch.cuda.set_device(self.device)

            yield

    def ensure_tensor_on_device(self, **inputs):
        """
        Ensure PyTorch tensors are on the specified device.

        Args:
Sylvain Gugger's avatar
Sylvain Gugger committed
880
881
            inputs (keyword arguments that should be `torch.Tensor`, the rest is ignored):
                The tensors to place on `self.device`.
882
            Recursive on lists **only**.
883
884

        Return:
885
            `Dict[str, torch.Tensor]`: The same as `inputs` but on the proper device.
886
        """
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
        return self._ensure_tensor_on_device(inputs, self.device)

    def _ensure_tensor_on_device(self, inputs, device):
        if isinstance(inputs, ModelOutput):
            return ModelOutput(
                {name: self._ensure_tensor_on_device(tensor, device) for name, tensor in inputs.items()}
            )
        elif isinstance(inputs, dict):
            return {name: self._ensure_tensor_on_device(tensor, device) for name, tensor in inputs.items()}
        elif isinstance(inputs, UserDict):
            return UserDict({name: self._ensure_tensor_on_device(tensor, device) for name, tensor in inputs.items()})
        elif isinstance(inputs, list):
            return [self._ensure_tensor_on_device(item, device) for item in inputs]
        elif isinstance(inputs, tuple):
            return tuple([self._ensure_tensor_on_device(item, device) for item in inputs])
        elif isinstance(inputs, torch.Tensor):
903
904
            if device == torch.device("cpu") and inputs.dtype in {torch.float16, torch.bfloat16}:
                inputs = inputs.float()
905
            return inputs.to(device)
906
907
        else:
            return inputs
908
909
910
911
912
913

    def check_model_type(self, supported_models: Union[List[str], dict]):
        """
        Check if the model class is in supported by the pipeline.

        Args:
914
            supported_models (`List[str]` or `dict`):
915
916
917
                The list of models supported by the pipeline, or a dictionary with model class values.
        """
        if not isinstance(supported_models, list):  # Create from a model mapping
918
919
920
921
922
923
924
925
            supported_models_names = []
            for config, model in supported_models.items():
                # Mapping can now contain tuples of models for the same configuration.
                if isinstance(model, tuple):
                    supported_models_names.extend([_model.__name__ for _model in model])
                else:
                    supported_models_names.append(model.__name__)
            supported_models = supported_models_names
926
        if self.model.__class__.__name__ not in supported_models:
927
            logger.error(
Sylvain Gugger's avatar
Sylvain Gugger committed
928
929
                f"The model '{self.model.__class__.__name__}' is not supported for {self.task}. Supported models are"
                f" {supported_models}."
930
931
            )

932
933
    @abstractmethod
    def _sanitize_parameters(self, **pipeline_parameters):
934
        """
935
936
937
938
        _sanitize_parameters will be called with any excessive named arguments from either `__init__` or `__call__`
        methods. It should return 3 dictionnaries of the resolved parameters used by the various `preprocess`,
        `forward` and `postprocess` methods. Do not fill dictionnaries if the caller didn't specify a kwargs. This
        let's you keep defaults in function signatures, which is more "natural".
939

940
941
        It is not meant to be called directly, it will be automatically called and the final parameters resolved by
        `__init__` and `__call__`
942
        """
943
        raise NotImplementedError("_sanitize_parameters not implemented")
944

945
946
947
    @abstractmethod
    def preprocess(self, input_: Any, **preprocess_parameters: Dict) -> Dict[str, GenericTensor]:
        """
Kaito Sugimoto's avatar
Kaito Sugimoto committed
948
        Preprocess will take the `input_` of a specific pipeline and return a dictionary of everything necessary for
949
950
951
        `_forward` to run properly. It should contain at least one tensor, but might have arbitrary other items.
        """
        raise NotImplementedError("preprocess not implemented")
952

953
954
955
    @abstractmethod
    def _forward(self, input_tensors: Dict[str, GenericTensor], **forward_parameters: Dict) -> ModelOutput:
        """
Kaito Sugimoto's avatar
Kaito Sugimoto committed
956
        _forward will receive the prepared dictionary from `preprocess` and run it on the model. This method might
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
        involve the GPU or the CPU and should be agnostic to it. Isolating this function is the reason for `preprocess`
        and `postprocess` to exist, so that the hot path, this method generally can run as fast as possible.

        It is not meant to be called directly, `forward` is preferred. It is basically the same but contains additional
        code surrounding `_forward` making sure tensors and models are on the same device, disabling the training part
        of the code (leading to faster inference).
        """
        raise NotImplementedError("_forward not implemented")

    @abstractmethod
    def postprocess(self, model_outputs: ModelOutput, **postprocess_parameters: Dict) -> Any:
        """
        Postprocess will receive the raw outputs of the `_forward` method, generally tensors, and reformat them into
        something more friendly. Generally it will output a list or a dict or results (containing just strings and
        numbers).
972
        """
973
974
        raise NotImplementedError("postprocess not implemented")

975
976
    def get_inference_context(self):
        inference_context = (
977
978
979
            torch.inference_mode
            if version.parse(version.parse(torch.__version__).base_version) >= version.parse("1.9.0")
            else torch.no_grad
980
981
982
        )
        return inference_context

983
    def forward(self, model_inputs, **forward_params):
984
985
        with self.device_placement():
            if self.framework == "tf":
986
987
988
                model_inputs["training"] = False
                model_outputs = self._forward(model_inputs, **forward_params)
            elif self.framework == "pt":
989
                inference_context = self.get_inference_context()
990
                with inference_context():
991
992
993
994
995
996
997
                    model_inputs = self._ensure_tensor_on_device(model_inputs, device=self.device)
                    model_outputs = self._forward(model_inputs, **forward_params)
                    model_outputs = self._ensure_tensor_on_device(model_outputs, device=torch.device("cpu"))
            else:
                raise ValueError(f"Framework {self.framework} is not supported")
        return model_outputs

998
999
1000
    def get_iterator(
        self, inputs, num_workers: int, batch_size: int, preprocess_params, forward_params, postprocess_params
    ):
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
        if isinstance(inputs, collections.abc.Sized):
            dataset = PipelineDataset(inputs, self.preprocess, preprocess_params)
        else:
            if num_workers > 1:
                logger.warning(
                    "For iterable dataset using num_workers>1 is likely to result"
                    " in errors since everything is iterable, setting `num_workers=1`"
                    " to guarantee correctness."
                )
                num_workers = 1
            dataset = PipelineIterator(inputs, self.preprocess, preprocess_params)
1012
1013
1014
        if "TOKENIZERS_PARALLELISM" not in os.environ:
            logger.info("Disabling tokenizer parallelism, we're using DataLoader multithreading already")
            os.environ["TOKENIZERS_PARALLELISM"] = "false"
1015
1016
1017
        collate_fn = no_collate_fn if batch_size == 1 else pad_collate_fn(self.tokenizer, self.feature_extractor)
        dataloader = DataLoader(dataset, num_workers=num_workers, batch_size=batch_size, collate_fn=collate_fn)
        model_iterator = PipelineIterator(dataloader, self.forward, forward_params, loader_batch_size=batch_size)
1018
1019
1020
        final_iterator = PipelineIterator(model_iterator, self.postprocess, postprocess_params)
        return final_iterator

1021
    def __call__(self, inputs, *args, num_workers=None, batch_size=None, **kwargs):
1022
1023
        if args:
            logger.warning(f"Ignoring args : {args}")
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035

        if num_workers is None:
            if self._num_workers is None:
                num_workers = 0
            else:
                num_workers = self._num_workers
        if batch_size is None:
            if self._batch_size is None:
                batch_size = 1
            else:
                batch_size = self._batch_size

1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
        preprocess_params, forward_params, postprocess_params = self._sanitize_parameters(**kwargs)

        # Fuse __init__ params and __call__ params without modifying the __init__ ones.
        preprocess_params = {**self._preprocess_params, **preprocess_params}
        forward_params = {**self._forward_params, **forward_params}
        postprocess_params = {**self._postprocess_params, **postprocess_params}

        self.call_count += 1
        if self.call_count > 10 and self.framework == "pt" and self.device.type == "cuda":
            warnings.warn(
Sylvain Gugger's avatar
Sylvain Gugger committed
1046
1047
                "You seem to be using the pipelines sequentially on GPU. In order to maximize efficiency please use a"
                " dataset",
1048
1049
                UserWarning,
            )
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061

        is_dataset = Dataset is not None and isinstance(inputs, Dataset)
        is_generator = isinstance(inputs, types.GeneratorType)
        is_list = isinstance(inputs, list)

        is_iterable = is_dataset or is_generator or is_list

        # TODO make the get_iterator work also for `tf` (and `flax`).
        can_use_iterator = self.framework == "pt" and (is_dataset or is_generator or is_list)

        if is_list:
            if can_use_iterator:
1062
                final_iterator = self.get_iterator(
1063
                    inputs, num_workers, batch_size, preprocess_params, forward_params, postprocess_params
1064
1065
1066
1067
1068
                )
                outputs = [output for output in final_iterator]
                return outputs
            else:
                return self.run_multi(inputs, preprocess_params, forward_params, postprocess_params)
1069
        elif can_use_iterator:
1070
1071
1072
            return self.get_iterator(
                inputs, num_workers, batch_size, preprocess_params, forward_params, postprocess_params
            )
1073
1074
        elif is_iterable:
            return self.iterate(inputs, preprocess_params, forward_params, postprocess_params)
1075
        else:
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
            return self.run_single(inputs, preprocess_params, forward_params, postprocess_params)

    def run_multi(self, inputs, preprocess_params, forward_params, postprocess_params):
        return [self.run_single(item, preprocess_params, forward_params, postprocess_params) for item in inputs]

    def run_single(self, inputs, preprocess_params, forward_params, postprocess_params):
        model_inputs = self.preprocess(inputs, **preprocess_params)
        model_outputs = self.forward(model_inputs, **forward_params)
        outputs = self.postprocess(model_outputs, **postprocess_params)
        return outputs
1086
1087
1088
1089
1090
1091

    def iterate(self, inputs, preprocess_params, forward_params, postprocess_params):
        # This function should become `get_iterator` again, this is a temporary
        # easy solution.
        for input_ in inputs:
            yield self.run_single(input_, preprocess_params, forward_params, postprocess_params)
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110


class ChunkPipeline(Pipeline):
    def run_single(self, inputs, preprocess_params, forward_params, postprocess_params):
        all_outputs = []
        for model_inputs in self.preprocess(inputs, **preprocess_params):
            model_outputs = self.forward(model_inputs, **forward_params)
            all_outputs.append(model_outputs)
        outputs = self.postprocess(all_outputs, **postprocess_params)
        return outputs

    def get_iterator(
        self, inputs, num_workers: int, batch_size: int, preprocess_params, forward_params, postprocess_params
    ):
        if "TOKENIZERS_PARALLELISM" not in os.environ:
            logger.info("Disabling tokenizer parallelism, we're using DataLoader multithreading already")
            os.environ["TOKENIZERS_PARALLELISM"] = "false"
        if num_workers > 1:
            logger.warning(
Sylvain Gugger's avatar
Sylvain Gugger committed
1111
1112
                "For ChunkPipeline using num_workers>0 is likely to result in errors since everything is iterable,"
                " setting `num_workers=1` to guarantee correctness."
1113
1114
1115
1116
1117
1118
1119
1120
            )
            num_workers = 1
        dataset = PipelineChunkIterator(inputs, self.preprocess, preprocess_params)
        collate_fn = no_collate_fn if batch_size == 1 else pad_collate_fn(self.tokenizer, self.feature_extractor)
        dataloader = DataLoader(dataset, num_workers=num_workers, batch_size=batch_size, collate_fn=collate_fn)
        model_iterator = PipelinePackIterator(dataloader, self.forward, forward_params, loader_batch_size=batch_size)
        final_iterator = PipelineIterator(model_iterator, self.postprocess, postprocess_params)
        return final_iterator
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132


class PipelineRegistry:
    def __init__(self, supported_tasks: Dict[str, Any], task_aliases: Dict[str, str]) -> None:
        self.supported_tasks = supported_tasks
        self.task_aliases = task_aliases

    def get_supported_tasks(self) -> List[str]:
        supported_task = list(self.supported_tasks.keys()) + list(self.task_aliases.keys())
        supported_task.sort()
        return supported_task

1133
    def check_task(self, task: str) -> Tuple[str, Dict, Any]:
1134
1135
1136
1137
        if task in self.task_aliases:
            task = self.task_aliases[task]
        if task in self.supported_tasks:
            targeted_task = self.supported_tasks[task]
1138
            return task, targeted_task, None
1139
1140
1141
1142
1143

        if task.startswith("translation"):
            tokens = task.split("_")
            if len(tokens) == 4 and tokens[0] == "translation" and tokens[2] == "to":
                targeted_task = self.supported_tasks["translation"]
1144
1145
                task = "translation"
                return task, targeted_task, (tokens[1], tokens[3])
1146
1147
1148
1149
1150
1151
            raise KeyError(f"Invalid translation task {task}, use 'translation_XX_to_YY' format")

        raise KeyError(
            f"Unknown task {task}, available tasks are {self.get_supported_tasks() + ['translation_XX_to_YY']}"
        )

Sylvain Gugger's avatar
Sylvain Gugger committed
1152
1153
1154
1155
1156
1157
1158
1159
1160
    def register_pipeline(
        self,
        task: str,
        pipeline_class: type,
        pt_model: Optional[Union[type, Tuple[type]]] = None,
        tf_model: Optional[Union[type, Tuple[type]]] = None,
        default: Optional[Dict] = None,
        type: Optional[str] = None,
    ) -> None:
1161
1162
1163
        if task in self.supported_tasks:
            logger.warning(f"{task} is already registered. Overwriting pipeline for task {task}...")

Sylvain Gugger's avatar
Sylvain Gugger committed
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
        if pt_model is None:
            pt_model = ()
        elif not isinstance(pt_model, tuple):
            pt_model = (pt_model,)

        if tf_model is None:
            tf_model = ()
        elif not isinstance(tf_model, tuple):
            tf_model = (tf_model,)

        task_impl = {"impl": pipeline_class, "pt": pt_model, "tf": tf_model}

        if default is not None:
            if "model" not in default and ("pt" in default or "tf" in default):
                default = {"model": default}
            task_impl["default"] = default

        if type is not None:
            task_impl["type"] = type

1184
        self.supported_tasks[task] = task_impl
Sylvain Gugger's avatar
Sylvain Gugger committed
1185
        pipeline_class._registered_impl = {task: task_impl}
1186
1187
1188

    def to_dict(self):
        return self.supported_tasks