edge: remove retracted and wrong numbers from weight_path and kv_budget

An independent review (gpt-6-astra via the Codex hook) found that two modules
still ship claims this study had already withdrawn or that the vLLM source
contradicts. Documentation in executable files is how a retracted number gets
quoted again, so these are corrected at the source:

kv_budget.py claimed right-sizing the cache saved 8329 -> 5254 MB. That was
read from VmRSS (which counts the mmap'd checkpoint and moved 1-2 GB between
identical runs) and the two arms were different model files. On RssAnon the
saving is 188 MB. The retraction is now stated in the docstring, together with
why the number is small -- the benchmark generated 8 tokens, so no cache was
filled -- and the 1323 MiB flat-vs-hybrid difference at 32k that makes this
worth re-measuring on a benchmark that does fill it.

weight_path.py:
- bits_per_weight 4.16 -> 4.25 in all three 4-bit paths. 4 bits plus a bf16
  scale and zero per 128-weight group is 4 + 32/128; 4.16 matched no
  arithmetic and made the llama.cpp fidelity comparison look budget-matched
  when it is not (Q4_K is 4.5 bits over groups of 32, Q4_K_M promotes some
  tensors further).
- fused_cpu_norms True -> False in all three. Fused CPU RMSNorm is 3.6-4.5x
  faster in isolation and slower in situ; VLLM_OMNI_CPU_FUSED_NORMS already
  defaults to "0" for that reason, so the metadata contradicted the default.
- "tinygemm is the only option without AMX" is false: vLLM 0.28 selects
  ops.cpu_gemm_wna16 whenever use_w4a8 is off (mixed_precision/cpu.py:202).
  Unmeasured here, so the selector is unchanged, but the claim is not.
- W4A8's symmetric grid is our exporter's "sym": true, not a kernel limit --
  _process_gptq_weights_w4a8 uses checkpoint zero points when
  config.zero_points is set (same file, :98). An asymmetric export is a config
  change and the cheapest untried fidelity experiment.
- kv_budget's flat-accounting observation is flagged for re-checking, since
  CpuPlatform.support_hybrid_kv_cache() returns True (platforms/cpu.py:545).

Both fields now carry docstrings saying what they mean, so they cannot drift
back silently. tests/edge: 18 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tzhouamandClaude Opus 5 committed 2026-09-13 00:28:59 +08:00
1 parent cab5ef7010
commit 27b9400750
2 files changed
+60 -18

No files matched your search

