Prefetched pretoken-cache encode pipeline + single-pass allocation fixes - #4
Closed
marcelroed wants to merge 6 commits into
Closed
Prefetched pretoken-cache encode pipeline + single-pass allocation fixes#4marcelroed wants to merge 6 commits into
marcelroed wants to merge 6 commits into
Conversation
- ShortPretokenCache: linear-probe table, one cache line per probe, 2MiB-aligned + MADV_HUGEPAGE backing, explicit prefetch API - memoized_encode runs in 256-pretoken chunks via PretokenSpans: pull/key/hash/prefetch a chunk ahead of the probe phase, hiding DRAM latency of the ~1.3M-entry table - Inline 1-2 token encodings in the cache value (98% of hits skip the token_arena load); vocab_inv probe on the miss path - Fused per-scheme fill_spans_keyed for all Fast* pretokenizers - encode_st: ENCODE_TOKENIZER/ENCODE_PASSES knobs, THP re-enable
Each round is an independent cold-cache sample: the pool retains one forked tokenizer (and its pretoken caches) per rayon thread, so reusing it across rounds measured warm-cache reruns of the same input.
In-process repetition reuses allocator arenas, faulted pages, and grown buffers even with fresh worker pools, overstating first-pass throughput. Default to one round (restart the binary for independent samples); ENCODE_ROUNDS>1 stays available. Clear PR_SET_THP_DISABLE like encode_st.
A first pass over fresh input paid for growing every chunk's ids Vec from empty (re-copying ~the final size in doublings, with realloc churn across 255 threads), per-token pushes, and ~2.3M 4 KiB faults on the flat gather buffer. Reserve ids once from a bytes/4 estimate, extend from token slices, and MADV_HUGEPAGE the flat buffer before first touch. Single-pass 10 GB GPT-2 encode on 255 threads: 4.5 -> 6.1 GB/s.
- SpanBatch bundles the spans/keys/hashes arrays that every fill_spans_keyed signature threaded through separately. - fill_spans_keyed_with is the one shared pull-loop body (key/hash/prefetch/store); the mask helper, the DeepSeek fill, and SpanIter are thin walker closures around it. inline(always) core + inline(never) wrappers preserve the fused out-of-line codegen (verified: single-pass and warm encode_st throughput unchanged). - impl_mask_pretoken_spans! replaces five copies of the delegating impl; the redundant iterator-path macro on PretokenizerIter is gone (tests use the SpanIter adapter).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the pretoken-cache
HashMapwith a purpose-built encode pipeline and fixes the allocation behavior that dominated first-pass encoding. Includes the front-cache commit (1c10270) fromoptimize-encode-front-cachein its history; the front cache itself is superseded by the new table.ShortPretokenCache(src/bpe/pretoken_cache.rs): open-addressing table with self-contained 32-byte entries (one cache line per probe vs. hashbrown's two), 2 MiB-alignedMADV_HUGEPAGEbacking, and an explicit prefetch API.memoized_encodepulls 256 pretokens per chunk through the newPretokenSpans::fill_spans_keyedinterface — keys, hashes, and cache-line prefetches are derived in the pretokenizer's fused walker loop, so by probe time the lines are in L1 instead of costing a serial DRAM stall. EachFast*scheme gets a fused out-of-line chunk fill; ~90 % of encodings (1–2 tokens) are stored inline in the entry'su64value, eliminating the dependenttoken_arenaload.src/batch.rs): chunk outputVecs are reserved once from a bytes/4 estimate instead of doubling from empty (which re-copied ~the final size per chunk with realloc churn across 255 threads), token slices are appended withextend, and the multi-GB flat gather buffer isMADV_HUGEPAGEd before first touch.encode_docnow measures one cold round per process (restart for independent samples;ENCODE_ROUNDSopts into repetition). In-process repetition inherits worker pretoken caches, allocator arenas, and faulted pages, overstating steady throughput by ~45 % vs. a true first pass over fresh data — which is the workload that matters.Benchmarks
Whole-document encode (
cargo bench --bench encode_doc), GPT-2 vocab, 10 GB OWT as one document, 255 threads, AMD EPYC 7742 (Zen 2, AVX2). Single pass per process, 5 interleaved samples per variant:encode-perfbaseline (0e27c71)1c10270)fb0be88)All variants produce identical token counts (2,268,672,027). The single-pass gain comes from the allocation fixes; the two cache designs measure within noise of each other on a first pass (with the allocation fix applied to both: 6034 vs. 5995 MB/s), because a first pass is not hit-path-bound. On warm re-encodes of the same input (retained worker pools, earlier 5-round protocol at 10 GB) the pipeline reaches 8–10 GB/s vs. 6.3–6.5 GB/s for the front cache, so the cache redesign is kept for workloads that re-encode.
Also measured and rejected: a vocab-seeded shared cache (prebuilt vocab-entry → token table shared across workers) was 7 % slower single-pass — first-pass misses live in the Zipf tail, and the extra probe per lookup costs more than the saved merges.
Testing
memoized_encode_matches_reference_owt(ignored, run manually): token-for-token differential of the cached pipeline vs. the uncached BPE reference over 50 MB of OWT — all 11.4 M tokens match.span_source_tests: every scheme's fusedfill_spans_keyedreproduces its iterator's spans/keys/hashes exactly, including chunk-boundary and end-of-input handling.cargo test --release: 46 passed; the 6 failures are pre-existing missing-data-file failures on this host (~/data/tokenizers/*.tiktoken), unrelated to this change.pack_pretoken_keygained then == 0guard, which also makes its 16-byte read provably in-bounds).