Skip to content

perf(qwen-flash): 4096-token prefill chunks with an overlapped n-gram gather - #470

Merged
francescobozzo merged 7 commits into
gufo-org:mainfrom
Nickybuk:pr/a2-ngram-chunk
Oct 7, 2026
Merged

francescobozzo merged 7 commits into
gufo-org:mainfrom
Nickybuk:pr/a2-ngram-chunk

Conversation

@Nickybuk

@Nickybuk Nickybuk commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

This raises the Qwen3.8-Flash-Next prefill chunk from 2,048 to 4,096 tokens and keeps the larger chunk's n-gram gather off the critical path. Outputs stay byte-identical. On private repository prompts served by gufo serve, each 24K–102K-token prompt's median prefill is 6–11% faster on two machines, and the gain grows with length. Prompts under ~8K tokens are not slower (−0.0% to +0.6% per prompt in dedicated rechecks on both machines), and decode is unchanged. Peak memory rises 1.3 GiB at 262K context.

There are four code commits, plus one for the docs:

  1. Fix: prefill logits no longer depend on the chunk size, so the chunk can change without changing outputs.
  2. 128 n-gram readers, woken per read.
  3. Next-chunk prefetch: a prefill chunk reads the next chunk's n-gram rows while it computes.
  4. 4096-token chunks.

Why the chunk size changed outputs

The indexer budget (indexer_top_k) is 2,048 tokens:

  • A prefill batch that crosses the budget runs every query through the packed sparse attention kernel, including the queries below 2,048.
  • A batch that stops at the budget runs those queries on the dense tiles instead.

So the same prompt got different logits depending on where the chunk boundaries fell. At a 2,048-token chunk no batch crosses the budget, so serving was unaffected. Any larger chunk changed outputs: main's gpu_probe --batch 4096 and --batch 8192 dumps differ from --batch 2048 on 4/4 repository documents.

Fix. Executor::Attention splits the launch at the budget. Pre-budget queries always take the dense tiles. The sparse part goes first, because it is the launch that can be refused.

Why 4096 needs the n-gram changes

The PLE at layer 1 needs a gather of n-gram rows for every token in the chunk: 32,768 row lookups at 2,048 tokens and 65,536 at 4,096, served by random 4 KiB O_DIRECT reads except for repeats and cache hits. Today only layer 0 overlaps that gather. At 4,096 the gather doubles while the overlap stays at one layer, so the host waits longer for every chunk. Each part of this PR addresses that in turn:

  • Readers. The pool is the gather's queue depth. Upstream used min(32, cores) readers; this PR uses 128. Readers block in pread, so the core count is not a useful cap. A gather with fewer reads than readers, such as a decode step's, now wakes one reader per read instead of the whole pool.
  • Prefetch. The server already knows the next chunk: Prefill() takes min(max_input_tokens, remaining, PrefillCapacity()) per step. It now passes those tokens to Session::Sync, which passes them to Forward. Once a chunk's own rows have arrived, the executor starts reading the next chunk's rows into a pageable buffer. The gather then overlaps a whole chunk's forward instead of one layer.

The rows are a pure function of the n-gram history and the tokens. The next batch uses them only if both match exactly, from any session, and copies them into the one pinned buffer that uploads and captured graphs read. Any other gather first waits for a prefetch in flight and keeps its rows claimable, so a decode step interleaved by the scheduler does not discard them. A mismatch falls back to the normal read, so outputs cannot change, and nothing is prefetched past max_batch.

Correctness

Upstream gpu_probe decode-mode logit dumps (--prompt-file, --decode-file, --dump), compared by sha256sum:

  • Inputs: 4 documents from private repositories (2 code, 2 prose).
  • Each is a ~24–30K-token prompt followed by 392–491 teacher-forced decode steps.
Compared with main at --batch 2048 main this PR
--batch 2048 — byte-identical, 4/4 docs
--batch 4096 differs on 4/4 docs byte-identical, 4/4 docs
--batch 8192 differs on 4/4 docs byte-identical, 4/4 docs
  • Each build ran every dump on both machines, with the same hashes on A and B.
  • Through gufo serve with MTP, greedy outputs for 3 cases × 2 reps are identical on both builds and both machines: one hash per case and rep.

