Speed relative to fastText: x86 command line 4.13x, x86 Python 3.21x, Mac command line 1.72x, Mac Python 1.70x

GlotLID is a fastText model. Projects like FineWeb2 run it over billions of documents, so every bit of prediction speed counts. fastText-turbo is a small fork of fastText that predicts about 4x faster on x86 and 1.7x faster on Apple silicon, on one core, with the same models and the same labels.

pip uninstall fasttext      # both packages provide the `fasttext` module
pip install fasttext-turbo

Where the time goes

To label one sentence, fastText hashes each word and its character n-grams (2 to 5 characters for GlotLID) into rows of a big table. It averages those rows into one vector of 256 numbers. Then it scores that vector against each of the 2,102 labels, and softmax turns the scores into probabilities.

Pipeline: sentence, hash words and n-grams, average about 650 rows, score 2,102 labels, softmax and top-k
Fractions from a profile of fastText on GlotLID v3 (one x86 core, 20,000 sentences).

Two steps take almost all the time:

Everything else (reading, hashing, softmax, top-k) adds up to 7%. The output layer was the place to start.

Why the output layer was slow

fastText computes each label's score as one running total: add a[0]·b[0], then a[1]·b[1], and so on, 256 times.

fastText adds 256 products one after another into one running total

The multiplications are cheap. The trouble is that each addition needs the result of the one before it. A modern core can start several floating-point additions per cycle, but an addition takes about 4 cycles to finish. With only one chain of additions, the core spends most of its time waiting.

Fix 1: eight labels at once, exactly

fastText-turbo scores 8 labels together. Each label still has its own running total, added in exactly fastText's order, so every score is the same number, bit for bit. The 8 chains don't depend on each other, so the core can keep 8 additions in flight instead of 1.

Eight labels are scored side by side, each in fastText's order

One detail matters here: clang on macOS fuses a multiply and an add into one FMA instruction by default. That rounds differently from fastText built elsewhere, so this loop turns FMA contraction off. The training code uses only this exact path, which is why trained models stay byte-identical.

Fix 2: eight lanes inside one label (x86, prediction only)

On x86, an AVX register holds 8 floats. Instead of one total per label, fastText-turbo keeps 8 partial totals for each label: lane 0 adds products 0, 8, 16, …, lane 1 adds 1, 9, 17, …, and so on. One instruction advances all 8 lanes, and at the end the lanes are added together.

Eight lanes add every eighth product of one label, then the lanes are summed

This adds the same numbers in a different order, so the last bits of a score can change. On GlotLID v3 the largest change to any probability was 1.1e-5. A probability of 0.99 still prints as 0.99, and on 20,000 test sentences every top-1 label matched fastText. The lane version runs only when predicting; training never uses it.

On Apple silicon the exact 8-label version was already the fastest, so ARM builds keep it, and their output is byte-identical to fastText.

Smaller exact changes

After: where the time goes now

fastText: 12.7 s output layer, 3.6 s averaging, 1.3 s rest, 17.6 s total. fastText-turbo: 2.0 s, 3.5 s, and the rest, 6.3 s total
Profile on one x86 core, GlotLID v3, 20,000 sentences.

The output layer went from 12.7 s to 2.0 s, 6.5x faster. Averaging the rows did not change, so it is now the biggest cost: 3.5 s of 6.3 s, or 56%.

Where the next speedups are

Averaging is bound by memory. The ~650 rows of a sentence are scattered across a 1.7 GB table, so most reads miss the CPU cache and wait for main memory. Faster arithmetic won't help much here. Reading fewer bytes will:

Each of these changes the model or its storage, not only the code, so none of them is in fastText-turbo. They are candidates for future GlotLID models.

Two training options

We use fastText-turbo to train the next GlotLID, so it has two training options, both off by default. Real inputs are often short, or have been lowercased and stripped of punctuation before language identification. These options show the model such lines during training.

-cropProb trains on a random span of the line; -normProb trains on the lowercased line without punctuation and digits

Both are drawn again for each line in each epoch, and the label is kept. They are not saved in the model, so the result is an ordinary fastText model. With both at 0, training is byte-identical to fastText.

Benchmarks

All numbers are lines per second for GlotLID v3 on 20,000 sentences, on one core unless noted. 1x is fastText just before kpu's commits (6c2204b), measured separately for the command line and for Python, and on each machine. Ubuntu is an x86-64 server; Mac is Apple silicon.

Command line

VersionUbuntuMacOutput vs fastText
fastText before kpu (6c2204b)1,327 · 1.00x4,593 · 1.00xreference
fastText v0.9.21,341 · 1.01x4,581 · 1.00xidentical
kpu/fasterText1,370 · 1.03x4,486 · 0.98xidentical
fastText main (1142dc4)1,382 · 1.04x4,559 · 0.99xidentical
tshev/faster-FastText4,468 · 3.37x7,093 · 1.54xsame labels, probabilities differ
tshev/faster-FastText, giphy branch1,159 · 0.87x7,145 · 1.56xsame labels, probabilities differ
fastText-turbo5,486 · 4.13x7,907 · 1.72xUbuntu: same labels, probabilities within 1.1e-5; Mac: identical

tshev's forks are built with -ffast-math -march=native, so their binaries only run on the CPU they were built for. fastText-turbo uses neither, so the same wheel runs everywhere.

Python

model.predict(list) and one sentence at a time, each package in a fresh virtual environment.

PyPI packageNumPyUbuntu, listUbuntu, one by oneMac, listMac, one by one
fastText before kpu (built from source)11,137 · 1.00x1,125 · 1.00x3,945 · 1.00x3,988 · 1.00x
fasttext 0.9.311,292 · 1.14x1,281 · 1.14x3,904 · 0.99x3,808 · 0.95x
fasttext 0.9.321,289 · 1.13xfails3,998 · 1.01xfails
fasttext-wheel11,263 · 1.11x1,239 · 1.10x3,991 · 1.01x3,931 · 0.99x
fasttext-numpy221,386 · 1.22x1,367 · 1.22x3,941 · 1.00x3,927 · 0.98x
fasttext-numpy2-wheel21,352 · 1.19x1,332 · 1.18x3,972 · 1.01x3,948 · 0.99x
fasttext-predictnonefails1,230 · 1.09xfails3,980 · 1.00x
fasttext-turbo23,652 · 3.21x3,576 · 3.18x6,698 · 1.70x6,655 · 1.67x
fasttext-turbo13,452 · 3.04x3,415 · 3.04x6,765 · 1.71x6,699 · 1.68x

"fails": fasttext 0.9.3 raises a ValueError on single strings under NumPy 2, and fasttext-predict does not accept lists. Every package that ran gave the same top-1 label as fastText on all 20,000 sentences.

Threads

Command line, 200,000 sentences, compared with fastText before kpu on one thread.

ThreadsUbuntu, lines/sUbuntuMac, lines/sMac
fastText, 11,3261.00x3,9861.00x
turbo, 15,5254.17x6,8161.71x
turbo, 212,1589.17x13,8523.48x
turbo, 423,58017.8x27,3596.86x
turbo, 845,87834.6x51,98213.0x
turbo, 12––59,09414.8x
turbo, 1690,16668.0x––
turbo, 32121,69391.7x––
turbo, 44160,688121x––

Try it

pip install fasttext-turbo

import fasttext
model = fasttext.load_model("model.bin")
model.predict("Hello, how are you?", k=3)

The code is at github.com/CommonGlot/fastText-turbo and the package is fasttext-turbo on PyPI. It changes fastText in four commits on top of upstream, so it is easy to review.