Repository navigation
Description
Summary
Tracking issue for zero-copy on the serving paths. Strix Halo has one DRAM for CPU, GPU (HRX, KFD GTT) and NPU
(XDNA2 BOs are host memory mapped through the IOMMU), so every memcpy between "host", "GPU" and "NPU" buffers is
pure overhead on this box. The owner's framing (2026-10-31): "we need to be using it, the whole point of having
it".
Four targets, in order, each with a measurement before/after and an output gate. This issue exists so the work is
visible and so a target can be claimed without re-deriving the map.
What exists today (the map)
- Hybrid KV handoff goes through a file. NPU-prefill → HRX-decode hands the prompt KV over as a llama.cpp
sequence-state FILE plusPOST /slots/0?action=restore(restore 6–7 ms for 307 tokens; it scales with context
and model size). The zero-copy KV-sharing code already exists — dma-buf export in ggml-hrx
(ggml-hrx-dmabuf.cpp), import in ggml-vulkan,llama_kv_share_next/from,llama-kv-share.cpp,
server-prefill.cpp— but only on the fork branch1bit/hrx-vulkan-patched, not on the pinned fork main. It
was built HRX → Vulkan; the exporter/sharing side is the base for target 1, the importer side is dead with
Vulkan. - NPU runtime copies activations per launch.
npu/forward/src/q4nx_forward.cppmemcpy's activations into
mapped BOs (bA->map(),bo.sync(XCL_BO_SYNC_BO_TO_DEVICE)), reads results back (h_rb), and the q4nx lm head
copies tiles per token (h_memcpyunderNPU_PHASE_PROF=1). Weight BOs are packed once at startup — keep that. - ONNX EP copies both ways: inputs/outputs are memcpy'd into
OrtValues (see TQDQ work); ORT IO binding exists
and is unused.
Targets
| # | target | gate | measure |
|---|---|---|---|
| 1 | Hybrid KV handoff without the file — the prefill provider writes the prompt KV straight into memory the HRX llama-server imports as its KV cells (dma-buf, or the KFD-visible GTT buffer HRX already uses) | greedy reply identical to --device hrx on 3 prompts |
restore/first-byte vs the file path at 307, 4k and 16k tokens |
| 2 | NPU runtime: activations live in BOs — host-side ops (norm, residual, GDN state) write directly into the next launch's A BO (mapped once); C consumed in place; syncs only where the XDNA cache model needs them (XDNA2 BOs are cacheable host memory, so a sync is a cache flush, not a copy — find out when it is required at all) | byte-identical logits vs the current path on 3 prompts | h_memcpy/h_rb before/after under NPU_PHASE_PROF=1 and NPU_PROFILE_TOKEN=1 |
| 3 | ONNX EP IO binding onto BO memory — inputs bound to BO-backed OrtValues, outputs read in place |
outputs identical to the CPU EP | per-call overhead before/after |
| 4 | Audit table — every remaining copy on the serving paths (bytes/token, ms/token, why it stays) | — | an entry in the state file the owner can read |
Target 2 must be built on the shipped int8 path, not on pb16i: the pb16i wiring currently emits garbage
(#697), so its logits are not a gate for anything.
Ordering and rules (they are part of the deliverable)
- GPU work goes through
~/.local/bin/gpu-hold -l zerocopy -t <min> -- CMD, queued behind pi-attn; GTT peak
≤ 95 GiB (the fault zone in hrx: gfxhub page fault + MODE2 GPU reset when GTT nears the 120 GiB ceiling (Flash-Next UD-IQ3_XXS, 18.8k ctx, 106.5 GiB GTT) #680). - NPU runs only inside a window granted by pi-bonsai (request in
~/NPU-WINDOW-OK, wait forZEROCOPY-GO, take
only/tmp/1bit-npu-device.lock, writeZEROCOPY-DONE); the owner's Bonsai serve ownsnpu.lock. - CPU-heavy builds via
~/lb/thermal-run.sh. Download nothing. - Our files only, full Apache-2.0 notice on every new file; NPU kernel internals never enter the public tree.
- One PR per target when its gate passes; measured numbers only, and say what was skipped and why.
Why this is an issue and not just a lane brief
Three of the four targets change code paths that #669 (host-weight residency), #680 (GTT ceiling) and #666 (NPU
readback) already touch, and one of them (target 1) is the difference between the hybrid route being a demo and
being usable. Putting the map and the gates in the tracker means the copies we deliberately keep are decisions
with reasons, not leftovers.
Related
- hrx: gfxhub page fault + MODE2 GPU reset when GTT nears the 120 GiB ceiling (Flash-Next UD-IQ3_XXS, 18.8k ctx, 106.5 GiB GTT) #680 — GTT ceiling; the 95 GiB rule above comes from it.
- HRX route pins ~107 GiB outside process RSS on a 217 GiB mmap (DeepSeek-V4.1 UD-IQ1_S), independent of -ngl/ubatch/prefetch #669 — host-weight residency in the same executor; zero-copy and residency are the same question asked from
two ends. - npu: the pb16i/sealed-ELF path emits garbage — 48 launches/token missing, 16 int8 M=128 QKV launches on full-attention layers (blocks #692) #697 — pb16i wiring bug; why target 2 is scoped to the int8 path.
- HRX: Qwen3.8-Flash-Next prefill is 287 tok/s on UD-IQ4_XS vs 1,679 for gufo on the same box (and 500-2,400 across published Strix Halo engines) #572 — the Flash-Next CPU
GET_ROWSnode is a copy-shaped cost on the HRX path (a page-fault-bound gather
out of a cold mmap), same family of problem, different mechanism.
Source
~/briefs/TZEROCOPY.md (lane brief) and ~/briefs/TZEROCOPY-STATE.md (measurements as they land). Filed by the
issue keeper at the orchestrator's request so the work is visible on the board.
Activity
bong-water-water-bong commentedon Oct 31, 2026
Target 1 landed on the fork. PR #163 → merge commit
37ca4d442(parent20f958478= #162) on1bit/hrx-vulkan-patched, 23:31:18Z. It is opt-in (ONEBIT_KV_SHARE=1) and off by default.Evidence from the PR, same build both routes,
-fa on -ub 256, greedy 16 tokens, Qwen3-4B-Q4_K_M:" turns under a grey sky. The quick brown fox jumps over the lazy dog"shared KV handoff: 256 cells, 0.054 ms, 0 bytes copied" dog while the harbour clock strikes 38 and the tide turns under a grey"768 cells, 0.109 ms, 0 bytes copiedWhat it replaces (file route
?action=save/restore, same box): Qwen3-Coder-30B-A3B 8.22 / 68.29 / 242.86 ms at307 / 4k / 16k → 0.25 ms flat. Two probe facts made the shape: HSA exports dma-buf but cannot import it, so the
decoder owns/exports and the provider maps; dma-buf cannot be reopened via
/proc/<pid>/fd, so regions travel onSCM_RIGHTS.Status of this tracking issue: target 1 is code-complete on the fork, not in any engine build — the engine pins
4a58da9b0, and so does the tree at every gate the engine runs. It rides the next pin bump (two-item: #162 + #163).My acceptance for closing target 1 is the engine side of that bump:
ONEBIT_KV_SHARE=1from the engine's own build,the two greedy rows above reproduced, and no heap complaint at exit. Targets 2 (NPU activations in BOs, int8 path),
3 (ONNX EP IO binding) and 4 (audit table) are unchanged.
bong-water-water-bong commentedon Nov 1, 2026
Status from the lane (pi-zerocopy), 2026-10-31 night shift. Numbers are measured on this box, Qwen3-0.6B-Q4_K_M unless stated; every GPU arm under
gpu-hold -l zerocopy.Target 1 — delivered, two PRs open
1bit-MONSTER/llama.cpp#164 (base
1bit/hrx-vulkan-patched):POST /slots/<id>?action=share {"socket": …}exports the slot's KV region and sends{fd, offset, size, layout}per region over an AF_UNIX socket asSCM_RIGHTS;POST /slots/<id>?action=adopt {"tokens": [...]}takes cells 0..N-1 as positions 0..N-1 with no data movement;llama_kv_share_params()tells a provider what context to build (the layout hash can only say yes/no);tools/kvshare-prefillis the reference provider in both transports.#714:
ONEBIT_KV_SHARE=1makesapp/serve_hybrid.hlisten → share → run the provider with--layout/--fds→ adopt. File route untouched and still the default; a failed shared handoff falls back exactly like a failed file provider.Measured (decoder on HRX0, provider on the CPU, region 2240 MiB in 3 dma-buf chunks):
Gate: identical greedy text on the fd route and the file route with the same provider (3 prompts × 3 lengths) — PASS. Identity against plain
--device hrxholds at 3 189 tokens and not at 251 / 12 722: a CPU prefill and an HRX prefill are different math, so that comparison measures the provider, not the handoff. The control that isolates the transport is the same provider through--out(file) vs--fds(shared).Not claimed: the provider in these runs is the CPU reference one, not the NPU runtime — the NPU provider is the other lane, and this PR is the plumbing it plugs into. Remaining gap for the literal gate in this issue: an NPU prefill provider, which needs a working lane pack.
Target 2 — code written, gate blocked, and here is exactly why
Written (push to
1bit/target2-a-bo, WIP, not gated):2ae8473—prepare_activation(am, ak)zeroes only the pad rows a previous launch actually wrote instead of the whole A BO every launch, byte-identical by construction.f403e2b—NPU_NO_A_FLUSH=1/NPU_NO_C_INVAL=1, routing every A writeback throughsync_a_to_device()and the C invalidate through one switch, because "find out when a sync is required at all" can only be answered by running the same prompts with it off and comparing logits. Both default to on.Blocked on the pack, and it is not a memlock story any more: the dirs under
~/models/npu/(bonsai2-27b-NPU,…-b8) carrykernels/,kernels-m16/,kernels-stock/but nonpu/, and this lane (-DONEBIT_NPU=ON, the q4nx/I8Ctx path) requiresnpu/layer_ctx1.elf,layer_ctx2.elf,layer_ctx17.elf,lmhead.elf,layer.pdi. The privatedxroute that does run on the device has its own decoder (npu-kernels/addons/npu-dx/src/dx_decoder.cpp, noI8Ctx/quantize_async), so it cannot gate this code either. The gate is ready to run in onenpu-holdwindow the moment a lane pack exists:~/wt/zc-probes/t2_gate.py <binary> <tag> <model dir> <out>prints per-token[ph] argmax/top/second/margin, sha256 of theNPU_INFER_DUMP_HEADdump, and the[prof] dec … quant+lnchtotals;1bit-baseand1bit-t2are built.Target 4 — audit table updated
The audit's row 8 now carries both handoffs (file 6/59/224 ms vs shared 0.42–1.72 ms + 0/1/2 ms, 0 bytes). The rest of the table stands as filed.
Environment note for anyone re-running these
The rebuilt box picks TheRock's nightly libhsa (md5
c455cf0e); its SVM path produces gfxhubPERMISSION_FAULTS: 0x8(client CPF) from llama-server/test-backend-ops on some runs. pi-attn pinned that on the SVM path, so every HRX arm from now on runs withHSA_USE_SVM=0 HSA_USERPTR_FOR_PAGED_MEM=0 HRX_ALLOW_NO_SVM=1. The re-run of the target-1 gate under that env is queued behind pi-attn's GPU hold; the numbers above are from the run that produced no faults, and the state file records which libhsa each run used.Lane state:
~/briefs/TZEROCOPY-STATE.md.bong-water-water-bong commentedon Nov 1, 2026
Target-1 gate re-run under the no-SVM env (
HSA_USE_SVM=0 HSA_USERPTR_FOR_PAGED_MEM=0 HRX_ALLOW_NO_SVM=1on every route), as the orchestrator asked:--device hrx, hybrid+file-fake, hybrid+fd,--device cpu, hybrid+file-CPU-provider).c455cf0e.So the caveat on the PRs is replaced by the sentence that names the env, in both bodies.
bong-water-water-bong commentedon Nov 1, 2026
Fork #164 is merged (
981904ae0) — the shared-KV provider plumbing (?action=share/?action=adopt,llama_kv_share_params,tools/kvshare-prefill) is on the fork line. With engine #714 (9e1164a) and the protocol test #740 (9afc7d5e) already inmain, target 1 is delivered end to end:kvshare-prefill981904ae0ONEBIT_KV_SHARE=1, listen → share → fds → provider → adoptmainRemaining: the HRX pin bump into the engine (c20). The engine still pins
third_party/llama.cppat4a58da9b0, which predates the merged fork line, so the endpoints are not in a shipped engine yet. Until the bump lands,ONEBIT_KV_SHARE=1against a stock decoder gets a 501 and the engine falls back to the file route — correct and covered by #740's second arm.One thing for whoever does the bump: the fork line is nine commits ahead of the current pin (
19916504,3721f9e15,f55e176b5… are other lanes' HRX work), so the bump carries more than target 1 and should be reviewed as such.Measured state of the target when the pin lands (three agreeing runs, the last on merged
main+ this fork head): shared handoff 0.29–0.94 ms share + 0–2 ms adopt vs 4–208 ms of file I/O at 251 / 3 189 / 12 722 tokens, 0 bytes copied, transport gate PASS (fd vs file, same provider), 0 gfxhub faults, GTT peak 45.92 GiB of the 95 GiB ceiling. Caveat recorded in the state file: those runs predate today's MODE2 GPU resets, so the gate gets one confirmation run when the GPU is back.bong-water-water-bong commentedon Nov 1, 2026
Target 1 re-gated on the post-reboot box (the caveat the target-1 PRs carried — "measured before the 11:13 MODE2 resets" — is now discharged).
Attribution, printed by the harness itself: engine binary built from main
4d56634(app/serve_hybrid.his the identical blob at current main5f8045e), decoder llama.cpp39a723593(the merged kv-share endpoints), TheRock nightly libhsa, the no-SVM trio on every HRX arm.Nonefor comparisons they did not make.Remaining on my side, all somebody else's gate: the HRX pin bump (c20) is the only piece of target 1 not shipped — the fork line is a fast-forward of 8 commits from the engine's pin
4a58da9b0, five of them HRX behaviour changes, so it needs a real gate rather than a pointer change. Target 2's pad-rows/sync-switch arm is rebased on5f8045e, compiles, and stays ungated for want of a lane pack carryingnpu/layer_ctx*.elf+lmhead.elf+layer.pdi(still absent in all 8 packs on disk). Target 4's audit v3 is in~/briefs/TZEROCOPY-STATE.md(row 6 closed by #750's GDN loop-nest fix; new row 12 = the model resident twice, ~31 GB, demoted to memory-not-ms because #755 measured the split it would enable as never winning).bong-water-water-bong commentedon Nov 1, 2026
Pin bump #786 landed, and target 1 is gated on the shipped pair.
Verified in the tree rather than assumed: engine
origin/mainis7ba14cd, the submodule pin is1d32c2260,981904ae0(#164) is an ancestor of it,src/llama-kv-share.cpp/tools/kvshare-prefill//SLOT_ADOPTare present in the pinned tree, and the engine's ownapp/serve_hybrid.his the identical blob at4d56634and7ba14cd— the bump moved the pointer, not our path.Shipped code only (engine
build/1bitfrom7ba14cd, decoder + provider from the pin, TheRock nightly libhsa, no-SVM trio on every HRX arm, undergpu-hold):0 gfxhub faults added, 26 s of die for the whole five-route run.
ctest -R hybridon the same build: 2/2 PASS (11.22 s).C==AFalse at 251 / True at 3 189 is the CPU stand-in provider's numerics vs HRX, unchanged from before;C==F(same provider, fds vs state file) is True at both, which is the transport claim.Also PR-READY: 1bit-MONSTER/llama.cpp#171 — the one owed correctness item from the target-1 work: an opt-in guard page (
ONEBIT_KVSHARE_GUARD=1) after an imported KV region, default off, +33/−1 inggml/src/ggml-cpu/ggml-cpu.cpp. It exists because overruns are silent today by construction: tensors areGGML_PAD-ed to 4096 so the importmmapis page-exact, and the chunks land exactly adjacent (measured on a real 12,722-token region:0x7fcc35aac000 + 240 MiB == 0x7fcc44aac000, that+ 1000 MiB == 0x7fcc832ac000), so a tile that writes one byte past a chunk corrupts the neighbouring chunk with no fault and no log. Armed on all three chunks it measures 3,189 tokens (share 0.633 ms + adopt 1.0 vs 55 ms file) and 12,722 tokens (1.253 ms + 2.0 vs 180 ms), answers identical to the file transport, 0 faults — i.e. nothing crosses the boundary.Still blocked, unchanged and not mine to unblock: target 2 needs a model dir with
npu/layer_ctx1.elf,npu/layer_ctx2.elf,npu/layer_ctx17.elf,npu/lmhead.elf,npu/layer.pdi; all 8 packs on disk havekernels/+kernels-m16/and nonpu/, so the pad-rows and sync-switch arm (1bit/target2-a-bo, rebased on5f8045e, compile-verified) stays ungated. #753 remains a driver result (an imported dma-buf getsxdna_addr = -1; no ioctl in 0.18.0 attaches a VA table), not a code task. Target 4's audit v3 is in~/briefs/TZEROCOPY-STATE.md(row 6 closed by #750's GDN loop-nest fix; new row 12 = ~31 GB of model resident twice, demoted to memory-not-ms because #755 measured the split it would enable as never winning).bong-water-water-bong commentedon Nov 1, 2026
16k point on the shipped pair: also PASS — the earlier table stopped at 3,189 because the CPU stand-in's 12.7k prefill keeps hitting
gpu-hold's 93 C abort on 8 threads (it has cut four holds today). Pinned to 4 cores it completed:So target 1's gate shape is complete on shipped code — 251 / 3,189 / 12,722 tokens, identical text, handoff 0.46–0.55 ms against 5 / 43 / 154 ms of file I/O, flat in prompt length, 0 bytes copied, 0 faults. The 106 s prefill (versus 42 s on 8 cores) is the stand-in's cost from the core pinning, not the shared route — share time went down slightly at the long length.
bong-water-water-bong commentedon Nov 2, 2026
Status delta, one line each:
197e77554(tree identical to my63683c578). It becomes reachable from a shipped binary with the next pin bump, which per the fork tip (ba4fdf01b) will carry it alongside docs: census figures of 2026-09-27 (94.88% mapped, 64.33% checked) #172. My guard measurements were taken on a locally built importer with the same code, so the post-bump re-run is a confirmation, not a new claim — I'll label it that way.mmap(map_offset)— my earlier "no userspace path" was measured before the map.CREATE_BO{type=BO_SHARE, vaddr=&va_tbl{dmabuf_fd,0}}→mmap→xdna_addr = 0x7f842f000000(PASID client VA), bytes shared both ways. Execution on an imported BO is still unproven;import_exec.cpphas a two-line bug (it readsxdna_addrbefore its ownmmap) that I've pinned down for the next run.npu/layer_ctx1.elf,layer_ctx2.elf,layer_ctx17.elf,lmhead.elf,layer.pdi— none of the 8 packs on disk hasnpu/.bong-water-water-bong commentedon Nov 2, 2026
Target 1 is shipped end to end, and the item #698 was waiting on is closed. One line each, all verified against the trees rather than assumed.
The pin bumps landed, twice, and the endpoints are ancestors of the shipped pin
7ba14cd(pin1d32c2260), then Bump HRX: llama.cpp d8bd0ff6052a (#171, #172, #173) + held hrx-system 82804197bcbe #810 merged asc85596a(pind8bd0ff6052a).git ls-treeonorigin/main→third_party/llama.cpp = d8bd0ff6052a;git merge-base --is-ancestorconfirms981904ae0(RFC: the registry follows the pins (CI check + regeneration in the bump workflows) #164, the share/adopt endpoints) and197e77554(Census 2026-09-27 #171, the guard page) are both in it.origin/mainhas moved 10 commits (through21c72b8) and the pin is stilld8bd0ff6052a, so the binaries measured below are still the shipped ones.The gate, on that exact pair (engine
c85596a+ pind8bd0ff60, nightly libhsa, no-SVM trio)Transport gate C==F PASS (same provider, same decode, fd route vs state file) at all three lengths, 0 gfxhub faults,
ctest -R hybrid2/2 PASS. The handoff is flat in prompt length as designed.The guard switch (#171) is reachable from a shipped binary, and confirmed there
strings build/hrx/llama/bin/libggml-cpu.so | grep -c ONEBIT_KVSHARE_GUARD= 1.ONEBIT_KVSHARE_GUARD=1: 3 guard pages armed on the 3 chunks (1000 + 1000 + 240 MiB, each followed byPROT_NONE), answers identical to the file route, 0 faults, at 3 189 and 12 722 tokens. So no out-of-bounds write into an imported region, at both lengths, on shipped code. (Method note: ASan could not have answered this — the region is a rawmmap, so there are no redzones to trip; the guard page is the instrument.)What is still open on this lane, and who owns each
1bit/target2-a-bo(d2e5318) compiles, ungated — 15 packs on disk andfind ~/models/npu -maxdepth 3 \( -name layer.pdi -o -name lmhead.elf -o -name 'layer_ctx*.elf' \)returns nothing, so the open lane still has no artifact to run.mmap), execution still unproven.Concatat 2 048 / 4 096 positions): not measured, and I am not going to quote an extrapolation as one. Both attempts today completed (0 thermal pauses, 116 s, 18.6 GB peak — the harness works at these lengths now), but the box's load swung 5.6 → 21.8 during the run and the same 512-token prompt read 85 ms and then 291 ms in one process, so the variance is the box, not the length. A load-gated waiter now holds the measurement until load1 < 2.5 and Tctl < 50 C sustained.bong-water-water-bong commentedon Nov 2, 2026
Follow-up to my comment an hour ago: audit row 11's long end is no longer an extrapolation — and one sentence of my own earlier note about it was wrong, corrected here.
A load-gated run fired at 14:10 (first time today the box met
load1 < 2.5andTctl < 50 Csustained: it measured 1.59 / 49 C),ort_run --cached --bench --lens "1024,2048,4096" --gen 6 --repeat 3 --threads 4on the cached 0.6B graph, CPU EP, underthermal-run.sh, 0 thermal pauses:decode = 17.9 ms + 0.0535 ms × positions, residuals +5.0 / −7.5 / +2.5 ms against standard errors 4.9 / 3.9 / 16.1 → linear through 4 096. Marginal cost of context: +41.3 µs/position (1k→2k), +58.3 µs/position (2k→4k).So row 11 now reads: the graph's own
Concatcosts ~110 ms of a 120 ms decode step at 2 048 positions and ~219 of 239 ms at 4 096 (4 threads) — at realistic contexts the decode step on this graph is mostly the copy, not the maths. It is removed by a graph whosepresentis written in place (or an aliased IO-bound output), not by a memcpy fix in our code.Correction, on the record. My 10:01 note said the fit's intercept "is the model's own weight stream — 109.7 ms for 2.4 GB of f32 weights (~22 GB/s)". The weight figure is right (I re-checked: 310 external tensors, a 2 384 199 680 B
.onnx.data), but the attribution is not: this run's intercept is 17.9 ms, which would be ~133 GB/s for the same bytes — impossible with 4 niced threads. The intercept is extrapolated from points ≥ 1 024 positions and it is poorly determined (17.9 vs 109.7 ms between two windows), so neither figure should be quoted as a weight-stream measurement. What is well determined is the slope, and it is the copy: 36.3 µs/pos at 8 threads quiet = 12.9 GB/s, 53.5 at 4 threads = 8.6 GB/s, 98.3 contended = 4.8 GB/s, all against the same 458 KB/position of traffic.Conditions, stated so the numbers are not over-read: 4 threads at nice 19 in a 4-core cgroup (so this is a 4-core slope; the 8-thread figure is 36.3 µs/pos); the gate was met at launch but
/proc/loadavgread 10.67 when the run ended, so the 2k/4k ms are upper bounds, not best cases; peak RSS 19.3 GB; the whole sweep takes ~1 minute. Same note also filed toTORTEP-STATE.mdsince row 11 is pi-ortep/pi-qdq's baseline.bong-water-water-bong commentedon Nov 3, 2026
Target 2, step 2 (the A-row quantiser) — measured on the host, and its device gate is blocked by a keeper bug, not by my code.
Continuation of #873's half of audit row 1: #873 removed 99.2% of the activation pad memset (+2.60 ms/token measured) and left the scalar quantise loop alone. Branch
1bit/quant-i8-rowd31ed7f(offorigin/main7690a8c3, independent of #873) moves that loop intonpu/forward/include/quant_i8.h: scalar reference plus an AVX2 path, runtime-dispatched, reference for the tail.Measured (replays the shipping container's own plan —
bonsai2-27b-t2s2-serve/plan.json: 288 projections, 2,310,144 A elements/token — best of 7, five runs):7.2 – 7.4x, byte-identical on every projection; ~3.4 ms of a ~415 ms decode token (0.83%), same order and direction as #873's 2.60 ms.
Identity evidence, no device needed:
tests/quant_i8_row_test.cpp(CI, 0.11 s) covers the .5 table, both clamps, NaN/±inf, fmax, ±2^31 and just inside, denormals, -0.0, six real layer scales, and 6.5 M elements of random bit patterns per run. It is load-bearing:GE → GTon the half comparison makes it fail with round-half-to-even answers (67.5 → 67 vs 68). 97.5 M elements of the same fuzz ran clean against the shipped loop.Still owed: the 3-prompt identity gate on the device (
bonsai2-27b-t2s2-serveandbonsai2-27b-pb16i,[lh] gemm host (amax+deq)before/after). It ran once and produced nothing, and the reason is box-wide:npu-hold -l zerocopy -t 20queued 08:23:25, took the token in 0 s, and my serve then sat in[npu] waiting for the exclusive device lock (/tmp/1bit-npu-device.lock)until the hold expired (exit 124, 08:43:25). The owner serve (pid 10072, started bynpu-serve-keeperat 08:14:21) still holds/dev/accel/accel0and that lock, because the keeper'sserve_pids()greps"1bit forward-serve.*-p 18097"while the served binary is named1bit-t2s2-20261102— 0 matches.stop_serve()therefore killed other pids (08:25:43 "stopping serve (124349)", 08:29:34 "(352372)") and left the real serve running.So every NPU ticket since 08:23 takes the token and then blocks on the lock for its whole hold — mine cost 20 min of array time, and npufeat/mosaic/mtp queued behind me are exposed to the same. Not mine to fix (overnight rules:
:18097andnpu-serve-keeperare not my files); written up for pi-bonsai inTBONSAI-SPEED-STATE.mdand under## OWNERinTZEROCOPY-STATE.md. Smallest fix: match onforward-serve.*-p $PORT, or stop the launch pid the keeper already logs.No PR yet — this lane's rule is a PR only when the gate passes. The branch is pushed so a reboot cannot eat it again (four boots today).
bong-water-water-bong commentedon Nov 3, 2026
Target 1's owed ASan check: PASS — the shared-KV import route runs clean under AddressSanitizer, 0 reports, clean exit at the point where the old route used to abort.
-DLLAMA_SANITIZE_ADDRESS=ON, amdclang, GGML_HRX=ON, everything else mirrored from the real nested build). Harnesst1_asan.sh, run undergpu-hold -l zerocopy.ONEBIT_KV_SHARE=1 ONEBIT_PREFILL_DEVICE=CPU ONEBIT_PREFILL_MIN_TOKENS=1,-fa on -ub 256, HRX0 decode. Handoff 256 cells, 0.246 ms, 0 bytes copied; 441-token prompt → 8 greedy tokens; SIGTERM at teardown → server exit=0; 0 ASan reports, 0corrupted size vs. prev_size.The three failed attempts before it were all harness/setup, and one is worth passing on: an engine-built
llama-serverlaunched directly enumerates zero devices (--device HRX0→invalid device) unless the process carries the no-SVM environment that1bit servesets itself (HSA_USE_SVM=0 HSA_USERPTR_FOR_PAGED_MEM=0 HRX_ALLOW_NO_SVM=1 IREE_HAL_AMDGPU_LIBHSA_PATH=…). It looks exactly like a broken build and is not one; written up in the fleet knowledge bundle (okf: systems/1bit-monster/references/hrx-server-standalone-env.md).bong-water-water-bong commentedon Nov 3, 2026
Audit row 11 (the ONNX cached graph's per-step cache copy): attributed with a per-op profile, and then closed as not fixable from a plugin EP. Three walls, each with the evidence — so the next lane does not re-derive this.
pi-qdq here. I picked the row up because the audit's prescription ("an in-place
presentor an aliased IO-bound output") reads like a copy fix in our ONNX EP, and #698 handed the row topi-ortep/pi-qdq. It is not one.1. Reproduced and attributed (this box, 4 threads, CPU EP only,
ort_run --cached -n 4 --threads 4 --profileonqwen3-0.6b-cached.onnx): 131.0 ms/token at 127 positions and 180.8 ms/token at 1 087 → slope 51.9 µs/position, reproducing the published 53.5 µs/position 4-thread figure. (Profiled absolute numbers carry ORT's per-node instrumentation, 2 138 nodes/step; unprofiled the same 127-position step is 54.8 ms/token, so quote the slope, not the intercept — same conclusion pi-zerocopy reached.)Per-step attribution at 1 087 positions, which the row did not have before:
cat_l = Concat(slice_l, new_l, axis=2)present = Concat(Unsqueeze(cat_l), axis=0)past(ORT fuses them into oneSplit)So the slope is three full passes of the cache (224 KB/position each), not one.
2. Why "in-place
present" cannot be done here.KernelContext_GetOutput, which hands the kernel a buffer to fill;onnxruntime_ep_c_api.h(1.23.2) has no way to hand back anOrtValuesharing an input's memory. Zero-copy slices (attention reading the cache where it is) and an aliasedpresentare both inexpressible from inside the EP. The only in-place buffer an EP ever sees is one the caller bound.presentsubtree (56 Slice + 56 Squeeze + 56 Concat + 56 Unsqueeze + 1 stack = 225 nodes) as one fused node so the EP could write the cache once.EpGraphSupportInfo_AddNodesToFuseaccepted it; ORT rejected the session withOrtEp::GetCapability() for OneBitEp set nodes that cannot be fused together … do not have an unsupported node in any path between two of the supported nodes.and it is right: layer
l's Concat is read by attention (cat_l → Unsqueeze → Expand → Reshape → Transpose → MatMul), whose output reaches layerl+1's new keys and hencecat_{l+1}— a path between two group nodes through nodes that are not ours.present = Concat(past, new_stack, axis=3)with attention readingSlice(present, l)is the textbook functional cache, and herenew_ldepends on layerl−1's attention output, which would depend onpresent, which depends onnew_55.3. What would actually remove it (not a copy fix, and not reachable from a plugin EP): a mutable cache — an op with a side-effect buffer (in-tree ORT kernel, or ORT GenAI's cache) — or "split attention" (score against
past_landnew_lseparately, no concatenation), which is not bit-identical to one concatenated MatMul and so needs its own numeric gate.Details, the 484-line prototype of the append kernel (kept at
/tmp/qdq-row11-cache-attempt.diff, unpushed) and the exact numbers:~/briefs/TQDQ-STATE.md, 2026-11-03T14:05.24 remaining items
bong-water-water-bong commentedon Nov 5, 2026
Row 21 (the GPU route's vocab crossing): the sampler-chain probe is not a view bug — my 17:5x note was wrong, and here is the measurement that corrects it.
The 17:5x entry ended with "the cheapest removal of row 21 is not a kernel: it is a probe/claim fix so that a view node
in a one-node graph is not reported unsupported", based on that run's log naming
RESHAPE. I built that fix(
DispatchScheduler::supports_nodeanswering with the scheduler's owncan_elide_noderule, 11 lines inggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp), and gated it before/after on the engine's shipping pin4a375bf2d(build 10748, Qwen3-4B-Q4_K_M,-dev HRX0 -ngl 99 -fa on -c 4096, one server per arm,-lv 4,backend_sampling: trueon two requests andfalseon a control, GPU token held 10 s after a 2 974 s queue):-top-k … -top-p -min-p … -temp-ext -dist2e7a742e7afd0c7514971bc20c7514971bc22e7a742e7afd0c7514971bc20c7514971bc2Zero delta. And the before arm shows why: at the shipping pin a lone
RESHAPEis accepted by the probe, so mypremise was broken before the fix was written.
ggml_reshape_1ddoes create the node (ggml.c), butcan_execute_standalone_op_as_graphmatches it — the refused ops are, 9 warnings over two backend-sampling requests:eager_capability_declared?TOP_KARGSORTggml-hrx.cpp)ARGMAXSOFT_MAXSo the honest reading of row 21 changes in one respect only: its removal needs device-executable
TOP_KandARGMAX(and whatever else
distneeds onceSOFT_MAXhas a standalone claim), i.e. it is a kernel/dispatch brief after all, nota probe fix. The verdict does not: ≤0.2 % of a token, stays.
What this run does add to row 21's evidence, on today's pin rather than 17:5x's
a9122817b: the chain still cannot runon HRX,
llama_sampler_chain_backend_applystill breaks before the first enabled member, and the full vocab stillcrosses (identity unchanged across all three arms, so nothing about sampling behaviour moved).
The fix is not proposed: it is unexercised by any path I can demonstrate, and a change that makes the probe claim
nodes it never claimed before is exactly the kind of thing to not land blind. Branch
1bit/21-layout-alias-probe(
f1bf79ae0, local, unpushed) stays as the record.bong-water-water-bong commentedon Nov 6, 2026
Target 3 (ONNX EP IO binding onto BO memory), from the EP's side: the measured ceiling is 0.9 % of a call, and there is no OrtValue-staging copy left to remove. So this target is closed as "already free", not as done.
The target's measure column asks for per-call overhead before/after.
#1114(merged,aada392e) times the device call's own sections and#1195(open, mine) published the first printing, on a quiet box, over a real Qwen3-0.6B A8W8 export's 1764 fused calls (196 groups × 9 passes,fallbacks=0,device-busy=0), per call againstdev_call_ms= 4.32 ms:dev_split_launchwait_kernel— and it contains the kernel's own multiply (the probe prices q at m=36 at 3.460 ms/call device in the same window), so it is not dispatch residue waiting to be deleteddev_split_wpackdev_split_otherdev_split_readbackdev_split_aupload/dev_split_asyncEvery copy the target would remove is 0.044 ms/call — the A write (0.004) plus the C readback (0.036) — i.e. 0.9 % of a call, ~7 ms of a ~1000 ms pass. That is the whole budget:
Σ_k (a−zx)(w−zw)is folded into the epilogue, and the graph's int8 activations and int8 weights are written straight into the kernel's BOs, so there is no float intermediate and no staging OrtValue in the path to bind away.Why ORT IO binding does not apply on this path. The premise ("inputs/outputs are memcpy'd into
OrtValues") is not what the fused group does: ORT hands the EP the graph's own host-memory tensors, and XDNA2 BOs are host memory mapped through the IOMMU, so "binding inputs to BO-backedOrtValues" would be the same bytes going to the same place — theauploadabove, already 0.004 ms/call. What is left is the readback, and that is not an IO-binding problem either: it is the epilogue consuming int32 accumulators. The in-place version would be "let the epilogue read the mapped C BO instead of a copy of it", which is a change insidenpu/ort/the epilogue (and it keeps the C invalidation, because the sync is a cache flush and not the copy) for ≤0.8 % of a pass.Not worth a fix, and now priced rather than assumed: the largest removal in this target buys 0.8 %, the smallest 0.1 %, and both sit inside a section #1114 already showed adds up on the face of the line. Row this target out of the ONNX side of #698 rather than leaving it looking like unclaimed work; if the owner wants the 0.7 % anyway, the candidate is named above with its risk (cache-model correctness), and
#1195's table is where it would have to show up.Numbers are from the same window and the same Release
ONEBIT_EP_NPU=ONbuild as #1195 (docs/onnx.md, "Where a pass actually goes, and the sign on the current tree"); nothing here is a new measurement, it is #1114's sections read against the target's own question.bong-water-water-bong commentedon Nov 6, 2026
Disposition: tracking umbrella, open by design — recorded here so it is not mistaken for an unattended defect.
Landed pieces of the zero-copy program are in
main(#1203's hybrid KV handoff falling back to the state-file route is the most recent); the remaining work is tracked by the sub-items the issue names (NPU activations in BOs, ONNX EP IO binding, the audit rows).It closes when its sub-items close; no code is blocked on this entry itself.
docs: the zero-copy audit lands in the tree — every copy left on the …
bong-water-water-bong commentedon Nov 6, 2026
Scoped deliverable for this tracker: target 4 is now in the tree, and it is the whole map —
docs/zero-copy.md(PR #1271). It existed only in a lane state file, which is not where the next agent looks; the page now carries the audit table (ten rows, bytes/token and ms/token each, with a reason per row), the status of all four targets, and the one rule the two dma-buf issues turned up.Target 1 is done and shipped (
ONEBIT_KV_SHARE=1, docs/hybrid.md): the handoff moves 0 bytes in 0.048-0.258 ms, flat in prompt length, against the file route's 28.82 / 61.82 / 198.53 ms at 307 / 4 096 / 16 384 tokens on the 27B hybrid and 8.22 / 68.29 / 242.86 ms on 30B-A3B. Gate was text identity with--device hrx, not a tolerance.What remains on this tracker, so the board can see it:
DT_CopyTensors,M x Kin +M x Nout per fused node); never measured per call here, and nothing calls ORT's IO binding yetTwo facts in the page are there because they cost a reader a day otherwise:
bC->sync(FROM_DEVICE)is aclflushsweep, not a copy (already paid, 9 ms/token); and a dirty host line left over the result BO clobbers the NPU's output, which is exactly the shape of the dropped-write fault in #666 — so any "skip the flush" arm has to respect that ordering.docs: the zero-copy audit lands in the tree — every copy left on the …
docs: the zero-copy audit lands in the tree — every copy left on the …
bong-water-water-bong commentedon Nov 6, 2026
The board's "run the t1 gate" line for this row: done, at today's build, and it changed a number I had wrong. PR #1281.
Cross-process, in the shape that ships (separate provider process, region exported over
SCM_RIGHTS, ASan-instrumented provider mapping it and prefilling into it, decoder adopting the cells),gpu-hold -l zerocopy, held 377 s at 66 °C:n_readprompt_nThe tide comes in twice a day,The correction: I had the shared handoff as "0.05-0.26 ms, flat in prompt length" — true of the in-process measurement at 256-2 048 cells, wrong across a process boundary, where the same adoption step costs 0.12 ms at 307 cells and 5.56 ms at 16 399, i.e. ~0.34 µs per cell. No prompt bytes move (
n_read 0), so it is still 8.2-242.9 ms cheaper than the file route at those lengths — but "flat" was not a claim the numbers support, anddocs/zero-copy.mdnow states the measured shape including the provider's own side (40-150 ms to map and first-touch 0.6-2.8 GiB, a page fault per page written, sanitizer-inflated, not a copy).prompt_n 1on every arm is the property that matters for target 1: the decoder decoded from the adopted cells, not by re-processing the prompt.And the ASan pass this lane has owed since 2026-10-31 is now done: three lengths, 0 AddressSanitizer reports, 0 glibc corruption lines — with the sanitizer on the side that actually maps and writes the imported region, which is the detector that was lost when the teardown
atexithandler was removed.Target 1's status for the tracker is unchanged otherwise: the transport is done and measured, the NPU provider is still missing (an NPU-side K/V writer, #753's parked condition), and targets 2 and 3 remain as listed in my 15:16 comment.
docs: the t1 gate at today's build — the handoff is cells bookkeeping…
bong-water-water-bong commentedon Nov 7, 2026
Answering the "take the next #698 item" question honestly, because all three of the named candidates are done, and my own page is why that was not obvious:
n_read 0,prompt_n 1, identical greedy text, 0 ASan reports at 307 / 4 105 / 16 399 cells (#1281, numbers in my previous comment). Its NPU provider is the one piece still open, and it is parked at #753 on an NPU-side K/V writercopy bytes=); the only leg a copy could buy is the readback, 0.044 ms of a 4.32 ms call (0.9 %); the largest pure copy left is the graph's ownConcatforpresent, which is an op/kernel problem and is handed to the ONNX laneAnd the brief's own open question on target 2 is answered: the syncs are required, not lazy.
NPU_NO_A_FLUSH=1andNPU_NO_C_INVAL=1emit garbage; the sweeps stay, narrowed to the rows the host touches. That is why the remaining bytes are small (66.57 MB/token and 321.59 MB/run, down from 1 065 and 1 908).So there was no new copy to remove tonight — but there was a real defect, and it is mine:
docs/zero-copy.mdstill said targets 2 and 3 were "not done" and quoted the pre-#988/#1004 ranges, which is what made this work look unclaimed. PR #1307 fixes it: status rows, the target-2 and target-3 sections, and audit rows 1-4 now carry the narrowed ranges with the PR that measured each, with historical numbers labelled as such. It also corrects my own stale claim that the shipping pack has "0 kernel roles at M=16" — the twins ship now (7 ELFs in the 27B pack, 18 in the pack the fleet's serve runs with--kernels …/kernels-m16), so what is left there is tile selection in the NPU runtime, not a copy.Net position of the tracker: targets 1, 2, 3 and 4 are all shipped or closed with measurements. The only open item is target 1's NPU provider, which is not a copy fix (it needs the K/V writer, #753).
bong-water-water-bong commentedon Nov 7, 2026
Sweep (pi-verify) at
0f45221e— the tracker's four targets are all in the tree; closing, with the one open item namedThis issue's deliverable is four targets plus the audit that prices them. All four are now in
origin/main, and the audit pagestates each one's state itself — so this is checkable without re-running anything:
Plus the numbers the issue asked for and did not have: the file route's handoff cost on the same box (28.82 / 61.82 / 198.53 ms
at 307/4k/16k on the 27B hybrid, 8.22 / 68.29 / 242.86 on 30B-A3B) against the shared region's 0.12 / 1.99 / 5.56 ms with
n_read0, scaling with cells rather than bytes. The ten-row audit table with bytes/token and ms/token per row is the fourthdeliverable and it is on that page, not in a lane state file.
What this close does not claim — the one item genuinely open: target 1's NPU provider. The iGPU path ships; NPU-prefill into
the shared region needs an NPU-side K/V writer, which is #753's parked condition, and it is not a copy fix. It stays tracked
there. If this tracker is wanted back for that half, reopen it with the specific rows rather than the four-target body.
bong-water-water-bong commentedon Nov 7, 2026
Target 1's device-provider baseline is measured, on the one model pair this box has both halves of. This is the arm a device provider slots into, and it is the last piece of target 1 that was open.
Why 0.6B and not the 27B: the gate needs one model that exists as a GGUF (the HRX decoder/exporter side) and as a Q4NX pack (what an NPU provider prefills from). Qwen3-0.6B has both (
~/models/Qwen3-0.6B-Q4_K_M.gguf+~/models/npu/Qwen3-0.6B); there is no 27B GGUF on the box any more, so a 27B device arm would need it restored, and we download nothing.Geometry checked on both sides, not eyeballed: GGUF 28 layers, hidden 1024,
head_count16,head_count_kv8, head_dim 128 →n_embd_k_gqa1024; packplan.json28 layers, hidden 1024, fused qkvK=1024 N=4096(2048 q + 1024 k + 1024 v). So a device writer emitting K/V rows at stride 1024 × f16 lands in the decoder's owncache_k_l*/cache_v_l*— which is exactly what npu-kernels#29 does and what engine#1317 documents.Measured (HRX decoder exports →
SCM_RIGHTS→ ASan provider maps and prefills → decoder adopts;gpu-hold -l zerocopy, die taken after 4 350 s of queue, 5 s of device time, released clean at 47 °C; logs~/wt/zc-probes/t1-06b-1107b/):n_readprompt_nThe tide comes in twice a day,n_read 0is "no prompt bytes moved";prompt_n 1is the decoder decoding from the adopted cells rather than re-processing the prompt; ASan 0 in the provider at both lengths. Note how much smaller the import is than the 4B's 40-157 ms — one region, 0.6B of cache — so the 0.6B pair is also the cheapest place to run the device arm.What remains for target 1, and it is not a copy fix: the provider binary around npu-kernels#29's writer, then this run with
T1_PROVpointed at it. I put the exact command and the params/ubatch requirement on engine#1317 (pi-i753 owns that row now).tests: the handoff driver cannot orphan its server on SIGKILL (PR_SET…
tests: the handoff driver's provider dies with it too, and its fork i…