enwik8

enwik8 (Hutter Prize)

Measures how well a model predicts raw Wikipedia text one character at a time, scored as bits per character; lower is better.

Released 2006 Metric bits per character (test) SOTA 0.94 · Transformer-XL 24L + dynamic evaluation (2019)
01

At a glance

Task
Byte-level language modelling / compression
Size
100M bytes; standard split 90M train / 5M valid / 5M test
Metric
Bits per byte (bpb) or bits per character (bpc); lower is better
Source
Raw XML of English Wikipedia, March 2006, including markup
Licence
CC BY-SA (Wikipedia)
The Hutter Prize for Lossless Compression of Human Knowledge
Marcus Hutter · prize.hutter1.net, 2006
Read the paper ↗ Hugging Face ↗
02

What it tests

enwik8 asks a different question from word perplexity: how many bits does the model need per byte of raw text, markup and all? It is the same quantity a compressor minimises, which is why the Hutter Prize uses it to argue that compression is intelligence.

Because the unit is a byte, there is no vocabulary and no tokeniser argument — every model is scored on exactly the same thing, which makes it one of the fairest LM benchmarks. The price is that sequences are long (100 bytes is a sentence), so it rewards long-context architectures.

GPT-2 reached 0.93 bpb zero-shot on the 1.5B model, matching the SOTA of the time (0.99 bpb from Transformer-XL trained on enwik8).

03

Examples

Items from the dataset (or representative items where the originals are not freely redistributable).

What the data looks like
Bytes
<page>\n <title>Anarchism</title>\n <id>12</id>\n <revision>\n <id>42136831</id>\n <timestamp>2006-03-04T01:41:25Z</timestamp>… {{Anarchism}} '''Anarchism''' is a [[political philosophy]] …
Note
XML tags, wiki markup and article text all count.
04

Reported results

Scores reported on this benchmark, with the evaluation setting each one used.

SettingReportedContext
Zero-shot (1.5B)0.93 bpbPrior SOTA 0.99 (Transformer-XL)
05

State of the art

Best known · bits per character (test)
0.94
no human baseline
Model
Transformer-XL 24L + dynamic evaluation
2019
Human baseline
n/a

Expire-Span 0.95, Compressive Transformer 0.97. Research interest moved to larger corpora after 2021. Source: arxiv.org ↗ · checked October 2026.

06

Quickstart

Load the benchmark and score a Hugging Face causal LM on it in a few lines.

1Install
shell
pip install -U datasets transformers evaluate lm-eval sacrebleu rouge-score scikit-learn
2Load the data

text (single row with the whole 100 MB). Split bytes 0–90M / 90–95M / 95–100M yourself for the standard train/valid/test.

python · load
from datasets import load_dataset

ds = load_dataset("LTCB/enwik8", split="train")
print(ds)            # columns: text (single row with the whole 100 MB)
print(ds[0])
3Score a model

Two helpers cover every benchmark in this collection: loglik for anything multiple-choice (score each option, pick the highest) and generate for anything free-form. Swap MODEL for whatever you are evaluating.

python · helpers
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL = "gpt2"                                   # or "Qwen/Qwen2.5-1.5B", "meta-llama/Llama-3.2-1B", …
tok = AutoTokenizer.from_pretrained(MODEL); model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.bfloat16).eval().to("cuda")

@torch.no_grad()
def loglik(context, continuation):
    """Sum of log-probs of `continuation` tokens given `context` — the standard multiple-choice scorer."""
    ids = tok(context + continuation, return_tensors="pt").input_ids.to(model.device)
    n_ctx = len(tok(context).input_ids)
    logp = model(ids).logits.log_softmax(-1)[0, :-1]
    return logp.gather(1, ids[0, 1:, None])[n_ctx - 1:].sum().item()

@torch.no_grad()
def generate(prompt, max_new_tokens=32, stop=None):
    ids = tok(prompt, return_tensors="pt").input_ids.to(model.device)
    out = model.generate(ids, max_new_tokens=max_new_tokens, do_sample=False, pad_token_id=tok.eos_token_id)
    text = tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True)
    return text.split(stop)[0] if stop else text
python · evaluate
DATASET = "LTCB/enwik8"
import torch, math
raw = open("enwik8", "rb").read() if "enwik8" in DATASET else open("text8", "rb").read()
test = raw[95_000_000:]                       # standard split: last 5M bytes
text = test.decode("utf-8", errors="replace")
enc = tok(text, return_tensors="pt").input_ids[0]
nll, max_len, stride = 0.0, 1024, 1024
for i in range(0, enc.size(0) - 1, stride):
    ids = enc[i:i + max_len].unsqueeze(0).to(model.device)
    with torch.no_grad(): nll += model(ids, labels=ids).loss.item() * (ids.size(1) - 1)
print("bits per byte", nll / (math.log(2) * len(test)))
07

Pitfalls

  • Report bits per *byte*; bpc on the UTF-8-decoded text gives a slightly different number.
  • The standard split is by byte offset, not by line.
  • For a subword model, convert: bpb = (total NLL in nats) / (ln 2 × number of bytes).
08

References