Skip to content
Skip to lesson
Back to Topics
LearnProject Deep DivesDeep Dive - Transformers
🏛️HardModel Architecture

Deep Dive - Transformers

The same model ID still generates text, but assistant turns start with the wrong control tokens. The weights may not be what changed.

42 min read
Learning path
Step 194 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

After an upgrade, the same model ID might still generate text while starting assistant turns with the wrong control tokens. Did the weights change, or did the tokenizer format the conversation differently? A successful load is only the first check.

Hugging Face Transformers connects a model's saved weights to the code and input processing needed to use them. PyTorch gave you tensors, kernels, and autograd. That still isn't a loadable language model. A Hub name has to resolve to one revision. That revision's config.json has to pick a class. The class has to receive tensors with expected names. Text has to become the token IDs that class was trained on. packages those contracts.

It isn't one architecture, and it isn't the Hub. It's a model-definition library: a checkpoint, its configuration, preprocessing rules, and forward pass travel together. The same definition can then load in a notebook, a trainer, , , or without every team rewriting the family from scratch.[1][2]

Use tiny-decoder as a hypothetical checkpoint, not as a Hub repo you'd download. Its fixed dimensions let us check AutoClass lookup, chat rendering, and KV-cache bytes by hand. Later, a smaller randomly initialized Llama runs the real library on CPU without downloading model weights. Those checks establish mechanics, not language quality or serving performance.

tiny-decoder-config.json
{
  "model_type": "llama",
  "hidden_size": 1024,
  "num_hidden_layers": 16,
  "num_attention_heads": 16,
  "num_key_value_heads": 4,
  "vocab_size": 32000,
  "max_position_embeddings": 4096,
  "dtype": "bfloat16"
}

hidden_size / num_attention_heads is the head dimension: D=1024/16=64D = 1024 / 16 = 64. Grouped-query attention here uses Hkv=4H_{kv} = 4 key/value heads, not 16. Carry those values through each later check.

Four contracts around one checkpoint

Before opening weight shards, predict what a successful load must establish: identity, shape, parameters, and input/output semantics. A model repository is a versioned set of contracts, not one giant weight file. For tiny-decoder those pieces are:

ContractTypical files or classesJobA broken contract looks like
IdentityHub repo ID, revision, commitSelect one immutable snapshotCode and weights come from different versions
Configurationconfig.json, PreTrainedConfigDescribe architecture, dimensions, , and optionsWrong head size or layer count
Parametersmodel.safetensors shards and indexRestore learned into modulesMissing, unsafe, or mismatched tensors
Input/outputtokenizer, processor, ModelOutputMap user data to tensors and outputsToken IDs, masks, or pixels have the wrong semantics

The library keeps these contracts separate so each can evolve. Tokenizers may add a without changing weights. Model code can add a cache path while old checkpoints stay loadable. A Hub revision may point at new configuration while older snapshots remain available for .

If output is wrong, inspect the boundary that produced it. Don't rewrite attention kernels when the tokenizer added a different special token, and don't blame a prompt when the class loaded the wrong revision.

A family directory under src/transformers/models/ holds configuration and modeling code, often with tokenizer or processor files. Shared AutoClass mappings and output types live elsewhere in the package. PreTrainedConfig records hyperparameters and options without creating model parameters. PreTrainedModel subclasses torch.nn.Module and supplies the loading and initialization lifecycle:[3]

  1. Instantiation (__init__): Builds the neural network module hierarchy using dimensions from the config.
  2. Weight initialization (_init_weights): Defines parameter initialization formulas (such as normal distributions scaled by config.initializer_range and zeroed biases) for fresh training.
  3. Post-initialization (post_init): Collects model properties and initializes weights, including any configured weight tying. Tying can make input embeddings and the output LM head share parameters; it isn't enabled for every family or configuration.
  4. Checkpoint restoration (from_pretrained): Resolves artifact files, instantiates the architecture, parses .safetensors weights, casts precisions, validates tensor keys, and sets evaluation mode.
  5. Persistence (save_pretrained): Saves model configuration and weights, with a shard index when needed and generation configuration for generative models. Save the tokenizer or processor separately; saving the model doesn't capture preprocessing automatically.

The lifecycle explains what a downstream runtime can reuse. It can replace selected kernels while keeping parameter names and tokenizer behavior, but only when that backend supports the family. A load can still succeed on one machine and fail on another. Which snapshot did each cache resolve?

Hub identity and from_pretrained

from_pretrained() resolves a model source and constructs the class. Given a local directory or Hub ID, it finds configuration and weight files, resolves a revision, downloads missing bytes, and loads tensors into matching parameter names.[3]

The snapshot is the revision the cache resolved, not the branch name in the call. A branch such as main is convenient and mutable. A commit hash identifies one snapshot. The Hub cache keeps refs/ (branch or tag to commit), content-addressed blobs/, and snapshots/ that symlink the exact files for one revision while sharing unchanged blobs across revisions.[4]

Choose the model and tokenizer revisions, dtype, tensor format, and remote-code policy before calling the loader. This API sketch deliberately uses a nonexistent repo and placeholder revision. For a deployment, select a real commit from the repository history; for a local executable check, use the no-download CPU lab below.

load_pinned_checkpoint.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

repo_id = "org/tiny-decoder"
revision = "<commit-sha-from-model-card>"

tokenizer = AutoTokenizer.from_pretrained(repo_id, revision=revision)
model = AutoModelForCausalLM.from_pretrained(
    repo_id,
    revision=revision,
    dtype=torch.bfloat16,
    use_safetensors=True,
    trust_remote_code=False,
)

dtype=torch.bfloat16 makes precision explicit in v5; older examples use the legacy torch_dtype name. The current loading guide resolves an omitted dtype from configuration, then from the first floating-point checkpoint weight. That can load bf16 or fp16 when older application code assumed fp32. Pin the library version as well as the checkpoint, and inspect the loaded parameter dtype.[3]

use_safetensors=True makes the storage choice explicit. Safetensors stores tensors without pickle's arbitrary Python deserialization. It doesn't make custom model code safe.[5]

A safe parse still says nothing about tensor names, shapes, dtype, or architecture. The checkpoint can fail after parsing, or load into a semantically wrong class if those contracts were not checked.

