Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
The previous lesson traced a Transformer from token IDs all the way to unnormalized vocabulary scores. Now an editor prompt ends with The function returns the. How does text turn into a probability distribution over what comes next, and what mechanics govern the loop after the model picks a continuation?
A token is the atomic unit of text in the model's vocabulary. Tokenizers frequently split long words or prefix tokens with spaces, but our running example treats each visible word and punctuation mark as a distinct token. If the model chooses result, the conditioning context grows to The function returns the result. The updated distribution might next favor a period. This append-and-predict cycle is autoregressive generation.

Next-token scores, not a whole answer
An autoregressive language model doesn't emit completed answers or prewritten paragraphs in a single computational leap. Instead, it assigns a categorical probability distribution over the vocabulary for the single token that follows the prefix seen so far. A GPT-style large language model (LLM) executes this objective at immense scale.[1][2] Its output at each step is a vector of vocabulary scores, not a fixed sentence.
Suppose a model reads this four-token prompt:
"The function returns the"
The model's actual probabilities depend on its tokenizer, parameter weights, and earlier conversation context. In a simplified vocabulary slice, it might rank candidate continuations like this:
Next-token distributions after a prompt
| Candidate token | Probability | Sampling behavior |
|---|---|---|
result | 60% | Greedy argmax picks this directly; sampling makes it the most frequent choice. |
value | 25% | Plausible alternative continuation; frequently chosen under stochastic sampling. |
index | 10% | Lower-ranked candidate; selected occasionally when temperature is elevated. |
| Other tokens combined | 5% | The remaining vocabulary mass distributed across thousands of long-tail tokens. |
These values are didactic numbers rather than outputs from a specific checkpoint. The final row combines all omitted vocabulary entries so the total probability mass sums to exactly 100%.
Reading the distribution before sampling
The table above doesn't represent static facts stored in a database row. It represents the model's conditional belief given the prompt so far. Add even a single token to the input, and the model's conditional distribution shifts immediately.
| Prompt context | Top continuation | Why the prediction shifts |
|---|---|---|
The function returns the | result | Standard completion in code documentation corpora. |
The function returns the cached | value | The adjective cached shifts semantic expectation toward stored objects. |
The function returns the first | index | The ordinal modifier changes the likely noun to an offset or index. |
The function returns the (REST API doc context) | response | System instructions and surrounding context reshape the token ranking. |
The network doesn't memorize one rigid answer for a phrase. Context vectors modulate activations at every layer, yielding a distinct output distribution for every prefix.
Probability theory explains why this local next-token choice suffices to score entire documents. Under the chain rule of probability, the joint probability of any sequence factorizes exactly into a product of conditional probabilities:
Here represents the token at position , is total sequence length, and denotes the history of all preceding tokens. The first term is simply , conditioned on no prior text. When scoring continuations after a user prompt, each factor conditions on that prompt as well.
This factorization is an exact mathematical equality, not an approximation. It requires no independence assumptions. The language modeling objective tasks the network with approximating each individual conditional term .
Consider guessing continuations for this test runner log:
1The test runner says the assertionMultiple candidates compete for probability mass:
1passed
2failed
3holdsAll three candidates are grammatically and contextually sound. A language model doesn't require absolute certainty to generate helpful text. It needs calibrated relative probabilities across its vocabulary, leaving the final choice to a decoding algorithm.
Softmax probabilities from raw logits
Real tokenizers span tens of thousands of tokens, typically between 32,000 and 128,000 subwords. To trace the mathematics without clutter, shrink the vocabulary to five candidate tokens.
Take this prompt:
The compiler reported
The network projects its final hidden state through an unembedding matrix to produce logits, which are unconstrained real-valued scores:
| Token | Logit () | Semantic role |
|---|---|---|
error | 3.0 | Dominant continuation in compilation logs. |
warning | 1.5 | Plausible diagnostic outcome. |
success | -1.0 | Grammatically viable, but uncommon after reported. |
the | -2.0 | Grammatically unnatural immediately after reported. |
</s> | -3.0 | Premature sequence termination. |
The softmax operator converts these real-valued logits into normalized probabilities:
Subtracting is a numerical stabilization safeguard. As established in the softmax optimization lesson, subtracting the maximum logit keeps the largest exponent at , preventing floating-point overflow while preserving exact probability ratios.
1import math
2
3tokens = ["error", "warning", "success", "the", "</s>"]
4logits = [3.0, 1.5, -1.0, -2.0, -3.0]
5
6peak = max(logits)
7exps = [math.exp(logit - peak) for logit in logits]
8total = sum(exps)
9probs = [exp / total for exp in exps]
10
11for token, prob in zip(tokens, probs):
12 print(f"{token:10s} {prob:.3f}")
13print(f"sum {sum(probs):.3f}")
14print("top token ", tokens[max(range(len(probs)), key=lambda i: probs[i])])1error 0.800
2warning 0.178
3success 0.015
4the 0.005
5</s> 0.002
6sum 1.000
7top token errorerror commands 80% of the probability mass, followed by warning at roughly 18%. The probabilities sum cleanly to 1.0. Softmax guarantees valid probability math, but it can't tell whether a compiler actually failed. It only normalizes the model's internal scores. The next question is how a model acquires those scores from raw text.
Estimating probabilities from observed n-gram counts
Before neural networks dominated natural language processing, language models estimated sequence probabilities by counting token co-occurrences in text corpora.
A bigram model approximates the full conditional history using only the single immediately preceding token . This simplification is the first-order Markov assumption:
Under Maximum Likelihood Estimation (MLE), the bigram transition probability is the ratio of pair frequency to previous-token frequency:
Here counts how many times the adjacent pair appears, while counts total occurrences of the conditioning token.
| Previous token () | Next token candidate () | Observed count | Bigram probability |
|---|---|---|---|
returns | value | 3 | |
returns | result | 2 | |
returns | error | 1 |
Counting bigram events in code comments
We can verify these calculations directly on a toy corpus of six code comments. <bos> and <eos> act as explicit sequence delimiters, enabling the model to learn when continuations start and stop.
1from collections import Counter, defaultdict
2
3messages = [
4 "function returns value",
5 "function returns value",
6 "method returns result",
7 "handler returns value",
8 "test returns result",
9 "test returns error",
10]
11
12counts = defaultdict(Counter)
13for message in messages:
14 tokens = ["<bos>", *message.split(), "<eos>"]
15 for previous, current in zip(tokens, tokens[1:]):
16 counts[previous][current] += 1
17
18row = counts["returns"]
19total = sum(row.values())
20for token, count in row.most_common():
21 print(f"P({token:6s} | returns) = {count}/{total} = {count / total:.3f}")
22print("unseen token probability =", row["exception"] / total)1P(value | returns) = 3/6 = 0.500
2P(result | returns) = 2/6 = 0.333
3P(error | returns) = 1/6 = 0.167
4unseen token probability = 0.0The sparsity collapse and Laplace smoothing
The bigram table reveals two severe architectural flaws:
First, Markov amnesia. The bigram model conditions solely on returns. It can't distinguish function returns from test returns, even though our corpus shows that tests frequently return error while functions return value.
Second, sparsity collapse. The pair returns exception never appears in our six training sentences. Its raw count is zero, so . If an evaluation document contains that valid phrase, taking the negative log-likelihood () yields infinite loss. The entire test corpus probability crashes to zero because of one missing pair!
Laplace (add-one) smoothing mitigates zero-count collapse by adding pseudo-counts across the entire vocabulary :
If our vocabulary has four potential continuations (value, result, error, exception), the counts become , and the denominator becomes . exception receives probability instead of zero.
While smoothing prevents infinities, it doesn't solve the core issue. Widening the context to an n-gram model () creates an exponential parameter explosion. For a modest vocabulary of , a 4-gram table demands parameters. Most combinations never occur in any training set, and discrete counts can't transfer knowledge between similar words like function and method.
Neural language models and distributed representations
Yoshua Bengio and colleagues resolved this combinatorial bottleneck by introducing continuous distributed representations for words.[3]
Instead of treating tokens as orthogonal atomic symbols where one-hot vectors share zero overlap (), a neural language model maps each token ID to a continuous embedding vector in :
- Look up continuous embeddings for the previous context tokens in an embedding matrix .
- Concatenate these vectors into a single feature vector .
- Project through shared linear weights and add a bias vector to yield vocabulary logits.
- Pass logits through softmax to form next-token probabilities.
1context token IDs -> embedding lookup E -> concatenated vector x -> linear map Wx + b -> logits -> softmaxBecause words with similar grammatical and semantic roles develop similar embedding orientations, the model generalizes probability mass smoothly. If the training data contains method returns result, the model assigns sensible probability to function returns result without requiring explicit frequency counts for that exact sequence.

