diff --git a/settings.gradle.kts b/settings.gradle.kts index 9552ba852..5c393abaf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ include("skainet-compile:skainet-compile-opt") include("skainet-compile:skainet-compile-json") include("skainet-compile:skainet-compile-hlo") include("skainet-compile:skainet-compile-c") +include("skainet-compile:skainet-compile-minerva") // ====== BACKENDS include("skainet-backends:skainet-backend-api") diff --git a/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api b/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api new file mode 100644 index 000000000..6fe8d6cc1 --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/api/skainet-compile-minerva.api @@ -0,0 +1,151 @@ +public final class sk/ainet/compile/minerva/MinervaExportBackend { + public static final field INSTANCE Lsk/ainet/compile/minerva/MinervaExportBackend; + public static final field backendName Ljava/lang/String; + public static final field phaseOneScope Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaExportBundle { + public fun (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lsk/ainet/compile/minerva/MinervaTarget; + public final fun component4 ()Lsk/ainet/compile/minerva/MinervaQuantization; + public final fun component5 ()Ljava/util/List; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/util/List;Ljava/lang/String;)Lsk/ainet/compile/minerva/MinervaExportBundle; + public static synthetic fun copy$default (Lsk/ainet/compile/minerva/MinervaExportBundle;Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lsk/ainet/compile/minerva/MinervaExportBundle; + public fun equals (Ljava/lang/Object;)Z + public final fun getGeneratedFiles ()Ljava/util/List; + public final fun getManifestPath ()Ljava/lang/String; + public final fun getOutputDir ()Ljava/lang/String; + public final fun getProjectName ()Ljava/lang/String; + public final fun getQuantization ()Lsk/ainet/compile/minerva/MinervaQuantization; + public final fun getTarget ()Lsk/ainet/compile/minerva/MinervaTarget; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaExportFacade { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun exportGraph (Lsk/ainet/lang/graph/ComputeGraph;Lsk/ainet/compile/minerva/MinervaExportOptions;)Lsk/ainet/compile/minerva/MinervaExportResult; + public final fun exportModel (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lsk/ainet/compile/minerva/MinervaExportOptions;)Lsk/ainet/compile/minerva/MinervaExportResult; + public final fun exportModel (Ljava/lang/Object;Lsk/ainet/compile/minerva/MinervaExportOptions;)Lsk/ainet/compile/minerva/MinervaExportResult; + public final fun getBackendName ()Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaExportFailure { + public fun (Lsk/ainet/compile/minerva/MinervaExportFailureKind;Lsk/ainet/compile/export/GraphExportStage;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Lsk/ainet/compile/minerva/MinervaExportFailureKind;Lsk/ainet/compile/export/GraphExportStage;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public final fun component2 ()Lsk/ainet/compile/export/GraphExportStage; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/util/Map; + public final fun copy (Lsk/ainet/compile/minerva/MinervaExportFailureKind;Lsk/ainet/compile/export/GraphExportStage;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lsk/ainet/compile/minerva/MinervaExportFailure; + public static synthetic fun copy$default (Lsk/ainet/compile/minerva/MinervaExportFailure;Lsk/ainet/compile/minerva/MinervaExportFailureKind;Lsk/ainet/compile/export/GraphExportStage;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lsk/ainet/compile/minerva/MinervaExportFailure; + public fun equals (Ljava/lang/Object;)Z + public final fun getCode ()Ljava/lang/String; + public final fun getDetails ()Ljava/util/Map; + public final fun getKind ()Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public final fun getMessage ()Ljava/lang/String; + public final fun getStage ()Lsk/ainet/compile/export/GraphExportStage; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaExportFailureKind : java/lang/Enum { + public static final field GRAPH_VALIDATION_FAILED Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public static final field NOT_IMPLEMENTED Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public static final field RECORDING_FAILED Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public static final field UNSUPPORTED_MODEL_TYPE Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lsk/ainet/compile/minerva/MinervaExportFailureKind; + public static fun values ()[Lsk/ainet/compile/minerva/MinervaExportFailureKind; +} + +public final class sk/ainet/compile/minerva/MinervaExportOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZZLjava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZZLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component10 ()Z + public final fun component11 ()Z + public final fun component12 ()Z + public final fun component13 ()Ljava/util/Map; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Lsk/ainet/compile/minerva/MinervaTarget; + public final fun component4 ()Lsk/ainet/compile/minerva/MinervaQuantization; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Ljava/lang/String; + public final fun component9 ()Z + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZZLjava/util/Map;)Lsk/ainet/compile/minerva/MinervaExportOptions; + public static synthetic fun copy$default (Lsk/ainet/compile/minerva/MinervaExportOptions;Ljava/lang/String;Ljava/lang/String;Lsk/ainet/compile/minerva/MinervaTarget;Lsk/ainet/compile/minerva/MinervaQuantization;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZZLjava/util/Map;ILjava/lang/Object;)Lsk/ainet/compile/minerva/MinervaExportOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getCalibrationNpz ()Ljava/lang/String; + public final fun getCompilerScript ()Ljava/lang/String; + public final fun getDumpWeights ()Z + public final fun getGenerateFirmwareExample ()Z + public final fun getGenerateHostHarness ()Z + public final fun getKeyFile ()Ljava/lang/String; + public final fun getMetadata ()Ljava/util/Map; + public final fun getOutputDir ()Ljava/lang/String; + public final fun getProjectName ()Ljava/lang/String; + public final fun getQuantization ()Lsk/ainet/compile/minerva/MinervaQuantization; + public final fun getRunHostVerification ()Z + public final fun getRuntimeRoot ()Ljava/lang/String; + public final fun getTarget ()Lsk/ainet/compile/minerva/MinervaTarget; + public fun hashCode ()I + public final fun toMetadata ()Ljava/util/Map; + public fun toString ()Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaExportResult { + public fun (Lsk/ainet/compile/minerva/MinervaExportOptions;Lsk/ainet/compile/export/GraphExportStatus;Lsk/ainet/compile/minerva/MinervaExportBundle;Lsk/ainet/compile/export/GraphExportDiagnosticReport;Ljava/util/List;Lsk/ainet/compile/minerva/MinervaExportFailure;Ljava/util/Map;)V + public synthetic fun (Lsk/ainet/compile/minerva/MinervaExportOptions;Lsk/ainet/compile/export/GraphExportStatus;Lsk/ainet/compile/minerva/MinervaExportBundle;Lsk/ainet/compile/export/GraphExportDiagnosticReport;Ljava/util/List;Lsk/ainet/compile/minerva/MinervaExportFailure;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lsk/ainet/compile/minerva/MinervaExportOptions; + public final fun component2 ()Lsk/ainet/compile/export/GraphExportStatus; + public final fun component3 ()Lsk/ainet/compile/minerva/MinervaExportBundle; + public final fun component4 ()Lsk/ainet/compile/export/GraphExportDiagnosticReport; + public final fun component5 ()Ljava/util/List; + public final fun component6 ()Lsk/ainet/compile/minerva/MinervaExportFailure; + public final fun component7 ()Ljava/util/Map; + public final fun copy (Lsk/ainet/compile/minerva/MinervaExportOptions;Lsk/ainet/compile/export/GraphExportStatus;Lsk/ainet/compile/minerva/MinervaExportBundle;Lsk/ainet/compile/export/GraphExportDiagnosticReport;Ljava/util/List;Lsk/ainet/compile/minerva/MinervaExportFailure;Ljava/util/Map;)Lsk/ainet/compile/minerva/MinervaExportResult; + public static synthetic fun copy$default (Lsk/ainet/compile/minerva/MinervaExportResult;Lsk/ainet/compile/minerva/MinervaExportOptions;Lsk/ainet/compile/export/GraphExportStatus;Lsk/ainet/compile/minerva/MinervaExportBundle;Lsk/ainet/compile/export/GraphExportDiagnosticReport;Ljava/util/List;Lsk/ainet/compile/minerva/MinervaExportFailure;Ljava/util/Map;ILjava/lang/Object;)Lsk/ainet/compile/minerva/MinervaExportResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getArtifacts ()Ljava/util/List; + public final fun getBundle ()Lsk/ainet/compile/minerva/MinervaExportBundle; + public final fun getDiagnostics ()Lsk/ainet/compile/export/GraphExportDiagnosticReport; + public final fun getFailed ()Z + public final fun getFailure ()Lsk/ainet/compile/minerva/MinervaExportFailure; + public final fun getMetadata ()Ljava/util/Map; + public final fun getOptions ()Lsk/ainet/compile/minerva/MinervaExportOptions; + public final fun getStatus ()Lsk/ainet/compile/export/GraphExportStatus; + public final fun getSucceeded ()Z + public fun hashCode ()I + public final fun requireSuccess ()Lsk/ainet/compile/minerva/MinervaExportBundle; + public fun toString ()Ljava/lang/String; +} + +public final class sk/ainet/compile/minerva/MinervaQuantization : java/lang/Enum { + public static final field Q8 Lsk/ainet/compile/minerva/MinervaQuantization; + public final fun getCompilerId ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lsk/ainet/compile/minerva/MinervaQuantization; + public static fun values ()[Lsk/ainet/compile/minerva/MinervaQuantization; +} + +public final class sk/ainet/compile/minerva/MinervaTarget : java/lang/Enum { + public static final field ATMEGA328P Lsk/ainet/compile/minerva/MinervaTarget; + public final fun getCompilerId ()Ljava/lang/String; + public final fun getDisplayName ()Ljava/lang/String; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getFlashBytes ()I + public final fun getSramBytes ()I + public static fun valueOf (Ljava/lang/String;)Lsk/ainet/compile/minerva/MinervaTarget; + public static fun values ()[Lsk/ainet/compile/minerva/MinervaTarget; +} + diff --git a/skainet-compile/skainet-compile-minerva/build.gradle.kts b/skainet-compile/skainet-compile-minerva/build.gradle.kts new file mode 100644 index 000000000..724648210 --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.vanniktech.mavenPublish) + alias(libs.plugins.binary.compatibility.validator) + id("sk.ainet.dokka") +} + +kotlin { + explicitApi() + + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + sourceSets { + commonMain.dependencies { + api(project(":skainet-lang:skainet-lang-core")) + api(project(":skainet-compile:skainet-compile-core")) + api(project(":skainet-compile:skainet-compile-dag")) + } + + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} diff --git a/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportFacade.kt b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportFacade.kt new file mode 100644 index 000000000..ff7f37eea --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportFacade.kt @@ -0,0 +1,211 @@ +package sk.ainet.compile.minerva + +import sk.ainet.compile.export.GraphExportContext +import sk.ainet.compile.export.GraphExportStage +import sk.ainet.compile.export.GraphExportStatus +import sk.ainet.context.ExecutionContext +import sk.ainet.lang.graph.ComputeGraph +import sk.ainet.lang.graph.DefaultGraphExecutionContext +import sk.ainet.lang.tape.toComputeGraph +import sk.ainet.lang.tensor.ops.ValidationResult +import sk.ainet.lang.tensor.ops.VoidTensorOps +import sk.ainet.tape.Execution + +/** + * Public Minerva export facade. + * + * This scaffold accepts direct [ComputeGraph] inputs and exposes the same + * traced-forward-pass shape used by other SKaiNET export facades. Real + * compatibility validation and Minerva compiler invocation start in later + * implementation issues. + */ +public class MinervaExportFacade @kotlin.jvm.JvmOverloads constructor( + public val backendName: String = MinervaExportBackend.backendName +) { + + /** + * Export a model when the caller may already hold a [ComputeGraph]. + */ + public fun exportModel( + model: T, + options: MinervaExportOptions + ): MinervaExportResult { + return when (model) { + is ComputeGraph -> exportGraph(model, options) + else -> unsupportedModelResult(model, options) + } + } + + /** + * Export a model by recording one representative forward pass. + */ + public fun exportModel( + model: T, + forwardPass: (ExecutionContext) -> Unit, + options: MinervaExportOptions + ): MinervaExportResult { + if (model is ComputeGraph) return exportGraph(model, options) + + val context = exportContext(options) + return try { + val graphContext = DefaultGraphExecutionContext.tape(baseOps = VoidTensorOps()) + val (tape, _) = graphContext.record { + val currentTape = this.currentTape ?: error("Failed to create a recording tape.") + val globalStack = Execution.tapeStack + globalStack.pushTape(currentTape) + try { + forwardPass(this) + } finally { + globalStack.popTape() + } + } + val graph = tape?.toComputeGraph() + ?: return recordingFailedResult(options, context, "No tape was produced during recording.") + exportGraph(graph, options) + } catch (exception: Exception) { + recordingFailedResult( + options = options, + context = context, + reason = exception.message ?: exception.toString() + ) + } + } + + /** + * Export a [ComputeGraph] directly. + */ + public fun exportGraph( + graph: ComputeGraph, + options: MinervaExportOptions + ): MinervaExportResult { + val context = exportContext(options) + context.info( + stage = GraphExportStage.CAPTURE, + code = "minerva.graph.accepted", + message = "Accepted ComputeGraph for Minerva export.", + details = mapOf("nodes" to graph.nodes.size.toString()) + ) + + if (graph.nodes.isEmpty()) { + return graphValidationFailedResult( + options = options, + context = context, + errors = listOf("Minerva export requires at least one graph node.") + ) + } + + when (val validation = graph.validate()) { + is ValidationResult.Valid -> context.info( + stage = GraphExportStage.VALIDATION, + code = "minerva.graph.validation.passed", + message = "ComputeGraph validation passed before Minerva-specific checks." + ) + is ValidationResult.Invalid -> return graphValidationFailedResult( + options = options, + context = context, + errors = validation.errors + ) + } + + val failure = MinervaExportFailure( + kind = MinervaExportFailureKind.NOT_IMPLEMENTED, + stage = GraphExportStage.LOWERING, + code = "minerva.export.not_implemented", + message = "Minerva export API is scaffolded; compatibility validation, lowering, compiler invocation, packaging, and verification are implemented in follow-up issues.", + details = mapOf( + "nextStep" to "Implement MinervaCompatibilityValidator", + "issue" to "#691" + ) + ) + context.error( + stage = failure.stage, + code = failure.code, + message = failure.message, + details = failure.details + ) + return failedResult(options, context, failure) + } + + private fun unsupportedModelResult(model: Any, options: MinervaExportOptions): MinervaExportResult { + val context = exportContext(options) + val typeName = model::class.simpleName ?: "unknown" + val failure = MinervaExportFailure( + kind = MinervaExportFailureKind.UNSUPPORTED_MODEL_TYPE, + stage = GraphExportStage.CAPTURE, + code = "minerva.model.unsupported_type", + message = "Minerva export does not have a direct adapter for model type '$typeName'. Use a ComputeGraph or provide a forwardPass lambda to record one execution.", + details = mapOf("modelType" to typeName) + ) + context.error( + stage = failure.stage, + code = failure.code, + message = failure.message, + details = failure.details + ) + return failedResult(options, context, failure) + } + + private fun recordingFailedResult( + options: MinervaExportOptions, + context: GraphExportContext, + reason: String + ): MinervaExportResult { + val failure = MinervaExportFailure( + kind = MinervaExportFailureKind.RECORDING_FAILED, + stage = GraphExportStage.CAPTURE, + code = "minerva.model.recording_failed", + message = "Failed to record a forward pass for Minerva export: $reason" + ) + context.error( + stage = failure.stage, + code = failure.code, + message = failure.message, + details = failure.details + ) + return failedResult(options, context, failure) + } + + private fun graphValidationFailedResult( + options: MinervaExportOptions, + context: GraphExportContext, + errors: List + ): MinervaExportResult { + val failure = MinervaExportFailure( + kind = MinervaExportFailureKind.GRAPH_VALIDATION_FAILED, + stage = GraphExportStage.VALIDATION, + code = "minerva.graph.validation_failed", + message = "ComputeGraph validation failed before Minerva-specific checks.", + details = errors.mapIndexed { index, error -> "error$index" to error }.toMap() + ) + context.error( + stage = failure.stage, + code = failure.code, + message = failure.message, + details = failure.details + ) + return failedResult(options, context, failure) + } + + private fun failedResult( + options: MinervaExportOptions, + context: GraphExportContext, + failure: MinervaExportFailure + ): MinervaExportResult { + return MinervaExportResult( + options = options, + status = GraphExportStatus.FAILED, + diagnostics = context.diagnosticReport(), + artifacts = context.artifacts, + failure = failure, + metadata = context.metadata + ) + } + + private fun exportContext(options: MinervaExportOptions): GraphExportContext { + return GraphExportContext( + backendName = backendName, + targetName = options.projectName, + metadata = options.toMetadata() + ) + } +} diff --git a/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportModels.kt b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportModels.kt new file mode 100644 index 000000000..b6bd88fcc --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/MinervaExportModels.kt @@ -0,0 +1,167 @@ +package sk.ainet.compile.minerva + +import sk.ainet.compile.export.GraphExportArtifact +import sk.ainet.compile.export.GraphExportDiagnosticReport +import sk.ainet.compile.export.GraphExportStage +import sk.ainet.compile.export.GraphExportStatus + +/** + * Supported Minerva quantization modes for the phase-one API. + */ +public enum class MinervaQuantization( + public val compilerId: String +) { + Q8("q8") +} + +/** + * Validated Minerva target configurations exposed by the export API. + */ +public enum class MinervaTarget( + public val compilerId: String, + public val displayName: String, + public val flashBytes: Int, + public val sramBytes: Int +) { + ATMEGA328P( + compilerId = "atmega328p", + displayName = "ATmega328P", + flashBytes = 32 * 1024, + sramBytes = 2 * 1024 + ) +} + +/** + * Export options for the Minerva backend. + * + * Path values are strings so the API stays usable from common code. The phase + * one scaffold validates shape and intent but does not require a libminerva + * checkout until compiler integration lands. + */ +public data class MinervaExportOptions( + public val outputDir: String, + public val projectName: String, + public val target: MinervaTarget = MinervaTarget.ATMEGA328P, + public val quantization: MinervaQuantization = MinervaQuantization.Q8, + public val runtimeRoot: String? = null, + public val compilerScript: String? = null, + public val keyFile: String? = null, + public val calibrationNpz: String? = null, + public val dumpWeights: Boolean = false, + public val generateHostHarness: Boolean = true, + public val generateFirmwareExample: Boolean = true, + public val runHostVerification: Boolean = true, + public val metadata: Map = emptyMap() +) { + init { + require(outputDir.isNotBlank()) { "outputDir cannot be blank" } + require(projectName.isNotBlank()) { "projectName cannot be blank" } + require(projectName.none { it == '/' || it == '\\' }) { + "projectName must be a simple project directory name" + } + requireOptionalPath("runtimeRoot", runtimeRoot) + requireOptionalPath("compilerScript", compilerScript) + requireOptionalPath("keyFile", keyFile) + requireOptionalPath("calibrationNpz", calibrationNpz) + require(metadata.keys.all { it.isNotBlank() }) { "metadata keys cannot be blank" } + } + + public fun toMetadata(): Map { + return metadata + mapOf( + "target" to target.compilerId, + "quantization" to quantization.compilerId, + "phaseOneScope" to MinervaExportBackend.phaseOneScope, + "generateHostHarness" to generateHostHarness.toString(), + "generateFirmwareExample" to generateFirmwareExample.toString(), + "runHostVerification" to runHostVerification.toString(), + "dumpWeights" to dumpWeights.toString() + ) + } + + private fun requireOptionalPath(field: String, value: String?) { + require(value == null || value.isNotBlank()) { "$field cannot be blank when provided" } + } +} + +/** + * Stable categories for Minerva export failures. + */ +public enum class MinervaExportFailureKind { + UNSUPPORTED_MODEL_TYPE, + RECORDING_FAILED, + GRAPH_VALIDATION_FAILED, + NOT_IMPLEMENTED +} + +/** + * Typed failure detail carried by [MinervaExportResult]. + */ +public data class MinervaExportFailure( + public val kind: MinervaExportFailureKind, + public val stage: GraphExportStage, + public val code: String, + public val message: String, + public val details: Map = emptyMap() +) { + init { + require(code.isNotBlank()) { "failure code cannot be blank" } + require(message.isNotBlank()) { "failure message cannot be blank" } + } +} + +/** + * Future successful output bundle for a Minerva export. + */ +public data class MinervaExportBundle( + public val projectName: String, + public val outputDir: String, + public val target: MinervaTarget, + public val quantization: MinervaQuantization, + public val generatedFiles: List = emptyList(), + public val manifestPath: String? = null +) { + init { + require(projectName.isNotBlank()) { "projectName cannot be blank" } + require(outputDir.isNotBlank()) { "outputDir cannot be blank" } + require(generatedFiles.all { it.isNotBlank() }) { "generatedFiles cannot contain blank paths" } + require(manifestPath == null || manifestPath.isNotBlank()) { + "manifestPath cannot be blank when provided" + } + } +} + +/** + * Public result shape for Minerva export attempts. + */ +public data class MinervaExportResult( + public val options: MinervaExportOptions, + public val status: GraphExportStatus, + public val bundle: MinervaExportBundle? = null, + public val diagnostics: GraphExportDiagnosticReport = GraphExportDiagnosticReport.empty(), + public val artifacts: List = emptyList(), + public val failure: MinervaExportFailure? = null, + public val metadata: Map = emptyMap() +) { + init { + require(status != GraphExportStatus.SUCCESS || bundle != null) { + "Successful Minerva exports must include a bundle" + } + require(status == GraphExportStatus.SUCCESS || failure != null) { + "Failed or skipped Minerva exports must include a failure" + } + } + + public val succeeded: Boolean + get() = status == GraphExportStatus.SUCCESS + + public val failed: Boolean + get() = status == GraphExportStatus.FAILED + + public fun requireSuccess(): MinervaExportBundle { + if (!succeeded) { + val reason = failure?.message ?: "unknown failure" + error("Minerva export did not succeed: $reason") + } + return bundle ?: error("Minerva export succeeded without a bundle") + } +} diff --git a/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/package.kt b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/package.kt new file mode 100644 index 000000000..8959946ac --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/src/commonMain/kotlin/sk/ainet/compile/minerva/package.kt @@ -0,0 +1,13 @@ +package sk.ainet.compile.minerva + +/** + * Minerva graph export support for secure MCU inference. + * + * The first implementation slice is intentionally JVM-first and API-only: it + * defines the SKaiNET-facing export surface and result model before the + * validator, lowering, compiler adapter, packager, and host verifier are added. + */ +public object MinervaExportBackend { + public const val backendName: String = "minerva" + public const val phaseOneScope: String = "jvm-sequential-mlp-q8" +} diff --git a/skainet-compile/skainet-compile-minerva/src/commonTest/kotlin/sk/ainet/compile/minerva/MinervaExportFacadeTest.kt b/skainet-compile/skainet-compile-minerva/src/commonTest/kotlin/sk/ainet/compile/minerva/MinervaExportFacadeTest.kt new file mode 100644 index 000000000..3d3e1c8f5 --- /dev/null +++ b/skainet-compile/skainet-compile-minerva/src/commonTest/kotlin/sk/ainet/compile/minerva/MinervaExportFacadeTest.kt @@ -0,0 +1,123 @@ +package sk.ainet.compile.minerva + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import sk.ainet.compile.export.GraphExportStatus +import sk.ainet.lang.graph.DefaultComputeGraph +import sk.ainet.lang.graph.GraphNode +import sk.ainet.lang.tensor.ops.InputOperation +import sk.ainet.lang.tensor.ops.TensorSpec +import sk.ainet.lang.types.DType + +class MinervaExportFacadeTest { + + @Test + fun createsFacadeAndDefaultOptions() { + val facade = MinervaExportFacade() + val options = testOptions() + + assertEquals(MinervaExportBackend.backendName, facade.backendName) + assertEquals(MinervaTarget.ATMEGA328P, options.target) + assertEquals(MinervaQuantization.Q8, options.quantization) + assertEquals("jvm-sequential-mlp-q8", options.toMetadata()["phaseOneScope"]) + } + + @Test + fun rejectsInvalidOptionsWithClearMessages() { + val outputError = assertFailsWith { + testOptions(outputDir = "") + } + assertTrue(outputError.message?.contains("outputDir cannot be blank") == true) + + val projectError = assertFailsWith { + testOptions(projectName = "nested/project") + } + assertTrue(projectError.message?.contains("simple project directory name") == true) + } + + @Test + fun exportGraphRejectsEmptyGraphBeforePlaceholderStage() { + val result = MinervaExportFacade().exportGraph(DefaultComputeGraph(), testOptions()) + + assertEquals(GraphExportStatus.FAILED, result.status) + assertFalse(result.succeeded) + assertEquals(MinervaExportFailureKind.GRAPH_VALIDATION_FAILED, result.failure?.kind) + assertTrue(result.diagnostics.hasErrors) + assertTrue(result.failure?.details?.values?.any { it.contains("at least one graph node") } == true) + } + + @Test + fun exportGraphReturnsNotImplementedForValidatedGraph() { + val result = MinervaExportFacade().exportGraph(singleInputGraph(), testOptions()) + + assertEquals(GraphExportStatus.FAILED, result.status) + assertEquals(MinervaExportFailureKind.NOT_IMPLEMENTED, result.failure?.kind) + assertEquals("minerva.export.not_implemented", result.failure?.code) + assertTrue(result.diagnostics.infos.any { it.code == "minerva.graph.validation.passed" }) + assertTrue(result.metadata["target"] == MinervaTarget.ATMEGA328P.compilerId) + assertFailsWith { + result.requireSuccess() + } + } + + @Test + fun exportModelAcceptsComputeGraphFastPath() { + val graph = singleInputGraph() + val result = MinervaExportFacade().exportModel(graph, testOptions()) + + assertEquals(MinervaExportFailureKind.NOT_IMPLEMENTED, result.failure?.kind) + } + + @Test + fun exportModelReportsUnsupportedModelWithoutForwardPass() { + val result = MinervaExportFacade().exportModel("not-a-graph", testOptions()) + + assertEquals(GraphExportStatus.FAILED, result.status) + assertEquals(MinervaExportFailureKind.UNSUPPORTED_MODEL_TYPE, result.failure?.kind) + assertTrue(result.failure?.message?.contains("forwardPass") == true) + } + + @Test + fun exportModelProvidesForwardPassRecordingOverload() { + val result = MinervaExportFacade().exportModel( + model = object {}, + forwardPass = { }, + options = testOptions(projectName = "RecordedModel") + ) + + assertEquals(GraphExportStatus.FAILED, result.status) + val failure = assertNotNull(result.failure) + assertTrue( + failure.kind == MinervaExportFailureKind.GRAPH_VALIDATION_FAILED || + failure.kind == MinervaExportFailureKind.RECORDING_FAILED + ) + } + + private fun testOptions( + outputDir: String = "build/minerva", + projectName: String = "TinyMlp" + ): MinervaExportOptions { + return MinervaExportOptions( + outputDir = outputDir, + projectName = projectName, + metadata = mapOf("test" to "true") + ) + } + + private fun singleInputGraph(): DefaultComputeGraph { + val graph = DefaultComputeGraph() + graph.addNode( + GraphNode( + id = "input", + operation = InputOperation(), + inputs = emptyList(), + outputs = listOf(TensorSpec("x", listOf(1, 4), "FP32")) + ) + ) + return graph + } +}