trust_remote_code is a security boundary. AutoClass docs default it to false. When a repository needs custom Python classes, inspect the files, pin both model and code revisions, review dependencies, and run in a constrained environment before enabling it. Never turn it on just to silence an unknown-model error.[6]

A load failure is evidence about one layer

Use the first error to narrow the boundary. A loader can fail before reading bytes, during tensor restoration, or only when the first prompt reaches the model:

SymptomLikely layerFirst check
ValueError for unrecognized model typeAuto mapping or configconfig.json model_type, installed library support
Unexpected missing keysWeight index or class namesShard index, tied weights, conversion notes
Size mismatchConfig or checkpoint variantVocabulary, hidden size, export
safetensors parse errorArtifact integrityDownload , incomplete snapshot, file format
Code execution promptDynamic moduletrust_remote_code, code revision, audit result
Output has wrong rolesTokenizer or chat templateSpecial tokens and apply_chat_template
Numerics shifted after an upgradeDtype defaultExplicit dtype, config dtype, first weight dtype

The same method works for multimodal checkpoints. Separate missing image files, processor shape errors, model class selection, and generation bugs before changing weights. Each symptom points to a different receipt.

Multi-device dispatch and device_map="auto"

When a model would exceed one GPU's video memory (VRAM), ordinary construction can exhaust memory before weights even load. Hugging Face integrates with Accelerate to build an empty skeleton, place a model across GPUs, CPU RAM, and disk, and stream weights with device_map="auto". Disk offload doesn't require NVMe storage, though storage speed affects transfer time.[7]

Predict the memory timeline before choosing a map: first create shapes without parameter storage, then place shard bytes, then move each layer's inputs and weights to its execution device. The dispatch path has four coordinated stages:

  1. Meta device skeleton (init_empty_weights): Parameter tensors have shapes and dtypes but no tensor-data storage. Python modules and metadata still occupy RAM, and buffer handling depends on the initialization context. This avoids allocating a full randomly initialized parameter copy.
  2. Device map calculation (infer_auto_device_map): Accelerate inspects the footprint of each submodule, calculates available memory per GPU (max_memory), and plans placement across cuda:0, cuda:1, cpu, and disk. It respects _no_split_modules (such as ["LlamaDecoderLayer"]) so individual attention blocks aren't fractured across device boundaries.
  3. Checkpoint loading: The loader places tensors according to the map instead of requiring a complete CPU copy first. Accelerate exposes load_checkpoint_and_dispatch for this workflow; Transformers also has its own evolving loading implementation. Don't assume one universal shard order or that peak memory equals final parameter bytes.
  4. Dynamic execution hooks (AlignDevicesHook): When dispatch needs hooks, dispatch_model attaches them to the mapped blocks and their relevant children. Hooks align inputs with each block's execution device. In a GPU-backed map, CPU- and disk-offloaded weights execute on the main execution device, then their temporary device storage is released. Storage location and execution location aren't always the same. A CPU-only map runs on CPU.[8]

The device labels below show illustrative placement. Inspect model.hf_device_map for the actual map; the CPU/disk node shows weight storage, not a separate worker running alongside the GPUs.

Diagram showing config.json, meta skeleton no parameter data, infer_auto_device_map keep _no_split_modules intact, and load_checkpoint_and_dispatch stream safetensors to targets.
config.json, meta skeleton no parameter data, infer_auto_device_map keep _no_split_modules intact, and load_checkpoint_and_dispatch stream safetensors to targets.

The hooks solve inference fit and placement, not distributed training sharding. Accelerate documents this big-model path for inference. It can move sequential layers between devices while other GPUs wait; placement alone doesn't create tensor parallelism. Leave room beyond weights for activations, KV cache, and CUDA workspaces. A map that loads successfully can still run out of memory during generation.[9]

For multi-GPU training, use a supported or configuration; the chosen strategy determines which parameters, gradients, and optimizer states are sharded.[7]

AutoClass dispatch turns metadata into Python types

If config.json says llama but the shards came from another family, which stage should catch it? The Auto* family hides model-family imports while preserving a deterministic lookup. A causal-language-model load does roughly this:

  1. AutoConfig.from_pretrained reads config.json and takes model_type.
  2. A lazy mapping converts that type into a module name and config class.
  3. AutoModelForCausalLM looks up the task head associated with the config class.
  4. from_pretrained on that concrete class allocates modules and restores tensors.

At source commit b3a36037d3feb22e3f0174b3dd4248fcc0f0f722, CONFIG_MAPPING_NAMES lives in src/transformers/models/auto/auto_mappings.py. configuration_auto.py wraps it in _LazyConfigMapping and implements AutoConfig.from_pretrained. The built-in path requires model_type; missing or unknown types raise ValueError at the public loader. Authorized custom-code dispatch is a separate branch. None of these selections infer the architecture by examining tensor bytes. modeling_auto.py owns task maps such as MODEL_FOR_CAUSAL_LM_MAPPING_NAMES, where llama maps to LlamaForCausalLM.[10]

The mapping is the public extension story. A new family registers a config and model class, adds family files, writes conversion tests, and exposes a stable model_type. A 2022 design note argued for keeping each family readable in one modeling file. Later releases added a modular contributor path: write one modular file, then generate the expanded source the loader imports.[11][2]

AutoClass lookup for the tiny-decoder fixture: config.json model_type enters a lazy mapping that never reads weight shards. The llama key selects LlamaForCausalLM. Changing only model_type to gpt2 still returns a valid class, GPT2LMHeadModel, which is the wrong family for those shards.
Follow model_type, not the shards. tiny-decoder's llama key selects LlamaForCausalLM. The same files with model_type gpt2 still resolve to a class, just the wrong family.

The fixture isolates one decision: config selects a class; weight compatibility comes later. This standard-library lookup mirrors only the mapping, not the complete public loader or its error types. It doesn't import Transformers.

select_causal_lm.py
CONFIG_MAPPING = {"llama": "LlamaConfig", "gpt2": "GPT2Config"}
CAUSAL_LM = {"llama": "LlamaForCausalLM", "gpt2": "GPT2LMHeadModel"}

def select_causal_lm(config: dict) -> str:
    model_type = config["model_type"]
    if model_type not in CONFIG_MAPPING:
        raise KeyError(model_type)
    return CAUSAL_LM[model_type]

