Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .agents/benchmark-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -25101,3 +25101,132 @@ narrower f64 vector explains without anything further.

SGLang-Omni is still `gateable = no`. Every reference axis in `docs/BENCHMARKS.md`
stays `PENDING`. Everything above is an internal two-arm number on one named box.

## ENG-EXPERT-STREAM-DEVICE W0e — the CPU arm is measured and the CUDA arm loads without generating (2026-08-18/19, `dgx:gpu0`, source `95883dcae`, #1124)

One `rc hold` (`edb4b3d0-5d6e-422f-ade6-bff5339e3396`, 22:13:12Z to 00:58Z,
released by interrupting its client). GB10, sm_121a, driver 580.173.02, CUDA
13.0.88, 20 cores, 122,502 MiB RAM, 30,625 MiB swap. Checkpoint
`Qwen3.8-2.4T-A95B-UD-Q1_0` (369.97 GiB, 10 shards) from LOCAL NVMe at
`/home/mudler/ckpt/qwen3.8-q1_0`. Prompt fixed as token ids
`760,6511,314,9338,369`, greedy, 32 tokens, `--max-num-seqs 1`,
`VT_GGUF_PREFAULT=0 VT_MOE_EXPERT_STREAM=1 VT_MOE_EXPERT_STREAM_STATS_EVERY=1`.
Page cache dropped before every arm. Same binary both arms, built with CUTLASS
4.5.0 and Triton AOT: the configure log reports `fp4-mma`, `cutlass-nvfp4`,
`cutlass-fp8`, `marlin-nvfp4` and `fa2` all `ENABLED for [121a]`, plus
`FlashAttention-2 prefill/decode: ENABLED` and `Triton AOT W2: embedded trees
[sm_80;sm_86;sm_89;sm_90a;sm_100a;sm_121a]`.

Harness: `benchmarks/expert_stream_device_w0e.cpp`, a thin C-ABI client. It
exists because one generation has to yield three things no shipped command gives
together: the generated token ids (`vllm_complete_tokens`), a per-step arrival
timestamp (a logits processor used as a pure observer, which edits nothing so
the argmax is unchanged), and the expert-stream counters at both ends of the run.

### CPU arm, four runs, two slot counts

| slots | arena | load | TTFT | steady s/token, steps 4-32 | generate | peak RSS | min avail | peak swap |
|---|---|---|---|---|---|---|---|---|
| 4000 | 9.28 GiB | 271.1 s | 85.90 s | median 11.22, min 9.62, max 12.51 | 502.3 s | 86.5 GiB | 16,840 MiB | not sampled |
| 4000 | 9.28 GiB | 255.7 s | 79.09 s | median **11.05**, min 9.43, max 13.25 | 460.7 s | 86.5 GiB | 16,347 MiB | 6,883 MiB |
| 8000 | 18.55 GiB | 261.6 s | 94.25 s | median 45.40, min 20.73, max 82.68 | 1643.2 s | 86.6 GiB | 6,985 MiB | not sampled |
| 8000 | 18.55 GiB | 266.5 s | 132.74 s | median 39.98, min 16.06, max 94.30 | 1581.6 s | 86.6 GiB | 6,941 MiB | 30,625 MiB (all) |

The 8000-slot rows are a MEMORY result, not a cache result. A bigger cache came
out slower on every pairing of the medians: 3.62x same-rep on the pair carrying
the publishable figure (39.98 against 11.05), 4.05x pairing rep 1 with rep 1
(45.40 against 11.22), and 3.56x to 4.11x over all four pairings. It also came
out much less steady, stated as the max/min ratio of the steady window: 3.99x in
rep 1 (20.73 to 82.68 s) and 5.87x in rep 2 (16.06 to 94.30 s), against 1.30x
and 1.40x at 4000 slots.

The cause is NOT that the arena does not fit. 18.55 GiB of arena beside 62 GiB
of dense weights is 80.55 GiB on a 119.63 GiB box, and the columns agree that
nothing overflowed: peak RSS moves 86.5 -> 86.6 GiB for a 9.27 GiB arena delta.
The columns that DO move are `min avail`, 16,347 -> 6,941 MiB, a 9,406 MiB fall
that is about the arena delta, and peak swap, 6,883 -> 30,625 MiB, all of it.
The best-supported reading of that pattern is page-cache displacement: the
borrowed 370 GiB expert mapping is served out of whatever memory is free, the
arena takes that memory, and the reclaim pressure it creates pushes anonymous
pages to swap. That is a reading of these columns and not a proven mechanism.
This run sampled no page-cache size and no major-fault counter, so it cannot
separate displacement from plain reclaim pressure, and a run that wants to
settle it has to sample both.

