Runbooks/ LLM Inference Runbook Track A · From text to numbers Deep readQuick learn
  1. 00 Start
  2. /
  3. 01 Tokens
  4. 02 Anatomy
  5. 03 Journey
  6. /
  7. 04 Hardware
  8. 05 Phases
  9. 06 KV cache
  10. /
  11. 07 Attention
  12. 08 Position
  13. 09 Precision
  14. 10 Cache ops
  15. /
  16. 11 Many GPUs
  17. 12 Engines
  18. 13 Decode loop
  19. /
  20. 14 Planning
  21. 15 Production
Document 01 · Quick learn BPE · Measured, not estimated · The template that breaks silently

How a promptbecomes numbers

The model never sees your text. It sees a list of integers — and that list decides your bill, your context limit, your cache footprint and your latency, before any GPU has done a thing.

“Same nine thousand characters. Two thousand tokens of English. Six and a half thousand of base64.”
4.50 chars/token
English prose, measured
128,256
entries in the reference vocabulary
4.4×
Hindi vs the same English sentence
96%
of turn 8 already computed
The unit of everythingwhy tokenisation is upstream of every number

A model does not read text. It reads a list of integers — and every constraint you will ever argue about is measured in those integers, not in characters, words or megabytes.

The bill

Every hosted API charges per token, input and output separately.

Input and output are priced differently
The context limit

8,192 or 128,000 — tokens, not characters.

A prompt is rejected on this count before any work happens
The KV cache

128 KiB per token on the reference stack.

Tokens are literally the unit of memory per user
Prefill cost

Roughly 2 × parameters × tokens FLOPs.

Plus a term that grows with the square of the token count
The latency promise

Inter-token latency is per token by definition.

And throughput is quoted in tokens per second
The sentence to have ready

“Tokenisation is upstream of every number in the system. The same nine thousand characters is two thousand tokens of English prose and six and a half thousand of base64 — same text, three times the cost, three times the cache, three times the prefill. So before I tune anything I want to know what the traffic actually is.”

The analogy — a shorthand typist

Someone who has learned that certain letter sequences recur so often they deserve a single stroke. -tion gets one mark; the gets one mark; an unfamiliar surname has to be spelled out letter by letter.

Fast on the material it was trained on, clumsy on everything else — and it has no idea what any of the words mean. That is byte-pair encoding exactly
Building the vocabularybyte-pair encoding, six steps

Count every adjacent pair of symbols in a corpus, merge the most frequent one into a new symbol, repeat. Do it 128,000 times and you have a 128,000-entry vocabulary.

1 · Start

One symbol per character, plus an end-of-word marker.

2 · Count pairs

Across the corpus, weighted by word frequency.

3 · Merge the winner

(e,s) becomes es.

4 · Recount

(es,t) → est, then est_.

5 · Again

(l,o) → lo → low — a whole word.

6 · Stop

The ordered merge list is the vocabulary.

Encoding replays exactly these merges, in exactly this rank order

The vocabularies you will meet

r50k_base / GPT-2

50,257 entries. GPT-2, GPT-3.

Byte-level BPE — the design everything since has copied
SentencePiece BPE

32,000 entries. Llama 2, Mistral, many 2023 open models.

Noticeably inefficient on code and non-Latin scripts
cl100k_base

100,277 entries. GPT-3.5, GPT-4, the embedding models.

Added code and whitespace symbols; caps digit runs at three
Llama 3 tiktoken BPE

128,256 entries. Llama 3, 3.1, 3.2, 3.3.

The reference stack. Four times Llama 2’s vocabulary
Qwen / Gemma class

~150k–260k entries. Recent multilingual models.

Large vocabularies bought to cut the non-English penalty
Encodingwhat actually happens to your string

Encoding is not “look the word up”. It is two stages, and the first one is the reason the results look strange.

1 · Raw bytes

Sixteen characters. The leading spaces are content.

2 · Pre-split

A regex cuts words, whitespace runs, digits, punctuation.

3 · Replay merges

Within each piece only — never across a boundary.

4 · Map to ids

A list of integers. The only thing the GPU receives.

5 · Embedding lookup

Each id is a row in a 128,256 × 4,096 matrix.

Why the pre-tokenising regex exists

Without it, BPE would happily learn a single symbol for . The or dog jumped, and the vocabulary would fill with punctuation-and-word combinations that generalise badly. With it, words stay words.

