From ede4d71694affa00ac2dbd64e3e11071bc8d74de Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 12 Aug 2026 21:14:42 +0200 Subject: [PATCH 1/4] Update samples to their latest matching SKaiNET release skainet-transformers hasn't caught up to the core 0.40.x line yet (tops out at 0.39.1 on Maven Central), so the version target is mixed: - Core-only samples (SinusApproximator, MNISTDemo, GloVeEmbeddings, TinyTransformer, MnistJavaDemo, skainet-ui) -> SKaiNET 0.40.1 - KllamaDemo (uses skainet-transformers) -> SKaiNET/transformers 0.39.1, the latest matched pair Every KMP build also needs Kotlin 2.3.21 -> 2.4.10, since 0.39.1 and 0.40.1 klibs both declare that floor. Along the way, sk.ainet.core:skainet-data-basic was renamed to skainet-data-simple starting at 0.36.0 (MnistJavaDemo, SinusApproximator, MNISTDemo catalogs); GloVeEmbeddings' Gradle wrapper was bumped from 9.0 to the repo-standard 9.5.1. KllamaDemo needed no source changes -- the 0.39.1 compatibility typealiases cover the 0.34.1 API surface it uses. Every sample was rebuilt and tested (JVM/wasm/js/android + relevant unit tests) against the new versions. Co-Authored-By: Claude Sonnet 5 --- GloVeEmbeddings/gradle/libs.versions.toml | 8 ++++---- GloVeEmbeddings/gradle/wrapper/gradle-wrapper.properties | 2 +- GloVeEmbeddings/settings.gradle.kts | 2 +- KllamaDemo/gradle/libs.versions.toml | 6 +++--- MNISTDemo/gradle/libs.versions.toml | 6 +++--- MnistJavaDemo/build.gradle.kts | 2 +- MnistJavaDemo/gradle/libs.versions.toml | 6 +++--- SinusApproximator/CHANGELOG.md | 6 ++++++ SinusApproximator/gradle/libs.versions.toml | 6 +++--- TinyTransformer/gradle/libs.versions.toml | 8 ++++---- skainet-ui/build.gradle.kts | 2 +- skainet-ui/gradle/libs.versions.toml | 2 +- 12 files changed, 31 insertions(+), 25 deletions(-) diff --git a/GloVeEmbeddings/gradle/libs.versions.toml b/GloVeEmbeddings/gradle/libs.versions.toml index fdad168..9ff62d1 100644 --- a/GloVeEmbeddings/gradle/libs.versions.toml +++ b/GloVeEmbeddings/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -# Must match the Kotlin version SKaiNET was built with: SKaiNET 0.34.0 ships -# klibs compiled with Kotlin 2.3.21, and an older compiler cannot read them +# Must match the Kotlin version SKaiNET was built with: SKaiNET 0.40.1 ships +# klibs compiled with Kotlin 2.4.10, and an older compiler cannot read them # (JS/wasm fail with "Symbol for Any not found"). -kotlin = "2.3.21" +kotlin = "2.4.10" # SKaiNET BOM version — published to Maven Central. The BOM pins every # sk.ainet.core:* artifact so individual libraries are declared without versions. -skainet = "0.34.0" +skainet = "0.40.1" # Compose Multiplatform + Android. Pinned to 1.10.1 to match the shared # :skainet-ui design system (included build): a mismatched Compose pulls two diff --git a/GloVeEmbeddings/gradle/wrapper/gradle-wrapper.properties b/GloVeEmbeddings/gradle/wrapper/gradle-wrapper.properties index 2dcec85..5dd3c01 100644 --- a/GloVeEmbeddings/gradle/wrapper/gradle-wrapper.properties +++ b/GloVeEmbeddings/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/GloVeEmbeddings/settings.gradle.kts b/GloVeEmbeddings/settings.gradle.kts index f17d67d..c2a850b 100644 --- a/GloVeEmbeddings/settings.gradle.kts +++ b/GloVeEmbeddings/settings.gradle.kts @@ -17,7 +17,7 @@ pluginManagement { dependencyResolutionManagement { repositories { - // Resolve everything from Maven Central (SKaiNET 0.34.0 is published there). + // Resolve everything from Maven Central (SKaiNET 0.40.1 is published there). // No mavenLocal: an unrestricted mavenLocal is consulted first and can contain // a partial kotlin-stdlib (JVM jar + POM, no klib variants) that shadows // Central's variant-aware metadata, breaking JS/wasm with "Missing stdlib class". diff --git a/KllamaDemo/gradle/libs.versions.toml b/KllamaDemo/gradle/libs.versions.toml index c3cf62f..aafed56 100644 --- a/KllamaDemo/gradle/libs.versions.toml +++ b/KllamaDemo/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidx-testExt = "1.3.0" composeHotReload = "1.0.0" composeMultiplatform = "1.10.1" junit = "4.13.2" -kotlin = "2.3.21" +kotlin = "2.4.10" kotlinx-browser = "0.5.0" kotlinx-coroutines = "1.11.0" kotlinx-datetime = "0.7.1" @@ -20,8 +20,8 @@ kotlinxIo = "0.9.0" ktor = "3.4.0" logback = "1.5.29" material3 = "1.10.0-alpha05" -skainet = "0.34.0" -skainet-transformers = "0.34.1" +skainet = "0.39.1" +skainet-transformers = "0.39.1" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/MNISTDemo/gradle/libs.versions.toml b/MNISTDemo/gradle/libs.versions.toml index 6ffb75a..4b27c24 100644 --- a/MNISTDemo/gradle/libs.versions.toml +++ b/MNISTDemo/gradle/libs.versions.toml @@ -15,11 +15,11 @@ androidx-testExt = "1.2.1" composeHotReload = "1.0.0" composeMultiplatform = "1.10.1" junit = "4.13.2" -kotlin = "2.3.21" +kotlin = "2.4.10" kotlinx-coroutines = "1.10.2" ktor = "3.1.3" logback = "1.5.18" -skainet = "0.34.0" +skainet = "0.40.1" kotlinxIo = "0.8.2" ktorClientCore = "3.3.3" ktorClientPlugins = "3.1.1" @@ -44,7 +44,7 @@ skainet-compile-dag = { module = "sk.ainet.core:skainet-compile-dag", version.re skainet-backend-cpu = { module = "sk.ainet.core:skainet-backend-cpu", version.ref = "skainet" } skainet-backend-cpu-jvm = { module = "sk.ainet.core:skainet-backend-cpu", version.ref = "skainet" } skainet-data-api = { module = "sk.ainet.core:skainet-data-api", version.ref = "skainet" } -skainet-data-simple = { module = "sk.ainet.core:skainet-data-basic", version.ref = "skainet" } +skainet-data-simple = { module = "sk.ainet.core:skainet-data-simple", version.ref = "skainet" } skainet-io-core = { module = "sk.ainet.core:skainet-io-core", version.ref = "skainet" } skainet-io-gguf = { module = "sk.ainet.core:skainet-io-gguf", version.ref = "skainet" } skainet-io-onnx = { module = "sk.ainet.core:skainet-io-onnx", version.ref = "skainet" } diff --git a/MnistJavaDemo/build.gradle.kts b/MnistJavaDemo/build.gradle.kts index 992543f..fcdbd8b 100644 --- a/MnistJavaDemo/build.gradle.kts +++ b/MnistJavaDemo/build.gradle.kts @@ -1,7 +1,7 @@ plugins { java application - kotlin("jvm") version "2.3.21" + kotlin("jvm") version "2.4.10" } java { diff --git a/MnistJavaDemo/gradle/libs.versions.toml b/MnistJavaDemo/gradle/libs.versions.toml index 8f41104..d593767 100644 --- a/MnistJavaDemo/gradle/libs.versions.toml +++ b/MnistJavaDemo/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -skainet = "0.34.0" -kotlin = "2.3.21" +skainet = "0.40.1" +kotlin = "2.4.10" kotlinx-coroutines = "1.10.2" kotlinx-io = "0.8.2" junit = "4.13.2" @@ -11,7 +11,7 @@ skainet-lang-core = { module = "sk.ainet.core:skainet-lang-core-jvm", version.re skainet-lang-models = { module = "sk.ainet.core:skainet-lang-models-jvm", version.ref = "skainet" } skainet-backend-cpu = { module = "sk.ainet.core:skainet-backend-cpu-jvm", version.ref = "skainet" } skainet-data-api = { module = "sk.ainet.core:skainet-data-api-jvm", version.ref = "skainet" } -skainet-data-simple = { module = "sk.ainet.core:skainet-data-basic-jvm", version.ref = "skainet" } +skainet-data-simple = { module = "sk.ainet.core:skainet-data-simple-jvm", version.ref = "skainet" } skainet-data-transform = { module = "sk.ainet.core:skainet-data-transform-jvm", version.ref = "skainet" } skainet-io-core = { module = "sk.ainet.core:skainet-io-core-jvm", version.ref = "skainet" } skainet-io-image = { module = "sk.ainet.core:skainet-io-image-jvm", version.ref = "skainet" } diff --git a/SinusApproximator/CHANGELOG.md b/SinusApproximator/CHANGELOG.md index 10db63c..24731a2 100644 --- a/SinusApproximator/CHANGELOG.md +++ b/SinusApproximator/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.40.1] - 2026-08-12 + +### Changed +- Update to SKaiNET 0.40.1 from Maven Central. +- Bump Kotlin to 2.4.10 to match the compiler SKaiNET's 0.40.1 klibs are built with. + ## [0.34.0] - 2026-07-07 ### Changed diff --git a/SinusApproximator/gradle/libs.versions.toml b/SinusApproximator/gradle/libs.versions.toml index a50aafc..6e5a650 100644 --- a/SinusApproximator/gradle/libs.versions.toml +++ b/SinusApproximator/gradle/libs.versions.toml @@ -19,11 +19,11 @@ composeHotReload = "1.0.0" # (getAdapterMaxTextureSize) at first render on macOS/Metal. composeMultiplatform = "1.10.1" junit = "4.13.2" -kotlin = "2.3.21" +kotlin = "2.4.10" kotlinx-coroutines = "1.10.2" ktor = "3.1.3" logback = "1.5.18" -skainet = "0.34.0" +skainet = "0.40.1" kotlinxIo = "0.8.2" [libraries] @@ -45,7 +45,7 @@ skainet-compile-dag = { module = "sk.ainet.core:skainet-compile-dag", version.re skainet-backend-cpu = { module = "sk.ainet.core:skainet-backend-cpu", version.ref = "skainet" } skainet-backend-cpu-jvm = { module = "sk.ainet.core:skainet-backend-cpu", version.ref = "skainet" } skainet-data-api = { module = "sk.ainet.core:skainet-data-api", version.ref = "skainet" } -skainet-data-simple = { module = "sk.ainet.core:skainet-data-basic", version.ref = "skainet" } +skainet-data-simple = { module = "sk.ainet.core:skainet-data-simple", version.ref = "skainet" } skainet-io-core = { module = "sk.ainet.core:skainet-io-core", version.ref = "skainet" } skainet-io-gguf = { module = "sk.ainet.core:skainet-io-gguf", version.ref = "skainet" } skainet-io-onnx = { module = "sk.ainet.core:skainet-io-onnx", version.ref = "skainet" } diff --git a/TinyTransformer/gradle/libs.versions.toml b/TinyTransformer/gradle/libs.versions.toml index e5e50ef..f14ae6f 100644 --- a/TinyTransformer/gradle/libs.versions.toml +++ b/TinyTransformer/gradle/libs.versions.toml @@ -12,13 +12,13 @@ composeHotReload = "1.0.0" # classpath, which crashes with UnsatisfiedLinkError # (getAdapterMaxTextureSize) at first render on macOS/Metal. composeMultiplatform = "1.10.1" -# Must match the Kotlin version SKaiNET was built with: SKaiNET 0.34.0 ships -# klibs compiled with Kotlin 2.3.21, and an older compiler cannot read them. -kotlin = "2.3.21" +# Must match the Kotlin version SKaiNET was built with: SKaiNET 0.40.1 ships +# klibs compiled with Kotlin 2.4.10, and an older compiler cannot read them. +kotlin = "2.4.10" kotlinx-coroutines = "1.10.2" # SKaiNET BOM version — published to Maven Central. The BOM pins every # sk.ainet.core:* artifact so individual libraries are declared without versions. -skainet = "0.34.0" +skainet = "0.40.1" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/skainet-ui/build.gradle.kts b/skainet-ui/build.gradle.kts index fcbe2ac..f636e0f 100644 --- a/skainet-ui/build.gradle.kts +++ b/skainet-ui/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "sk.ainet.ui" -version = "0.34.0" +version = "0.40.1" kotlin { jvmToolchain(21) diff --git a/skainet-ui/gradle/libs.versions.toml b/skainet-ui/gradle/libs.versions.toml index 14a72fd..86b0de3 100644 --- a/skainet-ui/gradle/libs.versions.toml +++ b/skainet-ui/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "8.12.3" android-compileSdk = "36" android-minSdk = "24" composeMultiplatform = "1.10.1" -kotlin = "2.3.21" +kotlin = "2.4.10" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } From c7346c9ef3a04c0bca12ebf892c2c5267908f3e7 Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 12 Aug 2026 21:15:11 +0200 Subject: [PATCH 2/4] Convert AndroidNeonLlmDemo into the KMP KernelRace sample AndroidNeonLlmDemo showed SKaiNET's hand-written ARM NEON kernels with an Android-only NEON-vs-scalar A/B race, but every other sample in this repo is Kotlin Multiplatform. This turns it into KernelRace: Android stays the focus (the race itself is inherently Android-only kernel-SPI territory), and the same engine now also runs on Desktop and in the browser as a KMP app. iOS is deferred until the multiplatform structure is proven out. - shared/: engine, model resolution and ChatViewModel are platform-neutral commonMain code with expect/actual seams per platform (LlamaRuntimeBuilder for runtime construction, ModelProvider for Android download/cache/asset vs. Desktop cache vs. Wasm's bundled-at-build-time bytes). ChatViewModel depends on a GenerativeEngine interface and an injectable dispatcher specifically so it's unit-testable with a fake on every target. - composeApp/: shared UI on the skainet-ui design system, with a kernelControls slot so the Android-only race UI (KernelRaceApp's kernel pinning, the NEON/SCALAR chips, the split-screen race button) never leaks into common code. - Real test coverage: PromptTemplate/TokenStats/ModelResolver unit tests, a ChatViewModel test suite (fake engine, virtual-time coroutine scheduler), and a GgufSmokeTest that loads the actual SmolLM2 GGUF and generates tokens end to end when the model is present locally. - CI workflow (first build CI in this repo): JVM/Android unit tests, Android debug APK assembly, and a wasm compile check on every PR touching KernelRace/ or skainet-ui/. Verified end to end: clean builds pass on Android/JVM/wasmJs, the Android debug APK packages the NEON .so libs, and the wasm production webpack build correctly bundles the ~145 MB GGUF via Compose resources. The original AndroidNeonLlmDemo snapshot remains available at the 2026_08_arm_android git tag. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/kernelrace-ci.yml | 63 +++++ AndroidNeonLlmDemo/.gitignore | 4 - AndroidNeonLlmDemo/README.md | 50 ---- AndroidNeonLlmDemo/app/build.gradle.kts | 75 ------ .../kotlin/sk/ainet/demo/ChatViewModel.kt | 119 ---------- .../main/kotlin/sk/ainet/demo/LlmEngine.kt | 103 --------- .../main/kotlin/sk/ainet/demo/MainActivity.kt | 180 --------------- .../main/kotlin/sk/ainet/demo/ModelSource.kt | 110 --------- .../src/main/kotlin/sk/ainet/demo/PerfLog.kt | 54 ----- .../main/kotlin/sk/ainet/demo/ProcessTag.kt | 18 -- AndroidNeonLlmDemo/build.gradle.kts | 5 - AndroidNeonLlmDemo/gradle.properties | 2 - AndroidNeonLlmDemo/gradle/libs.versions.toml | 36 --- KernelRace/.gitignore | 5 + KernelRace/.java-version | 1 + KernelRace/README.md | 107 +++++++++ .../THIRD_PARTY_LICENSES/Apache-2.0.txt | 202 ++++++++++++++++ KernelRace/THIRD_PARTY_LICENSES/NOTICE | 9 + KernelRace/build.gradle.kts | 7 + KernelRace/composeApp/build.gradle.kts | 160 +++++++++++++ .../src/androidMain}/AndroidManifest.xml | 12 +- .../ainet/samples/kernelrace/KernelRaceApp.kt | 85 ++++--- .../samples/kernelrace/KernelRaceControls.kt | 110 +++++++++ .../ainet/samples/kernelrace/MainActivity.kt | 33 +++ .../samples/kernelrace}/ScalarActivity.kt | 35 ++- .../drawable}/skainet_logo.png | Bin .../kotlin/sk/ainet/samples/kernelrace/App.kt | 17 ++ .../sk/ainet/samples/kernelrace/BuildInfo.kt | 5 + .../ainet/samples/kernelrace/ui/ChatScreen.kt | 138 +++++++++++ .../sk/ainet/samples/kernelrace/main.kt | 23 ++ .../sk/ainet/samples/kernelrace/main.kt | 26 +++ .../src/wasmJsMain/resources/app.html | 12 + .../src/wasmJsMain/resources/index.html | 216 ++++++++++++++++++ .../src/wasmJsMain/resources/logo.png | Bin 0 -> 213270 bytes .../src/wasmJsMain/resources/styles.css | 7 + .../docs/screenshots/split_race.png | Bin KernelRace/gradle.properties | 12 + KernelRace/gradle/libs.versions.toml | 60 +++++ .../gradle/wrapper/gradle-wrapper.properties | 4 +- {AndroidNeonLlmDemo => KernelRace}/gradlew | 0 .../gradlew.bat | 0 KernelRace/kotlin-js-store/wasm/yarn.lock | 8 + KernelRace/scripts/fetch-model.sh | 38 +++ .../settings.gradle.kts | 15 +- KernelRace/shared/build.gradle.kts | 83 +++++++ .../engine/LlamaRuntimeBuilder.android.kt | 36 +++ .../kernelrace/model/AndroidModelProvider.kt | 114 +++++++++ .../kernelrace/platform/Platform.android.kt | 28 +++ .../samples/kernelrace/domain/ChatModels.kt | 10 + .../samples/kernelrace/domain/TokenStats.kt | 31 +++ .../kernelrace/engine/LlamaRuntimeBuilder.kt | 44 ++++ .../samples/kernelrace/engine/LlmEngine.kt | 81 +++++++ .../kernelrace/engine/PromptTemplate.kt | 8 + .../samples/kernelrace/model/ModelData.kt | 14 ++ .../samples/kernelrace/model/ModelProvider.kt | 11 + .../samples/kernelrace/model/ModelResolver.kt | 27 +++ .../samples/kernelrace/platform/Platform.kt | 30 +++ .../samples/kernelrace/vm/ChatViewModel.kt | 98 ++++++++ .../kernelrace/domain/TokenStatsTest.kt | 45 ++++ .../kernelrace/engine/PromptTemplateTest.kt | 27 +++ .../kernelrace/model/ModelResolverTest.kt | 36 +++ .../engine/LlamaRuntimeBuilder.jvm.kt | 32 +++ .../kernelrace/model/DesktopModelProvider.kt | 81 +++++++ .../kernelrace/platform/Platform.jvm.kt | 9 + .../kernelrace/engine/GgufSmokeTest.kt | 39 ++++ .../kernelrace/vm/ChatViewModelTest.kt | 140 ++++++++++++ .../engine/LlamaRuntimeBuilder.wasmJs.kt | 9 + .../kernelrace/platform/Platform.wasmJs.kt | 9 + KernelRace/webapp.json | 14 ++ README.md | 13 ++ 70 files changed, 2406 insertions(+), 829 deletions(-) create mode 100644 .github/workflows/kernelrace-ci.yml delete mode 100644 AndroidNeonLlmDemo/.gitignore delete mode 100644 AndroidNeonLlmDemo/README.md delete mode 100644 AndroidNeonLlmDemo/app/build.gradle.kts delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ChatViewModel.kt delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/LlmEngine.kt delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/MainActivity.kt delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ModelSource.kt delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/PerfLog.kt delete mode 100644 AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ProcessTag.kt delete mode 100644 AndroidNeonLlmDemo/build.gradle.kts delete mode 100644 AndroidNeonLlmDemo/gradle.properties delete mode 100644 AndroidNeonLlmDemo/gradle/libs.versions.toml create mode 100644 KernelRace/.gitignore create mode 100644 KernelRace/.java-version create mode 100644 KernelRace/README.md create mode 100644 KernelRace/THIRD_PARTY_LICENSES/Apache-2.0.txt create mode 100644 KernelRace/THIRD_PARTY_LICENSES/NOTICE create mode 100644 KernelRace/build.gradle.kts create mode 100644 KernelRace/composeApp/build.gradle.kts rename {AndroidNeonLlmDemo/app/src/main => KernelRace/composeApp/src/androidMain}/AndroidManifest.xml (77%) rename AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/SkainetDemoApp.kt => KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceApp.kt (55%) create mode 100644 KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceControls.kt create mode 100644 KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/MainActivity.kt rename {AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo => KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace}/ScalarActivity.kt (51%) rename {AndroidNeonLlmDemo/app/src/main/res/drawable-nodpi => KernelRace/composeApp/src/commonMain/composeResources/drawable}/skainet_logo.png (100%) create mode 100644 KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/App.kt create mode 100644 KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/BuildInfo.kt create mode 100644 KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/ui/ChatScreen.kt create mode 100644 KernelRace/composeApp/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/main.kt create mode 100644 KernelRace/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/main.kt create mode 100644 KernelRace/composeApp/src/wasmJsMain/resources/app.html create mode 100644 KernelRace/composeApp/src/wasmJsMain/resources/index.html create mode 100644 KernelRace/composeApp/src/wasmJsMain/resources/logo.png create mode 100644 KernelRace/composeApp/src/wasmJsMain/resources/styles.css rename {AndroidNeonLlmDemo => KernelRace}/docs/screenshots/split_race.png (100%) create mode 100644 KernelRace/gradle.properties create mode 100644 KernelRace/gradle/libs.versions.toml rename {AndroidNeonLlmDemo => KernelRace}/gradle/wrapper/gradle-wrapper.properties (63%) rename {AndroidNeonLlmDemo => KernelRace}/gradlew (100%) rename {AndroidNeonLlmDemo => KernelRace}/gradlew.bat (100%) create mode 100644 KernelRace/kotlin-js-store/wasm/yarn.lock create mode 100755 KernelRace/scripts/fetch-model.sh rename {AndroidNeonLlmDemo => KernelRace}/settings.gradle.kts (67%) create mode 100644 KernelRace/shared/build.gradle.kts create mode 100644 KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.android.kt create mode 100644 KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/model/AndroidModelProvider.kt create mode 100644 KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.android.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/ChatModels.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/TokenStats.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlmEngine.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplate.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelData.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelProvider.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelResolver.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.kt create mode 100644 KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModel.kt create mode 100644 KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/domain/TokenStatsTest.kt create mode 100644 KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplateTest.kt create mode 100644 KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/model/ModelResolverTest.kt create mode 100644 KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.jvm.kt create mode 100644 KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/model/DesktopModelProvider.kt create mode 100644 KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.jvm.kt create mode 100644 KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/engine/GgufSmokeTest.kt create mode 100644 KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModelTest.kt create mode 100644 KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.wasmJs.kt create mode 100644 KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.wasmJs.kt create mode 100644 KernelRace/webapp.json diff --git a/.github/workflows/kernelrace-ci.yml b/.github/workflows/kernelrace-ci.yml new file mode 100644 index 0000000..47ae20d --- /dev/null +++ b/.github/workflows/kernelrace-ci.yml @@ -0,0 +1,63 @@ +name: KernelRace CI + +on: + pull_request: + paths: + - "KernelRace/**" + - "skainet-ui/**" + - ".github/workflows/kernelrace-ci.yml" + push: + branches: + - develop + paths: + - "KernelRace/**" + - "skainet-ui/**" + - ".github/workflows/kernelrace-ci.yml" + +concurrency: + group: kernelrace-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make gradlew executable + run: chmod +x KernelRace/gradlew + + # composeApp's Kotlin compile tasks depend on the bundled GGUF (fetchModel — see + # composeApp/build.gradle.kts) even for a plain compile check, since Compose resource + # processing needs the file present. Cache it so only the first run per model version + # pays the ~145 MB download. + - name: Cache bundled GGUF model + uses: actions/cache@v4 + with: + path: KernelRace/composeApp/src/commonMain/composeResources/files/SmolLM2-135M-Instruct-Q8_0.gguf + key: kernelrace-smollm2-q8_0-gguf-v1 + + # Unit tests on JVM + Android (no device), the Android debug APK assembly (proves the + # JNI kernel packaging), and a wasm compile check. Deliberately skips instrumented tests + # (needs a real device) to keep this a fast PR check. + - name: Build and test + working-directory: KernelRace + run: | + ./gradlew \ + :shared:jvmTest \ + :shared:testDebugUnitTest \ + :composeApp:assembleDebug \ + :shared:compileKotlinWasmJs \ + :composeApp:compileKotlinWasmJs \ + --console=plain diff --git a/AndroidNeonLlmDemo/.gitignore b/AndroidNeonLlmDemo/.gitignore deleted file mode 100644 index 761be67..0000000 --- a/AndroidNeonLlmDemo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.gradle/ -build/ -local.properties -app/src/main/assets/*.gguf diff --git a/AndroidNeonLlmDemo/README.md b/AndroidNeonLlmDemo/README.md deleted file mode 100644 index 74b1540..0000000 --- a/AndroidNeonLlmDemo/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# AndroidNeonLlmDemo - -On-device LLM chat on Android, accelerated by SKaiNET's hand-written ARM NEON -kernels — with a built-in **NEON vs scalar** comparison so you can measure the -difference on your own phone. - -SmolLM2-135M (Q8_0 GGUF) decodes fully on-device through -`skainet-backend-jni-cpu`: hand-written NEON matmul kernels behind a JNI -bridge, with two `.so` tiers (`armv8-a` and `armv8.2-a+fp16+dotprod`) selected -at runtime per device. - -## What it demonstrates - -- **~5-line integration**: `DecoderGgufWeightLoader` → `OptimizedLLMRuntime` → - `generateUntilStop`, streaming tokens into Compose (see `LlmEngine.kt`). -- **NEON | SCALAR switch**: two chips re-pin the kernel registry (engine - reloads on the next run) — same APK, same model, full-device A/B with a live - tok/s counter. After every generation the kernel time breakdown is logged - under the `SKAINET_DEMO` tag. -- **Split-screen race**: one button launches a second process with the scalar - provider pinned and starts both generations simultaneously. - - ![Split-screen race: NEON at 44.7 tok/s vs scalar at 9.3 tok/s](docs/screenshots/split_race.png) -- **Model delivery**: downloads the GGUF from the Hugging Face Hub on first - run (SKaiNET's Ktor fetcher, streamed to disk with progress). To go fully - offline instead, place `SmolLM2-135M-Instruct-Q8_0.gguf` in - `app/src/main/assets/` before building. -- **SKaiNET design system** from the shared [`../skainet-ui`](../skainet-ui) - module (theme, logo, `FadingRingLoader`). - -## Run - -Real ARM64 hardware shows the point best (an x86 emulator falls back to scalar): - -```sh -./gradlew :app:installDebug -``` - -Tap **Generate on-device** — the model (~145 MB) downloads on first use. Then -flip the **SCALAR** chip and generate again to see the difference, or tap -**Race against scalar** for the side-by-side version. - -Reference numbers (SmolLM2-135M Q8_0, SKaiNET 0.39.1): ~6.4× decode-kernel -throughput NEON vs scalar on a Pixel 8a; generation is matmul-bound since -0.39.1's eager-op fast paths. - -## Requirements - -- ARM64 Android device (minSdk 24, one APK covers armv8.0 through armv9) -- JDK 17+ for the build; no NDK needed (kernels ship inside the AAR) diff --git a/AndroidNeonLlmDemo/app/build.gradle.kts b/AndroidNeonLlmDemo/app/build.gradle.kts deleted file mode 100644 index 1217864..0000000 --- a/AndroidNeonLlmDemo/app/build.gradle.kts +++ /dev/null @@ -1,75 +0,0 @@ -import java.util.Properties - -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.compose.compiler) -} - -android { - namespace = "sk.ainet.demo" - compileSdk = 36 - - defaultConfig { - applicationId = "sk.ainet.demo" - minSdk = 24 - targetSdk = 36 - versionCode = 1 - versionName = "1.0" - // ARM only — the JNI NEON kernels are the whole point of the demo. - ndk { abiFilters += "arm64-v8a" } - - // Optional Hugging Face token for gated repos. Lives in local.properties - // (gitignored) as HF_TOKEN=hf_xxx — never in source, never committed. - val hfToken = Properties().apply { - rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use(::load) - }.getProperty("HF_TOKEN") ?: "" - buildConfigField("String", "HF_TOKEN", "\"$hfToken\"") - // Shown next to the logo — always matches the dependency in the catalog. - buildConfigField("String", "SKAINET_VERSION", "\"${libs.versions.skainet.asProvider().get()}\"") - } - - buildFeatures { - compose = true - buildConfig = true - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - -kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) - } -} - -dependencies { - // Shared SKaiNET design system from ../skainet-ui (composite build) - implementation("sk.ainet.ui:skainet-ui") - - implementation(platform(libs.skainet.bom)) - implementation(libs.skainet.lang.core) - implementation(libs.skainet.backend.cpu) - // Hand-written ARM NEON kernels, dispatched at runtime (armv8-a / armv8.2-a+fp16+dotprod) - implementation(libs.skainet.backend.jni.cpu) - // hf:// model download from the Hugging Face Hub - implementation(libs.skainet.data.source) - implementation(libs.skainet.io.core) - implementation(libs.skainet.io.gguf) - - implementation(platform(libs.skainet.transformers.bom)) - implementation(libs.skainet.transformers.core) - implementation(libs.skainet.transformers.runtime.kllama) - implementation(libs.skainet.transformers.inference.llama) - implementation(libs.skainet.transformers.agent) - implementation(libs.kotlinx.io.core) - - implementation(platform(libs.compose.bom)) - implementation(libs.compose.ui) - implementation(libs.compose.material3) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.kotlinx.coroutines.android) -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ChatViewModel.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ChatViewModel.kt deleted file mode 100644 index a445ebe..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ChatViewModel.kt +++ /dev/null @@ -1,119 +0,0 @@ -package sk.ainet.demo - -import android.app.Application -import android.os.SystemClock -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import android.util.Log -import sk.ainet.backend.api.kernel.KernelRegistry -import sk.ainet.exec.kernel.jni.JniKernelProvider -import sk.ainet.exec.kernel.jni.JniKernels -import sk.ainet.exec.tensor.ops.KernelProfile - -data class UiState( - val status: String = "Model not loaded", - val kernelTier: String = "", - val output: String = "", - val tokensPerSecond: Double? = null, - val busy: Boolean = false, - val scalarMode: Boolean = false, -) - -/** - * NEON tier label from hardware capability (dispatched .so tier), NOT from - * KernelRegistry — the registry is mutated by mode switches (a scalar-mode - * run leaves it as [scalar] until the next load), which made the NEON chip - * mislabel itself "SCALAR" after switching back. - */ -private fun neonTierLabel(): String = - if (JniKernelProvider.isAvailable()) "ARM NEON" else "NEON (unavailable)" - -/** Cross-process race trigger: main window broadcasts, scalar window listens. */ -const val ACTION_RACE = "sk.ainet.demo.action.RACE" -const val EXTRA_PROMPT = "prompt" - -class ChatViewModel(app: Application) : AndroidViewModel(app) { - - private val _state = MutableStateFlow( - UiState(kernelTier = if ((app as SkainetDemoApp).isScalarProcess) "SCALAR" else neonTierLabel()) - ) - val state: StateFlow = _state - - /** - * Fullscreen A/B switch for clean screen recordings: the whole phone runs - * either the NEON or the scalar kernels — no split-screen CPU sharing. - * Reloads the model on the next generate (kernel lookups are cached per - * execution context). - */ - fun setKernelMode(scalar: Boolean) { - if (_state.value.busy || _state.value.scalarMode == scalar) return - viewModelScope.launch { - getApplication().setKernelMode(if (scalar) KernelMode.SCALAR else KernelMode.NEON) - _state.value = _state.value.copy( - scalarMode = scalar, - kernelTier = if (scalar) "SCALAR" else neonTierLabel(), - output = "", - tokensPerSecond = null, - status = "Kernel mode: ${if (scalar) "scalar" else "NEON"} — model reloads on next run", - ) - } - } - - /** Load the engine (download + weights) without generating — arms a fair race. */ - fun preload() { - if (_state.value.busy) return - _state.value = _state.value.copy(busy = true) - viewModelScope.launch { - try { - getApplication().engine { progress -> - _state.value = _state.value.copy(status = progress) - } - _state.value = _state.value.copy(busy = false, status = "Model loaded — ready to race") - } catch (e: Exception) { - _state.value = _state.value.copy(busy = false, status = "Error: ${e.message ?: e.javaClass.simpleName}") - } - } - } - - fun generate(prompt: String) { - if (_state.value.busy || prompt.isBlank()) return - _state.value = _state.value.copy(busy = true, output = "", tokensPerSecond = null, status = "Loading model…") - - viewModelScope.launch { - try { - val engine = getApplication().engine { progress -> - _state.value = _state.value.copy(status = progress) - } - _state.value = _state.value.copy(status = "Generating") - KernelProfile.reset() - - var tokenCount = 0 - var firstTokenAt = 0L - withContext(Dispatchers.Default) { - engine.generate(prompt) { piece -> - val now = SystemClock.elapsedRealtime() - if (tokenCount == 0) firstTokenAt = now - tokenCount++ - val elapsedS = (now - firstTokenAt) / 1000.0 - _state.value = _state.value.copy( - output = _state.value.output + piece, - // decode tok/s, measured from the first emitted token (excludes prefill) - tokensPerSecond = if (elapsedS > 0.5) (tokenCount - 1) / elapsedS else null, - ) - } - } - _state.value = _state.value.copy(busy = false, status = "Done — $tokenCount tokens, fully on-device") - // Hard evidence of which kernels actually ran this generation. - Log.i("SKAINET_DEMO_${ProcessTag.suffix}", "providers=${KernelRegistry.availableNames()}") - Log.i("SKAINET_DEMO_${ProcessTag.suffix}", KernelProfile.report()) - } catch (e: Exception) { - _state.value = _state.value.copy(busy = false, status = "Error: ${e.message ?: e.javaClass.simpleName}") - } - } - } -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/LlmEngine.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/LlmEngine.kt deleted file mode 100644 index 9c2ab28..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/LlmEngine.kt +++ /dev/null @@ -1,103 +0,0 @@ -package sk.ainet.demo - -import android.os.SystemClock -import android.util.Log -import java.io.File -import sk.ainet.apps.kllama.agent.generateUntilStop -import sk.ainet.apps.llm.InferenceRuntime -import sk.ainet.apps.llm.OptimizedLLMMode -import sk.ainet.apps.llm.OptimizedLLMRuntime -import sk.ainet.apps.llm.Tokenizer -import sk.ainet.apps.llm.tokenizer.TokenizerFactory -import sk.ainet.context.DirectCpuExecutionContext -import sk.ainet.io.AndroidRandomAccessSource -import sk.ainet.io.model.QuantPolicy -import sk.ainet.lang.types.FP32 -import sk.ainet.models.llama.DecoderGgufWeightLoader -import sk.ainet.models.llama.LlamaNetworkLoader - -private val TAG = "SKAINET_DEMO_${ProcessTag.suffix}" - -/** - * Loads a GGUF LLM and streams generated tokens — the whole integration. - * - * The five lines inside [load] + [generate] are the Scene 4 shot of the - * demo video: GGUF in, tokens out, no Python, no C++ in the app build. - * The hand-written ARM NEON kernels (skainet-backend-jni-cpu) register - * themselves through the kernel SPI just by being on the classpath. - */ -class LlmEngine private constructor( - private val runtime: InferenceRuntime, - private val tokenizer: Tokenizer, -) { - - fun generate(prompt: String, maxTokens: Int = 200, onToken: (String) -> Unit): String { - runtime.reset() - // SmolLM2-Instruct is a ChatML model — a raw prompt makes it emit - // <|im_end|> immediately. Wrap in the ChatML envelope it was trained on. - val templated = "<|im_start|>user\n$prompt<|im_end|>\n<|im_start|>assistant\n" - val promptTokens = tokenizer.encode(templated) - PerfLog.event( - "generate_start", - "promptTokens" to promptTokens.size, - "maxTokens" to maxTokens, - "eos" to tokenizer.eosTokenId, - ) - val genStart = SystemClock.elapsedRealtime() - var firstTokenAt = 0L - var tokenCount = 0 - val result = runtime.generateUntilStop( - prompt = promptTokens, - maxTokens = maxTokens, - eosTokenId = tokenizer.eosTokenId, - temperature = 0.7f, - onToken = { tokenId -> - val now = SystemClock.elapsedRealtime() - if (tokenCount == 0) firstTokenAt = now - tokenCount++ - Log.i(TAG, "token #$tokenCount id=$tokenId '${tokenizer.decode(tokenId)}' +${now - genStart}ms") - onToken(tokenizer.decode(tokenId)) - }, - decode = { tokenizer.decode(it) }, - ) - val genEnd = SystemClock.elapsedRealtime() - // Decode speed excludes prefill: measured from the first emitted token. - val decodeS = (genEnd - firstTokenAt) / 1000.0 - PerfLog.event( - "generate_done", - "tokens" to tokenCount, - "ttftMs" to if (tokenCount > 0) firstTokenAt - genStart else null, - "totalMs" to genEnd - genStart, - "decodeTokPerSec" to if (tokenCount > 1 && decodeS > 0) "%.2f".format((tokenCount - 1) / decodeS) else null, - "textLen" to result.text.length, - ) - Log.i(TAG, "result text len=${result.text.length}: '${result.text.take(200)}'") - return result.text - } - - companion object { - - /** Call from Dispatchers.IO — streams weights, never materializes the file on the ART heap. */ - suspend fun load(gguf: File): LlmEngine { - // --- the five lines ------------------------------------------------- - val ctx = DirectCpuExecutionContext.create() - val weights = DecoderGgufWeightLoader( - randomAccessProvider = { AndroidRandomAccessSource.open(gguf.path) }, - quantPolicy = QuantPolicy.NATIVE_OPTIMIZED, // keep Q8_0 packed for the NEON kernels - acceptedArchitectures = setOf("llama", "mistral"), // SmolLM2 is llama-family - ).loadToMapStreaming(ctx) - val runtime = OptimizedLLMRuntime( - model = LlamaNetworkLoader.fromWeights(weights), - ctx = ctx, - mode = OptimizedLLMMode.DIRECT, - dtype = FP32::class, - bos = weights.metadata.bosTokenId, - ) - val tokenizer = AndroidRandomAccessSource.open(gguf.path) - .use { TokenizerFactory.fromGgufSource(it) } - // -------------------------------------------------------------------- - - return LlmEngine(runtime, tokenizer) - } - } -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/MainActivity.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/MainActivity.kt deleted file mode 100644 index 2b779bc..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/MainActivity.kt +++ /dev/null @@ -1,180 +0,0 @@ -package sk.ainet.demo - -import android.content.Intent -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import sk.ainet.ui.components.indeterminateOrbitingFadingRingLoader -import sk.ainet.ui.theme.SKaiNETTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { SKaiNETTheme { ChatScreen() } } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ChatScreen(viewModel: ChatViewModel = viewModel()) { - val state by viewModel.state.collectAsState() - var prompt by remember { mutableStateOf("Explain what a NEON (ARM) instruction is, in two sentences.") } - - Scaffold( - topBar = { - TopAppBar( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - painter = painterResource(R.drawable.skainet_logo), - contentDescription = "SKaiNET logo", - modifier = Modifier.size(36.dp), - ) - Spacer(Modifier.width(10.dp)) - Text("SKaiNET", style = MaterialTheme.typography.titleLarge) - Spacer(Modifier.width(6.dp)) - Text( - BuildConfig.SKAINET_VERSION, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.align(Alignment.Bottom).padding(bottom = 4.dp), - ) - } - }, - actions = { - state.tokensPerSecond?.let { - Text( - "%.1f tok/s".format(it), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(end = 16.dp), - ) - } - }, - ) - }, - ) { padding -> - Column( - Modifier.fillMaxSize().padding(padding) - .padding(horizontal = 16.dp, vertical = 4.dp) - ) { - val context0 = androidx.compose.ui.platform.LocalContext.current - val inScalarProcess = context0.applicationContext - .let { it as? SkainetDemoApp }?.isScalarProcess == true - if (inScalarProcess) { - AssistChip(onClick = {}, label = { Text(state.kernelTier) }) - } else { - // Fullscreen A/B switch: whole phone on one kernel path — - // record one run per mode and compare. - Row(verticalAlignment = Alignment.CenterVertically) { - FilterChip( - selected = !state.scalarMode, - onClick = { viewModel.setKernelMode(false) }, - enabled = !state.busy, - label = { Text(if (state.scalarMode) "NEON" else state.kernelTier) }, - ) - Spacer(Modifier.width(8.dp)) - FilterChip( - selected = state.scalarMode, - onClick = { viewModel.setKernelMode(true) }, - enabled = !state.busy, - label = { Text("SCALAR") }, - ) - } - } - // Status on its own line — with large font scales it doesn't fit - // next to the chips and used to blow up the row height. - Text( - state.status, - style = MaterialTheme.typography.bodySmall, - maxLines = 2, - modifier = Modifier.fillMaxWidth().padding(top = 2.dp), - ) - Spacer(Modifier.height(6.dp)) - - val loading = state.busy && state.output.isEmpty() - if (loading) { - // SKaiNET fading-ring loader while the model downloads/loads - Column( - Modifier.weight(1f).fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - indeterminateOrbitingFadingRingLoader(size = 120.dp) - Spacer(Modifier.height(16.dp)) - Text(state.status, style = MaterialTheme.typography.bodyMedium) - } - } else { - Text( - text = state.output.ifEmpty { "…" }, - modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()), - style = MaterialTheme.typography.bodyLarge, - ) - } - - Spacer(Modifier.height(8.dp)) - OutlinedTextField( - value = prompt, - onValueChange = { prompt = it }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Prompt") }, - ) - Spacer(Modifier.height(8.dp)) - Button( - onClick = { viewModel.generate(prompt) }, - enabled = !state.busy, - modifier = Modifier.fillMaxWidth(), - ) { - Text(if (state.busy) "Generating…" else "Generate on-device") - } - - val context = androidx.compose.ui.platform.LocalContext.current - val isMainProcess = context.applicationContext - .let { it as? SkainetDemoApp }?.isScalarProcess == false - if (isMainProcess) { - Spacer(Modifier.height(4.dp)) - // Saveable: entering split-screen recreates the Activity, and plain - // remember{} would silently disarm the race button. - var raceArmed by rememberSaveable { mutableStateOf(false) } - // Tap 1: open the scalar twin adjacent and preload BOTH engines. - // Tap 2: broadcast the prompt so both windows start in the same instant. - TextButton( - onClick = { - if (!raceArmed) { - context.startActivity( - Intent(context, ScalarActivity::class.java).addFlags( - Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK - ) - ) - viewModel.preload() - raceArmed = true - } else { - context.sendBroadcast( - Intent(ACTION_RACE) - .setPackage(context.packageName) - .putExtra(EXTRA_PROMPT, prompt) - ) - viewModel.generate(prompt) - } - }, - enabled = !state.busy, - modifier = Modifier.fillMaxWidth(), - ) { - Text(if (raceArmed) "Start race 🏁" else "Race against scalar (split screen)") - } - } - } - } -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ModelSource.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ModelSource.kt deleted file mode 100644 index e4c961e..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ModelSource.kt +++ /dev/null @@ -1,110 +0,0 @@ -package sk.ainet.demo - -import android.content.Context -import android.os.SystemClock -import java.io.File -import java.io.FileNotFoundException -import kotlinx.io.Buffer -import kotlinx.io.buffered -import kotlinx.io.files.SystemFileSystem -import sk.ainet.data.source.KtorRemoteDataSourceFetcher -import kotlinx.io.files.Path as KotlinxPath - -const val HF_REPO = "unsloth/SmolLM2-135M-Instruct-GGUF" -const val HF_FILE = "SmolLM2-135M-Instruct-Q8_0.gguf" - -/** - * Resolves the GGUF file: bundled asset if present (offline demo builds), - * otherwise streamed from the Hugging Face Hub via SKaiNET's Ktor fetcher - * into filesDir. Chunked copy straight to disk — the model never sits on - * the ART heap, and progress is reported per real received bytes. - * (Not the resolver's CachePolicy.Bypass path: that buffers the whole - * artifact in memory before returning — exactly what a 145 MB file on a - * 256 MB heap must avoid.) - * - * Both app processes (NEON and :scalar) share filesDir, so the model is - * fetched once per device. - */ -object ModelSource { - - suspend fun resolve(context: Context, onProgress: (String) -> Unit): File { - val dir = File(context.filesDir, "models").apply { mkdirs() } - val target = File(dir, HF_FILE) - if (target.exists()) { - PerfLog.event("model_cached", "file" to target.name, "fileMB" to target.length() / 1_000_000) - return target - } - - copyFromAssetsOrNull(context, target)?.let { - PerfLog.event("model_from_assets", "file" to it.name, "fileMB" to it.length() / 1_000_000) - return it - } - - return download(target, onProgress) - } - - private fun copyFromAssetsOrNull(context: Context, target: File): File? = try { - context.assets.open(HF_FILE).use { input -> - target.outputStream().use { output -> input.copyTo(output) } - } - target - } catch (_: FileNotFoundException) { - null - } - - private suspend fun download(target: File, onProgress: (String) -> Unit): File { - val url = "https://huggingface.co/$HF_REPO/resolve/main/$HF_FILE" - // HF_TOKEN comes from local.properties via BuildConfig (gated repos only). - val headers = BuildConfig.HF_TOKEN.takeIf { it.isNotBlank() } - ?.let { mapOf("Authorization" to "Bearer $it") } ?: emptyMap() - - // Stream to a .part sibling and rename so an interrupted download - // retries instead of leaving a truncated GGUF behind. - val tmp = File(target.parentFile, target.name + ".part") - val fetcher = KtorRemoteDataSourceFetcher() - PerfLog.event("download_start", "url" to url) - val startedAt = SystemClock.elapsedRealtime() - var received = 0L - try { - val content = fetcher.fetch(url, headers) - val totalMb = content.sizeBytes?.let { "%.0f".format(it / 1e6) } ?: "?" - content.source.use { source -> - SystemFileSystem.sink(KotlinxPath(tmp.path)).buffered().use { sink -> - val chunk = Buffer() - var lastReported = -1L - while (true) { - val n = source.readAtMostTo(chunk, 1024 * 1024) - if (n == -1L) break - sink.write(chunk, n) - received += n - val mb = received / 1_000_000 - if (mb != lastReported) { - lastReported = mb - onProgress("Downloading model… $mb / $totalMb MB") - } - } - } - } - check(tmp.renameTo(target)) { "rename failed: $tmp -> $target" } - val durS = (SystemClock.elapsedRealtime() - startedAt) / 1000.0 - PerfLog.event( - "download_done", - "fileMB" to received / 1_000_000, - "durMs" to (durS * 1000).toLong(), - "MBps" to if (durS > 0) "%.1f".format(received / 1e6 / durS) else null, - ) - return target - } catch (e: Exception) { - tmp.delete() - PerfLog.event( - "download_failed", - "receivedMB" to received / 1_000_000, - "durMs" to SystemClock.elapsedRealtime() - startedAt, - "error" to (e.message ?: e.javaClass.simpleName), - ) - throw e - } finally { - fetcher.close() - } - } -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/PerfLog.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/PerfLog.kt deleted file mode 100644 index 9e5ddc8..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/PerfLog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package sk.ainet.demo - -import android.os.Debug -import android.os.SystemClock -import android.util.Log - -/** - * One-line structured perf events for the demo, greppable with: - * - * adb logcat -s SKAINET_PERF_NEON:I SKAINET_PERF_SCALAR:I - * - * The tag carries the process suffix (NEON vs SCALAR — see [ProcessTag]), so - * the one-phone race can be watched as two side-by-side terminal windows: - * - * adb logcat -s SKAINET_PERF_NEON:I # window 1 - * adb logcat -s SKAINET_PERF_SCALAR:I # window 2 - * - * Every line carries the offset since process start plus a memory snapshot - * (ART heap used/max, native heap, total PSS), e.g.: - * - * event=model_load_done | +8123ms | heap=41/256MB | native=163MB | pss=310MB | durMs=6210 | kernels=neon - * - * Logcat itself prefixes the wall-clock timestamp. - */ -object PerfLog { - - val TAG = "SKAINET_PERF_${ProcessTag.suffix}" - - private const val MB = 1024 * 1024L - - private val processStart = SystemClock.elapsedRealtime() - - fun event(name: String, vararg details: Pair) { - val line = buildString { - append("event=").append(name) - append(" | +").append(SystemClock.elapsedRealtime() - processStart).append("ms") - append(" | ").append(memorySnapshot()) - for ((key, value) in details) { - if (value != null) append(" | ").append(key).append('=').append(value) - } - } - Log.i(TAG, line) - } - - /** Reading PSS costs a few ms — fine per event, never call per token. */ - private fun memorySnapshot(): String { - val rt = Runtime.getRuntime() - val heapUsed = (rt.totalMemory() - rt.freeMemory()) / MB - val heapMax = rt.maxMemory() / MB - val native = Debug.getNativeHeapAllocatedSize() / MB - val pssMb = Debug.MemoryInfo().also { Debug.getMemoryInfo(it) }.totalPss / 1024 - return "heap=$heapUsed/${heapMax}MB | native=${native}MB | pss=${pssMb}MB" - } -} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ProcessTag.kt b/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ProcessTag.kt deleted file mode 100644 index f9dfd0b..0000000 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ProcessTag.kt +++ /dev/null @@ -1,18 +0,0 @@ -package sk.ainet.demo - -import java.io.File - -/** - * ":scalar" vs main process, read once from /proc/self/cmdline (works pre- - * and post-API 28, no Context needed — usable from plain objects like - * [PerfLog]). Splits log tags so the one-phone NEON-vs-scalar race can be - * watched as two separately filterable `adb logcat` streams. - */ -internal object ProcessTag { - private val NUL_BYTE = 0.toChar() - - val isScalar: Boolean by lazy { - File("/proc/self/cmdline").readText().substringBefore(NUL_BYTE).endsWith(":scalar") - } - val suffix: String by lazy { if (isScalar) "SCALAR" else "NEON" } -} diff --git a/AndroidNeonLlmDemo/build.gradle.kts b/AndroidNeonLlmDemo/build.gradle.kts deleted file mode 100644 index b93419d..0000000 --- a/AndroidNeonLlmDemo/build.gradle.kts +++ /dev/null @@ -1,5 +0,0 @@ -plugins { - alias(libs.plugins.android.application) apply false - alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.compose.compiler) apply false -} diff --git a/AndroidNeonLlmDemo/gradle.properties b/AndroidNeonLlmDemo/gradle.properties deleted file mode 100644 index 7a9e1c2..0000000 --- a/AndroidNeonLlmDemo/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -android.useAndroidX=true diff --git a/AndroidNeonLlmDemo/gradle/libs.versions.toml b/AndroidNeonLlmDemo/gradle/libs.versions.toml deleted file mode 100644 index 9ae011e..0000000 --- a/AndroidNeonLlmDemo/gradle/libs.versions.toml +++ /dev/null @@ -1,36 +0,0 @@ -# Toolchain versions mirror the SKaiNET repos' own catalogs (release/0.39.0) -[versions] -agp = "8.12.3" -kotlin = "2.3.21" -compose-bom = "2026.04.01" -activity-compose = "1.12.4" -lifecycle = "2.9.4" -coroutines = "1.11.0" -skainet = "0.39.1" # official release from Maven Central -skainet-transformers = "0.39.1" # lock-step release against engine 0.39.1 - -[libraries] -skainet-bom = { module = "sk.ainet:skainet-bom", version.ref = "skainet" } -skainet-lang-core = { module = "sk.ainet.core:skainet-lang-core" } -skainet-backend-cpu = { module = "sk.ainet.core:skainet-backend-cpu" } -skainet-backend-jni-cpu = { module = "sk.ainet.core:skainet-backend-jni-cpu" } -skainet-data-source = { module = "sk.ainet.core:skainet-data-source" } -skainet-io-core = { module = "sk.ainet.core:skainet-io-core" } -skainet-io-gguf = { module = "sk.ainet.core:skainet-io-gguf" } -skainet-transformers-bom = { module = "sk.ainet.transformers:skainet-transformers-bom", version.ref = "skainet-transformers" } -skainet-transformers-core = { module = "sk.ainet.transformers:skainet-transformers-core" } -skainet-transformers-runtime-kllama = { module = "sk.ainet.transformers:skainet-transformers-runtime-kllama" } -skainet-transformers-inference-llama = { module = "sk.ainet.transformers:skainet-transformers-inference-llama" } -skainet-transformers-agent = { module = "sk.ainet.transformers:skainet-transformers-agent" } -kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version = "0.9.1" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } -androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } -compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } -compose-material3 = { module = "androidx.compose.material3:material3" } -compose-ui = { module = "androidx.compose.ui:ui" } -kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } - -[plugins] -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -android-application = { id = "com.android.application", version.ref = "agp" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/KernelRace/.gitignore b/KernelRace/.gitignore new file mode 100644 index 0000000..66c2275 --- /dev/null +++ b/KernelRace/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +build/ +local.properties +composeApp/src/commonMain/composeResources/files/*.gguf +composeApp/src/androidMain/assets/*.gguf diff --git a/KernelRace/.java-version b/KernelRace/.java-version new file mode 100644 index 0000000..5f39e91 --- /dev/null +++ b/KernelRace/.java-version @@ -0,0 +1 @@ +21.0 diff --git a/KernelRace/README.md b/KernelRace/README.md new file mode 100644 index 0000000..932d853 --- /dev/null +++ b/KernelRace/README.md @@ -0,0 +1,107 @@ +# Kernel Race + +On-device LLM chat, accelerated by SKaiNET's hand-written ARM NEON kernels on Android — with a +built-in **NEON vs scalar** comparison so you can measure the difference on your own phone. The +same Kotlin codebase also runs on Desktop and in the browser (WebAssembly), where it shows the +*other* kernel tiers SKaiNET dispatches to when there's no NEON hardware to target. + +SmolLM2-135M (Q8_0 GGUF) decodes fully on-device through `skainet-backend-jni-cpu` on Android: +hand-written NEON matmul kernels behind a JNI bridge, with two `.so` tiers (`armv8-a` and +`armv8.2-a+fp16+dotprod`) selected at runtime per device. + +This example began as the Android-only **AndroidNeonLlmDemo** (still available as a frozen +snapshot at the git tag [`2026_08_arm_android`](../../tree/2026_08_arm_android/AndroidNeonLlmDemo)) +and was converted into a proper Kotlin Multiplatform sample: shared engine/view-model logic, a +Compose Multiplatform UI, and platform-specific runtime construction for Android, Desktop and +Wasm. iOS is deferred until the multiplatform structure is proven out. + +## What it demonstrates + +- **~5-line integration**: `DecoderGgufWeightLoader` → `OptimizedLLMRuntime` → + `generateUntilStop`, streaming tokens into Compose (see `shared/.../engine/LlmEngine.kt` and + the per-platform `LlamaRuntimeBuilder.*.kt` actuals). +- **NEON | SCALAR switch** (Android only): two chips re-pin the kernel registry (engine reloads + on the next run) — same APK, same model, full-device A/B with a live tok/s counter. +- **Split-screen race** (Android only): one button launches a second process with the scalar + provider pinned and starts both generations simultaneously. + + ![Split-screen race: NEON at 44.7 tok/s vs scalar at 9.3 tok/s](docs/screenshots/split_race.png) +- **Cross-platform kernel tiers**: the same Kotlin `LlmEngine` runs on three different kernel + paths — Android's ARM NEON JNI kernels, Desktop's native-optimized file-based load, and Wasm's + in-memory FP32 fallback (browsers have no filesystem, so the model is bundled at build time + instead of downloaded). +- **Model delivery**: Android downloads the GGUF from the Hugging Face Hub on first run + (SKaiNET's Ktor fetcher, streamed to disk with progress) or uses a bundled asset if present; + Desktop downloads to a local cache dir; Wasm bundles the model into the production build via + `scripts/fetch-model.sh` (see the Web section below for the size trade-off this implies). +- **SKaiNET design system** from the shared [`../skainet-ui`](../skainet-ui) module (theme, + logo, `FadingRingLoader`). + +## Run + +### Android (the full experience) + +Real ARM64 hardware shows the point best (an x86 emulator falls back to scalar): + +```sh +./gradlew :composeApp:installDebug +``` + +Tap **Generate on-device** — the model (~145 MB) downloads on first use. Then flip the +**SCALAR** chip and generate again to see the difference, or tap **Race against scalar** for the +side-by-side version. To go fully offline, place `SmolLM2-135M-Instruct-Q8_0.gguf` in +`composeApp/src/androidMain/assets/` before building. + +Reference numbers (SmolLM2-135M Q8_0, SKaiNET 0.39.1): ~6.4× decode-kernel throughput NEON vs +scalar on a Pixel 8a. + +### Desktop + +```sh +./gradlew :composeApp:run +``` + +Downloads the model to `~/.skainet-examples/kernelrace/models/` on first run. No NEON kernels +here — this is SKaiNET's file-based native-optimized load path on plain JVM. + +### Web (Wasm) + +```sh +./gradlew :composeApp:wasmJsBrowserDevelopmentRun +``` + +The first build runs `scripts/fetch-model.sh`, which bundles the ~145 MB GGUF straight into the +production JS/Wasm bundle (browsers have no persistent filesystem to download into at runtime). +That's a deliberately large asset for a web page — acceptable for a local dev run or a +maintainer-approved deploy, but worth knowing about before wiring this into CI or a public +samples page. + +## Requirements + +- Android: ARM64 device (minSdk 24, one APK covers armv8.0 through armv9), JDK 21+ +- Desktop: JDK 21+ with the JDK Vector API (incubator) enabled — wired automatically by the + Gradle build +- Web: a Chromium-based browser for the wasm GC runtime + +## Architecture + +``` +KernelRace/ +├── shared/ # Engine, view-model, model resolution — platform-neutral +│ └── src/ +│ ├── commonMain/ # LlmEngine, ChatViewModel, ModelResolver (pure, unit-tested) +│ ├── androidMain/ # AndroidModelProvider, NEON-aware LlamaRuntimeBuilder actual +│ ├── jvmMain/ # DesktopModelProvider, file-based LlamaRuntimeBuilder actual +│ └── wasmJsMain/ # Bytes-only LlamaRuntimeBuilder actual (no filesystem) +└── composeApp/ # Compose Multiplatform UI + platform entry points + └── src/ + ├── commonMain/ # App/ChatScreen (skainet-ui themed), kernelControls slot + ├── androidMain/ # KernelRaceApp (kernel pinning), race UI, manifest + ├── jvmMain/ # Desktop window entry point + └── wasmJsMain/ # Browser entry point + bundled model resource +``` + +The race mechanics (multi-process kernel pinning, `KernelRegistry`, the split-screen button) are +entirely Android-specific and live in `composeApp/androidMain`. Common code only sees a +`kernelControls` composable slot and a `GenerativeEngine` interface, which is what makes +`shared`'s `ChatViewModel` unit-testable with a fake on every target — see `shared/src/*Test/`. diff --git a/KernelRace/THIRD_PARTY_LICENSES/Apache-2.0.txt b/KernelRace/THIRD_PARTY_LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/KernelRace/THIRD_PARTY_LICENSES/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/KernelRace/THIRD_PARTY_LICENSES/NOTICE b/KernelRace/THIRD_PARTY_LICENSES/NOTICE new file mode 100644 index 0000000..19aba81 --- /dev/null +++ b/KernelRace/THIRD_PARTY_LICENSES/NOTICE @@ -0,0 +1,9 @@ +This product bundles/downloads the SmolLM2-135M-Instruct language model (Q8_0 +quantization) released by Hugging Face (HuggingFaceTB), distributed under the +Apache License 2.0 (see Apache-2.0.txt in this directory). + +Model source: https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct +GGUF build: https://huggingface.co/unsloth/SmolLM2-135M-Instruct-GGUF +GGUF file: SmolLM2-135M-Instruct-Q8_0.gguf (~145 MB) + +Attribution: HuggingFaceTB. See https://github.com/huggingface/smollm. diff --git a/KernelRace/build.gradle.kts b/KernelRace/build.gradle.kts new file mode 100644 index 0000000..58a2e4a --- /dev/null +++ b/KernelRace/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false +} diff --git a/KernelRace/composeApp/build.gradle.kts b/KernelRace/composeApp/build.gradle.kts new file mode 100644 index 0000000..10b1033 --- /dev/null +++ b/KernelRace/composeApp/build.gradle.kts @@ -0,0 +1,160 @@ +import java.util.Properties +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +kotlin { + jvmToolchain(21) + + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + androidMain.dependencies { + implementation(libs.compose.uiTooling) + implementation(libs.androidx.activity.compose) + // Hand-written ARM NEON kernels — Android JNI only. + implementation(libs.skainet.backend.jni.cpu) + } + commonMain.dependencies { + implementation(projects.shared) + implementation("sk.ainet.ui:skainet-ui") + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.kotlinx.coroutines) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + } + wasmJsMain.dependencies { + implementation(libs.kotlinx.coroutines) + } + } +} + +android { + namespace = "sk.ainet.samples.kernelrace" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "sk.ainet.samples.kernelrace" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + // ARM only — the JNI NEON kernels are the whole point of the race. + ndk { abiFilters += "arm64-v8a" } + + // Optional Hugging Face token for gated repos. Lives in local.properties + // (gitignored) as HF_TOKEN=hf_xxx — never in source, never committed. + // SmolLM2 itself is a public repo, so this is normally left blank. + val hfToken = Properties().apply { + rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use(::load) + }.getProperty("HF_TOKEN") ?: "" + buildConfigField("String", "HF_TOKEN", "\"$hfToken\"") + // Shown next to the logo — always matches the dependency in the catalog. + buildConfigField("String", "SKAINET_VERSION", "\"${libs.versions.skainet.asProvider().get()}\"") + } + + buildFeatures { + compose = true + buildConfig = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +// Single source-of-truth for the JVM flags SKaiNET needs: the JDK Vector API (incubator) for +// SIMD-accelerated CPU ops on desktop. +val skainetSimdJvmArgs = listOf( + "--add-modules", "jdk.incubator.vector", + "--enable-preview", + "-Dskainet.cpu.vector.enabled=true", +) + +tasks.withType().configureEach { + jvmArgs(skainetSimdJvmArgs) +} + +tasks.withType().configureEach { + jvmArgs(skainetSimdJvmArgs) +} + +val fetchModel by tasks.registering(Exec::class) { + description = "Downloads the SmolLM2-135M-Instruct Q8_0 GGUF into composeResources/files. Skips if present." + group = "build setup" + val scriptPath = rootProject.layout.projectDirectory.file("scripts/fetch-model.sh") + commandLine("bash", scriptPath.asFile.absolutePath) + inputs.file(scriptPath) + val modelFile = rootProject.layout.projectDirectory.file( + "composeApp/src/commonMain/composeResources/files/SmolLM2-135M-Instruct-Q8_0.gguf" + ) + outputs.file(modelFile) + onlyIf { !modelFile.asFile.exists() || modelFile.asFile.length() < 100L * 1024 * 1024 } +} + +// Every Kotlin compile task AND every Compose-resources copy task depends on the model being on +// disk — Gradle 9's strict implicit-dependency validator fails otherwise, since the +// resource-copy task reads from a directory fetchModel writes into. +tasks.matching { + it.name.startsWith("compileKotlin") || + it.name.startsWith("compileJava") || + it.name.startsWith("convertXmlValueResourcesFor") || + it.name.startsWith("copyNonXmlValueResourcesFor") || + it.name.startsWith("prepareComposeResourcesTaskFor") || + it.name.startsWith("generateResourceAccessorsFor") +}.configureEach { dependsOn(fetchModel) } + +compose.desktop { + application { + mainClass = "sk.ainet.samples.kernelrace.MainKt" + + jvmArgs += skainetSimdJvmArgs + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "sk.ainet.samples.kernelrace" + packageVersion = "1.0.0" + } + } +} diff --git a/AndroidNeonLlmDemo/app/src/main/AndroidManifest.xml b/KernelRace/composeApp/src/androidMain/AndroidManifest.xml similarity index 77% rename from AndroidNeonLlmDemo/app/src/main/AndroidManifest.xml rename to KernelRace/composeApp/src/androidMain/AndroidManifest.xml index f395c92..bfe8695 100644 --- a/AndroidNeonLlmDemo/app/src/main/AndroidManifest.xml +++ b/KernelRace/composeApp/src/androidMain/AndroidManifest.xml @@ -1,13 +1,13 @@ - + @@ -23,13 +23,13 @@ + android:taskAffinity="sk.ainet.samples.kernelrace.scalar" /> diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/SkainetDemoApp.kt b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceApp.kt similarity index 55% rename from AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/SkainetDemoApp.kt rename to KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceApp.kt index 76fbc2f..3ebd2b7 100644 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/SkainetDemoApp.kt +++ b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceApp.kt @@ -1,39 +1,45 @@ -package sk.ainet.demo +package sk.ainet.samples.kernelrace import android.app.Application import android.content.ComponentCallbacks2 import android.os.Build -import android.os.SystemClock +import java.io.File +import java.util.ServiceLoader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import java.util.ServiceLoader import sk.ainet.backend.api.kernel.KernelProvider import sk.ainet.backend.api.kernel.KernelRegistry +import sk.ainet.context.DirectCpuExecutionContext import sk.ainet.exec.kernel.ScalarKernelProvider +import sk.ainet.samples.kernelrace.engine.GenerativeEngine +import sk.ainet.samples.kernelrace.engine.LlmEngine +import sk.ainet.samples.kernelrace.model.AndroidModelProvider +import sk.ainet.samples.kernelrace.platform.logEvent + +/** Which kernels the next engine (re)build pins — full-power A/B comparison. */ +enum class KernelMode { NEON, SCALAR } /** - * Application-scoped model holder: the engine is heavyweight, so it lives - * for the app lifetime and is dropped under memory pressure. + * Application-scoped model holder: the engine is heavyweight, so it lives for the app lifetime + * and is dropped under memory pressure. * * Kernel wiring happens here, once per process, before any matmul: - * - main process → ServiceLoader discovers the JNI NEON provider from - * skainet-backend-jni-cpu (highest priority wins). Done directly because - * backend-api's KernelServiceLoader is jvmMain-only — absent from the - * library's Android variant — while java.util.ServiceLoader works on ART. - * - ":scalar" process → [ScalarKernelProvider] is pinned first; every - * dispatch site only auto-installs when the registry is EMPTY, so the - * NEON provider never registers and this process stays scalar. - * This is what powers the one-phone split-screen race in the demo video. + * - main process → ServiceLoader discovers the JNI NEON provider from skainet-backend-jni-cpu + * (highest priority wins). Done directly because backend-api's KernelServiceLoader is + * jvmMain-only — absent from the library's Android variant — while java.util.ServiceLoader + * works on ART. + * - ":scalar" process → [ScalarKernelProvider] is pinned first; every dispatch site only + * auto-installs when the registry is EMPTY, so the NEON provider never registers and this + * process stays scalar. + * This is what powers the one-phone split-screen race. */ -/** Which kernels the next engine (re)build pins — full-power A/B comparison. */ -enum class KernelMode { NEON, SCALAR } - -class SkainetDemoApp : Application() { +class KernelRaceApp : Application() { private val mutex = Mutex() - private var engine: LlmEngine? = null + private var engine: GenerativeEngine? = null + private val modelProvider by lazy { AndroidModelProvider(this, hfToken = BuildConfig.HF_TOKEN) } val isScalarProcess: Boolean by lazy { currentProcessName().endsWith(":scalar") } @@ -41,13 +47,12 @@ class SkainetDemoApp : Application() { var kernelMode: KernelMode = KernelMode.NEON private set - /** Kernel lookups are cached per execution context, so switching modes - * drops the engine; the next generate reloads with the new pin. */ + /** Kernel lookups are cached per execution context, so switching modes drops the engine; + * the next generate reloads with the new pin. */ suspend fun setKernelMode(mode: KernelMode) = mutex.withLock { if (mode != kernelMode) { kernelMode = mode engine = null - PerfLog.event("kernel_mode_switch", "mode" to mode, "engineDropped" to true) } } @@ -58,42 +63,31 @@ class SkainetDemoApp : Application() { } else { ServiceLoader.load(KernelProvider::class.java).forEach { KernelRegistry.register(it) } } - PerfLog.event( + logEvent( "app_start", "process" to if (isScalarProcess) "scalar" else "main", "providers" to KernelRegistry.availableNames(), ) } - suspend fun engine(onProgress: (String) -> Unit = {}): LlmEngine = mutex.withLock { + suspend fun engine(onProgress: (String) -> Unit = {}): GenerativeEngine = mutex.withLock { engine ?: withContext(Dispatchers.IO) { - val gguf = ModelSource.resolve(this@SkainetDemoApp, onProgress) + val model = modelProvider.resolve(onProgress) onProgress("Loading model…") val kernels = if (isScalarProcess || kernelMode == KernelMode.SCALAR) "scalar" else "neon" - PerfLog.event( - "model_load_start", - "file" to gguf.name, - "fileMB" to gguf.length() / 1_000_000, - "kernels" to kernels, - ) - val loadStart = SystemClock.elapsedRealtime() - LlmEngine.load(gguf).also { + logEvent("model_load_start", "kernels" to kernels) + LlmEngine.load(DirectCpuExecutionContext(), model).also { if (isScalarProcess || kernelMode == KernelMode.SCALAR) { - // Android's platform ops factory re-registers ServiceLoader - // providers (JNI NEON included) on every context creation, - // which defeats the onCreate pin. Kernel lookups are lazy — - // resolved at the FIRST matmul — so re-pin scalar after the - // load has constructed the context, before any forward runs. + // Android's platform ops factory re-registers ServiceLoader providers (JNI + // included) on every context creation, which defeats the onCreate pin. + // Kernel lookups are lazy — resolved at the FIRST matmul — so re-pin scalar + // after the load has constructed the context, before any forward runs. KernelRegistry.clearForTesting() KernelRegistry.register(ScalarKernelProvider) } - // NEON mode needs no action: the load's context creation just - // re-registered the ServiceLoader providers (JNI at priority 100). - PerfLog.event( - "model_load_done", - "durMs" to SystemClock.elapsedRealtime() - loadStart, - "kernels" to kernels, - ) + // NEON mode needs no action: the load's context creation just re-registered + // the ServiceLoader providers (JNI at priority 100). + logEvent("model_load_done", "kernels" to kernels) } }.also { engine = it } } @@ -101,12 +95,11 @@ class SkainetDemoApp : Application() { override fun onTrimMemory(level: Int) { super.onTrimMemory(level) if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { - PerfLog.event("trim_memory", "level" to level, "engineDropped" to (engine != null)) engine = null // reloaded lazily on next generate } } private fun currentProcessName(): String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) getProcessName() - else java.io.File("/proc/self/cmdline").readText().substringBefore('\u0000') + else File("/proc/self/cmdline").readText().substringBefore(0.toChar()) } diff --git a/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceControls.kt b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceControls.kt new file mode 100644 index 0000000..135206e --- /dev/null +++ b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/KernelRaceControls.kt @@ -0,0 +1,110 @@ +package sk.ainet.samples.kernelrace + +import android.content.Intent +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AssistChip +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import sk.ainet.samples.kernelrace.platform.kernelTierLabel +import sk.ainet.samples.kernelrace.vm.ChatViewModel + +const val ACTION_RACE = "sk.ainet.samples.kernelrace.action.RACE" +const val EXTRA_PROMPT = "prompt" + +/** + * The Android-only race UI, injected into the platform-neutral [sk.ainet.samples.kernelrace.ui.ChatScreen] + * via its `kernelControls` slot. In the `:scalar` process this collapses to a read-only chip + * (that window is a race participant, not a driver); the main process gets the NEON/SCALAR + * switch plus the split-screen race button. + */ +@Composable +fun KernelRaceControls(busy: Boolean, viewModel: ChatViewModel, currentPrompt: () -> String) { + val context = LocalContext.current + val app = remember { context.applicationContext as KernelRaceApp } + + if (app.isScalarProcess) { + AssistChip(onClick = {}, label = { Text("SCALAR") }) + return + } + + val scope = rememberCoroutineScope() + var scalarMode by rememberSaveable { mutableStateOf(app.kernelMode == KernelMode.SCALAR) } + + // Fullscreen A/B switch: whole phone on one kernel path — record one run per mode and compare. + Row(verticalAlignment = Alignment.CenterVertically) { + FilterChip( + selected = !scalarMode, + onClick = { + scope.launch { + app.setKernelMode(KernelMode.NEON) + scalarMode = false + viewModel.onKernelModeChanged(kernelTierLabel(), scalarMode = false) + } + }, + enabled = !busy, + label = { Text(if (scalarMode) "NEON" else kernelTierLabel()) }, + ) + Spacer(Modifier.width(8.dp)) + FilterChip( + selected = scalarMode, + onClick = { + scope.launch { + app.setKernelMode(KernelMode.SCALAR) + scalarMode = true + viewModel.onKernelModeChanged("SCALAR", scalarMode = true) + } + }, + enabled = !busy, + label = { Text("SCALAR") }, + ) + } + + Spacer(Modifier.height(4.dp)) + + // Saveable: entering split-screen recreates the Activity, and plain remember{} would + // silently disarm the race button. + var raceArmed by rememberSaveable { mutableStateOf(false) } + // Tap 1: open the scalar twin adjacent and preload BOTH engines. + // Tap 2: broadcast the prompt so both windows start in the same instant. + TextButton( + onClick = { + if (!raceArmed) { + context.startActivity( + Intent(context, ScalarActivity::class.java).addFlags( + Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK + ) + ) + viewModel.preload() + raceArmed = true + } else { + context.sendBroadcast( + Intent(ACTION_RACE) + .setPackage(context.packageName) + .putExtra(EXTRA_PROMPT, currentPrompt()) + ) + viewModel.generate(currentPrompt()) + } + }, + enabled = !busy, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (raceArmed) "Start race 🏁" else "Race against scalar (split screen)") + } +} diff --git a/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/MainActivity.kt b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/MainActivity.kt new file mode 100644 index 0000000..019a0c1 --- /dev/null +++ b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/MainActivity.kt @@ -0,0 +1,33 @@ +package sk.ainet.samples.kernelrace + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import sk.ainet.samples.kernelrace.vm.ChatViewModel + +class MainActivity : ComponentActivity() { + + private val app get() = applicationContext as KernelRaceApp + + private val viewModel: ChatViewModel by viewModels { + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + ChatViewModel(loadModel = { onProgress -> app.engine(onProgress) }) as T + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + App( + viewModel = viewModel, + skainetVersion = BuildConfig.SKAINET_VERSION, + kernelControls = { busy, vm, currentPrompt -> KernelRaceControls(busy, vm, currentPrompt) }, + ) + } + } +} diff --git a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ScalarActivity.kt b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/ScalarActivity.kt similarity index 51% rename from AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ScalarActivity.kt rename to KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/ScalarActivity.kt index 7da654a..d86e724 100644 --- a/AndroidNeonLlmDemo/app/src/main/kotlin/sk/ainet/demo/ScalarActivity.kt +++ b/KernelRace/composeApp/src/androidMain/kotlin/sk/ainet/samples/kernelrace/ScalarActivity.kt @@ -1,4 +1,4 @@ -package sk.ainet.demo +package sk.ainet.samples.kernelrace import android.content.BroadcastReceiver import android.content.Context @@ -9,20 +9,29 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.core.content.ContextCompat -import sk.ainet.ui.theme.SKaiNETTheme +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import sk.ainet.samples.kernelrace.vm.ChatViewModel /** - * The "before" half of the split-screen race. Runs in the `:scalar` - * process, where SkainetDemoApp pins ScalarKernelProvider — same APK, - * same model file, same code, no NEON. + * The "before" half of the split-screen race. Runs in the `:scalar` process, where + * [KernelRaceApp] pins [sk.ainet.exec.kernel.ScalarKernelProvider] — same APK, same model file, + * same code, no NEON. * - * Preloads the engine on launch and starts generating when the main - * window broadcasts [ACTION_RACE], so both sides fire simultaneously - * from a single button. + * Preloads the engine on launch and starts generating when the main window broadcasts + * [ACTION_RACE], so both sides fire simultaneously from a single button. */ class ScalarActivity : ComponentActivity() { - private val viewModel: ChatViewModel by viewModels() + private val app get() = applicationContext as KernelRaceApp + + private val viewModel: ChatViewModel by viewModels { + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + ChatViewModel(loadModel = { onProgress -> app.engine(onProgress) }) as T + } + } private val raceReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -36,7 +45,13 @@ class ScalarActivity : ComponentActivity() { this, raceReceiver, IntentFilter(ACTION_RACE), ContextCompat.RECEIVER_NOT_EXPORTED, ) viewModel.preload() - setContent { SKaiNETTheme { ChatScreen(viewModel) } } + setContent { + App( + viewModel = viewModel, + skainetVersion = BuildConfig.SKAINET_VERSION, + kernelControls = { busy, vm, currentPrompt -> KernelRaceControls(busy, vm, currentPrompt) }, + ) + } } override fun onDestroy() { diff --git a/AndroidNeonLlmDemo/app/src/main/res/drawable-nodpi/skainet_logo.png b/KernelRace/composeApp/src/commonMain/composeResources/drawable/skainet_logo.png similarity index 100% rename from AndroidNeonLlmDemo/app/src/main/res/drawable-nodpi/skainet_logo.png rename to KernelRace/composeApp/src/commonMain/composeResources/drawable/skainet_logo.png diff --git a/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/App.kt b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/App.kt new file mode 100644 index 0000000..b39c41c --- /dev/null +++ b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/App.kt @@ -0,0 +1,17 @@ +package sk.ainet.samples.kernelrace + +import androidx.compose.runtime.Composable +import sk.ainet.samples.kernelrace.ui.ChatScreen +import sk.ainet.samples.kernelrace.vm.ChatViewModel +import sk.ainet.ui.theme.SKaiNETTheme + +@Composable +fun App( + viewModel: ChatViewModel, + skainetVersion: String, + kernelControls: @Composable (busy: Boolean, viewModel: ChatViewModel, currentPrompt: () -> String) -> Unit = { _, _, _ -> }, +) { + SKaiNETTheme { + ChatScreen(viewModel = viewModel, skainetVersion = skainetVersion, kernelControls = kernelControls) + } +} diff --git a/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/BuildInfo.kt b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/BuildInfo.kt new file mode 100644 index 0000000..b273f04 --- /dev/null +++ b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/BuildInfo.kt @@ -0,0 +1,5 @@ +package sk.ainet.samples.kernelrace + +/** Desktop/wasm display version — keep in sync with `skainet` in gradle/libs.versions.toml. + * Android reads the real catalog value at build time via `BuildConfig.SKAINET_VERSION` instead. */ +const val SKAINET_VERSION = "0.39.1" diff --git a/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/ui/ChatScreen.kt b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/ui/ChatScreen.kt new file mode 100644 index 0000000..0cdc132 --- /dev/null +++ b/KernelRace/composeApp/src/commonMain/kotlin/sk/ainet/samples/kernelrace/ui/ChatScreen.kt @@ -0,0 +1,138 @@ +package sk.ainet.samples.kernelrace.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.painterResource +import sk.ainet.samples.kernelrace.vm.ChatViewModel +import sk.ainet.ui.components.indeterminateOrbitingFadingRingLoader +import kernelrace.composeapp.generated.resources.Res +import kernelrace.composeapp.generated.resources.skainet_logo + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatScreen( + viewModel: ChatViewModel, + skainetVersion: String, + kernelControls: @Composable (busy: Boolean, viewModel: ChatViewModel, currentPrompt: () -> String) -> Unit, +) { + val state by viewModel.state.collectAsState() + var prompt by remember { mutableStateOf("Explain what a NEON (ARM) instruction is, in two sentences.") } + + Scaffold( + topBar = { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(Res.drawable.skainet_logo), + contentDescription = "SKaiNET logo", + modifier = Modifier.size(36.dp), + ) + Spacer(Modifier.width(10.dp)) + Text("SKaiNET", style = MaterialTheme.typography.titleLarge) + Spacer(Modifier.width(6.dp)) + Text( + skainetVersion, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + actions = { + state.tokensPerSecond?.let { + Text( + "${formatFixed1(it)} tok/s", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(end = 16.dp), + ) + } + }, + ) + }, + ) { padding -> + Column( + Modifier.fillMaxSize().padding(padding) + .padding(horizontal = 16.dp, vertical = 4.dp) + ) { + kernelControls(state.busy, viewModel) { prompt } + + Text( + state.status, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + modifier = Modifier.fillMaxWidth().padding(top = 2.dp), + ) + Spacer(Modifier.height(6.dp)) + + val loading = state.busy && state.output.isEmpty() + if (loading) { + Column( + Modifier.weight(1f).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + indeterminateOrbitingFadingRingLoader(size = 120.dp) + Spacer(Modifier.height(16.dp)) + Text(state.status, style = MaterialTheme.typography.bodyMedium) + } + } else { + Text( + text = state.output.ifEmpty { "…" }, + modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()), + style = MaterialTheme.typography.bodyLarge, + ) + } + + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = prompt, + onValueChange = { prompt = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Prompt") }, + ) + Spacer(Modifier.height(8.dp)) + Button( + onClick = { viewModel.generate(prompt) }, + enabled = !state.busy, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (state.busy) "Generating…" else "Generate on-device") + } + } + } +} + +private fun formatFixed1(value: Double): String { + val rounded = kotlin.math.round(value * 10) / 10 + val whole = rounded.toLong() + val frac = kotlin.math.round((rounded - whole) * 10).toInt().let { if (it < 0) -it else it } + return "$whole.$frac" +} diff --git a/KernelRace/composeApp/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/main.kt b/KernelRace/composeApp/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/main.kt new file mode 100644 index 0000000..269aa5b --- /dev/null +++ b/KernelRace/composeApp/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/main.kt @@ -0,0 +1,23 @@ +package sk.ainet.samples.kernelrace + +import androidx.compose.runtime.remember +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import sk.ainet.context.DirectCpuExecutionContext +import sk.ainet.samples.kernelrace.engine.LlmEngine +import sk.ainet.samples.kernelrace.model.DesktopModelProvider +import sk.ainet.samples.kernelrace.model.ModelData +import sk.ainet.samples.kernelrace.vm.ChatViewModel + +fun main() = application { + Window(onCloseRequest = ::exitApplication, title = "SKaiNET Kernel Race") { + val viewModel = remember { + ChatViewModel(loadModel = { onProgress -> + val model = DesktopModelProvider().resolve(onProgress) as ModelData.FilePath + onProgress("Building runtime…") + LlmEngine.load(DirectCpuExecutionContext(), model) + }) + } + App(viewModel = viewModel, skainetVersion = SKAINET_VERSION) + } +} diff --git a/KernelRace/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/main.kt b/KernelRace/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/main.kt new file mode 100644 index 0000000..7d17410 --- /dev/null +++ b/KernelRace/composeApp/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/main.kt @@ -0,0 +1,26 @@ +package sk.ainet.samples.kernelrace + +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import kernelrace.composeapp.generated.resources.Res +import kotlinx.browser.document +import sk.ainet.context.DirectCpuExecutionContext +import sk.ainet.samples.kernelrace.engine.LlmEngine +import sk.ainet.samples.kernelrace.model.ModelData +import sk.ainet.samples.kernelrace.vm.ChatViewModel + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport(document.body!!) { + val viewModel = remember { + ChatViewModel(loadModel = { onProgress -> + onProgress("Downloading model bundle…") + val bytes = Res.readBytes("files/SmolLM2-135M-Instruct-Q8_0.gguf") + onProgress("Building runtime…") + LlmEngine.load(DirectCpuExecutionContext(), ModelData.Bytes(bytes)) + }) + } + App(viewModel = viewModel, skainetVersion = SKAINET_VERSION) + } +} diff --git a/KernelRace/composeApp/src/wasmJsMain/resources/app.html b/KernelRace/composeApp/src/wasmJsMain/resources/app.html new file mode 100644 index 0000000..9aa46fc --- /dev/null +++ b/KernelRace/composeApp/src/wasmJsMain/resources/app.html @@ -0,0 +1,12 @@ + + + + + + Kernel Race + + + + + + diff --git a/KernelRace/composeApp/src/wasmJsMain/resources/index.html b/KernelRace/composeApp/src/wasmJsMain/resources/index.html new file mode 100644 index 0000000..b809585 --- /dev/null +++ b/KernelRace/composeApp/src/wasmJsMain/resources/index.html @@ -0,0 +1,216 @@ + + + + + + Kernel Race - SKaiNET Examples + + + + + + +
+ +
+ +
+ +
+ + + +
+ © 2026 SKaiNET. All rights reserved. · + ~145 MB model download on first load — NEON vs scalar race itself is Android-only; this build runs the scalar kernel path. +
+ + + + diff --git a/KernelRace/composeApp/src/wasmJsMain/resources/logo.png b/KernelRace/composeApp/src/wasmJsMain/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3ae74ca10331e3f7aa6f1584709e115b37b9744e GIT binary patch literal 213270 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