Speed: private repository prompts

gufo serve llm --context 262144 --sessions 1 --speculative mtp, 12 prompts from private repositories, 8 interleaved ABBA rounds (4 per build, each prompt twice per round), so 8 samples per prompt and build. Prefill tok/s as median [min–max] per prompt.

Machine A

Tokens main this PR Change
6,541 1390 [1375–1404] 1406 [1399–1411] +1.1%
7,614 1417 [1391–1423] 1416 [1146–1419] −0.0%
7,765 1409 [1137–1435] 1408 [1354–1441] −0.1%
7,930 1446 [1220–1452] 1446 [1440–1451] +0.1%
8,447 1375 [1122–1385] 1383 [1300–1389] +0.6%
23,977 1420 [1185–1433] 1506 [1479–1521] +6.1%
26,639 1427 [1288–1435] 1523 [1486–1530] +6.7%
27,205 1412 [1216–1416] 1506 [1503–1512] +6.7%
27,269 1411 [1408–1414] 1501 [1493–1509] +6.4%
30,541 1432 [1428–1437] 1531 [1517–1534] +7.0%
93,416 1380 [1304–1388] 1508 [1505–1511] +9.3%
102,422 1386 [1383–1388] 1511 [1499–1513] +9.1%

Machine B

Tokens main this PR Change
6,541 1307 [594–1340] 1332 [1068–1357] +1.9%
7,614 1391 [1118–1395] 1394 [1015–1397] +0.2%
7,765 1164 [637–1392] 1340 [744–1405] +15.1%
7,930 1410 [1046–1426] 1411 [957–1425] +0.1%
8,447 1326 [683–1347] 1340 [945–1352] +1.1%
23,977 1364 [733–1387] 1456 [1132–1470] +6.7%
26,639 1398 [1012–1407] 1499 [1249–1519] +7.2%
27,205 1368 [1278–1381] 1476 [1394–1490] +7.8%
27,269 1381 [1374–1384] 1473 [1256–1489] +6.7%
30,541 1391 [1372–1407] 1494 [1450–1501] +7.4%
93,416 1347 [1028–1355] 1492 [1486–1498] +10.7%
102,422 1358 [1178–1362] 1496 [1469–1498] +10.2%

Machine B's drive loses most of its random-read rate under sustained random reads, on every build. That is where the low minimums in its table come from, and the 7,765-token +15.1% is that noise: half of main's samples for that prompt were slow ones. The recheck below is the figure to use for short prompts.

Short prompts, rechecked

The five short prompts again, same builds and server flags, in a separate run with more samples. Each round sends every short prompt 4 times with no long prompt before it, then three pairs: the 102K-token prompt followed by the 8,447-token prompt. Interleaved ABBA, 4 rounds per build on each machine: 16 samples per short prompt and 12 per pair. Token counts differ from the table above by up to 20 because each request carries a unique prefix. Median prefill tok/s:

Tokens A main A this PR A change B main B this PR B change
6,560 1436 1442 +0.4% 1414 1422 +0.6%
7,633 1452 1453 +0.1% 1444 1445 +0.0%
7,784 1458 1458 +0.0% 1439 1445 +0.4%
7,949 1461 1460 −0.0% 1447 1447 +0.0%
8,466 1429 1431 +0.1% 1411 1416 +0.4%
8,467 after 102K 1398 1398 +0.0% 1369 1372 +0.2%
102,442 before it 1392 1512 +8.6% 1370 1501 +9.5%

Short prompts are within −0.0% to +0.6% on both machines, including the 8,467-token prompt straight after the 102K-token one.

Speed: gufo bench

gufo bench -m … -p 4096 -n 32 -d 0,32768 -r 6, 8 interleaved ABBA rounds of 6 repetitions, 4 rounds per build. Mean of the round means, tok/s:

Machine Build pp4096 pp4096 @ d32K tg32 tg32 @ d32K
A main 1662 1545 27.13–27.25 26.16–26.19
A this PR 1749 (+5.3%) 1634 (+5.8%) 27.13–27.17 26.14–26.17
B main 1650 1534 26.84–27.01 26.00
B this PR 1739 (+5.4%) 1624 (+5.9%) 26.85–27.01 25.95–26.01

