Skip to content

Commit c3aa466

Browse files
authored
Merge pull request #4 from raws-labs/hardening/benchmark-provenance
Harden Cortex benchmark provenance
2 parents a48f13d + e315802 commit c3aa466

15 files changed

Lines changed: 1728 additions & 404 deletions

File tree

cortex-m-deployability/BUILD.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ BENCH_MODELS=ds_cnn BENCH_CONFIGS=cmsis_nn scripts/run_all.sh f446 # one cell
3939
This vendors CMSIS (first run), builds each (board, model, config), flashes +
4040
captures each on the SiliconRig remote lab until `BENCH_DONE`, writes
4141
`results/summary.json`, and runs the device-to-device parity gate. Raw serial logs
42-
land in `results/raw/` (gitignored; `summary.json` is the tracked artifact).
42+
land in `results/raw/` (gitignored; `summary.json` is the tracked artifact). Each
43+
raw capture also carries a machine-readable provenance record for the exact
44+
repositories, dependencies, tools, build invocation, model, firmware, and
45+
allocated board. Collection fails if that evidence is missing or incomplete.
4346

4447
## Manual steps
4548

@@ -118,6 +121,7 @@ cat /dev/ttyACM0 > results/raw/pico_ts_cmsis.log # stop at BENCH_DONE
118121
| `BENCH_OPT_LEVEL` | `-O2` | optimization for ALL benchmark code; consistent across boards + matches TFLM's `-O2` kernels (the clean-CMake default would otherwise be `-O0`) |
119122
| `TIGRIS_PLAN` | `ds_cnn_i8.tgrs` | the `.tgrs` plan embedded in flash (compile at a budget that fits the board) |
120123
| `TIGRIS_CODEGEN` | sibling TiGrIS venv CLI | `tigris` executable used to generate the backend deployment core during the build |
124+
| `TIGRIS_RUNTIME_ROOT` | sibling `tigris-runtime` | exact runtime checkout compiled into the firmware; `run_all.sh` binds this to the checkout it validates and records |
121125
| `TIGRIS_FAST_ARENA_BYTES` | `131072` | static fast arena backing store; the harness provisions only a tight slice of it (`peak + align`) so the compactor engages and the measured RAM is the true minimum |
122126
| `TIGRIS_SLOW_ARENA_BYTES` | `262144` | slow arena backing store; use `8192` on the F446 |
123127
| `TFLM_MODEL` | `ds_cnn` | TFLM model: `<name>_tflite_i8.h` in `MODELS_DIR` |
@@ -135,14 +139,15 @@ cat /dev/ttyACM0 > results/raw/pico_ts_cmsis.log # stop at BENCH_DONE
135139
as its DWT under-counts XIP-stall cycles.)
136140
- **RAM metric:** `sram_peak_bytes` is the MEASURED runtime working set, directly
137141
comparable to TFLM's `arena_used_bytes`. For TiGrIS it is the fast + slow arena
138-
high-water + CMSIS-NN scratch + the runtime tensor table; the harness provisions
139-
a tight arena (`peak + alignment`) so the executor's reactive compactor engages
140-
and the figure is the true minimum, not a lazy bump high-water. (Provision a
141-
generous arena and the same model reports far more RAM - that is a measurement
142-
artifact, not a real cost.)
143-
- **Vendor pins** (`fetch.sh`): CMSIS-NN `6d9d61d8` (full SHA, asserted after
144-
checkout; matches TFLM's bundled pin), cmsis-device-h7 `master`, cmsis-device-f4
145-
`3c77349`. Override via `DEV_*_REF`.
142+
high-water + CMSIS-NN scratch + the runtime tensor table + the caller-owned
143+
executor workspace; the harness provisions a tight arena (`peak + alignment`)
144+
so the executor's reactive compactor engages and the figure is the true
145+
minimum, not a lazy bump high-water. (Provision a generous arena and the same
146+
model reports far more RAM - that is a measurement artifact, not a real cost.)
147+
- **Vendor pins** (`fetch.sh`): CMSIS-NN `6d9d61d8` (matches TFLM's bundled
148+
pin), CMSIS-Core `45dab712`, cmsis-device-h7 `de8243d2`,
149+
cmsis-device-f4 `3c77349`, and TFLM `074b75f8`. Every checkout is asserted
150+
against its full commit. RP2350 runs likewise assert pico-sdk `a1438dff`.
146151
- **Parity** is checked device-to-device by `scripts/validate_accuracy.py` (reads
147152
`results/summary.json` by default, which carries each cell's `OUTPUT_I8`; pass a
148153
`results/raw/` dir to read logs instead). It groups by (board, model) and compares

cortex-m-deployability/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ set(TIGRIS_FAST_ARENA_BYTES "131072" CACHE STRING "fast arena size (bytes)")
1313
set(TIGRIS_SLOW_ARENA_BYTES "262144" CACHE STRING "slow arena size (bytes)")
1414
set(TIGRIS_CODEGEN "${CMAKE_CURRENT_SOURCE_DIR}/../../tigris/.venv/bin/tigris"
1515
CACHE FILEPATH "TiGrIS codegen executable")
16+
set(TIGRIS_RUNTIME_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../tigris-runtime"
17+
CACHE PATH "pinned TiGrIS runtime checkout")
1618
# Optimization level for ALL benchmark code (TiGrIS runtime, CMSIS-NN, harness, TFLM
1719
# glue). MUST be consistent across boards and frameworks for a fair comparison: TFLM's
1820
# own kernels build at -O2, and without this the clean-CMake path defaults to -O0 (no
@@ -50,7 +52,7 @@ if(BENCH_FRAMEWORK STREQUAL "tigris")
5052
add_subdirectory(third_party) # cmsis-nn static lib
5153

5254
set(TIGRIS_HAS_CMSIS_NN ON CACHE BOOL "" FORCE)
53-
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../tigris-runtime
55+
add_subdirectory(${TIGRIS_RUNTIME_ROOT}
5456
${CMAKE_BINARY_DIR}/tigris_runtime)
5557

5658
if(BENCH_KERNEL STREQUAL "cmsis_nn")

cortex-m-deployability/README.md

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ ties differently from on-device CMSIS-NN, so it is not used as a baseline. All
2323
boards build at `-O2`; each runs at its rated clock, verified per run by a clock
2424
guard in `results.py`. Latency is the median of 30 runs (DWT cycle counter on
2525
M7/M4, hardware `time_us` on M33). RAM is the activation working set: TiGrIS
26-
`sram_peak` (fast-arena peak + slow-pool spill + scratch + tensor table) against
27-
TFLM `arena_used`; weights live in flash and are excluded, as is stack. Captured
28-
remotely on a SiliconRig hardware-in-the-loop lab via `scripts/run_all.sh`.
26+
`sram_peak` (fast-arena peak + slow-pool spill + scratch + tensor table +
27+
executor workspace) against TFLM `arena_used`; weights live in flash and are
28+
excluded, as is stack. Captured remotely on a SiliconRig hardware-in-the-loop
29+
lab via `scripts/run_all.sh`.
2930

3031
For each TiGrIS cell, CMake runs `tigris codegen <plan> --backend
3132
{cmsis-nn|reference} --format core`. The generated, backend-specific deployment
@@ -46,44 +47,42 @@ int8 weights plus biases, measured from the committed plans.
4647

4748
| Framework | Kernel | Latency | Cycles | RAM (work. set) | Flash (firmware) |
4849
|---|---|---|---|---|---|
49-
| TiGrIS | cmsis_nn | 11.21 ms | 5.38 M | 16.8 KB | 108 KB |
50-
| TFLM | cmsis_nn | 12.80 ms | 6.14 M | 22.2 KB | 166 KB |
51-
| TiGrIS | s8_ref | 63.12 ms | 30.30 M | 16.7 KB | 84 KB |
50+
| TiGrIS | cmsis_nn | 11.14 ms | 5.35 M | 25.5 KB | 118 KB |
51+
| TFLM | cmsis_nn | 12.80 ms | 6.14 M | 22.2 KB | 176 KB |
52+
| TiGrIS | s8_ref | 81.96 ms | 39.34 M | 25.3 KB | 93 KB |
5253

5354
**Anomaly detection:**
5455

5556
| Framework | Kernel | Latency | Cycles | RAM (work. set) | Flash (firmware) |
5657
|---|---|---|---|---|---|
57-
| TiGrIS | cmsis_nn | 1.19 ms | 569 K | 2.7 KB | 361 KB |
58-
| TFLM | cmsis_nn | 1.16 ms | 558 K | 15.5 KB | 408 KB |
59-
| TiGrIS | s8_ref | 3.02 ms | 1.45 M | 2.7 KB | 337 KB |
58+
| TiGrIS | cmsis_nn | 1.19 ms | 570 K | 11.6 KB | 370 KB |
59+
| TFLM | cmsis_nn | 1.16 ms | 558 K | 15.5 KB | 417 KB |
60+
| TiGrIS | s8_ref | 3.05 ms | 1.46 M | 11.4 KB | 346 KB |
6061

6162
**Timeseries:**
6263

6364
| Framework | Kernel | Latency | Cycles | RAM (work. set) | Flash (firmware) |
6465
|---|---|---|---|---|---|
65-
| TiGrIS | cmsis_nn | 0.306 ms | 147 K | 1.9 KB | 77 KB |
66-
| TFLM | cmsis_nn | 0.345 ms | 166 K | 2.9 KB | 135 KB |
67-
| TiGrIS | s8_ref | 1.13 ms | 544 K | 2.0 KB | 53 KB |
66+
| TiGrIS | cmsis_nn | 0.298 ms | 143 K | 11.4 KB | 87 KB |
67+
| TFLM | cmsis_nn | 0.345 ms | 166 K | 2.9 KB | 145 KB |
68+
| TiGrIS | s8_ref | 1.61 ms | 774 K | 10.7 KB | 62 KB |
6869

6970
- Output is bit-exact device-to-device: every (model, framework, kernel) cell
7071
emits the identical INT8 vector (max abs diff 0), checked by
7172
`scripts/validate_accuracy.py`.
72-
- CMSIS-NN vs the portable reference kernels (same model, both `-O2`): 5.6x
73-
(DS-CNN) / 2.5x (AD) / 3.7x (TS) on the M7.
73+
- CMSIS-NN vs the portable reference kernels (same model, both `-O2`): 7.4x
74+
(DS-CNN) / 2.6x (AD) / 5.4x (TS) on the M7.
7475
- Cycles are clock-independent; ms is at 480 MHz.
75-
- Latency, cycles, and RAM come from the tracked `results/summary.json`. The
76-
flash column is a build-time `arm-none-eabi-size` measurement that
77-
`summary.json` does not carry, so it is not refreshed by a rerun; these values
78-
are from the 2026-06-27 rig build and predate the codegen-core harness.
76+
- Latency, cycles, RAM, and firmware sizes come from the same tracked,
77+
provenance-bearing run in `results/summary.json`.
7978

8079
## NUCLEO-F446RE (Cortex-M4F @ 180 MHz)
8180

8281
| Model | TiGrIS cmsis | TFLM cmsis | TiGrIS s8 | RAM (TiGrIS / TFLM) |
8382
|---|---|---|---|---|
84-
| TS | 1.56 ms | 1.80 ms | 11.00 ms | 1.9 / 2.9 KB |
85-
| AD | 4.97 ms | 4.82 ms | 15.11 ms | 2.7 / 15.5 KB |
86-
| DS-CNN | 59.90 ms | 68.19 ms | 513.20 ms | 16.8 / 22.2 KB |
83+
| TS | 1.56 ms | 1.80 ms | 8.41 ms | 11.4 / 2.9 KB |
84+
| AD | 4.97 ms | 4.82 ms | 16.53 ms | 11.6 / 15.5 KB |
85+
| DS-CNN | 63.53 ms | 68.19 ms | 467.58 ms | 25.5 / 22.2 KB |
8786

8887
Output is byte-identical to the H753 (same weights, two architectures). The
8988
128 KB SRAM holds every model.
@@ -96,11 +95,11 @@ byte-identical to the H753 and F446. Weights are read from QSPI flash via XIP.
9695

9796
| Model | TiGrIS cmsis | TiGrIS s8 | RAM |
9897
|---|---|---|---|
99-
| TS | 2.73 ms | 9.02 ms | 1.9 KB |
100-
| AD | 35.08 ms | 44.52 ms | 2.7 KB |
101-
| DS-CNN | 62.94 ms | 412.77 ms | 16.8 KB |
98+
| TS | 2.68 ms | 8.37 ms | 11.4 KB |
99+
| AD | 35.01 ms | 44.56 ms | 11.6 KB |
100+
| DS-CNN | 67.83 ms | 412.06 ms | 25.5 KB |
102101

103-
The FC-heavy AD is slower here (35.08 ms vs 4.97 ms on the F446): each of its
102+
The FC-heavy AD is slower here (35.01 ms vs 4.97 ms on the F446): each of its
104103
265 KB of weights is read once per inference from XIP flash with no reuse, so it
105104
is QSPI-bandwidth-bound. The conv models reuse weights across spatial positions
106105
and stay fast.
@@ -109,39 +108,41 @@ and stay fast.
109108

110109
MobileNetV2 (alpha 0.35, 224x224, INT8, 591 KB weights, 52 convs with
111110
inverted-residual ADD skips) has a naive activation peak of 735 KB, larger than
112-
any of these boards' SRAM. TiGrIS tiles it to a 300 KB working set (129 KB fast +
113-
172 KB slow-pool spill, 2 tiled stages), with bit-exact output across boards.
111+
any of these boards' SRAM. TiGrIS tiles it to a 307.7 KB working set (127.1 KB
112+
fast + 171.5 KB slow-pool spill + 9.1 KB scratch/runtime metadata, 2 tiled
113+
stages), with bit-exact output across boards.
114114