Training a tiny neural n-gram with gradient descent
Let's implement a complete neural n-gram model from scratch in Python. It reads two context tokens, maps each to a 6-dimensional embedding, concatenates them into 12 features, and projects them to 8 vocabulary logits.
The backward pass applies the exact cross-entropy gradient derived in the softmax lesson: . Gradients flow through the projection weights back into the embedding rows, updating representations through gradient descent.
1import math
2import random
3
4random.seed(7)
5sentences = [
6 "function returns value",
7 "function returns value",
8 "method returns result",
9 "handler returns value",
10 "test returns result",
11 "test returns error",
12]
13vocab = sorted({word for sentence in sentences for word in sentence.split()})
14to_id = {word: index for index, word in enumerate(vocab)}
15n_vocab = len(vocab)
16emb_dim = 6
17
18contexts = []
19targets = []
20for sentence in sentences:
21 words = sentence.split()
22 for index in range(2, len(words)):
23 contexts.append([to_id[words[index - 2]], to_id[words[index - 1]]])
24 targets.append(to_id[words[index]])
25
26embeddings = [[random.uniform(-0.3, 0.3) for _ in range(emb_dim)] for _ in range(n_vocab)]
27weights = [[random.uniform(-0.2, 0.2) for _ in range(n_vocab)] for _ in range(emb_dim * 2)]
28bias = [0.0] * n_vocab
29
30def concat_embeddings(token_ids):
31 return embeddings[token_ids[0]] + embeddings[token_ids[1]]
32
33def logits_from(features):
34 logits = bias[:]
35 for i, value in enumerate(features):
36 for j, weight in enumerate(weights[i]):
37 logits[j] += weight * value
38 return logits
39
40def softmax(logits):
41 peak = max(logits)
42 exps = [math.exp(logit - peak) for logit in logits]
43 total = sum(exps)
44 return [exp / total for exp in exps]
45
46def mean_loss():
47 total = 0.0
48 for token_ids, target in zip(contexts, targets):
49 logits = logits_from(concat_embeddings(token_ids))
50 peak = max(logits)
51 total += (peak - logits[target]) + math.log(sum(math.exp(z - peak) for z in logits))
52 return total / len(contexts)
53
54def predict(text):
55 token_ids = [to_id[word] for word in text.split()]
56 logits = logits_from(concat_embeddings(token_ids))
57 return vocab[max(range(n_vocab), key=lambda index: logits[index])]
58
59initial_loss = mean_loss()
60learning_rate = 0.4
61for _ in range(200):
62 grad_embeddings = [[0.0] * emb_dim for _ in range(n_vocab)]
63 grad_weights = [[0.0] * n_vocab for _ in range(emb_dim * 2)]
64 grad_bias = [0.0] * n_vocab
65 scale = 1.0 / len(contexts)
66 for token_ids, target in zip(contexts, targets):
67 features = concat_embeddings(token_ids)
68 dlogits = [prob * scale for prob in softmax(logits_from(features))]
69 dlogits[target] -= scale
70 for j, grad in enumerate(dlogits):
71 grad_bias[j] += grad
72 for i, value in enumerate(features):
73 for j, grad in enumerate(dlogits):
74 grad_weights[i][j] += value * grad
75 dfeatures = [
76 sum(weights[i][j] * dlogits[j] for j in range(n_vocab))
77 for i in range(emb_dim * 2)
78 ]
79 for offset, token_id in enumerate(token_ids):
80 start = offset * emb_dim
81 for dim in range(emb_dim):
82 grad_embeddings[token_id][dim] += dfeatures[start + dim]
83 for i in range(n_vocab):
84 bias[i] -= learning_rate * grad_bias[i]
85 for dim in range(emb_dim):
86 embeddings[i][dim] -= learning_rate * grad_embeddings[i][dim]
87 for i in range(emb_dim * 2):
88 for j in range(n_vocab):
89 weights[i][j] -= learning_rate * grad_weights[i][j]
90
91print(f"loss before training: {initial_loss:.3f}")
92print(f"loss after training: {mean_loss():.3f}")
93print("after 'function returns':", predict("function returns"))
94print("after 'test returns': ", predict("test returns"))
95assert predict("function returns") == "value"
96assert mean_loss() < initial_loss1loss before training: 2.145
2loss after training: 0.235
3after 'function returns': value
4after 'test returns': resultNotice what happened: the model distinguishes function returns from test returns! It outputs value after function returns and result after test returns.
The loss dropped from 2.145 to 0.235 nats. In our dataset, test returns appears twice with conflicting targets: once with result and once with error. An optimal model splits its probability between those two targets, giving a theoretical minimum cross-entropy floor of . The trained network reaches 0.235, virtually matching the dataset's entropy floor.
Does distinguishing function returns from test returns prove that neural embeddings are strictly necessary?
Answer
No. A discrete trigram table also sees two tokens of context. What neural embeddings provide is parameter sharing: semantically similar words share embedding space. That shared geometry lets neural models generalize to unseen prefixes, which discrete count tables can't do without smoothing.
Temperature scaling on output logits
During inference, we don't always take the argmax token. We often sample from the distribution. A temperature parameter rescales logits before applying softmax:
- (Cooling): Amplifies logit differences. The top token monopolizes probability mass, making generation focused and deterministic.
- (Unscaled): Standard softmax probabilities derived directly from learned parameters.
- (Heating): Flattens logit differences. The distribution approaches uniform randomness, giving low-probability tokens a higher chance of being sampled.
1import math
2
3logits = [3.0, 1.5, -1.0, -2.0, -3.0]
4tokens = ["error", "warning", "success", "the", "</s>"]
5for temperature in [0.5, 1.0, 2.0]:
6 scaled = [logit / temperature for logit in logits]
7 peak = max(scaled)
8 exps = [math.exp(logit - peak) for logit in scaled]
9 total = sum(exps)
10 probabilities = [exp / total for exp in exps]
11 ranked = sorted(probabilities, reverse=True)
12 if temperature == 1.0:
13 print(f"{tokens[0]:<11}{probabilities[0]:.3f}")
14 print(f"{tokens[1]:<11}{probabilities[1]:.3f}")
15 print(f"T={temperature:.1f} top={ranked[0]:.3f} second={ranked[1]:.3f}")1T=0.5 top=0.952 second=0.047
2error 0.800
3warning 0.178
4T=1.0 top=0.800 second=0.178
5T=2.0 top=0.575 second=0.272At , error surges to 95.2% probability. At , it drops to 57.5%, leaving significant mass for alternatives. Temperature is a sampling control, not a parameter update: it alters decoding entropy without modifying underlying weights.
Causal versus masked language modeling
The architecture that powers modern generative models is causal language modeling (autoregressive generation). It conditions strictly on earlier tokens ().
In contrast, masked language modeling (BERT) hides tokens and reconstructs them using both left and right context.[4]
| Objective | Visible context | Core training task | Primary architectural use |
|---|---|---|---|
| Causal language modeling | Only prior tokens () | Predict the single next token | Generative decoders (GPT, LLaMA, Claude) |
| Masked language modeling | Full bidirectional context ( and ) | Reconstruct masked tokens in place | Encoders for embeddings, reranking, classification (BERT) |
Because masked language modeling allows tokens to attend to the right, it can't naturally roll out text token by token. Causal modeling strictly enforces the autoregressive chain rule factorization.
Teacher forcing and parallel causal training
How does a causal model learn from large corpora? Every document provides supervised labels for free. At each token position , the ground-truth target label is simply the observed token .
The causal attention mask and parallel GEMM
During pre-training, the model receives ground-truth tokens at every position rather than its own predictions. This mechanism is teacher forcing.
In recurrent neural networks (RNNs), hidden state dependencies forced training to run sequentially step by step. In a decoder Transformer, teacher forcing unlocks massive parallelism:
- The entire token sequence enters the network at once.
- The causal attention mask sets attention logits for all positions .
- Because , position can't attend to future tokens .
- The GPU executes dense General Matrix Multiplies (GEMM) across all positions simultaneously in an parallel forward pass!

