|
| 1 | += Minerva Secure MCU Export |
| 2 | + |
| 3 | +Minerva export packages a supported SKaiNET compute graph for secure MCU inference through libminerva. Phase one is JVM-first and intentionally narrow: static sequential MLP graphs, Q8 quantization, and the ATmega328P target. |
| 4 | + |
| 5 | +Use this backend when the output must be a Minerva project bundle with compiler input, generated weights, firmware and host harnesses, a manifest, and host verification metadata. |
| 6 | + |
| 7 | +== When to Use Each Export Path |
| 8 | + |
| 9 | +[cols="1,2",options="header"] |
| 10 | +|=== |
| 11 | +| Export path | Use it when |
| 12 | + |
| 13 | +| StableHLO |
| 14 | +| You need a portable MLIR/IREE-compatible graph for native, accelerator, or ecosystem compiler flows. |
| 15 | + |
| 16 | +| Arduino / C99 |
| 17 | +| You need standalone generated C with static memory allocation and no external secure runtime. |
| 18 | + |
| 19 | +| Minerva |
| 20 | +| You need a secure MCU project bundle that is compiled by libminerva and checked by host verification. |
| 21 | +|=== |
| 22 | + |
| 23 | +== Setup |
| 24 | + |
| 25 | +Inside this repository, use the Minerva module directly: |
| 26 | + |
| 27 | +[source,kotlin] |
| 28 | +---- |
| 29 | +dependencies { |
| 30 | + implementation(project(":skainet-compile:skainet-compile-minerva")) |
| 31 | +} |
| 32 | +---- |
| 33 | + |
| 34 | +For a published application, use the SKaiNET BOM and the Minerva artifact: |
| 35 | + |
| 36 | +[source,kotlin] |
| 37 | +---- |
| 38 | +dependencies { |
| 39 | + implementation(platform("sk.ainet:skainet-bom:0.28.1")) |
| 40 | + implementation("sk.ainet.core:skainet-compile-minerva") |
| 41 | +} |
| 42 | +---- |
| 43 | + |
| 44 | +Configure libminerva through `MinervaExportOptions` or environment variables used by the maintained JVM sample: |
| 45 | + |
| 46 | +[source,bash] |
| 47 | +---- |
| 48 | +export MINERVA_COMPILER_SCRIPT=/opt/libminerva/tools/compile_model.py |
| 49 | +export MINERVA_RUNTIME_ROOT=/opt/libminerva |
| 50 | +export MINERVA_CALIBRATION_NPZ=/secure/project/calibration.npz |
| 51 | +export MINERVA_KEY_FILE=/secure/project/device.key |
| 52 | +export MINERVA_RUN_CMAKE=true |
| 53 | +export MINERVA_RUN_CTEST=true |
| 54 | +---- |
| 55 | + |
| 56 | +`MINERVA_KEY_FILE` and the generated `include/secrets.example.h` are placeholders for integration. Do not commit real device keys or derived secrets. |
| 57 | + |
| 58 | +== Compatibility Matrix |
| 59 | + |
| 60 | +[cols="1,2",options="header"] |
| 61 | +|=== |
| 62 | +| Area | Phase-one support |
| 63 | + |
| 64 | +| Host platform |
| 65 | +| JVM export path. |
| 66 | + |
| 67 | +| Target |
| 68 | +| `MinervaTarget.ATMEGA328P`. |
| 69 | + |
| 70 | +| Quantization |
| 71 | +| `MinervaQuantization.Q8`. |
| 72 | + |
| 73 | +| Graph shape |
| 74 | +| Static, single-path, sequential MLPs. |
| 75 | + |
| 76 | +| Tensor shapes |
| 77 | +| Fully known rank-2 shapes. |
| 78 | + |
| 79 | +| Layer pattern |
| 80 | +| `Input -> MatMul -> Add? -> activation?`, repeated in sequence. |
| 81 | + |
| 82 | +| Activations |
| 83 | +| `Relu`, `Sigmoid`, and `Tanh` after a dense layer. |
| 84 | + |
| 85 | +| Out of scope |
| 86 | +| CNNs, attention blocks, recurrent models, dynamic shapes, branching graphs, transformers, and arbitrary ONNX operators. |
| 87 | +|=== |
| 88 | + |
| 89 | +== Export API |
| 90 | + |
| 91 | +The public entry point is `MinervaExportFacade`. The facade accepts an already-built `ComputeGraph`, or it can record a representative forward pass for compatible SKaiNET models. |
| 92 | + |
| 93 | +[source,kotlin] |
| 94 | +---- |
| 95 | +import sk.ainet.compile.minerva.MinervaExportFacade |
| 96 | +import sk.ainet.compile.minerva.MinervaExportOptions |
| 97 | +
|
| 98 | +val options = MinervaExportOptions( |
| 99 | + outputDir = "build/minerva", |
| 100 | + projectName = "TinySecureMlp", |
| 101 | + compilerScript = "/opt/libminerva/tools/compile_model.py", |
| 102 | + runtimeRoot = "/opt/libminerva", |
| 103 | + calibrationNpz = "/secure/project/calibration.npz", |
| 104 | + keyFile = "/secure/project/device.key" |
| 105 | +) |
| 106 | +
|
| 107 | +val result = MinervaExportFacade().exportGraph(graph, options) |
| 108 | +val bundle = result.requireSuccess() |
| 109 | +println(bundle.outputDir) |
| 110 | +---- |
| 111 | + |
| 112 | +If `compilerScript` is missing, export still performs compatibility checks, Minerva lowering, and NPZ schema creation before returning a typed compiler prerequisite failure. That makes local validation possible before libminerva is installed. |
| 113 | + |
| 114 | +== Generated Project Layout |
| 115 | + |
| 116 | +A successful export writes a project directory under `outputDir/projectName`: |
| 117 | + |
| 118 | +[source,text] |
| 119 | +---- |
| 120 | +build/minerva/TinySecureMlp/ |
| 121 | + manifest.json |
| 122 | + generated/ |
| 123 | + model.npz |
| 124 | + weights.c |
| 125 | + include/ |
| 126 | + weights.h |
| 127 | + secrets.example.h |
| 128 | + host/ |
| 129 | + CMakeLists.txt |
| 130 | + main.c |
| 131 | + firmware/ |
| 132 | + main.c |
| 133 | +---- |
| 134 | + |
| 135 | +The manifest records the target, quantization, libminerva root, compiler command summary, NPZ schema version, layer count, and generated files. `secrets.example.h` contains placeholder values only. |
| 136 | + |
| 137 | +== Host Verification |
| 138 | + |
| 139 | +Host verification always checks the package structure, generated weight files, `model.npz` integrity, and placeholder secret hygiene. It also computes the SKaiNET reference output for a deterministic reference input. |
| 140 | + |
| 141 | +Use these metadata keys to opt into external host checks: |
| 142 | + |
| 143 | +[source,kotlin] |
| 144 | +---- |
| 145 | +metadata = mapOf( |
| 146 | + MinervaHostVerificationMetadata.RUN_CMAKE_BUILD to "true", |
| 147 | + MinervaHostVerificationMetadata.RUN_CTEST to "true", |
| 148 | + MinervaHostVerificationMetadata.HOST_OUTPUT_PATH to "host-output.txt" |
| 149 | +) |
| 150 | +---- |
| 151 | + |
| 152 | +`RUN_CMAKE_BUILD` configures and builds `host/CMakeLists.txt`. `RUN_CTEST` runs the packaged CTest smoke test. `HOST_OUTPUT_PATH` lets a real host run write comma- or whitespace-separated float outputs that are compared with the SKaiNET reference output using `hostVerificationTolerance`. |
| 153 | + |
| 154 | +Local CI recipe: |
| 155 | + |
| 156 | +[source,bash] |
| 157 | +---- |
| 158 | +./gradlew :skainet-compile:skainet-compile-minerva:jvmTest |
| 159 | +./gradlew :skainet-compile:skainet-compile-minerva:minervaHostVerification \ |
| 160 | + -Pminerva.hostVerification.enabled=true \ |
| 161 | + -Pminerva.runtimeRoot="$MINERVA_RUNTIME_ROOT" \ |
| 162 | + -Pminerva.compilerScript="$MINERVA_COMPILER_SCRIPT" |
| 163 | +cmake -S build/minerva/TinySecureMlp/host -B build/minerva/TinySecureMlp/host/build |
| 164 | +ctest --test-dir build/minerva/TinySecureMlp/host/build --output-on-failure |
| 165 | +---- |
| 166 | + |
| 167 | +The Gradle `minervaHostVerification` task is gated. It only runs when the `minerva.hostVerification.enabled`, `minerva.runtimeRoot`, and `minerva.compilerScript` properties are present. |
| 168 | + |
| 169 | +== Firmware Integration |
| 170 | + |
| 171 | +The generated firmware example intentionally contains integration placeholders. Wire it to the runtime API exposed by your pinned libminerva checkout and keep the real secret configuration in a private header that is excluded from source control. |
| 172 | + |
| 173 | +Before flashing firmware: |
| 174 | + |
| 175 | +* Replace `secrets.example.h` with a private header outside the repository. |
| 176 | +* Confirm the libminerva inference entry point and output-authentication API names against the libminerva version used by your product build. |
| 177 | +* Re-run host verification after any compiler, runtime, calibration, or key change. |
| 178 | +* Keep `manifest.json` with release artifacts so the compiler command and schema version remain auditable. |
| 179 | + |
| 180 | +== ONNX to Minerva Workflow |
| 181 | + |
| 182 | +Use the existing ONNX loader path to inspect the model and extract a supported static MLP graph before calling Minerva export. The first phase does not include a general ONNX-to-Minerva importer; unsupported ONNX operators should be rejected before export. |
| 183 | + |
| 184 | +[source,kotlin] |
| 185 | +---- |
| 186 | +import kotlinx.io.asSource |
| 187 | +import sk.ainet.io.onnx.OnnxLoader |
| 188 | +import java.io.File |
| 189 | +
|
| 190 | +suspend fun loadOnnxForMinerva(path: String) { |
| 191 | + val loaded = OnnxLoader.fromModelSource { |
| 192 | + File(path).inputStream().asSource() |
| 193 | + }.load() |
| 194 | + val graph = loaded.proto.graph ?: error("ONNX model has no graph") |
| 195 | +
|
| 196 | + val ops = graph.node.map { it.opType }.toSet() |
| 197 | + require(ops.all { it in setOf("MatMul", "Gemm", "Add", "Relu", "Sigmoid", "Tanh") }) { |
| 198 | + "ONNX graph contains operators outside the Minerva phase-one scope: $ops" |
| 199 | + } |
| 200 | +
|
| 201 | + // Convert the inspected static MLP to a SKaiNET ComputeGraph, then call MinervaExportFacade. |
| 202 | +} |
| 203 | +---- |
| 204 | + |
| 205 | +== Maintained JVM Sample |
| 206 | + |
| 207 | +The maintained sample is `sk.ainet.compile.minerva.examples.MinervaTinyMlpExportSample` in the Minerva module. It builds a tiny two-layer MLP, reads Minerva paths from environment variables, invokes the export facade, and prints bundle and verification status. |
| 208 | + |
| 209 | +Run the sample after configuring libminerva: |
| 210 | + |
| 211 | +[source,bash] |
| 212 | +---- |
| 213 | +./gradlew :skainet-compile:skainet-compile-minerva:jvmTest |
| 214 | +./gradlew :skainet-compile:skainet-compile-minerva:jvmJar |
| 215 | +---- |
| 216 | + |
| 217 | +The sample graph is covered by `MinervaTinyMlpExportSampleTest`, which validates compatibility, lowering, and NPZ generation without requiring real device keys. |
| 218 | + |
| 219 | +== Troubleshooting |
| 220 | + |
| 221 | +[cols="1,2",options="header"] |
| 222 | +|=== |
| 223 | +| Symptom | Fix |
| 224 | + |
| 225 | +| `minerva.compiler.script_missing` |
| 226 | +| Set `MinervaExportOptions.compilerScript` or `MINERVA_COMPILER_SCRIPT`. |
| 227 | + |
| 228 | +| `minerva.compiler.runtime_root_not_found` |
| 229 | +| Point `runtimeRoot` or `MINERVA_RUNTIME_ROOT` at the libminerva checkout or install directory. |
| 230 | + |
| 231 | +| Compatibility fails for an unsupported operation |
| 232 | +| Reduce the graph to the phase-one MLP pattern, or use StableHLO for a general compiler flow. |
| 233 | + |
| 234 | +| CMake or CTest verification fails |
| 235 | +| Inspect `minerva-host-verification.log`, confirm CMake is installed, and confirm the generated host harness is linked against the pinned libminerva runtime. |
| 236 | + |
| 237 | +| Secret leak check fails |
| 238 | +| Remove real secrets from generated files and regenerate the bundle. Only placeholders belong in source control. |
| 239 | +|=== |
0 commit comments