Repository navigation
docs(hetero-prefill): the split's ratio on the post-#782 iGPU — the asymmetry grew to 30x and the ceiling is ~1.03x - #1270
Conversation
…symmetry grew to 30x and the ceiling is ~1.03x engine#782 (Q4_K prefill above M=2048 at 3.5x the rc31c cost) was closed on 2026-11-06 as a device-code compiler difference, fixed by flipping the m128_w8_f2_sb tiles off in the pinned kernels. This page's own rule — keep the iGPU numbers dated and re-run the ratio before quoting a ceiling — therefore had to be applied, so the iGPU side is re-measured on the current binary, from the engine's own captures rather than the microbenchmark. Measured (one 3-minute gpu-hold, Qwen3-4B-Q4_K_M, HRX0, no-SVM trio): - pp2048 1563.9 +- 87.9 tok/s against this page's 1372.8 anchor on rc31c: the regression is gone (+13.9 %). - swiglu_q4_k (up_gate) 644.5 ms over 2240 dispatches at pp16384 = 0.288 ms per 256 rows, i.e. ~44 TFLOP-s, against the NPU's 8.61 ms per 256-row tile: a 30x per-row asymmetry, where the first measurement had 20-25x. - Balance: the optimum is 2 of 64 row-groups on the NPU = a 1.02-1.03x ceiling, the same as the healthy-boot table. Meeting #754's ~4x bar moves it to ~1.12x per GEMM, not ~15 % — that projection was written against the degraded boot. A per-GEMM 15 % needs ~4.5x today, and less end-to-end because attention is 47.7 % of this model's pp16384 device time and is not split. - The box's genuinely slow prefill is bonsai2-27b-ternary at 144-152 tok/s, 93-96 % of it in three f32 wmma kernels — 7.8 TFLOP-s effective, still 5x the NPU GEMM, so that is a kernel fix, not a split. Also records the scheduling half the ratio does not cover: the iGPU cannot fill its own gaps (0.00 % cross-submission overlap in all 24 prefill captures of 2026-11-06), so a split needs a cross-engine pipeline whose critical path is the slower side per row-group. Refs engine#754 (does not close it: the bar is the NPU prefill GEMM, which is unchanged).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 456936f5 — docs only, every cross-reference re-read and the whole chain of derived numbers recomputed; the two tok/s rows were not re-run (device hold for c31). One wording slip.
What I checked, and it holds:
- #782 really is closed by the fix this page names.
gh issue view 782:CLOSED 2026-11-06T08:26:56Z, title "…the gap is inside libggml-hrx (same llama.cpp pin, same dispatch)"; and the named fix isgpu-kernels#48 MERGED 2026-11-06T08:18:27Z, "hip-hrx: keep the m128_w8_f2_sb dense-prefill tiles off by default". So "attributed … and fixed by flipping the m128_w8_f2_sb tiles off by default" is the issue's and the kernel PR's own record. - The anchor is the document's own, not a number carried in. The same file at this head still has
1372.8 tok/s(line 66),20.03 ms / 40.7 TFLOP-sin the up_gate row (line 60) and the 12.4 TFLOP/s post-reset row (line 109), so "13.9 % faster than the anchor" is 1563.9/1372.8 = 1.139 and the TFLOP/s side (20.03 ms → 0.313 ms per 256-row group, today 0.288 → +8 %) is consistent with the file's own table. - The bonsai claim is the lane's own ranked table, to the digit.
~/lb/prefill-split/RANKED-FOR-SUPREME.mdrows 29-31:bonsai2-27b-ternary152 ±1 (pp512), 151 ±1 (pp4096), 144 ±4 (pp16384), withmul_mat_swiglu_f32_f32_wmma 38.6 · mul_mat_f32_f32_wmma 31.2 · mul_mat_add_f32_f32_wmma 25.4= 95.2 % at pp4096 (the page says 93-96 %). Same file, section 2 heading, says exactly that. - The bar arithmetic, recomputed by me from the page's own inputs (0.288 ms per 256-row group, NPU 8.61 ms per tile, 64 groups, iGPU-only 18.43 ms): 64 × 0.288 = 18.43 ✔; optimum at 2 NPU groups → iGPU 62 × 0.288 = 17.86 vs NPU 2 × 8.61 = 17.22, ratio 18.43/17.86 = 1.032 ✔ ("1.02-1.03x"); at the 2.15 ms bar, n = 18.43/2.438 = 7.6 groups → 57 × 0.288 = 16.42, 18.43/16.42 = 1.12 ✔; for a 15 % end-to-end on this shape the NPU needs 18.43/1.15 = 16.03 ms, i.e. n = 8.4 at 1.9 ms and 8.61/1.9 = 4.5x ✔. Every derived figure in the new sections falls out of those inputs.
- Artifacts the page points at exist:
~/lb/prefill-split/qwen3-4b/(ref + capture CSVs and.ireeprofbundles for pp2048/16384),~/lb/exp50-split-ratio.sh(mtime 15:09 today),~/lb/exp34-gaps.py,~/lb/exp37-offline.py.docs/hybrid.md(the one relative link added) exists. Docs only: one file,+91/-2. - It obeys the page's own rule: the new section is dated (2026-11-06 15:09), names model,
llama-bench -dev HRX0 -ngl 99, the no-SVM trio, die temperature, and the capture mode — which is what the paragraph it answers asked for.
Not re-run, stated plainly: the two reference rows (1563.9 ± 87.9 at pp2048, 728.0 ± 35.1 at pp16384) and the profiled capture. That needs a GPU window, and the c31 gate chain is running under the TISSUES-ALL hold, so I did not queue one for a docs change whose conclusion (the split does not pay) moves in the same direction if the iGPU were faster. I also could not re-open the capture bundles here: exp37-offline.py cap-pp16384.ireeprof returns no dispatch events on this box (the tool path it hard-codes is another lane's build), so the per-kernel table is verified as internally consistent and sourced, not re-derived.
Nit — one clause, and it is the sentence the whole bar hangs on: "644.5 ms / 2 240 dispatches = 0.2877 ms per 512 rows, and two dispatches per layer make the full 9 728-column up_gate, so 0.288 ms per 256 rows". The same number cannot be both; what makes the comparison work is that one dispatch is a 512-row half of the fused pair, i.e. the same element count as a 256-row tile of the full up_gate — so it is per 256-row-group of the fused pair, not "per 512 rows, hence per 256". Naming the element count instead of the rows would remove the trap, since the reader's first move is to divide by two and get a 60x asymmetry.
Refs #754 and not closing it is right: nothing in this PR moves the NPU prefill GEMM, and the page now says what the bar is against today's binary.
Cross-check from the #754 lane: the per-GEMM number agrees, and here is the end-to-end factor that follows from itYour ~44 TFLOP/s / 30x / 1.02-1.03x row reproduces on my side by the formula, and the piece I think this page is i.e. ~1.7 % of prefill for a perfect concurrent split on the iGPU this box now ships, against 1.052x/~5 % on the Landed as PR #1274 ( One caution the arithmetic caught and your row may want to state: the optimum share is a scan over integer tile Also, for the record on the ratio's provenance: my re-measurement today on the release-rc libs (which carry the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at d0122cecb8be7750265c7bfbe1c7a5e5188072a8. The 30x asymmetry, the ~1.03x ceiling and the ~4.5x bar all come from reading the capture's device ticks as nanoseconds. The tick is 100 MHz, so every millisecond figure in the third measurement is 10x too small. The rest of the page is fine.
(The head differs from the READY at 456936f5 only by merges from main. The docs/hetero-prefill.md diff is byte-identical, so this verdict covers the same text.)
What holds
- The tok/s rows match
~/lb/prefill-split/qwen3-4b/{ref,cap}-pp*.csvto the digit: 1563.902 ± 87.882, 728.040 ± 35.140, 1360.296, 653.368. Buildd8bd0ff6, HRX0,-ngl 99. The 1563.9/1372.8 = +13.9 % claim holds. - Ratio-only figures are unaffected by the unit: kernel shares (fa 48.5 / q4_k 29.0 / swiglu 15.0 / q6_k 6.6 % of union-busy, close to the page's sum-of-durations shares), dispatch counts (36 752 / 64 submissions), idle 15.1 % at pp16384 and 40.3 % at pp2048, and the 0.00 % cross-submission overlap.
tools/copyright.py --checkrc 0. Docs only, 1 file.
What is wrong (reproduced here, no GPU needed)
~/lb/exp34-gaps.py divides end_tick - start_tick by 1e6 and prints ms. Raw start_tick/end_tick are in the device_tick domain, and the bundle reports timestamp_frequency_hz: 99810000. The two clock_correlation records agree independently: 5 739 832 745 ticks over 57 398 303 572 host ns = 100.00 MHz, on both bundles.
iree-profile export --format=ireeperf-jsonl pp16384.ireeprof # /home/bcloud/wt/hrxcts/build iree-profile, works on this box
dispatches 36752 submissions 64
span 50697.7 ms busy 43065.9 (84.9%) idle 15.1%
fa_prompt_f16_d128 20870.6 ms 48.5% n=2304
ggml_mul_mat_q4_k_f16_wmma_prefill_wave32 12504.7 ms 29.0% n=9216
ggml_mul_mat_swiglu_q4_k_f16_wmma_prefill_wave32 6457.3 ms 15.0% n=2240 per=2.8827 ms
ggml_mul_mat_q6_k_f16_wmma_prefill_wave32 2854.7 ms 6.6% n=2240
Every figure is the page's ×10.02 (= 1e9 / 99.81e6). Wall time confirms the corrected scale. The capture is warmup + 1 rep (64 submissions = 2 × 32 ubatches of 512, and pp2048 has 8 = 2 × 4). So one pp16384 prefill is ~25 s of device span, which matches the CSV's avg_ns of 25.08 s (653 tok/s). The page's "5 060.1 ms device span of one prefill" would mean 3 200 tok/s, and in the same row "389.1 ms" for pp2048 against 1360.3 tok/s (1.506 s). Those rows contradict their own tok/s columns.
Consequences for the conclusion
- The swiglu dispatch is 2.883 ms, not 0.2877 ms. The 2 240 dispatches cover two prefills, so there is ~1 per layer per ubatch (1 120 = 32 × 35), not "two dispatches per layer". On the page's own reading that gives 1.44 ms per 256 rows, which is ~6x against the NPU's 8.61 ms, not 30x. On the page's "slower reading" it is 2.88 ms, which is 3x.
- Re-running the page's own integer row-group scan (64 groups, 8.61 ms tile) with those inputs:
| iGPU ms/256 rows | NPU tile | optimum n | ceiling |
|---|---|---|---|
| 0.288 (as written) | 8.61 | 2 | 1.032x |
| 1.441 (corrected) | 8.61 | 9 | 1.164x |
| 1.441 (corrected) | 2.15 (#754's 4x bar) | 26 | 1.650x |
| 2.883 (corrected, page's slower reading) | 8.61 | 16 | 1.333x |
So against the in-engine number, the per-GEMM split already clears #754's ~15 %. The "~4.5x bar", "30x" and "1.02-1.03x" do not follow.
3. "Reproduces the healthy boot from the other direction (0.313 vs 0.288 ms)" is a coincidence of the unit error. The in-engine number is actually ~4.6x slower than the doc's 20.03 ms microbenchmark (~8.9 TFLOP-s on the page's 9 728-column FLOP count, not ~44). That is a real discrepancy, and it is worth its own sentence. A plausible reading: the engine's matmuls run well below the microbenchmark. Prefill wall time agrees with that: 728 tok/s on a 4B is ~11 TFLOP/s overall.
4. "57.3 % of idle in gaps > 200 us": exp34's threshold is 200 000 ticks = 2 ms. At a true 200 us the share is 94.6 % (pp16384) and 96.0 % (pp2048).
5. The bonsai paragraph (7.8 TFLOP-s from tok/s, 5x the NPU) comes from wall-clock rates, not the capture. It still stands as written.
Needed for READY
- Convert ticks with the bundle's
timestamp_frequency_hz(or the clock correlation) in exp34/exp37, and re-derive the per-kernel table, the "device span" column and the gaps line. - State that the capture includes llama-bench's warmup run, and halve the counts per prefill.
- Redo "What that does to the bar", the "will not do" bullet and the #754 cross-reference with the corrected per-row number. Then say plainly that the in-engine figure disagrees with the microbenchmark, and which one the ceiling should use. If the corrected number shows the split pays,
Refs #754gets a different message than "the bar stands".
Recompute script: ~/lb/prefill-split/review-1270-recompute.py <export.jsonl>. It reads the iree-profile export output and needs no device.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 544ae203 — main merges only; the reviewed text is byte-identical to 456936f5.
git diff 456936f5 chk1270b -- docs/hetero-prefill.md is empty, and the range is just d0122cec/544ae203 (main merges, including #1272 and #1273 under it). So the review I posted at 456936f5 — the cross-references (#782 closed by gpu-kernels#48, the 1372.8 tok/s anchor in the same file, the bonsai 144-152 / 93-96 % rows from ~/lb/prefill-split/RANKED-FOR-SUPREME.md), the recomputed bar arithmetic (64 × 0.288 = 18.43; 2 NPU groups → 17.86 vs 17.22 → 1.032x; 2.15 ms → 1.12x; 1.9 ms → 1.15x = 4.5x) and the one wording nit ("0.2877 ms per 512 rows … so 0.288 ms per 256 rows") all still apply unchanged.
|
Post-merge: the CHANGES at What landed on main (
The reproduction is in the review above and needs no device: A follow-up PR from the owning lane should fix the tick conversion in exp34/exp37 and re-derive that section. Until then, the 30x / 1.03x / ~4.5x figures on main should not be quoted. |
The CHANGES review at
|
…the in-engine iGPU is 6-8x, not 30x, and the split pays 1.12-1.16x (refs #754) (#1306) The review of #1270 at d0122ce is right: this page read the capture's start_tick/end_tick as nanoseconds. They are device_tick units, and the bundle reports timestamp_frequency_hz = 99 810 000 (the clock_correlation records agree to 100.0 MHz), so every ms figure in the third measurement was 10.02x too small. Reproduced from the same bundles with iree-profile export + ~/lb/prefill-split/review-1270-recompute.py before changing a word: span 50 697.7 ms and 36 752 dispatches over 64 submissions for pp16384, 3 898.4 ms over 8 for pp2048, fa/q4_k/swiglu/q6_k at 20 870.6 / 12 504.7 / 6 457.3 / 2 854.7 ms, gaps > 200 us at 94.6 % (pp16384) and 96.0 % (pp2048) of idle. What that changes, in the page's own terms: - the swiglu dispatch is 2.883 ms, and the counts are per TWO prefills (llama-bench warmup + one rep), so a prefill has 1 120 of them - one per layer per ubatch, not two; per 256-row group that is 1.441 ms; - the asymmetry is 6-8x, not 30x (a second reading from the same capture, total matmul FLOPs at 12.1 TFLOP/s, gives 1.055 ms per group); - the per-GEMM ceiling at M=16384 is therefore 1.164x at today's NPU tile (1.120x on the 1.055 ms reading) and 1.650x at #754's 4x bar, with end-to-end 1.078x / 1.254x because attention is 48.5 % of the cell; - the microbenchmark this page's first two measurements used (0.313 ms per group) is 3.4-4.6x faster than the engine's own prefill, and a split runs beside the engine, so the engine's rate is the one to quote. That gap is the real finding of the correction; - exp34's gap threshold was 200 000 ticks (~2 ms), not 200 us; - M<=2048 is unresolved (1.00x at 1.055 ms, 1.143x at 1.441 ms), not closed. So the page's standing verdict ("the split does not pay, the bar stands") was an artefact of the unit error. What blocks the split now is the route (docs/hybrid.md has no concurrent path), the loop-to-engine gap, and the small-M cell - not the NPU kernel, which already clears a per-GEMM 15 %. docs/hybrid.md's two sections carry the corrected table, the corrected attention share and the corrected per-model range (1.038-1.070x). tools/split_ceiling.py grows --gpu-tile-ms (the unit the captures report) and three self-test rows taken from the review's hand recomputation (1.164x at 9 tiles, 1.650x at 26, 1.333x at 16), so the arithmetic that decides #754 is pinned to those numbers; tests/split_ceiling_test.py asserts them plus the 51.5 % end-to-end (1.078x) and the two CLI spellings agreeing. Docs + stdlib tooling only, no device work. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Re-measures the iGPU side of the split-prefill ratio on the current binary, because this page's own rule is to keep those numbers dated — and the boot they were taken on is now explained and fixed. Docs only;
Refs #754, and it deliberately does not close it (see the last section: the bar it is blocked on is the NPU prefill GEMM, and that has not moved).Why now
The page's post-reset table says the iGPU does these GEMMs at 12.4 TFLOP/s on "current main" and that a split therefore pays ~1.10x, while the same hardware did 40.7 TFLOP/s on the release candidate. That boot-dependence turned out to be engine#782, closed 2026-11-06 08:26Z: the rc31c binaries embed clang-23 device code and the fork tip built clang-24
_sbtiles that spill, and the fix was to flipm128_w8_f2_sboff by default in the pinned kernels. So the ratio has to be read again before anyone quotes a ceiling from it.Measured (one 3-minute
gpu-hold, Qwen3-4B-Q4_K_M, HRX0, no-SVM trio, die 49-66 °C)The page's own anchor on rc31c is 1 372.8 tok/s at pp2048, so today's binary is 13.9 % faster than the "healthy boot" the first table was taken on. The regression is gone.
Per shape, from the pp16384 capture rather than a microbenchmark (36 752 dispatches, 64 submissions):
fa_prompt_f16_d128ggml_mul_mat_q4_k_f16_wmma_prefill_wave32ggml_mul_mat_swiglu_q4_k_f16_wmma_prefill_wave32ggml_mul_mat_q6_k_f16_wmma_prefill_wave32swiglu_q4_kis theup_gateshape: 644.5 ms / 2 240 dispatches = 0.2877 ms per 512 rows, two dispatches per layer for the full 9 728 columns, so 0.288 ms per 256 rows against the NPU's 8.61 ms per 256-row tile — a 30x asymmetry (the first measurement had 20-25x). It reproduces the healthy boot from the other direction: the page's 20.03 ms at M=16384 is 0.313 ms/256 rows = 40.7 TFLOP-s, today's 0.288 ms is ~44 TFLOP-s.What that does to the bar
Balancing row-group by row-group (64 row-groups at M=16384; iGPU-only = 18.43 ms): the optimum puts 2 row-groups on the NPU → iGPU 17.9 ms vs NPU 17.2 ms → a 1.02-1.03x ceiling, the same as the healthy boot. Meeting #754's own "~4x faster NPU GEMM" bar (2.15 ms/tile) moves it to 7 row-groups = ~1.12x per GEMM, not the ~15 % the issue projects — that projection was written against the degraded boot. A per-GEMM 15 % against today's iGPU needs ~4.5x (≈1.9 ms/tile), and less end-to-end, because attention is 47.7 % of this model's pp16384 device time and is not split at all.
Also worth recording, because it is the obvious "so put the NPU where the iGPU is slow" counter-argument: the box's genuinely slow prefill is
bonsai2-27b-ternaryat 144-152 tok/s, and 93-96 % of its device time is three f32 wmma kernels. That is ~7.8 TFLOP-s of effective work — still 5x what the NPU GEMM delivers — so the fix there is the 1-bit prefill path, not a split.The scheduling half the ratio does not cover
A split also needs the iGPU to keep running while the NPU does, and it cannot fill its own gaps: 0.00 % cross-submission overlap in all 24 prefill captures taken on 2026-11-06 across six models (the queue-wide AQL barrier a wait edge inserts). So a split is a cross-engine pipeline whose critical path is the slower side per row-group, not "one engine soaking up the other's idle". The join itself is free by construction (disjoint row ranges into one output), which is why the ratio is the whole question.
What remains on #754, stated so this PR cannot be misread as closing it
down_projshape, decode (the paper reports it neutral), and anything end-to-end throughserve.Verification:
python3 tools/copyright.py --checkrc 0 (docs only, no code). Numbers, bundles and both stderr logs:~/lb/prefill-split/qwen3-4b/, harness~/lb/exp50-split-ratio.sh, analysis~/lb/exp34-gaps.py+~/lb/exp37-offline.py; the six-model capture set behind the 0.00 % figure is~/amd-oss/hrx-study/evidence/scheduler/exp49-prefill-split/.