Every round mean is within 0.5% of its build's mean. The round spreads over six repetitions are ± 11–17 tok/s at d0 and ± 0.7–2.3 at d32K on both builds. tg32 is given as the range of the round means.

A served decode benchmark (--sessions 1 --context 262144, MTP, 4 interleaved ABBA rounds, 2 per build, 2 reps per case per round; mean tok/s):

Case A main A this PR B main B this PR
26,671-token prompt, 512 out: prefill 1422 1522 (+7.0%) 1395 1494 (+7.1%)
same, decode 37.18 37.13 36.93 36.87
copy (4,963 tokens in, 1,536 out): decode 64.10 64.05 63.59 63.62
code (52 in, 1,024 out): decode 52.71 52.57 52.26 52.23

Each case has the same output hash on every build and machine.

Memory, threads and concurrent sessions

Peak GTT+VRAM sampled every second, plus threads and RSS of gufo, over a fixed request sequence:

Machine A Machine B
--sessions 1 --context 262144, main 93.32 GiB 93.33 GiB
same, this PR 94.64 GiB (+1.3 GiB) 94.65 GiB (+1.3 GiB)
--sessions 2 --context 131072, main 91.97 GiB 91.98 GiB
same, this PR 93.29 GiB (+1.3 GiB) 93.30 GiB (+1.3 GiB)
threads 51 → 147 51 → 147
RSS 0.80 → 0.81 GiB 0.80 → 0.81 GiB

The extra memory is the 4096-token scratch. The prefetch adds one pageable row buffer of at most 40 MiB, allocated on first use. The 96 extra threads are idle readers blocked on a condition variable. Over the 12-prompt run the peak differs less than this, and on B it is lower on this PR (106.1 vs 107.6 GiB), because the prompt snapshot budget is sized from the memory free at startup.

Concurrent sessions. While another request decodes, the scheduler bounds prefill steps by --prefill-chunk (default 512), not by kPrefillChunkTokens, so other streams' gaps do not change. Measured with one session streaming a code answer while another prefills a 26.6K-token repository prompt (--sessions 2 --context 131072, ABBA, 2 reps per round):

Machine Build Max gap per rep Mean gap Second prompt wall
A main 0.66–0.68 s 0.20–0.22 s 32.4–32.7 s
A this PR 0.66–0.68 s 0.20–0.22 s 32.4 s
B main 0.67–0.69 s 0.21–0.22 s 32.7–33.5 s
B this PR 0.67–1.14 s 0.20–0.22 s 32.7–33.9 s

The typical worst gap is 0.67 s on both builds. One rep on B had a 1.14 s gap; the other three reps of this PR on B were 0.67–0.68 s.

A request that arrives during a long prefill. Any other n-gram gather first waits for a prefetch in flight. So a request that arrives while another session prefills in 4,096-token chunks can wait for that session's prefetch before its own first gather, and if the next step is not the predicted chunk, the prefetched rows go unused.

  • The prefetch starts early in a chunk whose forward takes about 2.7 s, so it has usually finished by then.
  • Time to first token for this case has not been measured.

Who this helps, and what it does not change

  • Helps: single-request prefill of long prompts on Qwen3.8-Flash-Next (measured 24K–102K tokens). The gain grows with depth.
  • Costs: about 1.3 GiB more peak memory, and 96 more idle threads.
  • Storage dependence. The gain comes from keeping the n-gram gather off the critical path, so it depends on the drive. Measured on two different NVMe drives in identical machines; B's drive loses most of its random-read rate under sustained load, and its gain is as large as A's.
    • The prefetch can hide a chunk's gather only while it completes within one chunk's forward, about 2.7 s at 4,096 tokens.
    • A slower drive gets less of the gain, but never a different result: a late prefetch is waited for and used, and a mismatched one falls back to the normal read.
    • SATA and network storage have not been measured.
  • Fallback. If a target loses on long prompts, kPrefillChunkTokens can go back to 2048 without touching the rest. That combination was not measured.
  • Hardware: gfx1151 (Strix Halo, 128 GB) only, the one target this model ships for. The comments record the measurements so they can be revisited per target.
  • Unchanged:
    • outputs;
    • decode speed;
    • prompts below ~8K tokens: −0.0% to +0.6% per prompt in the rechecks;
    • prefill beside an active decode (--prefill-chunk);
    • every other model.

