Skip to content

hip flashnext: random-access advice on the n-gram table mappings - #1615

Draft
gilby wants to merge 3 commits into
ashhart:mainfrom
gilby:strix/hip-ngram-readahead
Draft

gilby wants to merge 3 commits into
ashhart:mainfrom
gilby:strix/hip-ngram-readahead

Conversation

@gilby

@gilby gilby commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

What this changes

The HIP Flash Next engine advises MADV_RANDOM over the n-gram table tensors' spans after mapping (File.adviseRandom, Checkpoint.adviseRandom), so a gather's random single-row reads fault only their pages instead of whole readahead windows. Fixes #1602. Stacked on #1611 (same files: safetensors.zig, checkpoint.zig, hip/tables.zig); the second commit is this change.

The issue guessed at first-use graph capture; measuring showed otherwise: with graphs off the first request was equally slow, the engine replays 4,563 of 4,571 rounds, and the first request read 1,419,132 KiB from disk with 11,262 major faults while the identical repeat read 0. The slowdown follows new prompt content (new n-gram rows), not server start. The CUDA path already advises its tables this way.

Receipt

  • Environment: TensorFold 7c068895 plus hip flashnext: parallel n-gram gather, release weight pages after upload #1611 and this change, tensorfold-native 1.1.8 built with zig build native-hip -Dhip-arch=gfx1151, Ubuntu archive HIP 7.1 (clang 21), AMD Ryzen AI MAX+ 395 with Radeon 8060S gfx1151 (128 GB unified memory), page cache dropped before each server start. Checkpoint TensorFold/Qwen3.8-Flash-Next-MLX-4bit-MTP revision 2b170fa6, MTP 3 drafts, --parallel 1 --context 131072 --prompt-cache-gib 0, fresh process per run, base and candidate on the same machine.
  • Exactness: token_sha256 of tools/bench_openai.py, tools/bench_concurrent.py and both tools/bench_cache.py prompts identical to the base build; bench_concurrent --alone --serial every cell alone 3/3 and 12/12 equal, serial equal, 0 failed.
  • Decode speed, tools/bench_openai.py --tokens 256 --reps 3 --temperatures 0: first request after start (14-token prompt) 24.4 tok/s on the base, 35.8 / 35.9 / 34.9 tok/s with this change alone over three fresh processes, 35.5 tok/s stacked on hip flashnext: parallel n-gram gather, release weight pages after upload #1611; later requests unchanged (base 50.03 / 43.93, stacked 50.13 / 44.09 median code / chat).
  • Prompt speed, tools/bench_cache.py --require-cold on public 7,377- and 29,364-token arrays, first cold request ttft_s: base 48.44 / 108.97 s; this change alone 28.9 s at 7,377; stacked on hip flashnext: parallel n-gram gather, release weight pages after upload #1611 25.30 / 102.22 s.
  • Tests run: zig build test -Dhip=true --summary all 466/466 steps, 0 failed, on the stacked tree; a new host test checks that adviseRandom marks the tensor's pages random in /proc/self/smaps (fails with the advice disabled: 25 passed, 1 failed). python3 tools/zig/lean_check.py 425 on base and change. git diff --check clean.
  • Not run: hip-kernel-test and the kernel contract (no kernel change). The residual cold cost is random-read latency (about 4 s for 5,888 rows on this disk); parallel reads could cut it further and are left out. Metal and CUDA (Linux HIP path only; the advice is a no-op elsewhere and on heap-copied files).

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.
  • No precision traded for speed. If the bits change, the description says which sums change and why.
  • A new or changed GPU kernel is registered with the kernel contract (docs/kernel-contract.md); each backend's SUMMARY line is in the receipt. (No kernel change.)
  • Prompt processing is no slower than the last release.
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

gilby added 2 commits October 29, 2026 15:07
The prompt's n-gram gather copied each id's rows one at a time on the
host. Rows now locate the shard by division when the shard sizes allow
it and split the ids across up to eight threads into disjoint staging
rows, same bytes. After every weight is on the device the checkpoint's
pages are dropped from the mapping and the page cache, except the
n-gram table spans a gather still reads.

Fixes ashhart#1606
A prompt's n-gram gather reads one 100-byte row per id at random rows,
but the table shards were mapped without advice, so each faulted page
pulled a readahead window: about 1.4 GB read from disk for a first short
request. The table tensors' spans now get MADV_RANDOM after mapping.

Fixes ashhart#1602
@ashhart

ashhart commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Thanks — the random-access advice makes sense for scattered n-gram rows. This PR contains all of #1611 plus the madvise change; could you say whether it replaces 1611 or should be rebased on top of it once 1611 lands? Either works, but we'll only land one copy. Please also add a short description with before/after numbers (page faults, or gather / first-prefill time) so we can see what the advice buys.

@gilby

gilby commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

It stacks on #1611 rather than replacing it: after #1611 lands I'll rebase this so it holds only the advice commit.

What the advice buys, on the same machine and checkpoint (TensorFold/Qwen3.8-Flash-Next-MLX-4bit-MTP, gfx1151), from the description's receipt:

  • Page faults: on the base, the first request after a fresh start read 1,419,132 KiB from disk with 11,262 major faults while the identical repeat read 0. The gather reads one 100-byte row per id at random rows, and without advice each faulted page pulled a readahead window; with MADV_RANDOM only the row pages are read.
  • First request decode (14-token prompt): 24.4 tok/s on the base, 35.8 / 35.9 / 34.9 tok/s with the advice over three fresh processes (35.5 stacked on hip flashnext: parallel n-gram gather, release weight pages after upload #1611); later requests unchanged.
  • First cold prefill (tools/bench_cache.py --require-cold, 7,377 tokens): 48.44 s on the base, 28.9 s with the advice alone, 25.30 s stacked on hip flashnext: parallel n-gram gather, release weight pages after upload #1611.

I also pushed a zig fmt fix for the failing check.

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.

HIP Flash Next: the first request after load decodes at half speed

2 participants