Files
vllm-omni/ROADMAP.md
T
tzhouamandClaude Opus 5 b36a3431bd Package this fork as vllm-embedding: README, docs, roadmap, harness
Turns a branch of accumulated edge/CPU work into something someone else can
read, reproduce and continue.

README.md states what the fork is for (multimodal models on edge CPUs rather
than servers), the headline memory and speed results with their costs, and why
the numbers are worth reading -- five retracted conclusions, all measurement
errors rather than coding errors. Upstream's README is preserved at
docs/README.vllm-omni.md and NOTICE records the Apache-2.0 derivation and the
files this fork changes.

docs/edge/:
  methodology.md  the measurement discipline, which is the most transferable
                  thing here: what each of the five retractions was, and the
                  rule it left behind. Includes the two traps specific to this
                  stack -- vLLM's AOT compile cache keying on config so two
                  arms sharing an artifact load the wrong graph, and allocated
                  not being resident until mincore(2) says so.
  results.md      every measured figure with its spread and baseline, and an
                  explicit note that the four memory levers overlap and must
                  not be added.
  cpu-4bit.md     the two 4-bit paths, the packed layout as measured rather
                  than assumed, why the path choice is a config field, and the
                  GPTQ output-head trap that invalidated a whole round of W4A8
                  numbers.
  harness.md      how to run a comparison, and the two arguments that matter
                  more than they look (--context-len, --prompt-len), each of
                  which has produced a wrong conclusion by being left at its
                  default.

ROADMAP.md orders by whether the verification method is correct rather than by
expected size, lists what is done with results, what is next with expected
gains and how each would be verified, and -- at equal length -- what was
considered and rejected, including the CPU megakernel with the measurements
that rule it out for now.

benchmarks/edge_harness/ brings the tooling into the repo so the numbers are
reproducible: the grid runner, the single-run benchmarks, the memory ledger,
the fidelity metric, the Android agent suite, the quantizers, and the tests
that pin the harness's judgement. 82 tests pass from the new location.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1ax5wYLA6XbqEL1i8L9gZ
2026-09-14 13:25:32 +08:00

6.1 KiB
Raw Blame History

Roadmap

Ordered by whether the verification method is correct, not by expected size. That ordering is deliberate: this project retracted five conclusions, and in every case the fix was to the measurement rather than the code.

Each item states the measurement it starts from, what it should gain, and how that gain would be verified. An item without a verification method is not on this list.

Done

item result
H1 Task oracles judged by device state, with variants alarm oracle needed a saved alarm, not a typed 7; a Wi-Fi round trip now fails the already-satisfied task
H2 Harness semantics arm order really rotates; best respects metric direction; provenance stamped; failures recorded
H3 The three missing measurements ready_s, peak RssAnon, memory at a filled context — all previously unmeasurable
H4 Contention guard aggregate stolen capacity + straggler rule, on the target cores rather than host load average
H5 Regression gates bands from observed spread; throughput explicitly ungateable at ~30% swing
H6 Fidelity metric that ranks top-1 agreement and KL, replacing a saturating 12-prompt greedy check
H8 Retracted numbers purged from source two modules were still shipping withdrawn figures as documentation
I0 Blocked weight repack 489 MB of load-time peak, no cost
I1 One packed weight layout 615 MB resident, at ~1.63× prefill
I2 Measure cpu_gemm_wna16 2.2× slower than tinygemm; routing was right, now for a measured reason
I7 Memory ledger, by attribution found a 618 MB RoPE table and an 843 MB KV cache, both inside an "unexplained 1.6 GB"

Next

I6 — import cost. import vllm_omni is ~30 s against import vllm's 5.2 s, with transformers' auto image-processing (4.6 s) and the openai client (1.3 s) identifiable inside it, for a text model that needs neither. Every stage subprocess pays it again. Expect: an upper bound of ~6 s per process; deferred imports often move cost rather than remove it. Verify: ready_s, which now exists — the old timer started after imports, so the benchmark meant to prove this could not see it.