Overlap with open PRs

This merges cleanly with main and with every open PR except #459, where both add a row to the same table in EXPERIMENTS.md. #459 also changes the prefill GEMM kernels; if it lands first, I'll rebase and re-measure.

Method

  • Two Strix Halo machines, A and B (gfx1151, 128 GB, same model files, different NVMe drives). Each build is a nix build of the exact branch and of main 33d1b20, with the same flags.
  • Interleaved ABBA rounds so drift cannot favor one side. Each round recorded kernel OOM, memory-compaction and allocation-stall counters. There were no OOMs and no allocation stalls. The only compaction stalls (66 on A, 41 on B) were in main's first round of the 12-prompt run on each machine.
  • Prompt text stayed on the machines; only numbers were collected.

🤖 Generated with Claude Code

@fedeizzo
fedeizzo self-requested a review October 7, 2026 11:36
@fedeizzo

fedeizzo commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

I tested this locally against the PR's base using matched production builds on gfx1151. Vision functionality passes in AR and MTP, and large MTP contexts work through 238,884 tokens. The roughly 1.32 GiB increase in GPU allocations is confirmed and fits this 128 GB machine.

However, I found a repeatable MTP decode slowdown after switching from text to vision, including with the default automatic cache budget:

  • Warmed image answers: 73 → 237 ms, exceeding the slowdown threshold in 7/8 paired comparisons.
  • Subsequent arithmetic text requests: 72 → 169 ms, exceeding it in 6/6 comparisons.

Reproduction sequence: C1, context 262144, MTP with seven drafts, thinking off, greedy sampling, cache lookup disabled; send two identical 4,648-token text requests, then repeated 128×128 image/color requests, followed by arithmetic requests. ABBA comparisons produce identical outputs.

I'd investigate and resolve this before merging. Correctness looks good, but the performance regression remains unexplained.

All local validation results: correctness, performance, vision, large contexts and memory

Environment and method

Candidate 5bddc5230808f1e7bbafcefc0c278ec978770895, exact author base 33d1b208a72e9664fdc1267ae1c0d465cfb9b8f9. Ryzen AI Max+ 395, gfx1151, 128 GB UMA, NVMe on encrypted Btrfs; Linux 7.2.9, pinned production Nix toolchain, ROCm 7.2.3 / HIP Clang 22. Local Flash-Next UD-Q4_K_XL, shared Q8_0 MTP sidecar and BF16 mmproj.

Performance controls use matched production binaries and workload histories. ABBA means base/PR/PR/base. HTTP slowdown flags use 5% plus 3 ms per request/phase; medians below summarize a shape without clearing individual flags. Functional suites record candidate correctness; they are not all matched timing qualifications.

Builds and correctness

Check Scope Result
Production Nix builds Exact base and PR, same toolchain Pass
N-gram test Focused CPU test Pass
Independent attention operator GPU against independent FP64 formulas Pass
Native session prefill AR/MTP, images, graphs, sampled snapshots, chunk boundaries through 4096 Pass
Numerical chunk comparison 21,858 input tokens + 40 teacher-forced steps; base batch 2048, PR 2048/4096/8192 9,932,800 finite logits, byte-identical across all four runs
HTTP batch/concurrent sessions AR and MTP Pass
HTTP cancellation Text, thinking and tool requests; AR/MTP Pass
RAM continuation AR/MTP Pass
Disk restart AR/MTP Pass
Growing disk checkpoints AR/MTP Pass
Text long-context continuation About 12K tokens, automatic cache, AR/MTP Exact replay prefill 0; cancellation resume prefill 24, retry 0
Vision discovery/input suites PNG, JPEG, lossless/lossy WebP; Chat/Responses; buffered/streaming; invalid input and recovery Pass in AR/MTP
Image long-context continuation About 12.4K tokens, cached replay, endpoint continuation and cancellation/resume Pass in AR/MTP
Matched image outputs 128×128, 1024×1024, two images, image-plus-code generation Correct answers; messages identical across builds/repeats