The price: the split is now an arbitrary hand-written rule, and it is where most of the surprises live
The trap — the leading space belongs to the token

hello and hello are two different tokens with two different ids. Both are one token, so the count does not change, but the model sees a different symbol.

A prompt ending in a trailing space forces the next word into the rarer no-leading-space branch. “The prompt broke after I tidied the whitespace” is usually this
What surprises peoplemeasured with a real tokeniser, not illustrated

Characters per token — higher is cheaper. Everything here was produced by running an actual tokeniser over actual samples.

EN
English prose1.20 tokens per word — the rule of thumb holds
4.50
{}
JSON payloads6.33 tokens per word; keys re-sent every time
3.39
PY
Python source2.25 tokens per word — ~40% more tokens per character
3.17
ID
UUIDs and hex ids21.5 tokens per word. One UUID is ~20 tokens
1.70
AR
Arabic prose3.0× the tokens of the same English sentence
1.40
64
base64 blobs44.0 tokens per word. Near worst case
1.36
HI
Hindi prose4.4× the tokens of the same English sentence
0.91
JA
Japanese prose3.0× English, and far fewer characters too
0.67
The architect-level consequence

These multipliers are a capacity input, not trivia. If a quarter of your traffic is Hindi, your average tokens-per-request is not the English figure, your p95 prompt length is not the English figure, and a context limit set from English testing will reject Hindi requests that say the same thing.

Measure tokens per request on real traffic, segmented by language and content type, before you size anything
Digits are capped at three, on purpose

2024 is two tokens. 1,000,000 is five. The cap makes numeric representations regular, so 1997 and 1998 share structure instead of being unrelated symbols.

It helps arithmetic overall even though it costs tokens — and it is part of why prompting a model to work digit by digit helps
The chat templatethe failure that throws no error at all

A base model continues text. A chat model was trained on text with a very specific shape — role headers, turn boundaries, an end-of-turn marker — and it only behaves like a chat model when given that exact shape.

1 · Messages

The application sends a list of role/content pairs.

2 · Render

A Jinja template in tokenizer_config.json flattens it.

3 · Reserved ids

The markers are single tokens above 128,000 — not text.

4 · The failure

Wrong template, or applied twice. Markers become ordinary text.

5 · The defence

Server-side rendering, logged once, plus one canary test.

The special tokens, and what breaks without each

<|begin_of_text|>

Marks the start of a sequence.

Usually minor — but double-adding it is the common bug
<|eot_id|>

Ends a turn. The server stops generating here.

Without it the model never stops — it writes the user’s next message too
<|start_header_id|>

Opens a role header.

Roles blur. The model stops distinguishing system from user — a prompt-injection hole
The generation prompt

The trailing assistant header saying it is now its turn.

Omit it and the model continues the user’s message instead of replying
Reserved / unused ids

Slots kept free for tool calls, thinking blocks.

Nothing goes wrong — until someone fine-tunes onto them
Double-templating — the bug that survives review

Your gateway applies the chat template to build a string. Then it posts that string to /v1/chat/completions as the content of a user message — and the engine applies the template again. You now have header markers nested inside header markers. The model still answers, so tests pass; it just answers worse.

The one-line detector: log the rendered prompt for one request at startup and count the begin-of-text tokens. Not exactly one = this bug
Tool calling is the same mechanism

“Function calling” is not a separate capability. The schemas are rendered into the prompt by the same template, the model emits a specially-shaped block of text, and the server parses it back out.

Every tool definition is tokens you pay for on every request. Twenty tools at 80 tokens each is 1,600 tokens — 0.2 GiB of cache and 20% of an 8k window, permanently
Coming back outdetokenisation, and the corruption nobody logs

Turning ids back into text sounds like the easy direction. It is the one that produces user-visible corruption if you do it naively.

One character, two tokens

A grinning face is four UTF-8 bytes: three in the first token, one in the second.

Naive decode

Three bytes alone are not valid UTF-8. You emit � or drop the byte.

Correct approach

Buffer bytes; emit only what decodes cleanly; hold the incomplete sequence.

Same shape elsewhere

Stop strings across boundaries, partial words, any Japanese, Arabic or Devanagari.

Stop strings have the same problem

You configure the stop string \n\nUser:. The model emits a token containing \n\nUs and then one containing er:. Neither matches, but the concatenation does. So matching has to run on the accumulated text, not per token — and once it matches, the server has to retract characters it already streamed.

