Skip to content

tracking: zero-copy on the serving paths (hybrid KV handoff, NPU activations in BOs, ONNX EP IO binding, audit) #698

Closed

Description

@bong-water-water-bong

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 plus POST /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 branch 1bit/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.cpp memcpy'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_memcpy under NPU_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 for ZEROCOPY-GO, take
    only /tmp/1bit-npu-device.lock, write ZEROCOPY-DONE); the owner's Bonsai serve owns npu.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

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

  1. bong-water-water-bong commented on Oct 31, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Target 1 landed on the fork. PR #163 → merge commit 37ca4d442 (parent 20f958478 = #162) on
    1bit/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:

    n_tokens A: prefill+decode on HRX B: CPU prefill into the decoder's KV region
    307 " turns under a grey sky. The quick brown fox jumps over the lazy dog" identical text, shared KV handoff: 256 cells, 0.054 ms, 0 bytes copied
    1024 " dog while the harbour clock strikes 38 and the tide turns under a grey" identical text, 768 cells, 0.109 ms, 0 bytes copied

    What it replaces (file route ?action=save/restore, same box): Qwen3-Coder-30B-A3B 8.22 / 68.29 / 242.86 ms at
    307 / 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 on
    SCM_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=1 from 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.

  2. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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 as SCM_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-prefill is the reference provider in both transports.

    #714: ONEBIT_KV_SHARE=1 makes app/serve_hybrid.h listen → 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):

    prompt tokens file transport (restore) shared transport (share + adopt)
    251 6 ms 1.72 + 0.00 ms
    3 189 59 ms 0.42 + 1.00 ms
    12 722 224 ms 1.01 + 2.00 ms

    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 hrx holds 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 through sync_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) carry kernels/, kernels-m16/, kernels-stock/ but no npu/, and this lane (-DONEBIT_NPU=ON, the q4nx/I8Ctx path) requires npu/layer_ctx1.elf, layer_ctx2.elf, layer_ctx17.elf, lmhead.elf, layer.pdi. The private dx route that does run on the device has its own decoder (npu-kernels/addons/npu-dx/src/dx_decoder.cpp, no I8Ctx/quantize_async), so it cannot gate this code either. The gate is ready to run in one npu-hold window 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 the NPU_INFER_DUMP_HEAD dump, and the [prof] dec … quant+lnch totals; 1bit-base and 1bit-t2 are 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 gfxhub PERMISSION_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 with HSA_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.

  3. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Target-1 gate re-run under the no-SVM env (HSA_USE_SVM=0 HSA_USERPTR_FOR_PAGED_MEM=0 HRX_ALLOW_NO_SVM=1 on every route), as the orchestrator asked:

    • 0 gfxhub faults added during the whole run (all five routes: --device hrx, hybrid+file-fake, hybrid+fd, --device cpu, hybrid+file-CPU-provider).
    • Transport gate still PASS: identical greedy text on the fd route and the file route with the same provider, 3 prompts at 251 / 3 189 / 12 722 tokens.
    • Handoff still flat: share 0.50 / 0.52 / 0.44 ms, adopt 0 / 1 / 2 ms (run above: 1.72/0.42/1.01 + 0/1/2 ms — same thing within run-to-run noise).
    • Absolute wall times in this run are higher than in the run quoted on the PRs (file restore 22/67/249 ms, first byte 428 ms at 251 tokens) because the die was shared with pi-attn's work; the numbers on the PRs are from the run where I had the die to myself, and they produced no faults either. Both runs used the nightly libhsa c455cf0e.

    So the caveat on the PRs is replaced by the sentence that names the env, in both bodies.

  4. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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 in main, target 1 is delivered end to end:

    half where state
    provider plumbing, dma-buf fds over a unix socket, kvshare-prefill fork 981904ae0 merged
    engine: ONEBIT_KV_SHARE=1, listen → share → fds → provider → adopt engine #714 merged
    protocol test, 11 checks, no model/GPU/dma-buf engine #740 merged
    docs/hybrid.md second provider contract engine main merged

    Remaining: the HRX pin bump into the engine (c20). The engine still pins third_party/llama.cpp at 4a58da9b0, which predates the merged fork line, so the endpoints are not in a shipped engine yet. Until the bump lands, ONEBIT_KV_SHARE=1 against 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.

  5. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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.h is the identical blob at current main 5f8045e), decoder llama.cpp 39a723593 (the merged kv-share endpoints), TheRock nightly libhsa, the no-SVM trio on every HRX arm.

    prompt tokens file route: restore shared: share shared: adopt transport gate
    251 4.0 / 7.0 ms 0.542 ms 0.0 ms C==F PASS
    3 189 69 ms 0.423 ms 1.0 ms C==F PASS (B==A, C==A, C==E also PASS)
    12 722 217 ms 0.583 ms 2.0 ms C==F PASS
    • 0 gfxhub faults added (box-wide count stayed at 3 lines, all from the boot-time foreign serve). The 251-token attempt was cut by the thermal guard after its routes had answered; the fault count was 3 before and 3 after.
    • Handoff flat in prompt length: 0.42-0.58 ms against 4-217 ms of file I/O for the same provider and decode, 0 bytes copied.
    • The 12 722 point was run transport-only (C fd vs F file, same provider): route A's 12.7k-token HRX prefill is the harness's biggest heat source and the transport claim does not need it. Narrowed runs print None for 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 on 5f8045e, compiles, and stays ungated for want of a lane pack carrying npu/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).

  6. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Pin bump #786 landed, and target 1 is gated on the shipped pair.

    Verified in the tree rather than assumed: engine origin/main is 7ba14cd, the submodule pin is 1d32c2260, 981904ae0 (#164) is an ancestor of it, src/llama-kv-share.cpp / tools/kvshare-prefill/ / SLOT_ADOPT are present in the pinned tree, and the engine's own app/serve_hybrid.h is the identical blob at 4d56634 and 7ba14cd — the bump moved the pointer, not our path.

    Shipped code only (engine build/1bit from 7ba14cd, decoder + provider from the pin, TheRock nightly libhsa, no-SVM trio on every HRX arm, under gpu-hold):

    prompt tokens file route: restore shared: share shared: adopt first byte (file / shared) gate
    251 5 ms 0.545 ms 0.0 ms 184 / 497 ms C==F PASS, B==A PASS
    3 189 43 ms 0.532 ms 1.0 ms 626 / 3 632 ms C==F PASS, B==A PASS, C==A PASS, C==E PASS

    0 gfxhub faults added, 26 s of die for the whole five-route run. ctest -R hybrid on the same build: 2/2 PASS (11.22 s). C==A False 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 in ggml/src/ggml-cpu/ggml-cpu.cpp. It exists because overruns are silent today by construction: tensors are GGML_PAD-ed to 4096 so the import mmap is 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 have kernels/+kernels-m16/ and no npu/, so the pad-rows and sync-switch arm (1bit/target2-a-bo, rebased on 5f8045e, compile-verified) stays ungated. #753 remains a driver result (an imported dma-buf gets xdna_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).

  7. bong-water-water-bong commented on Nov 1, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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:

    shipped pair, 12,722 tokens shared (fd) file control
    answer identical to the file route —
    share / adopt 0.462 ms / 2.0 ms restore 154 ms
    stand-in prefill 106.3 s (4 cores) 95.6 s (4 cores)
    gate C==F PASS, 0 gfxhub faults —

    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.

  8. bong-water-water-bong commented on Nov 2, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Status delta, one line each:

    • Fork Census 2026-09-27 #171 (guard page) MERGED as 197e77554 (tree identical to my 63683c578). 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.
    • NPU: execute on an imported dma-buf (XRT cannot bind one today; iGPU->NPU sharing already works) #753 is bucket A (lane-fixable, no amdxdna change): the imported dma-buf does get a device address, but only after 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.cpp has a two-line bug (it reads xdna_addr before its own mmap) that I've pinned down for the next run.
    • Target 2 still waits on a pack with npu/layer_ctx1.elf, layer_ctx2.elf, layer_ctx17.elf, lmhead.elf, layer.pdi — none of the 8 packs on disk has npu/.
  9. bong-water-water-bong commented on Nov 2, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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

    The gate, on that exact pair (engine c85596a + pin d8bd0ff60, nightly libhsa, no-SVM trio)

    prompt tokens file route: restore shared: share + adopt bytes moved
    251 5 ms 0.545 ms + 0.0 ms 0
    3 189 43 ms 0.532 ms + 1.0 ms 0
    12 722 154 ms 0.462 ms + 2.0 ms 0

    Transport gate C==F PASS (same provider, same decode, fd route vs state file) at all three lengths, 0 gfxhub faults, ctest -R hybrid 2/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.
    • With ONEBIT_KVSHARE_GUARD=1: 3 guard pages armed on the 3 chunks (1000 + 1000 + 240 MiB, each followed by PROT_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 raw mmap, so there are no redzones to trip; the guard page is the instrument.)

    What is still open on this lane, and who owns each

    • Target 2 (NPU activations live in BOs): arm 1bit/target2-a-bo (d2e5318) compiles, ungated — 15 packs on disk and find ~/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.
    • NPU: execute on an imported dma-buf (XRT cannot bind one today; iGPU->NPU sharing already works) #753 (NPU executes on an imported dma-buf): parked on a per-run OK from the orchestrator, as committed on that issue; bucket A stands (an imported dma-buf gets a PASID client VA after mmap), execution still unproven.
    • Audit row 11's long end (ONNX Concat at 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.
    • Row 12 (the same weights resident twice, ≈31 GB on the hybrid route): unchanged — headroom, not ms, per docs(hetero-prefill): HeteroMosaic's split prefill measured on our stack — never wins, and why #755's measurement that the split it would enable never wins (≤1.013x).
  10. bong-water-water-bong commented on Nov 2, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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.5 and Tctl < 50 C sustained: it measured 1.59 / 49 C), ort_run --cached --bench --lens "1024,2048,4096" --gen 6 --repeat 3 --threads 4 on the cached 0.6B graph, CPU EP, under thermal-run.sh, 0 thermal pauses:

    positions decode avg (ms/token) p50 n
    1 024 77.7 73.7 18
    2 048 119.9 117.8 18
    4 096 239.4 218.4 18

    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 Concat costs ~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 whose present is 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/loadavg read 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 to TORTEP-STATE.md since row 11 is pi-ortep/pi-qdq's baseline.

  11. bong-water-water-bong commented on Nov 3, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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-row d31ed7f (off origin/main 7690a8c3, independent of #873) moves that loop into npu/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):

    arm ms/token ns/element
    scalar (shipped loop) 3.96 – 4.05 1.72 – 1.76
    AVX2 dispatch 0.55 0.238

    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 → GT on 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-serve and bonsai2-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 20 queued 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 by npu-serve-keeper at 08:14:21) still holds /dev/accel/accel0 and that lock, because the keeper's serve_pids() greps "1bit forward-serve.*-p 18097" while the served binary is named 1bit-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: :18097 and npu-serve-keeper are not my files); written up for pi-bonsai in TBONSAI-SPEED-STATE.md and under ## OWNER in TZEROCOPY-STATE.md. Smallest fix: match on forward-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).

  12. bong-water-water-bong commented on Nov 3, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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.

    • Build: standalone sanitized configure of the pinned llama.cpp (-DLLAMA_SANITIZE_ADDRESS=ON, amdclang, GGML_HRX=ON, everything else mirrored from the real nested build). Harness t1_asan.sh, run under gpu-hold -l zerocopy.
    • Route: 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, 0 corrupted size vs. prev_size.
    • Scope: ASan has no shadow over the imported mapping (device-owned memory), so this covers the import path, cells bookkeeping, slot/attach and teardown — not an out-of-bounds access inside the region itself.

    The three failed attempts before it were all harness/setup, and one is worth passing on: an engine-built llama-server launched directly enumerates zero devices (--device HRX0 → invalid device) unless the process carries the no-SVM environment that 1bit serve sets 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).

  13. bong-water-water-bong commented on Nov 3, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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 present or an aliased IO-bound output") reads like a copy fix in our ONNX EP, and #698 handed the row to pi-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 --profile on qwen3-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:

    what node(s) ms/step
    per-layer cat_l = Concat(slice_l, new_l, axis=2) 56 ~48
    present = Concat(Unsqueeze(cat_l), axis=0) 1 20.4
    56 slices of past (ORT fuses them into one Split) 56 → 1 21.7
    ~90 of a ~170 ms step

    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.

    1. A plugin EP cannot return an aliased tensor. Kernel outputs come from KernelContext_GetOutput, which hands the kernel a buffer to fill; onnxruntime_ep_c_api.h (1.23.2) has no way to hand back an OrtValue sharing an input's memory. Zero-copy slices (attention reading the cache where it is) and an aliased present are both inexpressible from inside the EP. The only in-place buffer an EP ever sees is one the caller bound.
    2. ORT refuses the fusion, by convexity. I built the whole present subtree (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_AddNodesToFuse accepted it; ORT rejected the session with
      OrtEp::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 layer l+1's new keys and hence cat_{l+1} — a path between two group nodes through nodes that are not ours.
    3. The graph rewrite is cyclic. present = Concat(past, new_stack, axis=3) with attention reading Slice(present, l) is the textbook functional cache, and here new_l depends on layer l−1's attention output, which would depend on present, which depends on new_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_l and new_l separately, 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.

  14. 24 remaining items

  15. bong-water-water-bong commented on Nov 5, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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_node answering with the scheduler's own can_elide_node rule, 11 lines in
    ggml/src/ggml-hrx/dispatch/dispatch-scheduler.cpp), and gated it before/after on the engine's shipping pin
    4a375bf2d
    (build 10748, Qwen3-4B-Q4_K_M, -dev HRX0 -ngl 99 -fa on -c 4096, one server per arm, -lv 4,
    backend_sampling: true on two requests and false on a control, GPU token held 10 s after a 2 974 s queue):

    arm chain marks refusals greedy temp control (host chain)
    before (pin) -top-k … -top-p -min-p … -temp-ext -dist 9 2e7a742e7afd 0c7514971bc2 0c7514971bc2
    after (pin + fix) identical 9, identical 2e7a742e7afd 0c7514971bc2 0c7514971bc2

    Zero delta. And the before arm shows why: at the shipping pin a lone RESHAPE is accepted by the probe, so my
    premise was broken before the fix was written. ggml_reshape_1d does create the node (ggml.c), but
    can_execute_standalone_op_as_graph matches it — the refused ops are, 9 warnings over two backend-sampling requests:

    op named in the refusal sampler in eager_capability_declared?
    TOP_K top-k no
    ARGSORT top-p yes — refused standalone because only a fused dispatch claims it (by design, ggml-hrx.cpp)
    ARGMAX min-p, temp-ext no
    SOFT_MAX dist yes — same fused-only reason

    So the honest reading of row 21 changes in one respect only: its removal needs device-executable TOP_K and ARGMAX
    (and whatever else dist needs once SOFT_MAX has a standalone claim), i.e. it is a kernel/dispatch brief after all, not
    a 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 run
    on HRX, llama_sampler_chain_backend_apply still breaks before the first enabled member, and the full vocab still
    crosses (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.

  16. bong-water-water-bong commented on Nov 6, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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 against dev_call_ms = 4.32 ms:

    section ms/call share what it is
    dev_split_launch 3.90 90 % launch + wait_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 deleted
    dev_split_wpack 0.25 5.8 % weight pack + upload, a cache miss (once per weight) — #1047's subject, corroborated at 0.24
    dev_split_other 0.13 3 % device lock, kernel lookup, the EP's own frame
    dev_split_readback 0.036 0.8 % C invalidation + the copy back
    dev_split_aupload / dev_split_async 0.004 / 0.004 0.2 % the A flush

    Every 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-backed OrtValues" would be the same bytes going to the same place — the aupload above, 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 inside npu/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=ON build 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.

  17. bong-water-water-bong commented on Nov 6, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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.

  18. bong-water-water-bong commented on Nov 6, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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:

    target what is left who can move it
    1 the NPU provider that writes K/V into the shared region: the gate above prefills with the CPU stand-in. Needs an NPU-side K/V writer — kernel work in the private tree, tracked in #753 (parked until the writer exists) private NPU lane
    2 activations live in BOs. The measured ceiling is smaller than the target implies: the A stage is 50 ms and the readback 9 ms of a measured 1 330 ms token, i.e. ~1.5 %, so the honest win is a few percent. The two levers are the 128-row pad (the shipping container registers 0 kernel roles at M=16) and the 368 launches per token — NPU-runtime work, not a copy fix. Row 6 of the audit is the bigger number (604 MB/token of GDN recurrence) and only a kernel removes it NPU-runtime + kernel lanes
    3 ONNX EP IO binding onto BO memory. Priced in the audit (DT_CopyTensors, M x K in + M x N out per fused node); never measured per call here, and nothing calls ORT's IO binding yet ONNX EP lane
    4 done in #1271 —

    Two facts in the page are there because they cost a reader a day otherwise: bC->sync(FROM_DEVICE) is a clflush sweep, 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.

  19. bong-water-water-bong commented on Nov 6, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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:

    prompt tokens region provider import decoder adopt cells n_read prompt_n greedy text ASan
    307 648 MiB 40.57 ms 0.122 ms 307 0 1 The tide comes in twice a day, 0
    4 105 1.1 GiB 78.73 ms 1.988 ms 4 105 0 1 identical 0
    16 399 2.8 GiB 150.40 ms 5.558 ms 16 399 0 1 identical 0

    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, and docs/zero-copy.md now 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 1 on 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 atexit handler 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.

  20. bong-water-water-bong commented on Nov 7, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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:

    candidate state evidence
    hybrid KV dma-buf handoff (target 1) shipped and re-measured at today's build 0 bytes, 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 writer
    NPU activations in BOs (target 2) closed #908 + #873 (quantise straight into the mapping, pad memset gone), #988 (C invalidate 1 065.09 → 66.57 MB/token, 11.0-11.3 → 2.0-4.0 ms/token), #1004 (A writeback 1 908.62 → 321.59 MB/run); each gated byte-identical at token/logprob/head level
    ONNX EP IO binding (target 3) closed by measurement the fused path's node boundary already moves 0 bytes (1 983 trace lines, never a copy 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 own Concat for present, which is an op/kernel problem and is handed to the ONNX lane

    And the brief's own open question on target 2 is answered: the syncs are required, not lazy. NPU_NO_A_FLUSH=1 and NPU_NO_C_INVAL=1 emit 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.md still 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).

  21. bong-water-water-bong commented on Nov 7, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Sweep (pi-verify) at 0f45221e — the tracker's four targets are all in the tree; closing, with the one open item named

    This issue's deliverable is four targets plus the audit that prices them. All four are now in origin/main, and the audit page
    states each one's state itself — so this is checkable without re-running anything:

    $ sed -n '29,37p' docs/zero-copy.md      (at 0f45221e)
    | 1 | hybrid KV handoff without the file | **shipped**, opt-in: ONEBIT_KV_SHARE=1 … 0 bytes copied; the cost is cell
    |                                          metadata, 0.12 ms at 307 cells to 5.56 ms at 16 399 |
    | 2 | NPU runtime: activations live in BOs | **closed** — C invalidate 1065.09 -> 66.57 MB/token, A writeback 1908.62 -> 321.59 MB/run |
    | 3 | ONNX EP IO binding onto BO memory | **closed for this tree** — node boundary moves 0 bytes (1 983 trace lines, no `copy bytes=`) |
    | 4 | this audit | **this page** |
    

    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_read 0
    , scaling with cells rather than bytes. The ten-row audit table with bytes/token and ms/token per row is the fourth
    deliverable 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.

  22. bong-water-water-bong commented on Nov 7, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    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_count 16, head_count_kv 8, head_dim 128 → n_embd_k_gqa 1024; pack plan.json 28 layers, hidden 1024, fused qkv K=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 own cache_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/):

    prompt tokens regions provider import decoder adopt n_read prompt_n greedy text ASan
    307 1 1.276 ms 0.109 ms 0 1 The tide comes in twice a day, 0
    1 027 1 1.103 ms 0.227 ms 0 1 identical 0

    n_read 0 is "no prompt bytes moved"; prompt_n 1 is 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_PROV pointed at it. I put the exact command and the params/ubatch requirement on engine#1317 (pi-i753 owns that row now).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperfMeasured performance gap against a named target

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions