Skip to main content

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:

NPUCompilerProduces
Ethos-U55 / U65 / U85vela --accelerator-config ethos-u…Vela-rewritten .tflite
DRP-AI3 (V2N / V2M)DRP-AI TVM toolchainDRP-AI object
DEEPX DX-M1 (V2M101/102)dxcom (+ PTQ calibration)DXNN blob
CPU (always available)TFLM reference / Helium / NEONplain .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.yaml that declares your model under a models: block.
  • One .alpmodel per model in build/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
OptionDefaultWhat it picks
--boardboard.yamlPath to the board.yaml.
--outbuild/modelsOutput directory for the .alpmodels.
--metadata-rootthe 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():

CodeValueMeaning
ALP_ERR_VERSION-11Package container version is newer than this loader supports.
ALP_ERR_NO_BACKEND-12No blob for any backend available on this SoM (and no CPU fallback).
ALP_ERR_NO_FIT-13A backend matched but no blob fits the device NPU envelope, and no CPU fallback.
ALP_ERR_NOT_FOUND-14An explicitly-requested backend is absent from the package.
ALP_ERR_INVAL-1Bad 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_alpmodel returns NULL with ALP_ERR_NO_BACKEND — the package has no blob for any backend this SoM can drive, and no CPU fallback. Check that the model's compile: block covers a backend the som.sku actually 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 its compile: config) or open with ALP_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 no compile: entry, or its vendor toolchain wasn't installed at build time.

See also

Questions about this page? Discuss in Community Forum