0Day 0

What a language model predicts

A language model assigns a probability to every possible next token. Cross entropy, perplexity and bits per byte are three ways of reading how much probability it gave the right one.

Everything octlm builds serves one task. Given the tokens so far, output a probability for every token in the vocabulary as a guess for the next one. Generation, training and every metric on this site follow from that.

Definition

A probability for every possible next token

Text is first split into tokens, small pieces with integer IDs. The Day 1 tokenizer has 1,024 of them, the Day 4 one has 8,192. Characters vs bytes covers how the split works. For now a token is a number between 0 and V − 1.

The model reads tokens x1,…,xtx_1, \dots, x_t and produces one score per vocabulary entry, a vector of VV numbers called logits. Softmax turns the scores into probabilities:

p(xt+1=v∣x≤t)=ezv∑u=1Vezup(x_{t+1} = v \mid x_{\le t}) = \frac{e^{z_v}}{\sum_{u=1}^{V} e^{z_u}}

The exponent makes every probability positive, and the division makes them sum to 1. A higher score means a higher probability, and only the differences between scores matter. Adding 5 to every score changes nothing.

That is the whole interface. The model does not output a word. It outputs a distribution, and something outside the model picks from it. It takes the most likely token for greedy decoding, or a random draw for sampling, which the sampling post covers.

Playground

Scoring one guess

Playground

Context: The cat sat on the. The true next token is " mat". Move the model's score for it and watch every metric follow.

" mat"0.245
" floor"0.366
" sofa"0.222
" roof"0.110
" dog"0.041
" moon"0.016
p(correct)0.245softmax of the scores
Cross entropy1.406 nats−ln p
Bits2.028−log₂ p
Perplexity4.08e^loss = 1/p
Bits per byte0.882bits ÷ bytes per token

A uniform guess over these 6 words gives p = 0.167, loss 1.792 nats and perplexity 6. Perplexity reads as "the model is as unsure as a fair choice among this many tokens".

Six candidate tokens with fixed scores except the correct one. The bars are the softmax. Every metric below reads only the probability of the correct token.

What happened?

The loss

Cross entropy, averaged over every position

Training adjusts the model's weights to raise the probability of the token that actually came next. The loss for one position is the negative log of that probability:

ℓt=−ln⁡p(xt+1∣x≤t)\ell_t = -\ln p(x_{t+1} \mid x_{\le t})

Over a whole text of NN predictions, the loss is the average:

L=1N∑t=1N−ln⁡p(xt+1∣x≤t)\mathcal{L} = \frac{1}{N} \sum_{t=1}^{N} -\ln p(x_{t+1} \mid x_{\le t})

Why a logarithm? The probability of the whole text is the product of the per-token probabilities, and the log turns that product into a sum. Minimizing the average negative log probability is the same as maximizing the probability of the training text. It also has a meaning from information theory. −log⁡2p-\log_2 p is the number of bits an ideal compressor using the model would spend on that token. A language model is a compressor, and its loss is its compressed size.

The natural log gives the loss in nats. Dividing by ln⁡2\ln 2 converts to bits. PyTorch's cross_entropy uses nats, and so does every loss field in runs/.

A useful reference point is the loss of a model that knows nothing and spreads probability evenly, which is ln⁡V\ln V. For the Day 1 vocabulary that is ln⁡1024≈6.93\ln 1024 \approx 6.93 nats. A fresh model should start near there. The baseline block shows what happened when ours did not.

Playground
loss, nats (−ln p)bits (−log₂ p)your p
0.01.02.03.04.05.06.00.00.20.40.60.81.0probability of the correct tokenpenaltyloss, nats (−ln p)bits (−log₂ p)your p
Hover the chart to read values.
Loss1.386 nats
Bits2.000
Perplexity4.001 / p
Halving p costs+0.693 natsexactly one bit, at any p

The curve is steep near 0 and flat near 1. A model that gives the right token 1 percent pays 4.6 nats. Raising that to 2 percent saves as much loss as raising 50 percent to 100 percent. The loss punishes confident mistakes far more than it rewards extra confidence.

The penalty the loss assigns to one prediction, as a function of the probability the model gave the correct token. Natural log in blue, base 2 in orange.
Training

One sequence is many training examples

A decoder predicts at every position in parallel. Feed it TT tokens and it returns TT distributions. The target for position tt is the token at t+1t + 1, so the targets are the inputs shifted left by one.

