Skip to content

encode_ordinary_batch — reproducible multi-second tail stalls on 32-core box (o200k_base, num_threads=8) #530

Description

@daechoi

Running encode_ordinary_batch with its default num_threads=8 on a 32-core Sapphire Rapids box, I'm seeing large tail spikes on the slowest run of every 10-run batch. Same input, same build, the worst run is 1.1× to 7.6× slower than the median depending on corpus. Multilingual text is the worst offender: two back-to-back runs produced worst-of-10 times of 9.3s and 14.6s, against medians of 5.0s and 4.8s.

I read the performance comment in src/lib.rs around line 232 that documents the fancy_regex/regex scratch-buffer contention and the thread-local clone workaround. That's clearly the right fix for the contention it describes — but the tail spikes I'm seeing are on top of that workaround, so something else is going on.

Repro

import time, tiktoken

enc = tiktoken.get_encoding("o200k_base")
BATCH_SIZE, RUNS = 256, 10

with open("corpora/multilingual.txt") as f:
    text = f.read()
docs = [text] * BATCH_SIZE

enc.encode_ordinary_batch([text])  # warm up

times = []
for _ in range(RUNS):
    t0 = time.perf_counter()
    enc.encode_ordinary_batch(docs)
    times.append(time.perf_counter() - t0)

times.sort()
print(f"median: {times[RUNS // 2] * 1000:.0f} ms")
print(f"worst : {times[-1] * 1000:.0f} ms  ({times[-1] / times[RUNS // 2]:.1f}x median)")

Full harness with five corpora, parity-checked against riptoken: https://github.com/daechoi/riptoken/blob/main/scripts/bench.py

Numbers

Two consecutive full-harness runs. All times in ms. docs is [text] * 256 per corpus, num_threads=8 default.

Run 1:

corpus tokens/batch median worst-of-10 worst/median
english prose 10,240,256 953 7018 7.4×
python source 18,560,256 3015 7044 2.3×
rust source 22,528,256 3596 10015 2.8×
multilingual + emoji 21,913,600 5027 9287 1.8×
random-ish bytes 30,720,000 2856 3247 1.1×

Run 2 (immediate re-run, same machine, same build):

corpus median worst-of-10 worst/median
english prose 1227 9285 7.6×
python source 3059 8663 2.8×
rust source 5231 8775 1.7×
multilingual + emoji 4819 14567 3.0×
random-ish bytes 2856 3266 1.1×

Multilingual worst-of-10 moved from 9.3s to 14.6s between runs. The variance doesn't track corpus size — random-ish bytes has the most tokens but the tightest ratio, and english prose (smallest by token count) has the worst ratio. So whatever's going on, it's not a straight "more work, more variance" story.

Environment

  • 32-core Intel Sapphire Rapids, 164 GB RAM, Ubuntu 24.04, Linux 6.8.0-1030-ibm
  • Python 3.12.3
  • tiktoken 0.12.0
  • o200k_base

Where I am

I don't have a root cause yet. The regex_tls array at src/lib.rs:325 is 128 pre-cloned regex instances indexed by hash_current_thread() % 128; with 8 threads, slot collisions should be rare, so the residual variance is presumably not the same scratch-buffer contention the comment already addresses. Candidates I'd want to rule out: GIL scheduling under ThreadPoolExecutor, allocator pressure on the batch output path, something in the merge path. Would need to profile a worst run with py-spy / perf to say.

If this is a known issue you've already looked at, feel free to close. If it's useful I'm happy to profile and come back with a more specific report, or PR the benchmark harness if you want it in tree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions