Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
A Transformer forward pass produces logits. Softmax turns that vector of scores into a probability distribution over possible next tokens. Language modeling trains that distribution.
A large language model (LLM) does one repeated job: it predicts the next token. You'll build that job from counts into a trainable objective, then see how a causal Transformer uses it during generation.
One token at a time: An LLM doesn't generate a whole paragraph at once. It predicts a single token, appends it to its input, and runs the process again. This is called autoregressive generation.

What is a language model?
An autoregressive language model is a probability engine for token sequences: given the tokens so far, it assigns a probability distribution over the vocabulary for what comes next.
Editor autocomplete is a useful first approximation. A language model doesn't treat every possible token as equally likely; it ranks them using the code, text, and instructions already in the context.
The probability of a whole sequence factorizes into a product of next-token predictions, one per position. This is just the chain rule of probability.
Read it left to right: the chance of the first token, times the chance of the second given the first, times the chance of the third given the first two, and so on. Each factor is one next-token distribution. That single building block, "distribution over the next token given everything before it," is the entire job. Generation, training, and perplexity are all built on it.
Suppose a model receives this prompt:
"The function returns the"
Its actual probabilities depend on its tokenizer, weights, and preceding context. In a simplified example, it might rank four possible continuations like this:
Probability table
| Candidate token | Probability | What reader should notice |
|---|---|---|
result | 60% | Greedy decoding would choose this; sampling makes it the likeliest choice. |
value | 25% | Plausible alternatives remain available when sampling. |
index | 10% | Lower-ranked words can appear with higher temperature. |
exception | 0.0001% | A grammatical token can still be unlikely in this context. |
This table is only an excerpt. Tokens omitted from the table receive the remaining probability mass.
Read the distribution before sampling
The table above isn't a list of words the model "knows." It's the model's next-step belief after reading the prompt.
That distinction matters because the same model can behave differently after one extra token.
| Prompt so far | Likely next token | Why |
|---|---|---|
The function returns the | result | Common code explanation completion. |
The function returns the cached | value | New context changes the expected object. |
The function returns the first | index | The adjective changes the likely noun. |
The function returns the plus a style instruction for API docs | value or response | Task instructions can change likely continuations. |
The model doesn't store one fixed answer for "the function returns the." It computes a distribution from the whole context. Change the context, and you change the probabilities.
Try the mental move:
- Read this prompt.
- Predict three likely next tokens.
- Compare your predictions with the options below.
1The test runner says the assertionPossible continuations:
1passed
2failed
3timedAll three are plausible. A model doesn't need certainty to generate text. It needs a probability ranking, then the decoding algorithm picks from that ranking.
Tiny vocabulary example
A production vocabulary contains many tokens, with its exact size set by the model's tokenizer. That's too many for a beginner example, so shrink the world to five tokens.
Use this prompt:
The compiler reported
The model produces logits, which are raw scores before softmax:
| Token | Logit | Meaning |
|---|---|---|
error | 3.0 | Strong continuation. |
warning | 1.5 | Plausible but less likely. |
success | -1.0 | Grammatically possible, semantically odd. |
the | -2.0 | Usually wrong after this prompt. |
</s> | -3.0 | Ending now is unlikely. |
Softmax turns those scores into probabilities:
One line in the code deserves explanation before you read it: logits - logits.max().
Why subtract the maximum logit? Because softmax only cares about differences between logits, not their absolute level. If you subtract the same constant from every score, the ranking and final probabilities stay the same. But the numerics get much safer: the largest shifted logit becomes 0, so its exponential is e^0 = 1 instead of some huge number. On tiny toy logits like [3.0, 1.5, -1.0, ...] this barely matters. On real model logits it prevents overflow and keeps the computation stable.
1import numpy as np
2
3tokens = ["error", "warning", "success", "the", "</s>"]
4logits = np.array([3.0, 1.5, -1.0, -2.0, -3.0])
5
6# Subtract max for numerical stability, then exponentiate
7exp = np.exp(logits - logits.max())
8probs = exp / exp.sum()
9
10for token, prob in zip(tokens, probs):
11 print(f"{token:10s} {prob:.3f}")
12print(f"sum {probs.sum():.3f}")
13print("top token ", tokens[int(np.argmax(probs))])1error 0.800
2warning 0.178
3success 0.015
4the 0.005
5</s> 0.002
6sum 1.000
7top token errorThe exact numbers are less important than the shape:
- One token is far ahead.
- Another token still has a real chance.
- Some tokens are valid but unlikely.
- The probabilities add to 1.
That's next-token prediction in miniature.
From bigram tables to neural n-grams
The smallest context-aware language model is a bigram model. It looks at one previous token and predicts the next token from a table of counts:
1P(next_token | previous_token)If the code-comment corpus often contains returns -> value, the row for returns assigns high probability to value. If it never contains returns -> exception, an unsmoothed table assigns that pair zero probability.
| Previous token | Candidate next token | Count | Probability |
|---|---|---|---|
returns | value | 85 | 0.85 |
returns | result | 10 | 0.10 |
returns | error | 5 | 0.05 |
That table already teaches the core language-modeling contract: read context, produce a probability distribution over the next token. Its weakness is context length. A bigram model can't tell the difference between function returns the and test returns the, because it only sees the final token.
Build a count-based language model
Start with six code-comment fragments. Each adjacent token pair becomes one training event: after returns, which token appeared next?
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.0An unseen pair receives probability zero in this unsmoothed table. More data or smoothing helps, but a count table still can't share what it learned about related contexts.
An n-gram model predicts the next token from the previous n - 1 tokens:
1P(next_token | previous n - 1 tokens)For tiny corpora, count tables still work. For real language, the table explodes because most exact contexts are rare. A neural n-gram model addresses that sparsity by learning embeddings and a small MLP (multi-layer perceptron):
- Look up embeddings for the previous
n - 1tokens. - Concatenate them into one vector.
- Apply a matrix multiply, bias, and a nonlinearity such as the Gaussian Error Linear Unit (GELU).
- Project to vocabulary logits.
- Softmax into next-token probabilities.
1previous n - 1 token IDs -> embedding table -> joined embeddings -> matmul + GELU -> logits -> softmaxThis is the bridge from count-based autocomplete to modern LLMs. The objective stays next-token prediction. The model family changes: bigram table, neural n-gram MLP, recurrent network, then Transformer. Each step gives the model a better way to use context.

