Repository navigation
[Feature]: Layer-major prefill for streamed experts — 4–6× faster long prompts on a 6 GB card (PRs offered) #1670
Description
What do you want to do?
Make prompt reading fast on a small-VRAM card when the experts are streamed (--mmap-experts). On a 6 GB RTX 3060 Laptop with Qwen3.8-Flash-Next Q2_0, a layer-major prefill takes long prompts from 62–99 tok/s to 378–443 tok/s (a 223.7K-token prompt from ~2,500 s to ~520 s), with decode about the same (19–21 tok/s). I have this working on a local branch and would like to open PRs if you're interested.
Why it is slow today
With --prefill auto on 6 GB, the prompt is read in 512-token chunks and every chunk walks all layers, so each layer's routed experts are streamed from the file tier once per chunk. On this box a 12.9K-token prompt moves ~509 GB of expert bytes, almost all of it the same experts re-read 25 times. #1407 looks like the same re-read pattern on a layer split ("file tier re-reads 24–35 GB per request").
What I changed (all opt-in env switches, default off)
STRATA_PREFILL_LAYER_MAJOR=1— layer-major prefill. A super-chunk of up toSTRATA_PREFILL_LM_TOKENS(default 8192, shrunk to free VRAM minusSTRATA_PREFILL_LM_MARGIN_MIB) is read layer by layer. Each 512-token sub-chunk runs attention, the hyper-connection read, the router and the shared expert in order (GDN / QSA / PLE state advances as in the default path). The layer's routed experts then run once for the whole super-chunk: each expert is staged once, its rows from every sub-chunk go into one MMQ group, and the results are added per token. The super-chunk's residuals live in VRAM or pinned RAM (STRATA_PREFILL_LM_R=vram|host|auto, copies overlapped on the copy stream). Every streamed expert of a layer is prefetched when its attention starts, and the ones the routing skips are dropped (STRATA_LM_PREFETCH=0turns this off). Expert traffic drops by about S / 512: the 12.9K prompt goes from ~509 GB to ~79 GB.- Two small kernels (
moe_shared_init,moe_scatter_add). - A stager fix the prefetch needs: a job now waits for the previous job on its pinned buffer, and
skip()marks unneeded jobs ready without reading them. Before, two read threads could write the same buffer. The default walk consumes jobs in order, so it isn't affected. on_chunkstill reports every sub-chunk; serve doesn't checkpoint mid-super-chunk.- Not used for a layer split, a peer GPU, image prompts, the fused layout, or prompts under two chunks.
- Two small kernels (
STRATA_PREFILL_DIRECT=1— split page cache vs O_DIRECT by residency. On the prompt path, each expert blob gets amincoreresidency check. Mostly cached (≥STRATA_PREFILL_DIRECT_MIN, default 50%) means memcpy from the mapping; otherwise it's read with O_DIRECT. Decode stays on the page cache. Prompts no longer evict the decode working set: prefill +3–27% and decode +11–17% on this box.STRATA_PREFILL_WARM=1/STRATA_BIN_WARM=1— the per-layer walk warms this and the next layer's file-tier experts, and the pack'sexperts.binis warmed too (Linux). Small: +1–9% prefill.
Results
Ryzen 7 5800H, RTX 3060 Laptop 6 GB, 27 GiB RAM, Apacer AS2280P4 NVMe (Gen3, 256 GB), Ubuntu 24.04, driver 595.99.02.
Engine: 0.1.39 + 10 commits (idx-int8-524k), with your bit-plane Q2_0 kernel and Linux O_DIRECT tier commits cherry-picked.
Model: Qwen3.8-Flash-Next GSQ-RCO Q2_0 pack. Flags: --mmap-experts --expert-cache auto --prefill auto --kv q4_0 --kv-resident 32768 --max-context 262144 --spec 3 --spec-min-p 0.5 --mtp … --vram-reserve-mib 650.
Methodology: page cache evicted before each run, unique prefix per request (no reuse), temperature 0.6, thinking off; tok/s from the engine's own strata serve: prompt … log lines.
| prompt 7.4K | prompt 8.1K | prompt 12.9K | prompt 51.5K | decode (prose / code) | |
|---|---|---|---|---|---|
| 0.1.39 base | 69 | 74 | 95 | – | 17.6 / – |
| + bit-plane, both warms | 75 | 81 | 96 | – | 18.5 / 18.3 |
+ PREFILL_DIRECT |
95 | 98 | 99 | 62.5 | 20.6 / 21.4 |
+ PREFILL_LAYER_MAJOR |
378 | 406 | 443 | 415 | 19.1 / 19.8 |
223,706-token needle prompt: 2,400–2,611 s before, 498–538 s after (3/3 found).
Correctness with layer-major on, served for agent use:
- 17-check tool-calling test (tool choice from a menu, nested/optional/enum params, multi-turn chains, no-tool and error recovery): 17/17
- needle at 223.7K: 3/3
- BFCL
multi_turn_base: 55.5% (200 entries, 7 h 52 min) - engine errors: none across 2,547 requests
Side finding: with --resident-budget-gib 14 on 27 GiB RAM, the Linux O_DIRECT tier picks unbuffered reads by itself and decode falls from 18.4 to 4.5 tok/s, because decode misses go to the NVMe. STRATA_UNBUFFERED_LOAD=0 restores it. Prompt reads did get faster in that mode (117 vs 77 tok/s); item 2 above gets that gain without the decode loss.
Proposal
My branch is 0.1.39-based and main has moved a lot (#1107, #1247, #1441 also touch prefill). Before rebasing, I'd like to know if you want this and in what shape. My suggestion is three PRs against current main, each re-measured after the rebase:
- Layer-major prefill, with its kernels and the stager fix
PREFILL_DIRECT- The two warm switches
Happy to change names, defaults or structure to fit your plans for the prefill path.
Activity
CC-David-CC commentedon Oct 9, 2026
put your branch up on github and i'll test it. idc that its on 1.39 i have a private branch with some divergences i'm maintaining and i'd love to see how you got this in practice
vizdatom commentedon Oct 9, 2026
Thanks! It's up: https://github.com/vizdatom/Strata/tree/perf-ryzen
Diff against its base: vizdatom/Strata@7026cc8...perf-ryzen
Base: 0.1.39 + 10 commits (
7026cc84, theidx-int8-524kbranch).Cherry-picked from upstream main:
845ac757,97566cf3,6316091ab3980df3The new work:
6b275dc4:STRATA_BIN_WARM1cd6dac4:STRATA_PREFILL_WARMbd841299,1d943c06:STRATA_PREFILL_DIRECT2d8bd284: layer-major kernels00caa81d: stager wait/skip6453e8a7:STRATA_PREFILL_LAYER_MAJOR3720d627: debug dumps only (STRATA_DUMP_*), used for the layer-major A/BThe environment I run it with (engine flags as in the issue):
Layer-major only applies with streamed experts on a single GPU and prompts of two or more chunks. The engine log line
strata serve: prompt N tokens = … read in … ms (… tok/s)is where the gains show. Curious how it behaves on your setup.Niko1221 commentedon Oct 13, 2026
Thanks for the report. To keep the tracker current we are closing issues older than three days: 0.1.44 is out with a long list of fixes, and many older reports are covered by it or by 0.1.43.1.
If this still happens on 0.1.44, please leave a comment here with your engine log (and the version line it prints), and we will reopen it. A new issue that links this one works too.
willthehuman commentedon Oct 14, 2026
Still reproducing on 0.1.44, so per your comment above here is the log and the version line.
Version line the engine prints:
strata 0.1.44.1Setup: i7-8700, 31.9 GB RAM, RTX 4070 SUPER 12 GB, Q2_0,
--mmap-experts --resident-budget-gib 16 --max-context 131072, storage on ntfs3 (DRAM-less NVMe).What the prompt path still does here, from the engine's own per-request line, for a single 17,280-token prompt:
44.2 GB of expert bytes for one 17K-token prompt, with the OS delivering 66 GB and 45,079 major faults; prefill 442.4 tok/s on that request. The same prompt with
STRATA_UNBUFFERED_LOAD=1reads the same bytes at 343 faults and 630.0 tok/s:So on 0.1.44.1 the read shape still dominates the prompt path — the amplification this issue describes is unchanged, and a 12 GB card here streams every layer's experts per chunk the same way you measured on the 6 GB one. Changing the read path recovers 43% of the prefill; it does not reduce the 44 GB of expert traffic, which is the part your layer-major proposal targets.
I do not have the layer-major path to test on 0.1.44.1 (vizdatom's
perf-ryzenbranch is 0.1.39 + 10 commits). Would you reopen this? I am happy to test a rebased branch or an upstream implementation and report numbers with the same protocol — cold cache verified, identical prompt, interleaved arms — plus whatever logs you want.vizdatom commentedon Oct 15, 2026
Hi @willthehuman, thanks for re-measuring on 0.1.44.1, that's exactly the case the issue is about. On small cards, layer-major saves a lot and on cards that don't stream experts, it costs almost nothing.
I've pushed my port onto 0.1.43.1: https://github.com/vizdatom/Strata/tree/layer-major-0.1.43.1 (diff: vizdatom/Strata@a0116b2...layer-major-0.1.43.1). It uses the same env switches as in the issue. On my 6 GB card it needs
--expert-cache 4 --prefill 512 --no-prefill-borrow, because with auto the engine exits at startup.Feel free to take it from here and do whatever you want with it. My hardware is busy with another project now, so a rebase onto 0.1.44 and re-testing aren't in my near-term plans.
solomon159753-oss commentedon Oct 18, 2026
Superseded by #1670 (comment) after moving this report to my durikim1-cpu account. Please use the replacement comment for the measurements and experiment branch.
durikim1-cpu commentedon Oct 18, 2026
Data point from HIP on Windows (AMD RX 7800 XT 16 GB, gfx1101) on a 0.1.44.1 base (the thread so far has a CUDA one on 0.1.44.1): it still matters there too.
Credit first: the layer-major code is @vizdatom's, from his
layer-major-0.1.43.1branch(
2e69b6db"prefill: layer-major prefill (STRATA_PREFILL_LAYER_MAJOR=1)",f0173382kernels for the layer-majorexpert pass,
149b8995the stager wait,7013dad4/bd900165the prompt-path direct reads,7753c2e2the debugdumps). I ported it onto my 0.1.44.1-based tree and ran it on HIP/Windows; I did not design it.
Setup
amdhip64_7.dll10.0.3686.0, hipBLASLt 1.5.0).expert RAM budget 20 GiB,
STRATA_PREFILL_LAYER_MAJOR=1with a 16,384-token super-chunk.buffer and a few existing opt-ins, among them the gfx1101 hipBLASLt table). The candidate adds the port and also carries libratechw's opt-in HIP prompt-attention patches (
STRATA_ATTN_GATE,STRATA_QPREP, not enabled in these runs). It is not a measurement of 0.1.44.1 as released.timing for TTFT. Control, then candidate, then the control again on the same day.
Results (medians of 3)
The control measured again afterwards stayed at 901.4 tok/s, so the gain is not drift. Decode did not get faster
(27.0 sits between the two controls), as on @vizdatom's RTX 3060 Laptop (6 GB) numbers.
Per request, the expert file source's logical reads fell from about 171 GB to 82 GB, average GPU busy rose from
71.7% to 84.5%, and observed physical SSD reads fell from 2.46 to 1.46 GB/s (whole drive). "Logical" is what the
engine asked for, not what came off the SSD.
Correctness on the layer-major arm: a 12-question set 11/12 (the same arithmetic question fails without it),
long-context retrieval 12/12, and a real coding agent (6 tasks × 3) 18/18 functional (15/18 on a stricter
process check).
On top of it (my own, small)
shared-arena-guard.patch(112 added lines inprefill.cpp, both opt-in):STRATA_PREFILL_LM_CACHE_ARENA=1lets thelayer-major pass borrow its storage from the expert cache through the existing lend/refill protocol and return it
before decoding, instead of a separate allocation;
STRATA_PREFILL_LM_CACHE_GUARD=1charges that borrowed capacityonce and keeps the original free-VRAM margin. With a 32,768-token super-chunk and a 23 GiB budget this reached
1,408.9 prompt tok/s / 27.5 decode / 42.70 s TTFT on the same 60K prompt. A 32K super-chunk with a separately
preallocated buffer (20 GiB budget) reached 1,416.1 prompt tok/s but only 20.7 decode. These two rows differ in more
than one setting, so read them as a direction, not an A/B.
Not tested
The port on a clean 0.1.44.1 or current
main, CUDA or Linux builds of the port, contexts above 64K with thesesettings except one 100K check at a 128K context and a 22 GiB budget (100,055 tokens: 1,185.1 prompt tok/s, 23.9 decode, 3/3 retrieval), and any other GPU.
Port and the extra patch, for anyone who wants to try it on HIP/Windows: https://github.com/durikim1-cpu/Strata/tree/rx7800xt-experiments
(
layer-major-port-0.1.44.1-hip.patchis against my tree, not againstmain).If you would consider reopening this for an opt-in layer-major path, I can re-measure the port on a clean build.
prefill stager: a job waits for the previous job on its buffer; skip() (
prefill: the per-layer walk warms this layer's and the next layer's f…
prefill: kernels for a layer-major expert pass (moe_shared_init, moe_…
prefill: layer-major prefill (STRATA_PREFILL_LAYER_MAJOR=1) (#3976, #…
layer-major prefill: STRATA_HC_Q8 dequant, guards for APR/NVFP4/WMMA …
prefill stager: a job waits for the previous job on its buffer; skip() (