116 lines
5.0 KiB
Markdown
116 lines
5.0 KiB
Markdown
# Measured results
|
||||
|
|
|
|||
|
|
Spark-X2.5-1.7B unless stated. Xeon Platinum 8480C, 24 pinned physical cores,
|
|||
|
|
memory bound to the same NUMA node, W4A16 g128, `max_model_len` 2048.
|
|||
|
|
Each row is a median over at least three interleaved passes against a control
|
|||
|
|
arm running in the same grid.
|
|||
|
|
|
|||
|
|
## Memory
|
|||
|
|
|
|||
|
|
Every lever below was measured as its own A/B, with the alternative reachable by
|
|||
|
|
a flag so both arms run identical code.
|
|||
|
|
|
|||
|
|
| lever | before | after | saving | cost |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| KV budget 1 GiB → 192 MiB | 3264 MB | 2421 MB | **843 MB** | fewer cached tokens |
|
|||
|
|
| RoPE table → `max_model_len` | 3891 MB | 3273 MB | **618 MB** | none |
|
|||
|
|
| One weight layout, not two | 4591 MB | 3976 MB | **615 MB** | ~1.63× prefill |
|
|||
|
|
| Blocked repack (peak) | 4502 MB | 4013 MB | **489 MB** peak | none |
|
|||
|
|
|
|||
|
|
**These do not add.** The single-layout change removed `to_split_halves`, which
|
|||
|
|
was doing its own whole-matrix expansion, so part of the blocked-repack saving
|
|||
|
|
was already gone by the time it was measured. The report each was taken from
|
|||
|
|
states its baseline; quote them individually.
|
|||
|
|
|
|||
|
|
### Where the memory actually is
|
|||
|
|
|
|||
|
|
From `memory_ledger.py`, which counts unique tensor storages by `data_ptr` (so
|
|||
|
|
tied weights are counted once) and checks residency with `mincore(2)`:
|
|||
|
|
|
|||
|
|
| | MB | note |
|
|||
|
|
|---|---|---|
|
|||
|
|
| KV cache | 1024 | fully resident — 1024.1 of 1023.8 allocated |
|
|||
|
|
| int4 packed weights | 686 | |
|
|||
|
|
| RoPE cos/sin tables | 640 | **before the cap** — sized for 1 048 576 positions |
|
|||
|
|
| embedding / head | 408 | tied; includes a 128 MB row-major copy |
|
|||
|
|
| imports (`import vllm_omni`) | 501 | |
|
|||
|
|
|
|||
|
|
The 640 MB RoPE entry is the clearest single finding in the project. Spark
|
|||
|
|
advertises a 1 048 576-token context and the model passed
|
|||
|
|
`max_position_embeddings` straight to `get_rope`, so vLLM built
|
|||
|
|
`torch.arange(1048576)` cos/sin tables — two of them, because the sliding and
|
|||
|
|
full layers use different theta and partial-rotary factors. At `max_model_len`
|
|||
|
|
2048 they need 1.25 MB between them. It had been sitting inside an
|
|||
|
|
"unexplained ~1.6 GB" remainder.
|
|||
|
|
|
|||
|
|
### Peak vs settled
|
|||
|
|
|
|||
|
|
`release_after_load()` runs only once loading and warm-up return, so a device
|
|||
|
|
that cannot hold the *peak* never reaches the release. Sampling `RssAnon` on a
|
|||
|
|
timer through the load, rather than reading the endpoint:
|
|||
|
|
|
|||
|
|
| | peak | settled | transient |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| whole-matrix repack | 4502 MB | 3905 MB | 550 MB |
|
|||
|
|
| blocked repack | 4013 MB | 3892 MB | 105 MB |
|
|||
|
|
|
|||
|
|
`unpack_nibbles` expands 4-bit codes to int32 — eight times the packed form —
|
|||
|
|
because the kernel's packer wants int32. Whole-matrix that is 104 MiB for
|
|||
|
|
`gate_up` and **1024 MiB for the tied embedding**, against 24 MiB of packed
|
|||
|
|
weights for a whole layer. Chunking is also *faster* (79 ms vs 208 ms for the
|
|||
|
|
embedding) because the working set stays in cache.
|
|||
|
|
|
|||
|
|
## Speed
|
|||
|
|
|
|||
|
|
| path | decode | prefill (pp512) | spread |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| W4A8, AMX `int4_scaled_mm_cpu` | 74.4 tok/s | — | 1.1% |
|
|||
|
|
| W4A16, tinygemm | 71.6 tok/s | 2190 tok/s | 9.7% |
|
|||
|
|
| W4A16, one layout | 71.6 tok/s | 1344 tok/s | 18.3% |
|
|||
|
|
| W4A16, `prefill_dequant: false` | 73.0 tok/s | 453 tok/s | 0.3% |
|
|||
|
|
| `cpu_gemm_wna16` fallback | 32.3 tok/s | — | 1.5% |
|
|||
|
|
| llama.cpp Q4_K_M | 90.0 tok/s | 1121 tok/s | — |
|
|||
|
|
|
|||
|
|
`cpu_gemm_wna16` is the kernel vLLM selects when `use_w4a8` is off. It exists —
|
|||
|
|
`weight_path.py` used to call tinygemm "the only option" without AMX, which was
|
|||
|
|
false — but at 2.2× slower than tinygemm the routing was right anyway. Caveat:
|
|||
|
|
`_get_isa_hint` returns `"amx"` whenever AMX is present regardless of the W4A8
|
|||
|
|
flag, so this measured its *best* path on this host, not the `"vec"` path a
|
|||
|
|
genuinely AMX-less machine takes.
|
|||
|
|
|
|||
|
|
## Bandwidth headroom
|
|||
|
|
|
|||
|
|
A node-local streaming read on the same 24 cores sustains **222 GB/s**.
|
|||
|
|
|
|||
|
|
| | GB/s | of ceiling |
|
|||
|
|
|---|---|---|
|
|||
|
|
| W4A16 GEMM only | 132 | 60% |
|
|||
|
|
| W4A8 GEMM only | 153 | 69% |
|
|||
|
|
| W4A16 whole step | 73 | 33% |
|
|||
|
|
| llama.cpp whole step | 86 | 39% |
|
|||
|
|
|
|||
|
|
Neither engine is bandwidth-bound. An earlier draft asserted the GEMM ran at
|
|||
|
|
"85% of what this socket delivers" and concluded the remainder was framework
|
|||
|
|
overhead; at 60% of the measured ceiling, kernel and scheduling efficiency are
|
|||
|
|
still on the table. See [ROADMAP.md](../../ROADMAP.md) item I10.
|
|||
|
|
|
|||
|
|
## Fidelity
|
|||
|
|
|
|||
|
|
Against each build's own bf16 output over 12 greedy prompts:
|
|||
|
|
|
|||
|
|
| build | exact reproductions | bits/weight |
|
|||
|
|
|---|---|---|
|
|||
|
|
| llama.cpp Q4_K_M | 4/12 | 4.5 (groups of 32, mixed types) |
|
|||
|
|
| W4A16 | 2/12 | 4.25 |
|
|||
|
|
| W4A8 | 1/12 | 4.25 |
|
|||
|
|
|
|||
|
|
**Not a matched-budget comparison** — Q4_K_M stores more. And greedy divergence
|
|||
|
|
saturates: it discards everything after the first divergent token, so one
|
|||
|
|
unlucky token scores the same as a build that is wrong everywhere.
|
|||
|
|
`fidelity_kl.py` replaces it with top-1 agreement and KL over a corpus.
|
|||
|
|
|
|||
|
|
On the Android agent harness, every build navigates a multi-screen task
|
|||
|
|
correctly and only some notice they have arrived: W4A8 turned Wi-Fi on and then
|
|||
|
|
tapped the switch off again. **Navigation survives quantization; recognising
|
|||
|
|
completion does not.** That is one run per cell — enough to show a failure
|
|||
|
|
reproduces, not to rank success rates.
|