Train a tiny neural n-gram
The next cell trains a small two-token-context, or trigram, model. It isn't a Transformer, but it exposes the same contract: embeddings and shared weights produce logits, and cross-entropy rewards the true next token.
1import torch
2from torch import nn
3
4torch.manual_seed(7)
5sentences = [
6 "function returns cached value",
7 "method returns cached value",
8 "handler returns cached value",
9 "function returns default result",
10 "method returns default result",
11 "test returns default error",
12]
13vocab = sorted({word for sentence in sentences for word in sentence.split()})
14to_id = {word: index for index, word in enumerate(vocab)}
15
16contexts, targets = [], []
17for sentence in sentences:
18 words = sentence.split()
19 for index in range(2, len(words)):
20 contexts.append([to_id[words[index - 2]], to_id[words[index - 1]]])
21 targets.append(to_id[words[index]])
22
23x = torch.tensor(contexts)
24y = torch.tensor(targets)
25model = nn.Sequential(
26 nn.Embedding(len(vocab), 6),
27 nn.Flatten(),
28 nn.Linear(12, 16),
29 nn.GELU(),
30 nn.Linear(16, len(vocab)),
31)
32optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
33
34with torch.no_grad():
35 initial_loss = nn.functional.cross_entropy(model(x), y).item()
36for _ in range(120):
37 loss = nn.functional.cross_entropy(model(x), y)
38 optimizer.zero_grad()
39 loss.backward()
40 optimizer.step()
41
42query = torch.tensor([[to_id["returns"], to_id["cached"]]])
43with torch.no_grad():
44 final_loss = nn.functional.cross_entropy(model(x), y).item()
45 predicted_id = int(model(query).argmax(dim=-1).item())
46
47print(f"loss before training: {initial_loss:.3f}")
48print(f"loss after training: {final_loss:.3f}")
49print("after 'returns cached':", vocab[predicted_id])1loss before training: 2.304
2loss after training: 0.390
3after 'returns cached': valueThe tiny corpus is deliberately simple, so this cell demonstrates optimization rather than a production-quality model. The important bridge is that a neural model can use one set of learned parameters across contexts instead of storing an isolated probability row for every exact phrase.
Why is a neural n-gram model more useful than a bigram count table?
Answer
A bigram table only sees one previous token and stores a separate row of counts for each observed pair. A neural n-gram model reads several previous tokens, turns them into embeddings, and uses shared weights to generalize across similar contexts. It still predicts the next token, but it can learn patterns such as "cached value" or "default error" instead of memorizing only exact adjacent pairs.
Temperature changes sampling
A temperature parameter rescales logits before softmax: softmax(logits / T), where T > 0. Values below 1 stretch logit gaps and sharpen the distribution; values above 1 compress gaps and give lower-ranked tokens more probability. Adding the same constant to every logit wouldn't change softmax at all. APIs that expose a zero-temperature mode should implement it as greedy decoding rather than divide by zero.
For logits [3.0, 1.5, -1.0, -2.0, -3.0], the two leading probabilities change directly:
1import numpy as np
2
3logits = np.array([3.0, 1.5, -1.0, -2.0, -3.0])
4for temperature in [0.5, 1.0, 2.0]:
5 scaled = logits / temperature
6 probabilities = np.exp(scaled - scaled.max())
7 probabilities /= probabilities.sum()
8 ranked = np.sort(probabilities)[::-1]
9 if temperature == 1.0:
10 print(f"correctly {probabilities[0]:.3f}")
11 print(f"slowly {probabilities[1]:.3f}")
12 print(f"T={temperature:.1f} top={ranked[0]:.3f} second={ranked[1]:.3f}")1T=0.5 top=0.952 second=0.047
2correctly 0.800
3slowly 0.178
4T=1.0 top=0.800 second=0.178
5T=2.0 top=0.575 second=0.272Production note: Low temperature makes source-grounded API-doc answers more repeatable, but it doesn't make unsupported facts true. Higher temperature produces more varied drafts for creative tools. Temperature is one lever among several for shaping generation; a later chapter covers the full decoding toolkit.
Causal language modeling
This specific flavor of language modeling, reading from left to right and predicting the future, is called causal language modeling (or autoregressive modeling). GPT-style models use this objective.[1][2]
It's called "causal" because the model is bound by causality: each position can use earlier context, but it can't peek at future tokens. In a Transformer block, a token position may attend to itself and earlier positions; the mask blocks later positions.
GPT-style decoder models are causal language models.
| Objective | Can read | Good for | Poor fit |
|---|---|---|---|
| Causal language modeling | Earlier context, no future tokens | Generating text left to right | Filling a middle blank with future context |
| Masked language modeling | Left and right context | Classification, extraction, sentence understanding | Open-ended generation |
Masked language modeling, used by Bidirectional Encoder Representations from Transformers (BERT), hides a token and predicts it from both left and right context.[3] That two-sided objective builds input representations; the left-to-right generation loop needs causal modeling.
How the model learns the distribution
We keep saying the model "predicts" the next token. Where does that skill come from? From training on text where the next token is already known. Every sentence in the training data is a free set of labeled examples: at each position, the correct answer is the token that came next.
Teacher forcing
During training the model is fed the real previous tokens at every position, not the tokens it would have guessed. This is called teacher forcing. The model never gets to wander off on its own mistakes while learning; each position is scored against ground truth using the true history.
This distinction matters whenever you interpret a training loss or measure generation latency:
| Phase | What feeds position t | Why |
|---|---|---|
| Training (teacher forcing) | The true tokens x_1 ... x_{t-1} from the dataset | Stable targets; all positions can be scored in one parallel forward pass. |
| Generation (autoregressive) | The model's own previously sampled tokens | No ground truth exists; the model must build on what it produced. |
Because the true history is known up front, training scores every position of a sequence in a single forward pass. The causal mask is what makes this honest: the logit produced from prefix x_1 ... x_{t-1} can't read its target token x_t.
This side-by-side view helps lock in why training is parallel while generation isn't:

