Read OSS

Pipelines, Tokenizers, and Extending Transformers

Intermediate

Prerequisites

  • Article 1: Import system
  • Article 2: Auto classes
  • Basic understanding of tokenization (BPE, SentencePiece)
  • Familiarity with NLP/vision/audio inference tasks

Pipelines, Tokenizers, and Extending Transformers

Throughout this series, we've traced Transformers from its lazy import system through model resolution, weight loading, generation, and training. This final article covers the layers that sit above and below the model: the high-level pipeline() API that makes inference a one-liner, the tokenizer hierarchy that handles text preprocessing across three backends, the ProcessorMixin for multimodal models, and the extension points that let you contribute new models to the library.

The pipeline() Factory

The pipeline() function is Transformers' simplest API. A single call resolves a task string to the right Pipeline subclass, auto-selects a model and tokenizer, and returns a callable:

generator = pipeline("text-generation", model="meta-llama/Llama-2-7b-hf")
output = generator("Once upon a time")
flowchart TD
    A["pipeline('text-generation',<br/>model='...')"] --> B["Resolve task → Pipeline class"]
    B --> C["PipelineRegistry lookup"]
    C --> D["TextGenerationPipeline"]
    D --> E["Auto-select model<br/>(AutoModelForCausalLM)"]
    E --> F["Auto-select tokenizer<br/>(AutoTokenizer)"]
    F --> G["Load model<br/>from_pretrained()"]
    G --> H["Return configured<br/>Pipeline instance"]

The function signature accepts task, model, tokenizer, config, processor, device, device_map, dtype, and trust_remote_code. If you only provide task, the library picks a default model. If you provide model, it infers the task from the model's config.

The resolution relies on a PipelineRegistry that maps task strings (like "text-generation", "text-classification", "image-to-text") to Pipeline subclasses and their default models. Task aliases are supported — "sentiment-analysis" maps to TextClassificationPipeline.

Pipeline Base Class: preprocess → _forward → postprocess

Every pipeline inherits from Pipeline, which defines the three-step pattern:

sequenceDiagram
    participant User
    participant Pipe as Pipeline.__call__
    participant Pre as preprocess()
    participant Fwd as _forward()
    participant Post as postprocess()

    User->>Pipe: pipe("Hello world")
    Pipe->>Pre: preprocess("Hello world")
    Pre-->>Pipe: model_inputs (dict of tensors)
    Pipe->>Fwd: _forward(model_inputs)
    Note over Fwd: model(**inputs)<br/>with torch.no_grad()
    Fwd-->>Pipe: model_outputs
    Pipe->>Post: postprocess(model_outputs)
    Post-->>User: [{"generated_text": "..."}]

The base Pipeline.__call__ handles batching, chunking for long inputs, and the no-grad context. Subclasses override three methods:

  1. preprocess() — converts raw inputs (strings, images, audio) to model-ready tensors
  2. _forward() — runs the model (default: self.model(**inputs))
  3. postprocess() — converts model outputs to human-readable results

Class flags control which preprocessing components are loaded:

class TextGenerationPipeline(Pipeline):
    _load_tokenizer = True
    _load_image_processor = False
    _load_feature_extractor = False
    _pipeline_calls_generate = True  # Uses model.generate() instead of model()

The _pipeline_calls_generate = True flag is key — text generation pipelines call model.generate() (Part 5) instead of the raw forward pass, inheriting all the decoding, caching, and streaming machinery.

Tip: For production inference with batching, use pipe(texts, batch_size=16). The Pipeline handles padding, collation, and iterating through batches internally. For streaming, pass a TextStreamer to the pipeline's generation kwargs.

The Tokenizer Hierarchy

Tokenization in Transformers has been through multiple architectural iterations. The current system provides a common interface with three backend implementations:

