Skip to content
heliaAOT
Guides
HELIA HUB

Residency reports

Use a residency report to confirm a placement rule or compare two conversions without scraping console output. Start from a configuration that converts and write the module to a directory:

Terminal window
helia-aot convert --path convert.yaml --module.path ./out --memory.dump-residency-json

Success writes <prefix>_residency.json inside out/<module.name>/. For the default names, that is out/helia_aot_nn/aot_residency.json. An archive output contains the report inside the exported module. Keep it with the model hash, configuration and compiler version that produced it.

Every conversion prints an arena summary; --verbose 2 adds planner diagnostics. For tooling, set memory.dump_residency_json: true and the conversion writes <prefix>_residency.json next to the module. This is the report for the fixture model from the committed documentation export:

DTCM
constant, cold44,176 bytes
persistent128 bytes
scratch463,600 bytes
The residency report for the demo fixture model converted for apollo510_evb.

Example source: residency-example.json, exported by the documentation data generator.

View aot_residency.json
aot_residency.json
{
"arenas": {
"constant": [
{
"alignment": 16,
"kind": "cold",
"memory": "dtcm",
"region_id": 2,
"source_memory": "dtcm",
"storage": {
"enum_name": "aot_arena_const_dtcm",
"runtime": {
"data_offset_bytes": 0,
"memory": "dtcm",
"owner": "generated",
"size_bytes": 44176,
"symbol": "aot_arena_const_dtcm__blob"
},
"sidecar": null,
"source": null
},
"tensor_count": 14,
"total_size": 44176,
"used": 44176
}
],
"persistent": [
{
"alignment": 16,
"memory": "dtcm",
"region_id": 1,
"storage": {
"enum_name": "aot_arena_persistent_dtcm",
"runtime": {
"data_offset_bytes": 0,
"memory": "dtcm",
"owner": "generated",
"size_bytes": 128,
"symbol": "aot_arena_persistent_dtcm_buffer"
},
"sidecar": null,
"source": null
},
"total_size": 128,
"used": 128
}
],
"scratch": [
{
"alignment": 16,
"fragmentation": 0,
"memory": "dtcm",
"peak_live": 5696,
"region_id": 0,
"storage": {
"enum_name": "aot_arena_dtcm",
"runtime": {
"data_offset_bytes": 0,
"memory": "dtcm",
"owner": "generated",
"size_bytes": 5696,
"symbol": "aot_arena_dtcm_buffer"
},
"sidecar": null,
"source": null
},
"total_size": 463600,
"used": 5696
}
]
},
"compiler_version": "0.33.0",
"module_prefix": "aot",
"plan_hash": "87f6d61da9206a74",
"schema_version": 5,
"tensor_layout_hash": "b8c2130e3dd06b16",
"tensors": [
{
"id": "0",
"memory": "dtcm",
"offset": 4096,
"role": "scratch",
"size": 768,
"source_memory": "dtcm"
},
{
"id": "1",
"memory": "dtcm",
"offset": 0,
"role": "constant",
"size": 432,
"source_memory": "dtcm"
},
{
"id": "3",
"memory": "dtcm",
"offset": 0,
"role": "scratch",
"size": 4096,
"source_memory": "dtcm"
},
{
"id": "4",
"memory": "dtcm",
"offset": 4096,
"role": "scratch",
"size": 1024,
"source_memory": "dtcm"
},
{
"id": "5",
"memory": "dtcm",
"offset": 624,
"role": "constant",
"size": 9216,
"source_memory": "dtcm"
},
{
"id": "7",
"memory": "dtcm",
"offset": 0,
"role": "scratch",
"size": 4096,
"source_memory": "dtcm"
},
{
"id": "8",
"memory": "dtcm",
"offset": 4096,
"role": "scratch",
"size": 1024,
"source_memory": "dtcm"
},
{
"id": "9",
"memory": "dtcm",
"offset": 10608,
"role": "constant",
"size": 8,
"source_memory": "dtcm"
},
{
"id": "10",
"memory": "dtcm",
"offset": 4096,
"role": "scratch",
"size": 1024,
"source_memory": "dtcm"
},
{
"id": "11",
"memory": "dtcm",
"offset": 10624,
"role": "constant",
"size": 32768,
"source_memory": "dtcm"
},
{
"id": "12",
"memory": "dtcm",
"offset": 43392,
"role": "constant",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "13",
"memory": "dtcm",
"offset": 43520,
"role": "constant",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "14",
"memory": "dtcm",
"offset": 0,
"role": "persistent",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "15",
"memory": "dtcm",
"offset": 256,
"role": "scratch",
"size": 32,
"source_memory": "dtcm"
},
{
"id": "18",
"memory": "dtcm",
"offset": 0,
"role": "scratch",
"size": 10,
"source_memory": "dtcm"
},
{
"id": "19",
"memory": "dtcm",
"offset": 16,
"role": "scratch",
"size": 10,
"source_memory": "dtcm"
},
{
"id": "conv_2d_0_multiplier",
"memory": "dtcm",
"offset": 496,
"role": "constant",
"size": 64,
"source_memory": "dtcm"
},
{
"id": "conv_2d_0_scratch",
"memory": "dtcm",
"offset": 4864,
"role": "scratch",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "conv_2d_0_shift",
"memory": "dtcm",
"offset": 560,
"role": "constant",
"size": 64,
"source_memory": "dtcm"
},
{
"id": "conv_2d_0_weight_sum",
"memory": "dtcm",
"offset": 432,
"role": "constant",
"size": 64,
"source_memory": "dtcm"
},
{
"id": "conv_2d_2_multiplier",
"memory": "dtcm",
"offset": 10096,
"role": "constant",
"size": 256,
"source_memory": "dtcm"
},
{
"id": "conv_2d_2_scratch",
"memory": "dtcm",
"offset": 5120,
"role": "scratch",
"size": 576,
"source_memory": "dtcm"
},
{
"id": "conv_2d_2_shift",
"memory": "dtcm",
"offset": 10352,
"role": "constant",
"size": 256,
"source_memory": "dtcm"
},
{
"id": "conv_2d_2_weight_sum",
"memory": "dtcm",
"offset": 9840,
"role": "constant",
"size": 256,
"source_memory": "dtcm"
},
{
"id": "fully_connected_6_packed",
"memory": "dtcm",
"offset": 43648,
"role": "constant",
"size": 528,
"source_memory": "dtcm"
},
{
"id": "svdf_5_scratch_a",
"memory": "dtcm",
"offset": 0,
"role": "scratch",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "svdf_5_scratch_b",
"memory": "dtcm",
"offset": 128,
"role": "scratch",
"size": 128,
"source_memory": "dtcm"
},
{
"id": "svdf_5_weight_sum",
"memory": "dtcm",
"offset": 43520,
"role": "constant",
"size": 128,
"source_memory": "dtcm"
}
]
}