Shift tokens into inputs and labels
For a decoder, one training sequence supplies several labeled predictions. The input is the sequence shifted right; the target is the same sequence shifted left.
| Context visible to model | Correct next token |
|---|---|
<bos> | function |
<bos> function | returns |
<bos> function returns | value |
<bos> function returns value | <eos> |
1import numpy as np
2
3vocab = {"<bos>": 0, "function": 1, "returns": 2, "value": 3, "<eos>": 4}
4tokens = ["<bos>", "function", "returns", "value", "<eos>"]
5ids = np.array([vocab[token] for token in tokens])
6inputs = ids[:-1]
7targets = ids[1:]
8
9print("input token ids: ", inputs.tolist())
10print("target token ids:", targets.tolist())
11for position in range(len(inputs)):
12 context = " ".join(tokens[: position + 1])
13 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: the training loss
At each position the model outputs a distribution over the vocabulary. We want the probability it assigns to the true next token to be high. The loss for one position is the negative log of that probability, and the loss for a sequence is the average over positions. This is cross-entropy loss, the same objective introduced in the softmax chapter, now applied at every position.
If the model gives the true token probability 1.0, that term contributes -log(1) = 0 loss: a perfect prediction. If it gives the true token a tiny probability, -log of a small number is large: a big penalty. Causal pretraining minimizes this average over a large corpus.
The logits at every shifted position can be scored together. This cell calculates stable cross-entropy for the four targets above.
1import numpy as np
2
3targets = np.array([1, 2, 3, 4]) # function, returns, value, <eos>
4logits = np.array([
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])
10shifted = logits - logits.max(axis=1, keepdims=True)
11probs = np.exp(shifted) / np.exp(shifted).sum(axis=1, keepdims=True)
12true_probs = probs[np.arange(len(targets)), targets]
13losses = -np.log(true_probs)
14
15print("true-token probabilities:", np.round(true_probs, 3).tolist())
16print("per-token losses: ", np.round(losses, 3).tolist())
17print(f"mean cross-entropy: {losses.mean():.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: loss in token units
Cross-entropy is measured in nats (because we used natural log), which is hard to feel. Perplexity is the friendlier form: just exponentiate the average loss.
Perplexity is often interpreted as an effective number of equally likely choices per step. A perplexity of 1 means the model assigned probability 1 to every evaluated token. A model that predicts uniformly over a 50,000-token vocabulary has perplexity 50,000, but the converse doesn't follow: a measured perplexity of 50,000 alone doesn't prove every prediction was uniform or that the model learned nothing. Lower is better when the tokenizer, evaluated data, and averaging convention match.[4][5]
Work a tiny example by hand. Suppose the model predicts four tokens, assigning the true token these probabilities: 0.5, 0.5, 0.5, 0.125.
1import numpy as np
2
3# Probability the model gave to the token that was actually correct, per position
4true_token_probs = np.array([0.5, 0.5, 0.5, 0.125])
5
6per_position_loss = -np.log(true_token_probs) # cross-entropy term per token
7avg_loss = per_position_loss.mean() # the training loss
8perplexity = np.exp(avg_loss) # loss in "effective choices"
9
10print(f"per-position loss: {np.round(per_position_loss, 4)}")
11print(f"average loss: {avg_loss:.4f}")
12print(f"perplexity: {perplexity:.4f}")
13
14# Perplexity equals the reciprocal geometric mean of the true-token probabilities
15geo_mean = np.prod(true_token_probs) ** (1 / len(true_token_probs))
16print(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.8284A perplexity of about 2.83 means that, averaged across these four steps, the model behaved as if it were choosing between roughly 2.83 equally likely options. The one bad prediction (0.125) drags the average up. That's the whole point of the metric: it tells you, in token units, how surprised the model was by real text.
When you fine-tune a model and watch the loss curve, you're watching average cross-entropy fall. When an eval report uses the same tokenizer, evaluated tokens, and natural-log averaging, perplexity is
expof that same loss. Hold those choices constant before treating a mismatch as a regression.
A model assigns the true next token a probability of 0.01 at one position. Roughly how much does that position contribute to the loss, and is that good or bad?
Answer
The per-position loss is -log(0.01) which is about 4.6 nats, a large penalty. The model was very surprised by the correct token, so that prediction was bad. A confident, correct prediction (probability near 1.0) would contribute close to 0. Training pushes the model to raise the probability it assigns to the tokens that actually occur, lowering this loss.
The autoregressive loop
When you ask a chat model to write an API-doc sentence, it doesn't output the whole message in one step. It generates the message one token at a time in a loop.
This is the autoregressive generation loop:
- Input: The model receives the prompt.
- Predict: It calculates the probabilities for the very next token.
- Pick: We take the most likely token or sample from the distribution, optionally adjusting temperature.
- Append: That chosen token is glued to the end of the prompt.
- Repeat: The new, longer context determines the distribution for the next token.
This list describes the dependency between generated tokens, not an inefficient serving implementation. Production decode usually reuses earlier attention state through a key-value (KV) cache. It still performs one sequential decode step for each new token, but it doesn't rebuild earlier keys and values. The serving section below makes that tradeoff concrete.

This explains why generation speed is measured in tokens per second (t/s). The model has to run another decoding step for every generated token. A KV cache avoids recomputing earlier keys and values from scratch, but each new token still has to attend to previous context.
A tiny generation loop in code
This example isn't a neural network. It's a small hand-written language model that shows the same loop: look at current text, choose one next token, append it, and repeat.
1next_token_table = {
2 "The function returns the": [("value", 0.60), ("result", 0.30), ("index", 0.10)],
3 "The function returns the value": [(".", 0.80), ("when", 0.20)],
4 "The function returns the value.": [("</s>", 1.0)],
5}
6
7text = "The function returns the"
8
9for step in range(3):
10 choices = next_token_table[text]
11 token, _probability = max(choices, key=lambda item: item[1])
12
13 if token == "</s>":
14 break
15
16 text = text + ("" if token == "." else " ") + token
17 print(step, repr(text))
18print("final text:", repr(text))10 'The function returns the value'
21 'The function returns the value.'
3final text: 'The function returns the value.'Real LLMs replace the hand-written table with a Transformer, but the control flow is the same.

The loop ends when one of these things happens:
| Stop condition | Example |
|---|---|
| Model emits an end token | </s> or another stop marker. |
| Application or API hits an output-token limit | The server stops after a token budget. |
| Application stop string appears | The app stops at \n\nUser: or a JSON boundary. |
| Safety or policy system interrupts | The provider refuses or truncates output. |
Picking a token from the distribution
The loop says "sample a token," but how? The model hands you a distribution, and a decoding strategy turns that distribution into one chosen token. Greedy decoding is the simplest strategy: always take the highest-probability token. It's deterministic, but it can fall into loops, repeating "We apologize for the inconvenience. We apologize for the inconvenience..." because "apologize" keeps scoring highest after "We."
The alternative is to sample: draw a token at random in proportion to its probability, so the second- or third-ranked token sometimes wins. Sampling can reduce repetition and add variety, but careless sampling can also hurt quality. The temperature knob from earlier controls how adventurous that draw is. Greedy versus sampling is the core fork; the full toolkit of decoding methods (and how to tune them) gets a dedicated chapter later, so here we only need the idea that one distribution can be turned into text in more than one way.
Common mistake: Setting a high temperature in factual docs generation and wondering why answers drift away from the source, or setting temperature to 0 in a creative app and wondering why every user gets the same paragraph. At each step, the current context produces a distribution; the decoding strategy decides how much you gamble on it.
Use a seeded random generator when testing a sampler, so samples repeat.
1import numpy as np
2
3tokens = np.array(["value", "result", "index"])
4logits = np.array([2.4, 1.7, 0.2])
5
6def probabilities(temperature):
7 scaled = logits / temperature
8 exp = np.exp(scaled - scaled.max())
9 return exp / exp.sum()
10
11rng = np.random.default_rng(4)
12greedy = tokens[int(logits.argmax())]
13samples = [str(rng.choice(tokens, p=probabilities(1.4))) for _ in range(8)]
14
15print("greedy:", greedy)
16print("sampled at temperature 1.4:", samples)
17print("probabilities:", np.round(probabilities(1.4), 3).tolist())1greedy: value
2sampled at temperature 1.4: ['index', 'value', 'index', 'value', 'result', 'value', 'result', 'value']
3probabilities: [0.551, 0.334, 0.115]The context window limit
Because the output is constantly being appended to the input, the text the model has to read keeps growing. Late in a 1000-token support answer, the model is reading the original prompt plus the 999 tokens it already generated.
This growing sequence must fit into the model's context window, its short-term memory limit. If the context window is 8,000 tokens and the prompt plus generated text reaches 8,001 tokens, the system has to truncate earlier context, reject the request, summarize, or use a longer-context strategy.
What the KV cache changes
Without a cache, the model would recompute attention information for the full prompt at every generation step. That would be wasteful.
The KV cache stores key and value tensors from earlier tokens at each attention layer. New decode queries can attend to these cached tensors instead of rebuilding earlier keys and values at every step.[6]
Serving systems usually split this into two phases:
- Prefill: process the full prompt in parallel and build the initial KV cache.
- Decode: generate one new token at a time, appending each token and reusing the cache.

| Step | Naive work | With KV cache |
|---|---|---|
| Select generated token 1 (prefill) | Process the prompt; use its final-position logits. | Process the prompt once, select token 1, and retain the prompt's keys and values. |
| Select generated token 2 | Recompute the prompt plus generated token 1. | Process token 1's new position against the cached prompt, then append its keys and values. |
| Select generated token 3 | Recompute the prompt plus generated tokens 1 and 2. | Process token 2's new position against the earlier cache, then append its keys and values. |
| Long answer | Work keeps repeating old context. | Memory grows, but old computation is reused. |
Serving systems care so much about KV cache memory because the cache is both an accelerator and a memory bill. It speeds generation, but it also consumes GPU memory proportional to context length, number of layers, key/value-head count, head dimension, and batch size.
Production note: If a model seems cheap at 4K context but expensive at 128K context, check KV cache memory before blaming the model weights. Long context changes serving economics.
Estimate cache memory
A simplified cache estimate makes the tradeoff concrete. For each request, layer, token, and key/value head, the server stores one key vector and one value vector. The estimate uses bfloat16 or float16 values at two bytes each.
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 GiBThis is only the KV cache, not model weights, activations, allocator overhead, or batching metadata. It also shows why fewer key/value heads can reduce cache cost.
Why next-token training can learn useful structure
Across large mixtures of code, math, dialogue, and documentation, local word association often isn't enough to minimize next-token loss. Reusable features for syntax, arithmetic, and task structure can improve predictions across many examples.
Worked prompt
Consider this prompt:
"A job has a 5-second deadline. After 2 seconds elapsed, the remaining budget is"
| Model family | Likely shortcut | Better internal feature |
|---|---|---|
| Markov-style autocomplete | Nearby token pattern like 2, 3 | None; it mostly follows local statistics. |
| Large language model | Still predicts one token | A subtraction-like representation helps reduce loss across many examples. |
A simple Markov chain might look at 2 and guess 3 because 2, 3 is common. A larger neural language model can benefit from features that track subtraction-like structure across many related examples, because those features can improve next-token predictions. This is a pressure from the objective, not a guarantee of correct arithmetic.
Next-token prediction explains the training signal, not human-like understanding or guaranteed reasoning. Memorization can occur, and each claimed capability still needs task-specific measurement.[1][7]
Common failure cases
Treating probabilities as facts
The model's top token can be wrong.
Suppose an internal docs assistant sees:
The rate-limit page says free-tier requests are
A model might assign high probability to unlimited because that phrase is common in marketing copy. But if your current API contract says free-tier requests are capped at 60 per minute, the statistically likely continuation is still wrong.
The fix isn't to hope the base model "knows" your API contract. Put the trusted spec into context, retrieve the right document, or use a tool that checks the source of truth.
| Symptom | Cause | Fix |
|---|---|---|
| Plausible but false answer | Training distribution dominates missing local facts. | Add retrieved source text. |
| Old limit repeated | Model has stale or generic priors. | Query current docs or database. |
| Confident unsupported claim | Probability isn't evidence. | Require citations or source-grounded output. |
| Long answer drifts | Context fills with generated text. | Re-anchor with instructions and source snippets. |
The "database" myth
An LLM doesn't query a hidden fact table during generation. Learned parameters can reproduce stale, conflicting, or fabricated continuations. Ground current facts with retrieval, tools, or explicit source context.
Mastery check
Key concepts
- Language-model objective: next-token prediction
- Chain-rule factorization of sequence probability
- Bigram and neural n-gram models share the next-token target
- Teacher forcing: training on true history in one parallel pass
- Cross-entropy loss as averaged negative log-likelihood
- Perplexity equals exp(loss): effective choices per token
- Autoregressive generation loop
- Causal vs masked language modeling (GPT vs BERT)
- Context window limits and KV-cache serving tradeoffs
- The database myth: LLMs generate instead of retrieving facts
Evaluation rubric
- Foundational: Defines an autoregressive language model, writes the chain-rule factorization, and traces the append-and-repeat generation loop.
- Foundational: Explains bigram counts, softmax, temperature, and the difference between greedy decoding and sampling.
- Intermediate: Connects neural n-gram and Transformer logits to teacher forcing, cross-entropy, and perplexity.
- Intermediate: Contrasts causal and masked language modeling, then explains why training is parallel while generation is sequential.
- Intermediate: Estimates KV-cache growth and distinguishes likely continuations from grounded facts.
Follow-up questions
If an LLM just predicts the next token, how can it do math or write code?
Answer
To accurately predict the next token in a vast corpus of human text (which includes technical docs, GitHub repositories, and worked reasoning traces), a model often can't rely only on local word association. It has pressure to build useful internal representations of underlying concepts. To predict the answer to '2+2=', an arithmetic-like internal feature is more useful than memorizing every string. By optimizing for next-token prediction over massive, diverse datasets, reasoning-like behavior can emerge as a byproduct of minimizing loss.
Why does generation slow down as the output gets longer?
Answer
Because of autoregressive generation. To generate the 100th token, the model must condition on the previous 99 tokens. To generate the 101st, it must condition on 100 tokens. KV cache prevents rebuilding earlier keys and values, but attention work and memory still grow with context length.
Why don't we pick the highest-probability token every time?
Answer
Picking the top token every time is called greedy decoding, and it can cause repetition loops. A documentation generator might get stuck writing 'This endpoint returns' over and over because 'returns' keeps scoring highest after 'endpoint.' Sampling instead draws a token in proportion to its probability, introducing enough variance to break loops and produce more natural text. Greedy versus sampling is the core fork; a later chapter covers the full decoding toolkit.
How does training (teacher forcing) differ from generation, and why does it matter for speed?
Answer
During training the model is fed the true previous tokens from the dataset at every position, so the targets are fixed and known in advance. The causal mask lets it predict all positions in a single parallel forward pass. During generation no ground truth exists, so the model must feed its own sampled tokens back in one at a time. Training a sequence can be fast and parallel, while generation stays inherently sequential.
Why can a model with low perplexity on API-doc examples still answer your current rate limit incorrectly?
Answer
Perplexity only measures how well the model predicts held-out text from a distribution similar to training or evaluation data. It doesn't guarantee access to today's source of truth. The model may have learned common API-limit language or older company behavior, so a statistically likely continuation can still be wrong unless you ground the answer with retrieval, tools, or fresh context.
Common pitfalls
- Symptom: A creative demo keeps giving nearly identical completions. Cause: Decoding is too deterministic, often from greedy decoding or very low temperature. Fix: Sample from the distribution and raise temperature carefully instead of always taking the top token.
- Symptom: Training loss looks good, but long generations drift after one bad token. Cause: Teacher forcing was confused with real generation, so evaluation never tested the model feeding on its own outputs. Fix: Run autoregressive generation during eval, not loss-only checks.
- Symptom: A model answers a policy question confidently but cites the wrong company rule. Cause: High next-token probability was treated like evidence, even though the model is generating from patterns instead of querying a live source. Fix: Retrieve the current policy or call a tool before trusting the answer.
- Symptom: Generation gets slower or more expensive as outputs get longer. Cause: The key-value cache saves recomputation but still grows with context length, layers, and batch size. Fix: Watch context budgets and cache memory, not model weight size alone.
- Symptom: Someone claims perplexity and cross-entropy disagree. Cause: They forgot perplexity is
exp(loss)under natural-log averaging, or compared runs with different token accounting. Fix: Hold tokenizer, evaluated tokens, and averaging convention constant before comparing them. - Symptom: Bigram tables, neural n-grams, and GPT-style models feel unrelated. Cause: Architecture changes were mistaken for objective changes. Fix: Trace all three back to the same job: predict the next token from context.