text8
Measures how well a model predicts cleaned-up Wikipedia text (lower-case letters and spaces only) one character at a time.
At a glance
What it tests
text8 is the sanitised sibling of enwik8. The same Wikipedia text, but all markup is gone, everything is lower case, and the alphabet is 26 letters plus space. It is the cleanest possible character-level task.
Because there is no markup to exploit, models score slightly worse here than on enwik8 even though the text is simpler — there is less redundancy to compress.
GPT-2 scored 0.98 bpc zero-shot against a SOTA of 1.08 — the only benchmark in its table where the 345M model already beat the prior best.
Examples
Items from the dataset (or representative items where the originals are not freely redistributable).
Reported results
Scores reported on this benchmark, with the evaluation setting each one used.
| Setting | Reported | Context |
|---|---|---|
| Zero-shot (1.5B) | 0.98 bpc | Prior SOTA 1.08 |
State of the art
Transformer-XL alone 1.08. GPT-2's 0.98 is zero-shot and therefore not directly comparable to models trained on text8. Source: arxiv.org ↗ · checked October 2026.
Quickstart
Load the benchmark and score a Hugging Face causal LM on it in a few lines.
pip install -U datasets transformers evaluate lm-eval sacrebleu rouge-score scikit-learn
Download http://mattmahoney.net/dc/text8.zip (31 MB) — a single file of 100,000,000 characters. Not on the Hub in an official form.
curl -LO http://mattmahoney.net/dc/text8.zip && unzip text8.zip # one 100,000,000-character fileTwo 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.
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
DATASET = "text8" 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)))
Pitfalls
- Same byte-offset split convention as enwik8.
- Digits were spelled out ('one nine nine two'), which a tokeniser trained on normal text handles oddly.
- Word-level perplexity on text8 is sometimes reported (with a 44k vocab) — a different number entirely.
References
- Data and descriptionhttp://mattmahoney.net/dc/textdata.html
- Krause et al. 2019 (dynamic evaluation)https://arxiv.org/abs/1904.08378