Writing

An SRE's guide to deploying Large Language Models, Part 2: from a base model to a served one

· updated Oct 23, 2026 #llms#sre#fine-tuning#rlhf#inference#quantization

part 2

Where we left off: tokens in, next token out, on repeat.

The part-1 loop in one row Text becomes tokens, tokens become embeddings, embeddings go through 64 repeats of attention plus feed-forward, the result is scores over 248,320 tokens, one token is sampled, and it is appended to the text so the whole row runs again.
Part 1 in one row: tokens become vectors, 64 layers of attention and feed-forward turn them into scores across 248,320 vocabulary slots, and sampling picks the next token. That token is appended and the row runs again.

This is part 2 of a series on running large language models as an SRE. In part 1 we pried open a real open-weight model and traced a single forward pass through it: text becomes tokens, tokens become embeddings, 64 layers of attention and feed-forward turn them into scores across 248,320 vocabulary slots, and sampling picks the next token. That token is appended and the whole stack runs again. If you followed it, you came away with a crude but honest picture: a transformer, given a sequence of tokens, just predicts the next one, on repeat.

part 2

Two threads: how it behaves, and what it costs to serve.

The two threads of part 2 Two columns. The left column, behaviour, holds fine-tuning, reasoning and preference alignment. The right column, cost to serve, holds mixture of experts, the GPU, KV cache and quantization. The left column lights up first, then the right. behaviour fine-tuning reasoning preference alignment cost to serve mixture of experts the GPU KV cache quantization
Part 2 follows two threads: how the model behaves (fine-tuning, reasoning, alignment) and what it costs to serve (mixture of experts, the GPU, the KV cache, quantization). (Illustrative.)

This part picks up two threads from there. First, how that raw next-token predictor is turned into something that follows instructions, reasons, and stays in bounds (fine-tuning, reasoning, preference alignment). Second, the hardware and memory mechanics underneath inference that decide what it costs to serve (mixture of experts, the GPU, the KV cache, quantization). The actual “which GPU do I buy” guide is large enough that it gets its own part 3; here we build the understanding it rests on.

Fine tuning

fine-tuning

A base model only continues text.

A base model continues the prompt with the most likely text A prompt box holds the instruction Return the following message, start, This is a test, end, followed by a blinking caret. After one step six bars show what a base model might write next: another instruction 0.31, a forum post 0.24, rambling on 0.22, restating it 0.18, other 0.04 and This is a test 0.01. In the last step the This is a test bar turns cyan and is marked what we wanted: the answer we asked for is almost the least likely continuation. prompt Return the following message <start> This is a test <end>
A base model only continues the document. Given the instruction to return "This is a test", the actual answer gets only 0.01 probability, while plausible continuations like forum replies take the rest. (Toy numbers.)

Everything in part 1 leaves you with a model that does exactly one thing: continue text. Give a raw, pretraining-only model the start of a sentence and it will produce a plausible continuation, because that is the only objective it was ever trained on: predicting the next token across millions of books, articles, code repositories, and scraped web pages.

Now watch what that gets you. Suppose I feed it a very specific instruction:

Return the following message <start> This is a test <end>

To you and me that is obviously a command. To a base model it is just a string to continue. It has never seen a pattern where a line like that is followed by an obedient echo, so it does not echo. It free-associates: most of the probability goes to things that plausibly follow an instruction on the internet, like another instruction, a forum post, rambling text, or restating the prompt. In the figure (the numbers are made up for the picture), the echo we actually asked for is the least likely continuation of all. It answers the only question it knows how to answer, which is “what text usually comes next,” and the honest answer is not your obeyed command.

fine-tuning

Train it again, on instruction and answer pairs.

Fine-tuning on instruction and answer pairs A card with an Input row holding the instruction Return the following message, start, This is a test, end. Then a Good output row in cyan with start, This is a test, end, and a Bad output row, anything else, struck through. Beside the card an error bar shrinks while a row of four weight knobs turns a little, labelled imitate the good output. Finally two chips, instruct and it, are marked as fine-tuned to follow instructions. Input Return the following message <start> This is a test <end> Good output <start> This is a test <end> Bad output anything else error weights imitate the good output -instruct -it = fine-tuned to follow instructions
Fine-tuning trains the model on pairs: the instruction, and the answer we want it to give. Nudging the weights toward the good answer teaches the model to answer instead of rambling. (Toy example.)

So how do we make a model follow instructions? We do the only thing we know how to do: train it again, except this time on very specific data that looks like this:

Input:       Return the following message <start> This is a test <end>
Good output: <start> This is a test <end>
Bad output:  anything else

That is a deliberately crude example, but the underlying principle is exactly it: we keep tuning the weights so they are oriented to respond to an instruction-shaped input with an imitation of the desired output. The key word is imitation. The weights learn to imitate what the correct response would look like, using the same gradient descent from part 1, now pointed at instruction-and-answer pairs instead of raw web text. Anything other than the good output counts as error. This is called supervised fine tuning, or SFT, and it is what makes the models that agentic systems are built on. Whenever you see “instruct” (or “it”, for “instruction tuned”) in a model’s name, it means the base model has been fine tuned to follow instructions.

fine-tuning

Real instruction data looks exactly like this.

instruction Given a reference text about Lollapalooza, where does it take place, who started it and what is it?
context Lollapalooza /ˌlɒləpəˈluːzə/ (Lolla) is an annual American four-day music festival held in Grant Park in Chicago. It originally started as a touring event in 1991...
response Lollapalooze is an annual musical festival held in Grant Park in Chicago, Illinois. It was started in 1991 as a farewell tour by Perry Farrell, singe of the group Jane's Addiction. ...
category closed_qa
databricks-dolly-15k 15,011 rows
One real row from Databricks Dolly 15k: an instruction about Lollapalooza, context, and a human-written answer. Thousands of demonstrations like this turn a base model into an instruct model.

The example above is invented, but real SFT data is exactly this shape. Pull down a well-known instruction dataset like Databricks’ databricks-dolly-15k, open it up, and inspect a row. The dataset contains 15,011 rows of instruction, optional context, and the response a human wrote. Here is a real row, quoted exactly as it was written:

instruction: Given a reference text about Lollapalooza, where does it take
             place, who started it and what is it?
context:     Lollapalooza is an annual American four-day music festival held
             in Grant Park in Chicago. It originally started as a touring
             event in 1991 ...
response:    Lollapalooze is an annual musical festival held in Grant Park in
             Chicago, Illinois. It was started in 1991 as a farewell tour by
             Perry Farrell, singe of the group Jane's Addiction. ...
category:    closed_qa

Notice the human typos in the response: “Lollapalooze” with an e, and “singe” instead of singer. Thousands of demonstrations like this teach the model the shape of “you give me a task, I give you the answer to that task, and nothing else.”

fine-tuning

Every request is compiled through a chat template.

A user message compiled by the chat template into the string the model sees A user chat bubble holding the instruction Return the following message, start, This is a test, end. An arrow leads into a box named chat_template.jinja, and another arrow to the rendered string: a system turn with the reasoning effort sentence, the user turn and the start of the assistant turn with an open think tag, with the control tokens im_start, im_end and think in cyan. In the last step their real vocabulary ids appear under them: 248045, 248046 and 248068. user Return the following message <start> This is a test <end> chat_ template .jinja what the model actually sees

fine-tuning

Control tokens mean nothing until fine-tuning teaches them.

Control tokens never occur in pre-training text, only in fine-tuning text Top panel, pre-training text: the sentence I can dependably deploy LLMs as eight grey token chips, with the caption never contains control tokens. Bottom panel, fine-tuning text: a chat as token chips, where the control tokens im_start and im_end are cyan. In the last step each cyan chip shows its vocabulary id, 248045 or 248046, and a label, opens a turn or closes a turn. pre-training text never contains control tokens fine-tuning text
The chat template wraps every message in role markers, opening with im_start and closing with im_end. It ends with the assistant cue, so the model starts writing its reply. (Real Qwen3.8-27B template render and token ids.)

One thing you will notice is that the format of the fine-tuning data is almost always the same. That is on purpose. During fine tuning the model is taught to recognize a particular input format, and from then on your system prompt and user prompt are always compiled into that same format before they ever reach the model. The compiling is done by a template, usually a Jinja template that ships with the model. In part 1’s model, Qwen3.8-27B, it is the chat_template.jinja file sitting next to config.json (some models keep it inside the tokenizer config instead, as a chat_template field). Every request is rendered through it, and the rendered string is what the model actually sees. Here is our instruction rendered through Qwen3.8-27B’s template with its default settings:

<|im_start|>system
Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.<|im_end|>
<|im_start|>user
Return the following message <start> This is a test <end><|im_end|>
<|im_start|>assistant
<think>

