|
| 1 | +--- |
| 2 | +title: Build the Topo Template from scratch |
| 3 | +weight: 4 |
| 4 | + |
| 5 | +### FIXED, DO NOT MODIFY |
| 6 | +layout: learningpathall |
| 7 | +--- |
| 8 | + |
| 9 | +## Start from the application pieces |
| 10 | + |
| 11 | +The `topo-imx93-npu-deployment` repository is a Compose project with Topo metadata at the root. The Topo-specific part is not a replacement for Compose. The services still describe container builds, dependencies, ports, volumes, and runtime settings. The `x-topo` block adds the metadata Topo uses to identify the Template, check target requirements, and prompt for configuration. |
| 12 | + |
| 13 | +The project has three implementation areas: |
| 14 | + |
| 15 | +- `executorch-runner/`: builds the ExecuTorch `.pte` program and the Cortex-M33 firmware ELF. |
| 16 | +- `webapp/`: builds the Flask application that stages memory and sends `RUN` commands over `RPMsg`. |
| 17 | +- `compose.yaml`: connects the build artifacts, runtime services, Remoteproc Runtime settings, and Topo metadata. |
| 18 | + |
| 19 | +When bootstrapping this Template from scratch, first make the project work as a normal Compose build. Then add the `x-topo` metadata that lets Topo deploy it consistently to an Arm64 target. |
| 20 | + |
| 21 | +## Create the runner build pipeline |
| 22 | + |
| 23 | +The `executorch-runner/Dockerfile` is a multi-stage Dockerfile. It builds two artifacts from one build context: |
| 24 | + |
| 25 | +- `mv2_ethosu65_256.pte`: the MobileNetV2 ExecuTorch program lowered for `ethos-u65-256`. |
| 26 | +- `executorch_runner_cm33.elf`: the Cortex-M33 firmware image loaded by Linux `remoteproc`. |
| 27 | + |
| 28 | +The first half of the Dockerfile builds the model artifact: |
| 29 | + |
| 30 | +```Dockerfile |
| 31 | +FROM build-base AS executorch-base |
| 32 | +... |
| 33 | +FROM executorch-base AS pte-builder |
| 34 | +... |
| 35 | +RUN source /workspace/executorch/examples/arm/arm-scratch/setup_path.sh && \ |
| 36 | + python /usr/local/bin/export_mv2_imx93.py |
| 37 | + |
| 38 | +FROM busybox:1.36 AS pte-artifacts |
| 39 | +COPY --from=pte-builder /workspace/build/mv2-imx93/mv2_ethosu65_256.pte /artifacts/mv2_ethosu65_256.pte |
| 40 | +``` |
| 41 | + |
| 42 | +The second half builds and packages the firmware: |
| 43 | + |
| 44 | +```Dockerfile |
| 45 | +FROM build-base AS runner-base |
| 46 | +ARG MCUXSDK_MANIFEST_URL=https://github.com/nxp-mcuxpresso/mcuxsdk-manifests.git |
| 47 | +ARG MCUXSDK_MANIFEST_REV=v25.09.00 |
| 48 | +... |
| 49 | +FROM runner-base AS runner-builder |
| 50 | +RUN /usr/local/bin/build-runner.sh /artifacts |
| 51 | + |
| 52 | +FROM scratch AS runner-runtime |
| 53 | +COPY --from=runner-builder /artifacts/executorch_runner_cm33.elf /executorch_runner_cm33.elf |
| 54 | +ENTRYPOINT ["/executorch_runner_cm33.elf"] |
| 55 | +``` |
| 56 | + |
| 57 | +The `runner-runtime` stage is intentionally a `scratch` image. The only payload is the ELF file. When the service starts with `runtime: io.containerd.remoteproc.v1`, containerd uses Remoteproc Runtime instead of a normal Linux process runtime. Remoteproc Runtime passes the ELF entrypoint to the Linux `remoteproc` driver, and the `imx-rproc` driver loads and releases the Cortex-M33. |
| 58 | + |
| 59 | +The project also applies patches before building the runner. One patch changes the MCUX SDK RAM linker and startup behavior so initialized data is loaded in-place by `remoteproc` rather than copied from a flash-style load address. The runner patches add RPMsg stability fixes and trace output used by the web application. |
| 60 | + |
| 61 | +## Add artifact-only Compose services |
| 62 | + |
| 63 | +At the root of the Template, create normal Compose services for the build outputs: |
| 64 | + |
| 65 | +```yaml |
| 66 | +services: |
| 67 | + pte-artifacts: |
| 68 | + platform: linux/arm64 |
| 69 | + scale: 0 |
| 70 | + build: |
| 71 | + context: executorch-runner |
| 72 | + dockerfile: Dockerfile |
| 73 | + target: pte-artifacts |
| 74 | + |
| 75 | + runner-artifacts: |
| 76 | + platform: linux/arm64 |
| 77 | + scale: 0 |
| 78 | + build: |
| 79 | + context: executorch-runner |
| 80 | + dockerfile: Dockerfile |
| 81 | + target: runner-artifacts |
| 82 | +``` |
| 83 | +
|
| 84 | +These services are not runtime application containers. `scale: 0` keeps them out of the running deployment while still making their build targets available to the rest of the Compose project. |
| 85 | + |
| 86 | +The web application imports the PTE artifact as a BuildKit additional context: |
| 87 | + |
| 88 | +```yaml |
| 89 | +services: |
| 90 | + webapp: |
| 91 | + platform: linux/arm64 |
| 92 | + build: |
| 93 | + context: . |
| 94 | + dockerfile: Dockerfile |
| 95 | + additional_contexts: |
| 96 | + pte_artifacts: service:pte-artifacts |
| 97 | +``` |
| 98 | + |
| 99 | +The webapp Dockerfile then copies from that context: |
| 100 | + |
| 101 | +```Dockerfile |
| 102 | +COPY --from=pte_artifacts /artifacts/mv2_ethosu65_256.pte /opt/mv2-imx93/mv2_ethosu65_256.pte |
| 103 | +``` |
| 104 | + |
| 105 | +This keeps the model export pipeline separate from the Flask app while still producing one deployable webapp image. |
| 106 | + |
| 107 | +## Add the remote processor service |
| 108 | + |
| 109 | +The Cortex-M33 firmware is represented as another Compose service: |
| 110 | + |
| 111 | +```yaml |
| 112 | +services: |
| 113 | + cm33-runner: |
| 114 | + platform: linux/arm64 |
| 115 | + build: |
| 116 | + context: executorch-runner |
| 117 | + dockerfile: Dockerfile |
| 118 | + target: runner-runtime |
| 119 | + runtime: io.containerd.remoteproc.v1 |
| 120 | + annotations: |
| 121 | + remoteproc.name: imx-rproc |
| 122 | +``` |
| 123 | + |
| 124 | +This is the key heterogeneous deployment hook. The service is still built by Docker, but it is not launched as a Linux userspace process. The `runtime` selects the containerd Remoteproc Runtime shim, and `remoteproc.name: imx-rproc` selects the i.MX 93 remote processor driver. |
| 125 | + |
| 126 | +After this service starts, Linux exposes the RPMsg device used by the Cortex-A web app. The Flask code waits for `/dev/ttyRPMSG*`, writes the `.pte` file to `0xC0000000`, writes the input tensor to `0xC036D000`, sends `RUN\n` over RPMsg, and parses the `CM33:` response lines into top-1 and top-5 ImageNet results. |
| 127 | + |
| 128 | +## Add the web application service |
| 129 | + |
| 130 | +The web application service extends `webapp/compose.yaml` from the root Compose file: |
| 131 | + |
| 132 | +```yaml |
| 133 | +services: |
| 134 | + webapp: |
| 135 | + platform: linux/arm64 |
| 136 | + extends: |
| 137 | + file: webapp/compose.yaml |
| 138 | + service: webapp |
| 139 | + depends_on: |
| 140 | + - cm33-runner |
| 141 | +``` |
| 142 | + |
| 143 | +The extended service is privileged and mounts `/sys` and `/dev`: |
| 144 | + |
| 145 | +```yaml |
| 146 | +services: |
| 147 | + webapp: |
| 148 | + privileged: true |
| 149 | + ports: |
| 150 | + - "${WEBAPP_PORT:-3001}:3000" |
| 151 | + volumes: |
| 152 | + - /sys:/sys |
| 153 | + - /dev:/dev |
| 154 | +``` |
| 155 | + |
| 156 | +Those mounts are required because the app checks `/proc/device-tree`, reads remoteproc state through `/sys/class/remoteproc`, talks to `/dev/ttyRPMSG*`, writes model and tensor data through `/dev/mem`, and checks for `/dev/ethosu0`. |
| 157 | + |
| 158 | +## Add Topo metadata |
| 159 | + |
| 160 | +After the Compose services are in place, add the root-level `x-topo` block: |
| 161 | + |
| 162 | +```yaml |
| 163 | +x-topo: |
| 164 | + name: "i.MX93 ExecuTorch runner" |
| 165 | + description: "Runs a Cortex-A web application that sends image inference commands to a resident CM33 ExecuTorch runner over RPMsg." |
| 166 | + features: |
| 167 | + - "remoteproc-runtime" |
| 168 | +``` |
| 169 | + |
| 170 | +Keep `x-topo` at the root of `compose.yaml`, not under `services`. The `features` entry is what tells Topo this Template needs a target with Remoteproc Runtime support. That is why `topo health` checks for: |
| 171 | + |
| 172 | +```output |
| 173 | +Remoteproc Runtime: ✅ (remoteproc-runtime) |
| 174 | +Remoteproc Shim: ✅ (containerd-shim-remoteproc-v1) |
| 175 | +Subsystem Driver (remoteproc): ✅ (imx-rproc) |
| 176 | +``` |
| 177 | + |
| 178 | +You can also add a deployment success message so users know exactly what to do after deployment: |
| 179 | + |
| 180 | +```yaml |
| 181 | +x-topo: |
| 182 | + deployment_success_message: | |
| 183 | + The i.MX93 ExecuTorch runner is deployed. |
| 184 | + Open http://<target-ip>:3001 and classify an ImageNet image. |
| 185 | +``` |
| 186 | + |
| 187 | +## Expose project configuration |
| 188 | + |
| 189 | +Topo arguments are metadata for project parameters. Compose still carries the values into the build. |
| 190 | + |
| 191 | +The current Template exposes optional cache image parameters: |
| 192 | + |
| 193 | +```yaml |
| 194 | +x-topo: |
| 195 | + args: |
| 196 | + EXECUTORCH_BASE_CACHE_IMAGE: |
| 197 | + description: Optional GHCR image used as a BuildKit cache source for the ExecuTorch PTE build. |
| 198 | + required: false |
| 199 | + default: ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04 |
| 200 | + IMX93_RUNNER_BUILD_CACHE_IMAGE: |
| 201 | + description: Optional GHCR image used as a BuildKit cache source for the CM33 runner build. |
| 202 | + required: false |
| 203 | + default: ghcr.io/arm-examples/topo-imx93-npu-deployment/imx93-runner-build:mcux-v25.09.00-armgcc14.2-ubuntu24.04 |
| 204 | +``` |
| 205 | + |
| 206 | +Those values are used by Compose interpolation in `build.cache_from`: |
| 207 | + |
| 208 | +```yaml |
| 209 | +cache_from: |
| 210 | + - ${EXECUTORCH_BASE_CACHE_IMAGE:-ghcr.io/arm-examples/topo-imx93-npu-deployment/executorch-base:et-v1.2.0-ubuntu24.04} |
| 211 | +``` |
| 212 | + |
| 213 | +For build-time configuration, wire Topo arguments into standard Compose `build.args`. The runner Dockerfile already declares project-specific arguments for the MCUX SDK manifest: |
| 214 | + |
| 215 | +```Dockerfile |
| 216 | +ARG MCUXSDK_MANIFEST_URL=https://github.com/nxp-mcuxpresso/mcuxsdk-manifests.git |
| 217 | +ARG MCUXSDK_MANIFEST_REV=v25.09.00 |
| 218 | +``` |
| 219 | + |
| 220 | +To expose the SDK revision through Topo, add matching Compose build args to the services that build `runner-base` descendants: |
| 221 | + |
| 222 | +```yaml |
| 223 | +services: |
| 224 | + runner-artifacts: |
| 225 | + build: |
| 226 | + args: |
| 227 | + MCUXSDK_MANIFEST_REV: ${MCUXSDK_MANIFEST_REV:-v25.09.00} |
| 228 | +
|
| 229 | + cm33-runner: |
| 230 | + build: |
| 231 | + args: |
| 232 | + MCUXSDK_MANIFEST_REV: ${MCUXSDK_MANIFEST_REV:-v25.09.00} |
| 233 | +
|
| 234 | +x-topo: |
| 235 | + args: |
| 236 | + MCUXSDK_MANIFEST_REV: |
| 237 | + description: MCUX SDK manifest revision used to build the Cortex-M33 runner. |
| 238 | + required: false |
| 239 | + default: v25.09.00 |
| 240 | +``` |
| 241 | + |
| 242 | +With that wiring, Topo can prompt for the value when the Template is cloned or extended, Compose passes the value into Docker BuildKit, and the Dockerfile consumes it through `ARG MCUXSDK_MANIFEST_REV`. |
| 243 | + |
| 244 | +Use this only for configuration that should be chosen at Template setup time. Runtime-only settings, such as `WEBAPP_PORT`, should remain normal Compose environment interpolation unless you intentionally want Topo to collect them as build-time parameters. |
| 245 | + |
| 246 | +## Lint the Template |
| 247 | + |
| 248 | +Before publishing the Template, validate the root Compose file: |
| 249 | + |
| 250 | +```bash |
| 251 | +check-jsonschema \ |
| 252 | + --schemafile ../topo-template-format/schema/topo-template-format.json \ |
| 253 | + compose.yaml |
| 254 | +``` |
| 255 | + |
| 256 | +Then review the Template the same way Topo Template linting does: |
| 257 | + |
| 258 | +- The Template root contains `compose.yaml`. |
| 259 | +- `compose.yaml` contains a root-level `x-topo.name`. |
| 260 | +- Non-remoteproc services set `platform: linux/arm64`. |
| 261 | +- The `cm33-runner` service uses `runtime: io.containerd.remoteproc.v1` and `remoteproc.name: imx-rproc`. |
| 262 | +- `x-topo.description` matches the README and the actual Cortex-A to Cortex-M33 RPMsg flow. |
| 263 | +- `x-topo.features` includes `remoteproc-runtime`. |
| 264 | +- `x-topo.args` entries are either consumed through Compose interpolation, such as the cache image values, or wired into `services.<service>.build.args` and declared as Dockerfile `ARG` instructions. |
| 265 | +- `deployment_success_message` tells the user to open the web app on the configured target port. |
| 266 | + |
| 267 | +## What you've accomplished |
| 268 | + |
| 269 | +You now understand how the `topo-imx93-npu-deployment` Template is built from ordinary Compose services plus Topo metadata: artifact-only build stages produce the model and firmware, Remoteproc Runtime starts the Cortex-M33 ELF, RPMsg connects the processors at runtime, and `x-topo.args` provides a path for setup-time configuration without replacing Docker or Compose. |
0 commit comments