From 2a6e64cf33b7040cbfbc224dae3876b2e676d9fd Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 8 Jun 2026 14:35:44 +0200 Subject: [PATCH] feat(minerva): ship real runtime adapter sample Refs #687 --- docs/export/minerva.md | 6 +- .../ROOT/pages/how-to/minerva-export.adoc | 8 +- .../minerva/MinervaJvmCompilerAndPackager.kt | 84 +++++++++++++++++-- .../MinervaJvmCompilerAndPackagerTest.kt | 64 +++++++++++++- 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/docs/export/minerva.md b/docs/export/minerva.md index 0ce865c0..dff35cc5 100644 --- a/docs/export/minerva.md +++ b/docs/export/minerva.md @@ -93,7 +93,9 @@ The generated host harness has a stable adapter ABI: int minerva_run_inference(const float *input, int input_count, float *output, int output_count); ``` -Copy `host/runtime_adapter.example.c` outside the generated bundle, wire that function to the pinned libminerva runtime, then point CMake at the adapter source. This keeps SKaiNET from hard-coding unverified libminerva runtime entry point names. +`host/runtime_adapter.example.c` implements that ABI against the libminerva public C API (`mnv_init`, `mnv_seed_prng`, `mnv_run`, and `mnv_verify_output`). The adapter converts SKaiNET's normalized float fixtures to libminerva Q8 activation buffers and converts Q8 outputs back to floats for parity comparison. + +Copy the adapter outside the generated bundle when product-specific scaling or entropy seeding needs local edits, then point CMake at the copied source. This keeps the generated host harness stable while leaving runtime policy in one reviewable adapter file. Add these metadata keys to opt into CMake, CTest, and parity comparison with a custom host output file: @@ -149,7 +151,7 @@ The first phase does not include a general ONNX-to-Minerva importer. Once any so ## Firmware Integration -The generated firmware example intentionally contains placeholders. Confirm the libminerva inference entry point and output-authentication API names against the pinned libminerva version used by your product build before flashing firmware. +The generated firmware example intentionally contains integration placeholders. Use the host adapter as the reference for the pinned libminerva public API, then move product-specific entropy seeding, input scaling, and secret provisioning into private firmware code before flashing. ## Maintained JVM Sample diff --git a/docs/modules/ROOT/pages/how-to/minerva-export.adoc b/docs/modules/ROOT/pages/how-to/minerva-export.adoc index c29c66d0..c8e05c8b 100644 --- a/docs/modules/ROOT/pages/how-to/minerva-export.adoc +++ b/docs/modules/ROOT/pages/how-to/minerva-export.adoc @@ -161,7 +161,9 @@ The generated host harness has a stable adapter ABI: int minerva_run_inference(const float *input, int input_count, float *output, int output_count); ---- -Copy `host/runtime_adapter.example.c` outside the generated bundle, wire that function to the pinned libminerva runtime, then point CMake at the adapter source. This keeps SKaiNET from hard-coding unverified libminerva runtime entry point names. +`host/runtime_adapter.example.c` implements that ABI against the libminerva public C API: `mnv_init`, `mnv_seed_prng`, `mnv_run`, and `mnv_verify_output`. The adapter converts SKaiNET's normalized float fixtures to libminerva Q8 activation buffers and converts Q8 outputs back to floats for parity comparison. + +Copy the adapter outside the generated bundle when product-specific scaling or entropy seeding needs local edits, then point CMake at the copied source. This keeps the generated host harness stable while leaving runtime policy in one reviewable adapter file. Use these metadata keys to opt into external host checks: @@ -204,12 +206,12 @@ The Gradle `minervaHostVerification` task is gated. It only runs when `minerva.h == Firmware Integration -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. +The generated firmware example intentionally contains integration placeholders. Use the host adapter as the reference for the pinned libminerva public API, then move product-specific entropy seeding, input scaling, and secret provisioning into private firmware code before flashing. Before flashing firmware: * Replace `secrets.example.h` with a private header outside the repository. -* Confirm the libminerva inference entry point and output-authentication API names against the libminerva version used by your product build. +* Confirm the pinned libminerva checkout still exposes the public API used by `host/runtime_adapter.example.c`. * Re-run host verification after any compiler, runtime, calibration, or key change. * Keep `manifest.json` with release artifacts so the compiler command and schema version remain auditable. diff --git a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt index a18c66d9..29896737 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmMain/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackager.kt @@ -666,22 +666,88 @@ public class JvmMinervaProjectPackager @kotlin.jvm.JvmOverloads constructor( private fun hostRuntimeAdapterExample(): String { return """ |/* - | * Copy this file outside the generated bundle and wire it to your pinned - | * libminerva runtime. Then configure CMake with: + | * Copy this file outside the generated bundle when you need local edits, + | * then configure CMake with: | * | * -DMINERVA_HOST_ADAPTER_SOURCE=/path/to/minerva_runtime_adapter.c | * - | * The generated host harness calls this stable shim so SKaiNET does not - | * need to hard-code libminerva runtime entry point names. + | * This adapter targets libminerva's public C API: + | * mnv_init, mnv_seed_prng, mnv_run, mnv_verify_output + | * + | * SKaiNET host fixtures use normalized float values. libminerva uses Q8 + | * activation buffers, so this sample clamps [-1, 1] floats to int8 and + | * converts Q8 outputs back to floats for parity comparison. | */ + |#include + |#include "minerva.h" |#include "weights.h" | + |#ifndef MNV_HOST_PRNG_SEED + |#define MNV_HOST_PRNG_SEED 0x534B4149UL + |#endif + | + |static mnv_ctx_t minerva_host_ctx; + |static int minerva_host_initialized = 0; + | + |static mnv_act_t minerva_float_to_q8(float value) { + | if (value >= 1.0f) { + | return (mnv_act_t)127; + | } + | if (value <= -1.0f) { + | return (mnv_act_t)-128; + | } + | + | if (value >= 0.0f) { + | return (mnv_act_t)((value * 127.0f) + 0.5f); + | } + | return (mnv_act_t)((value * 128.0f) - 0.5f); + |} + | + |static float minerva_q8_to_float(mnv_act_t value) { + | return value < 0 ? ((float)value / 128.0f) : ((float)value / 127.0f); + |} + | |int minerva_run_inference(const float *input, int input_count, float *output, int output_count) { - | (void)input; - | (void)input_count; - | (void)output; - | (void)output_count; - | return 1; + | if (input == 0 || output == 0) { + | return -1; + | } + | if (input_count != (int)MNV_INPUT_SIZE || output_count != (int)MNV_OUTPUT_SIZE) { + | return -2; + | } + | + | if (!minerva_host_initialized) { + | mnv_status_t status = mnv_init(&minerva_host_ctx, &mnv_model); + | if (status != MNV_OK) { + | return (int)status; + | } + | mnv_seed_prng(&minerva_host_ctx, (uint32_t)MNV_HOST_PRNG_SEED); + | minerva_host_initialized = 1; + | } + | + | mnv_act_t q_input[MNV_INPUT_SIZE]; + | mnv_act_t q_output[MNV_OUTPUT_SIZE]; + | for (int i = 0; i < input_count; ++i) { + | q_input[i] = minerva_float_to_q8(input[i]); + | } + | for (int i = 0; i < output_count; ++i) { + | q_output[i] = 0; + | } + | + | mnv_status_t status = mnv_run(&minerva_host_ctx, q_input, q_output); + | if (status != MNV_OK) { + | return (int)status; + | } + |#ifdef MNV_ENABLE_OUTPUT_AUTH + | status = mnv_verify_output(&minerva_host_ctx, q_input, q_output); + | if (status != MNV_OK) { + | return (int)status; + | } + |#endif + | + | for (int i = 0; i < output_count; ++i) { + | output[i] = minerva_q8_to_float(q_output[i]); + | } + | return 0; |} | """.trimMargin() diff --git a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt index 5c8cfee5..87b0fe3e 100644 --- a/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt +++ b/skainet-compile/skainet-compile-minerva/src/jvmTest/kotlin/sk/ainet/compile/minerva/MinervaJvmCompilerAndPackagerTest.kt @@ -135,7 +135,13 @@ class MinervaJvmCompilerAndPackagerTest { assertTrue(hostMain.contains("observed-output.txt")) val adapterExample = Files.readString(projectDir.resolve("host/runtime_adapter.example.c")) assertTrue(adapterExample.contains("minerva_run_inference")) - assertTrue(adapterExample.contains("return 1")) + assertTrue(adapterExample.contains("mnv_init")) + assertTrue(adapterExample.contains("mnv_seed_prng")) + assertTrue(adapterExample.contains("mnv_run")) + assertTrue(adapterExample.contains("mnv_verify_output")) + assertTrue(adapterExample.contains("MNV_INPUT_SIZE")) + assertTrue(adapterExample.contains("MNV_OUTPUT_SIZE")) + assertFalse(adapterExample.contains("return 1;")) val secretsExample = Files.readString(projectDir.resolve("include/secrets.example.h")) assertTrue(secretsExample.contains("replace-with-device-key")) assertFalse(secretsExample.contains("REAL_SECRET_KEY_MATERIAL")) @@ -160,6 +166,62 @@ class MinervaJvmCompilerAndPackagerTest { assertTrue(context.diagnostics.any { it.code == "minerva.packaging.completed" }) } + @Test + fun hostRuntimeAdapterExampleCompilesAgainstPublicMinervaApiShim() { + val root = tempDir("adapter-compile") + val fixture = packagedProject(root, "AdapterCompile") + val includeDir = fixture.projectDir.resolve("include") + val runtimeIncludeDir = root.resolve("runtime/include") + Files.createDirectories(runtimeIncludeDir) + Files.writeString( + includeDir.resolve("weights.h"), + """ + |#pragma once + |#include "minerva.h" + |#define MNV_INPUT_SIZE 4U + |#define MNV_OUTPUT_SIZE 3U + |extern const mnv_model_t mnv_model; + | + """.trimMargin() + ) + Files.writeString( + runtimeIncludeDir.resolve("minerva.h"), + """ + |#pragma once + |#include + |#define MNV_OK 0 + |#define MNV_ENABLE_OUTPUT_AUTH 1 + |typedef int mnv_status_t; + |typedef int8_t mnv_act_t; + |typedef struct mnv_ctx_t { int verified; } mnv_ctx_t; + |typedef struct mnv_model_t { int version; } mnv_model_t; + |mnv_status_t mnv_init(mnv_ctx_t *ctx, const mnv_model_t *model); + |void mnv_seed_prng(mnv_ctx_t *ctx, uint32_t seed); + |mnv_status_t mnv_run(mnv_ctx_t *ctx, const mnv_act_t *input, mnv_act_t *output); + |mnv_status_t mnv_verify_output(const mnv_ctx_t *ctx, const mnv_act_t *input, const mnv_act_t *output); + | + """.trimMargin() + ) + + val process = ProcessBuilder( + "cc", + "-std=c11", + "-Wall", + "-Werror", + "-I${runtimeIncludeDir}", + "-I${includeDir}", + "-c", + fixture.projectDir.resolve("host/runtime_adapter.example.c").toString(), + "-o", + root.resolve("runtime_adapter.example.o").toString() + ).start() + val stdout = process.inputStream.bufferedReader().readText() + val stderr = process.errorStream.bufferedReader().readText() + + assertEquals(0, process.waitFor(), stdout + stderr) + assertTrue(Files.isRegularFile(root.resolve("runtime_adapter.example.o"))) + } + @Test fun hostVerifierPassesStructuralAndReferenceChecksWithoutExternalRuntime() { val fixture = packagedProject(tempDir("host-verify-lightweight"), "LightweightVerify")