N9>pL60c6!ebV%t9SCO6aGc%fHBn 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 Hy?y1T?5L*?MQE<*V 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

S1!Q^}mvly-(@WOu`($pzwGy{IM0-Co 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/dev/null || stat -c%s "$DEST_FILE") + if (( size > MIN_SIZE_BYTES )); then + echo "Model already present at $DEST_FILE ($(( size / 1024 / 1024 )) MB) — skipping." + exit 0 + fi + echo "Existing $DEST_FILE looks truncated ($(( size / 1024 / 1024 )) MB) — re-downloading." + rm -f "$DEST_FILE" +fi + +echo "Fetching SmolLM2-135M-Instruct-Q8_0.gguf (~145 MB) from Hugging Face..." +curl -L --fail --progress-bar -o "$DEST_FILE" "$MODEL_URL" + +size=$(stat -f%z "$DEST_FILE" 2>/dev/null || stat -c%s "$DEST_FILE") +if (( size < MIN_SIZE_BYTES )); then + echo "Downloaded file is suspiciously small ($(( size / 1024 / 1024 )) MB). Aborting." >&2 + rm -f "$DEST_FILE" + exit 1 +fi + +echo "Model saved to $DEST_FILE ($(( size / 1024 / 1024 )) MB)." diff --git a/AndroidNeonLlmDemo/settings.gradle.kts b/KernelRace/settings.gradle.kts similarity index 67% rename from AndroidNeonLlmDemo/settings.gradle.kts rename to KernelRace/settings.gradle.kts index fa99a9b..f791703 100644 --- a/AndroidNeonLlmDemo/settings.gradle.kts +++ b/KernelRace/settings.gradle.kts @@ -1,4 +1,5 @@ -rootProject.name = "AndroidNeonLlmDemo" +rootProject.name = "KernelRace" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { @@ -24,15 +25,13 @@ dependencyResolutionManagement { } } mavenCentral() - mavenLocal { - mavenContent { - includeGroupAndSubgroups("sk.ainet") - } - } } } -// Shared SKaiNET design system (theme, logo colors, FadingRingLoader) +// Shared SKaiNET design system (theme + components). Consumed as an included +// build so the example always uses the local source, matching the sibling +// KllamaDemo / TinyTransformer examples. includeBuild("../skainet-ui") -include(":app") +include(":composeApp") +include(":shared") diff --git a/KernelRace/shared/build.gradle.kts b/KernelRace/shared/build.gradle.kts new file mode 100644 index 0000000..0dc217d --- /dev/null +++ b/KernelRace/shared/build.gradle.kts @@ -0,0 +1,83 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) +} + +kotlin { + jvmToolchain(21) + + androidTarget() + + jvm() + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.coroutines) + implementation(libs.kotlinx.io.core) + implementation(libs.androidx.lifecycle.viewmodel) + + // SKaiNET core. BOM applied via api() so its version constraints also reach + // composeApp's classpath — implementation-scoped constraints stay internal to + // this module and leave downstream consumers with unversioned coordinates. + api(project.dependencies.platform(libs.skainet.bom)) + // api, not implementation: composeApp constructs DirectCpuExecutionContext (and + // reads ExecutionContext/FP32/etc.) directly, so these need to be on its classpath. + api(libs.skainet.lang.core) + api(libs.skainet.backend.cpu) + api(libs.skainet.io.core) + api(libs.skainet.io.gguf) + + // SKaiNET-transformers: Llama inference + generation. agent and + // runtime-kllama both publish wasmJs targets as of 0.39.1, so the + // whole engine (unlike KllamaDemo's 0.34.1-era jvmMain-only split) + // lives in commonMain. + api(project.dependencies.platform(libs.skainet.transformers.bom)) + api(libs.skainet.transformers.inference.llama) + api(libs.skainet.transformers.core) + api(libs.skainet.transformers.runtime.kllama) + api(libs.skainet.transformers.agent) + } + androidMain.dependencies { + // hf:// / https:// model download from the Hugging Face Hub + implementation(libs.skainet.data.source) + // Hand-written ARM NEON kernels, dispatched at runtime (armv8-a / armv8.2-a+fp16+dotprod) + implementation(libs.skainet.backend.jni.cpu) + } + jvmMain.dependencies { + implementation(libs.skainet.data.source) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + } + } +} + +tasks.withType().configureEach { + // SIMD-accelerated CPU ops via JDK Vector API (incubator). Same flags the + // composeApp desktop run uses, so jvmTest exercises the same code path. + jvmArgs( + "--add-modules", "jdk.incubator.vector", + "--enable-preview", + "-Dskainet.cpu.vector.enabled=true", + ) +} + +android { + namespace = "sk.ainet.samples.kernelrace.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } +} diff --git a/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.android.kt b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.android.kt new file mode 100644 index 0000000..6a20fe1 --- /dev/null +++ b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.android.kt @@ -0,0 +1,36 @@ +package sk.ainet.samples.kernelrace.engine + +import sk.ainet.apps.llm.OptimizedLLMMode +import sk.ainet.apps.llm.OptimizedLLMRuntime +import sk.ainet.apps.llm.tokenizer.TokenizerFactory +import sk.ainet.context.ExecutionContext +import sk.ainet.io.AndroidRandomAccessSource +import sk.ainet.io.model.QuantPolicy +import sk.ainet.lang.types.FP32 +import sk.ainet.models.llama.DecoderGgufWeightLoader +import sk.ainet.models.llama.LlamaNetworkLoader +import sk.ainet.samples.kernelrace.model.ModelData + +/** + * The "five lines" fast path: random-access reads straight off the file, + * `QuantPolicy.NATIVE_OPTIMIZED` keeps Q8_0 packed for the hand-written ARM NEON kernels + * (skainet-backend-jni-cpu), which register themselves through the kernel SPI just by being + * on the classpath. + */ +actual suspend fun buildLlamaComponents(ctx: ExecutionContext, model: ModelData): LlamaComponents { + val path = (model as ModelData.FilePath).path + val weights = DecoderGgufWeightLoader( + randomAccessProvider = { AndroidRandomAccessSource.open(path) }, + quantPolicy = QuantPolicy.NATIVE_OPTIMIZED, + acceptedArchitectures = setOf("llama", "mistral"), // SmolLM2 is llama-family + ).loadToMapStreaming(ctx) + val runtime = OptimizedLLMRuntime( + model = LlamaNetworkLoader.fromWeights(weights), + ctx = ctx, + mode = OptimizedLLMMode.DIRECT, + dtype = FP32::class, + bos = weights.metadata.bosTokenId, + ) + val tokenizer = AndroidRandomAccessSource.open(path).use { TokenizerFactory.fromGgufSource(it) } + return LlamaComponents(runtime, tokenizer) +} diff --git a/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/model/AndroidModelProvider.kt b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/model/AndroidModelProvider.kt new file mode 100644 index 0000000..efb8f6c --- /dev/null +++ b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/model/AndroidModelProvider.kt @@ -0,0 +1,114 @@ +package sk.ainet.samples.kernelrace.model + +import android.content.Context +import java.io.File +import java.io.FileNotFoundException +import kotlinx.io.Buffer +import kotlinx.io.buffered +import kotlinx.io.files.SystemFileSystem +import kotlin.time.TimeSource +import sk.ainet.data.source.KtorRemoteDataSourceFetcher +import sk.ainet.samples.kernelrace.platform.logEvent +import kotlinx.io.files.Path as KotlinxPath + +/** + * Resolves the GGUF file: bundled asset if present (offline demo builds), otherwise streamed + * from the Hugging Face Hub via SKaiNET's Ktor fetcher into filesDir. Chunked copy straight to + * disk — the model never sits on the ART heap, and progress is reported per real received bytes. + * (Not the resolver's CachePolicy.Bypass path: that buffers the whole artifact in memory before + * returning — exactly what a 145 MB file on a 256 MB heap must avoid.) + * + * Both app processes (NEON and :scalar) share filesDir, so the model is fetched once per device. + */ +class AndroidModelProvider( + private val context: Context, + private val hfToken: String = "", +) : ModelProvider { + + override suspend fun resolve(onProgress: (String) -> Unit): ModelData { + val dir = File(context.filesDir, "models").apply { mkdirs() } + val target = File(dir, HF_FILE) + + return when (val plan = ModelResolver.plan(target.path, target.exists(), assetAvailable(context))) { + is ResolutionPlan.UseCached -> { + logEvent("model_cached", "file" to target.name, "fileMB" to target.length() / 1_000_000) + ModelData.FilePath(plan.path) + } + is ResolutionPlan.UseAsset -> { + copyFromAssets(context, target) + logEvent("model_from_assets", "file" to target.name, "fileMB" to target.length() / 1_000_000) + ModelData.FilePath(plan.path) + } + ResolutionPlan.Download -> { + download(target, onProgress) + ModelData.FilePath(target.path) + } + } + } + + private fun assetAvailable(context: Context): Boolean = try { + context.assets.open(HF_FILE).close() + true + } catch (_: FileNotFoundException) { + false + } + + private fun copyFromAssets(context: Context, target: File) { + context.assets.open(HF_FILE).use { input -> + target.outputStream().use { output -> input.copyTo(output) } + } + } + + private suspend fun download(target: File, onProgress: (String) -> Unit) { + val url = "https://huggingface.co/$HF_REPO/resolve/main/$HF_FILE" + val headers = hfToken.takeIf { it.isNotBlank() }?.let { mapOf("Authorization" to "Bearer $it") } ?: emptyMap() + + val tmp = File(target.parentFile, ModelResolver.partPath(target.name)) + val fetcher = KtorRemoteDataSourceFetcher() + logEvent("download_start", "url" to url) + val startedAt = TimeSource.Monotonic.markNow() + var received = 0L + try { + val content = fetcher.fetch(url, headers) + val totalMb = content.sizeBytes?.let { formatWholeMb(it.toDouble()) } ?: "?" + content.source.use { source -> + SystemFileSystem.sink(KotlinxPath(tmp.path)).buffered().use { sink -> + val chunk = Buffer() + var lastReported = -1L + while (true) { + val n = source.readAtMostTo(chunk, 1024 * 1024) + if (n == -1L) break + sink.write(chunk, n) + received += n + val mb = received / 1_000_000 + if (mb != lastReported) { + lastReported = mb + onProgress("Downloading model… $mb / $totalMb MB") + } + } + } + } + check(tmp.renameTo(target)) { "rename failed: $tmp -> $target" } + val durMs = startedAt.elapsedNow().inWholeMilliseconds + logEvent( + "download_done", + "fileMB" to received / 1_000_000, + "durMs" to durMs, + "MBps" to if (durMs > 0) formatWholeMb(received / (durMs / 1000.0)) else null, + ) + } catch (e: Exception) { + tmp.delete() + logEvent( + "download_failed", + "receivedMB" to received / 1_000_000, + "durMs" to startedAt.elapsedNow().inWholeMilliseconds, + "error" to (e.message ?: e::class.simpleName), + ) + throw e + } finally { + fetcher.close() + } + } +} + +private fun formatWholeMb(bytes: Double): String = (bytes / 1e6).toLong().toString() diff --git a/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.android.kt b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.android.kt new file mode 100644 index 0000000..c8e28fc --- /dev/null +++ b/KernelRace/shared/src/androidMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.android.kt @@ -0,0 +1,28 @@ +package sk.ainet.samples.kernelrace.platform + +import android.util.Log +import java.io.File +import sk.ainet.exec.kernel.jni.JniKernelProvider + +private const val NUL_CHAR = 0.toChar() + +/** + * NEON tier label from hardware capability (dispatched .so tier), NOT from KernelRegistry — + * the registry is mutated by mode switches (a scalar-mode run leaves it as scalar until the + * next load), which would mislabel the NEON chip "SCALAR" after switching back. + */ +actual fun kernelTierLabel(): String = + if (JniKernelProvider.isAvailable()) "ARM NEON" else "NEON (unavailable)" + +actual val supportsKernelRace: Boolean = true + +/** ":scalar" vs main process, read once from /proc/self/cmdline — splits log tags so the + * one-phone NEON-vs-scalar race can be watched as two separately filterable `adb logcat` streams. */ +private val processTag: String by lazy { + val cmdline = File("/proc/self/cmdline").readText().substringBefore(NUL_CHAR) + if (cmdline.endsWith(":scalar")) "SCALAR" else "NEON" +} + +internal actual fun platformLog(line: String) { + Log.i("SKAINET_PERF_$processTag", line) +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/ChatModels.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/ChatModels.kt new file mode 100644 index 0000000..e8494ee --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/ChatModels.kt @@ -0,0 +1,10 @@ +package sk.ainet.samples.kernelrace.domain + +data class UiState( + val status: String = "Model not loaded", + val kernelTier: String = "", + val output: String = "", + val tokensPerSecond: Double? = null, + val busy: Boolean = false, + val scalarMode: Boolean = false, +) diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/TokenStats.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/TokenStats.kt new file mode 100644 index 0000000..d5d13fa --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/domain/TokenStats.kt @@ -0,0 +1,31 @@ +package sk.ainet.samples.kernelrace.domain + +/** + * Running decode-speed estimate from streamed token arrival times. + * + * Excludes prefill: the clock starts at the *first* emitted token, not at + * generation start, so prompt-processing time never pollutes the tok/s + * number. Also excludes the first token itself from the count (there is no + * decode interval before it), and requires at least half a second of signal + * before reporting a rate — otherwise a couple of fast tokens produce a + * wildly noisy estimate. + */ +class TokenStats { + private var firstTokenAtMs: Long = -1 + var tokenCount: Int = 0 + private set + + /** Call once per emitted token with the current elapsed time (any monotonic clock, ms). */ + fun onToken(nowMs: Long) { + if (tokenCount == 0) firstTokenAtMs = nowMs + tokenCount++ + } + + /** Decode tokens/sec so far, or null if there isn't enough signal yet. */ + fun tokensPerSecond(nowMs: Long): Double? { + if (tokenCount == 0 || firstTokenAtMs < 0) return null + val elapsedS = (nowMs - firstTokenAtMs) / 1000.0 + if (elapsedS <= 0.5) return null + return (tokenCount - 1) / elapsedS + } +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.kt new file mode 100644 index 0000000..470ef3c --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.kt @@ -0,0 +1,44 @@ +package sk.ainet.samples.kernelrace.engine + +import kotlinx.io.Buffer +import sk.ainet.apps.llm.OptimizedLLMMode +import sk.ainet.apps.llm.OptimizedLLMRuntime +import sk.ainet.apps.llm.Tokenizer +import sk.ainet.apps.llm.tokenizer.GGUFTokenizer +import sk.ainet.context.ExecutionContext +import sk.ainet.io.model.QuantPolicy +import sk.ainet.lang.types.FP32 +import sk.ainet.models.llama.LlamaNetworkLoader +import sk.ainet.samples.kernelrace.model.ModelData + +/** Runtime + tokenizer built from the same GGUF bytes — kept together since both come from one load. */ +class LlamaComponents(val runtime: OptimizedLLMRuntime, val tokenizer: Tokenizer) + +/** + * Platform-split loader: + * - Android takes the "five lines" fast path: random-access reads straight off the file, + * `QuantPolicy.NATIVE_OPTIMIZED` keeps Q8_0 packed for the hand-written ARM NEON kernels. + * - Desktop JVM takes the same file-based path (no NEON kernels to dispatch to on non-Android + * hardware, but still avoids materializing the whole file on-heap). + * - wasmJs has no filesystem, so it falls back to [buildLlamaComponentsFallback]: a sequential + * in-memory `Buffer` + `QuantPolicy.DEQUANTIZE_TO_FP32`. Same correctness, much slower — + * the packed-quant kernels are JVM/Android-only. + */ +expect suspend fun buildLlamaComponents(ctx: ExecutionContext, model: ModelData): LlamaComponents + +/** Multiplatform fallback used by wasmJs (and available to every platform for testing). */ +suspend fun buildLlamaComponentsFallback(ctx: ExecutionContext, bytes: ByteArray): LlamaComponents { + val tokenizer = GGUFTokenizer.fromSource(Buffer().apply { write(bytes) }, false) + val sourceProvider = { Buffer().apply { write(bytes) } } + val model = LlamaNetworkLoader + .fromGguf(sourceProvider, QuantPolicy.DEQUANTIZE_TO_FP32, false) + .load(ctx) + val runtime = OptimizedLLMRuntime( + model = model, + ctx = ctx, + mode = OptimizedLLMMode.DIRECT, + dtype = FP32::class, + bos = tokenizer.bosTokenId, + ) + return LlamaComponents(runtime, tokenizer) +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlmEngine.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlmEngine.kt new file mode 100644 index 0000000..89774df --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/LlmEngine.kt @@ -0,0 +1,81 @@ +package sk.ainet.samples.kernelrace.engine + +import kotlin.math.round +import kotlin.time.TimeMark +import kotlin.time.TimeSource +import sk.ainet.apps.kllama.agent.generateUntilStop +import sk.ainet.context.ExecutionContext +import sk.ainet.samples.kernelrace.model.ModelData +import sk.ainet.samples.kernelrace.platform.logEvent + +/** What [sk.ainet.samples.kernelrace.vm.ChatViewModel] needs from an engine — lets tests + * substitute a fake without touching SKaiNET's own (private-constructor) [LlmEngine]. */ +interface GenerativeEngine { + suspend fun generate(prompt: String, maxTokens: Int = 200, onToken: (String) -> Unit): String +} + +/** + * Loads a GGUF LLM and streams generated tokens — the whole integration. + * + * On Android this is the Scene 4 shot of the demo video: GGUF in, tokens out, no Python, + * no C++ in the app build. The hand-written ARM NEON kernels (skainet-backend-jni-cpu) + * register themselves through the kernel SPI just by being on the classpath. + */ +class LlmEngine private constructor(private val components: LlamaComponents) : GenerativeEngine { + + private val tokenizer get() = components.tokenizer + private val runtime get() = components.runtime + + override suspend fun generate(prompt: String, maxTokens: Int, onToken: (String) -> Unit): String { + runtime.reset() + val templated = chatMlEnvelope(prompt) + val promptTokens = tokenizer.encode(templated) + logEvent( + "generate_start", + "promptTokens" to promptTokens.size, + "maxTokens" to maxTokens, + "eos" to tokenizer.eosTokenId, + ) + val genStart = TimeSource.Monotonic.markNow() + var firstTokenAt: TimeMark? = null + var tokenCount = 0 + val result = runtime.generateUntilStop( + prompt = promptTokens, + maxTokens = maxTokens, + eosTokenId = tokenizer.eosTokenId, + temperature = 0.7f, + onToken = { tokenId -> + if (tokenCount == 0) firstTokenAt = TimeSource.Monotonic.markNow() + tokenCount++ + onToken(tokenizer.decode(tokenId)) + }, + decode = { tokenizer.decode(it) }, + ) + val totalMs = genStart.elapsedNow().inWholeMilliseconds + val decodeS = firstTokenAt?.elapsedNow()?.inWholeMilliseconds?.let { it / 1000.0 } ?: 0.0 + logEvent( + "generate_done", + "tokens" to tokenCount, + "totalMs" to totalMs, + "decodeTokPerSec" to if (tokenCount > 1 && decodeS > 0) { + formatFixed2((tokenCount - 1) / decodeS) + } else null, + "textLen" to result.text.length, + ) + return result.text + } + + companion object { + /** Call off the main thread — streams weights, never materializes the whole file on-heap. */ + suspend fun load(ctx: ExecutionContext, model: ModelData): LlmEngine = + LlmEngine(buildLlamaComponents(ctx, model)) + } +} + +/** `"%.2f".format` isn't available in common code — round to 2 decimals by hand. */ +private fun formatFixed2(value: Double): String { + val rounded = round(value * 100) / 100 + val whole = rounded.toLong() + val frac = round((rounded - whole) * 100).toInt().let { if (it < 0) -it else it } + return "$whole.${frac.toString().padStart(2, '0')}" +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplate.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplate.kt new file mode 100644 index 0000000..9f4d0ca --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplate.kt @@ -0,0 +1,8 @@ +package sk.ainet.samples.kernelrace.engine + +/** + * SmolLM2-Instruct is a ChatML model — a raw prompt makes it emit + * `<|im_end|>` immediately. Wrap it in the ChatML envelope it was trained on. + */ +fun chatMlEnvelope(prompt: String): String = + "<|im_start|>user\n$prompt<|im_end|>\n<|im_start|>assistant\n" diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelData.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelData.kt new file mode 100644 index 0000000..6711bfe --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelData.kt @@ -0,0 +1,14 @@ +package sk.ainet.samples.kernelrace.model + +/** + * Where the GGUF weights ended up. [FilePath] platforms (Android, desktop) get random-access + * reads straight off disk; [Bytes] platforms (wasm — no filesystem) hold the whole model + * in memory. + */ +sealed interface ModelData { + data class FilePath(val path: String) : ModelData + data class Bytes(val bytes: ByteArray) : ModelData +} + +const val HF_REPO = "unsloth/SmolLM2-135M-Instruct-GGUF" +const val HF_FILE = "SmolLM2-135M-Instruct-Q8_0.gguf" diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelProvider.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelProvider.kt new file mode 100644 index 0000000..96326c8 --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelProvider.kt @@ -0,0 +1,11 @@ +package sk.ainet.samples.kernelrace.model + +/** + * Resolves the GGUF onto this platform. Android and desktop implementations + * ([AndroidModelProvider], [DesktopModelProvider]) do real file IO (cache hit → bundled asset → + * streamed Hugging Face download); wasm has no filesystem, so composeApp reads the bundled + * Compose resource bytes directly and wraps them as [ModelData.Bytes] — no provider needed there. + */ +interface ModelProvider { + suspend fun resolve(onProgress: (String) -> Unit): ModelData +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelResolver.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelResolver.kt new file mode 100644 index 0000000..384ab93 --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/model/ModelResolver.kt @@ -0,0 +1,27 @@ +package sk.ainet.samples.kernelrace.model + +/** What to do to get the GGUF onto local storage, decided without touching any IO. */ +sealed interface ResolutionPlan { + data class UseCached(val path: String) : ResolutionPlan + data class UseAsset(val path: String) : ResolutionPlan + data object Download : ResolutionPlan +} + +/** + * Pure resolution policy, ported from the original Android-only `ModelSource.resolve`: + * cache hit short-circuits, otherwise a bundled offline asset wins over downloading. + * Takes plain booleans/paths so it needs no filesystem access — the file-based platform + * providers ([AndroidModelProvider], [DesktopModelProvider]) do the actual IO and defer the + * decision here. + */ +object ModelResolver { + fun plan(targetPath: String, targetExists: Boolean, assetAvailable: Boolean): ResolutionPlan = when { + targetExists -> ResolutionPlan.UseCached(targetPath) + assetAvailable -> ResolutionPlan.UseAsset(targetPath) + else -> ResolutionPlan.Download + } + + /** Streamed downloads land here first; an interrupted download leaves this behind, + * never a truncated [targetPath], so a retry doesn't mistake it for a finished file. */ + fun partPath(targetPath: String): String = "$targetPath.part" +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.kt new file mode 100644 index 0000000..5b63003 --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.kt @@ -0,0 +1,30 @@ +package sk.ainet.samples.kernelrace.platform + +import kotlin.time.TimeSource + +/** Human-readable label for the kernel path this platform/process is running — shown in the UI. */ +expect fun kernelTierLabel(): String + +/** Only Android can pin a JNI NEON provider vs a scalar one and race them side by side. */ +expect val supportsKernelRace: Boolean + +/** Where the platform actually writes a log line — `adb logcat` on Android, stdout elsewhere. */ +internal expect fun platformLog(line: String) + +private val processStart = TimeSource.Monotonic.markNow() + +/** + * One-line structured perf events, greppable with `adb logcat -s SKAINET_PERF_NEON:I` on + * Android (see the platform actual for the process-tagged variant), or read straight off + * stdout on desktop/wasm. + */ +fun logEvent(name: String, vararg details: Pair) { + val line = buildString { + append("event=").append(name) + append(" | +").append(processStart.elapsedNow().inWholeMilliseconds).append("ms") + for ((key, value) in details) { + if (value != null) append(" | ").append(key).append('=').append(value) + } + } + platformLog(line) +} diff --git a/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModel.kt b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModel.kt new file mode 100644 index 0000000..626f887 --- /dev/null +++ b/KernelRace/shared/src/commonMain/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModel.kt @@ -0,0 +1,98 @@ +package sk.ainet.samples.kernelrace.vm + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlin.time.TimeSource +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import sk.ainet.samples.kernelrace.domain.TokenStats +import sk.ainet.samples.kernelrace.domain.UiState +import sk.ainet.samples.kernelrace.engine.GenerativeEngine +import sk.ainet.samples.kernelrace.platform.kernelTierLabel + +/** + * Drives model loading + generation. Holds no Android/JVM/wasm-specific state: how the model + * bytes are resolved (Android assets/download, desktop cache, bundled wasm bytes) and how the + * execution context + kernel pinning are set up is entirely the caller's job, injected as a + * suspend lambda — which is what makes this class unit-testable with a fake on every target. + * + * [generationDispatcher] defaults to [Dispatchers.Default] (real CPU-bound work off the UI + * thread) but is overridable so tests can run generation on the same virtual-time scheduler + * as [androidx.lifecycle.viewModelScope]'s Main dispatcher. + */ +class ChatViewModel( + private val loadModel: suspend (onProgress: (String) -> Unit) -> GenerativeEngine, + initialKernelTier: String = kernelTierLabel(), + private val generationDispatcher: CoroutineDispatcher = Dispatchers.Default, +) : ViewModel() { + + private val _state = MutableStateFlow(UiState(kernelTier = initialKernelTier)) + val state: StateFlow = _state + + private var engine: GenerativeEngine? = null + + fun generate(prompt: String) { + if (_state.value.busy || prompt.isBlank()) return + _state.value = _state.value.copy(busy = true, output = "", tokensPerSecond = null, status = "Loading model…") + + viewModelScope.launch { + try { + val loaded = engine ?: loadModel { progress -> + _state.value = _state.value.copy(status = progress) + }.also { engine = it } + _state.value = _state.value.copy(status = "Generating") + + val stats = TokenStats() + val clock = TimeSource.Monotonic.markNow() + withContext(generationDispatcher) { + loaded.generate(prompt) { piece -> + val nowMs = clock.elapsedNow().inWholeMilliseconds + stats.onToken(nowMs) + _state.value = _state.value.copy( + output = _state.value.output + piece, + tokensPerSecond = stats.tokensPerSecond(nowMs), + ) + } + } + _state.value = _state.value.copy( + busy = false, + status = "Done — ${stats.tokenCount} tokens, fully on-device", + ) + } catch (e: Exception) { + _state.value = _state.value.copy(busy = false, status = "Error: ${e.message ?: e::class.simpleName}") + } + } + } + + /** Loads the engine without generating — arms a fair race (Android split-screen only). */ + fun preload() { + if (_state.value.busy) return + _state.value = _state.value.copy(busy = true) + viewModelScope.launch { + try { + engine = loadModel { progress -> _state.value = _state.value.copy(status = progress) } + _state.value = _state.value.copy(busy = false, status = "Model loaded — ready to race") + } catch (e: Exception) { + _state.value = _state.value.copy(busy = false, status = "Error: ${e.message ?: e::class.simpleName}") + } + } + } + + /** Drops the cached engine so the next generate reloads under a different kernel pin + * (Android's fullscreen NEON/SCALAR switch calls this after re-pinning the registry). */ + fun onKernelModeChanged(newKernelTier: String, scalarMode: Boolean) { + if (_state.value.busy) return + engine = null + _state.value = _state.value.copy( + scalarMode = scalarMode, + kernelTier = newKernelTier, + output = "", + tokensPerSecond = null, + status = "Kernel mode changed — model reloads on next run", + ) + } +} diff --git a/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/domain/TokenStatsTest.kt b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/domain/TokenStatsTest.kt new file mode 100644 index 0000000..581137b --- /dev/null +++ b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/domain/TokenStatsTest.kt @@ -0,0 +1,45 @@ +package sk.ainet.samples.kernelrace.domain + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TokenStatsTest { + + @Test + fun noRateBeforeAnyToken() { + val stats = TokenStats() + + assertNull(stats.tokensPerSecond(nowMs = 0)) + assertEquals(0, stats.tokenCount) + } + + @Test + fun noRateWithinFirstHalfSecond() { + val stats = TokenStats() + + stats.onToken(nowMs = 0) + stats.onToken(nowMs = 200) + stats.onToken(nowMs = 400) + + // elapsed since the first token is only 400ms — below the 0.5s noise guard + assertNull(stats.tokensPerSecond(nowMs = 400)) + } + + @Test + fun excludesPrefillAndFirstTokenFromTheRate() { + val stats = TokenStats() + + // "prefill" — arrives long before the first decoded token — must not skew the clock + stats.onToken(nowMs = 5_000) + // 4 more tokens over exactly 1 second after the first + stats.onToken(nowMs = 5_250) + stats.onToken(nowMs = 5_500) + stats.onToken(nowMs = 5_750) + stats.onToken(nowMs = 6_000) + + // 5 tokens total, but the rate is measured over 4 decode intervals in 1s => 4 tok/s + assertEquals(5, stats.tokenCount) + assertEquals(4.0, stats.tokensPerSecond(nowMs = 6_000)) + } +} diff --git a/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplateTest.kt b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplateTest.kt new file mode 100644 index 0000000..95ebb1c --- /dev/null +++ b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/engine/PromptTemplateTest.kt @@ -0,0 +1,27 @@ +package sk.ainet.samples.kernelrace.engine + +import kotlin.test.Test +import kotlin.test.assertEquals + +class PromptTemplateTest { + + @Test + fun wrapsPromptInChatMlEnvelope() { + val envelope = chatMlEnvelope("Explain NEON in two sentences.") + + assertEquals( + "<|im_start|>user\nExplain NEON in two sentences.<|im_end|>\n<|im_start|>assistant\n", + envelope, + ) + } + + @Test + fun preservesEmbeddedNewlines() { + val envelope = chatMlEnvelope("line one\nline two") + + assertEquals( + "<|im_start|>user\nline one\nline two<|im_end|>\n<|im_start|>assistant\n", + envelope, + ) + } +} diff --git a/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/model/ModelResolverTest.kt b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/model/ModelResolverTest.kt new file mode 100644 index 0000000..da90c79 --- /dev/null +++ b/KernelRace/shared/src/commonTest/kotlin/sk/ainet/samples/kernelrace/model/ModelResolverTest.kt @@ -0,0 +1,36 @@ +package sk.ainet.samples.kernelrace.model + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class ModelResolverTest { + + @Test + fun cacheHitShortCircuitsBeforeCheckingTheAsset() { + val plan = ModelResolver.plan(targetPath = "/models/x.gguf", targetExists = true, assetAvailable = true) + + assertIs(plan) + assertEquals("/models/x.gguf", plan.path) + } + + @Test + fun assetWinsOverDownloadWhenNotCached() { + val plan = ModelResolver.plan(targetPath = "/models/x.gguf", targetExists = false, assetAvailable = true) + + assertIs(plan) + assertEquals("/models/x.gguf", plan.path) + } + + @Test + fun downloadsWhenNeitherCachedNorBundled() { + val plan = ModelResolver.plan(targetPath = "/models/x.gguf", targetExists = false, assetAvailable = false) + + assertEquals(ResolutionPlan.Download, plan) + } + + @Test + fun partPathNeverCollidesWithTheFinalTarget() { + assertEquals("/models/x.gguf.part", ModelResolver.partPath("/models/x.gguf")) + } +} diff --git a/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.jvm.kt b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.jvm.kt new file mode 100644 index 0000000..917f357 --- /dev/null +++ b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.jvm.kt @@ -0,0 +1,32 @@ +package sk.ainet.samples.kernelrace.engine + +import sk.ainet.apps.llm.OptimizedLLMMode +import sk.ainet.apps.llm.OptimizedLLMRuntime +import sk.ainet.apps.llm.tokenizer.TokenizerFactory +import sk.ainet.context.ExecutionContext +import sk.ainet.io.JvmRandomAccessSource +import sk.ainet.io.model.QuantPolicy +import sk.ainet.lang.types.FP32 +import sk.ainet.models.llama.DecoderGgufWeightLoader +import sk.ainet.models.llama.LlamaNetworkLoader +import sk.ainet.samples.kernelrace.model.ModelData + +/** File-based random-access reads, same shape as the Android fast path but without the + * NEON JNI kernels — desktop has no ARM hardware to dispatch to. */ +actual suspend fun buildLlamaComponents(ctx: ExecutionContext, model: ModelData): LlamaComponents { + val path = (model as ModelData.FilePath).path + val weights = DecoderGgufWeightLoader( + randomAccessProvider = { JvmRandomAccessSource.open(path) }, + quantPolicy = QuantPolicy.NATIVE_OPTIMIZED, + acceptedArchitectures = setOf("llama", "mistral"), + ).loadToMapStreaming(ctx) + val runtime = OptimizedLLMRuntime( + model = LlamaNetworkLoader.fromWeights(weights), + ctx = ctx, + mode = OptimizedLLMMode.DIRECT, + dtype = FP32::class, + bos = weights.metadata.bosTokenId, + ) + val tokenizer = JvmRandomAccessSource.open(path).use { TokenizerFactory.fromGgufSource(it) } + return LlamaComponents(runtime, tokenizer) +} diff --git a/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/model/DesktopModelProvider.kt b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/model/DesktopModelProvider.kt new file mode 100644 index 0000000..40b4086 --- /dev/null +++ b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/model/DesktopModelProvider.kt @@ -0,0 +1,81 @@ +package sk.ainet.samples.kernelrace.model + +import java.io.File +import kotlin.time.TimeSource +import kotlinx.io.Buffer +import kotlinx.io.buffered +import kotlinx.io.files.SystemFileSystem +import sk.ainet.data.source.KtorRemoteDataSourceFetcher +import sk.ainet.samples.kernelrace.platform.logEvent +import kotlinx.io.files.Path as KotlinxPath + +/** + * Desktop has no bundled-asset concept, so this is cache-hit-or-download: the model lands in + * `~/.skainet-examples/kernelrace/models/` and is reused across runs. + */ +class DesktopModelProvider( + private val cacheDir: String = defaultCacheDir(), +) : ModelProvider { + + override suspend fun resolve(onProgress: (String) -> Unit): ModelData { + val dir = File(cacheDir).apply { mkdirs() } + val target = File(dir, HF_FILE) + + return when (val plan = ModelResolver.plan(target.path, target.exists(), assetAvailable = false)) { + is ResolutionPlan.UseCached -> { + logEvent("model_cached", "file" to target.name, "fileMB" to target.length() / 1_000_000) + ModelData.FilePath(plan.path) + } + is ResolutionPlan.UseAsset -> error("desktop has no bundled asset") + ResolutionPlan.Download -> { + download(target, onProgress) + ModelData.FilePath(target.path) + } + } + } + + private suspend fun download(target: File, onProgress: (String) -> Unit) { + val url = "https://huggingface.co/$HF_REPO/resolve/main/$HF_FILE" + val tmp = File(target.parentFile, ModelResolver.partPath(target.name)) + val fetcher = KtorRemoteDataSourceFetcher() + logEvent("download_start", "url" to url) + val startedAt = TimeSource.Monotonic.markNow() + var received = 0L + try { + val content = fetcher.fetch(url, emptyMap()) + val totalMb = content.sizeBytes?.let { (it / 1_000_000).toString() } ?: "?" + content.source.use { source -> + SystemFileSystem.sink(KotlinxPath(tmp.path)).buffered().use { sink -> + val chunk = Buffer() + var lastReported = -1L + while (true) { + val n = source.readAtMostTo(chunk, 1024 * 1024) + if (n == -1L) break + sink.write(chunk, n) + received += n + val mb = received / 1_000_000 + if (mb != lastReported) { + lastReported = mb + onProgress("Downloading model… $mb / $totalMb MB") + } + } + } + } + check(tmp.renameTo(target)) { "rename failed: $tmp -> $target" } + logEvent( + "download_done", + "fileMB" to received / 1_000_000, + "durMs" to startedAt.elapsedNow().inWholeMilliseconds, + ) + } catch (e: Exception) { + tmp.delete() + logEvent("download_failed", "receivedMB" to received / 1_000_000, "error" to (e.message ?: e::class.simpleName)) + throw e + } finally { + fetcher.close() + } + } +} + +private fun defaultCacheDir(): String = + File(System.getProperty("user.home"), ".skainet-examples/kernelrace/models").path diff --git a/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.jvm.kt b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.jvm.kt new file mode 100644 index 0000000..efdde9c --- /dev/null +++ b/KernelRace/shared/src/jvmMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.jvm.kt @@ -0,0 +1,9 @@ +package sk.ainet.samples.kernelrace.platform + +actual fun kernelTierLabel(): String = "JVM (scalar)" + +actual val supportsKernelRace: Boolean = false + +internal actual fun platformLog(line: String) { + println("[SKAINET_PERF_JVM] $line") +} diff --git a/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/engine/GgufSmokeTest.kt b/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/engine/GgufSmokeTest.kt new file mode 100644 index 0000000..65637ae --- /dev/null +++ b/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/engine/GgufSmokeTest.kt @@ -0,0 +1,39 @@ +package sk.ainet.samples.kernelrace.engine + +import java.io.File +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import sk.ainet.context.DirectCpuExecutionContext +import sk.ainet.samples.kernelrace.model.HF_FILE +import sk.ainet.samples.kernelrace.model.ModelData + +/** + * Loads the real SmolLM2 GGUF and runs a couple of forward passes end to end. Not a hermetic + * unit test — it's gated on the model already sitting in the desktop cache dir (the same place + * [sk.ainet.samples.kernelrace.model.DesktopModelProvider] downloads it to), so a clean CI + * checkout with no cached model skips it rather than failing the build. + */ +class GgufSmokeTest { + + private val cachedModel = File( + System.getProperty("user.home"), + ".skainet-examples/kernelrace/models/$HF_FILE", + ) + + @Test + fun tokenizesAndGeneratesAFewTokens() = runTest { + if (!cachedModel.exists()) { + println("Skipping GgufSmokeTest: no cached model at $cachedModel") + return@runTest + } + + val ctx = DirectCpuExecutionContext() + val engine = LlmEngine.load(ctx, ModelData.FilePath(cachedModel.path)) + + val tokens = mutableListOf() + val text = engine.generate("Say hi in three words.", maxTokens = 8) { tokens.add(it) } + + check(tokens.isNotEmpty()) { "expected at least one streamed token" } + check(text.isNotBlank()) { "expected non-blank generated text" } + } +} diff --git a/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModelTest.kt b/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModelTest.kt new file mode 100644 index 0000000..4cf7993 --- /dev/null +++ b/KernelRace/shared/src/jvmTest/kotlin/sk/ainet/samples/kernelrace/vm/ChatViewModelTest.kt @@ -0,0 +1,140 @@ +package sk.ainet.samples.kernelrace.vm + +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import sk.ainet.samples.kernelrace.engine.GenerativeEngine + +/** + * [androidx.lifecycle.ViewModel.viewModelScope] launches on `Dispatchers.Main`, and + * `ChatViewModel.generate` hops to its injectable `generationDispatcher` for the actual + * generation call. Both need to run on the SAME virtual-time scheduler as the test body for + * `runTest` to see the launched work complete — an [UnconfinedTestDispatcher] tied to + * `runTest`'s own `testScheduler` does that: everything (Main + generation) executes eagerly, + * on the one scheduler `runTest` already knows how to wait for. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class ChatViewModelTest { + + @BeforeTest + fun setUp() = Dispatchers.setMain(UnconfinedTestDispatcher()) + + @AfterTest + fun tearDown() = Dispatchers.resetMain() + + private class FakeEngine(private val piecesToEmit: List) : GenerativeEngine { + var generateCalls = 0 + override suspend fun generate(prompt: String, maxTokens: Int, onToken: (String) -> Unit): String { + generateCalls++ + piecesToEmit.forEach(onToken) + return piecesToEmit.joinToString("") + } + } + + private fun viewModel( + loadModel: suspend (onProgress: (String) -> Unit) -> GenerativeEngine, + dispatcher: kotlinx.coroutines.CoroutineDispatcher, + ) = ChatViewModel(loadModel = loadModel, generationDispatcher = dispatcher) + + @Test + fun generateStreamsOutputAndEndsIdle() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + val engine = FakeEngine(listOf("Hel", "lo")) + val vm = viewModel(loadModel = { engine }, dispatcher = dispatcher) + + vm.generate("hi") + + val state = vm.state.value + assertEquals("Hello", state.output) + assertFalse(state.busy) + assertTrue(state.status.startsWith("Done")) + assertEquals(1, engine.generateCalls) + } + + @Test + fun blankPromptIsIgnored() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + val engine = FakeEngine(listOf("x")) + val vm = viewModel(loadModel = { engine }, dispatcher = dispatcher) + + vm.generate(" ") + + assertEquals(0, engine.generateCalls) + assertFalse(vm.state.value.busy) + } + + @Test + fun secondCallWhileBusyIsIgnored() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + // A load that never returns keeps the ViewModel busy for the duration of this test. + val engine = FakeEngine(listOf("a")) + val vm = viewModel(loadModel = { awaitCancellation() }, dispatcher = dispatcher) + + vm.generate("first") + vm.generate("second") // dropped: still busy, load hasn't resolved + + assertEquals(0, engine.generateCalls) + assertTrue(vm.state.value.busy) + } + + @Test + fun loadFailureSurfacesAsAnErrorStatus() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + val vm = viewModel(loadModel = { error("boom") }, dispatcher = dispatcher) + + vm.generate("hi") + + val state = vm.state.value + assertFalse(state.busy) + assertTrue(state.status.contains("Error")) + } + + @Test + fun engineIsCachedAcrossGenerations() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + val engine = FakeEngine(listOf("x")) + var loadCalls = 0 + val vm = viewModel(loadModel = { loadCalls++; engine }, dispatcher = dispatcher) + + vm.generate("first") + vm.generate("second") + + assertEquals(1, loadCalls) + assertEquals(2, engine.generateCalls) + } + + @Test + fun kernelModeChangeDropsTheCachedEngineAndClearsOutput() = runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + val engine = FakeEngine(listOf("x")) + var loadCalls = 0 + val vm = viewModel(loadModel = { loadCalls++; engine }, dispatcher = dispatcher) + + vm.generate("first") + + vm.onKernelModeChanged(newKernelTier = "SCALAR", scalarMode = true) + assertEquals("", vm.state.value.output) + assertEquals("SCALAR", vm.state.value.kernelTier) + assertTrue(vm.state.value.scalarMode) + + vm.generate("second") + + assertEquals(2, loadCalls) // engine reloaded after the mode change + } +} diff --git a/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.wasmJs.kt b/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.wasmJs.kt new file mode 100644 index 0000000..b688e98 --- /dev/null +++ b/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/engine/LlamaRuntimeBuilder.wasmJs.kt @@ -0,0 +1,9 @@ +package sk.ainet.samples.kernelrace.engine + +import sk.ainet.context.ExecutionContext +import sk.ainet.samples.kernelrace.model.ModelData + +/** No filesystem in the browser — the bytes were bundled at build time and read by composeApp + * via `Res.readBytes(...)`, handed down as [ModelData.Bytes]. */ +actual suspend fun buildLlamaComponents(ctx: ExecutionContext, model: ModelData): LlamaComponents = + buildLlamaComponentsFallback(ctx, (model as ModelData.Bytes).bytes) diff --git a/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.wasmJs.kt b/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.wasmJs.kt new file mode 100644 index 0000000..023dade --- /dev/null +++ b/KernelRace/shared/src/wasmJsMain/kotlin/sk/ainet/samples/kernelrace/platform/Platform.wasmJs.kt @@ -0,0 +1,9 @@ +package sk.ainet.samples.kernelrace.platform + +actual fun kernelTierLabel(): String = "Wasm (scalar)" + +actual val supportsKernelRace: Boolean = false + +internal actual fun platformLog(line: String) { + println("[SKAINET_PERF_WASM] $line") +} diff --git a/KernelRace/webapp.json b/KernelRace/webapp.json new file mode 100644 index 0000000..dd3fc4d --- /dev/null +++ b/KernelRace/webapp.json @@ -0,0 +1,14 @@ +{ + "id": "kernelrace", + "name": "Kernel Race", + "description": "On-device LLM chat with a NEON-vs-scalar kernel race on Android; this web build runs SKaiNET's scalar fallback path.", + "platforms": ["android", "desktop", "wasm"], + "distDirs": [ + "composeApp/build/dist/wasmJs/productionExecutable" + ], + "build": { + "command": "./gradlew :composeApp:wasmJsBrowserDistribution", + "description": "Builds the wasm browser distribution, including the bundled ~145 MB GGUF model" + }, + "screenshot": "docs/screenshots/split_race.png" +} diff --git a/README.md b/README.md index eec5254..792bcd8 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,19 @@ language toggle. --- +### 🏁 Kernel Race + +On-device LLM chat accelerated by SKaiNET's hand-written ARM NEON kernels on Android, with a +built-in **NEON vs scalar** A/B comparison — including a one-phone split-screen race. The same +Kotlin codebase also runs on Desktop and in the browser, showing the other kernel tiers SKaiNET +falls back to without NEON hardware. + +Kernel Race — split-screen NEON vs scalar generation + +📂 [`KernelRace/`](KernelRace/) · runs on Android · Desktop · Wasm + +--- + ### 💬 Kllama Demo A Qwen3 LLM **playground in the browser** — chat, completion, translation, tool From 7d460054fa1bf4fa9873294d95526b160d6e74fd Mon Sep 17 00:00:00 2001 From: Michal Harakal Date: Wed, 12 Aug 2026 21:15:32 +0200 Subject: [PATCH 3/4] Support non-web samples and platform chips on the examples page The samples page only ever showed a card for samples with a wasm build: generate-samples-page.py unconditionally emitted a demoUrl for every webapp.json it found. CLI-only and Android-only samples had no way to appear at all. - distDirs/build are now optional; a card only needs id/name/description. - demoUrl is emitted only when distDirs is non-empty; samples without one get a "How to run" button linking to the source instead of a dead "Try Demo". - New "platforms" array renders as chips on every card (web, android, ios, desktop, cli, ...), reusing the existing pill styling. - Added platforms to the four existing webapp.json files and a new MnistJavaDemo/webapp.json (the first CLI-only sample on the page). Co-Authored-By: Claude Sonnet 5 --- GloVeEmbeddings/webapp.json | 1 + MNISTDemo/webapp.json | 1 + MnistJavaDemo/webapp.json | 6 +++++ SinusApproximator/webapp.json | 1 + TinyTransformer/webapp.json | 1 + scripts/generate-samples-page.py | 42 ++++++++++++++++++++++++++++---- 6 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 MnistJavaDemo/webapp.json diff --git a/GloVeEmbeddings/webapp.json b/GloVeEmbeddings/webapp.json index ad41dee..4037477 100644 --- a/GloVeEmbeddings/webapp.json +++ b/GloVeEmbeddings/webapp.json @@ -2,6 +2,7 @@ "id": "gloveembeddings", "name": "GloVe Embeddings Explorer", "description": "Offline word-embedding explorer: nearest neighbours and vector analogies (king − man + woman ≈ queen), powered by SKaiNET", + "platforms": ["android", "ios", "desktop", "wasm"], "distDirs": [ "app/build/kotlin-webpack/wasmJs/productionExecutable", "app/build/processedResources/wasmJs/main" diff --git a/MNISTDemo/webapp.json b/MNISTDemo/webapp.json index 01fc492..328f69c 100644 --- a/MNISTDemo/webapp.json +++ b/MNISTDemo/webapp.json @@ -2,6 +2,7 @@ "id": "mnistdemo", "name": "MNIST Demo", "description": "Handwritten digit classifier with in-app training using SKaiNET ML framework", + "platforms": ["android", "ios", "desktop", "wasm"], "distDirs": [ "composeApp/build/kotlin-webpack/wasmJs/productionExecutable", "composeApp/build/processedResources/wasmJs/main" diff --git a/MnistJavaDemo/webapp.json b/MnistJavaDemo/webapp.json new file mode 100644 index 0000000..5ad4dff --- /dev/null +++ b/MnistJavaDemo/webapp.json @@ -0,0 +1,6 @@ +{ + "id": "mnistjavademo", + "name": "MNIST Java Demo", + "description": "The same MNIST digit detector as a pure-Java CLI — proof of SKaiNET's first-class Java interop from a Kotlin Multiplatform engine.", + "platforms": ["cli", "java"] +} diff --git a/SinusApproximator/webapp.json b/SinusApproximator/webapp.json index 41cf038..fe67dbc 100644 --- a/SinusApproximator/webapp.json +++ b/SinusApproximator/webapp.json @@ -2,6 +2,7 @@ "id": "sinusapproximator", "name": "Sinus Approximator", "description": "Sinus Approximator as web app", + "platforms": ["android", "ios", "desktop", "wasm"], "distDirs": [ "composeApp/build/kotlin-webpack/wasmJs/productionExecutable", "composeApp/build/processedResources/wasmJs/main" diff --git a/TinyTransformer/webapp.json b/TinyTransformer/webapp.json index 8d8cd63..fbd4ef9 100644 --- a/TinyTransformer/webapp.json +++ b/TinyTransformer/webapp.json @@ -2,6 +2,7 @@ "id": "tinytransformer", "name": "Tiny Transformer (KI-ENNA)", "description": "Train a tiny decoder-only transformer live in the browser — inspired by the KI-ENNA project (statistical-thinking.de)", + "platforms": ["android", "ios", "desktop", "wasm"], "distDirs": [ "composeApp/build/kotlin-webpack/wasmJs/productionExecutable", "composeApp/build/processedResources/wasmJs/main" diff --git a/scripts/generate-samples-page.py b/scripts/generate-samples-page.py index 8d58f88..ffa0003 100755 --- a/scripts/generate-samples-page.py +++ b/scripts/generate-samples-page.py @@ -133,6 +133,7 @@ def find_webapp_configs(root_dir: Path, repo_url: str | None = None, branch: str "description": meta.get("description", ""), "screenshot": meta.get("screenshot"), "distDirs": meta.get("distDirs", []), + "platforms": meta.get("platforms", ["web"]), "sourceUrl": source_url, "project_root": project_root }) @@ -461,7 +462,9 @@ def generate_html(apps: list, release_tag: str = "", base_url: str = "https://ex "description": app["description"], "screenshot": app.get("screenshot_url") or app.get("screenshot"), "sourceUrl": app.get("sourceUrl"), - "demoUrl": f"./{app['id']}/" + "platforms": app.get("platforms", ["web"]), + # Only samples that actually ship a web dist get a live demo link. + "demoUrl": f"./{app['id']}/" if app.get("distDirs") else None } for app in apps ], indent=2) @@ -886,6 +889,27 @@ def generate_html(apps: list, release_tag: str = "", base_url: str = "https://ex line-height: 1.5; }} + .platform-chips {{ + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin-top: 0.75rem; + }} + + .platform-chip {{ + display: inline-flex; + align-items: center; + padding: 0.2rem 0.55rem; + font-size: 0.625rem; + font-weight: 500; + text-transform: capitalize; + color: hsl(var(--muted-foreground)); + background-color: hsl(var(--muted) / 0.6); + border: 1px solid hsl(var(--border)); + border-radius: 999px; + white-space: nowrap; + }} + /* Card Footer - Stack on mobile, side-by-side on larger */ .card-footer {{ position: relative; @@ -1078,10 +1102,17 @@ def generate_html(apps: list, release_tag: str = "", base_url: str = "https://ex ${{playIcon}} Try Demo ` - : ``; + : (project.sourceUrl + ? ` + How to run + ` + : ``); + + const platformChips = (project.platforms || ['web']).map(p => + `${{escapeHtml(p)}}` + ).join(''); return `

@@ -1095,6 +1126,7 @@ def generate_html(apps: list, release_tag: str = "", base_url: str = "https://ex ${{escapeHtml(project.id)}}

${{escapeHtml(project.description)}}

+
${{platformChips}}