Engines handle this. Home-grown wrappers frequently do not
The same tokens, over and overthe property that pays for prefix caching

In almost every application, most of the tokens in a request were also in the previous request. Reuse is a property of the token sequence, and it is exact-prefix-only.

Chat, multi-turn

System prompt plus the whole conversation so far.

Grows to 90%+ by turn 6–8
Agent loop

Tool schemas, instructions, scratchpad — resent every step.

Very high, and the step count multiplies it
Few-shot classification

The examples; only the item under test changes.

Often 95%+
Coding assistant

Open files and project context across many questions.

High within a session, near zero across sessions
RAG question answering

The system prompt only — passages differ per query.

5–15%. The one workload where it barely helps
Worked8-turn chat, 50-token system prompt → turn 8 is 234 prompt tokens, 225 already computed = 96% reusablemeasured
Why one character matters

Change one character near the start of the system prompt and every downstream token shifts, so the entire cache entry is invalidated. That is why engines hash blocks of token ids rather than comparing text.

Stable content first, variable content last. Never interpolate a timestamp or request id into the header
The tokeniser and the model are married

Token id 1,234 means whatever row 1,234 of the embedding matrix learned it to mean. Swap the tokeniser and every id points at the wrong row.

There is no partial compatibility and no graceful degradation — you get fluent nonsense
Answer it out loudthe ninety-second version, then eight questions
“Text becomes tokens by byte-pair encoding: the vocabulary is a list of merges learned by repeatedly combining the most frequent adjacent pair in a corpus, so common sequences become one symbol and rare ones stay split. It starts from bytes, so nothing is out of vocabulary. The token is the unit of the bill, the context limit, the KV cache and the prefill cost — so content type matters enormously: English prose is 4.5 characters a token, base64 is 1.4, and the same sentence in Hindi is 4.4 times the tokens of the English one. The one thing that breaks silently is the chat template: get it wrong, or apply it twice, and the model answers worse with no error and no metric change.”

Eight questions, and the spine of each answer

Architect

What is a token, and why should I care?

  • An entry in a learned vocabulary of byte sequences — a common word, a fragment, a run of whitespace
  • BPE merges the most frequent adjacent pair, repeatedly. Starts from raw bytes, so nothing is ever out of vocabulary
  • The bill, the context limit, the cache (128 KiB/token) and prefill cost are all per token
Land on: the same nine thousand characters is 2,000 tokens of English or 6,600 of base64.
Architect

Our costs are higher than we modelled. Where would you look first?

  • The token distribution of real traffic, segmented — before touching the serving stack
  • Three things blow it up: non-English (Hindi 4.4×), structured payloads (JSON, UUIDs, base64), and tool schemas resent every request
  • Fixes in order: strip machine-generated content, move variables to the end, prune the tool list per route
Quantisation and hardware come after that — none of it helps if you are paying for tokens you did not need to send.
Eng manager

A team reports the model “got worse”, but no metric moved.

  • First hypothesis: the chat template — the one failure here with no error, no log line, no metric change
  • The check takes minutes: log the rendered prompt and count begin-of-text markers. Not one = the bug
  • Durable fix: use the engine’s chat endpoint, not hand-built strings
And one canary test in CI whose correct answer depends on the system message being respected — that turns an invisible failure into a red build.
Architect

Why can a model not count letters, or do arithmetic reliably?

  • It never sees letters or numbers — it sees vocabulary entries
  • A word may be two or three tokens with no letter-level structure exposed at all
  • Digit runs are capped at three, so 2024 is the pieces “202” and “4”
That cap is a deliberate trade for regularity. It is also why working digit by digit helps — and why anything that must be exact calls a tool.
Architect

How does tokenisation interact with prefix caching?

  • Exact runs of token ids, matched block by block — typically sixteen tokens
  • All-or-nothing at a block boundary, and sensitive to anything that shifts the sequence
  • A timestamp at the top of the system prompt invalidates every block after it, every time
Measured: turn 8 of a chat is 234 prompt tokens of which 225 were already computed — 96%. The rule is one line: stable first, variable last.
Eng manager

We are switching model families. What is the tokenisation cost?

  • Bigger than expected, and mostly re-measurement rather than re-engineering
  • Every token-denominated number is invalidated: cost per request, p50/p95 prompt length, cache per user, the capacity model
  • Hard couplings: any speculative draft model must share the tokeniser; stop strings and hand-built templates must be rewritten
