Bytes and Unicode

Computers store text as Unicode code points packed into UTF-8 bytes, 1 to 4 per character. Byte-level BPE starts from the 256 possible bytes, so no text is ever unknown. The price is that scripts with more bytes per character, and less training data, cost many more tokens.

Key idea
Every character is a Unicode number stored as 1–4 UTF-8 bytes. Byte-level tokenizers start from the 256 bytes: nothing is unknown, but rare scripts are spelled out byte by byte
Real world
Phone numbers. A local call needs a few digits; an international call to a distant country needs a long prefix first. Every phone in the world can be reached, but some cost more digits to dial.
In an LLM
“😀” is code point U+1F600, stored as 4 bytes: F0 9F 98 80. GPT-2 reads “ 😀” as 2 tokens, Llama 2 as 5, Gemma as 1. And the same text costs GPT-2 about 8 times as many tokens in Hindi as in English.

Characters are numbers

A computer stores text as numbers. Unicode is the agreed list: it gives every character in every writing system a number, its , written U+ and four to six hex digits. “A” is U+0041, “é” is U+00E9, “東” is U+6771, “😀” is U+1F600. Unicode 17.0 (2025) assigns 159,801 characters, out of room for 1,114,112.

Code points still have to be stored as bytes, and a byte only holds a number from 0 to 255. The standard encoding is . It uses 1 byte for the 128 ASCII characters (English letters, digits, basic punctuation), 2 bytes for most other European, Greek, Cyrillic, Hebrew and Arabic letters, 3 bytes for most Asian scripts including Chinese, Japanese, Korean and Hindi, and 4 bytes for emoji and rarer characters.

1 byte: U+0000–007F (ASCII)2 bytes: to U+07FF3 bytes: to U+FFFF4 bytes: to U+10FFFF
“東” is U+6771 = 26,481, which needs 16 bits, so UTF-8 uses 3 bytes
code point in binary: 0110011101110001
11100110byte 1 = E6
10011101byte 2 = 9D
10110001byte 3 = B1
Grey bits are fixed markers: a leading 0 means “one byte”, 110 / 1110 / 11110 mean “start of a 2, 3 or 4-byte character”, and 10 means “continuation”. A decoder can always tell where a character starts.
what you see9 characters
code points9
UTF-8 bytes17
9 code points, 17 bytes. ASCII letters take 1 byte, accented Latin letters 2, Chinese characters 3 and emoji 4. A byte-level tokenizer starts from these bytes, so the same idea costs more raw material in some scripts than in others.
Every character has a Unicode number (its code point). UTF-8 stores that number in 1 to 4 bytes, coloured here by how many. Type anything, or click a character to see its bits.

Try the emoji family. It looks like one character, but it is five code points: woman, an invisible joiner, woman, joiner, girl. That is 18 bytes. A flag is two code points, “regional indicator” letters G and B, that your screen draws as one picture. Tokenizers never see the picture, only the bytes.

UTF-8’s design has two useful properties. ASCII text is unchanged, byte for byte, so old English files are already valid UTF-8. And the first bits of every byte say whether it starts a character or continues one, so a program can always find where characters begin.

Why not start from characters?

BPE, as on the previous page, starts from single characters. But which characters? There are almost 160,000 of them, most very rare. A vocabulary that included them all would spend most of its rows on symbols that hardly ever appear, and one that left them out would turn every missing character into an .

The fix in GPT-2 was to run BPE on bytes instead.

Byte-level BPE

starts from the 256 possible byte values, not from characters. Text is converted to UTF-8 first, and merges are learned over byte sequences. Since every possible text is some sequence of bytes, and all 256 bytes are in the vocabulary, nothing can ever be unknown. Common byte sequences, like “ the” or the three bytes of a frequent Chinese character, are merged into single tokens. Rare ones stay as separate bytes.

There is one practical wrinkle. BPE implementations, and the merges file they save, work with text strings, and many bytes are spaces, control codes or not valid characters on their own. So GPT-2 maps each of the 256 bytes to a printable character first. Bytes that are already printable keep their own character; the other 68 are moved to letters from U+0100 upwards. The space, byte 20 in hex, becomes Ġ, and the newline becomes Ċ. That is why GPT-2’s vocabulary file is full of tokens like “Ġthe”.

0123456789ABCDEF
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
Byte 20 (32) is a space. GPT-2 writes it as “Ġ” (U+0120): the byte is not a printable character by itself, so it was given a stand-in. This is why GPT-2’s vocabulary is full of tokens like “Ġthe”: Ġ is the space.
GPT-2’s byte table. Each cell is one of the 256 possible bytes (row = first hex digit, column = second) with the character GPT-2 uses for it. The 68 amber bytes are spaces, control codes and other unprintable bytes, moved to letters from U+0100 up. Click any cell.

Now watch real text go through all three stages. Notice that a token can end half-way through a character.

1 · UTF-8 bytes, grouped by character · 14
486920F0 9F 98 8020E6 9D B1E4 BA AC
2 · the same bytes as GPT-2’s printable characters
HiĠðŁĺĢĠæĿ±äº¬
3 · GPT-2 tokens
Loading GPT-2 data…
Loading GPT-2’s merges…
Three views of the same text. GPT-2 never sees characters, only bytes: tokens are runs of bytes, and a token can stop half-way through a character (shown as hex in ‹ ›).

