|
1 | 1 | # Weight-paging decode-speed work — continuation (morning pickup) |
2 | 2 |
|
3 | | -**Branch:** `feat/wp-vnext` (edit/build-check on **mad-lab-2026** at `/home/kmbandy/GitHub/llama.cpp`). |
4 | | -Pushed to `origin`; **mad-lab-main** is on `feat/dsws-phaseb-conversion` with the wp-vnext commits |
5 | | -cherry-picked. All GPU validation is on mad-lab-main's **R9700 (ROCm0, gfx1201, 32 GB)**; the 6900XT |
6 | | -is ROCm1. Remote shell is **fish** → always `ssh mad-lab-main bash -s <<'EOF'`. |
| 3 | +**Branch (CURRENT):** `feat/router-multigpu` = `feat/wp-vnext` (37 commits) **merged with** the router |
| 4 | +branch, + the `ensure_batch` change. Edit/build-check on **mad-lab-2026** at |
| 5 | +`/home/kmbandy/GitHub/llama.cpp`. Pushed to `origin`. **mad-lab-main** GPU box (R9700 / ROCm0 / gfx1201 |
| 6 | +/ 32 GB; 6900XT = ROCm1) has this branch checked out in a **git worktree at `~/llama-wp`** (its main |
| 7 | +`~/GitHub/llama.cpp` checkout is on `feat/dsws-phaseb-conversion` with uncommitted MAD-305 DSWS work — |
| 8 | +**do NOT touch/checkout that**; always use the `~/llama-wp` worktree for weight-paging builds/runs). |
| 9 | +Remote shell is **fish** → always `ssh mad-lab-main bash -s <<'EOF'`. |
| 10 | + |
| 11 | +--- |
| 12 | +--- |
| 13 | + |
| 14 | +# ★★★ 2026-07-07 EVENING — READ THIS FIRST (supersedes the "NEXT FRONT" below) ★★★ |
| 15 | +## `ensure_batch` (Colibri QD=N page-in) is BUILT + CORRECT but a **2× DECODE REGRESSION**. Root cause found. Fix = **completion demux**. Do that next. |
| 16 | + |
| 17 | +### What happened this session (chronological, so we don't rehash) |
| 18 | +1. **Inspiration:** the GLM-5.2 CPU engine **Colibri** (`github.com/JustVugg/colibri`, file `c/glm.c`) |
| 19 | + does exactly our thesis on CPU — streams routed experts from NVMe. Its decode loop (`glm.c:1016`) |
| 20 | + is the pattern we want: **batch the misses, issue ALL their reads concurrently (`omp parallel`, |
| 21 | + each a coalesced single read), then wait** — QD1→QDN. User: apply that to our weight paging. |
| 22 | +2. **Merged** `feat/wp-vnext` into `feat/router-multigpu` (0 conflicts; pre-existing WIP |
| 23 | + `examples/pagedattn-*`, `src/llama-graph.cpp`, `examples/CMakeLists.txt` stashed/popped — **do NOT |
| 24 | + touch those**). Merge commit on the branch; then implemented `ensure_batch`. |
| 25 | +3. **Implemented `ensure_batch`** — commit **`e5089c241`** (pushed). Three edits: |
| 26 | + - `wp-pager.h/.cpp`: new `WeightPager::ensure_batch(pages, out_ptrs, out_pinned)` — reserve **and |
| 27 | + pin** every cold-miss slot up front (alloc_slot skips pinned, so no sibling read can evict it), |
| 28 | + then on the P2P/direct-to-device path submit all misses in **one `io_uring` batch (QD=N)** into |
| 29 | + the VRAM slots, wait for all, harvest; on read failure, sync-fallback into the **same pinned |
| 30 | + slot**. Returns `out_pinned` for the caller to unpin next callback. |
| 31 | + - `wp-pager.cpp`: `page_in_sync_(page, reuse_slot=-1)` refactor — reads into a caller-owned pinned |
| 32 | + slot without releasing it on error (the 5 error paths guard `if (owns_slot) release_slot`). |
| 33 | + - `wp-eval-cb.cpp` (~line 598): the MoE active-expert page-in is gated by **`WP_ENSURE_BATCH=1`** |
| 34 | + (default OFF; the existing prefetch+ensure path stays in the `else` for A/B + rollback). New |
| 35 | + branch calls `ensure_batch`, records the returned pins into `s_pinned_pages_prev_op`/`s_range_pins`. |
| 36 | +4. **Built clean** on mad-lab-main worktree `~/llama-wp/build-hip` (gfx1201, **AITER OFF**, ROCWMMA-FA |
| 37 | + ON). `ensure_batch` compiles with zero errors (only pre-existing `nodiscard hipError_t` warnings). |
| 38 | +5. **Measured on R9700, DeepSeek V4 Flash paged, P2P, `WP_ENSURE_BATCH=1`, ring depth 4:** |
| 39 | + **decode = 0.023 t/s (vs 0.04 baseline) — a 2× REGRESSION.** Output **fully coherent** |
| 40 | + (`"...the founding of the city of Rome in 753 BC..."`) → correctness OK, pin-lifecycle sound, |
| 41 | + **no expert corruption**. So the problem is PERF, not correctness. |
| 42 | + - (0.04 baseline IS the valid apples-to-apples number: `WP_ENSURE_BATCH=0` on THIS merged branch == |
| 43 | + the old wp-vnext path. Do not re-excuse it as "different branch.") |
| 44 | +6. **Tried depth 8/16 first (WP_IOURING_DEPTH=16 WP_PREFETCH_DEPTH=16) — LOAD HANGS.** Confirmed via |
| 45 | + `/proc/<load-thread>/wchan = io_cqring_wait` (waiting on an io_uring completion that never arrives). |
| 46 | + Fell back to the known-good depth-4 ring for the run above. |
| 47 | + |
| 48 | +### ROOT CAUSE — one bug explains BOTH the regression and the depth-8/16 hang: **shared io_uring ring CROSS-DRAIN** |
| 49 | +The sync path (`page_in_sync_`, sentinel `req_id=(uint64)-1`) / `ensure_batch`'s wait loop AND the |
| 50 | +`PrefetchScheduler` all drain the **same** io_uring ring. The io layer's `wait_any()` |
| 51 | +(`wp-file-io.cpp` IoUringAsyncFileIO, ~line 255-306) **unconditionally `io_uring_cqe_seen()`s** every |
| 52 | +CQE it returns. Both the sync/ensure_batch waiters and `PrefetchScheduler::tick()` **DROP any completion |
| 53 | +whose `req_id` isn't theirs** — but the CQE is already consumed. `PrefetchScheduler::process_io_` |
| 54 | +(`wp-prefetch.cpp:302`) frees a queue slot only on **its own** completions (via `req_to_slot_`); a |
| 55 | +completion cannibalized by the sync/ensure path **never frees that scheduler slot** → `free_slots_` |
| 56 | +leaks empty → **speculative cross-layer prefetch stalls out**. |
| 57 | +- **DECODE (WP_ENSURE_BATCH=1):** `ensure_batch`'s `while(reaped<n) wait_any(-1)` eats the prefetch |
| 58 | + scheduler's completions → prefetch pipeline dies → we lose the **compute↔I/O overlap** the old path |
| 59 | + relied on → every MoE op becomes a cold, un-overlapped read → **2× slower** (net LOSS; "more |
| 60 | + concurrency" but no overlap, and capped at depth 4 anyway). |
| 61 | +- **LOAD at depth 16:** heavier prefetch traffic → `tick()` (`wait_any(0)` drain) eats |
| 62 | + `page_in_sync_`'s completion → `page_in_sync_` blocks forever in `io_cqring_wait`. At depth 4 the |
| 63 | + collision is rare enough to mostly work. |
| 64 | + |
| 65 | +### THE FIX (do this next — it is the real unlock, NOT "just avoid it") |
| 66 | +**Completion demux in the io layer** (`wp-file-io.cpp`, IoUringAsyncFileIO): drain the ring **once** |
| 67 | +into a shared `std::unordered_map<uint64_t req_id, IoResult>` (`io_uring_cqe_seen` exactly once per |
| 68 | +CQE). Every waiter — `page_in_sync_`, `ensure_batch`, `PrefetchScheduler` — first checks that map for |
| 69 | +ITS `req_id`(s); only if absent does it pull a fresh CQE (moving any non-matching completions into the |
| 70 | +map, never discarding). No completion is ever lost to another consumer. This single change is expected |
| 71 | +to fix ALL THREE: (a) the 2× decode regression (prefetch pipeline survives `ensure_batch`), (b) the |
| 72 | +depth-8/16 load hang (`page_in_sync_` never loses its CQE), (c) `ensure_batch` finally gets **true |
| 73 | +QD=8** (raise `WP_IOURING_DEPTH`/`WP_PREFETCH_DEPTH` to ≥8 once the demux lands). |
| 74 | +- **Watch:** the load/decode paths are single-threaded per op (`PrefetchScheduler` + |
| 75 | + `page_in_sync_`/`ensure_batch` run on the same inference thread during a forward pass), so the map |
| 76 | + needs no lock — **verify** that assumption before relying on it. |
| 77 | +- Files: `wp-file-io.cpp` (the demux + a `wait_for(req_id)` that consults it), `wp-prefetch.cpp` |
| 78 | + (`tick()`/`process_io_` pull from the map), `wp-pager.cpp` (`page_in_sync_`, `ensure_batch` waits). |
| 79 | + |
| 80 | +### VALIDATION GATES after the demux (this is the proof the cross-drain was the cause) |
| 81 | +1. **Decode ≥ 0.04** with `WP_ENSURE_BATCH=1` at depth 4 (regression gone: prefetch pipeline restored). |
| 82 | +2. **Depth-8 load does NOT hang** (`WP_IOURING_DEPTH=8 WP_PREFETCH_DEPTH=8`). |
| 83 | +3. **Decode at QD=8** climbs toward the **~0.4 t/s** ceiling (the ~10× headroom the diagnosis predicts). |
| 84 | +4. Output stays coherent; `routing_ptrs_discarded=0`; 0 faults. |
| 85 | + |
| 86 | +### EXACT REBUILD + RUN RECIPE (mad-lab-main, `~/llama-wp` worktree) |
| 87 | +``` |
| 88 | +# BUILD (fresh worktree build; ~25 min at -j2, RAM-capped for the 15 GB host; CPU-only, safe): |
| 89 | +ssh mad-lab-main bash -s <<'E' |
| 90 | +cd ~/llama-wp |
| 91 | +ROCM_PATH=/opt/rocm LD_LIBRARY_PATH=/opt/rocm/lib HIP_PATH=/opt/rocm cmake -S . -B build-hip \ |
| 92 | + -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1201 -DCMAKE_BUILD_TYPE=Release -DGGML_HIP_ROCWMMA_FATTN=ON \ |
| 93 | + -DGGML_HIP_AITER=OFF -DCMAKE_C_COMPILER=/opt/rocm/llvm/bin/clang -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++ |
| 94 | +cmake --build build-hip -j2 --target llama-perplexity llama-server |
| 95 | +E |
| 96 | +# RUN a decode measurement (server on :8081, R9700): |
| 97 | +ssh mad-lab-main bash -s <<'E' |
| 98 | +systemd-run --user --unit=dsv4-decode --collect -p MemoryMax=9000M --working-directory=/home/kmbandy/llama-wp \ |
| 99 | + --setenv=WP_SIZE_CLASS_SLOTS=1 --setenv=WP_BATCH_EVAL_CB=1 --setenv=WP_PAGED_BATCH=1 \ |
| 100 | + --setenv=WP_DENSE_PREFETCH_N=8 --setenv=WP_ENSURE_BATCH=1 \ |
| 101 | + --setenv=LLAMA_WP_TRANSPORT=p2p --setenv=LLAMA_WP_TRANSPORT_FORCE=1 \ |
| 102 | + bash -c './build-hip/bin/llama-server -m /home/kmbandy/Downloads/DeepSeek-V4-Flash-UD-Q8_K_XL-00001-of-00005.gguf \ |
| 103 | + --no-mmap --weight-paging --weight-paging-slots 384 --weight-paging-prefetch \ |
| 104 | + -ngl 99 --device ROCm0 --host 127.0.0.1 --port 8081 --ctx-size 4096 --parallel 1 > /tmp/dsv4-decode.log 2>&1' |
| 105 | +# wait for /health==200 (paged load ~3-5 min), then: |
| 106 | +curl -s http://127.0.0.1:8081/completion -H 'Content-Type: application/json' \ |
| 107 | + -d '{"prompt":"The history of the Roman Empire begins with","n_predict":16,"temperature":0,"cache_prompt":false}' \ |
| 108 | + | python3 -c "import sys,json;t=json.load(sys.stdin)['timings'];print('t/s',t['predicted_per_second'])" |
| 109 | +E |
| 110 | +``` |
| 111 | +- **DO NOT** set `WP_IOURING_DEPTH`/`WP_PREFETCH_DEPTH` > 4 until the demux lands (hangs the load). |
| 112 | +- To get clean pager stats (`prefetch_hit_rate`, `sync_fallbacks`, `io_effective_gb_s`) the server MUST |
| 113 | + shut down GRACEFULLY — `systemctl stop` hard-kills before `log_stats_summary()` runs. Send SIGTERM |
| 114 | + and let it drain, or add a signal handler, if you need those numbers. |
| 115 | +- R9700 shares the box with the murmur **captain (:8090, ROCm0)** and the **6900XT LFM2.5 (:8092, |
| 116 | + ROCm1)**. When no murmur is active the captain holds ~0 VRAM so DeepSeek fits the 32 GB fine; if a |
| 117 | + murmur is running, the captain will contend for ROCm0 — check `rocm-smi --showmeminfo vram` first. |
| 118 | + |
| 119 | +### Related (separate KG handoff notes written this session) |
| 120 | +- **KG `3b83ca79`** — this ensure_batch result + cross-drain root cause + demux fix (full detail). |
| 121 | +- **KG `5e2b8089`** — the recurrent-state mover HIP-only stub bug + 6900xt LFM2.5 concurrent GPU fault |
| 122 | + (a DIFFERENT subsystem; another session is picking up murmur; not on the critical path here). |
7 | 123 |
|
8 | 124 | --- |
9 | 125 | --- |
|
0 commit comments