Repository navigation
docs: where a Flash-Next prefill forward's 1.8 s goes, and the ~1.0 s that is not HRX (engine #572) - #646
Conversation
A later reading of #572 puts device time at 41.9 % of a Flash-Next pp512 forward and ~1.0 s per forward on the host. On the two models that fit on this box a fully offloaded forward is 99.98 % device wait: 263.5 ms of 263.5 ms on Qwen3-Coder-30B-A3B Q4_K_M (1,161 dispatches) and 870.2 ms of 870.2 ms on Qwen3.8-27B UD-Q4_K_XL (1,519), with graph lookup, binding, upload and flush together under 15 us. libhrx's own dispatch profile agrees (538.14 ms of device kernels over 2 forwards = 103 % of the 523.8 ms they took). Also records that rocprofv3 collects nothing on HRX, that the host leg only appears when a graph is not fully on HRX (the same 27B at -ngl 20: 3,746 graph computes per forward, 145.7 ms of host-staging upload), and that the 87 GiB Flash-Next file cannot be loaded beside the owner's NPU job, so its own host leg stays unmeasured.
The profiler's second pass closes the accounting: at -p 4096 -ub 2048 the steady state is 1143.7 ms of device wait in a 1158.6 ms graph, with the cache key, transient lease, rebind, upload, launch, flush and download under 0.25 ms together. The 4.5 s the first graph of a new ubatch costs is prepare and kernel JIT for the new shapes.
…path (engine #572) Loaded the 87 GiB file once the owner's NPU job exited. Per 512-token forward it is 1,795.4 ms: 676.8 ms of device kernels (37.7 %, from libhrx's dispatch bundle) and 618.6 ms of HRX graph compute with 0.13 ms of host phases in it, so ~1,177 ms is llama.cpp-side work on nodes HRX did not take, not a gap between HRX dispatches. Also corrects the 76 % device in the section above: 1,353.5 ms is two forwards of 676.8 ms compared against a one-forward 1,786 ms pass, i.e. the two-forwards reading #572 comment (b) retracted. Per forward the device leg is 37.7 %. Keeps the two smaller models as the control (no off-HRX node, host 0.01-1.3 %) and records that rocprofv3 collects nothing on HRX.
…ngine #572) llama-server -lv 4 prints the placement of the prefill graph: 6060 nodes, 1 off HRX (GET_ROWS x 1), 3 graph splits. That node reads per_layer_token_embd.weight, an IQ3_S [160, 320001536] ~20 GB table, and dispatch-get-rows.cpp declines it because kMaxGetRowsRowCount is 262,208. The host cost is the page faults of gathering rows out of that table: 35 ms at -p 128, ~1.18 s at -p 512.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES (posted as a comment: GitHub refuses --request-changes on a PR owned by the same account). One factual error in the paragraph that names the cause; everything else holds.
Wrong quant type and the size derived from it. The section says the declined GET_ROWS reads per_layer_token_embd.weight, "which this file carries as IQ3_S [160, 320001536] (~20 GB of table)". On this box the UD-IQ4_XS file the section measures carries it as IQ4_NL, and the table is ~28.8 GB, not ~20:
$ python3 <read the GGUF tensor directory of the files named in the doc>
Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf per_layer_token_embd.weight IQ4_NL [160, 320001536]
Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf token_embd.weight Q8_0 [2560, 248320]
$ ls -l ~/models/qwen38/Qwen3.8-Flash-Next-GGUF/UD-IQ4_XS/*.gguf | awk '{s+=$5} END {print s}'
93682584224 # 87.3 GiB, i.e. the file the section cites
IQ4_NL is 18 bytes per 32 elements, so 160 x 320,001,536 elements is 28.80 GB (26.8 GiB); at IQ3_S's 110/256 it would be 22.0 GB, which is where the "~20 GB" came from. The sibling docs-adjacent note in ~/briefs/TISSUES-572-STATE.md (05:40Z) already says IQ4_NL — the 05:32Z entry has the same slip. token_embd.weight Q8_0 [2560, 248320] in the doc is correct, so it is specifically this one tensor.
What does hold, checked here:
kMaxGetRowsRowCount = 262208andis_supported_row_count()as described, at the exact pin:gh api repos/1bit-MONSTER/llama.cpp/contents/ggml/src/ggml-hrx/dispatch_registration/common/dispatch-get-rows.cpp?ref=40c4ccb76...-> line 23static constexpr int64_t kMaxGetRowsRowCount = 262208;, line 51return row_count >= 1 && row_count <= kMaxGetRowsRowCount;, so a 320,001,536-row table is declined as claimed.- the "76 % device" correction:
docs/hrx.mdon main does say "Device time accounts for 1,355 ms of the 1,786 ms pp512 pass (76%)", and 1,353.5 ms is 2 x 676.8 ms, so the two-forward reading is right and #572's 41.9 % is the closer figure. - the "~1,177 ms / 66 %" arithmetic: 1,795.4 - 618.6 = 1,176.8 ms = 65.5 % of the forward, matching the state file's 65.6 %.
placement: 6060 nodes, 1 off HRX (GET_ROWS x 1), 3 graph splitsis the raw-lv 4line in~/briefs/TISSUES-572-STATE.md(05:32Z).
One inconsistency worth a sentence while you are in there: the device leg is quoted as 676.8 ms/forward from the dispatch bundle and as 618.6 ms from replay.wait in the same table (a 9 % gap the doc explains for the other two models — "the surplus being the profiler's own record writing" — but not for this one).
I read the GGML type id as IQ3_S when I parsed the tensor directory: it is IQ4_NL (GGML_TYPE_IQ4_NL = 20), 18 bytes per 32 values, so 160 x 320,001,536 elements is 28.80 GB / 26.8 GiB, not ~20 GB. Re-derived from ggml.h's enum and ggml.c's block size, and the reviewer's reading matches. Also: the 9 % between the 676.8 ms device leg from the dispatch bundle and the 618.6 ms replay.wait from the unprofiled run is the profiler's own record writing on the dispatch path (same sign, 2 % and 7 %, on the two control models), and the two-forward reading of the 1,355 ms is now backed by the bundle's own counts. And the kernel's index.assume is named, so nobody raises kMaxGetRowsRowCount alone.
|
Fixed in 831b2a9, and thanks — the quant type was my error, not a measurement. The tensor. You are right: The two corrections you asked me to check both hold, and I have re-verified them at the pin rather than relying on the earlier read:
The inconsistency you flagged. Added a sentence: 676.8 ms and 618.6 ms are the same device leg from two runs, and the 9 % is the profiler — kernel times come from a run with State file corrected too: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
Re-review at 831b2a9. Both points I raised are fixed; READY from my side.
The tensor and its size are right now: IQ4_NL [160, 320001536], "18 bytes per 32 values, so 28.80 GB of table (26.8 GiB)". That is what I read out of the GGUF myself (160 x 320,001,536 = 51,200,245,760 elements; x 18/32 = 28,800,138,240 B = 26.82 GiB), and the file's total is the 87.3 GiB the section cites. token_embd.weight Q8_0 [2560, 248320] still matches too.
The 676.8 / 618.6 ms gap is explained (profiled dispatch bundle vs unprofiled replay.wait, same sign as the two controls), and the added evidence is self-consistent: 5,134 dispatches = 2 x 2,567/forward, and the doc's own "2,566-dispatch HRX one and a 1-dispatch one" sums to the same 2,567.
The new kernel-bound paragraph checks out at the pin:
$ gh api repos/1bit-MONSTER/llama.cpp/contents/ggml/src/ggml-hrx/kernel-corpus/kernels/loom-libs/ops/get_rows_f32.loom?ref=40c4ccb76...
%token_id = index.assume %token [range(%token, 0, 262207)] : index
%bounded_token_id, %bounded_row_count = index.assume %token_id, %row_count [lt(%token_id, %row_count)] : index, index
line 44, as quoted, so "raising the matcher's cap alone would read out of range" is correct and is the useful part of the paragraph — it turns item 2 from "relax a constant" into "make the bound a compile-time config".
One number to fix while it is fresh: the new sentence says the profiler surplus on the controls is "(2 % and 7 %)", but the paragraph six lines below gives those runs as 103 % and 113 % of their replay.wait (538.14 vs 523.8 = 2.7 %, 1,871.15 vs 1,657.2 = 12.9 %). So the controls are 2.7 % and 12.9 %, and "7 %" is not in the document; the main model's 9 % (676.8 vs 618.6) is the one that holds.
…#572) Shapes and bytes from the GGUF tensor directory, FLOPs from TraceLens PerfModel (GEMM/flash_attention), peaks from TraceLens's measured Radeon 8060S spec (256 GB/s, 59.39 TFLOP/s fp16), measured device time per class from the libhrx dispatch bundle. One 512-token forward needs 64.87 GB of compulsory weight traffic (all 512 experts are touched at 512 tokens; the 28.8 GB PLE table is only gathered), so the memory roof is 253 ms against a 115 ms compute roof: memory-bound. The device leg is 676.8 ms = 37 % of that roof, 2.7x the floor. MoE experts are closest (306 ms vs 232 ms); the slack is dispatch-bound small ops - 1,189 elementwise/copy/norm dispatches at 106.6 ms with no weight traffic, 273 f32 small matmuls at 58.4 ms. The host leg is the largest line in the forward: one CPU GET_ROWS at ~1,177 ms = 4.6x the memory roof of the whole pass, to move 2.2 MB - page faults on a cold 28.8 GB mmap'd table, which is why it does not warm up between forwards.
Item 1 of #572, measured on the file itself once the owner's NPU job exited and the 87.3 GiB IQ4_XS could be loaded.
Result. A 512-token forward is 1,795.4 ms. Of that, 676.8 ms is device kernels (37.7 %, from libhrx's dispatch bundle) and 618.6 ms is HRX graph compute whose host phases total 0.13 ms. The remaining ~1,177 ms is llama.cpp-side work — the CPU computing graph nodes the HRX backend did not take — not a gap between HRX dispatches. #572's ~1.0 s was real; the HRX host path is not what it is.
And the tree's own 76 % is wrong: it is a two-forwards number.
docs/hrx.mdrecords "1,355 ms of the 1,786 ms pp512 pass (76 %)". 1,353.5 ms is exactly two forwards of 676.8 ms from the bundle I measured, compared against a one-forward pass time — the same two-forwards reading #572's comment (b) retracted, still in the table. Per forward the device leg is 37.7 %, so #572's 41.9 % was the closer figure.Instrumentation (all in the tree, none of it needed to read the docs):
GGML_HRX_HOST_GAP=1— the host-path profiler, ggml-hrx: an env-gated host-gap profiler (engine #572) llama.cpp#154, our own new file, off by default. Twelve phases per graph compute.HRX_PROFILE_FILE=<bundle> HRX_PROFILE_MODE=dispatch+iree-profile statistics— the only device-time source for HRX.rocprofv3exits 0 on an HRX run and writes no traceback files, because HRX dlopens TheRock'slibhsa-runtime64.so.1itself and libhrx writes its own AQL packets.Controls that separate the two legs (a model with no off-HRX node has no host leg):
replay.wait(device)Also recorded: the ubatch growth (1.3 % host at
-p 4096 -ub 2048, plus a one-time 4.5 s prepare/JIT on the first graph of a new ubatch) and the-ngl 20control (3,746 graph computes per forward instead of 2).Next step this hands over: name the CPU-resident nodes with
GGML_SCHED_DEBUG=2and give them a dispatch — that is item 2 of #572, and item 3's f32 batching is one of them.Documentation only; no behavior change.