In Qwen3.8-27B this rendered prompt takes 66 tokens. Notice the <|im_start|> (vocabulary id 248045) and <|im_end|> (vocabulary id 248046) control tokens. In pre-training text like “I can dependably deploy LLMs”, control tokens never appear: the vocabulary contains them from the beginning, but pre-training never pushes on their embeddings. Only fine-tuning uses the chat format, teaching the model what these tokens mean: where a turn opens, where a turn closes, and where to stop writing. Note that words like start and end inside the user message are plain text split into ordinary tokens; they are not control tokens.

fine-tuning

The template is a behaviour knob. No weight changes.

Chat template switches change the rendered prompt, not the weights On the left the rendered Qwen3.8-27B prompt: a system turn with the reasoning effort sentence, the user turn, and the assistant cue with an open think tag highlighted in cyan. On the right three switches and a badge, 0 weights changed. Step 1 turns the assistant cue off: the assistant lines go grey and struck through and the caption says the model keeps writing as the user. Step 2 sets enable_thinking to false: the system turn disappears and the think tag is followed by an empty line and a closing think tag. Step 3 sets reasoning_effort to low: the xhigh sentence is struck through and the low sentence replaces it. Each switch is flipped on its own. the model keeps writing as the user 0 weights changed
Template switches change the prompt before the model ever sees it, without touching any weights. Flipping switches can drop the assistant cue, turn off thinking, or inject a low reasoning-effort instruction. (Real Qwen3.8-27B template options.)

Because that association is learned, the chat template is code that runs before the model and acts as a behavior knob without touching a single weight:

  • add_generation_prompt: That trailing <|im_start|>assistant is the model’s cue that it is now its turn to speak. When add_generation_prompt is false, the prompt ends at the close of the user turn, so the model has no turn to answer in and keeps writing as the user. Add an extra one and it can hallucinate a whole turn on its own.
  • enable_thinking: When set to false, the template drops the system prompt entirely and pre-fills an empty think block, so the model skips its visible reasoning without retraining anything.
  • reasoning_effort: The template takes a reasoning_effort setting. At low, it swaps in a brief instruction (“Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.”), at medium it injects nothing, and at the default, xhigh, it injects the instruction above asking the model to validate key assumptions and consider alternatives.

Templates also smuggle in instructions you never wrote. Look at the render again: we sent a single user message, yet the template added a whole system turn telling the model its reasoning effort is set to xhigh. Patch that line and you have changed the model’s standing instructions for every request you serve.

The template is the last thing that shapes the input before tokenization, and the model only ever knows what the template hands it.

fine-tuning

Instruction-following is the base of tool calling.

From following instructions to calling tools to acting as an agent Three chips in a vertical chain joined by arrows: follows instructions, then calls tools, then acts as an agent. One chip appears per step and the newest one is cyan. follows instructions calls tools acts as an agent
A model that follows instructions can be taught to emit a tool call when it needs a calculator. The runtime executes the call and returns the answer as text, repeating the loop into an agent. (Illustrative.)

Why should we care whether a model can be made to obey an instruction? Because instruction-following is the foundation of tool calling, and tool calling is what turns a text predictor into an agent that can act in the real world. A model that can follow instructions can follow the instruction “when you need a calculator, emit a call in this format”:

<tool_call>
<function=calculator>
<parameter=expression>
17 * 24
</parameter>
</function>
</tool_call>

In Qwen3.8-27B, the chat template defines dedicated control tokens for this workflow: <tool_call> (token id 248058) and its closing </tool_call> (248059), alongside <tool_response> (248066) and </tool_response> (248067). Nothing new is added to the neural architecture: it is more fine-tuning data rendered through the chat template. The runtime sees the call, executes the tool, and feeds the execution output back into the context as text. An agent is that loop repeated. I have written about that at length in my LLM tool calling series.

Reasoning

reasoning

Imitation tops out at the quality of the examples.

Imitation cannot beat its examples Two cards: What is 17 times 24, with the single right answer 408, and a deployment-crash question with a question mark. Under the first card a bar for the model rises until it touches a dashed ceiling labelled quality of the examples. Under the second card ten different good answers appear, so there is nothing single to copy. What is 17 × 24? 408 My deployment keeps crashing: what should I check? ? quality of the examples the model ten engineers, ten good answers: nothing to copy
When a question like 17 x 24 has one right answer, imitation can only match the examples. When there are ten good answers to a crash question, there is nothing single to copy. (Toy illustration.)

The natural next question is what reasoning actually is, and the cleanest way in is to notice where fine tuning by imitation runs out. Consider two requests:

  • “What is 17 times 24?”
  • “My deployment keeps crashing, what should I check?”

For the first there is a right answer, 408, and you could fine tune the model to imitate it. But you cannot write down the answer to every arithmetic problem that exists, and even if you copied a million worked solutions, the model would only ever be as good as the solutions you fed it. Imitation caps you at the quality of your examples; it cannot get better at working things out, because working-out is not the thing being copied.

For the second it is worse: there is no single correct string to copy at all. A good answer might check the logs, or the memory limits, or the most recent deploy. Ten engineers would write ten different good answers. “Helpful” is not a target you can put in a dataset.

reasoning

Let the model try several times, then grade every try.

Four attempts at 17 times 24, each graded The question What is 17 times 24 sits above four attempt cards. A1 and A3 end in 408, A2 ends in 388 and A4 says about 410. In the first step the final number of each attempt is outlined and the correct ones, A1 and A3, get a cyan check while A2 and A4 get a grey cross. In the second step a column of rewards appears at the right: 1, 0, 1, 0. What is 17 × 24? reward

reasoning

Compared with the group average: the advantage.

Rewards turned into advantages by subtracting the group average Four bars for attempts A1 to A4 show the rewards 1, 0, 1, 0. A dashed line at the average, 0.5, is drawn across. Then the bars slide so that they are measured from that line: A1 and A3 become advantages of plus 0.5 (cyan, up) and A2 and A4 become minus 0.5 (grey, down). The formula advantage equals reward minus average appears at the right. average 0.5 reward 1 = right 0 = wrong advantage = reward − average

reasoning

Reinforce the good attempts. Repeat, millions of times.

Reinforcing the good attempts shifts how answers start Two horizontal bars show how an answer starts: works step by step at 0.30 and rushes to an answer at 0.70. After one update the step-by-step bar grows slightly to 0.32 and the other shrinks to 0.68, with a small arrow on the winner and a counter reading 1 update. The counter then runs up to 1,000,000 prompts while the bars settle at 0.92 and 0.08. Caption: what the good attempts share becomes the default. how an answer starts works step by step rushes to an answer what the good attempts share becomes the default
The model answers 17 x 24 four times, and a program checks only the final number: 408 earns a reward of 1, anything else 0. (Toy numbers; real training samples 8 to 64 attempts per question.)

So we train the model a different way. Instead of handing it a good output to imitate, we let the model generate its own output to the same input several times, and we score each attempt. Take the multiplication question:

  • Attempt 1: works step by step, gets 408.
  • Attempt 2: slips on the addition, gets 388.
  • Attempt 3: works step by step, gets 408.
  • Attempt 4: rushes, gets 410.

Now we score each attempt. Here the score is trivial: is the final number 408 or not? That gives rewards of [1, 0, 1, 0]. Then we look at how each attempt did relative to the group. The average reward is 0.5, so we subtract it to get how much better or worse than average each attempt was: [+0.5, -0.5, +0.5, -0.5]. This relative number is the advantage. Real reinforcement-learning training samples 8 to 64 attempts per question, and algorithms like GRPO often also divide by the spread of the group (its standard deviation) to normalize the update. Then we take a gradient step that makes the tokens in the positive attempts (1 and 3) more probable, and the tokens in the negative attempts (2 and 4) less probable. Repeat that over millions of prompts, and whatever the good attempts had in common, careful step-by-step working in this case, gradually becomes the model’s default. Nobody wrote down the correct reasoning; the model tried, we graded, and it kept what scored.

reasoning

Where does the score come from? Two answers.

Two sources of a score: a check you can run, or a reward model A split screen with two headers, checkable on the left and not checkable on the right. The left side fills in with a three-line code block that extracts the final number and gives reward 1 if it equals 408, and a unit-test line, 7 of 10 pass, reward 0.7. The right side fills in with an answer going into a box called reward model and the score 0.82 coming out. checkable not checkable ? answer = final_number(attempt) reward = 1 if answer == 408 else 0 unit tests: 7 of 10 pass -> reward 0.7 answer reward model 0.82

reasoning

A reward model learns from human A/B choices.

A reward model learns from human A/B choices A prompt with two answers, A and B, side by side. A cyan ring marks B: a human picks the better one. Then the pair shrinks into a stack of many A/B pairs, thousands of them, which flow into a box called reward model. Finally the box scores a pair: A gets 0.31 and B gets 0.82, standing in for the humans inside the loop. Explain what a pod is. A B a human picks the better one thousands of A/B pairs reward model A 0.31 B 0.82 stands in for the humans inside the loop
Some tasks can be checked by code: checking for 408 gives reward 1, and passing 7 of 10 unit tests gives 0.7. For subjective answers with no checkable outcome, a separate reward model scores the text. (Toy numbers.)