115115
| Board (SRAM) | TiGrIS (tiled) | TFLM (no tiling) |
116116
|---|---|---|
117-
| H753ZI (512 KB) | runs, 1.24 s, 300 KB | OOM at AllocateTensors |
118-
| RP2350 (520 KB) | runs, 6.55 s, 300 KB | n/a (no M33 lib) |
117+
| H753ZI (512 KB) | runs, 1.43 s, 307.7 KB | OOM at AllocateTensors |
118+
| RP2350 (520 KB) | runs, 7.03 s, 307.7 KB | n/a (no M33 lib) |
119119
| F446RE (128 KB) | does not fit | does not fit |
120120

121121
- On the H753, TFLM given a 480 KB arena (nearly all of the 512 KB SRAM) fails
122122
`AllocateTensors` with `ARENA_TOO_SMALL`: with no tiling it needs the full
123123
735 KB. TiGrIS runs the identical model on the same board.
124124
- The F446 cannot hold MobileNetV2: the 591 KB weight blob exceeds its 512 KB
125125
flash, and the 300 KB tiled working set exceeds its 128 KB SRAM.
126-
- RP2350 is ~5.3x slower than the H753 on this model (XIP-bound: 591 KB of
126+
- RP2350 is ~4.9x slower than the H753 on this model (XIP-bound: 591 KB of
127127
weights streamed from QSPI flash each inference, plus the lower clock).
128128

129129
## Reproduce
130130

131131
`SRIG_API_KEY=... ./scripts/run_all.sh` builds every cell, flashes and captures
132132
on the SiliconRig lab, aggregates, and runs the parity gate. Raw serial logs land
133133
in `results/raw/` (gitignored); the tracked artifact is `results/summary.json`,
134-
which carries each cell's numbers and `OUTPUT_I8` vector. See `BUILD.md` for the
135-
build knobs and a locally-attached-board (no-rig) path.
134+
which carries each cell's numbers, `OUTPUT_I8` vector, and deduplicated execution
135+
provenance. See `BUILD.md` for the build knobs and a locally-attached-board
136+
(no-rig) path.
136137

137138
`results/provenance.json` binds that summary and its collector/validator to
138-
SHA-256 digests, records the paths and hashes of all 27 source captures, and
139-
distinguishes declared dependency pins from unavailable execution provenance.
140-
The captures are not shipped in a clone, but their hashes are retained so an
141-
obtained capture set can be checked. Fields marked `unknown` are intentionally
142-
unresolved rather than reconstructed from commit dates or current sibling
143-
checkouts. Host validation always checks tracked artifact/tool hashes and also
144-
checks source-capture hashes when those gitignored files are present. When the
145-
complete capture set is available, it reruns the collector in a temporary
146-
directory and requires byte-identical `summary.json` output; a clean clone
147-
skips that reconstruction and does not report it as verified.
139+
SHA-256 digests and records the paths and hashes of all 27 source captures. The
140+
summary embeds the actual compiler, runtime, TFLM, CMSIS and SDK revisions;
141+
tool and Python-environment versions; build invocations; model and firmware
142+
hashes/sizes; capture timestamps; and SiliconRig board identities. The captures
143+
are not shipped in a clone, but their hashes are retained so an obtained capture
144+
set can be checked. Host validation always checks tracked artifact/tool hashes
145+
and also checks source-capture hashes when those gitignored files are present.
146+
When the complete capture set is available, it reruns the collector in a
147+
temporary directory and requires byte-identical `summary.json` output; a clean
148+
clone skips that reconstruction and does not report it as verified.

cortex-m-deployability/boards/pico2_rp2350/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ project(tigris_pico_bench C CXX ASM)
2323
pico_sdk_init()
2424

2525
set(BENCH_ROOT ${CMAKE_CURRENT_LIST_DIR}/../..)
26-
set(RT_DIR ${BENCH_ROOT}/../../tigris-runtime)
26+
set(TIGRIS_RUNTIME_ROOT "${BENCH_ROOT}/../../tigris-runtime"
27+
CACHE PATH "pinned TiGrIS runtime checkout")
28+
set(RT_DIR ${TIGRIS_RUNTIME_ROOT})
2729
set(NN_DIR ${BENCH_ROOT}/third_party/CMSIS-NN)
2830

2931
set(BENCH_KERNEL "cmsis_nn" CACHE STRING "TiGrIS backend: cmsis_nn | s8_ref")

cortex-m-deployability/harness/main.c

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ extern const unsigned int g_tigris_plan_len;
6363
static uint8_t s_fast_arena[TIGRIS_FAST_ARENA_BYTES] __attribute__((aligned(16)));
6464
static uint8_t s_slow_arena[TIGRIS_SLOW_ARENA_BYTES] __attribute__((aligned(16)));
6565
static void *s_tensor_ptrs[BENCH_MAX_TENSORS];
66+
static tigris_executor_workspace_t s_executor_workspace;
6667

6768
static const char *kernel_name(void)
6869
{
@@ -130,10 +131,12 @@ static uint32_t run_once(tigris_plan_t *plan, tigris_mem_t *mem,
130131
tigris_exec_stats_t stats;
131132
uint32_t c0 = platform_cycles();
132133
#ifdef BENCH_PROFILE_OPS
133-
tigris_exec_error_t err = tigris_run(plan, mem, dispatch, NULL, &stats);
134+
tigris_exec_error_t err = tigris_run_with_workspace(
135+
plan, mem, dispatch, NULL, &stats, &s_executor_workspace);
134136
#else
135137
(void)dispatch; /* generated core owns the normal dispatch call. */
136-
tigris_exec_error_t err = tigris_codegen_run(plan, mem, &stats);
138+
tigris_exec_error_t err = tigris_codegen_run(
139+
plan, mem, &stats, &s_executor_workspace);
137140
#endif
138141
uint32_t c1 = platform_cycles();
139142

@@ -268,7 +271,14 @@ int main(void)
268271
} else {
269272
base = TIGRIS_FAST_ARENA_BYTES;
270273
}
271-
uint32_t fast_size = base + align_reserve + tigris_weight_decompression_overhead(&plan);
274+
uint32_t fast_size = base + align_reserve
275+
+ tigris_weight_decompression_overhead(&plan);
276+
#if defined(BENCH_KERNEL_CMSIS_NN)
277+
/* The plan budget covers activations and transient weights, not backend
278+
* scratch. CMSIS-NN reserves its exact scratch requirement from the same
279+
* physical backing store before inference. */
280+
fast_size = tigris_cmsis_nn_fast_arena_required(&plan);
281+
#endif
272282
#ifdef TIGRIS_FAST_OVERRIDE
273283
/* Diagnostic: force a specific fast-arena size to probe the runtime minimum. */
274284
if (TIGRIS_FAST_OVERRIDE)
@@ -400,13 +410,16 @@ int main(void)
400410
* framework needs in SRAM at runtime, not a compile-time estimate.
401411
* act_peak = fast + slow arena high-water (the live activation set)
402412
* scratch = CMSIS-NN scratch carved from the fast arena (0 for s8_ref)
403-
* meta = the runtime tensor-pointer table (TiGrIS's RAM metadata;
404-
* TFLM keeps its equivalent TfLiteEvalTensor table inside
405-
* the arena, so arena_used already counts it - we add ours)
413+
* meta = the runtime tensor-pointer table plus caller-owned
414+
* executor workspace (TFLM keeps equivalent metadata
415+
* inside its arena, so arena_used already counts it)
406416
* plan.header->peak (the old reported value) is the compiler's compile-
407417
* time activation estimate; kept as plan_act_peak_bytes for reference. */
408418
uint32_t scratch_bytes = fast_size - mem.fast_size; /* prepare() carve; 0 if none */
409-
uint32_t meta_bytes = (uint32_t)plan.header->num_tensors * (uint32_t)sizeof(void *);
419+
uint32_t tensor_table_bytes =
420+
(uint32_t)plan.header->num_tensors * (uint32_t)sizeof(void *);
421+
uint32_t executor_workspace_bytes = (uint32_t)sizeof(s_executor_workspace);
422+
uint32_t meta_bytes = tensor_table_bytes + executor_workspace_bytes;
410423
uint32_t fast_peak = mem.fast_peak;
411424
uint32_t slow_peak = last_stats.slow_peak;
412425
uint32_t act_peak = fast_peak + slow_peak;
@@ -439,6 +452,7 @@ int main(void)
439452
"sram_slow_peak_bytes=%lu,"
440453
"sram_scratch_bytes=%lu,"
441454
"sram_meta_bytes=%lu,"
455+
"sram_executor_workspace_bytes=%lu,"
442456
"plan_act_peak_bytes=%lu,"
443457
"sram_budget_kb=%lu,"
444458
"sram_provisioned_kb=%lu,"
@@ -467,6 +481,7 @@ int main(void)
467481
(unsigned long)slow_peak,
468482
(unsigned long)scratch_bytes,
469483
(unsigned long)meta_bytes,
484+
(unsigned long)executor_workspace_bytes,
470485
(unsigned long)plan.header->peak,
471486
(unsigned long)(plan.header->budget / 1024),
472487
(unsigned long)(fast_size / 1024),

0 commit comments

Comments
 (0)