From 833fccf8701c96b2d0fdd02675a004d42491c69e Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Mon, 8 Jun 2026 15:37:05 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20render=20kernel=20matrix=20via=20build-?= =?UTF-8?q?logic=20=E2=86=92=20Antora=20(same=20pipeline=20as=20ops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the kernel × platform matrix with the operators.json → ops-status-matrix.adoc pipeline, replacing the standalone test-written .md. - KernelSupportMatrixTest now emits kernel-support.json (schema https://skainet.ai/schemas/kernel-support/v1) by introspecting the registered KernelProvider implementations (scalar floor auto-derived from supports(); SIMD/native tiers declared with the source-set→targets map). Keeps the scalar-floor drift gate. - New build-logic GenerateKernelMatrixTask + KernelSupportModule model render that JSON to docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc, registered as `generateKernelMatrix` in DocumentationPlugin (mirrors generateDocs). Timestamp omitted so the committed .adoc only changes when coverage changes. - Root build wires kernelInputFile/kernelOutputFile + generateKernelMatrix dependsOn the generator test; native-cpu test task stamps a stable skainet.version. nav.adoc gains the Reference entry next to the operator matrix. - Retire docs/kernel-support-matrix.md; the mindmap's companion link points at the Antora page. Verified: ./gradlew generateKernelMatrix renders Q6_K = panama-vector (post-#720) and is idempotent (no churn on re-run); native-cpu allTests green. Co-Authored-By: Claude Opus 4.8 --- .../src/main/kotlin/DocumentationExtension.kt | 6 ++ .../src/main/kotlin/DocumentationPlugin.kt | 11 +++ .../main/kotlin/GenerateKernelMatrixTask.kt | 81 ++++++++++++++++++ .../main/kotlin/models/KernelSupportModels.kt | 26 ++++++ build.gradle.kts | 9 ++ docs/eager-execution-backends-and-kernels.md | 8 +- docs/kernel-support-matrix.md | 20 ----- docs/modules/ROOT/nav.adoc | 1 + .../reference/kernel-support-matrix.adoc | 22 +++++ .../build.gradle.kts | 2 + .../exec/kernel/KernelSupportMatrixTest.kt | 85 +++++++++---------- 11 files changed, 201 insertions(+), 70 deletions(-) create mode 100644 build-logic/convention/src/main/kotlin/GenerateKernelMatrixTask.kt create mode 100644 build-logic/convention/src/main/kotlin/models/KernelSupportModels.kt delete mode 100644 docs/kernel-support-matrix.md create mode 100644 docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc diff --git a/build-logic/convention/src/main/kotlin/DocumentationExtension.kt b/build-logic/convention/src/main/kotlin/DocumentationExtension.kt index d1b6659b..d58fe7fc 100644 --- a/build-logic/convention/src/main/kotlin/DocumentationExtension.kt +++ b/build-logic/convention/src/main/kotlin/DocumentationExtension.kt @@ -14,4 +14,10 @@ open class DocumentationExtension(project: Project) { .convention(true) val generateIndex: Property = project.objects.property(Boolean::class.java) .convention(true) + + /** kernel-support.json emitted by KernelSupportMatrixTest (registry introspection). */ + val kernelInputFile: RegularFileProperty = project.objects.fileProperty() + + /** Rendered kernel × platform matrix Antora page. */ + val kernelOutputFile: RegularFileProperty = project.objects.fileProperty() } \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/DocumentationPlugin.kt b/build-logic/convention/src/main/kotlin/DocumentationPlugin.kt index 972f0c06..265768a7 100644 --- a/build-logic/convention/src/main/kotlin/DocumentationPlugin.kt +++ b/build-logic/convention/src/main/kotlin/DocumentationPlugin.kt @@ -20,6 +20,17 @@ class DocumentationPlugin : Plugin { } }) + // Kernel × platform matrix — kernel-side analogue of generateDocs. Renders the + // registry-introspected kernel-support.json into an Antora .adoc. + project.tasks.register("generateKernelMatrix", GenerateKernelMatrixTask::class.java, object : Action { + override fun execute(task: GenerateKernelMatrixTask) { + task.group = "documentation" + task.description = "Render the kernel × platform support matrix from kernel-support.json" + task.inputFile.set(extension.kernelInputFile) + task.outputFile.set(extension.kernelOutputFile) + } + }) + // Register schema validation task in plugin (migrated from skainet-lang-export-ops) val validateTaskProvider = project.tasks.register("validateOperatorSchema", SchemaValidationTask::class.java, object : Action { override fun execute(task: SchemaValidationTask) { diff --git a/build-logic/convention/src/main/kotlin/GenerateKernelMatrixTask.kt b/build-logic/convention/src/main/kotlin/GenerateKernelMatrixTask.kt new file mode 100644 index 00000000..8540cc7d --- /dev/null +++ b/build-logic/convention/src/main/kotlin/GenerateKernelMatrixTask.kt @@ -0,0 +1,81 @@ +import kotlinx.serialization.json.Json +import models.KernelSupportModule +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Renders `kernel-support.json` (registry-introspected by `KernelSupportMatrixTest`) into an + * Antora AsciiDoc matrix — the kernel-side counterpart of [GenerateDocumentationTask]'s + * `ops-status-matrix.adoc`. Rows are weight formats; columns are KMP platforms; each cell is + * the best provider that serves `FP32 × format` on that platform. + * + * Deliberately omits a generation timestamp so the committed `.adoc` only changes when the + * actual provider coverage changes (keeps the docs-CI staleness diff meaningful). + */ +@CacheableTask +abstract class GenerateKernelMatrixTask : DefaultTask() { + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val inputFile: RegularFileProperty + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @TaskAction + fun generate() { + val json = Json { ignoreUnknownKeys = true } + val module = json.decodeFromString(inputFile.get().asFile.readText()) + val out = outputFile.get().asFile + out.parentFile?.mkdirs() + + out.writeText(buildString { + appendLine("= Kernel × platform support matrix") + appendLine(":description: Which compute-kernel provider serves each weight format on each KMP target.") + appendLine("") + appendLine( + "Generated from `kernel-support.json` (version `${module.version}`) by " + + "`KernelSupportMatrixTest` — registry introspection of the registered " + + "`KernelProvider` implementations. Do not edit by hand; run " + + "`./gradlew generateKernelMatrix` to refresh.", + ) + appendLine("") + appendLine( + "Each cell is the best (highest-priority) provider that serves " + + "`${module.inputDtype} × format` `${module.op}` on that platform: " + + "*native-ffm* (100) → *panama-vector* (50) → *scalar* (0). An empty cell " + + "(`—`) means no provider carries a kernel there (the format is dequant-to-FP32 only).", + ) + appendLine("") + if (module.formats.isEmpty() || module.platforms.isEmpty()) { + appendLine("NOTE: No kernel-support data found in the source JSON.") + return@buildString + } + + val colSpec = (listOf("1") + List(module.platforms.size) { "1" }).joinToString(",") + appendLine("[cols=\"$colSpec\", options=\"header\"]") + appendLine("|===") + append("| Weight format ") + module.platforms.forEach { append("| $it ") } + appendLine("") + appendLine("") + module.formats.forEach { fmt -> + append("| `${fmt.name}` ") + module.platforms.forEach { p -> append("| ${fmt.byPlatform[p] ?: "—"} ") } + appendLine("") + } + appendLine("|===") + appendLine("") + appendLine( + "See also the eager backends & kernels mindmap " + + "(`docs/eager-execution-backends-and-kernels.md`) for the narrative overview and gaps.", + ) + }) + } +} diff --git a/build-logic/convention/src/main/kotlin/models/KernelSupportModels.kt b/build-logic/convention/src/main/kotlin/models/KernelSupportModels.kt new file mode 100644 index 00000000..ad61aeb8 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/models/KernelSupportModels.kt @@ -0,0 +1,26 @@ +package models + +import kotlinx.serialization.Serializable + +/** + * Machine-readable kernel × platform support, emitted by + * `KernelSupportMatrixTest` (registry introspection) and rendered to an Antora + * `.adoc` by `GenerateKernelMatrixTask` — the kernel-side analogue of + * `operators.json` → `ops-status-matrix.adoc`. + */ +@Serializable +data class KernelSupportModule( + val schema: String = "https://skainet.ai/schemas/kernel-support/v1", + val version: String = "", + val op: String = "matmul", + val inputDtype: String = "Float32", + val platforms: List = emptyList(), + val formats: List = emptyList(), +) + +@Serializable +data class KernelFormatSupport( + val name: String, + /** platform name -> best provider serving `inputDtype × name` there (or absent = none). */ + val byPlatform: Map = emptyMap(), +) diff --git a/build.gradle.kts b/build.gradle.kts index 96fc1b06..771b2133 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -109,12 +109,21 @@ documentation { outputDirectory.set(file("docs/modules/ROOT/pages/reference/operators/generated")) includeBackendStatus.set(true) generateIndex.set(true) + // Kernel × platform matrix — registry-introspected JSON (emitted by KernelSupportMatrixTest) + // rendered to an Antora page, mirroring the operators.json → ops-status-matrix.adoc pipeline. + kernelInputFile.set(file("skainet-backends/skainet-backend-native-cpu/build/generated/kernel-support/kernel-support.json")) + kernelOutputFile.set(file("docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc")) } tasks.named("generateDocs") { dependsOn(":skainet-lang:skainet-lang-core:kspCommonMainKotlinMetadata") } +tasks.named("generateKernelMatrix") { + // The generator test introspects the registered providers + emits kernel-support.json. + dependsOn(":skainet-backends:skainet-backend-native-cpu:jvmTest") +} + // Dokka aggregation – unified API reference across all library modules dokka { moduleName.set("SKaiNET") diff --git a/docs/eager-execution-backends-and-kernels.md b/docs/eager-execution-backends-and-kernels.md index 8f2aa33a..fa2a419c 100644 --- a/docs/eager-execution-backends-and-kernels.md +++ b/docs/eager-execution-backends-and-kernels.md @@ -89,6 +89,8 @@ those formats were JVM-only and broke on Native. - ❌ **Non-CPU eager backends** (IREE, Metal, GPU) — the `KernelProvider` SPI anticipates them, but none are implemented for the eager path today. > This mindmap is a hand-authored overview. Its companion -> [kernel × platform support matrix](kernel-support-matrix.md) is **machine-generated** from -> the registered `KernelProvider`s (`KernelSupportMatrixTest`) and CI-gated against drift in -> the scalar floor, so the per-platform coverage stays in sync with the code. +> [kernel × platform support matrix](modules/ROOT/pages/reference/kernel-support-matrix.adoc) is +> **machine-generated** from the registered `KernelProvider`s (`KernelSupportMatrixTest` → +> `kernel-support.json` → `generateKernelMatrix`, the kernel-side analogue of the +> `operators.json` → `ops-status-matrix.adoc` pipeline) and gated against scalar-floor drift, +> so the per-platform coverage stays in sync with the code. diff --git a/docs/kernel-support-matrix.md b/docs/kernel-support-matrix.md deleted file mode 100644 index b39abb14..00000000 --- a/docs/kernel-support-matrix.md +++ /dev/null @@ -1,20 +0,0 @@ -# Kernel × platform support matrix - -> Generated by `KernelSupportMatrixTest`. The scalar (all-platform) coverage is -> auto-derived from `KernelProvider.supports(...)`; re-run the test to refresh. -> Cell = best available provider for `FP32 × format` on that platform. - -| Weight format | JVM | Android | Native·linux | Native·apple | JS/WASM | -|---|:--:|:--:|:--:|:--:|:--:| -| `Float32` | native-ffm | panama-vector | scalar | scalar | scalar | -| `BFloat16` | native-ffm | panama-vector | scalar | scalar | scalar | -| `Q8_0` | native-ffm | panama-vector | scalar | scalar | scalar | -| `Q4_0` | native-ffm | panama-vector | scalar | scalar | scalar | -| `Q4_K` | native-ffm | panama-vector | scalar | scalar | scalar | -| `Q6_K` | panama-vector | panama-vector | scalar | scalar | scalar | -| `Q5_1` | panama-vector | panama-vector | scalar | scalar | scalar | -| `Q5_0` | panama-vector | panama-vector | scalar | scalar | scalar | - -Priority: native-ffm (100) → panama-vector (50) → scalar (0). Formats without any cell (e.g. Q5_K/Q2_K/Q3_K/IQ4) are dequant-to-FP32 only. - -See also the [eager backends & kernels mindmap](eager-execution-backends-and-kernels.md). diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 416f9db4..278537e1 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -18,6 +18,7 @@ ** xref:reference/graph-export-architecture.adoc[Graph export architecture] ** xref:reference/operators/generated/index.adoc[Operator reference] ** xref:reference/ops-status-matrix.adoc[Operator coverage matrix] +** xref:reference/kernel-support-matrix.adoc[Kernel × platform support] ** xref:reference/api.adoc[API reference (Dokka)] * Explanation ** xref:explanation/skainet-for-ai.adoc[SKaiNET for AI/ML] diff --git a/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc b/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc new file mode 100644 index 00000000..cedde8b5 --- /dev/null +++ b/docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc @@ -0,0 +1,22 @@ += Kernel × platform support matrix +:description: Which compute-kernel provider serves each weight format on each KMP target. + +Generated from `kernel-support.json` (version `0.28.1`) by `KernelSupportMatrixTest` — registry introspection of the registered `KernelProvider` implementations. Do not edit by hand; run `./gradlew generateKernelMatrix` to refresh. + +Each cell is the best (highest-priority) provider that serves `Float32 × format` `matmul` on that platform: *native-ffm* (100) → *panama-vector* (50) → *scalar* (0). An empty cell (`—`) means no provider carries a kernel there (the format is dequant-to-FP32 only). + +[cols="1,1,1,1,1,1", options="header"] +|=== +| Weight format | JVM | Android | Native·linux | Native·apple | JS/WASM + +| `Float32` | native-ffm | panama-vector | scalar | scalar | scalar +| `BFloat16` | native-ffm | panama-vector | scalar | scalar | scalar +| `Q8_0` | native-ffm | panama-vector | scalar | scalar | scalar +| `Q4_0` | native-ffm | panama-vector | scalar | scalar | scalar +| `Q4_K` | native-ffm | panama-vector | scalar | scalar | scalar +| `Q6_K` | panama-vector | panama-vector | scalar | scalar | scalar +| `Q5_1` | panama-vector | panama-vector | scalar | scalar | scalar +| `Q5_0` | panama-vector | panama-vector | scalar | scalar | scalar +|=== + +See also the eager backends & kernels mindmap (`docs/eager-execution-backends-and-kernels.md`) for the narrative overview and gaps. diff --git a/skainet-backends/skainet-backend-native-cpu/build.gradle.kts b/skainet-backends/skainet-backend-native-cpu/build.gradle.kts index 0dbc6747..9efc6865 100644 --- a/skainet-backends/skainet-backend-native-cpu/build.gradle.kts +++ b/skainet-backends/skainet-backend-native-cpu/build.gradle.kts @@ -120,6 +120,8 @@ val runBenchProperty = providers.systemProperty("skainet.runBench") tasks.withType().configureEach { jvmArgs("--enable-preview", "--enable-native-access=ALL-UNNAMED", "--add-modules", "jdk.incubator.vector") runBenchProperty.orNull?.let { systemProperty("skainet.runBench", it) } + // Stable version stamp for KernelSupportMatrixTest's kernel-support.json (avoids doc churn). + systemProperty("skainet.version", (findProperty("VERSION_NAME") ?: "dev").toString()) } tasks.withType().configureEach { diff --git a/skainet-backends/skainet-backend-native-cpu/src/jvmTest/kotlin/sk/ainet/exec/kernel/KernelSupportMatrixTest.kt b/skainet-backends/skainet-backend-native-cpu/src/jvmTest/kotlin/sk/ainet/exec/kernel/KernelSupportMatrixTest.kt index 66be594e..1565c4c4 100644 --- a/skainet-backends/skainet-backend-native-cpu/src/jvmTest/kotlin/sk/ainet/exec/kernel/KernelSupportMatrixTest.kt +++ b/skainet-backends/skainet-backend-native-cpu/src/jvmTest/kotlin/sk/ainet/exec/kernel/KernelSupportMatrixTest.kt @@ -6,11 +6,14 @@ import kotlin.test.assertEquals import sk.ainet.backend.api.kernel.KernelProvider /** - * Generates the kernel × platform support matrix (rendered to - * `build/kernel-support-matrix.md`) and **gates drift in the scalar floor**: the - * all-platform baseline coverage is auto-derived from `ScalarKernelProvider.supports(...)`, - * so adding/removing a scalar packed kernel without updating the docs fails this test (it - * runs under `java-tests` in CI). + * Emits `kernel-support.json` (introspected from the registered `KernelProvider`s) and + * **gates drift in the scalar floor**: the all-platform baseline coverage is auto-derived + * from `ScalarKernelProvider.supports(...)`, so adding/removing a scalar packed kernel + * without updating the docs fails this test (runs under `java-tests` in CI). + * + * The JSON is rendered to the Antora page `reference/kernel-support-matrix.adoc` by the + * build-logic `generateKernelMatrix` task — the kernel-side analogue of the + * `operators.json` → `ops-status-matrix.adoc` pipeline. * * The SIMD/native tiers (Panama, native-FFM) are env-availability-gated (`isAvailable()` * probes the JDK incubator module / the loaded `.so`), so their *capability* is declared @@ -20,55 +23,44 @@ class KernelSupportMatrixTest { private val formats = listOf("Float32", "BFloat16", "Q8_0", "Q4_0", "Q4_K", "Q6_K", "Q5_1", "Q5_0") - private data class Tier(val name: String, val priority: Int, val targets: Set, val formats: Set) + // platform key (display) -> the set of providers (by source-set) reaching it. + private val platforms = listOf("JVM", "Android", "Native·linux", "Native·apple", "JS/WASM") - // Source-set -> targets. commonMain reaches all; backend-cpu jvmMain -> {jvm,android}; - // backend-native-cpu jvmMain -> {jvm} (the native module declares only jvm()). - private val allTargets = setOf("jvm", "android", "native-linux", "native-apple", "js-wasm") + private data class Tier(val name: String, val priority: Int, val platforms: Set, val formats: Set) private fun scalarFormats(): Set = formats.filter { ScalarKernelProvider.supports("matmul", listOf("Float32", it)) }.toSet() + // Source-set -> platforms. commonMain reaches all; backend-cpu jvmMain -> {JVM,Android}; + // backend-native-cpu jvmMain -> {JVM} (the native module declares only jvm()). private fun tiers(): List = listOf( - Tier("scalar", 0, allTargets, scalarFormats()), - Tier("panama-vector", 50, setOf("jvm", "android"), + Tier("scalar", 0, platforms.toSet(), scalarFormats()), + Tier("panama-vector", 50, setOf("JVM", "Android"), setOf("Float32", "BFloat16", "Q8_0", "Q4_0", "Q4_K", "Q6_K", "Q5_1", "Q5_0")), - Tier("native-ffm", 100, setOf("jvm"), + Tier("native-ffm", 100, setOf("JVM"), setOf("Float32", "BFloat16", "Q8_0", "Q4_0", "Q4_K")), ) - private fun bestTier(fmt: String, target: String, tiers: List): Tier? = - tiers.filter { target in it.targets && fmt in it.formats }.maxByOrNull { it.priority } + private fun best(fmt: String, platform: String, tiers: List): String? = + tiers.filter { platform in it.platforms && fmt in it.formats }.maxByOrNull { it.priority }?.name - private fun render(tiers: List): String { - val cols = listOf("jvm" to "JVM", "android" to "Android", "native-linux" to "Native·linux", - "native-apple" to "Native·apple", "js-wasm" to "JS/WASM") + private fun renderJson(tiers: List): String { + val version = System.getProperty("skainet.version", "dev") val sb = StringBuilder() - sb.appendLine("# Kernel × platform support matrix") - sb.appendLine() - sb.appendLine("> Generated by `KernelSupportMatrixTest`. The scalar (all-platform) coverage is") - sb.appendLine("> auto-derived from `KernelProvider.supports(...)`; re-run the test to refresh.") - sb.appendLine("> Cell = best available provider for `FP32 × format` on that platform.") - sb.appendLine() - sb.append("| Weight format |") - cols.forEach { sb.append(" ${it.second} |") } - sb.appendLine() - sb.append("|---|") - cols.forEach { _ -> sb.append(":--:|") } - sb.appendLine() - for (fmt in formats) { - sb.append("| `$fmt` |") - for ((target, _) in cols) { - val t = bestTier(fmt, target, tiers) - sb.append(" ${t?.name ?: "—"} |") - } - sb.appendLine() + sb.append("{\n") + sb.append(" \"schema\": \"https://skainet.ai/schemas/kernel-support/v1\",\n") + sb.append(" \"version\": \"").append(version).append("\",\n") + sb.append(" \"op\": \"matmul\",\n") + sb.append(" \"inputDtype\": \"Float32\",\n") + sb.append(" \"platforms\": [").append(platforms.joinToString(", ") { "\"$it\"" }).append("],\n") + sb.append(" \"formats\": [\n") + formats.forEachIndexed { i, fmt -> + val cells = platforms.mapNotNull { p -> best(fmt, p, tiers)?.let { "\"$p\": \"$it\"" } } + sb.append(" {\"name\": \"").append(fmt).append("\", \"byPlatform\": {") + .append(cells.joinToString(", ")).append("}}") + sb.append(if (i == formats.lastIndex) "\n" else ",\n") } - sb.appendLine() - sb.appendLine("Priority: native-ffm (100) → panama-vector (50) → scalar (0). " + - "Formats without any cell (e.g. Q5_K/Q2_K/Q3_K/IQ4) are dequant-to-FP32 only.") - sb.appendLine() - sb.appendLine("See also the [eager backends & kernels mindmap](eager-execution-backends-and-kernels.md).") + sb.append(" ]\n}\n") return sb.toString() } @@ -81,17 +73,16 @@ class KernelSupportMatrixTest { assertEquals( setOf("Float32", "BFloat16", "Q8_0", "Q4_0", "Q4_K", "Q6_K", "Q5_1", "Q5_0"), scalarFormats(), - "ScalarKernelProvider coverage changed — update the matrix doc + this expected set", + "ScalarKernelProvider coverage changed — update the declared sets + run ./gradlew generateKernelMatrix", ) // Sanity: every provider singleton is a KernelProvider (compile-time anchor). val providers: List = listOf(ScalarKernelProvider, PanamaVectorKernelProvider, NativeKernelProvider) assertEquals(3, providers.size) - val md = render(tiers) - File("build").mkdirs() - File("build/kernel-support-matrix.md").writeText(md) - // Echo so CI logs carry the current matrix (easy to copy into docs/). - println("KERNEL_SUPPORT_MATRIX_BEGIN\n$md\nKERNEL_SUPPORT_MATRIX_END") + val jsonText = renderJson(tiers) + val outDir = File("build/generated/kernel-support").apply { mkdirs() } + File(outDir, "kernel-support.json").writeText(jsonText) + println("KERNEL_SUPPORT_JSON_BEGIN\n$jsonText\nKERNEL_SUPPORT_JSON_END") } }