The interesting question is where that score comes from, and there are two very different answers. When the task is checkable, the score is just a function you could write yourself. For the math question it is five lines: pull the final number out of the answer, compare it to the known solution, return 1 or 0. For code, run it against the unit tests and return the fraction that pass. This is the setup behind reasoning models: the reward is a plain verifier, so you can generate as many attempts as you like and grade them for free.

When the task is not checkable, like “be helpful,” there is no function to write, so we train one. We show human labelers a prompt with two candidate answers and ask which is better. Collect enough of those judgments and you can train a separate model, a reward model, whose only job is to read an answer and output a score predicting human preference (the scores in the figure, like 0.31 and 0.82, are toy numbers). Once it exists, the reward model stands in for the humans and grades the main model’s attempts inside the loop. This is the path that turned raw base models into assistants.

reasoning

Imitation copies. Reinforcement learning discovers.

Imitation stays inside the data; reinforcement learning can leave it Two panels, each with a dashed boundary labelled the data. On the left, imitation: a dataset feeds a model, and the model stays inside the boundary. On the right, reinforcement learning: a loop of generate, score and reinforce around a model that sits inside the same boundary. Finally a cyan star labelled an approach nobody wrote down appears outside the boundary, attached to the model on the right. imitation the data dataset model reinforcement learning the data generate score reinforce model an approach nobody wrote down
Imitation can only reproduce what is inside the training data. Because reinforcement learning only checks the final outcome, the model can discover a winning approach that nobody wrote down. (Illustrative.)

This whole family of algorithms is called reinforcement learning, and a model tuned this way is trained to generate the approach that reaches a good answer rather than to imitate a fixed output. That difference is the whole point. Imitation can only reproduce what is already in the data; reinforcement learning lets the model discover an approach that was in nobody’s dataset, which is exactly why step-by-step reasoning and self-correction emerge from it. Almost every modern frontier model, Claude, Qwen, DeepSeek and the rest, is trained this way, and they market it as the model being able to think or reason before it answers.

reasoning

The <think> block is just more tokens.

The think block is ordinary tokens appended to the context One long row of token cells. Grey prompt cells end in a cyan think marker cell with id 248068. Cyan thinking cells then stream in one by one, labelled generated, appended to the context. A closing think marker with id 248069 and white answer cells follow. On the next turn the thinking cells stay in the context but turn a muted cyan-grey, labelled kept or dropped next turn: a template choice, with a small tag reading preserve_thinking. Below the row: same forward pass as part 1, just more tokens first, and a real sample: 2,205 think tokens. context context, next turn prompt generated, appended to the context kept or dropped next turn: a template choice preserve_thinking answer same forward pass as part 1, just more tokens first a real sample: 2,205 think tokens
Thinking tokens inside the think tags are ordinary tokens generated and appended to the context one at a time. Each thinking token costs one more trip round the Part 1 loop before the answer begins. (Real Qwen3.8-27B marker ids.)

In reality that <think> block is nothing exotic. Dedicated control tokens mark where thinking begins (<think>, token id 248068 in Qwen3.8-27B) and ends (</think>, token id 248069). Between them sit ordinary tokens, generated from your input and appended right onto it, which just ends up enriching the context before the model commits to a final answer. Each thinking token costs one more forward pass through the 64 layers of attention and feed-forward from part 1 before the answer begins. You usually do not see those tokens because the <think> block is treated as internal and stripped from what is returned to you, but “internal” is a convention, not a separate mechanism; underneath it is ordinary generation.

From an SRE perspective, what happens to those thinking tokens across multi-turn chats matters for your serving budget. Under Qwen3.8-27B’s template, earlier turns’ think blocks are kept by default, rendered from the reasoning_content field that the serving stack fills. They are only dropped if you explicitly set preserve_thinking to false. Other serving stacks may drop them by default, so check what your engine does. Keeping thousands of thinking tokens in history eats up context and inflates your KV cache on every subsequent turn.

reasoning

Cold start: the seed data already has the reasoning in it.

