← all posts
// local · prompt-template

The prompt template can ruin a good local model

Here's a failure that happens to people constantly: they drop a new GGUF into a server config that worked fine for the last model, change nothing else, and the assistant starts ignoring the system prompt, echoing role tags into its own reply, or trailing off into a format from a different model family. Nothing throws an error. The logs are clean. Everyone blames the model, or the quantization, and almost nobody checks the file that actually broke: the prompt template.

Instruction-tuned models learn conversational boundaries during training, exact strings marking where the system message ends and the assistant may stop talking. Those boundaries aren't decoration wrapped around the weights. They're part of the contract the model trained against, and a runtime that gets them wrong runs a worse model even though the checkpoint on disk hasn't changed a byte. A private chat, a coding loop, and a nightly extraction job can run on one machine, sometimes the same weights, and still need different handling of that template. Get one wrong and you won't notice for weeks.

The part everyone skips when they swap a GGUF

Most local setups load a raw GGUF from one place and a chat template from somewhere else: a runtime default, a config copied from a different model's README, an old hand-edited Jinja file. That split is normal, and it's where things drift. The model doesn't know the template changed. It just sees input shaped differently than in training and answers worse, vaguer, occasionally strange. It doesn't complain.

The gap between a correct render and a mismatched one is often a handful of tokens:

correct render:
<|system|> You are a careful assistant.
<|user|> summarize this ticket
<|assistant|>

mismatched render (wrong template family):
### Instruction:
summarize this ticket
### Response:

Both are valid text a server will happily return. Only one matches what the model trained to expect, and the difference doesn't surface as an error. It surfaces as a model suddenly worse at following instructions, for no reason anyone can name.

Write down what you're running before you tune anything

Before touching a setting, capture a baseline: one named model, one fixed prompt set, the exact server command. Record the model artifact and the prompt template as part of it, not a footnote, because those two files are easiest to lose track of, and losing them invalidates the comparison. Pull the tokenizer metadata or model card, inspect the rendered prompt as text, and test the role-sensitive cases: does the system message survive a long conversation, does the model know where its own turn ends, does a multi-turn exchange keep roles straight five messages in. Keep the notebook boring:

  • artifact, runtime, and launch flags, together
  • workload and the fixed input set
  • cold start, warm start, p50 and p95 latency
  • peak memory and, when it matters, wall energy
  • quality failures and outright abstentions, not just pass or fail
  • the decision, who owns it, when to retest it

Skip that line and the exercise rots. A benchmark with no decision attached is trivia by the next model release, and a configuration nobody owns turns into folklore.

Why the fix usually gets blamed on the wrong thing

The easy target is quantization, since it's the dial everyone already knows how to argue about, a real question worth its own pass in picking a GGUF quant. But a wrapped-wrong prompt produces the same degraded, still-plausible output a bad quant does. The server returns text that reads like an answer, just to a slightly different question.

Local inference has a whole family of soft failures like this, none of which throws an error: partial CPU offload nobody notices until latency creeps, a cache that misses more than it should, swap growing quietly, a queue holding requests whose clients already disconnected, a fallback path changing which machine your data lands on.

Watch runtime logs and OS counters while a test runs, not just the final number. Change one variable at a time unless comparing whole configurations. Run more than once, and read the outputs instead of trusting a speed number for quality. If a change makes a real task worse, that cost belongs next to whatever gain it bought.

Simplicity has value too. An optimization needing hand repair after every driver update isn't free. A boring setup rebuilt from a service file or a short script beats a clever one you'll forget how to fix under pressure. Keep raw weights out of the report, but keep hashes and commands, so a rerun months out still means the same thing.

Pin the template the way you pin the weights

The rule that's held up for me: version the prompt template with the model artifact, as one unit, not two files that drift apart. It's duller than picking the biggest model that fits in memory, or chasing the newest accelerator, a question covered separately in sizing hardware for local LLMs, but it buys a stack whose limits you can see. Visible limits get scheduled around, priced, or handed off. Invisible ones turn into an evening page and a rushed upgrade nobody budgeted for.

Stop tuning once the workload clears its quality and latency bar with real headroom left. That margin isn't waste. It absorbs a longer document, one more user, a hot afternoon the cooling can't keep up with, or the next runtime release changing something you didn't ask for. Efficient local inference is mostly the discipline of spending model capacity only where it changes the answer.

Honest caveat, though: I don't bother with any of this for a model I'm only running for an afternoon. Pinning a template and writing down a baseline is overhead you pay for a system somebody else depends on. If it's just you poking at a checkpoint on a Saturday, skip it, run the vibes check, get on with your day. The discipline earns its keep once more than one workload or person relies on the thing staying the same, and not a day before.

#prompt-template#gguf#quality