|
1 | 1 | # Weight-paging decode-speed work — continuation (morning pickup) |
2 | 2 |
|
3 | 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`, FF-merged to the same tip |
5 | | -`78358b158`. All GPU validation is on mad-lab-main's **R9700 (ROCm0, gfx1201, 32 GB)**; the 6900XT |
| 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 | 6 | is ROCm1. Remote shell is **fish** → always `ssh mad-lab-main bash -s <<'EOF'`. |
7 | 7 |
|
| 8 | +--- |
| 9 | +--- |
| 10 | + |
| 11 | +# ★★★ 2026-07-07 FULL DAY — READ THIS FIRST ★★★ |
| 12 | +## WP_PAGED_BATCH shipped + DeepSeek V4 Flash (284B/162GB) RUNS on 32 GB. Next front: decode-speed I/O parallelism. |
| 13 | + |
| 14 | +### TL;DR of the whole day |
| 15 | +1. **Shipped the `WP_PAGED_BATCH` feature** (MoE-aware batched paging) — 9 commits, validated across |
| 16 | + **dense/MoE × resident/paged** (all 4 quadrants: correct PPL, zero faults). Fixes the batched-MoE |
| 17 | + near-null fault (H3 read-before-produce + H4 TLS take-steal) via routing-boundary breaks, and makes |
| 18 | + batching safe under **eviction** via a per-range pin lifecycle + reactive auto-break. |
| 19 | +2. **Fixed 3 DeepSeek-V4 × weight-paging load bugs** (fit-dry-run crash, tiny-weights-paged, |
| 20 | + view-only-leaf buffer_id). Root-caused the last two via **codex+Fable consults** converging on |
| 21 | + `ggml_backend_sched_split_graph` pass-4. |
| 22 | +3. **THE WIN: DeepSeek V4 Flash (284B total / 13B active, 162 GB Q8) runs paged on the 32 GB R9700.** |
| 23 | + Wikitext **PPL = 4.1524**, **0 GPU faults**, **62,233 evictions/run**, `routing_ptrs_discarded=0` |
| 24 | + (clean routing). A model **5× the size of VRAM**, computing correctly. |
| 25 | +4. **NEXT FRONT (start here after compact): decode-speed I/O parallelism.** DeepSeek decode is |
| 26 | + **~0.04 t/s**, gated by **~0.5 GB/s effective I/O = ~1/10th of the SN850's ~5-7 GB/s**. It is |
| 27 | + **QD≈1-serialized** in the expert page-in (`page_in_sync_` one-page-at-a-time), so P2P dma_buf, |
| 28 | + io_uring depth 16, and async ensure **did not help** (async made it worse). The fix is code, not |
| 29 | + flags: **issue a MoE op's N active-expert reads concurrently to fill the io_uring queue**, then wait. |
| 30 | + |
| 31 | +### Commit stack today (all on `feat/wp-vnext`, pushed; tip `d384d93f4`) |
| 32 | +``` |
| 33 | +d384d93f4 fix(wp): anchor paged weights to pool_buf so view-only leaves get a backend (DeepSeek fix #3) |
| 34 | +3faad72a0 fix(wp): keep tiny per-layer weights resident (don't page < 4KB) (DeepSeek fix #2) |
| 35 | +978a720bf fix(wp): disable weight paging in the memory-fit dry-run (DeepSeek fix #1) |
| 36 | +f25563762 feat(wp): reactive auto-break on pin-budget under WP_PAGED_BATCH (5b) |
| 37 | +2854c4f97 feat(wp): per-range pin lifecycle under WP_PAGED_BATCH (release after range sync) (5a) |
| 38 | +0c9a6e159 feat(wp): break batch range at routing boundaries under WP_PAGED_BATCH (H3/H4) |
| 39 | +e5cb0582e feat(wp): call mark_routing_boundaries before sched compute |
| 40 | +cb15e252a feat(wp): WP_PAGED_BATCH flag + batch_safe pinnability-governed under it |
| 41 | +f64ccfdc8 feat(wp): routing-boundary pre-pass (mark_routing_boundaries/is_routing_break) |
| 42 | +``` |
| 43 | +Design spec `docs/superpowers/specs/2026-07-07-moe-aware-batched-paging-design.md`; plan |
| 44 | +`docs/superpowers/plans/2026-07-07-moe-aware-batched-paging.md`. |
| 45 | + |
| 46 | +### The `WP_PAGED_BATCH` feature — architecture (5 units) |
| 47 | +Enable with `WP_PAGED_BATCH=1` (default OFF; requires `WP_BATCH_EVAL_CB=1` + `WP_SIZE_CLASS_SLOTS=1`). |
| 48 | +- **A. Routing-boundary pre-pass** — `WeightPager::mark_routing_boundaries(gf)` (wp-pager.cpp), called |
| 49 | + from `llama_context::graph_compute` before sched compute (llama-context.cpp ~2492). Marks every |
| 50 | + `MUL_MAT_ID` node + the view-root of its `src[2]` (ids-producer) into `routing_break_tensors_`. |
| 51 | + `is_routing_break(t)` is an O(1) lookup. Cached via a topology signature. |
| 52 | +- **B. Break at routing boundaries** — `eval_cb_op_return()` (wp-eval-cb.cpp ~249) returns `true` |
| 53 | + (end range) when `is_routing_break(t)`. This isolates each `MUL_MAT_ID`, so the router+ids-producer |
| 54 | + compute+sync first (ids valid = fixes **H3**), and no other op shares its range to `take()`-steal |
| 55 | + the routed-expert TLS (fixes **H4**; note `ggml_cuda_mul_mat_q` is called for regular mul_mat too, |
| 56 | + ggml-cuda.cu:2889). |
| 57 | +- **C. Per-range pin lifecycle (5a)** — under paged_batch, pins accumulate in `s_range_pins`, move to |
| 58 | + `s_range_pins_pending` when a range ends, and release at the **top of the next callback** (post-sync, |
| 59 | + guaranteed). Replaces the per-op MAD-231 unpin. `reset()` flushes at teardown. |
| 60 | +- **D. Reactive auto-break (5b)** — track `s_range_pinned_bytes` at pin sites; `eval_cb_op_return()` |
| 61 | + ends the range when it crosses **70% of `pool_arena_bytes()`**. Bounds a range's pinned working set |
| 62 | + so a dense stretch (no routing boundary) can't overflow pins under eviction (`alloc_slot -1`). This |
| 63 | + is what lets `batch_safe()` drop the `evictions==0` requirement — batching under real eviction. |
| 64 | +- **E. Flag** — `wp_paged_batch_enabled()` (wp-eval-cb.cpp); `batch_safe()` returns |
| 65 | + `pool_.size_class_slots_enabled()` when the flag is on (drops `evictions==0 && !has_experts`). |
| 66 | + |
| 67 | +**Validation matrix (all committed, all GPU-validated, PPL exact + 0 faults):** |
| 68 | +| | resident (evictions==0) | paged (evictions>0) | |
| 69 | +|---|---|---| |
| 70 | +| **dense (Qwen3.6-27B)** | PPL 5.4623 @ slots=345 | PPL 5.4623 @ slots=200, **2174 evict** | |
| 71 | +| **MoE (LFM2.5-8B-A1B)** | PPL 27.0938 @ slots=750 | PPL 27.0938 @ slots=300, **7784 evict** | |
| 72 | +| **MoE @ scale (DeepSeek V4)** | — | **PPL 4.1524 @ slots=384, 62233 evict** | |
| 73 | + |
| 74 | +Unit tests: `test_routing_boundary_prepass`, `test_wp_paged_batch_flag_default_off` in |
| 75 | +tests/test-weight-pager.cpp (builds/runs on mad-lab-2026 `build-army`, or mad-lab-main `build-hip`). |
| 76 | + |
| 77 | +### The 3 DeepSeek-V4 × weight-paging load fixes (root causes) |
| 78 | +1. **Fit-dry-run crash** (`978a720bf`, common/fit.cpp:~60): `common_get_device_memory_data`'s |
| 79 | + `no_alloc` dry-run cleared mmap/mlock/direct_io but NOT `weight_paging_enabled`, so paged tensors got |
| 80 | + null buffers and `ggml_gallocr` asserted `buffer_id>=0` during the dry-run reserve. **Fix:** |
| 81 | + `mparams_copy.weight_paging_enabled = false` in the dry-run. (Any paged model was un-loadable via the |
| 82 | + auto-fit path before this.) |
| 83 | +2. **Tiny weights paged** (`3faad72a0`, src/llama-model.cpp): DeepSeek V4's tiny per-layer weights |
| 84 | + (hyper-connection `hc_attn_scale` {3}, `hc_*_base` {hc_mix_dim}, sinks, expert-prob bias) were paged; |
| 85 | + a paged non-matmul leaf gets no backend → assert. **Fix:** `WP_MIN_PAGED_BYTES = 4096` floor |
| 86 | + (file-scope const) in both `is_paged_weight` and the `weight_page_infos` registration loop. Standard |
| 87 | + {n_embd} norms stay paged (27B/LFM unchanged). |
| 88 | +3. **View-only-leaf buffer_id** (`d384d93f4`, src/llama.cpp:287) — **the systemic one.** A paged weight |
| 89 | + consumed **only via a reshape view** (DeepSeek `wo_a = ggml_reshape_3d(...)->mul_mat`, deepseek4.cpp:1068) |
| 90 | + inherits `buffer_id -1` from `ggml_backend_sched_split_graph` **pass 4** (ggml-backend.cpp:1217-1241): |
| 91 | + the view's id comes from its `view_src` (paged leaf, `buffer==NULL` → -1), then the src-loop assigns |
| 92 | + the leaf = the view's still-`-1` id **before** the view's own fallback runs. Directly-consumed weights |
| 93 | + are rescued at line 1233 by their pass-3-assigned matmul; view-consumed ones aren't. **Fix (1 line):** |
| 94 | + set paged weights' `t->buffer = pool_buf` (instead of `nullptr`) at init, giving the scheduler a |
| 95 | + backend to name. Known-safe: the eval_cb **already** sets `src->buffer = pool_buf` on every patch |
| 96 | + (wp-eval-cb.cpp:1085/1094), so this just brings load-time state to the post-first-decode steady state; |
| 97 | + gallocr still skips the leaf (`is_allocated` checks `data!=NULL` first). **Eval-time correctness of |
| 98 | + reshaped/viewed paged weights is already handled** — the eval_cb resolves the page via `view_src` |
| 99 | + (wp-eval-cb.cpp:919-921) and patches the VIEW's data `= vram + view_offs` (1089-1097, the "B-P1" |
| 100 | + path). Root-caused by codex + Fable consults converging on the pass-4 ordering. |
| 101 | + |
| 102 | +### DeepSeek V4 Flash — run recipe + geometry |
| 103 | +- **Model:** `~/Downloads/DeepSeek-V4-Flash-UD-Q8_K_XL-0000{1..5}-of-00005.gguf` (162 GB; load via the |
| 104 | + `-00001-of-00005` file, llama.cpp auto-pulls the rest). Part 1 is a 5 MB **metadata-only shard** |
| 105 | + (that is normal, not truncated). Arch = `deepseek4` (upstream #24162, has the lightning-indexer / NSA |
| 106 | + sparse attention). Converter is absent from this tree — use a pre-quantized GGUF. |
| 107 | +- **Geometry:** 33,987 paged pages (after the tiny-weights fix; was 34,349), **max_page = 64 MiB**. |
| 108 | + slots=384 → 24 GB arena. Total VRAM ~28.6 GB (arena + resident embeds/output + KV/compute). Fits 32 GB. |
| 109 | +- **PPL command (works):** |
| 110 | + ``` |
| 111 | + ssh mad-lab-main bash -s <<'E' |
| 112 | + cd /home/kmbandy/GitHub/llama.cpp |
| 113 | + systemd-run --user --unit=dsv4 --collect -p MemoryMax=30000M --working-directory=/home/kmbandy/GitHub/llama.cpp \ |
| 114 | + --setenv=WP_SIZE_CLASS_SLOTS=1 --setenv=WP_BATCH_EVAL_CB=1 --setenv=WP_PAGED_BATCH=1 --setenv=WP_DENSE_PREFETCH_N=8 \ |
| 115 | + bash -c "./build-hip/bin/llama-perplexity -m ~/Downloads/DeepSeek-V4-Flash-UD-Q8_K_XL-00001-of-00005.gguf \ |
| 116 | + -f wikitext-2-raw/wiki.test.raw --chunks 2 -c 512 --no-mmap --weight-paging --weight-paging-slots 384 \ |
| 117 | + --weight-paging-prefetch -ngl 99 --device ROCm0 > /tmp/dsv4.log 2>&1" |
| 118 | + E |
| 119 | + ``` |
| 120 | + Expect PPL 4.1524, evictions ~62k, no fault. **~6.4 min / 2 chunks (I/O-bound).** |
| 121 | + |
| 122 | +### ★ NEXT FRONT — decode-speed I/O parallelism (start here) ★ |
| 123 | +**The measured problem:** DeepSeek decode ≈ **0.04 t/s**; per token ~11 GB streams from NVMe at |
| 124 | +**~0.5 GB/s effective ≈ 1/10th of the SN850's ~5-7 GB/s**. Not bandwidth-bound, not compute-bound, not |
| 125 | +sync-bound (batching is irrelevant at this residency). It is **QD≈1 serialized**. |
| 126 | + |
| 127 | +**What was tried and did NOT help (so don't rehash):** |
| 128 | +- P2P dma_buf transport (`LLAMA_WP_TRANSPORT=p2p LLAMA_WP_TRANSPORT_FORCE=1`) — confirmed active |
| 129 | + ("P2P enabled — pool dma_buf exported+mmap'd", `page_in_sync_ EXIT p2p`); **no change** (still 0.04). |
| 130 | +- `WP_PREFETCH_DEPTH=16 WP_IOURING_DEPTH=16` — **no change** (nothing to parallelize if reads are serial). |
| 131 | +- `WP_ASYNC_ENSURE=1` — **worse** (0.02 t/s); adds overhead on the same serial path (and its compose |
| 132 | + with the 5a/5b per-range pins is unvalidated — see Task 6). |
| 133 | + |
| 134 | +**Diagnosis:** the expert page-in goes through `page_in_sync_` **one page at a time, waiting on each**, |
| 135 | +so the deep queue is never filled. For a MoE op the N active experts are all known at once (post-routing) |
| 136 | +— they should be issued **concurrently** (fill io_uring SQ to depth N) then waited together, turning |
| 137 | +QD≈1 into QD≈N and approaching NVMe bandwidth (~10× headroom → ~0.4 t/s ceiling at 15% residency). |
| 138 | + |
| 139 | +**Where to work (code):** |
| 140 | +- eval_cb **Step 2 ensure loop** (wp-eval-cb.cpp ~1051): `for (j...) pager->ensure(page_idx)` — serial, |
| 141 | + each `ensure()` blocks. This is the serialization to break. |
| 142 | +- The **"Pass 1: fire async prefetch for every active expert"** comment (~wp-eval-cb.cpp:531 in the MoE |
| 143 | + routing branch) — intended concurrency; verify whether it actually issues parallel reads or is inert. |
| 144 | +- `WeightPager::ensure` / `page_in_sync_` (wp-pager.cpp) and the io_uring file-io layer (wp-file-io) — |
| 145 | + the sync path vs a batched-submit path. |
| 146 | +**Measurement to do first:** `iostat -x /dev/nvme0n1 3` during a decode (was cut off last run — redo), |
| 147 | +plus the pager's `io_effective_gb_s` counter, to confirm QD/throughput and quantify the gap before |
| 148 | +coding. Likely a consult candidate (subtle async/io_uring path). |
| 149 | + |
| 150 | +### Other open items (lower priority than the I/O front) |
| 151 | +- **Task 6 (async compose):** `WP_ASYNC_ENSURE=1` + `WP_PAGED_BATCH` per-range pins is **unvalidated** |
| 152 | + (5a/5b were validated async-OFF). The async path operates on `s_pinned_pages_prev_op` which paged_batch |
| 153 | + bypasses (pins go to `s_range_pins`), so it may already be inert-but-safe — needs a correctness check. |
| 154 | +- **Flip `WP_PAGED_BATCH` default-on** — only after Task 6 + a decision; it's self-gated (size-class + |
| 155 | + batch_eval_cb) so low-risk, but hold until the I/O work settles. |
| 156 | +- **gpt-oss-20b** produces NaN under paging (pre-existing, gpt-oss/MXFP4-specific, `discarded:6`) — NOT a |
| 157 | + blocker; DeepSeek/LFM route cleanly (`discarded:0`). |
| 158 | + |
| 159 | +### State at compact |
| 160 | +- All 9 commits pushed; `feat/wp-vnext` tip `d384d93f4` == origin. Working tree: only pre-existing WIP |
| 161 | + (`examples/pagedattn-*`, `src/llama-graph.cpp`, `examples/CMakeLists.txt`) — do NOT touch. |
| 162 | +- mad-lab-main: wp-vnext commits cherry-picked; `src/llama.cpp` + `src/llama-model.cpp` present as |
| 163 | + staged-matching-origin (applied via `git checkout origin/feat/wp-vnext -- <file>`); the throwaway |
| 164 | + `ggml-alloc.c` WP-DIAG diagnostic was **reverted** (clean). GPU **clear** (VRAM ~1.3 GB baseline). |
| 165 | +- Build targets on mad-lab-main: `cmake --build build-hip -j2 --target llama llama-perplexity llama-server` |
| 166 | + in the capped `systemd-run --user` unit (MemoryMax=13000M CPUQuota=600%). |
| 167 | + |
| 168 | +--- |
| 169 | +--- |
| 170 | + |
8 | 171 | ## TL;DR of the session |
9 | 172 |
|
10 | 173 | Took **paged-resident dense-27B decode from 7.0 → 21.0 t/s = native speed**, with **PPL exactly |
|
0 commit comments