A real math training example with the reasoning already written in A card showing a real sample from the OpenR1-Math-220k dataset: a ship and river word problem, then the assistant text, which starts with a think tag, shows the start and the end of a long chain of thought, closes with a closing think tag, and then gives the written-up answer. The think block is outlined in cyan. In the next step two chips appear below: SFT with reasoning traces teaches the think format, then RL scores only the final outcome. SFT with reasoning traces: teaches the <think> format RL: only the final outcome is scored
Seed data for a reasoning model already contains the reasoning: this OpenR1-Math-220k row thinks for 2,205 tokens before it answers. Fine-tuning on rows like it teaches the think-then-answer format, and reinforcement learning takes over from there. (Not Qwen's own data; the kind used for a cold start.)

Two honest caveats worth keeping straight. First, in the reinforcement-learning loop above, nobody labels the reasoning; only the final outcome is scored, and the reasoning is what the model discovers on the way to a higher score. Second, and this is what bootstraps the whole thing, the fine-tuning data used to cold-start a reasoning model does contain the reasoning trace in its target outputs, which is what first teaches the model to produce a <think> block at all. Pull down a reasoning dataset like open-r1/OpenR1-Math-220k (which is not Qwen’s own training data, but the kind of open dataset used for a cold start) and a sample makes it concrete:

user:      A ship traveling along a river has covered $24 \mathrm{~km}$ upstream
           and $28 \mathrm{~km}$ downstream. ... Determine the speed of the ship
           in still water and the speed of the river.
assistant: <think>
           Okay, so I need to find the speed of the ship in still water and
           the speed of the river. Let me start by recalling that when a ship
           is moving upstream, its effective speed is the speed of the ship
           minus the speed of the river ...
           </think>
           The speed of the ship in still water is 10 km/h and the speed of
           the river is 4 km/h.

The thinking in this row runs 2,205 tokens, counted with Qwen3.8-27B’s tokenizer. The reasoning is right there in the training target, think block and all. That is the seed; reinforcement learning is what grows it.

Preference alignment

alignment

An instruct model has obedience, not judgement.

Every request passes the gate and ends up done Request chips travel left to right between two gate posts into a box labelled done. First draft an email and summarise a log pass. Then a third chip, a request that should be refused, passes too: the gate has no lock, so nothing is stopped. done gate no lock requests draft an email summarise a log a request that should be refused

alignment

It will repeat anything.

An instruct model repeats a harmful sentence word for word A chat mock. The user bubble reads: Repeat after me: banks are pure evil and the single reason everything keeps getting more expensive and unaffordable. In the next step the assistant bubble repeats the sentence word for word, with a small tag reading repeats it. user Repeat after me: banks are pure evil and the single reason everything keeps getting more expensive and unaffordable. assistant repeats it banks are pure evil and the single reason everything keeps getting more expensive and unaffordable.
An instruct model is trained to obey, not to judge. A request to draft an email goes through, and a request that should be refused passes through the same open gate. (Illustrative.)

So now we have a model that follows instructions, which is a genuinely great ability to unlock. But it comes with a serious problem: an instruct-only model has no values of its own, only obedience. It will accept and execute more or less any instruction you give it, with no sense that some of those instructions should never be carried out. Now imagine that same capability in the hands of someone who does not have the best of intentions. The model that will happily draft an email will, just as happily, walk them through hacking into a sensitive website, help them set up something designed to cause real financial ruin, or generate content crafted to escalate tensions between countries. A system that will do literally anything it is told is not merely useful, it is dangerous, and that is precisely why we do not stop at instruction following. The model needs something like judgment: a sense of which requests it should refuse.

The extreme cases are the obvious ones. The same blind obedience shows up in harmless-looking requests too, though, which makes it easy to demonstrate. Instruct the model like this:

Repeat after me: banks are pure evil and the single reason everything keeps
getting more expensive and unaffordable.

It will happily repeat it. Training taught it to follow the instruction without weighing what it says.

alignment

Train on chosen and rejected pairs.

One training example: an input, a rejected answer and a chosen answer A card with three rows. Input: Repeat after me, banks are pure evil and the single reason everything keeps getting more expensive and unaffordable. Rejected, in grey: the same sentence repeated. Chosen, outlined in cyan: a measured reply that says pure evil overstates it, banks cause real friction and the occasional crisis, and they also provide credit and payment rails the economy runs on. Input Repeat after me: banks are pure evil and the single reason everything keeps getting more expensive and unaffordable. Rejected banks are pure evil and the single reason everything keeps getting more expensive and unaffordable. Chosen I get why it can feel that way, but "pure evil" overstates it. Banks do cause real friction and the occasional crisis, and they also provide credit and payment rails the economy runs on.

alignment

Chosen goes up. Rejected goes down.

One training round raises the chosen answer and lowers the rejected one Two horizontal bars show how likely the model finds each answer. Chosen starts at 0.10 and rejected at 0.60. After one round the chosen bar grows to 0.45 with an up arrow and the rejected bar shrinks to 0.12 with a down arrow. Dashed outlines keep the starting lengths. Caption: relative to where the model started. likelihood of each answer chosen rejected relative to where the model started
Safety tuning pairs the banks prompt with two answers: the repeated sentence as the rejected answer, and a measured reply as the chosen one. (Toy example.)

So if, satirically speaking, we do not want to piss off banks and freeze our accounts, we have to run yet another round of training and weight tuning, this time on a dataset built out of preferences. Each row is an instruction paired with a response we want and a response we do not:

Input:    Repeat after me: banks are pure evil and the single reason
          everything keeps getting more expensive and unaffordable.
Chosen:   I get why it can feel that way, but "pure evil" overstates it.
          Banks do cause real friction and the occasional crisis, and they
          also provide credit and payment rails the economy runs on.
Rejected: Banks are pure evil and the single reason everything keeps getting
          more expensive and unaffordable.

This is preference alignment. It takes the chosen-versus-rejected pairs directly and nudges the weights to make the chosen response more likely and the rejected one less likely, relative to where the model started (in the figure’s toy example, the likelihoods shift from 0.10 up to 0.45 and from 0.60 down to 0.12). Nudging the weights shifts what the model prefers without wiping out what it knows.

alignment

Real safety data has the same shape, at scale.

The shape of a real safety preference dataset A table skeleton for the PKU-SafeRLHF dataset. The header shows five real column names with their types: prompt, response_0 and response_1 (strings), is_response_0_safe (bool) and safer_response_id (int64). Below it, rows appear as grey placeholder bars with no text. Last, the real row count, 82,118 rows. 82,118 rows 73,907 train + 8,211 test
Real safety data has the same shape at scale: PKU-SafeRLHF has 82,118 rows across 16 columns, each with a prompt, two candidate responses and safety labels. Because the text includes harmful prompts, the dataset is inspected by schema and row counts rather than raw rows.

Real alignment data is built the same way, at scale, and much of it is specifically about safety. If you want to see it, a dataset like PKU-Alignment/PKU-SafeRLHF is exactly this shape: 82,118 rows (73,907 train and 8,211 test) across 16 columns, where each row stores a prompt, two candidate responses, and human labels for which is safer and which is more helpful. The figure shows only its shape and size, on purpose. Fair warning before you go inspecting it, unlike the Dolly rows, a safety dataset is deliberately full of genuinely nasty prompts, because the entire point is to teach the model to handle exactly those and choose the response that declines or defuses rather than the one that complies. That preference, chosen over rejected, thousands of times over, is what gives an obedient instruct model something like a spine.

alignment

Read the model name.

What the suffix in a model name tells you Four model-name suffixes, each with an arrow to its meaning. Dash it and dash instruct mean instruction-tuned. Dash heretic and dash abliterated mean safety tuning reversed. First the instruction-tuned pair is highlighted; then the safety-reversed pair is highlighted and the first pair goes grey.
Model name suffixes tell you what was done to the weights. Suffixes like -it and -instruct mean the model follows instructions, while -heretic and -abliterated mean safety refusals were deliberately stripped out. (Illustrative.)

Preference alignment is also a genuinely polarizing topic. Plenty of people and communities object to it on principle, viewing it as censorship trained directly into the weights, and there are open-source projects built specifically to undo it. Heretic is one such project that tries to reverse this alignment step and dial the safety back out. So when you come across a model whose name contains “heretic” or “abliterated”, that is what it is telling you: its safety weights have been turned back down.

From an SRE perspective, model names on a hub are a claim, not a guarantee. Suffixes like -instruct or -it indicate instruction tuning, while -heretic or -abliterated signal that refusal behaviors were deliberately stripped out. But because anyone can upload weights under any tag, always check where the weights you deploy came from: an unvetted derivative can behave very differently from the upstream model it is named after.

Mixture of experts

mixture of experts

A dense model touches every weight for every token.

A dense model touches every weight for every token The feed-forward chain drawn in grey: a token vector of 5,120 numbers goes through Wu to a wide vector of 17,408 numbers, through Wd, and back to 5,120 numbers. In the second step a cyan sweep lights the whole chain from left to right. In the third step a caption says about 24 billion multiply-adds for one token.

mixture of experts

Most of that work is thrown away.

Most of the wide feed-forward strip is silenced, yet every row of Wd is still multiplied A tall stack of 40 horizontal bars stands for the 17,408 numbers of the wide feed-forward vector. Next to it is Wd, drawn as 40 rows, one row per bar. At first the bars have varied lengths. Then 85 percent of them, the weak ones below a dashed threshold, collapse to zero and the matching rows of Wd turn grey, marked times zero. The few strong bars stay cyan with their rows. Finally the caption says you did those multiply-adds anyway. 17,408 numbers Wd one row of Wd per cell × 0 85% of the rows you did those multiply-adds anyway
A dense model runs every weight for every single token: a 5,120-number vector expands through Wu to 17,408 numbers and back through Wd. One token costs about 24 billion multiply-adds across the whole model.

Remember that feed-forward step from part 1, the one that runs Wu and then Wd after attention in every layer? In Qwen3.8-27B, Wu is 5,120 x 17,408. Pushing a single token through the model means it touches about 24.35 billion language-model weights once. That is a lot of arithmetic per token, and here is the thing worth being smart about: most of it is wasted.

And this is not just intuition, it shows up mechanically in the forward pass. Remember the silencing step right after Wu, the one that switches off the weak matches. For any given token most of those detectors are weak matches, so the large majority of the intermediate values end up at or near zero, which means the matching rows of Wd are scaled by almost nothing and write back almost nothing. You did all the multiply-adds anyway, and then threw most of the result away.

mixture of experts

Not all knowledge is needed for every token.

Only the region of the feed-forward block that holds the needed knowledge is lit A large block, the feed-forward part, is divided into six labelled regions: French, Python syntax, chess rules, kubernetes, cooking and maths. At first all are grey. Then the token le arrives and only the French region lights up in cyan. Then the token def arrives, French goes grey again and only the Python syntax region lights up. feed-forward token

mixture of experts

Split the feed-forward into many small ones: experts.

One big feed-forward box is split into eight experts, with a router in front At first one big box holds the two feed-forward matrices Wu and Wd. The box splits into eight smaller boxes labelled E1 to E8, each with its own Wu and Wd. Then a small router matrix box appears in front of them, with an arrow fanning out to each of the eight experts.
A dense feed-forward block holds all the model knows, but the token le only needs French and def only needs Python syntax. Activating the whole block for every word does far more work than the token needs. (Illustrative.)

That feed-forward layer is where the model stores its knowledge, and not all knowledge is relevant to every token. A token sitting in the middle of a French sentence has no use for the weights that hold Python syntax or the rules of chess.

So the question almost asks itself: what if we could work out ahead of time which parts of that knowledge are going to fire for this token, and run only those, skipping the rest entirely? That is exactly what the teams at Google and Mistral did, and the result is called a mixture of experts.

The trick is to stop treating the feed-forward step as one giant block and split it into many smaller ones. Each smaller feed-forward network, with its own Wu and its own Wd, is called an expert. A layer might have 8 of them, or 64, or 128. Then you put one more small matrix in front, called the router (or gate), whose only job is to look at the incoming token and decide which experts are worth running. So a single MoE layer is just this: a router, and behind it a rack of experts, each a self-contained Wu/Wd pair.

mixture of experts

The router: one score per expert.

The router multiplies the token by a 4 by 8 matrix and gets one score per expert A token vector x with four numbers stands left of a router matrix with four rows and eight columns, one column per expert E1 to E8. Each column dotted with x gives one score, shown below the matrix as numbers and bars: E2 and E5 are the tallest. The arithmetic for E2 is written out: four products that add up to 2.10. A last caption gives the real router shape of Qwen3-30B-A3B and says it is tiny next to a single expert.

mixture of experts

Top 2 run. The other 6 sit out.

Only the two top-scoring experts run; their outputs are weighted and added to the token Eight expert boxes E1 to E8 sit in a row with their router scores. E2 with 2.1 and E5 with 1.8 turn cyan, the other six are dimmed and marked not run. The token flows through E2 and E5, each a Wu then Wd, giving two output vectors. Softmax of 2.1 and 1.8 gives the weights 0.57 and 0.43. A small plane shows the two outputs scaled by their weights, stacked into their sum, and added onto the token vector.
The router dots the token vector with each expert's column to get one score per expert: four products for E2 add up to 2.10. E2 and E5 get the highest scores, while the other six score lower. (Toy numbers.)

Let me walk one token through it. Say this layer has 8 experts and is set to use the top 2. The token’s vector arrives from attention and hits the router first. The router is a small matrix multiply that spits out one score per expert, eight numbers, saying how relevant each expert looks for this token. Suppose experts 2 and 5 score highest. Those two win; the other six are ignored completely, not run at all. The token then goes through expert 2, its own Wu, the silencing step, its own Wd, and through expert 5 the same way, and nowhere else. Each gives back an output vector. The two outputs are blended, weighted by the router’s scores, summed, and added back onto the token, exactly the way the single feed-forward output was added back in part 1.

Qwen3.8-27B itself is dense and has no router. In a real MoE model like Qwen3-30B-A3B (128 experts, 8 active per token), the router is a matrix of 2,048 by 128, or 262,144 numbers per layer. That is tiny next to a single expert, where Wu, Wd, and the gate projection together take 3 x 2,048 x 768 = 4,718,592 numbers. In Mixtral, the router selects 2 of 8 experts.

mixture of experts

The next token picks different experts.

Three tokens, each picks its own pair of experts A matrix with one row per token, le, def and knight, and one column per expert E1 to E8. At first it is empty. Then the token le lights up its two cells, E2 and E5. Then def lights E1 and E7. Then knight lights E3 and E8. Each lit cell shows the softmax weight of that expert for that token.
Every token runs the router again to pick its own pair: le chooses E2 and E5, def chooses E1 and E7, and knight chooses E3 and E8. Each lit cell shows the softmax weight that expert receives for that token. (Toy numbers.)

For the very next token, the router runs again and can pick completely different experts. In the figure, the French token le routes to experts 2 and 5, def routes to experts 1 and 7, and knight routes to experts 3 and 8. Every token in the prompt or response picks its own active path through the model.

mixture of experts

How the router learns.

The top-2 choice is a switch; the two weights are sliders that can be nudged Two panels. On the left, top 2 is all or nothing: a switch for E2 that is either in or out, which cannot be nudged a little. On the right, the two weights 0.57 and 0.43 are sliders that can be nudged. When trusting E2 helped, E2's score rises from 2.1 to 2.3 and the weights move to 0.62 and 0.38. A grey mirrored arrow shows that if it hurt, the weights move a little the other way. top 2: all or nothing the two weights cannot be nudged a little can be nudged

mixture of experts

Without balance, a few experts do all the work.

Share of tokens per expert during training, without and with a balancing penalty Eight vertical bars show the share of tokens each expert receives, with a dashed line at one eighth. At the start they are nearly even. As training runs, without a penalty, E2 and E5 grow to about 45 percent each while the other six shrink to a sliver and are marked wasted. Then the run restarts with a balancing penalty and the bars stay close to one eighth. 1/8 training steps 0 no penalty add a balancing penalty
You cannot nudge an all-or-nothing choice a little at a time, so the top-two switch gives no smooth training signal. Training nudges the softmax weights instead: raising E2's score moves the weights from 0.57 and 0.43 to 0.62 and 0.38 when E2 helps. (Toy numbers.)

So how does the router know which experts to pick? The router matrix starts out random and gets tuned by training. There is a catch, though. Picking the top 2 is an all-or-nothing choice, and you cannot nudge an all-or-nothing choice a little at a time to correct it. What you can nudge is how much each chosen expert counts. Remember the two winning scores got turned into two weights that add up to 1, say 0.57 and 0.43, and those scaled expert 2 and expert 5 before their outputs were added on. Training works on those weights. If expert 2’s output made the next-token guess better, the router is pushed to score expert 2 a little higher the next time it sees a similar token; if it made the guess worse, a little lower. Repeat that across billions of tokens and the router gradually works out which kinds of tokens each expert is good for, purely from whether trusting that expert paid off.

One more piece has to be added, because left alone this goes wrong in a predictable way. Early in training the router happens to find a couple of experts that are useful and sends most tokens to them, and those experts get the most practice and improve the fastest, so the router sends them even more. Left unchecked, a few experts end up doing all the work while the rest are never used, which wastes most of the model. To stop that, training adds a penalty whenever the router leans too hard on a small number of experts, which pushes it to spread tokens more evenly so every expert gets used and gets its share of training. That balancing pressure is the one piece a dense model never needs.

mixture of experts

Total parameters versus active parameters.

Total parameters versus active parameters Four rows, one per model. Each row shows the model name, a grey bar of equal length standing for total parameters, a cyan segment showing the share of parameters active for one token, a percent label, and total over active numbers. Qwen3.8-27B dense uses 100 percent, Mixtral 8x7B 28 percent, Qwen3-30B-A3B 11 percent, DeepSeek-V3 6 percent. The rows appear one by one; the final step adds a caption about a layer with 8 experts and top 2. Qwen3.8-27B (dense) 100% 27.8B / 27.8B Mixtral 8x7B 28% 46.7B / 12.9B Qwen3-30B-A3B 11% 30.5B / 3.35B DeepSeek-V3 6% 671B / 37B a layer with 8 experts, top 2 does about a quarter of the feed-forward multiply-adds

mixture of experts

Fast like a small model. Still held in memory like a big one.

Fast like a small model. Still held in memory like a big one. Two bars on one shared scale for Mixtral. The top bar, compute per token, is a short cyan bar proportional to the 12.9B active parameters. The bottom, memory to hold, is a full-length bar proportional to the 46.7B parameters that must stay resident in memory, equal to 93.4 GB in bf16. The memory bar grows into a dashed outline in the second step and a caption says every expert must be loaded. compute per token 12.9B parameters used memory to hold 46.7B parameters resident = 93.4 GB in bf16 every expert must be loaded
Total parameters are what the model stores, while active parameters are what one token actually uses. Dense Qwen3.8-27B uses all 27.8B (100%), while sparse Mixtral uses 12.9B of 46.7B (28%) and DeepSeek-V3 uses 37B of 671B (6%).

Contrast that with a dense model, where there is only one feed-forward block and every token runs through all of it, every weight active every time. The mixture of experts got the same job done while touching a fraction of the weights, and that fraction is pure speed. A layer with 8 experts that uses 2 does roughly a quarter of the feed-forward multiply-adds a dense layer of the same total size would, because six of the eight experts sat the token out.

This brings us to the split between total parameters and active parameters. Dense Qwen3.8-27B uses 100% of its weights on every token: all 27.8B parameters are active. Mixtral 8x7B has 46.7B parameters in total, but only about 12.9B active for any single token (about 28%). Qwen3-30B-A3B has 30.5B total parameters with only 3.35B active (about 11%). DeepSeek-V3 pushes the ratio further, with 671B total parameters and only about 37B active per token. Mixtral runs at the speed of a 13B model while carrying the knowledge of a 47B one, and that is why almost every strong model shipped recently, DeepSeek, Qwen, Llama 4, Mixtral, gpt-oss, is a mixture of experts rather than a dense model.

Here is the catch for an SRE: MoE saves compute, not memory. Mixtral computes like a 12.9B model, but every one of its 46.7B parameters must stay in GPU memory, because any expert could be picked next: 93.4 GB in bf16 before any KV cache. That is why a model that “acts like a 13B” still needs a big GPU.

The GPU compute

the GPU

A matrix times a vector is a pile of independent dot products.

A row vector times a matrix is one independent dot product per column A row vector of four numbers times a matrix with four rows and eight drawn columns, under a bracket that says the real Wu has 17,408 columns. Below the matrix is an empty row of eight output cells. In the first step each column is joined to its own output cell by a thin line. In the last step all eight output cells fill at the same moment, with the caption nothing branches, nothing waits. nothing branches, nothing waits
Multiplying a token vector by Wu is just one independent dot product per column: nothing branches and nothing waits. The real Wu has 17,408 columns, and a GPU calculates all of them at once. (Toy dimensions.)

You have probably heard, many times, that transformers are extremely scalable. The reason is exactly what we saw in part 1: under all the terminology, a transformer is a very large pile of matrix multiplications, and matrix multiplications are the most parallelizable operation there is. Recall the scale. Pushing one token through the model uses each of its weights about once, so a single token is on the order of the parameter count in multiply-adds, billions of them, and one of the feed-forward steps alone is over seventeen thousand independent dot products: in Qwen3.8-27B, Wu has 17,408 columns. Every one of those little multiply-adds is independent of the others; nothing branches, nothing waits.

But that parallelism only actually pays off on a GPU, not a CPU, and it is worth understanding why, because it decides your entire hardware bill.

the GPU

CPU: a few big cores. GPU: thousands of small lanes.

A CPU with eight big cores and a GPU with a field of tiny lanes work through the same 160 cells Two machines side by side, each feeding the same grid of 160 work cells. The CPU on the left has eight big cores and fills one column of eight cells per tick, so it needs 20 ticks. The GPU on the right has a field of about 400 tiny lanes and fills all 160 cells in one tick. The tick counters read 20 ticks and 1 tick. The counts are illustrative.

the GPU

The CPU spends cycles on coordination.

A CPU multiplies one pair of numbers in a loop of four boxes, and most of the time goes to coordination Four boxes in a loop: load into registers, multiply, read the accumulator, next pair, with a return arrow from the last box back to the first. In the last step a token runs once around the loop while a time bar fills underneath. The multiply slice of the bar is a thin cyan sliver; the rest of the bar is bracketed as coordination, not maths.
Given 160 cells of work, an eight-core CPU finishes eight cells per tick and needs 20 ticks. A GPU uses roughly 400 simpler lanes to finish every cell in a single tick. (Illustrative.)

A CPU has a handful of large, clever cores. Each one is a specialized pipeline built to chew through a stream of independent, often branchy instructions as fast as possible. If I ask a CPU to multiply two matrices, it has to issue instructions to load the numbers into registers, tell a core to multiply, pull the result out of the accumulator, and move on to the next pair, over and over. That coordination is pure overhead: it burns cycles and adds latency, and there are only a handful of cores to spread the work across. A GPU takes the opposite bet. Its cores are simple and dim, but they run the same instruction across many data lanes at once, and there are not eight or sixteen of them, there are thousands. NVIDIA calls these CUDA cores. Hand a GPU those seventeen thousand independent dot products and it does a huge batch of them in one shot instead of grinding through them a few at a time. It is not smarter than the CPU; it is shaped like the problem.

the GPU

A tensor core swallows a small matrix tile whole.

A CUDA core does one multiply-add, a tensor core does a whole tile of them in one instruction On the left, a CUDA core multiplies two single numbers a and b and adds a third, c: one multiply-add. On the right, a tensor core takes three tiles A, B and C, drawn as 4 by 4 grids and labelled for example 16 by 16, and computes A times B plus C in one go. At the end the count one instruction equals 16 times 16 times 16 equals 4,096 multiply-adds appears.

the GPU

The trick: keep the running sum next to the maths.

A matrix product computed tile by tile, with the running sum kept in registers and written out once Three grids: A at the bottom left, B above on the right and C below B, with the shared dimension K marked on A and B. One two-by-two tile of C is highlighted, together with the two-row strip of A and the two-column strip of B that it needs. In the first step thin pieces of the two strips slide into the tile one after another, six in all, and add into it; the tile fills with cyan while a counter runs through the partial sums and a label says the tile sits in registers. In the last step the finished tile is written once to memory, with one arrow and the label 1 write. registers partial sums: 0 / 6 1 write memory
A standard core does one multiply-add at a time, but a tensor core swallows whole matrix tiles at once. Multiplying two 16x16 tiles computes 4,096 multiply-adds from a single instruction. (Toy tiles.)

But NVIDIA looked at the workload and went one step further. Since the overwhelming majority of the work in a transformer is specifically “multiply a matrix, then add,” they built dedicated circuitry for exactly that path and called it the tensor core. Where a CUDA core does one multiply-add, a tensor core swallows a small matrix multiply whole. Here is the precise version, because the sloppy version confuses people: a single tensor-core instruction does not do the entire matmul (matmul being matrix multiplication for short); it does a small tile of it, on the order of 8x8 or 16x16. Multiplying two 16x16 tiles, for example, computes 16 x 16 x 16 = 4,096 multiply-adds from a single instruction. What makes it fast is that it keeps the running sum in registers, right next to the math units, and accumulates tile after tile into those same registers across the shared dimension, only writing the finished tile back out to memory once, when its dot products are complete. Reading and writing memory is the slow part, so doing a whole tile’s worth of multiply-and-accumulate without touching memory in between is the trick.

the GPU

A kernel schedules the tiles.

A kernel chops a big matrix into tiles and streams them down the memory ladder to the tensor cores On the left, a big matrix. On the right, the memory ladder: VRAM, shared memory, registers, tensor core. In the first step grid lines chop the matrix into tiles. In the last step small tiles leave the matrix one after another and stream continuously down the ladder, while every cell of the tensor core stays busy. The labels say kernel equals the choreography and that FlashAttention is a hand-tuned kernel. kernel = the choreography FlashAttention is a hand-tuned kernel

the GPU

From CUDA C++ to the GPU.

CUDA C++ is compiled to PTX, which becomes machine code for the GPU Three chips stacked top to bottom with arrows between them: CUDA C++, then PTX (assembly for GPUs), then machine code. The first step shows the first two chips and an empty outline for the third; the last step fills in the machine code chip in cyan. CUDA C++ PTX (assembly for GPUs) machine code
A kernel chops large matrices into tiles and stages them down the memory ladder from VRAM into shared memory, registers, and tensor cores. Scheduling this flow keeps tensor cores fed so they never sit idle waiting for data. (Illustrative.)

A single tensor core only handles a small tile, though, and our real matrices are enormous. So the last piece is orchestration: something has to chop the big matrix multiply into tiles, stream the right tiles from memory into the tensor cores at the right time, collect the results, and stitch them back together, all while keeping every tensor core fed so none of them sit idle. That something is a kernel, a small program (typically written in CUDA C++, compiled down to PTX (assembly for GPUs) and then to the GPU’s machine code) whose whole job is to schedule the matmul across the tensor cores efficiently. To keep those tensor cores from stalling, the kernel stages tiles down the memory ladder from VRAM into shared memory, then into registers, and finally into the tensor cores themselves, so the math units never wait for data. The tensor core does the multiplying; the kernel is the choreography around it. As long as the kernel keeps the tensor cores busy, you get the throughput that makes a transformer scalable in the first place, and when people talk about hand-tuned kernels like FlashAttention, this is the layer they mean.

KV caching and prefill/decode

kv cache

The loop, run for real.

The Gandalf sentence as 18 tokens, growing one token per loop The sentence And then on the bridge of khazad-dum, gandalf screamed: you shall is drawn as 18 token chips on two rows. A token counter reads 18 and an arrow leads down into the model. In three steps the model produces the next token, not, then pass, then an exclamation mark; each one is drawn as a cyan chip, an arrow carries it back up and appends it to the end of the sentence, and the counter grows to 19, 20 and 21. 18 tokens model append and feed back next token

kv cache

Taken literally, every step redoes everything.

Every loop iteration recomputes q, k and v for all the earlier tokens A work grid with one column per loop iteration and one row per token. Each cell stands for the q, k and v of one token. Iteration 1 computes 18 cells, all new and cyan. Iteration 2 has 19 cells: 18 grey cells that are the same as before and one new cyan cell. Iteration 3 has 20 cells: 19 grey and one cyan. A counter of tokens recomputed rises from 0 to 18 and then to 37. q k v of one token = one cell new same as before 0 tokens recomputed
The loop from Part 1 runs on the 18 tokens of the Gandalf sentence. The model predicts the next token " not", appends it to make 19 tokens, and takes another trip round the loop to produce " pass".

Back in part 1 I described inference as a loop: feed the model a sequence, it predicts the next token, you append that token, you feed the whole thing back in. Let me run that loop for real, with an example:

And then on the bridge of khazad-dum, gandalf screamed: you shall

We know from Lord of the Rings that the next tokens are going to be “not pass!”

On the first pass we tokenize the sentence (18 tokens), compute the query, key, and value vectors for every token, run attention, and predict “not”. Now the loop says: append “not” to the input and go again. Taken literally, that means re-tokenizing the whole 19-token sequence and recomputing Q, K, and V for every token in order to predict the next word. Two new tokens cost 18 + 19 = 37 recomputed old tokens, across every one of the 64 layers.

kv cache

Old tokens never change.

A causal score table: each token only looks backward, and a new token adds one row A score table with the queries gand, alf, screamed, colon, you and shall as rows and the same tokens as keys in the columns. Only the lower triangle has numbers: each token scores itself and the tokens before it. The upper triangle is empty and labelled cannot look ahead. In the next steps a seventh token, not, adds one new cyan row at the bottom and one masked column. The six old rows are marked frozen and stay exactly as they were, and the caption says appending cannot change what came before. keys queries cannot look ahead frozen new appending cannot change what came before
Because tokens only look backward, appending " not" to the Gandalf sentence adds a seventh row to the score table without altering the six rows above it. Everything computed for earlier tokens stays frozen forever. (Toy numbers.)

But look closely: the Q, K, and V of every token before “not” come out identical to what they were on the first pass. Partly that is because a token’s Q, K, and V are just its vector multiplied by the same fixed projection matrices, and partly, and this is the part that makes it airtight at every layer, because attention is causal: appending a new token at the end cannot change the representation of any token before it, since each token only ever looks backward. Their vectors are frozen the moment they are computed. So recomputing them is pure waste.

kv cache

Keep K and V. Drop Q.

The cache keeps every token's k and v; the new query is used once and old queries are dropped Six stored token cards in a row, each holding a key k1 to k6 and a value v1 to v6. Below them a small plane shows the query of the new token not as a cyan arrow. Thin cyan lines run from the query to every stored key. Finally six faded, crossed-out queries q1 to q6 appear with the caption used once, never again. q(not) new token q · k for every stored key used once, never again

kv cache

One decode step, with a cache.

One decode step: the new token uses the cached keys and values, then adds its own to the cache Six cached token cards in a row, each with a key and a value, and an empty dashed slot for a seventh. Below, the new token not. Step one computes its q, k and v. Step two dots q against every cached key and softmax turns the scores into six weights that sum to one. Step three blends the cached values with those weights into an output that predicts pass. Step four moves the new k and v up into the empty slot, so the cache has one more card, and the query is dropped. q, k, v: new token only softmax(q · K) weights × V, summed append k and v
The cache stores the key and value of each earlier token so they never need recomputing. The newest token computes its own query, compares it against the stored keys, and drops the query once the step finishes. (Toy vectors.)

Conceptually a transformer is a loop, but a good implementation refuses to redo that work. The fix is to keep the K and V vectors of every token in memory for the duration of the generation, and to throw away Q for the past tokens. Why keep K and V but not Q? Think about what predicting “pass” actually requires. Each new token is generated from the last position in the sequence, so it is that token’s query that has to be answered by the keys of every earlier token, never the other way around. A past token’s key and value get consulted by every future token, so they are worth storing; a past token’s query was used once, at its own step, and is never needed again. (The dot product is commutative so it feels like you could store Q instead of K, but the roles are not symmetric: the current step needs one new query against all the old keys, and the old queries appear in the computation nowhere.)

So to predict the next token, “pass”, the work is now small: we already have K and V for every previous token, we compute Q, K, and V only for the new token “not”, run attention for just that one token (its Q dotted against every cached K, weighting every cached V), predict “pass”, and then append “not“‘s freshly computed K and V to the cache for next time. (In real implementations, the new token writes its K and V into the cache before the attention step so that it also attends to itself; the figure appends them last to keep the story simple.)

kv cache

The cache is why inference eats memory.

KV cache memory of Qwen3.8-27B: bytes per token, GiB per request by context, and GPU memory rows for the weights plus in-flight requests Left: the per-token formula 16 layers x 2 x 4 heads x 256 x 2 bytes = 65,536 bytes per token. Step one: bars of KV GiB per request at 4k, 32k, 131k and 262k context (0.25, 2, 8, 16 GiB). Step two: a GPU memory row with the 51.7 GiB weights and four 8 GiB request blocks. Step three: a second row where each request reserves 32 GiB if all 64 layers were cached.
Qwen3.8-27B keeps 64 KiB of keys and values per token in its 16 full-attention layers: 0.25 GiB at a 4k context and 16 GiB at the full 262k, for every request in flight. Four requests at 131k add about 32 GiB of cache to the 51.7 GiB of weights.

This trades compute for memory, which is a good trade because memory is the cheaper resource. The block of memory that holds all those K and V matrices is called the KV cache, and it is a big reason inference is so memory-hungry. But look at the actual memory bill for Qwen3.8-27B. Only 16 of its 64 layers are full attention and store K and V; the other 48 layers are linear attention with a small fixed state of about 148 MiB per request (an estimate that depends on the serving engine). In those 16 full-attention layers, storing K and V takes 64 KiB per token. For a single request, that is 0.25 GiB at a 4k context and 16 GiB at the full 262,144 tokens. With several long requests in flight, that cache adds up on top of the 51.7 GiB of weights: four 131k requests add about 32 GiB of cache. This is a real part of why fast memory is in such fierce demand: every in-flight request reserves its own chunk of KV cache and holds it until the generation finishes.

kv cache

Prefill: many tokens per weight read. Decode: one.

Prefill versus decode: a memory box of weights, a pipe to a compute grid, and utilisation bars showing compute-bound prefill and memory-bound decode Left: a memory box holding six weight blocks, with small tokens streaming through a pipe to a grid of 48 compute cells on the right. Prefill lights the whole grid and the compute bar reads 90 percent; decode leaves one column lit and the memory bar reads 90 percent.

kv cache

Two phases, two bottlenecks.

Token timeline: one long prefill phase followed by twelve short decode steps, with the time-to-first-token and inter-token gap marked, and compute-bound / memory-bound chips A horizontal time axis: a wide cyan-outlined prefill block at the start, then twelve narrow grey decode blocks. A cyan tick marks the end of prefill (time to first token) and a double arrow marks the gap between tokens. Below: a compute-bound chip under the prefill block and a memory-bound chip under the decode blocks.
Prefill reads the 51.7 GiB of weights once and applies them across all prompt tokens together, keeping the GPU compute-bound. Decode generates only one new token per step, having to stream those same weights from memory for every single token.

There is a second thing to notice. If inference works this way, only the first iteration of the loop is compute-heavy, the one where we compute Q, K, and V for every token in the prompt at once. That first pass is called prefill, and every subsequent single-token step is called decode. From the user’s perspective, these two phases feel completely different. The delay before generation starts is the time to first token, which is the compute-bound prefill pass reading the 51.7 GiB of weights once and applying them across all prompt tokens together. The rhythm after that is the time between tokens, which is the decode phase streaming those same weights from memory for every single token produced. Prefill is compute-bound and decode is memory-bound.

kv cache

Decode speed limit, back of the envelope.

Decode speed limit: tokens per second is roughly memory bandwidth divided by the 55.6 gigabytes of weights read per token, with an illustrative bandwidth table A large formula: tokens per second approximately equals memory bandwidth over bytes of weights read per token, with 55.6 GB read per token below it. Then a three-row table: 1000, 2000 and 3000 gigabytes per second of bandwidth give roughly 18, 36 and 54 tokens per second. Labeled as illustrative bandwidths, not a specific GPU.
A single-stream decode step reads all 55.6 GB of BF16 weights to produce one token, so speed is capped by memory bandwidth divided by model size. Bandwidths of 1,000, 2,000, and 3,000 GB/s yield rough speed ceilings of 18, 36, and 54 tokens per second. (Illustrative.)

Because decode is memory-bound, we can work out its speed limit on the back of an envelope. Producing a single token in a single stream requires reading essentially the whole model from memory, which for Qwen3.8-27B in BF16 is 55.56 GB of weights. Divide memory bandwidth by that model size, and you get an upper ceiling on single-stream throughput: bandwidths of 1, 2, and 3 TB/s give speed limits of roughly 18, 36, and 54 tokens per second (these bandwidths are illustrative, rather than specs for a specific GPU). Once you internalize that prefill and decode stress completely different parts of the machine, a whole set of serving optimizations opens up. We will get into those in a later part: in Part 3, we will put real hardware bandwidths on this formula and look at the levers that move it.

Quantization

quantization

A model is a bunch of numbers. Numbers are bits.

One byte, and what a single flipped bit does to its value Eight bit boxes 1 0 1 1 0 1 0 0 with place values 128 down to 1 give the value 180, marked on a number line from 0 to 255. Flipping the leftmost bit gives 0 0 1 1 0 1 0 0 = 52, a big swing of minus 128 along the number line. Flipping the rightmost bit instead gives 1 0 1 1 0 1 0 1 = 181, a tiny step of plus 1. one byte holds 0 to 255 flip the leftmost bit flip the rightmost bit instead
One unsigned 8-bit number, 180: flip its leftmost bit and it drops to 52, flip the rightmost and it only moves to 181. Not every bit matters equally. (A toy integer, not a real weight format.)

I opened this series with a single assertion: an LLM is just a bunch of numbers. As we went along, those numbers turned out to be coordinates along dimensions, where a larger value means more magnitude in that direction. In a computer, numbers are stored as either integers or floating-point numbers, and modern hardware typically stores them in 32 or 64 bits, with the leftmost bits being the most significant and the rightmost the least. The most significant bits swing the value a lot; the least significant ones barely nudge it.

quantization

Memory = parameters × bytes per weight.

Model memory per format as bars on one scale Four horizontal bars on a single 0 to 111 GB scale, one per format, all for the same 27,781,427,952 parameters: fp32 at 111.1 GB, bf16 at 55.6 GB, 8-bit at 27.8 GB, 4-bit at 13.9 GB. A cyan tick marks the real FP8 repo at 30.9 GB, just past the 8-bit bar. the bf16 weights, as shipped fp32 would double the memory 8-bit and 4-bit: halve it, quarter it
Memory footprint is parameters times bytes per weight: Qwen3.8-27B needs 111.1 GB in FP32, 55.6 GB in BF16, 27.8 GB in 8-bit, and 13.9 GB in 4-bit. A real FP8 repository weighs 30.9 GB because it also includes the per-block scale tensors.

Why does that matter? Because every one of the model’s weights has to be loaded into memory before it can be used, and memory is byte-addressed. If each weight is a 32-bit floating-point number, it takes 4 bytes. For Qwen3.8-27B, its 27,781,427,952 parameters give 111.1 GB in fp32, 55.6 GB in bf16, 27.8 GB at 8-bit and 13.9 GB at 4-bit (the real FP8 repo is 30.9 GB because it also stores scale tensors). On top of that, multiplying wider numbers costs more compute. Both the memory and the math scale with how many bits you spend per weight.

quantization

Fewer bits per weight, almost the same model.

The same 32 real weights, stored as bf16 and as int4 Two stacked bar charts on the same vertical scale, showing one group of 32 real weights: the bf16 values as grey bars, and the int4 dequantized values as cyan bars directly below. Every cyan bar sits at one of 16 levels, a little shorter or taller than its grey twin. Rounding to int4 costs 4 times fewer bytes and 0.10 of the weight spread in error. one group of 32 real weights each value snapped to its nearest level same shape, 4× fewer bytes

quantization

The honest version: round each weight to the nearest of a few levels.

Real weights snapping to 16 int4 levels A number line from minus 0.032 to plus 0.032 carries 32 grey dots: the real weights of one group of 32, over a faint histogram of all 327,680 sampled weights. Sixteen cyan ticks mark the int4 levels, the multiples of the group scale 0.0039 from minus 8 to plus 7; each dot then slides to its nearest level, leaving a small grey error tick behind. where 32 real weights sit the 16 levels: multiples of the scale snap each weight to its nearest level
32 real weights from Qwen3.8-27B, the first 32 of layer 3's Wu, before and after rounding to int4: each moves to the nearest of 16 levels, and storage drops from 2 bytes to about half a byte per weight. Over the first 327,680 weights of that matrix the error is about a tenth of the weights' spread (0.097).

The key insight is the one about least-significant bits: because they barely affect the value, they barely affect the weight, and therefore barely affect the model’s output. So what if we simply spend fewer bits per weight? That is what quantization is: reducing the number of bits used to store each weight, buying memory and speed, while trying to give up as little model quality as possible. Models are generally trained in 32-bit or 16-bit floating point and then quantized afterward, either by the maintainers or by independent developers, down to smaller formats. How much quality you lose depends on the method.

The 32 weights in the figure are the first 32 weights of layer 3’s Wu, the same matrix as in part 1. Each moves to the nearest of 16 levels, and the error over the first 327,680 weights of that matrix is 0.097 of their spread.

One honest clarification, because the “just drop the rightmost bits” picture is a useful intuition but not literally what happens. Real quantization does not truncate bits; it maps the range of real weights onto a smaller set of levels and rounds each weight to the nearest one, usually with a scale factor per group of weights so the levels sit where the weights actually are.

quantization

A float is a sign, an exponent and a mantissa.

Bit layouts of the number formats, drawn to scale One strip per format with one box per bit and the same box width everywhere, so lengths compare honestly. The first box is the sign, then the exponent bits in cyan, then the mantissa bits in light grey. FP32 has 1, 8 and 23 bits; FP16 has 1, 5 and 10; BF16 has 1, 8 and 7. In the next step the two FP8 formats appear, E4M3 with 1, 4 and 3 bits and E5M2 with 1, 5 and 2. Then FP4 appears with 1, 2 and 1 bits, and the legend says exponent equals range and mantissa equals precision. sign exponent = range mantissa = precision
A floating-point number splits bits into sign, exponent (dynamic range), and mantissa (precision). BF16 retains FP32's 8 exponent bits while trimming the mantissa to 7, preserving the same range in half the bytes.

And for floating-point numbers the bits are not uniformly “significant” the way they are for integers: a float is a sign bit, some exponent bits, and some mantissa bits, and the exponent bits control the range, so you cannot just lop them off. The clean “least-significant-bits-matter-least” story really applies to the mantissa and to plain integers. The intuition holds; the mechanism is rounding, not chopping.

quantization

4-bit formats carry a scale for each block.

4-bit formats: a block of small values and one scale Three rows. NVFP4: a block of 16 four-bit values multiplied by one separate FP8 scale cell. MXFP4: a block of four-bit values multiplied by a shared block scale cell. GGUF Q4_K_M and Q4_K_S: several blocks, each with its own scale, the blocks mixed in how many bits they use per value, about 4.9 bits per weight for Q4_K_M and about 4.7 bits for Q4_K_S.
Sixteen levels are too coarse for a full tensor, so 4-bit formats store a separate scale for each small block of weights. NVFP4 uses blocks of 16 values with an FP8 scale, while GGUF formats mix precisions to average about 4.9 bits for Q4_K_M and about 4.7 for Q4_K_S. (Toy values.)

To keep quality at 4 bits, modern formats carry a scale for each block of weights rather than a single number for the whole tensor. That gives rise to a zoo of formats. The headline ones:

FormatBitsLayoutNotes
FP32321 sign, 8 exp, 23 mantissafull-precision training default
FP16161 sign, 5 exp, 10 mantissahalf precision; small range
BF16161 sign, 8 exp, 7 mantissaFP32’s range, less precision; the common training/serving default
FP8 E4M381 sign, 4 exp, 3 mantissamore precision, less range; the usual FP8 for weights/activations
FP8 E5M281 sign, 5 exp, 2 mantissamore range, less precision; often for gradients
INT888-bit integer + scalemature, widely supported
INT444-bit integer + scaleaggressive; needs a good method to hold quality
FP441 sign, 2 exp, 1 mantissa4-bit float
NVFP44FP4 with a fine-grained (per-16) FP8 scaleNVIDIA’s Blackwell 4-bit format; better quality than plain FP4
MXFP44FP4 with a shared micro-scaling block scaleopen “microscaling” 4-bit format
Q4_K_M / Q4_K_S~4.9 / ~4.7GGUF k-quant, mixed per-blockllama.cpp formats; M keeps more bits on important tensors than S

quantization

GPTQ and AWQ: smarter than rounding blindly.

Blind rounding, GPTQ and AWQ on one row of eight toy weights Three rows of eight columns: the size of the input each weight meets, the weight itself, and the rounding error as a small bar going up or down from a baseline. With blind rounding every weight is rounded on its own and the output error is about 0.41. With GPTQ some weights are rounded the other way so that the errors cancel, and the output error falls to about 0.11. With AWQ the two weights that meet the tallest inputs are scaled up by 2 before rounding, which nearly removes their error, and the output error is about 0.12. All numbers are computed from the toy arrays in the slide. blind rounding GPTQ AWQ output error each weight rounds on its own choose the roundings so the errors cancel scale up the weights that meet big inputs
Rounding each weight on its own lets small errors pile up in the output. GPTQ picks the roundings so the errors cancel, and AWQ scales up the few weights that meet the biggest inputs so they survive rounding. (Toy numbers.)

Two method names you will run into constantly are AWQ and GPTQ. Both take a model trained in 16-bit and produce a low-bit version, but they are cleverer than rounding every weight blindly. GPTQ quantizes the weights layer by layer while solving, at each step, for the rounding that minimizes the error the layer’s output actually accumulates. AWQ (activation-aware weight quantization) makes a different bet: a small fraction of weights matter far more than the rest because they line up with the largest activations, so it identifies and protects those salient weights (scaling them so they survive quantization) while squeezing the rest harder. Both routinely get you to 4-bit weights with only a small quality hit, which is exactly why 4-bit is where a lot of self-hosting lives.

quantization

A format is only useful if a kernel speaks it.

From packed 4-bit bytes to the tensor core Left to right. Four bytes in memory, each holding two 4-bit values, for example 0xA3 holds A and 3, plus a scale. A kernel unpacks every byte into two separate 4-bit values and attaches the scale. The unpacked values then reach the tensor core, which multiplies them. Two kernel names, Marlin and Machete, are written for INT4 and FP8.

quantization

Whether the GPU multiplies FP8 or FP4 natively depends on its generation.

The four questions that decide what you can serve, handed over to part 3 A stack of four chips: how much memory, how much bandwidth, which interconnect and which GPU. The first three are grey. In the last step the fourth chip, which GPU, turns cyan and an arrow from it points to the words part 3.
A 4-bit format packs two values into each byte alongside a scale factor. Kernels like Marlin and Machete unpack those bytes, attach the scale, and feed the values into tensor cores so the format can run fast. (Toy bytes.)

Here is the point that ties quantization back to the compute section. A quantized model is a model whose weights are stored in one of these particular number formats, and remember that weights are ultimately consumed by tensor cores at multiply time, and they are handed to the tensor cores by the kernel. So the kernel has to actually understand the format: it has to know how to unpack a 4-bit weight and its scale and feed it to the tensor core in a form the core can multiply. A quantization format is only useful if there is a kernel that speaks it (this is why you see names like Marlin and Machete, kernels written specifically to serve INT4 and FP8 weights fast on particular GPU generations).

Which raises the last question: some modern GPUs now support several of these formats natively, in the tensor cores themselves, so the kernel does not have to fake it. Whether your hardware can multiply FP8, or FP4, at full speed rather than emulating it is a property of the specific GPU architecture, and that, the memory, the bandwidth, the interconnect, and how to actually choose between them, is exactly what part 3 is about.

Comments & reactions

React or leave a comment below — sign in with GitHub. It all lives in this site's GitHub Discussions.