Skip to content

Commit b898214

Browse files
committed
Pin coordinated core revisions for benchmarks
1 parent 2255126 commit b898214

10 files changed

Lines changed: 334 additions & 7 deletions

File tree

.github/workflows/host-validation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ jobs:
6262
- name: Validate tracked JSON artifacts
6363
run: python -W error tests/validate_tracked_json.py
6464

65+
- name: Validate exact TiGrIS core pins
66+
run: python -W error scripts/check_core_versions.py --manifest-only
67+
6568
- name: Validate tracked Cortex-M output parity
6669
working-directory: cortex-m-deployability
6770
run: >-

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
Each benchmark suite is self-contained and lives in its own subdirectory: model preparation on the host, a device harness, and scripts that collect machine-parseable results. Anyone with the matching hardware should be able to reproduce any number end-to-end.
99

10+
Canonical device runs use the exact compiler and runtime commits recorded in
11+
[`core-versions.json`](core-versions.json). Both hardware orchestrators verify
12+
the sibling checkouts, accepted plan schema, and clean tracked state before
13+
building or flashing. A development-only run may set
14+
`TIGRIS_ALLOW_UNPINNED_CORE=1`, but results from that override are not canonical
15+
until their exact revisions are recorded and validated.
16+
1017
## How it is organized
1118

