Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ open class DocumentationExtension(project: Project) {
.convention(true)
val generateIndex: Property<Boolean> = 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()
}
11 changes: 11 additions & 0 deletions build-logic/convention/src/main/kotlin/DocumentationPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ class DocumentationPlugin : Plugin<Project> {
}
})

// 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<GenerateKernelMatrixTask> {
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<SchemaValidationTask> {
override fun execute(task: SchemaValidationTask) {
Expand Down
81 changes: 81 additions & 0 deletions build-logic/convention/src/main/kotlin/GenerateKernelMatrixTask.kt
Original file line number Diff line number Diff line change
@@ -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<KernelSupportModule>(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.",
)
})
}
}
Original file line number Diff line number Diff line change
@@ -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<String> = emptyList(),
val formats: List<KernelFormatSupport> = emptyList(),
)

@Serializable
data class KernelFormatSupport(
val name: String,
/** platform name -> best provider serving `inputDtype × name` there (or absent = none). */
val byPlatform: Map<String, String> = emptyMap(),
)
9 changes: 9 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 5 additions & 3 deletions docs/eager-execution-backends-and-kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 0 additions & 20 deletions docs/kernel-support-matrix.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
22 changes: 22 additions & 0 deletions docs/modules/ROOT/pages/reference/kernel-support-matrix.adoc
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions skainet-backends/skainet-backend-native-cpu/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ val runBenchProperty = providers.systemProperty("skainet.runBench")
tasks.withType<Test>().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<JavaExec>().configureEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>, val formats: Set<String>)
// 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<String>, val formats: Set<String>)

private fun scalarFormats(): Set<String> =
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<Tier> = 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>): Tier? =
tiers.filter { target in it.targets && fmt in it.formats }.maxByOrNull { it.priority }
private fun best(fmt: String, platform: String, tiers: List<Tier>): String? =
tiers.filter { platform in it.platforms && fmt in it.formats }.maxByOrNull { it.priority }?.name

private fun render(tiers: List<Tier>): 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<Tier>): 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()
}

Expand All @@ -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<KernelProvider> = 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")
}
}
Loading