Actual draft execution was checked: AR has zero drafts; MTP functional cache/batch checks execute 708 drafts, separate text long-context checks 117, and vision suites 214 (142 accepted). Numerical-logit SHA256: 98f6f92cedf1f52afaf83444cf348a6f4aa1353449ccdc4692aa72525691edfc.

Served text prefill and deep decode

C1, context 262144, MTP/7 drafts, greedy, thinking off, fixed 1 GiB cache, uncached inputs, warmed execution shapes, ABBA.

Prompt tokens Base prefill tok/s PR prefill tok/s Change
7,385 1356.62 1414.12 +4.24%
26,618 1314.56 1361.30 +3.56%
89,348 1253.38 1341.15 +7.00%

All outputs match; no paired prefill phase is flagged. The gains partially reproduce the claim, with smaller gains on these public prompts.

Decode control at 89,348-token depth Base ms PR ms Result
Original 32-token output, first pair 908.20 959.51 Flagged; remains inconclusive/unqualified
Original 32-token output, second pair 893.64 903.84 Within threshold
Focused 128-token output, first pair 3293.097 3293.376 +0.008%; no phase flag
Focused 128-token output, second pair 3292.627 3304.477 +0.360%; no phase flag

The focused longer outputs match and execute 140 drafts each. They find no sustained text-only deep-decode regression; the original short-output flag remains retained.

Standard AR benchmark

Production, depth zero, warmed, three repetitions per point in ABBA. Values are means of the two round means per build; all output hashes match and actual draft counts are zero.

Point Base tok/s PR tok/s Change
pp2048 1495.125 1467.180 -1.869%
pp4096 1470.910 1520.455 +3.368%
tg128 26.855 26.880 +0.093%

Concurrent serving

C2, context 131072, MTP, ABBA: one 512-token code stream is already decoding when a second 26,618-token prefill arrives.

Check Base PR Result
Generated outputs Both streams Both streams Identical across all rounds
Active-decode prefill chunks 52, maximum 512 tokens 52, maximum 512 tokens Expected bounded work
Maximum SSE gap, seconds 0.757 / 0.768 0.766 / 0.753 No paired request-phase flag
Second-request wall time, seconds 36.135 / 36.546 36.134 / 36.286 No paired request-phase flag

Small text prompts before vision

C1/MTP, context 262144, fixed 1 GiB cache, ABBA, warmed, four measured repetitions per shape/build round. All generated messages match. Positive latency change means slower.

Prompt Actual tokens Median prefill latency change Median decode latency change
Arithmetic 26 +0.28% +3.58%
Coding 23 -0.31% +0.24%
Short archive 640 +0.58% +0.26%
Repeated-word tail boundary 2088 +2.29% +1.01%
Archive 2725 -12.03% +0.74%
Archive 4648 +0.70% +0.49%

Arithmetic and tail each have one isolated decode flag: 71.51→80.89 ms and 76.64→87.35 ms. Those points remain inconclusive/unqualified.

Initial image timings and focused transition controls

Initial ABBA image measurements, fixed 1 GiB cache; messages match. Decode is flagged in both pairs for every image shape.

Image input Median prefill latency change Base median decode ms PR median decode ms
128×128 -1.08% 75.51 198.07
1024×1024 +5.58% 74.90 213.25
Two 128×128 images -1.12% 75.94 193.20

The two initial 1024-pixel prefill/TTFT flags are not reproduced in focused controls; retain them as inconclusive. The decode slowdown is reproduced.

Focused ABBA, same 4,648-token preceding text history and fixed 1 GiB cache, every image shape warmed, followed by text controls:

Request after vision activation Base median decode ms PR median decode ms Flagged paired decode requests
128×128 image, one-word color 73.43 198.75 8/8
1024×1024 image, one-word color 74.05 146.59 8/8
Two images, one-word color 72.61 159.16 8/8
Image plus 128-token code answer 2450.23 2669.63 4/6
Arithmetic text, 26-token input 72.56 169.18 6/6
Tail text, 2088-token input 75.32 264.48 6/6

