Prefetch: capacity-mode readahead, plus the model and accuracy experiment - #5
Prefetch: capacity-mode readahead, plus the model and accuracy experiment#5SHTUSIST wants to merge 10 commits into
Conversation
…both HBF's case rests on the accelerator issuing a read before the data is needed. HBFSim models no prefetch at all, so the paper had no way to say what a prefetcher would be worth, and the question could not wait for a device-side implementation. This adds a deterministic model of prefetching, the sweep that turns it into a figure, and the document that says what the numbers may be used to claim. THE FIGURE. Prefetch accuracy on the x axis, modeled time on the y axis, one curve per media latency tR at 1, 2, 4, 5, 10 and 20 microseconds. Accuracy alone does not decide the time, because a correct prediction still arrives too late if it was issued too near its use, if bandwidth is short, or if the staging buffer dropped it. That objection is answered by the curve family rather than by a different x axis: how accurate shows on the x axis, whether it arrives in time shows as the spacing between curves and as whether a curve still falls near accuracy 1. THE MODEL, in src/prefetch/prefetch_model.cpp, is a discrete-event simulation over an access stream. Three policies: None, which is what HBFSim does today and is the baseline; NextPage, which fetches page N+1 on a demand for page N and carries no model of the workload; and Accuracy, which predicts the page the stream will demand `lead_distance` accesses later and is right with a stated probability, which is how the axis is swept without inventing a predictor. Four knobs, each a real limit: lead distance, staging capacity, media concurrency, and the accelerator time between accesses that a prefetch has to hide behind. The model is not a measurement of any predictor, GPU or device, and the artifacts carry that sentence in a `disclaimer` field. WHAT CAME OUT. The naive next-page policy reaches accuracy 0.99995 on a sequential stream, 0.00035 on a random one, and 0.99958 on a Mixture-of-Experts stream shaped like Qwen3-30B-A3B. Its accuracy on a Mixture-of-Experts stream is (P-1)/P for P pages per expert, measured at 0.029, 0.514, 0.757, 0.878 and 0.939 for P of 1, 2, 4, 8 and 16 against a theoretical 0.000, 0.500, 0.750, 0.875 and 0.938. At the real expert size -- 3 x 2048 x 768 parameters in bf16 is 9,437,184 bytes, or 2304 pages of 4 KiB -- one token is 884,736 page accesses. Without prefetch all 884,736 stall, for 12,386,304,000 ns modeled. With the naive policy 374 stall, for 6,194,648,000 ns, a speedup of 2.00. The 374 are the first page of each expert, against 48 layers x 8 experts = 384. So "expert routing cannot be predicted" is about which expert, not about how many bytes. The unpredictable part is 384 pages out of 884,736, which is 0.04%; the rest is reading forward inside an expert already chosen, and a prefetcher with no model at all covers it. The speedup stops at 2.00 because at that setting the media read is longer than the compute between accesses, so bandwidth and concurrency bound the result rather than accuracy -- which is the reason the figure needs the curve family. TESTS. tests/cpu/prefetch_model_test.cpp holds 12 property assertions, written before the implementation. They pin what the paper's latency argument depends on: accuracy 0 buys nothing, lead distance 0 hides nothing even at accuracy 1, time is monotone in accuracy, a one-page buffer loses a prefetch issued eight accesses early, and one media read at a time still stalls at accuracy 1. One assertion was wrong on its first run: it demanded no stall at all under a perfect prefetcher, and the model was right to disagree, because a prefetcher issuing L accesses ahead cannot cover the first L accesses. It now asserts the misses equal exactly that warm-up. The media servers use a min-heap rather than a scan so a large concurrency setting stays cheap on million-access streams. All 252 swept cells are identical before and after that change. benchmarks/prefetch/README.md lists every file, the build, and the commands that reproduce each result. docs/46-预取实验设计.md carries the design, and section 8.0 of docs/45-预取与延迟掩盖的核实.md records that its earlier recommendation against an accuracy x axis is superseded by this one, and that its Mixture-of-Experts section reads more pessimistically than these numbers support. Test suite: the same four tests fail before and after (context_lifecycle, vmem_tuning, run_with_bpftime, mqsim_benchmark), all pre-existing in a CPU-only build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prefetch model in this branch says what a prefetcher would be worth. Nothing made HBFSim actually fetch anything ahead of its use. This adds that for capacity mode: on a demand miss for page N, the page service queues N+1 through N+k, and the worker fetches them when it has no request to serve. This is a system-side prefetcher, not an application-side one. The application does not ask for it and does not know it happened, and the only thing the policy has to go on is which pages have already been demanded. It is off unless switched on, so no existing measurement moves. THREE DESIGN DECISIONS, THE SECOND OF WHICH I GOT WRONG FIRST. Queueing happens after the demand is served, never before. The miss path is a synchronous blocking read under one mutex, so reading a second page there would double the synchronous work on the path a warp is blocked on. Readahead may take a clean victim once the cache is full. The first version refused to evict at all, and measurement showed that makes the feature do nothing in the only regime capacity mode exists for: on a 1024-access Mixture-of-Experts stream with 128 frames it fetched 22 pages and skipped 1970, because a warm cache never has a free frame. A dirty victim is still refused and put back, because writing it costs a program on the media and a program is dearer than the read the readahead would save. A readahead page is published unreferenced, so hbm_cache.cpp publish() gained a defaulted `referenced` parameter and every existing caller is unchanged. The replacement policy is CLOCK, so a page nothing ever asks for is now the first candidate to be evicted again rather than the last. WHAT IT IS WORTH. hbf_capacity_readahead_bench drives the real page service and reports media reads, not wall-clock time. With the readahead depth at least as large as one expert, the implementation reproduces what the model predicts for a next-page policy, which is (P-1)/P for P pages per expert: P=4 depth=4 implementation 0.7559 model 0.7500 P=8 depth=8 implementation 0.8916 model 0.8750 P=16 depth=16 implementation 0.9370 model 0.9375 P=16 depth=32 implementation 0.9443 model 0.9375 Below that the depth caps the hit rate near D/(D+1): at depth 4, P of 8 and 16 both stop at 0.7500 and 0.7568. THE RESULT THAT MATTERS MOST IS A CLIFF. At P=8, depth 8, 256 frames, varying how many queued pages the worker drains between two demands: drain fully hit 0.8916 media reads -86.79% drain 8 hit 0.8916 media reads -86.79% drain 4 hit 0.8916 media reads -86.79% drain 2 hit 0.8916 media reads -86.79% drain 1 hit 0.0635 media reads +14.17% Draining slower than the queue fills does not degrade the benefit gradually. It turns an 86.79% reduction in media reads into a 14.17% increase, because queued pages go stale before they are used while still costing bandwidth and frames. Any deployment of this has to keep the drain rate above the fill rate. tests/cpu/capacity_readahead_test.cpp covers the contract: off by default, the queue is not drained on the demand's own path, a dirty victim is refused, a demanded page survives the readahead that follows it, a page that became resident between queueing and draining is skipped, and a readahead past the end of the backing store is skipped rather than failed. Test suite: the same four tests fail before and after (context_lifecycle, vmem_tuning, run_with_bpftime, mqsim_benchmark), all pre-existing in a CPU-only build; the count goes from 33 to 34. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e README The submodule pointer was already behind two commits made inside paper/ by an earlier session; this moves it forward to include those and the prefetch reference document added there. benchmarks/prefetch/README.md now lists the real readahead alongside the model, with the commands that reproduce the agreement at (P-1)/P and the drain-rate result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zzyuanyi
left a comment
There was a problem hiding this comment.
Blocking correctness issues found while reproducing the benchmarks. (1) hbf_capacity_readahead_bench labels demand misses as media_reads and computes media_reads_avoided_fraction from that value, but every successful readahead_pages_fetched is also a backing-store media read. Reproduced at P=8/depth=8/256 frames: drain=0 reports 111 reads and -86.79%, but the implementation performed 111 demand reads + 868 readahead reads = 979 versus the 840-read baseline (+16.55%). drain=1 reports 959 and +14.17%, but actually performed 959 + 825 = 1784 (+112.38%). This reverses the headline conclusion. (2) drain_per_demand is passed as an extra printf argument but has no JSON placeholder, producing -Wformat-extra-args and omitting the parameter that determines the result. (3) fill_free_frame_locked completes a clean eviction before the backing read/copy succeeds; an out-of-range or failed speculative read therefore discards a valid resident page, which test case 4 does not detect. (4) no production construction path calls set_readahead_pages; only tests and the benchmark can enable the feature. The five targeted tests pass, but they do not cover these failures. Please correct the accounting/artifacts, make speculative replacement rollback-safe, expose an intentional runtime configuration path, and regenerate the claimed results.
All four review points hold. Each was reproduced before being fixed. ACCOUNTING. The benchmark reported only demand-path misses as `media_reads` and computed an "avoided" fraction from them, so a readahead that removed waiting looked like a reduction in media work. Every successful readahead is also a read of the backing store. Reproduced at P=8, depth 8, 256 frames against an 840-read baseline: drain 0 111 demand + 868 readahead = 979 total +16.55% drain 1 959 demand + 825 readahead = 1784 total +112.38% This reverses the headline. The readahead removes 89.16% of the reads a warp waits on and still raises total media traffic by 16.55%. It trades bandwidth for latency, which is the wrong trade whenever the tier is bandwidth-bound -- and the array supply rate is exactly what this project argues is binding. The benchmark now prints demand_media_reads, readahead_media_reads, total_media_reads, demand_reads_avoided_fraction and total_media_reads_change_fraction, and the README carries both numbers with that reading. FORMAT. `drain_per_demand` was passed to printf with no placeholder, so the build warned -Wformat-extra-args and the artifact omitted the parameter that decides the result. Now compiles clean under -Wall -Wformat-extra-args and the field appears in the JSON. ROLLBACK SAFETY. fill_free_frame_locked completed a clean eviction before the speculative read, so a read that failed -- an address past the end of the backing store is the ordinary case -- destroyed a valid resident page and returned false. The read now happens first and a frame is taken only once the bytes are in hand. tests/cpu/capacity_readahead_test.cpp case 4b covers it: with four clean demanded pages filling every frame and a readahead running off the end of the store, the old ordering leaves three of the four resident and the new ordering leaves all four. The earlier case 4 could not catch this because free frames were still available there. PRODUCTION PATH. Nothing outside tests and the benchmark could switch the readahead on. `readahead_pages` is now a profile field, optional and defaulting to 0 so no existing profile changes behaviour, parsed in profile.cpp, declared in the schema, and applied by CapacityRuntime's constructor. Setting it after the worker starts is safe because the queue only fills on a demand miss and no demand can arrive before a range is registered. Test suite: the same four tests fail before and after (context_lifecycle, vmem_tuning, run_with_bpftime, mqsim_benchmark), all pre-existing in a CPU-only build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All four points hold. Each reproduced before fixing; pushed as 84ff0c0. (1) Accounting — you are right, and it reverses my headline. I reproduced your numbers exactly against the 840-read baseline:
The benchmark now prints The honest conclusion is the more interesting one: the readahead removes 89.16% of the reads a warp waits on and still raises total media traffic by 16.55%. It trades bandwidth for latency — which is the wrong trade whenever the tier is bandwidth-bound, and the array supply rate is precisely what this project argues is binding. That belongs in the paper as a limitation of readahead on this tier, not as a win. (2) Format. Confirmed with (3) Rollback safety. Confirmed. The read now happens first and a frame is taken only once the bytes are in hand. New case 4b covers it: four clean demanded pages filling every frame, readahead running off the end of the store — old ordering leaves (4) Production path. Confirmed — only tests and the benchmark could enable it. Thank you for reproducing the benchmark rather than reading it — (1) would have gone into the paper as a result. The claimed results in |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two lines still described the benchmark as reporting media reads avoided, which is the framing the accounting correction removed: the readahead avoids demand-path reads and raises the total. Both now say what it actually reports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You are right, and this is the more fundamental of the two rounds. I verified both halves against the code:
So the readahead performs a real backing read and a real host-to-device copy, and the simulated timeline never sees either. It contributes no modeled latency, no queueing, no contention, nothing to the thermal path, and nothing to the unified report; and the hit it enables reports zero media time. In simulation terms the speculative read has been deleted from the timeline, which is exactly the shape that would manufacture a favourable result. I am therefore withdrawing the performance claim rather than patching around it. The traffic counts stand and reproduce — they are a statement about media reads issued, nothing more. For the real fix I would rather agree the shape with you before writing it, because it is not local. The readahead has no originating request, so there is no slot or ticket to carry a Does that match how you would do it, or would you rather the readahead go through the normal request path with a synthetic request id? I will not regenerate any performance number until the speculative read is on the timeline. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A readahead performs a real backing read that no GPU waits on, so it never
reached the timing engine: no modeled latency, no queueing, no contention, and
the demand that later hit the prefetched page reported no media time. In
simulation the speculative read had been deleted from the timeline.
RequestDispatcher::submit_speculative queues a read; poll_once drains one only
when it popped no demand and nothing is in flight, so a readahead can never
delay a request a warp is blocked on. The read goes to the same engine as a
demand, so it occupies the same channels and contends with demand traffic. Its
completion is discarded in publish, because no shared-memory slot is behind it.
Of the three shapes considered, this is the only one that is semantically
right. A callback mirroring flush(model_program) would follow a hook that has
no production consumer today, and submitting outside the dispatcher breaks the
ticket-to-engine-id pairing. Carrying the reads on the next demand's spare
media-action slot would put them back on that demand's critical path, which is
the thing the readahead exists to avoid.
I FOUND ONE BUG BY RE-READING THIS AND FIXED IT: publish originally erased the
speculative group itself, but both callers erase after publish returns, one of
them through the iterator used to reach the call. Erasing inside invalidated
that iterator and the caller then erased through it. publish now leaves the
erase to the callers.
TWO THINGS I COULD NOT VERIFY, MARKED IN THE CODE AS "UNVERIFIED, PLEASE
CHECK":
1. Speculative tickets set bit 63 to stay clear of real tickets, which are
the request's own `sequence`. I could not establish from the producer
side that `sequence` never reaches 2^63. If it can, the namespace must be
separated another way, for example a flag on DispatchGroup.
2. next_speculative_ticket_ wraps after 2^63 speculative reads. Not
reachable in any run here, but nothing enforces it.
THERE IS NO TEST FOR THIS PATH, and there is no GPU on the machine it was
written on, so it has never run against a live engine. The suite is unchanged
at 88% with the same four pre-existing failures, which only says it did not
break what was already there. Please run it before trusting it, and I will not
regenerate any performance number until it has been.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Speculative reads are now on the timeline in 54386c8. This has never run against a live engine and I am asking you to test it before it is trusted. What I chose, and why not the other twoI went with the synthetic-request shape rather than either option I floated earlier.
So: One bug I found by re-reading, already fixed
Two things I could not verify — marked in the code as
|
…ground Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two separate pieces of work, kept in one PR because the second is what validates the first. Reviewers can read them independently.
Part 1 — Capacity-mode readahead: HBFSim actually fetches ahead
This is a system-side prefetcher, not an application-side one. The application does not ask for it and does not know it happened; the only thing the policy has to go on is which pages have already been demanded. It is the analogue of OS page-cache readahead, not of
cp.async.On a demand miss for page N,
CapacityPageServicequeues N+1..N+k;CapacityWorkerfetches them only when a slot scan claimed nothing. Off unless switched on, so no existing measurement moves.Three design decisions, and I got the second one wrong first
Queueing happens after the demand is served. The miss path is a synchronous blocking read under one mutex — reading a second page there would double the synchronous work on the path a warp is blocked on.
Readahead may take a clean victim. My first version refused to evict at all. Measurement killed it: on a 1024-access MoE stream with 128 frames it fetched 22 pages and skipped 1970, because a warm cache never has a free frame — the feature did nothing in the only regime capacity mode exists for. A dirty victim is still refused and put back, because writing it costs a program on the media, dearer than the read the readahead would save.
A readahead page is published unreferenced.
HbmCache::publishgained a defaultedreferencedparameter (every existing caller unchanged). The policy is CLOCK, so a page nothing ever asks for is now the first candidate to be evicted again rather than the last.What it is worth
hbf_capacity_readahead_benchdrives the real page service and reports media reads, not wall-clock time.The result that matters most is a cliff. P=8, depth 8, 256 frames, varying how many queued pages the worker drains between two demands:
Draining slower than the queue fills does not degrade gradually — it turns an 86.79% reduction into a 14.17% increase, because queued pages go stale before use while still costing bandwidth and frames. Any deployment must keep drain rate above fill rate.
Part 2 — The prefetch model and its accuracy experiment
The figure: x = prefetch accuracy, y = modeled time, one curve per media latency tR (1, 2, 4, 5, 10, 20 µs).
Accuracy alone does not decide the time — a correct prediction still arrives late if issued too near its use, if bandwidth is short, or if the staging buffer dropped it. The fix is more curves, not a different x axis: "how accurate" is the x axis; "did it arrive in time" is the spacing between curves and whether a curve still falls near accuracy 1.
Three policies:
None(what HBFSim did),NextPage(no model of the workload),Accuracy(right with a stated probability — how the axis is swept without inventing a predictor). Four knobs, each a real limit: lead distance, staging capacity, media concurrency, and the compute between accesses a prefetch hides behind.Headline: expert routing being unpredictable is about which expert, not how many bytes
One Qwen3-30B-A3B token = 48 layers × 8 experts × 2304 pages = 884,736 page accesses.
The 374 are the first page of each expert, against 48 × 8 = 384. The unpredictable part is 0.04% of the traffic. Speedup stops at 2.00 because there the media read is longer than the compute between accesses — bandwidth-bound, not accuracy-bound, which is exactly what the curve family shows.
The two parts cross-validate
The model predicts a next-page policy reaches (P−1)/P on an MoE stream. The real implementation reproduces it when the readahead depth covers one expert:
Below that, depth caps the hit rate near D/(D+1): at depth 4, P of 8 and 16 both stop at 0.7500 and 0.7568.
Tests (TDD)
prefetch_model_test.cpp— 12 property assertions, written first. One was wrong on its first run: it demanded zero stall under a perfect prefetcher, and the model was right to disagree, because a prefetcher issuing L ahead cannot cover the first L accesses.capacity_readahead_test.cpp— off by default; queue not drained on the demand's own path; dirty victim refused; a demanded page survives the readahead after it; a page that became resident between queueing and draining is skipped; a readahead past the end of the store is skipped, not failed.What this is not
Not a measurement of any predictor, GPU, or device. Artifacts carry
"disclaimer": "modeled, not measured on any device or GPU". The readahead experiment drives the host-side page service — media reads avoided, not wall-clock. The MoE stream is synthetic, not captured from a vLLM run.compute_ns_per_accessis a parameter, not measured from a workload.docs/46-预取实验设计.mdcarries the design;benchmarks/prefetch/README.mdlists every file and the exact reproduction commands.Test status
CPU-only build. Same four tests fail before and after —
context_lifecycle,vmem_tuning,run_with_bpftime,mqsim_benchmark— all pre-existing. Count 32 → 34.🤖 Generated with Claude Code