Drop the row-major embedding copy. embed_tokens.weight_packed is 128 MB, kept solely because the tinygemm layout was believed un-addressable for a single-row lookup. That premise is now known false. Verify: RssAnon plus an embedding-output SNR assertion.

I10 first step — why is the GEMM at 60%? The decode GEMM sustains 132 GB/s against a measured 222 GB/s ceiling. Part is arithmetic (tinygemm dequantizes per activation row) and part may be that each of 113 calls streams only a few MB, too little for the prefetcher to reach steady state. Attribute before optimising. Expect: unknown — a diagnosis, whose output is whether the 2.76 ms implied by the gap is reachable at all.

I3 — asymmetric W4A8. The symmetric grid is our exporter's choice, not the kernel's limit; models/spark2_5-gptq-asym-g128 already exists. The cheapest untried fidelity experiment, and it tests the stated cause of W4A8's error. Verify: fidelity_kl.py and the scored agent suite.

I5 — attention wrapper consolidation. Four ops per layer, 1.31 ms/step, of which 0.83 ms is wrapper self time; at batch 1 the cache-update loop runs two iterations while the wrapper rebuilds context and allocates a dummy tensor for compile ordering. Expect: 0.3–0.5 ms, revised down from an earlier 0.6–0.9 ms that assumed a workload never profiled. Verify: per-op profile and unprofiled step latency — one outer call that still launches two threaded kernels removes neither thread launch.

I4 — a better quantizer. Ours reproduces 2/12 greedy prompts against its own bf16; Q4_K_M manages 4/12 — but at 4.5 bits against our 4.25, so close the budget gap first (g64, or fp16 scale+zero, both config changes) before implementing error-compensated rounding. Timebox it; a null result is worth recording.

Considered and rejected

A CPU megakernel. On GPU this fuses a forward pass into one launch to remove launch overhead and the cross-kernel prefetch bubble. On CPU the analogues are OpenMP fork/join and ATen dispatch — and llama.cpp already is one: ggml_threadpool starts threads once and they walk the whole graph, synchronising at ggml_barrier through an atomic spin, where vLLM enters a separate omp parallel region per kernel.

Measured before deciding: a parallel region costs ~1.2 µs at 24 threads, and spin-waiting does not help (OMP_WAIT_POLICY=ACTIVE moved it 1.23 → 1.18 µs), so it is barrier cost rather than thread wake-up. At a few hundred regions per decode step that is 0.3–0.4 ms, 2–3% — the same money as I5, far more cheaply. The larger prize is the streaming bubble, but capturing it means replacing the execution model inside vLLM's CPU backend, which reimplements ggml. Not next.

More kernel fusion. Four attempts, four losses: vLLM's fused CPU RMSNorm is 3.6–4.5× faster in isolation and slower in situ (inductor already fuses the native form with the residual add); fused gelu_and_mul 2.7× slower at Spark's shape; cpp.min_chunk_size −6% decode; an inline gate reduction −44%.

MiniCPM-o CPU overlay. The 8 B thinker at bf16 is ~19 GB, so it buys no edge deployability. The argument for it — that three-stage parallel init should beat the two-stage 2.1× — is also not how overlap works: the bound is ΣT/max(T).

Speculative decoding for tg128. It would inflate a fixed-prompt ignore_eos benchmark without representing anything. Measure it on the agent suite or not at all.

Thread count and warm-up batch size as memory levers. 64 MB and 2 MB.

Harness work still open

  • A real device in the agent loop. The toolkit has 38 tests and has never touched a phone — this host has no adb, no emulator, no /dev/kvm. The real-device path and per-task resets exist; what is untested is whether a real Settings app matches the fixtures.
  • Success rates, not single runs. The build comparison that produced "navigation survives quantization, completion does not" is one run per cell. Variants now exist so repeats are trials; they need running.