Messages match throughout; the 128-token answers execute 122 drafts each. The roughly 9% longer code-answer decode shows that the finding also affects a substantial output.

Additional ABBA using the default automatic cache budget:

Request Base median decode ms PR median decode ms Flagged paired decode requests
First image after text 71.61 71.41 0/2
Repeated warmed image 72.52 236.76 7/8
Subsequent arithmetic text 71.78 169.02 6/6

One warmed PR image sample is fast; the slowdown is not asserted for every request. In a separate run that activates vision before large text prefill, PR decoding is normal (77.95 ms first image, 182.03 ms at 238K depth). That different history does not qualify the text-to-vision transition above. Root cause remains unisolated.

Actually occupied large contexts

C1, context limit 262144, MTP/mmproj, fixed 1 GiB cache; one matched base/PR sequence, not an ABBA speed qualification. All inputs are fully prefilled and all outputs match: text marker cobalt-river-73, last image color blue.

Actual input tokens Correctness Base prefill s PR prefill s Base decode ms PR decode ms
124,504 Pass 99.851 95.128 171.77 446.91
238,884 Pass 202.728 191.731 188.07 464.65
124,635, two images after text Pass 101.417 96.105 80.99 260.49

Faster prefill does not clear the slower decode phases.

Memory

Global GTT+VRAM and process/system memory sampled every 200–250 ms. GPU allocations consume shared RAM on UMA; GPU and process RSS counters overlap and should not simply be added.

Workload / metric Base PR Observation
Original C1 text sequence, peak GPU GiB 90.378 91.700 +1.322 GiB
Original C1 text sequence, peak process RSS MiB 822.5 826.6 Startup peak masks steady-state cost
C2 sequence, peak GPU GiB 90.224 91.546 / 92.079 +1.322 to +1.855 GiB including transient
Large-context/image sequence, peak GPU GiB 90.939 92.261 +1.322 GiB
Large-context/image sequence, final anonymous RSS MiB 497.93 565.45 +67.5 MiB
Large-context/image sequence, final total RSS MiB 1515.82 1562.38 +46.6 MiB; file residency also changes
Large-context/image sequence, minimum available GiB 21.68 20.67 Both retain headroom
Maximum threads 51 147 +96
Large-context OOM / compaction / allocation-stall deltas 0 / 0 / 0 0 / 0 / 0 No swap configured
PR-only automatic-cache 238K run, peak GPU GiB — 98.148 Cache budget 11.435 GiB
Same automatic-cache run, peak process RSS MiB — 1592.33 Candidate memory check
Same automatic-cache run, minimum available GiB — 14.688 OOM/allocation-stall deltas zero

Cache-budget and setup limitations

Run / initial expectation Observed result Assessment
Initial 8192-batch numerical comparison Accidentally used a longer prompt Setup mismatch retained; matching-input rerun passes byte-identically
Initial roughly 12K replay with explicit 1 GiB cap Final checkpoint skipped with byte_capacity Corrected automatic-budget AR/MTP runs pass with zero-prefill replay
Default-budget 238,884-token replay, initially asserted zero prefill 238,877 cached tokens, exactly seven tokens recomputed; answer matches Partial replay works. Zero-prefill assertion exits 1 and remains retained; logs explicitly show final checkpoint exceeds the byte budget

For the last case, 11,845,269,388 retained bytes plus the requested 6,679,072,616-byte final checkpoint exceed the 12,278,530,048-byte budget. Zero-prefill replay is not claimed at that occupancy/budget.

Raw commands, requests, timings, memory traces, corrected setup failures and outputs are retained locally. These checks do not reproduce the author's private prompts, second machine, SATA/network storage, every request-arrival history, 392–491-step numerical trace or separate d32K benchmark matrix. Vision checks establish functionality, not a new upstream encoder-accuracy qualification. No full model sweep was run.

@Nickybuk