“ 😀” is the space plus 4 bytes. GPT-2 has a merge for the space plus the first three bytes (shared by many emoji that start F0 9F 98), but none for the last one, so the emoji is split across two tokens, neither of which is a character on its own. “ 東京” fares worse: five tokens (space included) for two characters. The model has to learn to put such pieces back together.

example · phone numbers

Every phone in the world can be dialled with the ten digits 0 to 9: nobody is unreachable. But a call to your neighbour takes a few digits, while a call to a distant country needs an international prefix and a country code first. Byte-level tokenizers work the same way: any text can be written with the 256 bytes, but text the tokenizer saw rarely in training takes many more tokens to write.

Byte fallback

SentencePiece tokenizers, used by Llama 2, Mistral and Gemma, take a middle road. They build the vocabulary from characters, covering at least 99.95% of the characters in the training data, plus 256 extra tokens <0x00> to <0xFF>, one per byte. A character missing from the vocabulary is written as its UTF-8 bytes using those tokens. This is : nothing is unknown, but byte tokens are only used when needed. (More on SentencePiece in WordPiece and Unigram.)

Here is how six real tokenizers handle “ 😀”:

TokenizerTokensHow
GPT-2 (byte-level BPE, 50K)ĠðŁĺ Ģ2 tokens; the merges stop one byte short
Llama 3 (byte-level BPE, 128K)ĠðŁĺĢ1 token: space and all four bytes merged
Llama 2 (SentencePiece, 32K)▁ <0xF0> <0x9F> <0x98> <0x80>5 tokens: not in the vocabulary, so byte fallback
Gemma 2 (SentencePiece, 256K)▁😀1 token
T5 (SentencePiece, no fallback)▁ <unk>unknown: the emoji is lost
BERT (WordPiece)[UNK]unknown: the emoji is lost

Why some languages cost more

Put the two effects together. A Hindi character takes 3 bytes against 1 for an English letter, and a tokenizer trained mostly on English text learned few merges for Hindi byte sequences. So the same meaning takes many more tokens.

Loading GPT-2 data…
The same text in many languages.

With GPT-2, Article 1 is 33 tokens in English and 296 in Hindi, for the same sentence. Tamil and Burmese come out at about one token per byte: GPT-2 learned almost nothing about them. Newer tokenizers with larger, more multilingual vocabularies close much of the gap: GPT-4o brings Hindi down to 56 tokens. But no tokenizer here is fair to every language.

This matters in practice. Paid APIs charge per token. A context window holds fewer pages. Generation is slower, since it produces one token at a time. And a model has fewer chances to learn from text that is cut into tiny, meaningless pieces. Choosing a bigger vocabulary is the main remedy.

example · the cost of one sentence

Ask an API to translate a 1,000-word English document into Hindi. With a tokenizer like GPT-2’s, the Hindi output would be about 8 times as many tokens as the English input: 8 times the price per word, and 8 times as long to generate. With GPT-4o’s tokenizer it is about 1.6 times. The words are the same; only the tokenizer changed.

going deeper

UTF-8 bit layout

Code pointsBytesBit pattern
U+0000 – U+007F10xxxxxxx
U+0080 – U+07FF2110xxxxx 10xxxxxx
U+0800 – U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 – U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

The x bits hold the code point, most significant first (7, 11, 16 or 21 bits).

GPT-2’s byte table

From OpenAI’s encoder.py:

def bytes_to_unicode():
    bs = list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1)) + list(range(ord("®"), ord("ÿ") + 1))
    cs = bs[:]
    n = 0
    for b in range(256):
        if b not in bs:          # space, control codes, etc.
            bs.append(b)
            cs.append(256 + n)   # move to Ā, ā, Ă, ...
            n += 1
    return dict(zip(bs, map(chr, cs)))

The 188 printable bytes keep their characters; the other 68 become U+0100 to U+0143. Byte 0x20 (space) is the 33rd unprintable byte, so it maps to U+0100 + 32 = U+0120, “Ġ”.

How the language numbers were made

Texts: Article 1 (and Articles 1–30 for the “× English” ratio) of the Universal Declaration of Human Rights, from the NLTK udhr2 corpus. Tokenizers: tiktoken (gpt2, cl100k_base, o200k_base) and the Hugging Face tokenizers of Llama 2 7B, Llama 3 8B and Gemma 2 2B, without special tokens.

In papers

Related

  • bpe - BPE builds a vocabulary by starting from single characters and repeatedly merging the most frequent adjacent pair into a new token. The ordered list of merges is the tokenizer. To encode new text, cut it into chunks with a regular expression, then replay the merges inside each chunk.
  • why-tokens - A model needs a fixed list of units to read and write. Characters make the text very long; whole words need a huge list and still meet words they have never seen. Subword tokens sit in between, and every modern LLM uses them.
  • wordpiece-unigram - BPE is not the only way to build a subword vocabulary. WordPiece (BERT) merges by likelihood and splits words greedily, longest piece first. Unigram (T5, ALBERT) gives every piece a probability and picks the most probable split with the Viterbi algorithm. SentencePiece is the library that trains both on raw text, spaces included.
  • vocab-size - A bigger vocabulary packs more text into each token, so sequences get shorter and every language gets cheaper. But each extra token adds a row to the embedding table and a score the model must compute at every step, and rare tokens get too little training. Models moved from 32k–50k tokens to 128k–262k as they grew.