Read it as three things:

  • used against total_size per arena. The gap is headroom, not waste: used is the emitted arena size, while total_size records the planner’s capacity for that arena. Scratch capacity can include space the plan did not use; constant and persistent envelopes need not have that same headroom. Do not sum total_size across roles as a measure of occupied memory.
  • kind and source_memory on constant arenas, which say whether the weights are cold or staged and where they come from.
  • The two hashes. plan_hash covers the arena envelope only (role, memory, source memory, size, alignment) and mirrors the generated plan-hash macro, so it catches arena ABI drift between separately compiled binaries. tensor_layout_hash covers per-tensor placement, so it catches two tensors swapping offsets inside one arena, which the envelope hash cannot see.

Check schema_version before consuming the report in tooling. The current schema is 5. To check the runtime arenas against a model-specific bank budget, sum used across roles within each memory:

import json
from collections import defaultdict
from pathlib import Path
report = json.loads(Path("out/helia_aot_nn/aot_residency.json").read_text())
if report["schema_version"] != 5:
raise ValueError("Review the residency schema before reading this report")
used = defaultdict(int)
for arenas in report["arenas"].values():
for arena in arenas:
used[arena["memory"]] += arena["used"]
# Example application budget, not a target's total physical capacity.
budgets = {"dtcm": 64 * 1024}
for memory, budget in budgets.items():
if used[memory] > budget:
raise ValueError(f"{memory}: {used[memory]} bytes exceeds {budget}")
print(dict(used))

Passing this check means those runtime arenas fit the stated budgets. It does not account for all firmware storage: include staged source blobs, operator code, stacks, heaps and other application allocations when reviewing the linker map and runtime memory use.

Neither layout hash is a checksum of the weight values. Keep a model-content hash separately; matching layout hashes do not establish matching predictions. Compare reports and linker maps in CI to catch unintended layout changes.

The full field list is in the Generated module reference.

Schema v5 adds compiler_version and a storage object on each arena. Existing v4 fields and both layout hashes keep their meanings. Enable memory.dump_residency_json to export this contract; the offline HTML report embeds the same record. Consumers requiring storage identities should require v5. A consumer supporting v4 may retain its explicit older-version fallback; unknown versions or unmapped memory kinds must produce a diagnostic rather than silently dropping an arena.

  • storage.enum_name is the generated C region enumerator for region_id.
  • storage.runtime describes the kernel-visible backing storage. owner is generated for internal arenas or application for externally bound arenas. symbol is the exact emitted C object name, or null when the application supplies the buffer. It is not the enumerator or a guessed application name.
  • Each storage object has memory, size_bytes, and data_offset_bytes. size_bytes includes any address-zero guard; the usable arena begins at symbol + data_offset_bytes. used remains the usable size. External callers provide at least used bytes with the arena’s alignment; compiler guard bytes are not added to that binding requirement.
  • storage.source is a separate generated source object for staged constants, including in external-arena mode. It is null for other arenas. Its memory can differ from runtime memory; count both when budgeting physical storage.
  • storage.sidecar gives the exact relative filename and size_bytes of the constant binary emitted in external-arena mode. Sidecars contain payload and inter-tensor padding, but no address-zero guard. A staged source sidecar is an additional representation of the generated source, not another runtime allocation. Internal mode has no sidecars.

These are compiler emission identities, not a promise that an optimizer or linker retains every symbol in a final firmware image. Reconcile against the actual linked ELF. Application-owned buffers require application-provided symbol information. Optional generated test-harness example buffers are not module-owned storage. This record does not account for code, stacks, heaps or other firmware allocations and does not define platform memory capacities.