omni-bench development
Embeddable benchmark framework, not a model runner: hosts own inference via
adapter seams; omni-bench owns datasets, scoring, metrics, and the JSON spec.
North star: quality + speed tied to specific hardware; parity
(Python↔Swift, CUDA↔MLX) is a derived view. Primary consumer: a Swift host
wrapping in-process models (e.g. mlx-omni).
Layout
schemas/*.schema.json — the SSOT (open spec, JSON Schema draft 2020-12).
Everything else follows it. Pinned schema_version (0.1.0, re-cut
2026-07-04 — the pre-release 0.1.0/0.2.0 iterations are retired).
python/src/omni_bench/ — core/ (prepare, producer, scorer, diff, validate,
identity, hardware), asr/ (normalizers, metrics, registry — the registry is
the modality-agnostic task table despite the package name), textgen/
(synthetic shared-prefix task), models/ (generated — never hand-edit;
regenerate with just codegen-python).
swift/ — producer SDK mirroring the Python producer (Transcriber,
Generator, Producer, ArtifactWriter, JCS identity).
fixtures/ — committed goldens; each dir has a generate.py to regenerate.
Pipeline & artifacts
prepare → run → score → diff:
manifest.json (+references.jsonl) → run-artifact.jsonl → result.json → parity-report.json.
- Producer never aborts: a failing sample becomes an
error record.
- Scorer is the trust anchor: scoring exists only in Python, once. Never
reimplement scoring in a host (measurer drift is the enemy this design kills).
- Identity is a JCS (RFC 8785) hash over a fixed subset (model, backend,
hardware, os, task, dataset, run_profile.decode). Identity fields are
host-declared and trusted — a bogus value silently breaks the parity join.
Adapter seams (structural, no inheritance; Swift mirrors Python 1:1)
- ASR:
Transcriber.transcribe(audio, language, task) -> Transcript.
- Text generation:
Generator.generate(prompt, task) -> Generation +
reset_cache(). All latency/token metrics (ttft_s, prefill_s,
prompt_tokens, generated_tokens, reused_tokens, cache_hit, decode_s)
are host-reported via backend_native — the seam is non-streaming, the
producer cannot observe TTFT. The scorer derives the rates: tok_per_s
(micro) and prefill_tok_per_s (cold samples only in cache mode — cache-free).
- Shared-prefix cold/warm scenario:
mode: "shared_prefix_cold_warm" +
per-sample prefix_group. Producer resets the cache at every group boundary;
first sample after reset is phase: "cold", rest "warm"; nothing is
warmup-discarded — the scorer's cache block reports cold/warm medians and
ttft_speedup = cold / warm. Built-in offline task: textgen.shared_prefix.en.v1.
Schema evolution rules (bite hard)
- Objects are closed (
additionalProperties: false) ⇒ ANY additive field is
a breaking change ⇒ bump the minor schema_version in lockstep across all
schemas, then update SCHEMA_VERSION in core/{prepare,producer,scorer,diff}.py,
Swift ArtifactWriter.writeHeader, regenerate models AND all fixtures.
- After any schema change:
just codegen-python (commit models/), rerun
fixtures/{scoring,parity,textgen}/generate.py, hand-bump fixtures/examples/*
and fixtures/xlang/manifest.json.
- Determinism is a hard invariant:
sort_keys JSON, samples sorted by id,
monotonic clocks, no timestamps in WAVs — prepare twice ⇒ identical hashes.
Nothing under omni_bench/ may use wall-clock or randomness in artifacts.
Commands
just check # validate-schemas + pytest + swift test — run before commit
just codegen-python # schemas -> pydantic models (stages *.schema.json to tmp)
uv run --project python pytest python/tests -q
cd swift && swift test
uv run --project python python -m omni_bench.cli prepare textgen.shared_prefix.en.v1 # offline, CI-safe
just/uv may live outside the default PATH (e.g. /opt/homebrew/bin).
Only textgen.shared_prefix.en.v1 is downloads-free; the ASR tasks hit
HuggingFace — tests monkeypatch get_task instead.
Gotchas
- datamodel-codegen parses EVERY file in its input dir — stage
*.schema.json
into a temp dir named schemas (the dir name lands in the generated header;
keep it stable), which is why the justfile does exactly that.
- Required-nullable is the repo idiom (
"type": ["x", "null"] + required):
Swift writes explicit NSNull(), omission fails validation. phase is the
exception (optional) so ASR artifacts keep their v1 shape.
- ASR hyp-identity in
diff uses the language normalizer; text-generation uses
NFC-exact comparison — deterministic decoding makes exact identity the gate.
- Hardware keys the identity: off macOS the collector can't see the accelerator;
pass explicit
hardware={"soc": ..., "mem_gb": ...} (producer warns otherwise).
- Keep contract docs in sync on any schema change:
schemas/README.md,
docs/integration.md, and the README CLI/task lists.
1---2name: omni-bench3description: Develop the omni-bench benchmark framework (ASR/STT + text generation) built on a producer/scorer split with an open JSON spec as SSOT. Use when working in an omni-bench checkout — changing the JSON schemas, the prepare/producer/scorer/diff pipeline, the adapter seams (Transcriber / Generator), datasets, fixtures, or the Swift producer SDK; or when running and validating benchmark artifacts. Encodes the schema-evolution rules, determinism invariants, commands, and repo gotchas that are easy to get wrong.4---56# omni-bench development78Embeddable benchmark **framework**, not a model runner: hosts own inference via9adapter seams; omni-bench owns datasets, scoring, metrics, and the JSON spec.10North star: quality + speed **tied to specific hardware**; parity11(Python↔Swift, CUDA↔MLX) is a derived view. Primary consumer: a Swift host12wrapping in-process models (e.g. mlx-omni).1314## Layout1516- `schemas/*.schema.json` — **the SSOT** (open spec, JSON Schema draft 2020-12).17 Everything else follows it. Pinned `schema_version` (`0.1.0`, re-cut18 2026-07-04 — the pre-release 0.1.0/0.2.0 iterations are retired).19- `python/src/omni_bench/` — `core/` (prepare, producer, scorer, diff, validate,20 identity, hardware), `asr/` (normalizers, metrics, registry — the registry is21 the modality-agnostic task table despite the package name), `textgen/`22 (synthetic shared-prefix task), `models/` (**generated** — never hand-edit;23 regenerate with `just codegen-python`).24- `swift/` — producer SDK mirroring the Python producer (`Transcriber`,25 `Generator`, `Producer`, `ArtifactWriter`, JCS identity).26- `fixtures/` — committed goldens; each dir has a `generate.py` to regenerate.2728## Pipeline & artifacts2930`prepare → run → score → diff`:31manifest.json (+references.jsonl) → run-artifact.jsonl → result.json → parity-report.json.3233- Producer never aborts: a failing sample becomes an `error` record.34- Scorer is the **trust anchor**: scoring exists only in Python, once. Never35 reimplement scoring in a host (measurer drift is the enemy this design kills).36- Identity is a JCS (RFC 8785) hash over a fixed subset (model, backend,37 hardware, os, task, dataset, run_profile.decode). Identity fields are38 host-declared and trusted — a bogus value silently breaks the parity join.3940## Adapter seams (structural, no inheritance; Swift mirrors Python 1:1)4142- ASR: `Transcriber.transcribe(audio, language, task) -> Transcript`.43- Text generation: `Generator.generate(prompt, task) -> Generation` +44 `reset_cache()`. All latency/token metrics (`ttft_s`, `prefill_s`,45 `prompt_tokens`, `generated_tokens`, `reused_tokens`, `cache_hit`, `decode_s`)46 are **host-reported** via `backend_native` — the seam is non-streaming, the47 producer cannot observe TTFT. The scorer derives the rates: `tok_per_s`48 (micro) and `prefill_tok_per_s` (cold samples only in cache mode — cache-free).49- Shared-prefix cold/warm scenario: `mode: "shared_prefix_cold_warm"` +50 per-sample `prefix_group`. Producer resets the cache at every group boundary;51 first sample after reset is `phase: "cold"`, rest `"warm"`; **nothing is52 warmup-discarded** — the scorer's `cache` block reports cold/warm medians and53 `ttft_speedup = cold / warm`. Built-in offline task: `textgen.shared_prefix.en.v1`.5455## Schema evolution rules (bite hard)5657- Objects are **closed** (`additionalProperties: false`) ⇒ ANY additive field is58 a breaking change ⇒ bump the minor `schema_version` **in lockstep across all59 schemas**, then update `SCHEMA_VERSION` in `core/{prepare,producer,scorer,diff}.py`,60 Swift `ArtifactWriter.writeHeader`, regenerate models AND all fixtures.61- After any schema change: `just codegen-python` (commit `models/`), rerun62 `fixtures/{scoring,parity,textgen}/generate.py`, hand-bump `fixtures/examples/*`63 and `fixtures/xlang/manifest.json`.64- Determinism is a hard invariant: `sort_keys` JSON, samples sorted by id,65 monotonic clocks, no timestamps in WAVs — `prepare` twice ⇒ identical hashes.66 Nothing under `omni_bench/` may use wall-clock or randomness in artifacts.6768## Commands6970```bash71just check # validate-schemas + pytest + swift test — run before commit72just codegen-python # schemas -> pydantic models (stages *.schema.json to tmp)73uv run --project python pytest python/tests -q74cd swift && swift test75uv run --project python python -m omni_bench.cli prepare textgen.shared_prefix.en.v1 # offline, CI-safe76```7778`just`/`uv` may live outside the default PATH (e.g. `/opt/homebrew/bin`).79Only `textgen.shared_prefix.en.v1` is downloads-free; the ASR tasks hit80HuggingFace — tests monkeypatch `get_task` instead.8182## Gotchas8384- datamodel-codegen parses EVERY file in its input dir — stage `*.schema.json`85 into a temp dir named `schemas` (the dir name lands in the generated header;86 keep it stable), which is why the justfile does exactly that.87- Required-nullable is the repo idiom (`"type": ["x", "null"]` + required):88 Swift writes explicit `NSNull()`, omission fails validation. `phase` is the89 exception (optional) so ASR artifacts keep their v1 shape.90- ASR hyp-identity in `diff` uses the language normalizer; text-generation uses91 NFC-exact comparison — deterministic decoding makes exact identity the gate.92- Hardware keys the identity: off macOS the collector can't see the accelerator;93 pass explicit `hardware={"soc": ..., "mem_gb": ...}` (producer warns otherwise).94- Keep contract docs in sync on any schema change: `schemas/README.md`,95 `docs/integration.md`, and the README CLI/task lists.