VLM Benchmark — on-device optimization A/B
Benchmark an optimization change (a PR or branch, in the addon or the qvac-fabric) on a
specific AWS Device Farm device, using the packages/llm-llamacpp/benchmarks/vlm-benchmark
framework. It builds the change from source, runs a baseline vs optimized A/B on the device
(N runs each), pairs them by CPU fingerprint, and reports speed and quality — with
quality regression as the primary gate. This codifies the QVAC-21320 / QVAC-21297 methodology.
Scope: Qwen3.5-0.8B VLM, projector mmproj CPU-vs-GPU (mmproj=both) by default. The model is
selectable (--model) — the framework already ships multiple VLMs (Qwen3.5 f16/q8, Gemma-4-E2B)
and a new one can be added to config.cjs; Qwen3.5-q8 is just the default. Device Farm only (no local
ADB run yet). Thermal / cross-unit variance is handled by CPU-fingerprint matching over multiple runs,
not interleaving.
Source safety (read-only target). This skill never modifies the PR or branch under test. It only reads the target —
gh pr view/gh pr diff/git diff— to resolve its ref/commit and classify the change. Every change (harness/config edits and the optimization itself — the fabric overlayREFor cherry-picked addon commits) is made only on the skill's owntest/<slug>/test/<slug>-baserefs. It never commits to, pushes to, or otherwise alters the source PR's branch, the fabric repo, ormain.
Usage
/vlm-benchmark #2921
/vlm-benchmark https://github.com/tetherto/qvac/pull/2921
/vlm-benchmark https://github.com/tetherto/qvac-fabric-llm.cpp/pull/172 --model gemma4-q4 --device pixel9
/vlm-benchmark feat/QVAC-21297-opencl-vision-encoder --device s25 --preset full --runs 2
/vlm-benchmark feat/my-opt --mmproj both --preset base
Arguments
| Arg | Required | Default | Meaning |
|---|---|---|---|
| target | yes | — | #<n> / PR URL → PR mode; otherwise a branch name |
--device |
no | s25 |
Device Farm phone as a matrix_mobile token: s25 (S25 Ultra / Adreno), pixel9 (Pixel 9 / Mali), s26 (S26 Ultra), iphone16 / iphone17 / iphone17pro. Passed as -f matrix_mobile=<token> at dispatch (Step 8) — the workflow resolves it to a single-device device_model filter. A friendly name ('Pixel 9') maps to its token (pixel9). Append -cpu/-gpu to force the model backend; bare token = the harness picks per --mmproj. |
--model |
no | qwen3.5-q8 |
Which VLM to benchmark — a config.cjs catalog label (qwen3.5-f16, qwen3.5-q8, gemma4-q4 = Gemma-4-E2B Q4) or a new one the skill adds (Step 6). A friendly value like gemma4/gemma4-e2b maps to the gemma4-q4 catalog entry. Default = MODEL_2 / current mmprojModel |
--preset |
no | full |
full (5 tasks×5, sharpest — DEFAULT; but overruns the mobile session for mmproj=both → use only on desktop or mmproj=gpu, or raise mobile_timeout_min — see Step 8) · base (5 tasks×3 = the cognitive set with matrix_samples=3; the mobile mmproj=both choice) · smoke (textvqa×1, wiring check). Default to base on mobile mmproj=both. |
--runs |
no | 2 |
runs per build (2 baseline + 2 optimized = a 4-run CPU-matched set) |
--mmproj |
no | both |
both = projector CPU-vs-GPU per run · gpu = projector on GPU only |
Prerequisites & paths
The commands below use placeholders — resolve each once, up front, then substitute (don't assume hard-coded paths):
ghCLI authenticated totetherto/qvac(Actions dispatch + read);nodeon PATH (formatch-devices.js).<qvac>— the local qvac main clone (origin=tetherto/qvac). Discover it, don't guess: rungit worktree listfrom any qvac checkout → the first line's path is the main worktree. The session's current directory may be a read-only worktree — never build or push from it.<feature-branch>— the resolved target branch from Step 1 (the PR head branch, or the branch argument). The optimized ref istest/<slug>; the baseline ref istest/<slug>-base.<pr-repo>— the GitHub repo the PR lives in (resolved in Step 1):tetherto/qvac-fabric-llm.cppfor a fabric PR,tetherto/qvacfor an addon PR. Used only to read the PR; Actions dispatch always targetstetherto/qvac.<slug>— the feature-branch basename (branchfeat/QVAC-21297-opencl-vision-encoder→ slugQVAC-21297-opencl-vision-encoder).<worktree>— the isolated worktree this run creates in Step 5:<qvac>/../wt-<slug>(a sibling of the clone — the repo'swt-<name>convention). All edits, build, and push happen here.<base>— the branch the worktree is cut from; chosen in Step 4 (origin/main, or the qvac feature branch related to the target).
Workflow
Step 1 — Resolve the target
- PR (
#<n>or URL): resolve the repo from the target — aqvac-fabric-llm.cppPR URL →tetherto/qvac-fabric-llm.cpp(a fabric PR); aqvacPR URL or a bare#<n>→tetherto/qvac(an addon PR). Thengh pr view <n> --repo <pr-repo> --json headRefName,commits,files,title,body→ the head branch is the optimization. (<pr-repo>is only for reading the PR; all Actions dispatch in Steps 7–8 targetstetherto/qvac, where the workflow lives.) - Branch: use it directly.
- Parse the flags off the tail (
--device/--model/--mmproj/--preset/--runs); unset ones take their defaults, but--device/--model/--mmprojare reconciled against the PR in Step 3 before anything is built.
Step 2 — Detect the change scope (from the diff)
Run gh pr diff <n> --repo <pr-repo> (PR; <pr-repo> from Step 1) or git diff main...<branch>
(branch). Classify:
- Addon change — touches
packages/*/addon/**or addon JS/config. The optimization is the addon commit(s); baseline is the pre-optimization state (Steps 5–6 build it as<base>+ harness edits, addon change omitted; the optimized ref adds the addon commits on top). - Fabric change — the optimization is a
qvac-fabric(tetherto/qvac-fabric-llm.cpp) commit (a fabric PR/commit, or a branch that bumps the fabric). The A/B uses a self-contained vcpkg overlay-port whoseREFis the only delta: optimized overlayREF= the fabric PR/branch head commit; baseline overlayREF= the pre-change fabric commit (the PR's merge-base with its base, or the published registry version's commit). Both refs use the overlay — Step 6 covers creating the port and computing itsSHA512directly (no CI round-trip). - Mixed / dependency — a PR that needs both (addon + a fabric overlay): apply both on the optimized build; the baseline strips only the optimization under test. Keep the optimization the sole delta between the two builds (isolation).
Step 3 — Reconcile the passed options against the PR
Before building, cross-check --device / --model / --mmproj against what the change actually
targets, from the PR title/body/linked ticket + the diff-touched code:
- Device / backend —
ggml-opencl/ "OpenCL" / "Adreno" → an Adreno device (tokens25);ggml-vulkan/ "Vulkan" / "Mali" → a Mali device (tokenpixel9); "Metal" → iOS/desktop. Plus any explicit device/SoC mention in the PR (S25,Pixel 9,Adreno 830,Snapdragon) → its token. - Model — a Qwen / Gemma / other model or projector mention.
- mmproj axis — whether it's a projector (
mmproj) change vs an LLM change.
Then, per axis:
- (a) user did not pass it and the PR implies a value → adopt the PR's value and notify.
- (b) user passed it and it conflicts with the PR (e.g.
--device pixel9(Mali/Vulkan) on an Adreno/OpenCL-only fix — the change wouldn't even be exercised) → AskUserQuestion: proceed as passed / switch to the PR's value / cancel. - (c) no PR signal → keep the passed value or the default.
Branch-mode targets with no PR: best-effort from the branch name / diff, else proceed with passed/defaults.
Step 4 — Choose <base>
git -C <qvac> fetch origin- Default:
<base>=origin/main. The optimization is layered as the sole delta (fabric overlayREF/ cherry-picked addon commits per Step 2), so the baseline stays clean regardless of base — this is the case for fabric PRs (target intetherto/qvac-fabric-llm.cpp) and standard addon PRs alike. - Exception — an existing qvac feature branch the target builds on: a branch-mode target that names a
qvac branch, or an optimization explicitly stacked on another unmerged qvac feature branch → set
<base>= that branch so the A/B builds on the same foundation.
The mmproj=both benchmark harness may not live on <base> yet — until it merges to main it lives on
test/QVAC-21257-mmproj-gpu-benchmark-changes (the harness + addon instrumentation grafted cleanly
onto main). Step 5 brings it in once the worktree exists. (Do not use the older
feat/QVAC-21257-mmproj-gpu-config — it is ~100 commits behind main and does not merge cleanly.)
Step 5 — Create the isolated worktree + bring in the harness
Create the worktree from <qvac> (never the read-only session checkout or other in-use worktrees):
git -C <qvac> worktree add <worktree> -b test/<slug> <base>
(drop -b if test/<slug> already exists.) All later edits, build, and push happen inside <worktree>.
Bring in the mmproj=both harness. Check whether it is already present:
git -C <worktree> show HEAD:packages/llm-llamacpp/benchmarks/vlm-benchmark/config.cjs contains
mmprojGpu/mmprojModel and benchmarks/vlm-benchmark/match-devices.js exists.
- Present (
<base>already carries it — e.g. once the harness has merged tomain) → nothing to do. - Absent → merge the grafted harness branch in — it sits directly on
main, so this is a clean merge (no conflicts):git -C <worktree> merge --no-edit origin/test/QVAC-21257-mmproj-gpu-benchmark-changesIt brings themmproj=bothaxis (mmprojGpu/mmprojModel,legsFor, per-rowvision_ms), the addonmmproj-use-gpukey +VisionEncodeMsinstrumentation (whichvision_msdepends on),match-devices.js, and thematrix_mmproj_gpuinput + conditional targeted-android prebuild inbenchmark-vlm-model-comparison.yml. (Never merge the oldfeat/QVAC-21257-mmproj-gpu-config: it trailsmainby ~100 commits, its addon C++ conflicts withmain'sContextSlideOutcomerefactor, and itsmatrix-s25/android_flagship_modelswiring predatesmain'smatrix_mobile/device_model.)
Two refs, one delta. The A/B is two refs that share identical harness config and differ only by the optimization. Build them in this order (Step 6 gives the exact edits):
- baseline =
test/<slug>-base=<base>(+ harness) + the harness/config edits, no optimization. - optimized =
test/<slug>= the baseline plus the optimization only — fabric: point the self-containedpackages/llm-llamacpp/vcpkg/ports/qvac-fabricoverlay-portREFat the fabric commit with a computedSHA512(Step 6); addon: cherry-pick / merge the PR/branch's addon commits on top.
(The QVAC-21297 run named these …-s25-stock / …-s25.)
Step 6 — Adjust the harness (notify the user, no approval)
Inside the worktree, make the edits and notify the user of each (file + what changed) — no approval needed, since it's an isolated branch/worktree:
- Optimization — apply the optimization per Step 2 (the sole delta between baseline and optimized):
- Fabric — use a self-contained overlay-port at
packages/llm-llamacpp/vcpkg/ports/qvac-fabric/. Copyportfile.cmake+vcpkg.json+android-vulkan-version.cmakefrom the registry porttetherto/qvac-registry-vcpkg:ports/qvac-fabric/into that dir — for a Mali / Android-Vulkan target the port MUST carryandroid-vulkan-version.cmake+ its Vulkan-C++-header provisioning (the port owns the Androidvulkan.hpp, so the build works even on a fabric commit that predates a fabric-side Android Vulkan build fix — this is why the baseline builds cleanly). Add"overlay-ports": ["vcpkg/ports"]topackages/llm-llamacpp/vcpkg-configuration.json—mainhas nooverlay-portskey, so add it as the first field (the path is relative to that file's dir →packages/llm-llamacpp/vcpkg/ports). In the portfile, change the registry'sREF v${VERSION}to the fabric commit and setSHA512to the hash computed directly (no CI round-trip; seeSKILL-fabric-release.mdPhase A). The fabric repo is private, so the archive fetch needs a token. Usecurlon the/archive/<ref>.tar.gzURL — notgh api …/tarball/<ref>: the/tarball/endpoint returns a differently-named top-level dir (owner-repo-shortshavsrepo-fullsha) → a differentSHA512that won't match what vcpkg fetches (andgh apican't write to a file without a redirect anyway). Commit-SHA/archive/tarballs are byte-stable, so the computed hash equals vcpkg's fetch (validate once against aSHA512already pinned in a known port):curl -fsSL -H "Authorization: Bearer $GH_TOKEN" https://github.com/tetherto/qvac-fabric-llm.cpp/archive/<ref>.tar.gz -o /tmp/f.tgz openssl dgst -sha512 /tmp/f.tgz # or: vcpkg hash /tmp/f.tgz
The baseline ref pins the pre-change fabric commit; the optimized ref pins the PR/branch commit — the overlayvcpkg_from_github(REPO tetherto/qvac-fabric-llm.cpp REF <fabric-commit> SHA512 <computed-sha512>)REF+SHA512is the only difference between them. (Fallback: if the computed hash is ever wrong, vcpkg prints the expected hash on the first failed fetch.) - Addon — cherry-pick / merge the PR/branch's addon commits onto the optimized ref only.
- Fabric — use a self-contained overlay-port at
- Model (
--model, as reconciled in Step 3) — where it's set depends on the mmproj axis:--mmproj both(mmproj-compare) → the run usesconfig.mmprojModel, read directly by the compare path (it does not honormatrix_models). SetmmprojModelinconfig.cjson thetest/ref (e.g.mmprojModel: GEMMA4_Q4forgemma4-q4/ Gemma-4-E2B). If the model isn't defined yet, add a{label, name, ctx_size, llm: hf(...), mmproj: hf(...)}literal (match the blobrepo/sha/fileto the registry/HF source) and pointmmprojModelat it.--mmproj gpu(two-models) → pass the model(s) via-f matrix_models=…at dispatch (forwarded to the phone asQVAC_VLM_MODELS); no branch edit.
- Device / preset / samples / mode are DISPATCH INPUTS — not harness edits. Main's benchmark forwards
them to the phone via the
matrix-mobilejob'sdevice_envchannel (QVAC_VLM_PRESET←matrix_preset,QVAC_VLM_SAMPLES←matrix_samples,QVAC_VLM_MODE←matrix_mode, anddevice_model/device_manufacturerfrom thematrix_mobiletoken). Pass them as-fflags in Step 8 — do not edit the branch for these. The--devicetoken fixes the GPU backend (s25→Adreno/OpenCL,pixel9→Mali/Vulkan) → which drives the Step 9 routing-proof grep and which opts are even relevant. - mmproj (
--mmproj) — the one run knob that IS a test-branch config edit for mobile: setmmprojGpu: 'both'|'gpu'inconfig.cjs(mobile readsconfig.mmprojGpu;QVAC_VLM_MMPROJ_GPUis desktop-only and not indevice_env).bothalso readsconfig.mmprojModel(see the Model bullet).
Commit these harness edits on test/<slug> — this commit is the baseline state. Branch the baseline
ref at it: git -C <worktree> branch test/<slug>-base. Then apply the optimization on test/<slug>
(fabric: set the overlay-port REF+computed SHA512 to the optimized fabric commit; addon: cherry-pick
/ merge the PR's addon commits) and commit. The two refs now differ by exactly the optimization —
nothing else.
Step 7 — Push the two refs (the one approval gate)
Notify the user of the exact pushes and ask for approval — noting they target separate
test/<slug> refs (never main), so it's safe. Push both the optimized and baseline
refs from Step 5:
git -C <worktree> push -u origin test/<slug>
git -C <worktree> push -u origin test/<slug>-base
Push only on approval.
Step 8 — Run baseline + optimized on Device Farm (--runs each)
Dispatch each ref --runs times — the optimized ref and the baseline ref from Step 5. <device>
is the --device token (default s25); matrix_desktop=none forces zero desktop cells → the run
is mobile-only, which triggers the targeted android-only prebuild (prebuild-candidate-mobile, built
with the Vulkan SDK for Mali) and skips the heavy full-desktop prebuild + desktop legs. ⚠️ Use the
literal none (any non-empty unknown token works), not an empty string — the workflow defaults an
empty/unset matrix_desktop to linux-cpu (${MATRIX_DESKTOP:-linux-cpu}), which would run the full
desktop prebuild and skip the targeted mobile build. matrix_sources=addon@candidate builds the
addon from the dispatched ref so the ref's fabric overlay / addon commits actually take effect:
# optimized — dispatch --runs times
gh workflow run benchmark-vlm-model-comparison.yml --repo tetherto/qvac \
--ref test/<slug> \
-f matrix_sources=addon@candidate -f matrix_desktop=none \
-f matrix_mobile=<device> -f matrix_preset=<preset> -f matrix_mode=two-models
# baseline — dispatch --runs times (identical inputs; only --ref differs)
gh workflow run benchmark-vlm-model-comparison.yml --repo tetherto/qvac \
--ref test/<slug>-base \
-f matrix_sources=addon@candidate -f matrix_desktop=none \
-f matrix_mobile=<device> -f matrix_preset=<preset> -f matrix_mode=two-models
--preset → dispatch inputs. matrix_preset names a config.cjs preset (smoke, cognitive,
full); there is no base preset, so map the arg:
full→-f matrix_preset=fullbase→-f matrix_preset=cognitive -f matrix_samples=3(the 5 VQA tasks × 3 = 15/cell, no OCR)smoke→-f matrix_preset=smoke
⚠️ Mobile session budget (critical for mmproj=both). On mobile, mmproj=both runs two cells
(CPU + GPU) in one Device-Farm session → ~2× the inferences. full × mmproj=both overruns the
~30-min per-test ceiling: the CPU cell finishes but the GPU cell is cut off mid-run (matrix-mobile
shows failure while matrix-combine still succeeds on partial data — the GPU cell's quality then covers
only the first 1–2 tasks and is not comparable). So for a mobile mmproj=both verdict default to
base, and/or raise the ceiling with -f mobile_timeout_min=<min> (e.g. 60; capped by the 120-min
DF/GH job ceiling). full on mobile is safe only for mmproj=gpu (one cell) or with a raised timeout.
Default --runs 2 → 4 dispatches total (--runs = dispatches per ref, for CPU-fingerprint
matching across units — not samples/task, which the preset governs). Device / preset / mode reach the
phone via the matrix-mobile job's device_env (Step 6) — pass them as -f inputs, identical for both
refs; only mmprojGpu (+ mmprojModel for --mmproj both) is baked into each ref's config.cjs.
Monitor each run to completion (poll gh run view <id> --repo tetherto/qvac --json status,conclusion,jobs):
the candidate prebuild (prebuild-candidate-mobile on a mobile-only run) is the early build signal;
matrix-mobile is the on-device leg (skips if the prebuild fails). Record which run ID is baseline vs
optimized — needed in Step 10.
Step 9 — Retrieve metrics
The raw [VLMROW]/[VLMSEG] markers a mobile run needs are in the console-logs-*-Android
artifact — NOT in gh run view --log (they print to the on-device Android logcat, which Device Farm
captures as an artifact). For each run, download it and keep the logcat as that run's marker file for
Step 10:
gh run download <run-id> --repo tetherto/qvac --name console-logs-<pkg>-<device>-Android --dir <run-dir>
# → <run-dir>/Android/Android_logcat_full.txt holds the [VLMROW]/[VLMSEG] markers
Use a distinct <run-dir> per build+index (e.g. opt1/, base1/) so Step 10 can pair them, matching
the run IDs recorded in Step 8. (On desktop legs the markers are on native stderr, so there the
gh run view --log run log carries them; mobile does not.)
For the ready-made summary, read the vlm-matrix-consolidated-<run#> artifact (~2 KB markdown — full
quality + speed + samples tables) or the identical matrix-combine job-log render:
- Quality: per-task
textvqa / vizwiz / gqa / docvqa / ai2d+ Overall % (equal-weight mean). - Speed:
mmproj-encode(from the addonvision_msRuntimeStat), TTFT, wall; small/large resolution buckets; samples run / passed / failed. - Confirm the projector routed to the intended backend from logcat — Adreno OpenCL:
Adreno GPU version 830 found keeping OpenCL backend→using device GPUOpenCL; Mali Vulkan:found device description: Mali-G715→removing OpenCL backend … rely on Vulkan/cpu only→using device Vulkan0 (Mali-G715).
Step 10 — Pair + detect quality regression (primary gate)
- Pair baseline↔optimized runs with
benchmarks/vlm-benchmark/match-devices.js(build-invariant CPUmmproj-encodefingerprint → same-unit-equivalent), passing the per-run logs saved in Step 9 (--shipping= the baseline runs,--optimized= the optimized runs):node benchmarks/vlm-benchmark/match-devices.js --shipping base1/Android/Android_logcat_full.txt [base2/…] --optimized opt1/Android/Android_logcat_full.txt [opt2/…](the per-run logcat files downloaded in Step 9). - Quality Δ = (optimized − baseline) Overall %, plus per-task Δ. Flag ANY negative Δ (overall or any task) as a regression — this is the gate.
- With
--mmproj both, also report the within-run GPU-vs-CPU quality Δ per build (the projector-backend regression — the QVAC-21297 case: stock GPU −4.1 pp → fixed GPU Δ0). full(5 tasks×5) gives the sharpest quality signal on desktop /mmproj=gpu.base(3 samples) is coarser and can noise-mask small regressions (QVAC-21320 saw the same Adreno bug read as base −4 pp vs full −18.8 pp) — but on mobilemmproj=bothit is the verdict preset:fulloverruns the Device-Farm per-test window and truncates the GPU cell (Step 8), so a completedbasebeats a truncatedfull. Raisemobile_timeout_minif you must runfullon mobile.smoke(1 sample) is a wiring check only — it cannot produce a quality verdict, so on smoke, skip the regression gate + the full Step 11 report and just confirm routing /GPU==CPU/ 0 device-loss.
Step 11 — Report → write QVAC-<ticket>-benchmark-results.md
Write to QVAC-<ticket>-benchmark-results.md in the working dir where the prior docs live (repo root,
alongside QVAC-21320-benchmark-results.md / QVAC-21297-benchmark-results.md). Derive <ticket> as
the QVAC-\d+ match from the PR title (gh pr view <n> --repo <pr-repo> --json title) or the branch
name; if there is no ticket, fall back to <slug>-benchmark-results.md and say so in the doc.
Never discard prior results — layer the newest on top (the shape QVAC-21320-benchmark-results.md
already has: newest first, older sections below):
- Doc absent → create it in full with the Write tool, section-for-section per the template below.
- Doc present → do not overwrite. Read it, then insert THIS run at the top of the results (below
the doc's title + goal/context paragraph, above the prior run) in order:
- Summary of the latest result — the
> **Result:** …blockquote + the# Latest results …section (method one-liner, quality gate, CPU-matched table, recommendation) for this run. - Latest result details — the
# Detailed resultsblock for this run. - Results before that — the previously-latest content, demoted beneath: rename the prior
# Latest results …heading to a dated/labelled# Previous results — <date/axis>and keep it and all older sections intact below (preserve verbatim; only demote headings / reorder).
- Summary of the latest result — the
Either way the skill does not commit/push the doc (see Notes).
Match the two reference docs section-for-section, in this order (copy their exact column/wording):
# QVAC-<ticket>: <one-line descriptor> (<device> / <SoC + backend>)- Goal / context paragraph (italic) — what the optimization is, the ticket goal, related-ticket links.
> **Result:** …blockquote — one-paragraph headline verdict (what the A/B proved, quality first).---- Headline section
# Latest results — <compared axis> (<device>):- Method in one line (italic): N runs, Device Farm pool, baseline-vs-optimized definitions, model,
mmprojmode, preset (tasks×samples,n=/cell), matched-by CPU fingerprint, link to CI runs. - Quality paragraph (the primary gate) — Overall Δ + notable per-task shifts.
- Speed paragraph + the CPU-matched comparison table:
| Metric | Baseline-CPU | Baseline-GPU | Optimized-CPU | Optimized-GPU | <opt vs base> | <opt vs shipping-CPU> |rowsmmproj-encode (ms)·TTFT (ms)·wall (ms)·quality (Overall %); then a short column-meaning gloss. - Primary robust metric paragraph — the within-run CPU/GPU encode ratio (device variance cancels).
## Recommendation— numbered, actionable verdict.
- Method in one line (italic): N runs, Device Farm pool, baseline-vs-optimized definitions, model,
---# Detailed results:## Quality — lmms-eval overall %— per-task table| Build | Config | textvqa | vizwiz | gqa | docvqa | ai2d | Overall % |+ a Δ-callout line (flag ANY negative per-task or overall Δ; note base-preset granularity if the preset is coarse).## Per-run summary—| Run | Build | CPU enc | GPU enc | GPU TTFT | within-run CPU/GPU | quality GPU vs CPU |, one row per run, run-ID linked.## Resolution dependence— small vs largemmproj-encodebucket table.## Caveats— decode-TPS artifact, peak-RSS not captured, CPU-fingerprint match %.## Setup & isolation— model/blobs, device + logcat backend-routing proof, baseline/optimized build definitions (overlayREF/ addon commit), the isolation statement (optimization = sole delta).## CI runs— build-validation, smoke, and the N-run matched set, all run-ID linked.## Investigation(optional) — headroom / profiler / accuracy-guard notes when relevant.## Verdict— closing paragraph.
QVAC-21320-benchmark-results.md and QVAC-21297-benchmark-results.md are the worked templates —
copy their exact column headers and phrasing.
Notes
- Device Farm only — no local ADB run yet. Thermal / cross-unit variance is handled by CPU-fingerprint matching + N runs, not interleaving.
- Isolation — the optimization under test must be the ONLY delta between baseline and optimized builds. Don't drag in unrelated fabric/addon changes (e.g. keep OpenCL work free of Vulkan opts).
- Worktree — created from
<qvac>(the main clone) at<worktree>=<qvac>/../wt-<slug>(the repo'swt-<name>convention; see Prerequisites). Never edit in the read-only session checkout or other in-use worktrees. - Model support —
config.cjsalready ships multiple VLMs (Qwen3.5 f16/q8, Gemma-4-E2B); select via--model→mmprojModel(mmproj-compare) /models(two-models), and add a new one viahf()blob literals. No framework code change is needed to benchmark a different model. - Approval policy — harness edits happen in the isolated worktree and are notify-only; the
only approval gate is the push to
origin(Step 7), and even that is a separatetest/<slug>, nevermain. - Harness source — until the mmproj benchmark tooling merges to
main, it lives ontest/QVAC-21257-mmproj-gpu-benchmark-changes(grafted cleanly ontomain), merged in by Step 5. The olderfeat/QVAC-21257-mmproj-gpu-configis NOT usable — it trailsmainby ~100 commits and its addon C++ conflicts withmain'sContextSlideOutcomerefactor (the removedFullWipe). End state: once the grafted branch lands onmain, drop the Step 5 merge and base directly onorigin/main. - Device targeting — a
-f matrix_mobile=<token>dispatch input (s25/pixel9/s26/iphone*), not a branch edit. Thecontextjob resolves each token todevice_model+device_manufacturerand thematrix-mobilejob runs it as a single-device Device Farm filter (one phone per token). - What reaches the phone — the
matrix-mobilejob forwards dispatch inputs to the device via itsdevice_env(QVAC_VLM_PRESET←matrix_preset,QVAC_VLM_MODE←matrix_mode,QVAC_VLM_SAMPLES←matrix_samples,QVAC_VLM_MODELS←matrix_models, plusdevice_model). So preset/mode/samples/ model are-finputs, notconfig.cjsedits. The exception ismmprojGpu(QVAC_VLM_MMPROJ_GPUis desktop-only, absent fromdevice_env) → set it inconfig.cjson thetest/ref. - Per-op OpenCL profiling (
GGML_OPENCL_PROFILING/CLPROF) is out of scope: its per-kernel logcat routing is blocked underGGML_BACKEND_DL(the backend module's stderr is not captured). - Assets it drives:
benchmarks/vlm-benchmark/{config.cjs,harness.cjs,stage.cjs,aggregate.js,match-devices.js},.github/workflows/{benchmark-vlm-model-comparison,reusable-prebuild-targeted,integration-mobile-test-llm-llamacpp}.yml. Worked examples of the output format:QVAC-21320-benchmark-results.md,QVAC-21297-benchmark-results.md. - Writes the results doc
QVAC-<ticket>-benchmark-results.mdto disk (Step 11) — creating it if absent, otherwise prepending the new run as Latest and demoting the prior run to history (never discarding prior results) — but does NOT commit/push it or open a PR; the caller handles version control.