tiny_decoder = {"model_type": "llama", "hidden_size": 1024}
wrong_type = {**tiny_decoder, "model_type": "gpt2"}

assert select_causal_lm(tiny_decoder) == "LlamaForCausalLM"
assert select_causal_lm(wrong_type) == "GPT2LMHeadModel"

try:
    select_causal_lm({"model_type": "not-a-family"})
except KeyError as exc:
    assert str(exc) == "'not-a-family'"
else:
    raise AssertionError("unknown model_type should raise")

print(select_causal_lm(tiny_decoder))
print(select_causal_lm(wrong_type))
Selected classes
LlamaForCausalLM
GPT2LMHeadModel

The gpt2 row is the trap: AutoClass can return a valid class that still isn't the class those shards belong to. Validate model_type, , a known input, and a known output before serving. A correct class still needs the text to become the token IDs that class was trained on.

Tokenizers define more than integer IDs

Tokenization turns text into IDs. Before generation, predict the rest: padding, masks, truncation, special tokens, and the serialized conversation all affect the model input.[12]

  • Vocabulary and merge rules determine which strings map to which IDs.
  • Special tokens mark beginning, end, padding, tool calls, or image placeholders.
  • Padding side affects batched decoder-only generation.
  • Truncation policy decides which context disappears at a limit.
  • Attention masks distinguish real tokens from padding.
  • Offset mappings connect token spans back to source text for extraction tasks.
  • Chat templates turn a list of roles into the exact control-token sequence a checkpoint learned.

PreTrainedTokenizerBase owns the common encoding contract. The v5 migration favors the Rust tokenizers backend while retaining alternatives for families that need them. Backend support and legacy class names vary by release; inspect the selected tokenizer rather than assuming every family uses the same implementation.[2][12]

The model doesn't know whether a token came from a user prompt or a template. It sees IDs. A chat template is model behavior, not presentation polish.

Compare the rendered string, special-token IDs, and attention mask when two clients produce different answers.

The fixture uses a small ChatML-style template so you can see the control tokens. It isn't Llama 3's production template. This local word-level tokenizer has no downloaded files; the rendering and tokenization calls use the real Transformers API. Install transformers==5.16.1 and jinja2==3.1.6 for this example. No PyTorch model is needed for tokenization.

render_chat_template.py
from tokenizers import Tokenizer
from tokenizers.models import WordLevel
from tokenizers.pre_tokenizers import WhitespaceSplit
from transformers import PreTrainedTokenizerFast

words = ["[UNK]", "<|im_start|>", "<|im_end|>", "system", "user", "assistant",
         "Answer", "with", "one", "sentence.", "What", "does", "a", "revision", "pin?"]
backend = Tokenizer(WordLevel({word: i for i, word in enumerate(words)}, unk_token="[UNK]"))
backend.pre_tokenizer = WhitespaceSplit()
tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=backend, unk_token="[UNK]",
    additional_special_tokens=["<|im_start|>", "<|im_end|>"],
)
tokenizer.chat_template = (
    "{% for m in messages %}{{ '<|im_start|>' + m['role'] + '\\n' + "
    "m['content'] + '<|im_end|>\\n' }}{% endfor %}"
    "{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n' }}{% endif %}"
)