Playground
PositionModel seesMust predict
0
<|bos|>
Once
1
<|bos|>Once
2
<|bos|>Once
upon
3
<|bos|>Once upon
4
<|bos|>Once upon
a
5
<|bos|>Once upon a
6
<|bos|>Once upon a
time
7
<|bos|>Once upon a time
,
8
<|bos|>Once upon a time,
9
<|bos|>Once upon a time,
there
10
<|bos|>Once upon a time, there
11
<|bos|>Once upon a time, there
was
12
<|bos|>Once upon a time, there was
13
<|bos|>Once upon a time, there was
a
14
<|bos|>Once upon a time, there was a
15
<|bos|>Once upon a time, there was a
little
16
<|bos|>Once upon a time, there was a little
17
<|bos|>Once upon a time, there was a little
cat
18
<|bos|>Once upon a time, there was a little cat
.
19
<|bos|>Once upon a time, there was a little cat.
<|eos|>

20 tokens in, 20 predictions out, all from one forward pass. The target row is the input row shifted left by one. The causal mask is what stops position 3 from reading " " in its own input.

Each row is one prediction the model makes during a single forward pass. octlm adds <|bos|> at the start of a document and <|eos|> at the end, so the model also learns how documents begin and end.

What happened?

Perplexity

Perplexity is the loss in a friendlier unit

Perplexity is eLe^{\mathcal{L}}, the exponent of the average loss. It is the geometric mean of 1/p1/p across positions:

PPL⁡=eL=(∏t=1N1p(xt+1∣x≤t))1/N\operatorname{PPL} = e^{\mathcal{L}} = \left(\prod_{t=1}^{N} \frac{1}{p(x_{t+1} \mid x_{\le t})}\right)^{1/N}

A perplexity of 30 means the model is, on average, as unsure as a fair choice among 30 tokens. A uniform model has perplexity VV.

Perplexity has a trap. It depends on what a token is. A tokenizer whose tokens cover more text makes each prediction harder, because each token carries more information, and so its perplexity is higher even when the model is better. The Day 1 results hit this directly: the BPE model's perplexity was 8 times the character model's, and it was the better model. Measuring a tokenizer works through the numbers.

Bits per byte

A unit every tokenizer shares

Every tokenizer in octlm turns text into tokens and back without loss, so the underlying text is the same whatever the split. Bits per byte measures the model against that text:

bpb⁡=∑t−ln⁡p(xt+1∣x≤t)ln⁡2⋅UTF-8 bytes of the targets\operatorname{bpb} = \frac{\sum_t -\ln p(x_{t+1} \mid x_{\le t})}{\ln 2 \cdot \text{UTF-8 bytes of the targets}}

The numerator is the total information in nats over all predictions. The denominator is the number of bytes those predictions covered, times ln⁡2\ln 2 to turn nats into bits. The result is how many bits the model needs per byte of text. Plain UTF-8 English costs 8 bits per byte. A good compressor like gzip reaches about 2 to 3. A trained language model goes lower.

Special tokens like <|eos|> count toward the loss but cover 0 bytes. PAD targets count toward neither.

octlm computes all three numbers in one pass over the validation blocks:

octlm/train.pyline 125
def evaluate(model: Decoder, blocks: TokenBlocks, pad_id: int) -> dict[str, float]:
    model.eval()
    device = next(model.parameters()).device
    nll_sum = 0.0
    token_count = 0
    byte_count = 0
    with torch.inference_mode():
        for inputs, targets, byte_lengths in zip(
            blocks.inputs, blocks.targets, blocks.target_bytes, strict=True
        ):
            inputs, targets = inputs.to(device), targets.to(device)
            byte_lengths = byte_lengths.to(device)
            logits = model(inputs.unsqueeze(0))
            losses = F.cross_entropy(
                logits.flatten(0, 1), targets, ignore_index=pad_id, reduction="none"
            )
            valid = targets != pad_id
            nll_sum += losses[valid].sum().item()
            token_count += valid.sum().item()
            byte_count += byte_lengths[valid].sum().item()
    model.train()
    nll = nll_sum / max(1, token_count)
    return {
        "bits_per_byte": nll_sum / (math.log(2) * max(1, byte_count)),
        "loss": nll,
        "perplexity": math.exp(nll),
        "tokens": token_count,
    }

nll_sum adds up the loss over real targets. byte_count adds up how many bytes those targets decode to, from a per-token table the tokenizer provides. Perplexity comes from the average, bits per byte from the total.

Skipped

What we did not measure, and why