The operational conclusion does not depend on which of the two it is. Both reps
of each slot count reproduce each other, so this is the box and not a fluke,
more slots is not a free knob, and the publishable figure is the 4000-slot one.

Per-token deltas, 4000-slot rep 2, seconds: 79.09, 51.14, 12.52, then 11.69,
11.51, 11.35, 11.59, 13.25, 11.58, 9.48, 12.16, 11.22, 11.42, 11.51, 11.05,
10.70, 10.69, 11.93, 11.12, 11.26, 10.99, 9.85, 10.39, 9.58, 11.35, 10.17,
11.03, 10.31, 9.43, 10.25, 10.38, 10.64. Step 1 is prefill; steps 2 and 3 are
still filling a cold cache.

G0-LIVE, gated on the DIFFERENCE and never the total. At 4000 slots the
after-prefill snapshot reads `steps=1 hits=0 misses=10074 evictions=0 fills=4000
bytes=9961472000 exhausted=6074` and the final one `steps=32 hits=37096
misses=58538 evictions=48464 fills=52464 bytes=130654666752 exhausted=6074`, so
the decode-phase delta is 0 over 31 decode steps. At 8000 slots the same pair
reads 2074 and 2074, delta 0. Counters are byte-identical between the reps of
each slot count.

All four runs produced the same 32 ids:
`11751,13,11751,369,264,3177,7172,303,279,17631,919,314,9338,11,383,279,181474,10629,13,1049,369,279,7526,3177,303,9338,321,369,3750,364,1141,25438`,
which detokenize to " Paris. Paris is a city located in the northern part of
France, on the Seine River. It is the largest city in France and is known for
its iconic", `finish_reason=length`.

### CUDA arm, seven attempts, zero decode steps

| slots | arena | prompt tokens | load | RSS after load | steps | peak system used | peak swap |
|---|---|---|---|---|---|---|---|
| 8000 | 18.55 GiB | 5 | 267.2 s | 61.20 GiB | 0 | 100,215 MiB | not sampled |
| 4000 | 9.28 GiB | 5 | 271.6 s | 61.20 GiB | 0 | 94,737 MiB | not sampled |
| 4000 | 9.28 GiB | 5 | 255.3 s | 61.20 GiB | 0 | 120,351 MiB | 30,569 MiB |
| 3500 | 8.12 GiB | 1 | 262.8 s | 61.20 GiB | 0 | 120,296 MiB | 28,138 MiB |
| 3500 | 8.12 GiB | 1 | 265.3 s | 61.20 GiB | 0 | 120,347 MiB | 30,461 MiB |
| 3500 | 8.12 GiB | 1 | 272.3 s | 61.20 GiB | 0 | 120,306 MiB | 30,172 MiB |
| 64 | 0.15 GiB | 1 | 268.9 s | 61.20 GiB | 0 | 118,257 MiB | 30,211 MiB |