messages = [
    {"role": "system", "content": "Answer with one sentence."},
    {"role": "user", "content": "What does a revision pin?"},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
ids = tokenizer.apply_chat_template(
    messages, tokenize=True, add_generation_prompt=True, return_dict=False,
)

assert prompt.startswith("<|im_start|>system\n")
assert prompt.endswith("<|im_start|>assistant\n")
assert ids == tokenizer(prompt, add_special_tokens=False)["input_ids"]
assert tokenizer.unk_token_id not in ids
print(prompt)
Rendered prompt
<|im_start|>system
Answer with one sentence.<|im_end|>
<|im_start|>user
What does a revision pin?<|im_end|>
<|im_start|>assistant

When rendering a real template to text and tokenizing afterward, use add_special_tokens=False if the template already inserted those tokens. Otherwise a second beginning-of-sequence (BOS) or end-of-sequence (EOS) token can change the prompt. Prefer apply_chat_template(..., tokenize=True) when possible.[13]

Decoder-only batched generation commonly needs left padding because the decoding loop reads logits at the final input position. An attention mask alone doesn't move that position.[14] Reusing EOS as a pad token can work with an explicit mask, but don't erase genuine EOS training labels by masking every occurrence of that ID.

Suppose the prompts have two and four tokens. Right padding makes the short row [a, b, PAD, PAD]; the usual loop reads the last PAD position's logits. Left padding gives [PAD, PAD, a, b], so its final position is the real token b. The mask keeps padding out of attention, while the padding side decides which position the loop reads. This distinction matters to generation loops that read the final position; training batches can use their chosen padding side when their masks and loss positions account for padding.

Processors make multimodal inputs explicit

tiny-decoder is text-only. The moment the checkpoint grows an image or audio tower, a tokenizer isn't enough. An image processor may resize and normalize pixels; an audio feature extractor may create log-mel features. Check the required sample rate explicitly: many extractors expect already-resampled audio and reject a mismatched rate instead of resampling it.

A ProcessorMixin composes those pieces with a tokenizer and returns a dictionary of named tensors.[15]

Diagram showing text / messages, processor tokenize + preprocess, raw image, and aligned batch.
text / messages, processor tokenize + preprocess, raw image, and aligned batch.

Inputs extend beyond input_ids. They can include pixel_values, image_sizes, pixel_attention_mask, input_features, or model-specific placeholder indices.

The language model must know where those features become and how many positions they occupy. A processor that emits a valid tensor with the wrong normalization or placeholder count can produce confident nonsense.

src/transformers/processing_utils.py defines common modality discovery and save/load behavior. Keep processor and model revisions aligned when a checkpoint changes image resolution, audio rate, or special-token layout.[15]

A schematic chat-processor call looks like this. It needs an image file, that processor's image dependencies, and a compatible real vision-language chat repo, so it isn't an executable lab. Its multimodal chat template coordinates text, image placeholders, and pixel preprocessing; raw prose alone isn't a universal image prompt.[16]

prepare_multimodal_batch.py
from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained(
    "org/vision-language-model",
    revision="<commit-sha>",
)
messages = [{"role": "user", "content": [
    {"type": "image", "path": "chart.png"},
    {"type": "text", "text": "Describe this chart."},
]}]
inputs = processor.apply_chat_template(
    messages, add_generation_prompt=True, tokenize=True,
    return_dict=True, return_tensors="pt",
)

forward and generate are different layers

Before choosing an API, ask whether the caller needs one forward pass or a decoding policy. Calling a model's forward computes one pass through the neural network. For a causal model, it takes input_ids, optional attention_mask, position_ids, past_key_values, and optional labels.

With dictionary-style outputs enabled, it returns a ModelOutput subclass such as CausalLMOutputWithPast. You can use attributes (outputs.logits, outputs.loss) or indexing over non-None fields. Call to_tuple() for a tuple; the object isn't itself an ordinary tuple, and integer index meanings can change when optional fields appear.[1]

For LlamaForCausalLM, a full-sequence forward computes with shape [batch, sequence, vocabulary]. Its causal-LM loss pairs position 0's logits with position 1's label, then position 1 with position 2, and so on. Pass unshifted labels; shifting them yourself would shift the target twice. Labels equal to -100 are excluded from the loss.[17]

That lets a PyTorch training loop execute loss = model(**batch).loss followed by loss.backward(). An attention mask controls attention, not which targets contribute to loss; mask unwanted labels separately. Classification heads and bare base models have different output and loss contracts. PreTrainedModel doesn't impose causal cross-entropy on all of them.

generate owns the autoregressive decoding loop. It consumes a GenerationConfig object, loaded from generation_config.json or passed as arguments, applies LogitsProcessorList transformations, evaluates StoppingCriteriaList, and orchestrates KV cache updates across iterative steps.[18]

At the pinned revision, GENERATION_MODES_MAPPING in src/transformers/generation/utils.py routes decoding modes into execution routines. Greedy mode shares _sample with sampling, but do_sample=False makes its token choice argmax.[10][18]

  • Greedy search (_sample with sampling disabled): Selects the argmax token at each step (do_sample=False, num_beams=1).
  • Multinomial sampling (_sample): Applies temperature scaling, top-k filtering, and nucleus top-p filtering before sampling from the probability distribution (do_sample=True, num_beams=1).
  • Beam search (_beam_search): Keeps a bounded set of hypotheses (num_beams > 1, do_sample=False). Final ranking includes configured length penalties and constraints; pruning means it needn't find the globally most probable sequence.
  • Assisted decoding (_assisted_decoding): Employs a smaller draft model or candidate generator for speculative execution, verifying candidate tokens in parallel with the target model.

The neural architecture lives in forward; generate wraps that pass with state, policy, and stopping decisions.

Once model is loaded and batch holds tokenizer tensors (input_ids, attention_mask), an explicit greedy call looks like this. This fragment needs the actual model and tokenizer; the CPU lab below supplies its own small model.

make_decoding_policy_explicit.py
outputs = model.generate(
    **batch,
    max_new_tokens=96,
    do_sample=False,
    num_beams=1,
    return_dict_in_generate=True,
    use_cache=True,
)
new_ids = outputs.sequences[:, batch["input_ids"].shape[1]:]
text = tokenizer.batch_decode(new_ids, skip_special_tokens=True)

For a decoder-only model, outputs.sequences includes the input prefix. Slice it off when the caller wants only the completion. Explicit num_beams=1 also matters: do_sample=False alone can still select beam search if a saved generation config requests several beams.

max_new_tokens, stop tokens, repetition controls, and sampling parameters belong in an explicit generation configuration. A pipeline can supply defaults that hide those choices.

Capture the resolved config when comparing quality or latency across releases.

KV caches turn repeated prefix work into state

At each autoregressive step, a new token attends to all earlier keys and values. Ask what can persist before computing the next token: a stores per-layer key and value tensors, so the next forward pass projects only the new token and reads prior state.[19]

For one tiny-decoder token, a layer stores four key heads and four value heads, each containing 64 bf16 values. That's 2×4×64×2=10242 \times 4 \times 64 \times 2 = 1024 bytes per layer, or 16,384 bytes across 16 layers. At 512 cached tokens the payload is 8 MiB; at 4096 it's 64 MiB.

A conventional full-attention cache uses tensors shaped like [batch, kv_heads, sequence, head_dim]. Grouped-query or stores fewer key/value heads than query heads. Generalizing the calculation:

KV bytes≈2×L×B×S×Hkv×D×bytes(dtype).\text{KV bytes} \approx 2 \times L \times B \times S \times H_{kv} \times D \times \text{bytes(dtype)}.

LL is layer count, BB is active sequence count, SS is cached sequence length, HkvH_{kv} is key/value head count, and DD is head dimension. The leading 2 accounts for keys and values. The trailing bytes(dtype) is 2 for bfloat16.

This counts K/V tensor payload only, not weights, activations, allocator overhead, or temporary copies. Beam expansion can increase BB. Sliding-window layers cap their retained length; hybrid models need a per-layer sum rather than one full-context SS everywhere.

src/transformers/cache_utils.py defines DynamicCache, StaticCache, QuantizedCache, and EncoderDecoderCache. Many decoder models use a dynamic cache that grows as tokens arrive. A static cache allocates a maximum sequence length when initialized, often lazily on the first update, so later calls see a stable shape. Unused slots still occupy storage once allocated.

Dynamic and static caches support offloading through their cache options; quantized cache has a different support matrix. Sliding or hybrid layer types, when present in config, shrink the cached length toward a window instead of the full SS.[19]

The cache API is an interoperability seam, but it isn't a serving engine's paged pool. Transformers' generation loop usually owns one model call and one cache object per batch.

vLLM and SGLang add request scheduling, physical block allocation, prefix sharing, and multi-request batching around model execution.

Bar chart of tiny-decoder key-value cache memory in mebibytes. DynamicCache grows from 8 MiB at sequence length 512 to 64 MiB at 4096. StaticCache reserved for 4096 tokens stays at 64 MiB even when only 512 tokens are live. Formula is 2 times L times B times S times Hkv times D times 2 bytes, with L=16, B=1, Hkv=4, D=64, bfloat16.
Dynamic cache for tiny-decoder grows from 8 MiB at S=512 to 64 MiB at S=4096. A StaticCache reserved for 4096 tokens stays at 64 MiB even when only 512 tokens are live.
tiny_decoder_kv_bytes.py
def kv_bytes(
    layers: int,
    batch: int,
    seq_len: int,
    kv_heads: int,
    head_dim: int,
    dtype_bytes: int,
) -> int:
    dimensions = (layers, batch, seq_len, kv_heads, head_dim, dtype_bytes)
    if any(type(value) is not int or value < 1 for value in dimensions):
        raise ValueError("cache dimensions and bytes per element must be positive integers")
    return 2 * layers * batch * seq_len * kv_heads * head_dim * dtype_bytes

LAYERS, BATCH, KV_HEADS, HEAD_DIM, DTYPE_BYTES = 16, 1, 4, 64, 2
mib = 1024 * 1024

assert kv_bytes(LAYERS, BATCH, 512, KV_HEADS, HEAD_DIM, DTYPE_BYTES) == 8 * mib
assert kv_bytes(LAYERS, BATCH, 4096, KV_HEADS, HEAD_DIM, DTYPE_BYTES) == 64 * mib

dynamic_512 = kv_bytes(LAYERS, BATCH, 512, KV_HEADS, HEAD_DIM, DTYPE_BYTES) // mib
static_max = kv_bytes(LAYERS, BATCH, 4096, KV_HEADS, HEAD_DIM, DTYPE_BYTES) // mib
print(dynamic_512)
print(static_max)
KV cache MiB
8
64

Those 64 MiB describe one request's cache. They don't admit a second user, share a prefix, or page blocks the way a serving engine does.

Run a real model without a downloaded checkpoint

The arithmetic fixture has 16 layers. For an executable API check, shrink it to two Llama layers, width 32, four query heads, two KV heads, and a 32-token vocabulary. The architecture is real; the random weights haven't learned language. Fixed token IDs keep preprocessing out of this test so a failure points at model behavior rather than a tokenizer.

Save the next two blocks in one script, or run them in order in one notebook. They were checked on CPU with torch==2.12.0 and transformers==5.16.1. The same contracts were also rechecked October 10, 2026, with torch==2.13.0 and transformers==5.19.0; the reproducible lab keeps its original pins. Neither run downloads a model. First compare the model's loss with an explicit shifted, masked cross-entropy, run backward, and round-trip the weights through local safetensors.

local_model_contract.py
import tempfile
import torch
from transformers import AutoConfig, AutoModelForCausalLM, DynamicCache, StaticCache
from transformers.utils import logging

logging.disable_progress_bar()
torch.manual_seed(7)
torch.set_num_threads(1)
config = AutoConfig.for_model(
    "llama", vocab_size=32, hidden_size=32, intermediate_size=64,
    num_hidden_layers=2, num_attention_heads=4, num_key_value_heads=2,
    max_position_embeddings=32, attention_dropout=0.0,
    bos_token_id=1, eos_token_id=None, pad_token_id=0,
    tie_word_embeddings=True,
)
model = AutoModelForCausalLM.from_config(config, attn_implementation="eager").eval()
ids = torch.tensor([[1, 7, 9, 4, 2]])
labels = ids.clone()
labels[:, 3] = -100
out = model(ids, labels=labels, use_cache=False)
expected_loss = torch.nn.functional.cross_entropy(
    out.logits[:, :-1].reshape(-1, 32), labels[:, 1:].reshape(-1), ignore_index=-100,
)
torch.testing.assert_close(out.loss, expected_loss)
out.loss.backward()
assert model.get_input_embeddings().weight.grad.abs().sum() > 0
assert model.get_input_embeddings().weight is model.get_output_embeddings().weight

with tempfile.TemporaryDirectory() as saved:
    model.save_pretrained(saved)
    restored = AutoModelForCausalLM.from_pretrained(
        saved, local_files_only=True, use_safetensors=True, dtype=torch.float32,
        attn_implementation="eager",
    )
    assert not restored.training
    with torch.no_grad():
        torch.testing.assert_close(restored(ids).logits, out.logits)
print(type(restored).__name__, tuple(out.logits.shape))
print("shifted masked loss, backward, tied weights, reload: passed")
Real model checks
LlamaForCausalLM (1, 5, 32)
shifted masked loss, backward, tied weights, reload: passed

The target at input position 3 is ignored, so logits at position 2 don't contribute to loss. That token can still provide attention context for later positions. Also notice that eval() didn't disable gradients: it controls training-dependent module behavior, while torch.no_grad() controls gradient recording.

Now keep the first four tokens in a cache and feed only the fifth. Its logits should agree with the last position of a full five-token forward. In float32, this reduced model stores 2×2×1×5×2×8×4=12802 \times 2 \times 1 \times 5 \times 2 \times 8 \times 4 = 1280 K/V bytes dynamically. Static capacity 16 needs 4096 bytes after allocation, even though only five positions are live.

cache_and_generate_contract.py
with torch.no_grad():
    full = model(ids, use_cache=False).logits[:, -1:]
    changed = ids.clone()
    changed[:, -1] = 3
    original_logits = model(ids, use_cache=False).logits
    changed_logits = model(changed, use_cache=False).logits
    torch.testing.assert_close(original_logits[:, :-1], changed_logits[:, :-1])
    for cache in [DynamicCache(config=config), StaticCache(config=config, max_cache_len=16)]:
        model(ids[:, :-1], past_key_values=cache, use_cache=True)
        tail = model(
            ids[:, -1:], attention_mask=torch.ones_like(ids),
            past_key_values=cache, use_cache=True,
        ).logits
        torch.testing.assert_close(tail, full, atol=1e-6, rtol=1e-5)
        assert cache.get_seq_length() == ids.shape[1]
        stored = sum(t.numel() * t.element_size() for layer in cache.layers
                     for t in [layer.keys, layer.values])
        print(type(cache).__name__, "positions", int(cache.get_seq_length()), "bytes", stored)

    # A same-length prefix can still belong to a different conversation.
    stale = DynamicCache(config=config)
    model(ids[:, :-1], past_key_values=stale, use_cache=True)
    different = ids.clone()
    different[:, 1] = 6
    wrong_tail = model(
        different[:, -1:], attention_mask=torch.ones_like(ids),
        past_key_values=stale, use_cache=True,
    ).logits
    correct_tail = model(different, use_cache=False).logits[:, -1:]
    assert not torch.allclose(wrong_tail, correct_tail, atol=1e-6, rtol=1e-5)

    result = model.generate(
        ids, max_new_tokens=3, do_sample=False, num_beams=1,
        use_cache=True, return_dict_in_generate=True,
    )
    generated = result.sequences
    manual = ids.clone()
    for _ in range(3):
        next_id = model(manual, use_cache=False).logits[:, -1].argmax(-1, keepdim=True)
        manual = torch.cat([manual, next_id], dim=1)
    assert torch.equal(generated, manual)
    assert result.past_key_values.get_seq_length() == generated.shape[1] - 1
    print("generated positions", generated.shape[1],
          "cached positions", int(result.past_key_values.get_seq_length()))
print("causal mask, cache equivalence, stale-prefix rejection, greedy/manual tokens: passed")
Cache and decoding checks
DynamicCache positions 5 bytes 1280
StaticCache positions 5 bytes 4096
generated positions 8 cached positions 7
causal mask, cache equivalence, stale-prefix rejection, greedy/manual tokens: passed

The stale-prefix assertion exposes a different failure: both conversations have five positions, but changing token 1 changes the K/V history needed by token 4. Reusing the old four-token cache computes a plausible next-token vector for the wrong context. The assertion detects the mismatch; application code must reject stale cache state. Cache reuse needs matching prefix IDs, model weights, positional state, and attention semantics. Compare those identities before reuse; rebuild the cache when they change, and copy a valid prefix cache before independent continuations mutate it.[19]

The changed final input token leaves earlier logits unchanged because causal attention hides future positions. That checks an architectural rule separately from cache equivalence.

Generation returns eight sequence positions but seven cached positions in this ordinary greedy path. Predict why before reading the trace: selecting a token doesn't compute that token's keys and values. They enter the cache on its next forward pass.[10]

Forward passTokens processed nowCached positions afterwardToken selected
PrefillFive prompt tokens5First new token (position 5)
Decode 1First new token6Second new token (position 6)
Decode 2Second new token7Third new token (position 7), then stop

Positions in the last column are zero-based. The final selected token is present in sequences but hasn't been forwarded. If you resume from this cache, supply that token and the correct attention mask and positions rather than skipping it. Assisted decoding and other cache paths can have different bookkeeping, so inspect the path you use.

EOS is disabled only to force exactly three steps in this random-model check. A deployed checkpoint needs its actual stop IDs. Cache internals such as cache.layers are version-sensitive; the important contract is agreement with the full forward, not that every backend exposes these fields.

These assertions cover loss wiring, gradients, local serialization, causal masking, two cache layouts, and greedy decoding. They don't establish useful language generation, training convergence, GPU performance, multi-device dispatch, or compatibility with a particular Hub checkpoint.

Pipelines are a boundary, not a serving fleet

If the first smoke test passes but concurrent requests collapse, separate pipeline defaults from serving scheduling. pipeline(task=..., model=...) is the short path for one task. It selects task-specific preprocessing, loads an appropriate model and tokenizer or processor, batches inputs when possible, calls the model, and postprocesses outputs.

Use pipelines for a first end-to-end check, offline jobs, small evaluation , and demos where transparent defaults are acceptable.

Don't assume the default pipeline path is a production scheduler. Transformers v5 exposes paged caching, continuous batching, and transformers serve, but a plain pipeline call doesn't provide the admission policy, fault isolation, autoscaling, or GPU topology controls of a dedicated serving fleet. For high-concurrency generation, compare the native serving path with vLLM, SGLang, , or another runtime, then validate outputs and stopping behavior against the same Transformers model definition.[2]

ConcernTransformers pipelineProduction serving engine
Model definitionBroad, model-family awareSupports a selected compatibility set
Input normalizationTask and modality processorsOften adapter-specific or preprocessed upstream
Batch behaviorLocal batching and padding and admission policy
KV memoryModel cache objectPaged or shared physical allocation
APIPython callLong-lived HTTP or RPC service
ScaleOne process or user-managed workersParallel replicas, ranks, routing, health checks

When an optimized server disagrees with an eager Transformers run, compare , prompt template, logits at the first generated position, sampling seed, stop conditions, and cache implementation before comparing full text. Those intermediate receipts tell you whether the drift starts before or after model execution.

Training, serving, and the drift they introduce

Once the model definition loads, surrounding systems can specialize without owning every family:

  • PyTorch supplies tensor, autograd, compiler, and distributed execution.
  • and wrap parameters, gradients, and optimizer state for training.
  • injects adapters while the base model remains mostly frozen.
  • Quantizers replace or transform weight loading paths.
  • vLLM and SGLang map supported definitions into high-throughput runtimes.
  • Exporters target ONNX, , TFLite, or other formats where supported.

For training, Trainer or a custom PyTorch loop owns batching, optimization, evaluation, and checkpoint cadence. It still hands tokenizer-produced fields to the selected model class; it can't repair a mismatched config or chat template.

The catalog is a set of task contracts, not one leaderboard. The same loading ideas cover text, vision, audio, video, and multimodal systems. Hub availability doesn't mean every backend supports every task.[2]

ApplicationTypical entry pointWhat to validateCommon production boundary
Text generationAutoModelForCausalLM + tokenizerChat template, stop IDs, KV cache, decoding policyvLLM or SGLang scheduler
ClassificationAutoModelForSequenceClassificationLabel map, pooling, threshold Batch service or feature pipeline
Retrieval embeddingsencoder model + poolingNormalization, truncation, dimension index and embedding version
Vision-language chatAutoProcessor + supported multimodal model classImage tokens, template, pixel budgetMultimodal serving adapter

A fused kernel may implement slightly different masking. A quantizer may require a calibration artifact. A serving backend may interpret a cache or scaling option differently.

Keep a small eager reference test with fixed tokens and compare logits within an agreed tolerance after every backend or library change.

Transformers' Apache-2.0 library license doesn't grant rights to Hub model weights, datasets, or remote-code modules. Inspect each repository card and license before commercial use.[20][21]

Strengths and limits

StrengthWhy it mattersCost or limit
Wide model coverageOne API spans text, vision, audio, video, and multimodal familiesCoverage can lag a newly released architecture
Shared definitionTraining and serving systems can target common classesBackend support still needs per-model work
Reproducible Hub loadingRevision and snapshot concepts make artifacts inspectableLoading main or unreviewed code defeats that benefit
Typed preprocessingTokenizers and processors expose named inputs and masksSmall preprocessing changes can alter model behavior
Rich generation policySampling, beams, assisted decoding, and cache modes are availableDefaults can hide policy and use more memory than a tuned server
Python-first ergonomicsA researcher can inspect and modify a model quicklyPython orchestration alone isn't a high-throughput serving design

Supporting many families means preserving old checkpoints while adding new shapes, modalities, kernels, and dispatch rules. Read the exact model and processor code for a checkpoint rather than assuming every model follows a decoder-only template.

A report that the model changed is too coarse. Which contract moved?

Failure modes worth designing for

Revision drift

An application loads a moving branch, while its evaluation artifact records only a model name. A later run changes tokenizer or config without changing application code. Fix it with commit revisions, model cards that name revisions, and a release receipt containing model, processor, and code identities.

Silent template mismatch

Two clients use the same weights but format roles differently. The model sees different control tokens and may answer with an empty turn or tool-call text. Store the rendered template and special-token IDs in a fixture, then compare first-step logits.

Shape-compatible wrong class

A custom config can select a class that accepts tensors but applies the wrong positional or attention rule. Compare known outputs, inspect model_type, and test architecture-specific invariants such as causal masking or image-token count.

Weight conversion gaps

Converted checkpoints can contain renamed, tied, sharded, or transposed tensors. A loader can report missing keys that look harmless while a task head stays randomly initialized. Treat nonzero missing or unexpected keys as a review event unless the model card explains them.

Dtype auto-load

Omitting dtype follows the checkpoint's saved floating precision. Quality, memory, and kernel choice can move after an upgrade even when the revision didn't. Pin dtype in the load call and record it in the receipt.

Cache pressure

Long prompts, large batches, and many generated tokens grow KV memory. Dynamic caches can exhaust a process before compute saturates. Static caches can reserve a large max length for a short request. Track cache bytes and sequence lengths, cap admission, and use a runtime with an allocation policy when traffic is concurrent.

Unsafe custom code

Enabling remote code can execute repository Python during import or loading. Pin and audit code, isolate permissions, and prefer an upstream-supported model class. A successful load isn't a security review.

Processor drift

An image resize or audio sampling default changes after an upgrade. Output quality falls while text fixtures still pass. Keep representative raw inputs, processor configs, and intermediate tensor checks in multimodal evaluation.

Offload latency thrashing

Offloaded layers may need weight transfers at each decoding step. Disk reads and host-to-device transfers can then dominate token latency. Measure the actual device map, interconnect, and workload; fully resident weights avoid those transfers, while runtime and workload still determine latency.

Project identity

Hugging Face is the company behind Transformers. Its official history says it open-sourced a PyTorch implementation of BERT in 2018, before the library expanded across model families and modalities.[22] Hugging Face engineers maintain the project with a large contributor community, while model authors and infrastructure teams contribute architecture support, conversion code, tests, and documentation.[1]

FieldCurrent project fact
OriginHugging Face released a PyTorch BERT implementation in 2018. Thomas Wolf, Lysandre Debut, Victor Sanh, and collaborators documented the expanding library in its 2020 EMNLP demonstration paper.[22][23]
StewardshipHugging Face engineers maintain the repository with model authors, infrastructure teams, and community contributors.[1]
Contributor pathPublic model-integration guides, issues, pull requests, tests, and maintainer review define the contribution workflow.[24]
Source licenseTransformers library code is Apache-2.0.[20]
Commercial boundaryHugging Face's hosted Hub and enterprise products are company services. They're distinct from the open-source library.
Asset boundaryHub model weights, datasets, spaces, and remote code keep their own licenses and usage conditions.[21]

The 2020 paper describes a common interface for inference and training across many NLP models.[23] Transformers implements ideas from model families with their own research histories, including the original Transformer, BERT, GPT-style decoders, encoder-decoder systems, vision transformers, speech encoders, and later multimodal models. The library turns those papers into maintained configuration, model, tokenizer, processor, conversion, and test code.

Current releases add cache classes, compiler-friendly paths, quantization integrations, paged caching, continuous batching, transformers serve, and modular contributor workflows. They also dropped TensorFlow and Flax as supported backends in favor of PyTorch. Check the release notes at the revision you deploy; API names and defaults move.[2]

Follow a request through the source

The source-reading path uses official commit b3a36037d3feb22e3f0174b3dd4248fcc0f0f722 (August 1, 2026), independently of the CPU lab's installed 5.16.1 release.[10] Open that revision on GitHub or in a local checkout. Follow the same request from config to generated token:

  1. Start with src/transformers/models/auto/auto_mappings.py and find CONFIG_MAPPING_NAMES.
  2. Open src/transformers/models/auto/configuration_auto.py for _LazyConfigMapping, model_type_to_module_name, and AutoConfig.from_pretrained.
  3. Open src/transformers/models/auto/modeling_auto.py and find MODEL_FOR_CAUSAL_LM_MAPPING_NAMES (llama → LlamaForCausalLM).
  4. Follow src/transformers/models/llama/ for configuration_llama.py and modeling_llama.py.
  5. Read src/transformers/modeling_utils.py around PreTrainedModel lifecycle (_init_weights, post_init, tie_weights), shard indexes, safetensors, and device or quantizer hooks.
  6. Trace revision and file resolution in src/transformers/utils/hub.py.
  7. Read src/transformers/tokenization_utils_base.py for padding, truncation, special tokens, and batch encodings.
  8. Read src/transformers/processing_utils.py for multimodal processor composition.
  9. Follow src/transformers/generation/utils.py and src/transformers/generation/configuration_utils.py for GenerationConfig, GENERATION_MODES_MAPPING, logits processors, stopping criteria, and cache updates.
  10. Inspect src/transformers/cache_utils.py for DynamicCache, StaticCache, QuantizedCache, and the offloading flag.
  11. Finish at src/transformers/pipelines/base.py to see where preprocessing, batching, model invocation, and postprocessing meet.

Keep a tiny fixture while reading. Pin one checkpoint revision, record tokenizer output, run one forward pass, save first-position logits, and test one generated sequence. That fixture tells you whether a later change moved at the identity, input, model, cache, or decoding boundary.

A release receipt for a Transformers application

Before promoting a model, predict what a future investigator will need to reproduce: identity, inputs, model class, runtime, policy, and evidence. Record:

  • Hub repository and immutable revision for model, tokenizer, and processor.
  • Transformers, PyTorch, tokenizer backend, and CUDA or accelerator versions.
  • Config hash and model class selected by AutoClass.
  • Safetensors shard index and expected parameter count.
  • Prompt or chat-template rendering and special-token IDs.
  • Processor settings, raw input fixture, and intermediate tensor shapes for non-text modalities.
  • Dtype, quantization mode, device map, parallelism, and cache implementation.
  • Generation configuration, random seed, stop IDs, and maximum token policy.
  • Reference logits or outputs and tolerance for optimized backends.
  • License, model-card restrictions, and custom-code review decision.

This receipt separates reproducibility from performance. A faster server that changes the prompt template isn't an equivalent deployment. A correct output from an unpinned revision isn't a reproducible release. Use the receipt to separate a weight change from a template change, a cache-length mismatch, and a device-map stall.

Test a change before trusting it

Follow-up questions

  • Double the CPU lab's KV-head count from two to four while keeping query heads fixed. Predict both cache sizes, then rerun. The payload should double from 1280 to 2560 bytes dynamically and from 4096 to 8192 bytes statically.
  • Change one training label to -100. Which logits lose a target, and can that input token still affect later predictions? Check the previous position's loss term; label masking doesn't remove the token from attention.
  • A new backend matches full-forward logits but diverges after the second generated token. What would you compare next? Start with the first divergent token, cache positions and masks, decoding transforms, and stopping rules rather than replacing the weights.

Evaluation rubric

  • Traces a pinned checkpoint through configuration, concrete model class, tensor loading, and preprocessing.
  • Distinguishes causal-LM loss shifting, attention masks, label masks, and optional output fields.
  • Reproduces cached/full-logit and manual/greedy agreement with an explicit tolerance and version.
  • Calculates K/V payload using stored KV heads, dtype bytes, live length, and allocated capacity.
  • Separates local CPU evidence from language quality, accelerator performance, and serving compatibility.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A checkpoint has valid weights and a plausible config, but model_type selects a different causal-LM class with compatible shapes. What failure mode is this?
2.Why pin a Hub revision when producing a benchmark receipt?
3.What does device_map=auto primarily change during model loading?
4.A repository requires custom modeling code during from_pretrained. What is the relevant trust decision?
5.Two chat clients use the same messages but produce different prompts and outputs. Which evidence should be compared first?
6.A serving wrapper needs one forward pass for scoring and another loop for autoregressive output. Which API distinction matters?
7.A tiny decoder uses L=16, B=1, Hkv=4, D=64, bf16, and a dynamic cache length of 512. How much KV storage is occupied by that sequence?
8.A serving test has correct token IDs but different cache lengths and stopping behavior from the reference script. Which debugging order is strongest?
9.In the CPU lab, labels are unshifted and labels[:, 3] is -100. Which loss term is removed?
10.The ordinary greedy CPU lab returns eight sequence positions after three new tokens, but its cache has seven positions. Why?

10 questions remaining.

References

Transformers Source Repository

Hugging Face · 2026

https://github.com/huggingface/transformers

Transformers v5: Simple Model Definitions Powering the AI Ecosystem

Hugging Face · 2025

https://huggingface.co/blog/transformers-v5

Loading Models

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/models

Understand Caching

Hugging Face · 2026

https://huggingface.co/docs/huggingface_hub/main/guides/manage-cache

Safetensors Documentation

Hugging Face · 2026

https://huggingface.co/docs/safetensors/main/index

Transformers Auto Classes

Hugging Face · 2026

https://huggingface.co/docs/transformers/model_doc/auto

Accelerate Documentation: Big Model Inference.

Hugging Face · 2026

https://huggingface.co/docs/accelerate/usage_guides/big_modeling

Accelerate Model Dispatch and Execution Devices (e203e1d)

Hugging Face Contributors · 2026

https://github.com/huggingface/accelerate/blob/e203e1ddf33388e22f4a7985ff3103717c0f9181/src/accelerate/big_modeling.py

Accelerate: Loading Big Models into Memory

Hugging Face · 2026

https://huggingface.co/docs/accelerate/concept_guides/big_model_inference

Transformers Source Snapshot (b3a3603)

Hugging Face Contributors · 2026

https://github.com/huggingface/transformers/tree/b3a36037d3feb22e3f0174b3dd4248fcc0f0f722

Transformers Design Philosophy

von Platen, P. · 2022

https://huggingface.co/blog/transformers-design-philosophy

Tokenizer API

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/main_classes/tokenizer

Transformers Documentation: Chat templates.

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/chat_templating

Transformers: Text Generation and Padding Pitfalls

Hugging Face · 2026

https://huggingface.co/docs/transformers/llm_tutorial

Multimodal Processors

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/processors

Transformers: Multimodal Chat Templates

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/chat_templating_multimodal

Transformers Causal Language-Model Loss

Hugging Face · 2026

https://github.com/huggingface/transformers/blob/main/src/transformers/loss/loss_utils.py

Text Generation.

Hugging Face. · 2026 · Official documentation

https://huggingface.co/docs/transformers/en/main_classes/text_generation

KV Cache Strategies

Hugging Face · 2026

https://huggingface.co/docs/transformers/main/en/kv_cache

Transformers Apache License 2.0

Hugging Face · 2026

https://github.com/huggingface/transformers/blob/b3a36037d3feb22e3f0174b3dd4248fcc0f0f722/LICENSE

Licenses for Hub Repositories

Hugging Face · 2026

https://huggingface.co/docs/hub/en/repositories-licenses

We Raised $100 Million for Open & Collaborative Machine Learning

Hugging Face · 2022

https://huggingface.co/blog/series-c

Transformers: State-of-the-Art Natural Language Processing

Wolf, T., Debut, L., Sanh, V., et al. · 2020 · EMNLP 2020 System Demonstrations

https://aclanthology.org/2020.emnlp-demos.6/

Contributing to Transformers

Hugging Face · 2026

https://huggingface.co/docs/transformers/en/contributing

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.