Exposure bias and the train-inference gap
Teacher forcing provides a stable, clean training signal, but it creates a fundamental train-inference mismatch known as exposure bias.[5]
During training, position always conditions on pristine human text from the dataset. During inference, position conditions on the model's own previously sampled tokens. If the model makes a slight error or samples an unusual token at step 2, it enters an out-of-distribution prefix at step 3. The model was never trained on its own mistaken prefixes, so errors can compound rapidly across long generations.
Because of exposure bias, a low teacher-forced loss doesn't guarantee fluent rollouts. Evaluating a model requires checking both teacher-forced cross-entropy and free-running generations for repetition, drift, and stopping criteria.
Shifting tokens into inputs and target labels
Training a causal decoder requires aligning inputs and targets. Given the sequence [<bos>, function, returns, value, <eos>], input tokens drop the final token, while target labels drop the initial <bos>:
| Input prefix to model () | Supervised target label () |
|---|---|
<bos> | function |
<bos> function | returns |
<bos> function returns | value |
<bos> function returns value | <eos> |
In standard libraries like Hugging Face Transformers, DataCollatorForLanguageModeling with mlm=False takes raw token IDs and masks padding tokens with -100.[6] The causal model shifts logits and labels internally: logits[..., :-1, :] pairs with labels[..., 1:].[7]
1vocab = {"<bos>": 0, "function": 1, "returns": 2, "value": 3, "<eos>": 4}
2tokens = ["<bos>", "function", "returns", "value", "<eos>"]
3ids = [vocab[token] for token in tokens]
4inputs = ids[:-1]
5targets = ids[1:]
6
7print("input token ids: ", inputs)
8print("target token ids:", targets)
9for position in range(len(inputs)):
10 context = " ".join(tokens[: position + 1])
11 print(f"{context:25s} -> {tokens[position + 1]}")1input token ids: [0, 1, 2, 3]
2target token ids: [1, 2, 3, 4]
3<bos> -> function
4<bos> function -> returns
5<bos> function returns -> value
6<bos> function returns value -> <eos>Cross-entropy loss across sequence positions
At every position , the model emits a probability distribution over the vocabulary. We want the probability assigned to the ground-truth target to be as close to 1.0 as possible. Cross-entropy loss computes the negative log-probability averaged across all positions:
If the network assigns probability 1.0 to the correct token, the term contributes loss. If it assigns a tiny probability like 0.01, the penalty is large: nats. Causal pre-training minimizes this average negative log-likelihood across trillions of tokens.
1import math
2
3targets = [1, 2, 3, 4] # function, returns, value, <eos>
4logits = [
5 [0.0, 3.0, 0.5, -1.0, -2.0],
6 [-1.0, 0.1, 2.5, 0.0, -2.0],
7 [-1.0, 0.0, 0.2, 2.0, -1.5],
8 [-2.0, 0.0, -1.0, -0.5, 2.4],
9]
10
11true_probs = []
12losses = []
13for row, target in zip(logits, targets):
14 peak = max(row)
15 exps = [math.exp(logit - peak) for logit in row]
16 total = sum(exps)
17 prob = exps[target] / total
18 true_probs.append(prob)
19 losses.append((peak - row[target]) + math.log(total))
20
21print("true-token probabilities:", [round(prob, 3) for prob in true_probs])
22print("per-token losses: ", [round(loss, 3) for loss in losses])
23print(f"mean cross-entropy: {sum(losses) / len(losses):.3f}")1true-token probabilities: [0.864, 0.824, 0.724, 0.839]
2per-token losses: [0.146, 0.194, 0.323, 0.175]
3mean cross-entropy: 0.209Perplexity as an effective branching factor
Cross-entropy loss in natural logarithms produces units of nats (or bits if using base 2). Exponentiating cross-entropy yields perplexity:[8][9]
Perplexity is the reciprocal of the geometric mean of the target token probabilities.
Intuitively, perplexity represents the effective branching factor: the number of equally likely options the model is choosing between at each step.
- A perplexity of 1.0 means the model predicted every target token with 100% confidence ().
- If a model guesses uniformly across a vocabulary of 50,000 tokens, its loss is , and its perplexity is exactly .
- In our 4-position example above, a mean loss of 0.209 nats yields a perplexity of . The model predicts with the certainty of someone picking among roughly 1.2 equally weighted choices per token.
1import math
2
3# Probability the model gave to the token that was actually correct, per position
4true_token_probs = [0.5, 0.5, 0.5, 0.125]
5
6per_position_loss = [-math.log(prob) for prob in true_token_probs]
7avg_loss = sum(per_position_loss) / len(per_position_loss)
8perplexity = math.exp(avg_loss)
9
10print(f"per-position loss: {[round(loss, 4) for loss in per_position_loss]}")
11print(f"average loss: {avg_loss:.4f}")
12print(f"perplexity: {perplexity:.4f}")
13
14product = 1.0
15for prob in true_token_probs:
16 product *= prob
17geo_mean = product ** (1 / len(true_token_probs))
18print(f"1 / geometric mean:{1 / geo_mean:.4f}")1per-position loss: [0.6931, 0.6931, 0.6931, 2.0794]
2average loss: 1.0397
3perplexity: 2.8284
41 / geometric mean:2.8284Notice how one low-probability target (0.125 at position 4) pulls the average loss from 0.693 to 1.039, elevating perplexity to 2.828. Perplexity penalizes surprise heavily.
Perplexity is only comparable across models when they share the exact same tokenizer, vocabulary size, and text normalization. A byte-level tokenizer naturally achieves lower perplexity per token than a word-level tokenizer simply because its vocabulary is vastly smaller, not because its predictions are superior.[10]
A model assigns the true next token a probability of 0.01 at one position. Roughly how much does that position contribute to cross-entropy loss?
Answer
The contribution is -log(0.01) ≈ 4.605 nats. That is a heavy penalty. Confident correct predictions contribute near 0 loss, whereas low-probability surprises drive loss and perplexity up quickly.
The autoregressive generation loop
During inference, ground-truth continuations don't exist. The model must generate text one token at a time in a sequential loop:
- Input: Feed the prompt tokens into the network.
- Predict: Compute vocabulary logits and softmax probabilities for the next token.
- Select: Pick a token via greedy argmax or temperature-scaled sampling.
- Append: Concatenate the newly selected token to the input sequence.
- Repeat: Feed the expanded context back into the model to predict the subsequent token.

