Advanced

The Hugging Face Transformers ecosystem

Between weights on the Hub and a reply sit four moving parts — revision pinning, safetensors loading, the chat template, and generate() — and only one of them is the model.

Updated

01 · Concept

Concept

A colleague reports that Qwen3.8-27B “ignores the thinking toggle” — they set it, the model reasons anyway, and the flag appears to be decorative. Another reports that the same script gave different answers last month than it does today, with no code change. Both bugs are real, neither is in the model, and both live in the thin layer of plumbing between weights on the Hub and text on a screen. That layer has four parts: which bytes you fetched, how they were loaded, how your conversation became a string, and how tokens were sampled. Lesson 8.1 gave you the runtime; this lesson gives you everything wrapped around it.

Start with provenance. A Hub repository such as Qwen/Qwen3.8-27B is a git repository with large files, and a repository identifier alone names a branch, not a snapshot. Loading without pinning a revision means “whatever main points at when the download cache misses” — which is why the same script drifts across months as the vendor pushes a tokenizer fix or a config correction. Pin a commit hash, record it next to your evaluation results, and treat the model directory as an artifact with a version, exactly as you would a container image.

Then the format. Qwen3.8-27B ships bf16 safetensors, and safetensors is deliberately dull: a JSON header listing every tensor with its dtype, shape, and byte range, followed by one contiguous blob. Loading is a memory map plus slicing, so nothing is deserialized into Python objects and no code from the file ever runs. The 54 GB of bf16 weights are split across shards with an index file mapping tensor names to shards, which is how lesson 4.14 was able to open real weights and find model.layers.31.mlp.gate_proj.weight without loading the other fifty gigabytes. The practical consequences are three: loading is fast because pages arrive on demand, a malicious checkpoint cannot execute on load the way a pickle can, and tensor names are a stable public interface that conversion scripts and LoRA adapters depend on.

Now the part that causes the reported bug. Qwen3.8-27B is a chat model, and chat models are trained on a very specific string layout — role markers, turn boundaries, and the special tokens that delimit a thinking block. That layout is not baked into the weights and it is not in the model config. It is a Jinja template shipped with the tokenizer, and apply_chat_template is what renders it. You hand it a list of dictionaries with roles and contents, it hands you the exact string the model was trained to continue, and tokenizing that string gives ids for the forward pass. The model never sees a role. It sees tokens.

That is where enable_thinking lives: it is a variable consumed by the template, not a parameter of the model and not an argument to the generation loop. Setting it to false makes the template render a prompt whose scaffolding suppresses the thinking block; setting it to true (the default) renders the version that invites one. Identical weights, identical forward pass, different input tokens. So the sequence is: build messages, call the template with add_generation_prompt=True and your thinking choice, tokenize, then unpack the returned encoding into generate().

inputs = tok.apply_chat_template(
    messages,
    add_generation_prompt=True,
    enable_thinking=False,
    return_dict=True,
    return_tensors="pt",
).to(model.device)
out = model.generate(
    **inputs,
    do_sample=True,
    temperature=0.7,
    top_p=0.80,
    top_k=20,
    min_p=0.0,
    repetition_penalty=1.0,
    max_new_tokens=256,
)

This excerpt assumes tok and a single-device model were loaded from the same pinned revision and messages is already defined. In Transformers v5, apply_chat_template(..., return_tensors="pt") returns a BatchEncoding, so passing it as the positional ids argument fails; **inputs supplies both input_ids and attention_mask. do_sample=True is also required for temperature, top-p, top-k, and min-p to affect decoding. The length cap is explicit so a copied example has a finite boundary.

Because the template is a file rather than a property of the weights, it is also something people redistribute and replace. Repositories exist that contain no weights at all — just a chat_template.jinja and a note on what it changes — and because GGUF carries the template in its metadata header and MLX conversions keep it beside the tokenizer, one such file can be pointed at the same model under several different runtimes. What that buys is real but bounded: a template can change the system framing, the scaffolding around a thinking block, and how tool definitions are laid out, so it can measurably shorten replies or shift their format. It cannot change what the model knows. Treat a claimed token saving the way lesson 0.8 teaches you to treat any benchmark number — ask which model, which harness, how many samples, and against which baseline template — and be aware that swapping the template moves the model away from the layout it was tuned on, which is a trade rather than a free win.