classDiagram
    class PreTrainedTokenizerBase {
        +__call__(text, ...)
        +encode(text)
        +decode(ids)
        +from_pretrained()
        +save_pretrained()
        +padding_side: str
        +model_max_length: int
    }
    class PreTrainedTokenizerPython {
        «Pure Python BPE/WordPiece»
        +_tokenize(text) → list[str]
        +_convert_token_to_id(token) → int
    }
    class PreTrainedTokenizerTokenizers {
        «Rust tokenizers backend»
        +_tokenizer: tokenizers.Tokenizer
        +_batch_encode_plus()
    }
    class PreTrainedTokenizerSentencePiece {
        «SentencePiece backend»
        +sp_model: SentencePieceProcessor
    }
    PreTrainedTokenizerBase <|-- PreTrainedTokenizerPython
    PreTrainedTokenizerBase <|-- PreTrainedTokenizerTokenizers
    PreTrainedTokenizerBase <|-- PreTrainedTokenizerSentencePiece

PreTrainedTokenizerBase defines the common __call__ interface that handles padding, truncation, attention mask creation, and return type selection (pt / np / tf). It's ~2000 lines covering all the combinatorial logic of batch encoding.

The three backends:

As we saw in Part 1, the module alias system maps tokenization_utils_fasttokenization_utils_tokenizers for backward compatibility. The AutoTokenizer class selects the fastest available backend — preferring Rust tokenizers when the tokenizers library is installed.

Tip: Always use AutoTokenizer.from_pretrained(model_name) rather than the model-specific tokenizer class. The Auto class handles backend selection and ensures compatibility with the model's vocabulary.

ProcessorMixin for Multimodal Models

Modern models like LLaVA, Whisper, and CLIP accept more than text. The ProcessorMixin provides a unified __call__ interface that combines a tokenizer with one or more modality-specific processors:

classDiagram
    class ProcessorMixin {
        +tokenizer: PreTrainedTokenizerBase
        +image_processor: BaseImageProcessor
        +feature_extractor: FeatureExtractionMixin
        +__call__(*args, **kwargs)
        +from_pretrained()
        +save_pretrained()
    }
    class LlavaProcessor {
        +tokenizer: LlamaTokenizer
        +image_processor: CLIPImageProcessor
        +__call__(text, images)
    }
    class WhisperProcessor {
        +tokenizer: WhisperTokenizer
        +feature_extractor: WhisperFeatureExtractor
        +__call__(audio, text)
    }
    ProcessorMixin <|-- LlavaProcessor
    ProcessorMixin <|-- WhisperProcessor

The processor's __call__ method typically:

  1. Processes images/audio with the appropriate modality processor
  2. Tokenizes text
  3. Combines them into a single BatchFeature dict with the correct shapes

ProcessorMixin also handles serialization — save_pretrained() saves both the tokenizer and processor configs, and from_pretrained() loads them back.

Extension Points: Adding a New Model

Based on everything we've covered in this series, here's the complete checklist for contributing a new model to Transformers:

flowchart TD
    A["1. Create config<br/>with model_type"] --> B["2. Create model<br/>inheriting PreTrainedModel"]
    B --> C["3. Create tokenizer<br/>(or reuse existing)"]
    C --> D["4. Register in Auto mappings<br/>CONFIG_MAPPING_NAMES<br/>MODEL_FOR_*_MAPPING_NAMES"]
    D --> E["5. Add __all__ exports<br/>to each .py file"]
    E --> F["6. Add TYPE_CHECKING block<br/>in model __init__.py"]
    F --> G["7. Write tests<br/>and documentation"]

Step 1: Configuration. Create a configuration_<model>.py with a class that sets model_type and declares all hyperparameters as typed fields with defaults. Use @strict for validation. Declare base_model_tp_plan and base_model_pp_plan if your model supports parallelism.

Step 2: Model. Create modeling_<model>.py. Inherit from PreTrainedModel, set class flags (_supports_sdpa, etc.), and implement the forward pass using ALL_ATTENTION_FUNCTIONS.get_interface() for attention dispatch. Use GradientCheckpointingLayer for decoder layers. For task heads, use the generic classes (GenericForSequenceClassification, etc.) via multiple inheritance.

Step 3: Auto registration. Add entries to CONFIG_MAPPING_NAMES and the relevant MODEL_FOR_*_MAPPING_NAMES dicts in models/auto/. This is the only manual registration — the lazy import system (Part 1) discovers everything else from __all__ exports.

Step 4: Tokenizer. Either create a new tokenizer or reuse an existing one. Most modern LLMs reuse LlamaTokenizer or a SentencePiece-based tokenizer.