1219
Every suite follows the same three-step shape:
@@ -20,8 +27,8 @@ Suites are grouped by what they are measuring (e.g. latency against a peer frame
2027
## Quick start
2128

2229
```bash
23-
# Install the TiGrIS toolchain (used by every suite's model preparation)
24-
pip install tigris-ml
30+
# Prepare sibling compiler/runtime checkouts at the commits in core-versions.json
31+
python scripts/check_core_versions.py
2532

2633
# Enter the suite you want to run, then follow its README
2734
cd <suite>/

core-versions.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"format_version": 1,
3+
"profile": "coordinated-develop-2026-07-15",
4+
"plan_schema": 4,
5+
"compatibility_manifest": {
6+
"repository": "https://github.com/raws-labs/tigris.git",
7+
"path": "compatibility.json",
8+
"commit": "f8a188d5bbf0d9a7f6dc9b1bf35836b4b9bffaf1"
9+
},
10+
"compiler": {
11+
"repository": "https://github.com/raws-labs/tigris.git",
12+
"branch": "develop",
13+
"commit": "f8a188d5bbf0d9a7f6dc9b1bf35836b4b9bffaf1",
14+
"emits_schema": 4
15+
},
16+
"runtime": {
17+
"repository": "https://github.com/raws-labs/tigris-runtime.git",
18+
"branch": "develop",
19+
"commit": "89170124df0b67a57d9c4322645b92bd4e79c387",
20+
"accepts_schemas": [2, 3, 4]
21+
}
22+
}

cortex-m-deployability/BUILD.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ pip install numpy rich
2222
`SRIG_API_KEY` in the environment (and `SRIG_BASE_URL` if self-hosted). To
2323
reproduce on a locally-attached board instead, see "Manual steps" below.
2424

25+
The compiler and runtime must be sibling checkouts at the exact commits in
26+
`../core-versions.json`. `run_all.sh` checks them before building. Set
27+
`TIGRIS_ALLOW_UNPINNED_CORE=1` only for an explicitly non-canonical development
28+
run; the resulting revisions must be pinned before any summary is promoted.
29+
2530
## One-shot: build, flash, capture, validate the whole matrix
2631

2732
```bash

cortex-m-deployability/scripts/run_all.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,26 @@ set -euo pipefail
2121
: "${SRIG_API_KEY:?set SRIG_API_KEY (SiliconRig auth) before running}"
2222

2323
HERE="$(cd "$(dirname "$0")/.." && pwd)"
24-
TC="$(cd "$HERE/../../tigris-runtime" && pwd)/cmake/arm-none-eabi.cmake"
24+
TIGRIS_COMPILER_ROOT="${TIGRIS_COMPILER_ROOT:-$(cd "$HERE/../../tigris" && pwd)}"
25+
TIGRIS_RUNTIME_ROOT="${TIGRIS_RUNTIME_ROOT:-$(cd "$HERE/../../tigris-runtime" && pwd)}"
26+
TC="$TIGRIS_RUNTIME_ROOT/cmake/arm-none-eabi.cmake"
2527
MODELS_DIR="$(cd "$HERE/../tflm-esp32s3/models/output" && pwd)"
2628
PLAN_DIR="${TIGRIS_PLAN_DIR:-$HERE/build/plans}"
27-
TIGRIS_COMPILER="${TIGRIS_COMPILER:-$HERE/../../tigris/.venv/bin/tigris}"
29+
TIGRIS_COMPILER="${TIGRIS_COMPILER:-$TIGRIS_COMPILER_ROOT/.venv/bin/tigris}"
2830
RAW="$HERE/results/raw"
2931
PICO_SDK="${PICO_SDK_PATH:-$HOME/pico/pico-sdk}"
3032
PICOTOOL="${PICOTOOL_DIR:-$HOME/pico/picotool/install/lib/cmake/picotool}"
3133
NPROC="$(nproc)"
3234

35+
CORE_CHECK_ARGS=(
36+
--compiler-root "$TIGRIS_COMPILER_ROOT"
37+
--runtime-root "$TIGRIS_RUNTIME_ROOT"
38+
)
39+
if [ "${TIGRIS_ALLOW_UNPINNED_CORE:-0}" = 1 ]; then
40+
CORE_CHECK_ARGS+=(--allow-unpinned)
41+
fi
42+
python3 "$HERE/../scripts/check_core_versions.py" "${CORE_CHECK_ARGS[@]}"
43+
3344
BOARDS=("$@"); [ "${#BOARDS[@]}" -eq 0 ] && BOARDS=(h753 f446 rp2350)
3445
read -r -a MODELS <<< "${BENCH_MODELS:-ds_cnn ad ts mbv2}"
3546
read -r -a CONFIGS <<< "${BENCH_CONFIGS:-cmsis_nn s8_ref tflm}"

scripts/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Clone-local benchmark orchestration helpers."""

scripts/check_core_versions.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env python3
2+
"""Fail closed when a canonical benchmark uses unpinned TiGrIS core sources."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import re
9+
import subprocess
10+
from pathlib import Path
11+
12+
13+
ROOT = Path(__file__).resolve().parents[1]
14+
COMMIT_RE = re.compile(r"[0-9a-f]{40}")
15+
RUNTIME_SCHEMA_RE = re.compile(
16+
r"^#define\s+TIGRIS_SCHEMA_VERSION(?:_V\d+)?\s+(\d+)\s*$",
17+
re.MULTILINE,
18+
)
19+
20+
21+
def _schema_list(value: object, label: str, errors: list[str]) -> list[int]:
22+
if not isinstance(value, list) or not value:
23+
errors.append(f"{label} must be a non-empty list")
24+
return []
25+
if any(not isinstance(item, int) or item < 1 for item in value):
26+
errors.append(f"{label} must contain positive integers")
27+
return []
28+
result = list(value)
29+
if result != sorted(set(result)):
30+
errors.append(f"{label} must be sorted and duplicate-free")
31+
return result
32+
33+
34+
def validate_manifest(document: object) -> list[str]:
35+
errors: list[str] = []
36+
if not isinstance(document, dict):
37+
return ["top level must be an object"]
38+
if document.get("format_version") != 1:
39+
errors.append("format_version must be 1")
40+
if not isinstance(document.get("profile"), str) or not document["profile"]:
41+
errors.append("profile must be a non-empty string")
42+
plan_schema = document.get("plan_schema")
43+
if not isinstance(plan_schema, int) or plan_schema < 1:
44+
errors.append("plan_schema must be a positive integer")
45+
46+
compatibility = document.get("compatibility_manifest")
47+
compiler = document.get("compiler")
48+
runtime = document.get("runtime")
49+
if not all(isinstance(item, dict) for item in (compatibility, compiler, runtime)):
50+
return errors + [
51+
"compatibility_manifest, compiler, and runtime must be objects"
52+
]
53+
assert isinstance(compatibility, dict)
54+
assert isinstance(compiler, dict)
55+
assert isinstance(runtime, dict)
56+
57+
for label, component in (
58+
("compatibility_manifest", compatibility),
59+
("compiler", compiler),
60+
("runtime", runtime),
61+
):
62+
commit = component.get("commit")
63+
if not isinstance(commit, str) or not COMMIT_RE.fullmatch(commit):
64+
errors.append(f"{label}.commit must be a full Git SHA")
65+
repository = component.get("repository")
66+
if not isinstance(repository, str) or not repository.startswith(
67+
"https://github.com/raws-labs/"
68+
):
69+
errors.append(f"{label}.repository must be a RAWS Labs HTTPS URL")
70+
71+
if compatibility.get("commit") != compiler.get("commit"):
72+
errors.append("compatibility manifest must come from the pinned compiler")
73+
if compatibility.get("path") != "compatibility.json":
74+
errors.append("compatibility_manifest.path must be compatibility.json")
75+
for label, component in (("compiler", compiler), ("runtime", runtime)):
76+
if component.get("branch") != "develop":
77+
errors.append(f"{label}.branch must be develop")
78+
79+
emitted = compiler.get("emits_schema")
80+
accepted = _schema_list(
81+
runtime.get("accepts_schemas"), "runtime.accepts_schemas", errors
82+
)
83+
if emitted != plan_schema:
84+
errors.append("compiler schema must match plan_schema")
85+
if isinstance(emitted, int) and emitted not in accepted:
86+
errors.append("pinned runtime does not accept the compiler schema")
87+
return errors
88+
89+
90+
def _git(path: Path, *args: str) -> str:
91+
completed = subprocess.run(
92+
["git", "-C", str(path), *args],
93+
text=True,
94+
capture_output=True,
95+
check=False,
96+
)
97+
if completed.returncode != 0:
98+
raise RuntimeError(completed.stderr.strip() or "git command failed")
99+
return completed.stdout.strip()
100+
101+
102+
def validate_checkout(
103+
document: dict[str, object], compiler_root: Path, runtime_root: Path
104+
) -> list[str]:
105+
errors: list[str] = []
106+
for label, path, expected in (
107+
("compiler", compiler_root, document["compiler"]),
108+
("runtime", runtime_root, document["runtime"]),
109+
):
110+
assert isinstance(expected, dict)
111+
try:
112+
actual = _git(path, "rev-parse", "HEAD")
113+
dirty = _git(path, "status", "--porcelain", "--untracked-files=no")
114+
except RuntimeError as exc:
115+
errors.append(f"cannot inspect {label} checkout {path}: {exc}")
116+
continue
117+
if actual != expected["commit"]:
118+
errors.append(
119+
f"{label} HEAD {actual} does not match pin {expected['commit']}"
120+
)
121+
if dirty:
122+
errors.append(f"{label} checkout has tracked modifications")
123+
124+
compatibility_path = compiler_root / "compatibility.json"
125+
try:
126+
compatibility = json.loads(compatibility_path.read_text())
127+
except (OSError, json.JSONDecodeError) as exc:
128+
errors.append(f"cannot read compiler compatibility manifest: {exc}")
129+
else:
130+
integration = compatibility.get("integration", {})
131+
compiler = document["compiler"]
132+
runtime = document["runtime"]
133+
assert isinstance(compiler, dict)
134+
assert isinstance(runtime, dict)
135+
if integration.get("compiler_emits_schema") != compiler.get("emits_schema"):
136+
errors.append("compiler checkout compatibility schema disagrees with pin")
137+
if integration.get("runtime_accepts_schemas") != runtime.get(
138+
"accepts_schemas"
139+
):
140+
errors.append("compiler compatibility runtime set disagrees with pin")
141+
142+
header = runtime_root / "include/tigris.h"
143+
try:
144+
accepted = sorted(
145+
{int(value) for value in RUNTIME_SCHEMA_RE.findall(header.read_text())}
146+
)
147+
except OSError as exc:
148+
errors.append(f"cannot read runtime schema header: {exc}")
149+
else:
150+
runtime = document["runtime"]
151+
assert isinstance(runtime, dict)
152+
if accepted != runtime.get("accepts_schemas"):
153+
errors.append(
154+
f"runtime header accepts {accepted}, pin declares "
155+
f"{runtime.get('accepts_schemas')}"
156+
)
157+
return errors
158+
159+
160+
def main() -> int:
161+
parser = argparse.ArgumentParser()
162+
parser.add_argument("--manifest", type=Path, default=ROOT / "core-versions.json")
163+
parser.add_argument("--manifest-only", action="store_true")
164+
parser.add_argument("--compiler-root", type=Path, default=ROOT.parent / "tigris")
165+
parser.add_argument(
166+
"--runtime-root", type=Path, default=ROOT.parent / "tigris-runtime"
167+
)
168+
parser.add_argument(
169+
"--allow-unpinned",
170+
action="store_true",
171+
help="warn instead of failing checkout mismatches for development runs",
172+
)
173+
args = parser.parse_args()
174+
try:
175+
document = json.loads(args.manifest.read_text())
176+
except (OSError, json.JSONDecodeError) as exc:
177+
print(f"ERROR: cannot read {args.manifest}: {exc}")
178+
return 1
179+
errors = validate_manifest(document)
180+
if not errors and not args.manifest_only:
181+
errors.extend(
182+
validate_checkout(
183+
document, args.compiler_root.resolve(), args.runtime_root.resolve()
184+
)
185+
)
186+
if errors and args.allow_unpinned:
187+
for error in errors:
188+
print(f"WARNING: {error}")
189+
print("Development override accepted unpinned core sources.")
190+
return 0
191+
if errors:
192+
for error in errors:
193+
print(f"ERROR: {error}")
194+
return 1
195+
compiler = document["compiler"]
196+
runtime = document["runtime"]
197+
assert isinstance(compiler, dict)
198+
assert isinstance(runtime, dict)
199+
print(
200+
"Pinned TiGrIS core verified: "
201+
f"compiler={compiler['commit']} runtime={runtime['commit']} "
202+
f"schema={document['plan_schema']}"
203+
)
204+
return 0
205+
206+
207+
if __name__ == "__main__":
208+
raise SystemExit(main())

tests/test_core_versions.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python3
2+
"""Mutation tests for exact compiler/runtime benchmark pins."""
3+
4+
from __future__ import annotations
5+
6+
import copy
7+
import json
8+
import unittest
9+
from pathlib import Path
10+
11+
from scripts.check_core_versions import validate_checkout, validate_manifest
12+
13+
14+
ROOT = Path(__file__).resolve().parents[1]
15+
MANIFEST = ROOT / "core-versions.json"
16+
17+
18+
class CoreVersionContractTest(unittest.TestCase):
19+
def setUp(self) -> None:
20+
self.document = json.loads(MANIFEST.read_text())
21+
22+
def test_manifest_is_valid(self) -> None:
23+
self.assertEqual(validate_manifest(self.document), [])
24+
25+
def test_incompatible_schema_is_rejected(self) -> None:
26+
mutated = copy.deepcopy(self.document)
27+
mutated["runtime"]["accepts_schemas"] = [2, 3]
28+
self.assertTrue(
29+
any(
30+
"does not accept" in error
31+
for error in validate_manifest(mutated)
32+
)
33+
)
34+
35+
def test_abbreviated_commit_is_rejected(self) -> None:
36+
mutated = copy.deepcopy(self.document)
37+
mutated["compiler"]["commit"] = "f7f6f42"
38+
self.assertTrue(
39+
any(
40+
"full Git SHA" in error
41+
for error in validate_manifest(mutated)
42+
)
43+
)
44+
45+
def test_sibling_checkouts_match_when_present(self) -> None:
46+
compiler = ROOT.parent / "tigris"
47+
runtime = ROOT.parent / "tigris-runtime"
48+
if not (compiler / ".git").exists() or not (runtime / ".git").exists():
49+
self.skipTest("sibling core checkouts are not present")
50+
self.assertEqual(validate_checkout(self.document, compiler, runtime), [])
51+
52+
53+
if __name__ == "__main__":
54+
unittest.main()

tflm-esp32s3/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,17 @@ ESP32-S3-DevKitC-1 (N16R8): dual Xtensa LX7 at 240 MHz, 512 KB SRAM, 8 MB PSRAM,
3939
Before running the device builds, you also need the TiGrIS C runtime source tree next to this repo. The ESP-IDF components pull headers and source files from it at build time:
4040

4141
```bash
42-
# From the directory that contains tigris-bench
43-
git clone https://github.com/raws-labs/tigris-runtime
42+
# From the directory that contains tigris-bench, check out the exact compiler
43+
# and runtime commits recorded in ../tigris-bench/core-versions.json
44+
python tigris-bench/scripts/check_core_versions.py
4445
```
4546

4647
By default the build looks for `tigris-runtime/` as a sibling of `tigris-bench/`. Override with `-DTIGRIS_RUNTIME_DIR=/path/to/tigris-runtime` on the `idf.py build` invocation if you keep it elsewhere.
4748

49+
The orchestration script refuses mismatched or modified compiler/runtime
50+
checkouts before flashing. `TIGRIS_ALLOW_UNPINNED_CORE=1` is available only for
51+
non-canonical development runs.
52+
4853
## Quick start
4954

5055
### 1. Prepare models (host)

0 commit comments

Comments
 (0)