Nickybuk commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed numbers. I tried hard to reproduce this on both of my machines (Ryzen AI Max+ 395, 128 GB, Linux 7.0) and it does not show up. Each test ran the PR's base and this PR (code identical to 5bddc523), a fresh gufo serve per side in ABBA order, two to four server starts per side. All used your settings: C1, 262144 context, MTP with 7 drafts, greedy, thinking off, cache_prompt: false, BF16 mmproj loaded.

Median server predicted_ms, base → PR:

Test Result
4,648-token text, then warmed 128×128 / 1024×1024 / two-image requests, arithmetic, and images behind the text turn (1 GiB and auto cache, code and prose prompts) every phase within ±3 %, except one repeated colour pass (+8.2 % on one machine, −5.1 % on the other)
124K text → 238K text → 124K text + two images 993 → 991, 1012 → 1021, 159 → 159
Same short sequence with 6 and 9 GiB of GPU memory held by another process (minimum available 16.3 and 13.3 GiB) within ±2 %
Same short sequence with the n-gram shard on dm-crypt + Btrfs (compress=zstd:1) within ±0.7 %

Outputs are identical in every run.

I also checked whether the PR's extra 1.32 GiB could cross a TTM page limit: with ttm.pages_limit set between the two builds' peaks, the PR refuses to load instead of running slowly, so that is not what you are seeing.

The one slow state I did see came from a test setup that left the n-gram reads short of page cache (a buffered loop device). There, decode slowed by 15–60 % on both builds, so decode is sensitive to n-gram read latency, but it did not separate the base from the PR.

Since your vision-first run is normal, this looks like a state or ordering effect on your machine rather than a fixed cost. These would help narrow it down:

  1. Your sequence on the individual commits. Each one builds and serves on its own:
    1. e9e64fd9: dense split only
    2. b44c7e4a: + 128 readers, woken per read (your 51 → 147 threads)
    3. f5b0215a: + next-chunk prefetch
    4. 92af4db0: + 4096-token chunks (the +1.32 GiB; code identical to the PR head)
  2. The [http] ... event=completed log lines of one slow and one fast request (they include queue_ms, ttft_ms, decode_tps, draft_* and host_available_mib), and the load_completed line of both builds.
  3. mount | grep btrfs for the volume that holds the model, cat /proc/cmdline, and which distribution and kernel build you're on (/etc/os-release), so I can test on the closest kernel I can get.

If it bisects to a commit, I'll fix it here before merge.

Nickybuk and others added 7 commits October 7, 2026 20:56
@francescobozzo

Copy link
Copy Markdown
Collaborator

Pushed fixes and refreshed the benchmarks and project README.

  • Removed the extra 1.34 GiB scratch allocation and 96 reader threads, keeping 32 SSD readers.
  • Short-request TTFT during overlapping prefill improved from 6.36 s to 2.91 s (main: 3.79 s).
  • The 26K code-review control retains 1,429 tok/s pp, versus 1,333 on main. The full Performance-profile sweep measured 1,645 tok/s pp at d0, 1,446 at d128K, and up to 59.02 tok/s tg with MTP.

Numerical checks retain byte-identical logits; AR/MTP, batching, images, cancellation and cache replay checks pass. Small queue/snapshot timing flags remain inconclusive.

@francescobozzo
francescobozzo requested review from francescobozzo and removed request for fedeizzo October 7, 2026 22:14
@francescobozzo

Copy link
Copy Markdown
Collaborator

@Nickybuk always a please to see your PRs! When is the next one coming? 😂

I run this locally and can confirm the performance improvement on large prompts. On small batches up to pp2048 I don't see any regressions with respect to main

@francescobozzo
francescobozzo merged commit 47b6391 into gufo-org:main Oct 7, 2026
2 checks passed
Chiakai-Chang pushed a commit to Chiakai-Chang/gufo that referenced this pull request Oct 10, 2026
…-org#485)

# Conflicts:
#	src/cli/serve/continuation_cache.cpp
#	src/cli/serve/http_server.cpp
#	src/cli/serve/openai_chat.cpp
#	src/models/qwen/chat_template.cpp
#	src/models/qwen38_flash_next/engine.cpp
#	src/models/qwen38_flash_next/kernels/rocm/executor.cpp
#	src/models/qwen38_flash_next/kernels/rocm/executor.hpp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants