Token by Token
Part VI · ProductionChapter 30 / 37

Quality & Safety

Every speed-up is also a model change, so it needs an accuracy gate. And a model behind an API is an attack surface, so the platform needs guards on inputs, outputs, tools and the weights themselves.

The rest of this guide is about making inference faster and cheaper. This chapter covers the two things speed can cost you:

  • Quality. Quantization, an FP8 KV cache, a new engine version, a changed chat template or new speculative decoding settings all change the numbers the model computes. Some changes are harmless. Others quietly break maths, code or long documents while chat still looks fine.
  • Safety. A model behind an API takes untrusted text in, produces text that other systems act on, calls tools, and loads weights from the internet. Each of those is an attack surface the serving platform owns.

Every optimisation needs an accuracy gate

Treat any change below as a new model that must earn its way to production:

ChangeExpected effectWhere it tends to break
FP8 weights and activationsnear-lossless on large modelssmall models, outlier-heavy layers
FP8 KV cachesmall average changelong-context retrieval, attention to rare tokens
INT4 / FP4 weightsmeasurable on hard tasksmulti-step maths, code, low-resource languages
Engine or kernel upgradenone, in theorynumerics bugs, changed sampling defaults, tokenizer handling
Chat template / tokenizer changenone, in theorymissing special tokens, broken tool calls, structured output failures
Speculative decodinglossless with exact verificationimplementation bugs; output length and stop handling

The quantization chapter gives the short version: compare against the baseline on your own evals, include the fragile slices, set thresholds first. The gate below shows why those three rules matter:

InteractiveAccuracy gate for a precision change

Each row is a task family; the dot is the score change against BF16 and the whisker its 95% confidence interval. Pick W4A16: the average looks acceptable while maths and multilingual fail. Then drag Examples per task down to a few hundred: the intervals grow wider than the threshold, and the gate can no longer tell a regression from noise. Stack FP8 KV on top of a weight format to see regressions add up. Deltas are illustrative.

Two consequences. First, gate per task, never on the average: a model that loses 3 points on maths and gains nothing elsewhere has a small average delta and a real regression. Second, size the eval to the threshold: to resolve a 1-point change you need on the order of 1,000+ paired examples per task. Score the same items with both models and look at the items that flipped, not just the totals.

Which evals, and how far to trust them

No single eval is enough. Use a ladder, from cheap and coarse to expensive and close to users:

EvalCatchesMissesCost
Perplexity on a held-out text setgross breakage: wrong scales, broken kernels, wrong tokenizertask-level regressions; small changes barely move itminutes
Standard benchmarks (GSM8K, MMLU-style, HumanEval-style, IFEval, long-context retrieval)regressions on known capabilities, comparable across teamsyour domain; contamination makes absolute scores unreliabletens of minutes to hours
Golden set: your own prompts with reference answers or checksregressions on what your product doesanything not in the set; needs upkeepdepends on size
LLM-as-judge pairwise or rubric scoringquality of open-ended answers at scalebiases of the judge (below)judge tokens
Human review of sampled outputstone, subtle errors, safetyscale; reviewer driftexpensive
Online signals: thumbs, edits, retries, task successreal user impactslow and noisy; needs trafficfree once wired

LLM-as-judge is useful but biased in known ways: it prefers the answer shown first (position bias), prefers longer answers (verbosity bias), and rates outputs from its own model family higher (self-preference). Swap the order and average both, cap or normalise length, use a judge from a different family, and calibrate it against a few hundred human labels before trusting it.

Standard benchmarks run against any OpenAI-compatible endpoint with lm-evaluation-harness, so you test the exact engine, flags and chat template you will serve:

lm_eval --model local-chat-completions \
  --model_args model=meta-llama/Llama-3.1-8B-Instruct,base_url=http://candidate:8000/v1/chat/completions \
  --tasks gsm8k,ifeval --apply_chat_template
# chat endpoints support generative tasks only; log-likelihood tasks (e.g. MMLU) need local-completions

A validation workflow that holds up:

  1. Freeze the baseline: model revision, engine version, flags, chat template, sampling settings. Store its scores and outputs.
  2. Write thresholds before running: e.g. “no task drops more than 1 point; JSON validity ≥ 99.5%; median output length within ±10%”.
  3. Run the candidate on the same items, same sampling (greedy for comparability), through the real serving path.
  4. Diff: per-task deltas with intervals, flipped items, length and refusal changes, parse failures.
  5. Canary or shadow: send a slice of live traffic (or mirror it) and compare online signals before full rollout. Keep the baseline deployable for rollback.

Watching quality in production