Budget: a day to re-measure distributions on real traffic, a week of prompt regression, and a rebuild of the capacity model.
Architect

What is the risk in letting users send a raw prompt string?

  • They can type the special tokens
  • A user can write the end-of-turn marker then a fresh system header and impersonate the system role
  • At the token level that is indistinguishable from a genuine system message
Defence: tokenisers can treat special-token text as ordinary text, and every serious server does. But the principle is accept structured messages, not rendered strings.
Eng manager

Throughput is below target and GPU utilisation is only 55%.

  • Point them at the CPU, not the GPU
  • Tokenising, templating, detokenising, stop-string matching and stream formatting are all CPU work
  • The last four run once per generated token per user — at 40 req/s × 300 tokens that is 12,000 times a second
Ask for a CPU profile of the server process first. Buying a second GPU to fix a saturated CPU core is an expensive way to learn this.
Fast answersthe eleven that come up most

QHow many tokens is a word, really?

For English prose, measured: 1.20 tokens per word and 4.50 characters per token. The rule of thumb is accurate for that case and wrong by 2–5× for code, JSON, identifiers and every non-Latin script. Use it for a sanity check, never for a budget.

QWhy does the model sometimes produce a broken character mid-stream?

A single character can span two tokens and the server decoded one alone. A grinning face is four UTF-8 bytes split three-and-one; the first three are not valid UTF-8 by themselves. Correct servers buffer bytes and emit only what decodes cleanly.

QIs a larger vocabulary better?

Usually, on the text it was designed for, and not uniformly. Measured on the same samples, 50k → 100k saved 4% on English prose and 31% on Python — but cost 14% more on numeric text, because the newer vocabulary caps digit runs at three. And the embedding matrix grows with it.

QDoes the tokeniser run on the GPU?

No. Tokenising, chat templating, detokenising, stop-string matching and stream formatting are all CPU work. Usually negligible, occasionally the actual bottleneck — the detokenise-and-stream path runs once per output token per user.

QCan I add my own tokens for domain terms?

Mechanically yes, practically rarely. New tokens mean new rows in the embedding matrix and output layer, and those rows start random — the model produces garbage on exactly the tokens you added until you train them. It is a training project.

QWhy is the first token strange when I hand-build prompts?

Usually a missing generation prompt — the trailing assistant header. Without it the model may continue the user’s message rather than reply to it. Second most common: a trailing space forcing the no-leading-space branch.

QDo input and output tokens cost the same?

Not on your own hardware, and not on most APIs. Input is one parallel compute-bound pass; each output token is a full memory-bandwidth-bound pass through the model. Hosted prices are typically three to five times higher for output.

QWhat is the difference between a tokeniser and an embedding?

The tokeniser maps text to integers using a fixed table with no learned meaning — id 262 is just row 262. The embedding maps that integer to 4,096 learned numbers. One is a data file running on CPU in microseconds; the other is part of the model weights.

QWhy do models struggle with the middle of long documents?

Not tokenisation — that is exact and lossless. Middle-of-context weakness is a position and attention effect (document 08). What tokenisation contributes is that your document is longer in tokens than you think, especially with tables, code or non-English text.

QShould I strip whitespace and punctuation to save tokens?

Almost never. Removing the space from “hello world” does not save a token — measured, both are two, and the joined version splits worse. Aggressive stripping pushes text off the frequent paths. The real savings are structural.

QWhat is the single thing to check on day one?

Print the fully rendered prompt for one real request and read it with your own eyes. Count the begin-of-text tokens — exactly one. Confirm the role headers match the documented template. Confirm the generation prompt is present.

Anything that changes how many tokens your text becomes changes your bill, your capacity, your latency and whether the request is accepted at all. The same nine thousand characters is 2,000 tokens of English prose and 6,600 of base64 — and the one failure in this whole layer that produces no error, no log line and no metric change is the chat template.

English 4.50
JSON 3.39
Python 3.17
UUID 1.70
base64 1.36
Where this goes next

Questions to ask them

What is your p50 and p95 prompt length in tokens, on real traffic?

Is that segmented by language and content type, or one average?

Does the application build the prompt string itself, or use the server’s chat endpoint?

Is there a test that would fail if the chat template were wrong?

How much of a typical prompt is tool schemas and instructions that never change?

Has anyone profiled the CPU side of the server, or only the GPU?

← 00 · Start Read the full chapter → 02 · Anatomy →