prefill: configurable chunk size up to 4096, with --prefill-chunk auto - #53
Open
dwijenpatel wants to merge 1 commit into
Open
prefill: configurable chunk size up to 4096, with --prefill-chunk auto#53dwijenpatel wants to merge 1 commit into
dwijenpatel wants to merge 1 commit into
Conversation
Chunked prefill re-reads each layer's routed experts once per chunk, so expert I/O scales with prompt_tokens / chunk_tokens. On the long- synthesis benchmark case (3,015 prompt tokens, Gemma 4 26B-A4B, M5, prefill-dominated runs, iostat disk totals): chunk expert I/O disk-active 128 182 GB 55 s 512 60 GB 33 s 1024 32 GB 28 s 2048 22 GB 26 s 4096 14 GB 26 s (one sweep of the packed experts: the floor) Full community-protocol A/B (max-new 1024, per-case seeds): wall 86.1 -> 60.1 s, prefill reads 205 -> 50 GB, outputs byte-identical on all three cases, peak RSS unchanged (1606 -> 1578 MiB). - RuntimeConfiguration.allowedPrefillChunkTokens gains 256...4096 - PrefillRuntimeConfig.maxChunkTokens 128 -> 4096, with the measured reasoning recorded at the declaration - scratch clamp follows maxChunkTokens instead of a literal - the FP16 KV ring is sized from the CONFIGURED chunk rather than the static cap, so sliding-window ring memory grows only on opt-in and default installations see no change - CLI: --prefill-chunk <n|auto>; auto picks the smallest allowed size covering the prompt. Default stays 128: no behavior change without the flag. - docs/RUNTIME_CONTROLS.md: new control documented - tests: allowed-set widening, CLI parse/reject cases, scratch scaling Checks: swift build -c release, Scripts/test.sh (520/520), Scripts/check_markdown_links.rb all pass.
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.
Before/after on the community long-synthesis case (Gemma 4 26B-A4B,
M5 24 GB, protocol settings): total request wall time 86.1 s -> 60.1 s,
prefill disk reads 205 GB -> 50 GB, byte-identical output, peak process
RSS unchanged. One flag:
--prefill-chunk auto. Default behavior isunchanged: without the flag nothing changes.
Why prefill re-reads so much
Chunked prefill runs two nested loops: for each 128-token chunk, for each of
the 30 layers, read that layer's routed experts and run the chunk through.
The expert reads in the inner loop are the cost. A 3,015-token prompt is 24
chunks, and each chunk touches most of the expert pool per layer, so the
same expert bytes stream from disk ~24 times: 182 GB of prefill reads for a
model whose packed experts total ~12 GB. Prefill time is almost entirely
that I/O (the disk sits saturated for the whole phase).
Read volume is proportional to the number of chunks, so the fix is fewer,
larger chunks. With a chunk that covers the whole prompt, every layer's
experts are read once per prefill: one sweep of the file, the I/O floor.
Prompts up to 4,096 tokens cover today's product context ceiling.
Measured chunk-size sweep (Gemma 4 26B-A4B, this branch, long-synthesis
prompt with
--max-new 8so the numbers are prefill-dominated; disk-activeseconds from 1 Hz
iostat):14 GB is roughly the install size (13 GB): one sweep, the floor for chunked
prefill. The time curve flattens once the phase stops being I/O-bound, and
4,096 covers the product context ceiling, hence the new cap.
What changed
RuntimeConfiguration.allowedPrefillChunkTokens: [32, 64, 128] -> [32 ... 4096]PrefillRuntimeConfig.maxChunkTokens: 128 -> 4096 (doc comment records the measurement)PrefillChunkScratch: clamp followsmaxChunkTokensinstead of a hard 128KVCacheManager: the sliding-window ring is sized from the configuredchunk, not the static cap, so memory grows only when a larger chunk is
opted into
--prefill-chunk <n|auto>, validated against the allowed set;auto= smallest allowed size covering the prompt; default 128docs/RUNTIME_CONTROLS.md: new control documentedtests, a scratch-scaling test; two existing tests updated because they
froze the old 128 cap as a contract
Evidence
Community benchmark protocol (warmup then fresh measured process, per-case
seeds,
--max-new 1024 --max-context 4096, sampling defaults, no othermodel processes), plus
iostatdisk totals and/usr/bin/time -lpeak RSSon the long case. Experiment shape per RUNTIME_CONTROLS: baseline, then one
changed control.
Machine: MacBook Pro (Mac17,2), Apple M5, 10 cores, 24 GB, internal SSD.
macOS 26.5.2 (25F84). Swift 6.3.3 (swift-driver 1.148.6). Model installed
per README; manifest sha256
1cb53c24...; prompt hashes match the frozenreal-generation-v1set.Gemma 4 26B-A4B (this branch, f9b9b47 + runtime-controls doc):
Gemma is the interesting correctness case: it has sliding-window layers, so
the KV ring actually grows with the chunk size. Peak RSS stays flat because
the ring is sized from the configured chunk (opt-in memory).
Memory accounting. Peak process RSS does not include device-private
Metal allocations, so to be explicit: prefill scratch
(
PrefillChunkScratchLayout.totalPersistentBytes, allMTLBuffer, no modeltensors in Swift heap) grows from ~16 MiB at chunk 128 to ~490 MiB at chunk
4096 on Gemma's dimensions. That cost exists only when a larger chunk is
explicitly requested, and
autonever picks a chunk larger than the promptneeds.
Protocol change: none. Outputs are byte-identical per case at fixed
seed; the bounded-memory model path is preserved (experts still stream via
pread, nothing new touches Swift heap).Checks:
swift build -c releaseclean,Scripts/test.sh520/520,ruby Scripts/check_markdown_links.rball 22 files resolve.Limitations
the cap.
autoresolves from the tokenized prompt length at request time; the Macapp keeps the default (CLI-only control, as documented).
Happy to split the CLI flag from the ceiling raise if you'd prefer them
separate.