The LOAD is new and it works: this checkpoint used to refuse on `--device cuda`
(#1123) and now loads, and the `[expert-stream] ON` banner prints on the device
arm, which is the first production evidence that W0b's CUDA leg is reached and
answers true on real hardware.

The last row is the diagnosis. A 0.15 GiB arena dies where an 18.55 GiB one
does, 124x smaller and no further along, so the slot arena is not the cost. The
in-place tower fallback is not the cost either: a 1-token prompt has a protected
set of 93 x 3 x 10 = 2,790 slices, fits 3500 slots with no fallback taken, and
behaves exactly like the 5-token prompt whose 13,950-slice set fits no arena
here.

Growth is anonymous, not file-backed, so the GPU is not pinning the mapping's
pages through its address translation. That was the first hypothesis and it is
refuted. Sampling the container process every 5 s: `RssAnon` 8.1, 13.9, 25.9,
38.3, 49.4, 61.4 GB through the load while `RssFile` stays at 0.1-0.2 GB, then
`VmSwap` 0, 5.4, 11.4, 16.5, 27.1, 31.0 GB inside the forward. Host `RssAnon`
plus `VmSwap` reaches about 65 GB while the system reports ~119 GiB used, and
the ~42 GiB difference is device memory this unified part does not charge to the
process RSS. The non-expert weights are therefore resident twice, once as the
host-side `OwnedTensor` and once as the `ResidentWeight` device staging copy.
About 50 GiB of the 61.20 is the bf16 expansion the GDN V-head reorder forces on
`attn_qkv` and `ssm_out`, already measured in
[expert-streaming.md](specs/expert-streaming.md). Filed as
[#1299](https://github.com/mudler/vllm.cpp/issues/1299).

Each CUDA run was stopped by a guard that kills the container when MemAvailable
plus SwapFree falls under a floor, because an out-of-memory kill on GB10 takes
the machine down rather than the process. The guard is not what makes them fail:
the last rows were climbing at roughly 10 GB of swap per minute with under
600 MiB of swap left. The guard's own first version counted MemAvailable ALONE
and killed two runs while 29 GiB of swap sat untouched, which is worth recording
because it reported a box about to die when the kernel had a whole tier left.

Contention: the lease excluded every other `rc` job. Two things it did not
exclude are recorded rather than assumed away. An orphaned `VLLM::EngineCore`
from an earlier session held 3.32 GiB of host RSS throughout; it is not this
row's process and it was left alone. And the FIRST arm started seconds after the
previous holder's four-hour render released the device, with the one-minute load
average still at 17.5, which is one reason the first 8000-slot run is the
noisiest of the four. Every later arm started from a quiet box.
1 change: 1 addition & 0 deletions .agents/issue-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,4 @@ rather than merged. `scripts/check-agent-record.py` gates both.
| [#1375](https://github.com/mudler/vllm.cpp/issues/1375) | `MODEL-DIFFUSION-LTX25` | First end-to-end per-forward cost for the FULL 21.004 B LTX-2.5 DiT on GB10, measured on run `20260819T150230Z` with binary `0a43a750` built from [`7b9e207b1`](https://github.com/mudler/vllm.cpp/commit/7b9e207b1) (#1252). At 1024x576/25f (2304 latent tokens) the governor resolved **7 forward starts from the GPU busy/idle edge counter** and measured `per_forward ~162.0 s` with `first_dit = 481.5 s`, so the recipe's fixed 60 forwards (30 steps x 2 CFG legs, `ltx2_pipeline.cpp:521-529`) project **10 803 s against the rung's 7 153 s budget** and the rung was refused rather than run to the wall. The same lease then COMPLETED 768x448/25f (1344 tokens) in 2990 s, so the ceiling is geometry against lease length, not a defect. TWO instrument facts belong with the number, because both have already caused a wrong reading: `gpu_edges=0` means the GPU never went idle long enough to sample an edge (SATURATED), not that no work ran — this rung sampled 85% of 3191 samples above 50% utilisation; and `eu-stack` resolves no frames in the rc worker container, so phase attribution came from the cpu%/rss signature rather than from symbols. Owned by the LTX-2.5 row; spec [`ltx-2-5.md`](specs/ltx-2-5.md) | measurement |
| [#1387](https://github.com/mudler/vllm.cpp/issues/1387) | `ENG-EXPERT-STREAM-DEVICE` | `docs/FEATURES.md`'s routed-expert-streaming row still read "CPU keep-quant towers only" after W0c made the seam take the slot arm on `is_cpu()` OR `host_memory_is_device_addressable()` and W0d made the load-time fit refusal drop those towers from its bound. `AGENTS.md` routes a feature-surface change to that page, and the change that moved the surface did not write it. `scripts/check-doc-checkpoint.py` said so, on commit `939755f99` of `row/ENG-EXPERT-STREAM-DEVICE-W0`: a measurement was appended to `.agents/benchmark-record.md` with no `docs/FEATURES.md` edit beside it. The PAGE is FIXED IN FLOW while repairing the fresh review of [#1377](https://github.com/mudler/vllm.cpp/pull/1377): the row now names both the device arm ([#1124](https://github.com/mudler/vllm.cpp/issues/1124)) and the residency condition ([#1378](https://github.com/mudler/vllm.cpp/issues/1378)), and the observability detail it displaced to stay inside the 220-character cell budget is stated at `docs/USAGE.md:4598-4620`. The GATE is NOT fixed and needs a decision: the checker walks a range one COMMIT at a time, so once a commit is published on a branch that may not be force-pushed, no later commit can make it green, and `scripts/agent-preflight.sh` keeps reporting `doc-checkpoint range` red on this branch until it merges. The squashed commit that lands on `main` carries both paths and passes. Changing the walk is checker semantics and needs its own row, spec and red-first evidence per `AGENTS.md` "Changing the rules or a checker", so it is not folded in here. Spec [`expert-stream-device-slots.md`](specs/expert-stream-device-slots.md) | bug |
| [#1386](https://github.com/mudler/vllm.cpp/issues/1386) | — | `tools/bench/gpu_clock_state.py`'s `QUERY_FIELDS` collects nine fields and **none of them is thermal and none is electrical**, so the driver's own `SwThermalSlowdown` label can never be checked against a die reading on any window this helper has ever recorded. The measured consequence is that the nine windows of 2026-08-19 cannot distinguish a load transition from a thermal excursion. The concrete evidence is `clock-c1-r1.samples.json` in `/mnt/nas_share/rc/q38bf16/out/bench-20260819T035148Z/`: ours c1 r1 dips five times on the same period at the same `utilization.gpu = 96` — 48.83 s / 2177 MHz, 80.60 s / 2320 MHz, 109.28 s / 2210 MHz, 137.98 s / 2359 MHz, 166.07 s / 2268 MHz — and **two of those five carry `0x0000000000000000`**, no throttle bit at all (2210 and 2359), while three carry `0x20`. The 2210 MHz unlabelled dip is deeper than two of the three labelled ones, so the driver labels comparable excursions inconsistently and the bit alone cannot decide it. What would settle it: add `temperature.gpu` and `power.draw` to `QUERY_FIELDS`. That changes the clock-record schema, so it owes its own row and spec. Split out of [#1354](https://github.com/mudler/vllm.cpp/issues/1354) and owed under `## Owed` in [lease-clock-pinning.md](specs/lease-clock-pinning.md) | gap |
| [#1299](https://github.com/mudler/vllm.cpp/issues/1299) | `ENG-EXPERT-STREAM-DEVICE` | `Qwen3.8-2.4T-A95B UD-Q1_0` (369.97 GiB) now LOADS on `--device cuda` on a 119.631 GiB GB10 — `ENG-EXPERT-STREAM-DEVICE` W0 works, the slot store builds and the #1123 refusal correctly does not fire — and then exhausts the machine inside the FIRST forward, producing zero decode steps. Measured on `dgx:gpu0` inside one `rc hold` at source `95883dcae`, production stack (CUTLASS 4.5.0 found, `cutlass-fp8`/`cutlass-nvfp4`/`fa2` ENABLED for `[121a]`, Triton AOT `sm_121a`). Seven attempts, all identical: load 255-272 s, RSS after load 61.20 GiB, 0 decode steps, peak system `used` 118-120 GiB with 28-31 GiB of swap consumed. The slot arena is NOT the cost, and that is measured rather than argued: a **64-slot 0.15 GiB** arena dies exactly where an 8000-slot 18.55 GiB one does, and a 1-token prompt (protected set 2,790 slices, which fits 3500 slots with no in-place tower fallback at all) behaves exactly like a 5-token one. The growth is ANONYMOUS, not file-backed, so the GPU is not pinning the mapping's pages: `RssAnon` climbs 8.1 -> 61.4 GB through the load while `RssFile` stays at 0.1-0.2 GB, then `VmSwap` climbs 0 -> 31.0 GB inside the forward. Host `RssAnon` + `VmSwap` reaches ~65 GB while system `used` sits at ~119 GB, and the ~42 GiB difference is device memory the process RSS does not account for on this unified part — so the non-expert weights are resident TWICE, once as the host-side `OwnedTensor` and once as the `ResidentWeight` device staging copy. `.agents/specs/expert-streaming.md` measured the host half already: the GDN V-head reorder makes `attn_qkv` and `ssm_out` `kTransformedWeight`, which expands them from ~5.5 bits to bf16, about 50 GiB of the 61.20. The CPU arm pays it once and serves (32 tokens, steady 11.05 s/token at 4000 slots, decode-phase `exhausted` delta 0); the CUDA arm pays it twice and cannot. NOT #1123, whose refusal fired at load and no longer applies, and NOT a defect in W0b/W0c/W0d, which do what their spec says. This is the device-side half of the sentence `expert-streaming.md` already carries: "Whoever takes this needs BOTH: the streaming lane for the ~330 GiB of experts, and a transformed-weight path that does not expand." Owed under `## Owed` in [`expert-stream-device-slots.md`](specs/expert-stream-device-slots.md) | bug |
Loading
Loading