trust_remote_code: Dynamic Module Loading

For models not yet merged into Transformers, the dynamic_module_utils.py module enables loading arbitrary Python code from Hub repos:

model = AutoModelForCausalLM.from_pretrained(
    "org/new-model",
    trust_remote_code=True
)

This downloads the model's Python files from the Hub, executes them in a sandboxed module namespace, and registers the classes with the Auto system. The auto_map field in config.json tells Transformers which remote class to use for each Auto class:

{
    "model_type": "new_model",
    "auto_map": {
        "AutoConfig": "configuration_new_model.NewModelConfig",
        "AutoModelForCausalLM": "modeling_new_model.NewModelForCausalLM"
    }
}

The CLI Scaffolding Tool

The cli/add_new_model_like.py script generates the boilerplate for a new model by copying and adapting an existing one. It handles the tedious parts: creating the directory structure, renaming classes, updating the Auto mappings, and generating test files.

Config-Declared Parallelism Plans

As we saw in Part 3, LLaMA declares its tensor parallel plan directly in the config:

base_model_tp_plan = {
    "layers.*.self_attn.q_proj": "colwise",
    "layers.*.self_attn.k_proj": "colwise",
    "layers.*.self_attn.v_proj": "colwise",
    "layers.*.self_attn.o_proj": "rowwise",
    "layers.*.mlp.gate_proj": "colwise",
    "layers.*.mlp.up_proj": "colwise",
    "layers.*.mlp.down_proj": "rowwise",
}

This means tensor parallelism requires zero code changes — call model.tensor_parallel(device_mesh) and the library applies column-wise or row-wise sharding to each layer based on this plan. Similarly, base_model_pp_plan declares which layers should be placed on which pipeline parallel stage, with explicit input/output tensor specifications.

The Full Architecture

To close out the series, here's how all the pieces fit together:

flowchart TD
    subgraph "User API"
        A["pipeline()"] 
        B["AutoModelForCausalLM"]
        C["Trainer"]
    end

    subgraph "Resolution Layer"
        D["_LazyModule"]
        E["Auto Mappings"]
        F["CONFIG_MAPPING"]
    end

    subgraph "Model Layer"
        G["PreTrainedModel"]
        H["AttentionInterface"]
        I["Generic Head Classes"]
    end

    subgraph "Weight Layer"
        J["from_pretrained()"]
        K["WeightConverter"]
        L["HfQuantizer"]
    end

    subgraph "Generation"
        M["GenerationMixin"]
        N["Cache System"]
        O["LogitsProcessors"]
    end

    subgraph "Training"
        P["Trainer Loop"]
        Q["Callbacks"]
        R["Distributed Backends"]
    end

    subgraph "Preprocessing"
        S["Tokenizers"]
        T["ProcessorMixin"]
    end

    A --> E
    B --> E
    E --> D
    D --> G
    G --> H
    G --> I
    J --> K
    J --> L
    G --> M
    M --> N
    M --> O
    C --> P
    P --> Q
    P --> R
    A --> S
    A --> T

Every box in this diagram corresponds to code we've examined in detail across this series. The import system (Part 1) provides the foundation. Auto classes (Part 2) provide the dispatch. Model anatomy (Part 3) provides the computation. Weight loading (Part 4) provides initialization. Generation (Part 5) provides inference. The Trainer (Part 6) provides training. And this article covers the user-facing APIs and extension points that tie it all together.

Final Thoughts

Transformers is a library that manages exceptional complexity through consistent patterns: lazy loading everywhere, declarative configuration, composable pipelines, and progressive delegation from high-level APIs to low-level implementations. The codebase rewards reading — nearly every design decision has a clear rationale rooted in the constraints of supporting 450+ architectures, a dozen backends, and millions of users.

The patterns we've traced — _LazyModule for deferred imports, _LazyAutoMapping for deferred class resolution, AttentionInterface for pluggable backends, GeneralInterface for two-level dispatch, generic head classes for code reuse, and WeightConverter for checkpoint compatibility — are transferable to any large-scale library design. Understanding them makes you not just a better Transformers user, but a better systems programmer.