Tool calling rides on the same mechanism, which is why it belongs here rather than in a lesson of its own. You pass tool definitions to apply_chat_template alongside the messages; the template renders them into the exact special-token layout the model was trained to answer in; the model emits a structured call as ordinary tokens; and your harness parses that span back into a function name and arguments, runs it, and appends the result as another message. Every one of those steps is text. Nothing in the forward pass knows that a tool exists. When tool calling misbehaves, the bug is almost always in the rendering or the parsing rather than in the model — and lesson 8.5 covers the complementary half, constraining the decode so the emitted call is guaranteed to parse at all.

Sampling is the second quiet failure. The complete current instruct preset is temperature 0.7, top_p 0.80, top_k 20, min_p 0, presence penalty 1.5, and repetition penalty 1.0. Thinking mode uses temperature 1.0, top_p 0.95, top_k 20, min_p 0, presence penalty 0, and repetition penalty 1.0. The raw Transformers generate() interface at the cited revision supports the fields shown in the snippet but has no presence-penalty argument, so the snippet is a disclosed partial implementation of the card’s preset. Set every supported field explicitly, record unsupported fields, and log the effective request — do not assume omitted defaults match the card.

Thinking has three independent controls. enable_thinking decides whether a reasoning block exists. reasoning_effort controls depth and currently defaults to xhigh in the official card; the labels are API and template semantics, so inspect the effective template before comparing runtimes. preserve_thinking currently defaults to true and retains historical reasoning blocks. That can help an agent, but it also makes old reasoning consume prompt tokens on every turn. Measure tokens and wall time for the whole job and deliberately decide whether that history belongs in context.

None of this is a serving stack. Requests run one after another, memory is allocated per call, there is no paging and no continuous batching, and a second concurrent user waits. That is a feature for correctness work and a disqualification for production, which is why lesson 8.4 picks the thread up with vLLM. What you carry forward is the discipline: pin the revision, load safetensors, render the template, set the preset, and know which of the four is lying before you blame the model.

02 · Analogy

Analogy

A concert hall does not ship the composer. It ships a printed edition of the score, a librarian who hands out the right revision, a conductor who decides tempo and dynamics, and a stage manager who says when the piece ends. Blame for a bad performance usually lands on the composer and usually belongs to one of the other three. The Hub, safetensors, the chat template, and the generation loop are those three roles around Qwen3.8-27B.

03 · Teach it back

Teach it back

Trace a chat request through the Transformers stack and explain precisely where enable_thinking takes effect, and why calling it a model setting is wrong.

Minimum: 80 characters and 15 words. Your writing stays only in this browser.

Waiting for your explanation.

Compare with a model answer

A list of role-and-content messages goes to the tokenizer's apply_chat_template, which renders the model's Jinja template — stored with the tokenizer, not the weights — into one string carrying the exact special tokens the model was trained on, then tokenizes it. Current Transformers returns a BatchEncoding with input_ids and an attention_mask, and generate() receives that mapping, runs the forward pass repeatedly, samples when do_sample=True with the decoding parameters you supply, maintains the cache, and stops on a stop token or length. enable_thinking is an argument to apply_chat_template: it changes which scaffolding the template emits, and therefore which tokens the model receives. The weights, the config, and the forward pass are byte-for-byte identical either way. If you build the prompt string yourself, or pass the flag to generate() instead of the template, it does nothing at all and you will conclude the model ignores it.

04 · Check your understanding

Check your understanding

01Why does safetensors loading not execute arbitrary code, unlike a pickled checkpoint?
Answer and explanation

The file is a JSON header of names, dtypes, shapes, and byte offsets followed by a flat tensor blob, so loading is memory-mapping and slicing — There is no object graph to reconstruct; the loader reads offsets and maps bytes, which is both safer and faster than unpickling.

02From lesson 8.1, what should you compare when a Transformers reply and a serving engine's reply diverge?
Answer and explanation

The first differing token under greedy decoding, then whether the tokenizer, chat template, or sampling configuration explains it — not bitwise float equality — bf16 reductions vary with accumulation order, so eager PyTorch defines correctness at the level of token sequences and distributions, not identical floats.

03Which sampling preset does the model card give for thinking mode?
Answer and explanation

temperature 1.0, top_p 0.95, top_k 20 — Thinking mode is the default and wants temperature 1.0 with top_p 0.95 and top_k 20; the instruct preset is the tighter 0.7 / 0.80 / 20.

Complete the teach-back and answer the quiz correctly to finish this lesson.

◎ · Evidence marker

Sources

  1. Hugging Face (2026). Hugging Face Transformers Documentation.
  2. Hugging Face (2026). Transformers v5 Migration Guide — apply_chat_template returns BatchEncoding.
  3. Hugging Face (2026). safetensors Documentation.
  4. Qwen Team (2026). Qwen3.8-27B Model Card.