Files
vllm-omni/benchmarks/edge_harness/android_agent
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
..

Android agent toolkit for Spark-X2.5

Lets a text-only model operate a phone: observe the accessibility tree, call one tool, execute it on the device, observe again.

ui_tree.py    uiautomator XML -> indexed element list -> back to a tap point
device.py     AdbDevice (a real phone) | SimulatedDevice (scripted, in-process)
executor.py   one tool call -> one device action, errors as sentences
tool_parse.py recover a call from whatever shape the model emitted
agent.py      the loop: budget, no-op detection, undo detection, fallbacks
run_episode.py CLI

Running it

Serve the model (Spark emits GLM-4.5 tool format, so the stock parsers apply):

vllm serve <model> --port 8077 --enable-auto-tool-choice \
    --tool-call-parser glm45 --reasoning-parser glm45

Then:

python run_episode.py --task "Turn on Wi-Fi." --device sim   # no phone needed
python run_episode.py --task "Turn on Wi-Fi." --device adb   # a real phone

pytest test_android_agent.py runs 21 tests with no phone and no server.

What the loop does that a plain ReAct loop does not

Each one is a response to a measured failure of this model, not generic scaffolding:

  • Retry without thinking on an empty reply. The model can reason past its token budget without closing <think>; vLLM's reasoning parser only emits content once the block closes, so the reply arrives empty with no tool call. Raising the cap buys a longer silence. A non-thinking retry costs ~0.6 s.
  • Report actions that changed nothing. "the screen did not change" is information the model cannot otherwise have.
  • Report and stop on self-undo. Tapping a switch that is already on undoes the task. Each tap really does change the screen, so a no-change check cannot see it; the loop watches for the same action returning the screen to its previous state. Deliberately narrow -- opening a submenu and pressing back revisits a screen and must not trip it.
  • Recover malformed tool calls. See below.
  • A step budget, so a confused episode ends instead of running forever.

The loop never decides a task is finished. It reports what happened and lets the model call task_complete; a harness that completes tasks on the model's behalf measures itself.

Measured: which build can actually finish a task

Simulated phone, one run per cell, max_steps 10, Xeon 8480C 24 threads.

build decode "Turn on Wi-Fi." "Set an alarm for 7:30 AM."
bf16 41.0 tok/s done in 4 steps wrong app (Settings, not Clock), toggles Airplane mode, stuck
W4A16 g128 80.0 tok/s done in 4 steps right app, full navigation (Clock -> Alarm -> Add -> Save), then no tool call
W4A8 g128 86.6 tok/s turns Wi-Fi on, then toggles it off again (2 runs, same failure) taps "Phone" twice, reaches Alarm, then loops

Two things fall out, and both matter more than the throughput numbers:

Navigation survives quantization; completion does not. Every build walks home -> Settings -> Network -> Wi-Fi correctly. Only some of them notice that the switch now reads checked=true and stop. W4A8 -- the fastest build, and the one the speed-first selector recommends -- turns Wi-Fi on and then taps it off again, ending with the task undone.

The fidelity number had a task-level meaning all along. W4A8 reproduced 1/12 greedy prompts against its own bf16 and W4A16 2/12 (analysis/spark_edge_support.md 2c). That looked like a small difference on a proxy metric. It is the difference between finishing and not.

So for agentic use the recommendation is W4A16, not W4A8: 8% slower, and it is the only 4-bit build here that completed anything. weight_path.py's priority="fidelity" selects it.

Honest limits

  • No phone was involved. This host has no adb, no emulator and no /dev/kvm. The simulator emits real uiautomator XML, so the parser, the index-to-coordinate mapping and the stopping rules are exercised on the path hardware drives -- but whether a real Settings app is laid out the way these fixtures assume is untested.
  • Two tasks, one run each. Enough to show a failure mode reproduces, not enough to rank builds by success rate.
  • Stock glm45 does not always parse Spark. On 4 of 5 screens it does. On the Wi-Fi toggle screen the model emitted [tap]\n{"tool": "tap", "parameters": {"index": 1} -- unterminated, and tool_calls came back empty. tool_parse.py recovers that and reports every recovery, because an agent that silently repairs its model's output hides regressions in that model. The earlier claim that the stock parsers work "as-is" is true most of the time, which is not the same thing.