Offline evals pass, then quality drifts in production because traffic changes, a template is edited, or an upgrade slips through. Engine dashboards show latency, not correctness, so add cheap quality proxies per model, route and version:

  • Output length distribution (p50, p95). Sudden shifts mean rambling, early stopping or a changed default.
  • finish_reason mix: a jump in length means truncation, often with broken JSON.
  • Refusal rate, by pattern match or a small classifier.
  • JSON / tool-call parse failures and schema-validation errors.
  • Language mismatch (answer not in the prompt’s language), repetition loops, empty answers.
  • User signals: regenerate clicks, thumbs down, edits.

Expect one surprise: at temperature 0 the same prompt does not always give the same answer. Floating-point addition is not associative, and kernels pick their reduction strategy (split-K, tile sizes, attention splits) from the shape of the batch. How many other requests happen to share the step changes the order of additions, the logits move in the last bits, and a near-tie between two tokens can flip.

InteractiveSame prompt, different batch, different answer

The dot products use real fp32 arithmetic; only the kernel heuristic is simplified. Switch batch sizes and watch the split change and the greedy token flip between batch 1 and batch 4. Replay under live traffic to see run-to-run variation, then tick Batch-invariant kernels: one reduction strategy for every batch size, identical results, lower throughput.

The threat model: OWASP Top 10 for LLM applications

The OWASP Top 10 for LLM Applications (2025) is the common checklist. Several items belong to the application, but the serving platform can enforce a lot of them centrally:

RiskWhat it looks likeWhat the serving platform can do
LLM01 Prompt Injectioninstructions hidden in user input or retrieved contentleast-privilege tools, approval for side effects, input classifiers
LLM02 Sensitive Information DisclosurePII or secrets in outputs or logsoutput PII filters, prompt/response log redaction, tenant isolation of prefix caches
LLM03 Supply Chainmalicious weights, code or dependenciessafetensors only, no remote code, pinned revisions, scanning, signatures
LLM04 Data and Model Poisoningbackdoored fine-tunes or adaptersprovenance for models and LoRA adapters, eval gates before promotion
LLM05 Improper Output Handlingmodel output executed as SQL, HTML or shellschema-constrained output, treat output as untrusted downstream
LLM06 Excessive Agencyagents with broad tools and credentialsper-task tool allow-lists, scoped tokens, human-in-the-loop
LLM07 System Prompt Leakageusers extract the system promptkeep secrets out of prompts; enforce rules outside the model
LLM08 Vector and Embedding Weaknessespoisoned or cross-tenant retrievalper-tenant indexes and access checks on retrieval
LLM09 Misinformationconfident wrong answersgrounding, citations, evals on factuality
LLM10 Unbounded Consumptionhuge prompts, endless outputs, cost attackstoken budgets, max_tokens caps, rate limits, timeouts

Prompt injection

Direct injection is a user typing “ignore your instructions”. Indirect injection is worse: the instructions arrive inside content the model reads on the user’s behalf, such as a web page, an email, a PDF or a tool result. The root cause is architectural. Everything in the context, system prompt, user message and retrieved data alike, is one sequence of tokens; the model has no type system that marks some tokens as “data, never instructions”. Delimiters and “ignore instructions in documents” prompts lower the success rate but do not close the hole.

Step-throughAn agent reads a poisoned page

Play the attack with no defences, then tick one mitigation at a time. The classifier stops the obvious payload, but tick Attacker paraphrases and it passes. The allow-list, human approval and least-privilege token stop the attack whatever the model was tricked into, because they act on what the agent can do, not on what it reads.

Unbounded consumption

The cheapest attack on an LLM service is a request that costs a lot: a prompt at the full context length, max_tokens at the maximum, a reasoning model asked to “think as long as needed”, or thousands of parallel streams. Defences belong at the gateway and the engine: per-tenant token budgets (input and output tokens per minute, not just requests), a hard cap on max_tokens and prompt size, a max_model_len sized to the product, request timeouts, and cancellation that frees KV blocks when clients disconnect (see routing and autoscaling).

Guardrails and what they cost

Guardrails are checks outside the main model: an input classifier before the prompt reaches the engine, an output classifier on what comes back, and rules in between. Common building blocks are safety classifier models such as Llama Guard, ShieldGemma and Granite Guardian, and frameworks such as NeMo Guardrails and Guardrails AI that wire checks, rules and validators around the model call. A guard is itself a model call, so it adds latency and GPU time, and on a streaming response the output check fights the stream:

InteractiveGuardrails on a streaming response

