AI Model Packaging — the .alpmodel pipeline
End-to-end walkthrough for turning one source model into a single portable
package that runs on whatever NPU the target SoM happens to carry. You declare
the model once in board.yaml, run alp model build, and ship a fat
.alpmodel — the on-device loader picks the right backend blob at runtime with
no source changes.
This is the build-and-package companion to the runtime
<alp/inference.h> reference. Read that page for the
inference call surface; read this one for how the bytes a model compiles to are
produced and bundled.
:::caution ABI-EXPERIMENTAL
<alp/model.h>, alp_inference_open_alpmodel(), and the .alpmodel container
are [ABI-EXPERIMENTAL]. Pin your SDK to a commit if you depend on them — the
manifest layout and selection semantics can still change.
:::
1. The one thing the SDK can't abstract
The inference C API is uniform across silicon: the same alp_inference_* calls
run on Arm Ethos-U, Renesas DRP-AI3, DEEPX DX-M1, or the TFLM CPU reference. The
model artefact is not uniform — each NPU vendor ships its own offline
compiler that lowers a source model (.tflite, .onnx, …) to that NPU's
instruction set, and the outputs are not interchangeable:
| NPU | Compiler | Produces |
|---|---|---|
| Ethos-U55 / U65 / U85 | vela --accelerator-config ethos-u… | Vela-rewritten .tflite |
| DRP-AI3 (V2N / V2M) | DRP-AI TVM toolchain | DRP-AI object |
| DEEPX DX-M1 (V2M101/102) | dxcom (+ PTQ calibration) | DXNN blob |
| CPU (always available) | TFLM reference / Helium / NEON | plain .tflite |
.alpmodel solves the distribution side of this: it bundles a blob per
backend behind one file plus a capability envelope, so a single artefact is
portable across every NPU a SoM family can drive.
2. What you'll have at the end
- A
board.yamlthat declares your model under amodels:block. - One
.alpmodelper model inbuild/models/, each carrying a blob for every backend whose compile config you supplied (plus a CPU blob for fallback). - Firmware that loads the package with
alp_inference_open_alpmodel()and runs unchanged whether the host is an MCU embedding the bytes or a Linux SoM reading the package from storage.
3. Declare the model in board.yaml
alp model build reads the models: block, derives the target back-ends from
som.sku (you do not list backends by hand), and emits one .alpmodel per
entry. The full field reference lives in
board.yaml → models: block; a representative
entry:
som:
sku: E1M-V2M101 # silicon decides which backends are targeted
preset: e1m-x-evk
models:
- name: mobilenet # names the output: mobilenet.alpmodel
source: models/mobilenet.onnx
compile:
drpai:
spec: models/mobilenet.drpai.yaml # DRP-AI TVM compile spec
deepx_dxm1:
config: models/mobilenet.deepx.json # dxcom per-model JSON
calibration: models/calib/ # PTQ calibration dataset
Ethos-U needs no compile: block — the SDK runs Vela automatically as part
of alp model build. DRP-AI and DEEPX require per-model configuration (a
DRP-AI TVM spec, or a DEEPX JSON config plus a calibration dataset), supplied
under compile: so the build can invoke each vendor toolchain.
A backend with no compile: config is recorded as
coverage: skipped ("no compile config") in the package manifest — the build
still succeeds and emits blobs for every backend that is configured, so a
partial package is a normal, valid outcome while you bring backends online one
at a time.
4. Build the package
alp model build --board board.yaml --out build/models
| Option | Default | What it picks |
|---|---|---|
--board | board.yaml | Path to the board.yaml. |
--out | build/models | Output directory for the .alpmodels. |
--metadata-root | the SDK's metadata/ | Path to the metadata/ root. |
:::note Stage 2 toolchain gating
Real DRP-AI / DEEPX compilation depends on the respective vendor toolchain being
installed. Where a toolchain is absent the build records that backend as
skipped rather than failing, so CI and bring-up stay green. Vela (Ethos-U)
ships with the SDK and always runs.
:::
The result is a fat .alpmodel per model: a 24-byte header + CBOR manifest +
per-backend blobs + a capability requires envelope. The container magic is
"ALPM" (ALP_MODEL_MAGIC), container version 1.
5. Load and run the package
alp_inference_open_alpmodel() loads the package; a selection engine picks the
blob matching the active SoM and returns a handle that works with every
alp_inference_* accessor unchanged (get_input / invoke / get_output /
close — see the inference quick example).
On an MCU — embed the bytes
#include <alp/inference.h>
extern const uint8_t detector_alpmodel[];
extern const size_t detector_alpmodel_len;
alp_inference_t *infer = alp_inference_open_alpmodel(&(alp_model_open_opts_t){
.data = detector_alpmodel,
.size = detector_alpmodel_len,
.backend = ALP_INFERENCE_BACKEND_AUTO, // let the loader pick the best blob
});
if (infer == NULL) {
int err = alp_last_error(); // see the selection table below
return err;
}
On a Linux SoM — load from storage
Point at a storage path instead of embedding the bytes; everything else is identical:
alp_inference_t *infer = alp_inference_open_alpmodel(&(alp_model_open_opts_t){
.path = "/lib/firmware/detector.alpmodel",
.backend = ALP_INFERENCE_BACKEND_AUTO,
});
The full options struct:
typedef struct {
const void *data; // package bytes, or NULL to use `path`
size_t size; // byte count when `data` is set
const char *path; // storage path (Linux), or NULL
alp_inference_backend_t backend; // AUTO, or a forced backend
size_t arena_bytes; // 0 = size from the manifest
void *arena; // caller arena, or NULL for backend default
} alp_model_open_opts_t;
How a blob is selected
The loader picks the blob whose backend is available on the active SoC, whose
silicon_ref is compatible, and that fits the device NPU arena envelope
(ALP_SOC_NPU_ARENA_SRAM_KIB); ties break by the SoM's preferred backend.
backend = AUTO falls back to a CPU blob if one is present; pinning an explicit
NPU backend bypasses the CPU fallback. Failure stamps alp_last_error():
| Code | Value | Meaning |
|---|---|---|
ALP_ERR_VERSION | -11 | Package container version is newer than this loader supports. |
ALP_ERR_NO_BACKEND | -12 | No blob for any backend available on this SoM (and no CPU fallback). |
ALP_ERR_NO_FIT | -13 | A backend matched but no blob fits the device NPU envelope, and no CPU fallback. |
ALP_ERR_NOT_FOUND | -14 | An explicitly-requested backend is absent from the package. |
ALP_ERR_INVAL | -1 | Bad magic / truncated / corrupt package. |
6. Inspecting a package without the loader
Hand-written firmware that wants to read a package directly — to validate it, or
to drive its own dispatch — uses the read-side parser <alp/model.h>, gated on
CONFIG_ALP_SDK_MODEL_READER:
#include <alp/model.h>
alp_model_t m;
alp_status_t rc = alp_model_parse(detector_alpmodel, detector_alpmodel_len, &m);
// rc == ALP_OK; ALP_ERR_INVAL (bad magic / truncated / CBOR error);
// ALP_ERR_VERSION (container newer than this reader).
alp_model_parse() decodes the manifest once into a bounded, stack-friendly
alp_model_t view — no heap, and the blobs reference the source buffer, which
must outlive the view. It carries the model identity plus up to
ALP_MODEL_MAX_TARGETS (8) per-backend target entries (backend,
silicon_ref, blob_format, arena/SRAM requirements, and the blob slice). This
is the standalone-firmware path: no loader, no allocator, just a view over bytes
you already hold.
7. Arena sizing
Right-size the interpreter arena against the model's compiled requirement (the
manifest carries it; Vela's vela-out/network_summary_*.csv reports it for the
Ethos-U path). Round up to the nearest 64 KiB:
cores:
m55_hp:
app: ./src
inference:
default_arena_kib: 512 # 512 KiB; matches MobileNet v2 quant
Leaving arena_bytes = 0 in alp_model_open_opts_t takes the size from the
manifest; pass a non-zero value (or a caller arena) to override.
8. Running two NPUs at once
On a SoM that carries more than one NPU (e.g. V2M101 ships both DRP-AI3 and
DEEPX DX-M1), open multiple handles, each pinned to a backend, and dispatch
to them concurrently. That pattern is driven entirely by the runtime per-handle
backend field — see
concurrent multi-NPU on the inference page.
9. Troubleshooting
alp_inference_open_alpmodelreturns NULL withALP_ERR_NO_BACKEND— the package has no blob for any backend this SoM can drive, and no CPU fallback. Check that the model'scompile:block covers a backend thesom.skuactually has, or add a CPU blob for fallback.ALP_ERR_NO_FIT— a backend matched but its blob needs more NPU arena than the device provides. Re-compile smaller, or bump the arena if the silicon allows.ALP_ERR_NOT_FOUND— you pinned an explicit backend that isn't in the package. Either build that backend's blob (supply itscompile:config) or open withALP_INFERENCE_BACKEND_AUTO.ALP_ERR_VERSION— the package was built by a newer SDK than the loader on the device. Rebuild with the matching SDK, or update the device.- A backend you expected is missing from the package — check the build log
for
coverage: skipped ("no compile config"); that backend had nocompile:entry, or its vendor toolchain wasn't installed at build time.
See also
<alp/inference.h>reference — the runtime call surface and backend selection.- board.yaml →
models:block — the full schema for declaring models. - Realtime Object Detection (DEEPX) — a worked example using the inference path.