A deterministic generation loop in code
This small script demonstrates the sequential mechanics: read context, pick the top token, append it, and check for a stop token.
1next_token_table = {
2 "The function returns the": [("result", 0.60), ("value", 0.25), ("index", 0.10), ("other", 0.05)],
3 "The function returns the result": [(".", 0.80), ("and", 0.20)],
4 "The function returns the result.": [("</s>", 1.0)],
5}
6
7text = "The function returns the"
8
9for step in range(3):
10 choices = next_token_table[text]
11 assert abs(sum(probability for _, probability in choices) - 1.0) < 1e-12
12 token, _probability = max(choices, key=lambda item: item[1])
13
14 if token == "</s>":
15 break
16
17 text = text + ("" if token == "." else " ") + token
18 print(step, repr(text))
19print("final text:", repr(text))10 'The function returns the result'
21 'The function returns the result.'
3final text: 'The function returns the result.'In a full language model, the transition table isn't a hardcoded dictionary: billions of neural weights compute the distribution dynamically for any arbitrary token prefix.
The loop terminates when one of four conditions is satisfied:
| Termination condition | Trigger mechanism | Production handling |
|---|---|---|
| End-of-sequence token | Model samples </s>, <|endoftext|>, or <|im_end|> | Server cleanly closes the response stream. |
| Output token budget | Generated tokens reach user's max_tokens limit | Truncate output gracefully or flag incomplete generation. |
| Stop sequence match | Output contains predefined string like \nUser: or ``` | Halt generation and strip the trigger delimiter. |
| Safety filter cutoff | Moderation layer flags harmful generation | Substitute safe completion or raise guardrail event. |
Greedy selection versus temperature sampling
In greedy decoding, the engine always takes the argmax: . This choice is deterministic, but it often causes repetitive loops in open-ended text.
In stochastic sampling, the engine draws a random token weighted by its softmax probability. Sampling injects diversity, but pure multinomial sampling risks picking ungrammatical tail tokens. Modern serving combines temperature with top- (filtering to the most probable tokens) or top- nucleus sampling (filtering to the smallest set of tokens whose cumulative probability exceeds ).
1import math
2import random
3
4tokens = ["result", "value", "index"]
5logits = [2.4, 1.7, 0.2]
6
7def probabilities(temperature):
8 scaled = [logit / temperature for logit in logits]
9 peak = max(scaled)
10 exps = [math.exp(logit - peak) for logit in scaled]
11 total = sum(exps)
12 return [exp / total for exp in exps]
13
14rng = random.Random(4)
15greedy = tokens[max(range(len(logits)), key=lambda i: logits[i])]
16probs = probabilities(1.4)
17samples = [rng.choices(tokens, weights=probs, k=1)[0] for _ in range(8)]
18
19print("greedy:", greedy)
20print("sampled at temperature 1.4:", samples)
21print("probabilities:", [round(prob, 3) for prob in probs])1greedy: result
2sampled at temperature 1.4: ['result', 'result', 'result', 'result', 'result', 'result', 'index', 'value']
3probabilities: [0.551, 0.334, 0.115]Context window limits and memory growth
Because the model appends every emitted token back into its input, the context length grows at every step. At token 1,000, the model re-evaluates the prompt plus all 999 generated tokens.
Every architecture enforces a maximum context window (such as 8,192 or 128,000 tokens). Exceeding this limit forces the application to truncate historical messages, summarize earlier turns, or reject the request.
Production serving: prefill, decode, and the KV cache
Serving an autoregressive Transformer in production introduces a sharp computational divide between two distinct phases:
- The Prefill Phase: Processing the user's prompt tokens all at once.
- The Decode Phase: Generating new tokens sequentially one by one.

Why naive autoregressive generation is quadratic
In standard multi-head self-attention, token computes queries , keys , and values .
Without caching, generating step requires re-running linear projections for all previous tokens. To generate a 2,000-token response from a 1,000-token prompt, the GPU would recompute keys and values for prompt tokens 2,000 times! Over an entire sequence of length , the total operations scale quadratically as .
The KV cache and memory bandwidth bottlenecks
Because causal attention masks prevent future tokens from affecting past states, the Key () and Value () vectors of historical tokens never change during generation.
The Key-Value (KV) cache stores the evaluated and tensors from all previous positions across all Transformer layers in GPU High Bandwidth Memory (HBM).[11]
At decode step :
- The GPU only computes for the single newly generated token.
- It appends and to the cached tensors in HBM.
- It computes attention by multiplying query vector against all accumulated cached keys and values.
This reduces per-step computational complexity from to projection work and linear attention operations.
However, caching introduces a major hardware bottleneck:
- Prefill is compute-bound: Prompt tokens form a large matrix . The GPU runs General Matrix Multiplications (GEMM), achieving high arithmetic intensity (FLOPs per byte loaded) and saturating Tensor Cores.
- Decode is memory-bandwidth bound: At each step, the input is a single token vector . Computing attention requires executing General Matrix-Vector products (GEMV). The GPU must load gigabytes of weights and cached KV tensors from HBM into SRAM just to perform a handful of operations ( FLOPs per byte loaded).
In the decode phase, GPU execution units spend most of their time waiting for data to travel across the memory bus.
Sizing the KV cache in high bandwidth memory
The memory footprint of a standard KV cache per request is governed by this exact formula:
Where:
- : stores both Key and Value matrices
- : number of Transformer layers
- : number of Key/Value attention heads
- : head dimension ()
- : total sequence length (prompt plus generated tokens)
- : concurrent batch size
- : bytes per value (2 for FP16/BF16, 1 for FP8/INT8)
1batch = 4
2layers = 32
3kv_heads = 8
4head_dim = 128
5bytes_per_value = 2
6
7def cache_gib(sequence_length):
8 values = batch * layers * sequence_length * kv_heads * head_dim * 2
9 return values * bytes_per_value / (1024 ** 3)
10
11for sequence_length in [2_048, 32_768]:
12 print(f"{sequence_length:>6,} tokens: {cache_gib(sequence_length):.2f} GiB")12,048 tokens: 1.00 GiB
232,768 tokens: 16.00 GiBAt 32,768 tokens, our 4-request batch consumes 16 GiB of GPU memory solely for the KV cache. With a concurrent batch size of 32 requests, the cache demands 128 GiB, easily exceeding the memory needed for the model weights themselves!
Two architectural breakthroughs keep modern KV serving manageable:
- Grouped-Query Attention (GQA): Instead of allocating one KV head per query head (), GQA groups query heads to share a smaller number of KV heads (for instance, 32 query heads sharing 8 KV heads). This slashes KV cache memory by 4× with virtually no degradation in model quality.[12]
- PagedAttention (vLLM): Traditional serving engines allocated contiguous memory blocks for the maximum possible context length, wasting 60% to 80% of VRAM on memory fragmentation. PagedAttention partitions the KV cache into fixed-size virtual memory blocks (pages of 16 or 32 tokens), eliminating internal fragmentation and enabling near-optimal memory utilization.[13]
Why next-token objectives learn structured computation
It's common to wonder: if an LLM is only trained to predict the next token, how does it acquire reasoning, code synthesis, and multi-step logic?
Consider this code snippet:
1retries = 3
2retries -= 1
3retries ==What token comes next?
An n-gram model with a 2-token context only sees retries ==. It might predict 0 because == 0 is common in its training corpus.
A large Transformer reads the full prefix. To drive cross-entropy loss down across millions of similar code files, the network's internal attention heads learn to track variable assignments, decrement operations, and condition checks. Predicting the single token 2 correctly requires internally tracking execution state!
Next-token prediction acts as an information-dense compression objective. To predict the next word across web-scale text, the network must discover circuits that represent syntax, world facts, programmatic states, and conversational structure.[14]
However, next-token prediction doesn't equal conscious understanding. The model remains a probabilistic engine that mirrors its training distribution.
Failure modes and debugging
Next-token prediction supplies a powerful training signal, but it introduces distinct operational failure modes.
Treating probabilities as ground-truth facts
High probability reflects stylistic frequency in the training corpus, not verified truth.
Suppose an internal documentation assistant reads:
The rate-limit page says free-tier requests are
The base model might assign 75% probability to unlimited because marketing copy frequently pairs those words. But if your production API enforces 60 requests per minute, the statistically likely continuation is factually wrong!
| Failure symptom | Root mechanism | Production remediation |
|---|---|---|
| Plausible hallucination | Stylistic frequency overrides missing domain facts | Inject retrieved documentation into context (RAG) |
| Outdated API limits | Model weights reflect static pre-training snapshot | Provide tool calling for real-time API queries |
| Overconfident false citations | Model predicts plausible-looking paper URLs | Require explicit quote verification against retrieved documents |
| Repetitive runaway loops | Model gets trapped in self-reinforcing prefix | Apply frequency penalties, repetition penalties, or min-p sampling |
The relational database myth
A common misconception treats language models like relational databases that look up rows during generation.
An LLM contains no internal SQL engine or indexed tables. It possesses only numerical weights that parameterize categorical distributions over tokens. When asked for a specific phone number or timestamp, it doesn't query a database; it generates a plausible continuation. If accurate factual recall is required, system designs must provide external sources of truth via retrieval or function calling.
Stress-testing prefix distributions and cache budgets
To solidify these concepts, test how prefix evidence and serving dimensions reshape model behavior.
In count-bigram-events.py, add "function returns exception" to the training list. The new row for returns has 7 total observations: value: 3, result: 2, error: 1, exception: 1. The probability of value drops from to , and exception takes .
Now consider what happens in tiny-neural-ngram.py. The prefix function returns now appears twice with target value and once with target exception. A deterministic function can't output two different probabilities of 1.0 for the same input. The cross-entropy objective will drive the network toward assigning roughly 67% probability to value and 33% to exception.
Why can't any neural language model achieve zero loss on a dataset that contains both "function returns value" and "function returns exception"?
Answer
Because identical inputs can't map to conflicting one-hot vectors simultaneously. Softmax outputs must sum to 1.0. When the identical prefix maps to multiple targets, the optimal prediction matches their empirical frequencies, leaving an irreducible entropy floor known as the Bayes error rate.
Next, test the serving economics in kv-cache-memory-budget.py. Halving kv_heads from 8 to 4 (switching to a more aggressive Grouped-Query Attention ratio) reduces cache memory from 16.00 GiB to 8.00 GiB at 32,768 tokens. That 8 GiB savings directly translates to doubling the server's concurrent user capacity.
Language modeling unifies this entire pipeline: from chain-rule factorization and continuous embeddings to teacher-forced training and memory-bandwidth-bound KV cache serving.