Skip to content
Closed
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
111 changes: 111 additions & 0 deletions .agents/benchmark-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -24800,3 +24800,114 @@ CUDA/ROCm/Vulkan is not covered. The counter sees every global `operator new`
and nothing below it, so a raw `std::malloc` in the kernel is outside it (M-E
above). No full-model render has been rerun, so nothing here says what such a
run would now do — only that #1252 is not what stopped the last one.

## 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 3.6x slower and 4x more variable, which is the wrong direction, and the last
column says why: an 18.55 GiB arena does not fit beside 62 GiB of dense weights
on a 119.63 GiB box, so those runs swapped and one exhausted swap entirely. Both
reps of each slot count reproduce each other, so this is the box and not a
fluke. 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.
Loading
Loading