Compare the four output strategies. Per chunk keeps unsafe text off the screen but adds a chunk plus a check to TTFT and makes the stream bursty; push the guard latency above the chunk’s generation time and the backlog grows. At end is safest and kills streaming. Async keeps latency but lets text reach the user before it can be retracted. Running the input check in parallel with prefill hides most of its cost. Illustrative timings.

Practical rules: run input checks in parallel with prefill and cancel on a hit; use a small guard model (1–8B) on its own replicas so it scales independently; check output in chunks for interactive traffic and the full response for tool calls and actions; and measure the guard’s false-positive rate, because over-refusal is a quality regression too.

The model supply chain

Model weights are downloaded artefacts, often from public hubs, and loading them can run code. Treat a model like a container image:

  • Safetensors, not pickle. PyTorch .bin/.pt checkpoints use Python pickle, which can execute arbitrary code when loaded. Safetensors stores only a JSON header and raw tensors. Refuse pickle formats from untrusted sources, or convert them in a sandbox.
  • No remote code. trust_remote_code runs Python from the model repository inside your serving process. Keep it off; if a model truly needs it, review and vendor the code.
  • Pin revisions. A repo name points to a moving branch. Pin the commit (hf download <repo> --revision <sha>, and --revision in the engine), and mirror approved models into your own registry or object store.
  • Scan and sign. Scan artefacts for unsafe serialisation (e.g. Protect AI ModelScan), and sign and verify them (OpenSSF model-signing, built on Sigstore) so the weights you serve are the weights you approved.
  • Record it. An AI bill of materials (AI-BOM) lists model, revision, hashes, datasets where known, adapters, engine and licence for each deployment.
  • Read the licence. Open-weight licences differ: some restrict commercial use, user counts, use cases or derivative models. Legal review belongs in the promotion pipeline, like the eval gate.
Interview checkTry answering before you expand.
coreWhy is perplexity not enough to approve a quantized model?

Perplexity averages token-level likelihood over generic text. It catches gross breakage (wrong scales, broken kernels, tokenizer mismatch) but small changes barely move it, and it says nothing about multi-step reasoning, code, long context or tool calls, which are where quantization usually hurts. Approve with task evals per slice, with thresholds set in advance and enough examples to resolve them.

coreWhat is indirect prompt injection and why can't the model just ignore instructions in documents?

Instructions planted in content the model reads for the user (web pages, emails, retrieved chunks, tool results) that steer it to do something else. The model processes system prompt, user request and data as one token sequence with no hard boundary, and it is trained to follow instructions wherever they appear. Prompting reduces the rate but cannot guarantee separation, so defences must limit what a steered model can do: scoped tools and credentials, approval for side effects, isolation.

midFP8 KV cache passed your chat eval but users of the contract-review feature report missed clauses. Why, and what should the gate have included?

FP8 KV rounds keys and values; the effect concentrates in long contexts, where attention must pick a few relevant tokens out of tens of thousands, so retrieval over long documents degrades while short chat is unchanged. The gate needed a long-context slice (needle-style retrieval and real long documents at production lengths) with its own threshold, not a single chat average.

midYour temperature-0 regression test fails intermittently after an engine upgrade, with no code change. What's going on?

Probably batch-dependent numerics: kernels choose reduction strategies by batch shape, so the same prompt gets slightly different logits depending on what else is in the step, and near-ties flip. Confirm by running the prompt alone vs under load. Options: run tests with batch-invariant kernels (VLLM_BATCH_INVARIANT=1, slower), run them in isolation at batch 1, or make the test compare scores or semantic equivalence rather than exact strings.

seniorAdding an output safety classifier made your chat feel broken: long pauses, then bursts of text. What happened and how do you fix it?

The output guard buffers each chunk, checks it, then releases it, so users see bursts, and TTFT grows by one chunk plus one check. If the guard is slower than the engine generates a chunk, checks queue and stalls grow over the response. Fixes: a smaller, faster guard on dedicated replicas; smaller chunks with the guard sized to keep up; run the input check in parallel with prefill; consider async checking with retraction for low-severity categories and blocking only for high-severity ones; and measure TTFT and ITL with the guard in the path.

seniorA team wants to deploy a new open-weight model from a public hub tomorrow. What does your platform require before it serves traffic?

Safetensors weights only, no trust_remote_code (or reviewed, vendored code); a pinned commit mirrored to an internal registry with recorded hashes; a scan for unsafe serialisation and a signature check; licence review; an AI-BOM entry; then the eval gate (task slices plus safety and refusal evals) against the current model, a load test, and a canary with quality proxies (length, refusals, parse failures) before full rollout. Gateway limits (token budgets, max_tokens caps) apply from day one.

See it on the concept map →
Token by Token · LLM inference & serving, visualized