Performance release gate for on-device AI inference on Apple Silicon.
Runs paired llama-bench measurements (base vs. head), balances execution order with ABBA/BAAB scheduling, and produces a statistically grounded PASS, FAIL, or INCONCLUSIVE verdict.
Scope is intentionally narrow: two llama.cpp Metal builds, one Mac, one local GGUF model. Not a benchmark suite, not a dashboard, not a hosted service.
- Apple Silicon Mac on AC power (Low Power Mode off, nominal thermals, low background load)
- Python ≥ 3.10
llama-benchbinaries built with Metal- One local GGUF model — EdgeCI never downloads anything or makes network calls
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Verify:
edgeci --version
edgeci doctor --model /path/to/model.ggufCopy examples/edgeci.toml.example to .edgeci.toml, set your model path, then check runner health:
edgeci doctorRun a comparison:
edgeci compare \
--base /path/to/base/llama-bench \
--head /path/to/head/llama-bench \
--model /path/to/model.ggufResults go to ./edgeci-results/ by default:
result.json— machine-readable canonical result with provenance and raw measurementsreport.md— GitHub-flavored Markdown for PR commentsschedule.json— deterministic invocation order and seed
| Code | Meaning |
|---|---|
0 |
PASS |
1 |
FAIL |
2 |
INCONCLUSIVE |
Unenrolled runners operate in shadow mode: edgeci compare returns 0 regardless of verdict to avoid blocking CI with uncalibrated evidence. The report contains the real verdict. Calibration commands always use the codes above.
Read the report even on nonzero exits. FAIL means evidence of regression. INCONCLUSIVE means the protocol couldn't establish either direction.
EdgeCI walks up from the current directory looking for .edgeci.toml. --config overrides this; CLI flags override file values.
[model]
path = "/absolute/path/to/model.gguf"
[benchmark]
prompt_tokens = 512
generate_tokens = 128
pairs = 20
warmup_pairs = 2
gap_seconds = 15
timeout_minutes = 60
[budgets]
tg = 0.05
pp = 0.05
[preflight]
thermal_settle_seconds = 60
idle_cpu_threshold = 0.20
post_build_cooldown = 120
preflight_timeout = 600
[report]
format = "all"
output_dir = "./edgeci-results"The 0.05 budgets tolerate up to 5% throughput loss for token generation (tg) and prompt processing (pp). Lower = stricter. Changing pair count or timing changes measurement power; recalibrate on the target runner.
timeout_minutes caps the full comparison (validation, hashing, probes, preflight, measurement, recovery). EdgeCI kills the subprocess group on deadline or interrupt.
Checks seven prerequisites without running a benchmark:
- Apple Silicon hardware
- Nominal thermal state
- AC power, Low Power Mode off
- Normal memory pressure
- CPU load below threshold
llama-benchavailable- GGUF model exists
edgeci doctor --config .edgeci.tomlRuns preflight, discarded warm-ups, and measured pairs sequentially. --seed reproduces schedule order:
edgeci compare \
--base ./build-base/bin/llama-bench \
--head ./build-head/bin/llama-bench \
--model /models/model.gguf \
--seed pr-1842-attempt-1 \
--format all \
--output ./edgeci-resultsDo not run hardware-probing commands concurrently. EdgeCI holds a machine-wide lock at /tmp/edgeci.lock.
Full protocol as an A/A test. A healthy runner should show effects near zero and no false FAIL.
# Same binary path, hidden labels
edgeci calibrate --binary ./llama-bench --model /models/model.gguf
# Byte-for-byte copy
edgeci calibrate --binary ./llama-bench --model /models/model.gguf --mode copy
# Two independent builds of the same source
edgeci calibrate \
--binary ./build-a/bin/llama-bench \
--equivalent-binary ./build-b/bin/llama-bench \
--model /models/model.gguf \
--mode equivalent-buildCalibration sessions are stored in ~/.edgeci/calibrations/. Five clean sessions across two or more days enrolls the runner. Enrollment expires 14 days after the latest qualifying session and invalidates on OS, hardware, model, or protocol changes.
A session is clean only when both null-effect intervals include zero, effects stay within budgets, no diagnostic warnings fire, and the production protocol returns PASS. Large apparent A/A speedups don't qualify — they'd enroll a biased runner.
Hardware fingerprint, enrollment state, expiry, recent calibration history, observed A/A false-fail rate:
edgeci statusThe schedule is generated before execution. Each balanced block = one AB pair + one BA pair, arranged as ABBA or BAAB. Base and head never run concurrently.
Per metric, EdgeCI computes paired log-ratios ln(base / head), balances AB/BA order, then evaluates two preregistered 95% intervals:
- Stratified bootstrap over AB and BA ratios independently
- Block-t over balanced block averages
For throughput, positive log-effect = head is slower. Both intervals must fall below the regression boundary → PASS. Both lower bounds must exceed it → FAIL. Any disagreement or overlap → INCONCLUSIVE. Overall: FAIL if either metric fails, INCONCLUSIVE if either is inconclusive, PASS only when both pass.
High CV, large block drift, and interval disagreement produce warnings. Warnings never silently change the verdict. Completed measurements are never dropped for looking like outliers — only observable external events can contaminate a block.
- Dedicate the Mac during a run
- AC power, awake, Low Power Mode off
- Kill indexing, backups, builds, browsers, heavy background work
- Same model file and benchmark parameters for both binaries
- Let EdgeCI handle post-build cooldown and thermal settling
- Don't delete results because thermals changed after an invocation started — that's data
- Recalibrate after OS updates, hardware changes, or protocol changes
examples/workflow.yml.example is a two-job PR workflow. Benchmarking runs on a labeled self-hosted Apple Silicon runner with read-only permissions. A separate hosted job downloads the report and posts a PR comment with pull-requests: write.
Before using it:
- Set
EDGECI_MODEL_PATHto an absolute model path on the runner - Set
EDGECI_WHEELHOUSEto a runner-owned read-only directory with reviewed EdgeCI 0.1.0 and dependency wheels - Keep
.edgeci.tomlon the protected base branch — the template never reads protocol settings from PR head - Install build tools (Python 3, CMake, Ninja, compiler) on the Mac
- Create an
edgeci-approvedenvironment with required reviewers - Restrict the runner group to this repository/workflow; Actions runner ≥ 2.329.0
Fork PRs are rejected at the job boundary. PR code still runs on the self-hosted Mac after environment approval — use a dedicated runner with no sensitive credentials or network access.
The first M5 validation records a 20-pair null control, a synthetic boundary canary, observed variance, and limitations. Alpha evidence from one machine — not a cross-hardware false-alert rate claim.
pip install -e '.[test]'
pytestThe statistical core uses only the Python standard library (no NumPy/SciPy). The tool makes no network calls.
MIT. See LICENSE.