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
280 lines
11 KiB
Python
280 lines
11 KiB
Python
"""A scored task suite, judged by what the phone shows rather than by what the
|
|
agent said -- or by what the simulator happens to remember.
|
|
|
|
Three things were wrong with the first version of this file, and all three
|
|
made it score more generously than it claimed to:
|
|
|
|
* **``add_alarm`` passed on any typed string containing "7".** Typing ``7``
|
|
and stopping scored a success on a task whose prompt is "Set an alarm for
|
|
7:30 AM": no minute, no meridiem, and no requirement that the alarm was ever
|
|
saved. It now requires a *committed* alarm reading 7:30 AM, which the clock
|
|
screen lists, so the check is one a real phone can also answer.
|
|
* **``wifi_on_already`` scored final state only.** Its note said "any tap on
|
|
the switch fails the task"; the code could not see a tap, so turning Wi-Fi
|
|
off and back on passed. Switch flips are now recorded and the task fails on
|
|
any of them -- which is the exact failure W4A8 exhibited (it turned Wi-Fi on
|
|
and then tapped the switch off again).
|
|
* **Predicates read ``SimulatedDevice`` attributes**, so every scored run was a
|
|
simulator run by construction. They read :class:`PhoneState` now, which comes
|
|
from the UI dump for both backends.
|
|
|
|
Repeats are only trials if something differs between them, so each task
|
|
carries **variants**: the same goal against a renamed control, a reordered
|
|
screen, or a different starting state. Five runs of one fixture at temperature
|
|
0 measure repeatability; five variants measure whether the agent can do the
|
|
task.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Callable
|
|
|
|
from device import SimElement, SimulatedDevice, demo_phone
|
|
from phone_state import PhoneState, read_state
|
|
|
|
Predicate = Callable[[PhoneState], bool]
|
|
|
|
CLOCK_PACKAGE = "com.google.android.deskclock"
|
|
"""Clock app to reset for the alarm tasks. AOSP builds and several OEMs ship a
|
|
different package (``com.android.deskclock``, ``com.sec.android.app.clockpackage``
|
|
on Samsung); override before a real-device run, because clearing the wrong
|
|
package leaves a stale alarm and the next run scores against it."""
|
|
|
|
|
|
# --------------------------------------------------------------- variations
|
|
|
|
def rename(screen: str, old: str, new: str) -> Callable[[SimulatedDevice], None]:
|
|
"""Same control, different word. Android OEMs really do ship "WLAN"."""
|
|
def apply(d: SimulatedDevice) -> None:
|
|
for el in d.screens[screen].elements:
|
|
if el.label == old:
|
|
el.label = new
|
|
return apply
|
|
|
|
|
|
def reorder(screen: str, seed: int) -> Callable[[SimulatedDevice], None]:
|
|
"""Shuffle the tappable rows. Index-based tapping must not be luck."""
|
|
def apply(d: SimulatedDevice) -> None:
|
|
els = d.screens[screen].elements
|
|
head = [e for e in els if not e.clickable]
|
|
body = [e for e in els if e.clickable]
|
|
random.Random(seed).shuffle(body)
|
|
d.screens[screen].elements = head + body
|
|
return apply
|
|
|
|
|
|
def decoy(screen: str, label: str) -> Callable[[SimulatedDevice], None]:
|
|
"""A plausible wrong row, e.g. "Wi-Fi calling" next to "Wi-Fi"."""
|
|
def apply(d: SimulatedDevice) -> None:
|
|
d.screens[screen].elements.append(SimElement(label))
|
|
return apply
|
|
|
|
|
|
def chain(*fns: Callable[[SimulatedDevice], None]) -> Callable[[SimulatedDevice], None]:
|
|
def apply(d: SimulatedDevice) -> None:
|
|
for fn in fns:
|
|
fn(d)
|
|
return apply
|
|
|
|
|
|
@dataclass
|
|
class Variant:
|
|
variant_id: str
|
|
mutate: Callable[[SimulatedDevice], None] = lambda d: None
|
|
|
|
|
|
# --------------------------------------------------------------------- task
|
|
|
|
@dataclass
|
|
class Task:
|
|
task_id: str
|
|
prompt: str
|
|
reaches: Predicate
|
|
must_not: Predicate = lambda s: False
|
|
setup: Callable[[SimulatedDevice], None] = lambda d: None
|
|
max_steps: int = 10
|
|
note: str = ""
|
|
variants: list[Variant] = field(default_factory=lambda: [Variant("base")])
|
|
needs: tuple[str, ...] = ()
|
|
"""PhoneState facts this task's predicates require that a screen cannot
|
|
show (``toggled``, ``fields``, ``records``). A task listing any of these
|
|
cannot be scored on a real phone, and says so instead of scoring wrong."""
|
|
reset_adb: tuple[tuple[str, ...], ...] = ()
|
|
"""Commands that put a real phone into this task's starting state. Without
|
|
them a real-device run is scored against an unknown start, so an empty
|
|
tuple means "refuse to run me on a phone"."""
|
|
|
|
def variant(self, n: int) -> Variant:
|
|
return self.variants[n % len(self.variants)]
|
|
|
|
def build(self, run: int = 0) -> SimulatedDevice:
|
|
device = demo_phone()
|
|
self.variant(run).mutate(device)
|
|
self.setup(device)
|
|
return device
|
|
|
|
def scorable_on(self, state: PhoneState) -> str | None:
|
|
"""Why this task cannot be scored from ``state``, or None if it can."""
|
|
if state.source == "device" and self.needs:
|
|
return (f"needs {', '.join(self.needs)}, which a screen cannot "
|
|
f"show; simulator only")
|
|
return None
|
|
|
|
def score(self, device_or_state, declared_done: bool, run: int = 0) -> dict:
|
|
state = (device_or_state if isinstance(device_or_state, PhoneState)
|
|
else read_state(device_or_state))
|
|
blocked = self.scorable_on(state)
|
|
if blocked:
|
|
return {"task_id": self.task_id, "variant": self.variant(run).variant_id,
|
|
"scorable": False, "reason": blocked, "success": False,
|
|
"reached_goal": None, "collateral_damage": None,
|
|
"declared_done": declared_done}
|
|
reached = bool(self.reaches(state))
|
|
damaged = bool(self.must_not(state))
|
|
return {
|
|
"task_id": self.task_id,
|
|
"variant": self.variant(run).variant_id,
|
|
"scorable": True,
|
|
"reached_goal": reached,
|
|
"collateral_damage": damaged,
|
|
"declared_done": declared_done,
|
|
# The only row that counts: the phone is in the requested state,
|
|
# nothing else was broken, and the agent knew to stop.
|
|
"success": reached and not damaged and declared_done,
|
|
}
|
|
|
|
|
|
# ----------------------------------------------------------------- oracles
|
|
|
|
# Both the on-screen labels and the setting key, because `switch()` matches
|
|
# either source: "Wi-Fi" normalises onto the `wifi` setting by luck, but
|
|
# "Airplane mode" does not normalise onto `airplane`, and a name the setting
|
|
# lookup misses silently falls through to "control not on screen".
|
|
WIFI = ("Wi-Fi", "WiFi", "WLAN", "wifi")
|
|
AIRPLANE = ("Airplane mode", "Flight mode", "airplane")
|
|
|
|
|
|
def _wifi_is(want: bool) -> Predicate:
|
|
# `switch` returns None when the control is not on screen, which is not
|
|
# the same as off: an agent that never navigated there has not turned
|
|
# anything on.
|
|
return lambda s: s.switch(*WIFI) is want
|
|
|
|
|
|
def _airplane_on(s: PhoneState) -> bool:
|
|
return s.switch(*AIRPLANE) is True
|
|
|
|
|
|
def _alarm_set(hour: int, minute: int, pm: bool) -> Predicate:
|
|
"""A *saved* alarm reading the requested time.
|
|
|
|
The clock screen lists committed alarms, so this reads the screen and works
|
|
on a real phone. Typing the digits without saving leaves the list empty.
|
|
"""
|
|
wanted = f"{hour}:{minute:02d} {'PM' if pm else 'AM'}"
|
|
return lambda s: s.has_text(wanted)
|
|
|
|
|
|
SUITE: list[Task] = [
|
|
Task(
|
|
task_id="wifi_on",
|
|
prompt="Turn on Wi-Fi.",
|
|
reaches=_wifi_is(True),
|
|
must_not=_airplane_on,
|
|
note="Two screens deep, then a switch. The switch's state is visible, "
|
|
"so stopping is decidable from the screen.",
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("wlan", rename("network", "Wi-Fi", "WLAN")),
|
|
Variant("reordered", reorder("network", seed=7)),
|
|
Variant("decoy", decoy("network", "Wi-Fi calling")),
|
|
Variant("deep", chain(reorder("settings", seed=3),
|
|
decoy("network", "Wi-Fi preferences"))),
|
|
],
|
|
reset_adb=(("shell", "svc", "wifi", "disable"),),
|
|
),
|
|
Task(
|
|
task_id="wifi_on_already",
|
|
prompt="Turn on Wi-Fi.",
|
|
setup=lambda d: d.state.__setitem__("wifi", True),
|
|
reaches=lambda s: s.switch(*WIFI) is True and s.times_toggled("wifi") == 0,
|
|
must_not=_airplane_on,
|
|
note="Already satisfied. The only correct action is task_complete. "
|
|
"Any flip of the switch fails, including off-and-on again, which "
|
|
"final state cannot see and which W4A8 actually did.",
|
|
needs=("toggled",),
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("wlan", rename("network", "Wi-Fi", "WLAN")),
|
|
Variant("reordered", reorder("network", seed=11)),
|
|
],
|
|
),
|
|
Task(
|
|
task_id="wifi_off",
|
|
prompt="Turn off Wi-Fi.",
|
|
setup=lambda d: d.state.__setitem__("wifi", True),
|
|
reaches=_wifi_is(False),
|
|
must_not=_airplane_on,
|
|
note="The mirror of wifi_on: guards against a model that has simply "
|
|
"learned to tap switches.",
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("wlan", rename("network", "Wi-Fi", "WLAN")),
|
|
Variant("reordered", reorder("network", seed=5)),
|
|
],
|
|
reset_adb=(("shell", "svc", "wifi", "enable"),),
|
|
),
|
|
Task(
|
|
task_id="airplane_on",
|
|
prompt="Turn on airplane mode.",
|
|
reaches=_airplane_on,
|
|
note="Same screen as Wi-Fi, different row -- catches index confusion, "
|
|
"which is why one variant reorders that screen.",
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("reordered", reorder("network", seed=2)),
|
|
Variant("decoy", decoy("network", "Airplane mode settings")),
|
|
],
|
|
# `settings put global` needs WRITE_SECURE_SETTINGS, which adb has on a
|
|
# debuggable build but not on every retail phone; the run fails loudly
|
|
# if it is refused rather than scoring against an unknown start.
|
|
reset_adb=(("shell", "settings", "put", "global", "airplane_mode_on", "0"),),
|
|
),
|
|
Task(
|
|
task_id="open_clock",
|
|
prompt="Open the Clock app and go to the Alarm tab.",
|
|
reaches=lambda s: s.has_text("Add alarm") or s.has_text("Set time"),
|
|
note="Navigation only, no state change. Isolates 'can it find an app' "
|
|
"from 'can it finish'.",
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("reordered", reorder("home", seed=4)),
|
|
Variant("decoy", decoy("home", "Clock Widget")),
|
|
],
|
|
reset_adb=(("shell", "am", "force-stop", CLOCK_PACKAGE),
|
|
("shell", "input", "keyevent", "KEYCODE_HOME")),
|
|
),
|
|
Task(
|
|
task_id="add_alarm",
|
|
prompt="Set an alarm for 7:30 AM.",
|
|
reaches=_alarm_set(7, 30, pm=False),
|
|
must_not=lambda s: s.has_text("7:30 PM"),
|
|
max_steps=12,
|
|
note="The full flow: right app, right tab, add, type both fields, "
|
|
"leave the meridiem alone, save. Previously this passed on any "
|
|
"typed string containing '7'.",
|
|
variants=[
|
|
Variant("base"),
|
|
Variant("reordered", reorder("clock", seed=9)),
|
|
Variant("decoy", decoy("alarms", "Alarm settings")),
|
|
],
|
|
# `pm clear` removes saved alarms, so run N does not see run N-1's.
|
|
reset_adb=(("shell", "pm", "clear", CLOCK_PACKAGE),
|
|
("shell", "input", "keyevent", "KEYCODE_HOME")),
|
|
),
|
|
]
|
|
|
|
SUITE_BY_ID = {t.task_id: t for t in SUITE}
|