+23 -5
View File
@@ -4,9 +4,22 @@
``VLLM_CPU_KVCACHE_SPACE`` defaults to a server-shaped budget and is parsed
with ``int()``, so it cannot even express an edge-sized cache -- the smallest
non-zero value it accepts is 1 GiB. Measured on Spark-X2.5-1.7B (Xeon 8480C,
2048-token single stream), asking for what the model actually needs instead of
the 4 GiB default takes resident memory from **8329 MB to 5254 MB**.
non-zero value it accepts is 1 GiB.
**Retraction (2026-09-13).** This module used to claim that right-sizing the
budget took resident memory from 8329 MB to 5254 MB. That number was wrong
twice over: it was read from ``VmRSS``, which counts the mmap'd checkpoint's
pages and moved 1-2 GB between identical runs, and the two arms were not the
same model file. Re-measured on ``RssAnon``, which repeats to 0.2%, the saving
is **188 MB**.
That is small because of *when* it was measured, not because the budget does
not matter: the benchmark generated 8 tokens, so almost no cache was ever
touched. Allocation is what this module sizes, and the two coincide only at
long context -- see the arithmetic below, where the flat and hybrid budgets at
32 k differ by 1323 MiB. Sizing the cache is worth re-opening on a benchmark
that actually fills it (harness item H3), and worth nothing on one that does
not.
A hybrid-attention model does not *need* a full-length cache in every layer.
Spark-X2.5 is 3 sliding layers (window 512) to 1 full layer over 28 layers, so
@@ -15,8 +28,13 @@ at a 32 k context its real cache is 3.8x smaller than a flat budget:
flat 28 x 32768 = 917 504 token-layers
hybrid 21 x 512 + 7 x 32768 = 240 128 token-layers
But vLLM's CPU path does not currently take that discount: for a 4 GiB budget
it reported 71 527 tokens, i.e. 56 KiB/token, which is the flat figure. So
But vLLM's CPU path did not take that discount when this was measured: for a
4 GiB budget it reported 71 527 tokens, i.e. 56 KiB/token, which is the flat
figure. That observation now needs re-checking rather than trusting --
``CpuPlatform.support_hybrid_kv_cache()`` returns ``True`` in vLLM 0.28
(``vllm/platforms/cpu.py:545``), so either the model's attention groups are not
being reported as hybrid, or the budget is applied before that is known. Until
someone establishes which, the conservative number is the one to pass. So
``bytes_total`` is the **flat** number -- the one that is safe to pass today --
and the hybrid number is reported alongside it as ``bytes_total_hybrid``, for
a backend that accounts per layer type. A budget that is too small is not a
+37 -13
View File
@@ -7,11 +7,17 @@ model, and the reason is not preference -- it is that the fast kernel on each
one needs a different weight layout, and the layouts are mutually exclusive:
* an x86 core with AMX tiles has a 4-bit GEMM (`_C::int4_scaled_mm_cpu`) that
wants GPTQ-packed weights and quantizes activations to int8 -- the same
arithmetic llama.cpp's Q4_K_M does, and the fastest thing measured here;
* an x86 core without AMX has no such kernel, so 4-bit has to go through
PyTorch's tinygemm path, which wants its own packing and keeps activations
in bf16 -- slower, but more faithful, and the only option;
wants GPTQ-packed weights and quantizes activations to int8 -- int8
activations are what llama.cpp's Q4_K_M does too, though not at the same bit
budget (see below) -- and it is the fastest thing measured here;
* an x86 core without AMX falls back to PyTorch's tinygemm path, which wants
its own packing and keeps activations in bf16 -- slower, but more faithful.
This is *not* the only option there, as this module previously claimed:
vLLM 0.28 also has `ops.cpu_gemm_wna16`, a vector-ISA W4A16 kernel it
selects whenever `layer.use_w4a8` is off
(`vllm/model_executor/kernels/linear/mixed_precision/cpu.py:202`). It has
never been measured here, so the selector does not route to it yet -- but
"no AMX means tinygemm" is an untested assumption, not a fact;
* a phone NPU does not run vLLM at all. The deliverable there is an exported
graph, and its quantization is decided by the export toolchain, not by us.
@@ -40,8 +46,17 @@ embedding to its head, so the head has to be untied and emitted separately;
Fidelity is the reason both 4-bit rows stay: against each build's own bf16
output over 12 greedy prompts, llama.cpp's Q4_K_M reproduced 4/12 exactly,
w4a16 2/12 and w4a8 1/12. W4A8 pins the zero point at 8 (so the grid must be
symmetric) *and* quantizes activations to int8; W4A16 does neither.
w4a16 2/12 and w4a8 1/12.
Two caveats on that comparison, both of which were stated wrongly here before.
It is **not** at a matched bit budget: w4a16 and w4a8 store 4 bits plus two
bf16 per 128-weight group = **4.25 bits/weight**, while plain Q4_K is 4.5 bits
over groups of 32 and Q4_K_M promotes some tensors further. And the symmetric
grid is **our export's choice, not the kernel's limit**: vLLM's W4A8 path uses
the checkpoint's zero points whenever `config.zero_points` is set and
synthesises the constant 8 only otherwise (same file, `:98`). Our exporter
passes `"sym": true`. An asymmetric export is a config change, and it is the
cheapest untried fidelity experiment.
That difference has a task-level consequence, which is why ``priority`` is not
a matter of taste. Driving a phone through the Android agent harness
@@ -81,7 +96,16 @@ class WeightPath:
"""vLLM ``--quantization`` name, or None to keep the model dtype."""
group_size: int | None
bits_per_weight: float | None
"""Stored bits per weight, group overhead included: a 4-bit code plus a
bf16 scale and a bf16 zero per group is ``4 + 32 / group_size``, i.e.
**4.25** at g128. (This field read 4.16 until 2026-09-13, which matched no
arithmetic and made the llama.cpp fidelity comparison look budget-matched
when it is not.)"""
fused_cpu_norms: bool
"""vLLM's fused CPU RMSNorm. False everywhere on measurement: it is
3.6-4.5x faster in isolation and *slower* in situ, because inductor already
fuses the native form with the residual add. ``VLLM_OMNI_CPU_FUSED_NORMS``
defaults to "0" for the same reason."""
runs_in_vllm: bool
"""False means vLLM cannot execute here and an exported artifact is the
deliverable -- the phone case."""
@@ -166,8 +190,8 @@ def select_weight_path(
name="w4a16-tinygemm",
quantization="cpu_int4",
group_size=128,
bits_per_weight=4.16,
fused_cpu_norms=True,
bits_per_weight=4.25,
fused_cpu_norms=False,
runs_in_vllm=True,
verified=False,
rationale=(
@@ -185,8 +209,8 @@ def select_weight_path(
name="w4a8-amx",
quantization="gptq",
group_size=128,
bits_per_weight=4.16,
fused_cpu_norms=True,
bits_per_weight=4.25,
fused_cpu_norms=False,
runs_in_vllm=True,
verified=True,
rationale=(
@@ -207,8 +231,8 @@ def select_weight_path(
name="w4a16-tinygemm",
quantization="cpu_int4",
group_size=128,
bits_per_weight=4.16,
fused_cpu_norms=True,
bits_per_weight=4.25,
fused_cpu_norms=False,
runs_in_vllm=True,
verified=True,
rationale=(