diff --git a/skainet-backends/skainet-backend-api/build.gradle.kts b/skainet-backends/skainet-backend-api/build.gradle.kts index 601c87edb..108ae414b 100644 --- a/skainet-backends/skainet-backend-api/build.gradle.kts +++ b/skainet-backends/skainet-backend-api/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { macosArm64() linuxX64() linuxArm64() + androidNativeArm32() jvm() @@ -96,5 +97,9 @@ kotlin { val linuxArm64Main by getting { dependsOn(linuxMain) } + + val androidNativeArm32Main by getting { + dependsOn(nativeMain) + } } } diff --git a/skainet-backends/skainet-backend-cpu/gradle.properties b/skainet-backends/skainet-backend-cpu/gradle.properties index 8380ea84c..2be7380d5 100644 --- a/skainet-backends/skainet-backend-cpu/gradle.properties +++ b/skainet-backends/skainet-backend-cpu/gradle.properties @@ -1,4 +1,7 @@ POM_ARTIFACT_ID=skainet-backend-cpu POM_NAME=skainet neural network scripting API -kotlin.mpp.applyDefaultHierarchyTemplate=false \ No newline at end of file +kotlin.mpp.applyDefaultHierarchyTemplate=false +# DirectCpuExecutionContext (eager execution, no backend-native cinterop +# kernels here) is pure-Kotlin — safe to build for Android-native too. +skainet.targets=jvm,js,wasmJs,wasmWasi,apple,linux,androidNative \ No newline at end of file diff --git a/skainet-backends/skainet-backend-cpu/src/androidNativeArm32Main/kotlin/sk/ainet/exec/tensor/ops/PlatformCpuOpsFactory.androidNativeArm32.kt b/skainet-backends/skainet-backend-cpu/src/androidNativeArm32Main/kotlin/sk/ainet/exec/tensor/ops/PlatformCpuOpsFactory.androidNativeArm32.kt new file mode 100644 index 000000000..04b61b145 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/androidNativeArm32Main/kotlin/sk/ainet/exec/tensor/ops/PlatformCpuOpsFactory.androidNativeArm32.kt @@ -0,0 +1,20 @@ +package sk.ainet.exec.tensor.ops + +import sk.ainet.backend.api.kernel.KernelRegistry +import sk.ainet.exec.kernel.ScalarKernelProvider +import sk.ainet.lang.tensor.data.TensorDataFactory +import sk.ainet.lang.tensor.ops.TensorOps + +/** + * Portable fallback — identical to [PlatformCpuOpsFactory.linux.kt]'s, no + * accelerated (NEON cinterop) path yet for this target. See + * skainet-backend-native-cpu's own doc comments: its NEON kernels are built + * for Linux aarch64 (glibc, the SL2610 board target), not Android's bionic + * 32-bit ABI — wiring a real accelerated path here is separate, larger work. + */ +internal actual fun platformDefaultCpuOpsFactory(): (TensorDataFactory) -> TensorOps { + // Non-JVM has no ServiceLoader; register the scalar packed-quant kernels + // (Q4_K/Q6_K/Q5_1/Q5_0/Q8_0/Q4_0) so DefaultCpuOpsBase can dispatch them. + KernelRegistry.register(ScalarKernelProvider) + return { factory -> DefaultCpuOps(factory) } +} diff --git a/skainet-compile/skainet-compile-dag/build.gradle.kts b/skainet-compile/skainet-compile-dag/build.gradle.kts index 4281ec714..d030befbc 100644 --- a/skainet-compile/skainet-compile-dag/build.gradle.kts +++ b/skainet-compile/skainet-compile-dag/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { macosArm64 () linuxX64 () linuxArm64 () + androidNativeArm32() jvm() diff --git a/skainet-compile/skainet-compile-opt/build.gradle.kts b/skainet-compile/skainet-compile-opt/build.gradle.kts index 0315845fa..08458c2e2 100644 --- a/skainet-compile/skainet-compile-opt/build.gradle.kts +++ b/skainet-compile/skainet-compile-opt/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { macosArm64 () linuxX64 () linuxArm64 () + androidNativeArm32() jvm() diff --git a/skainet-io/skainet-io-core/src/androidNativeArm32Main/kotlin/sk/ainet/io/Posix32PreadRandomAccessSource.kt b/skainet-io/skainet-io-core/src/androidNativeArm32Main/kotlin/sk/ainet/io/Posix32PreadRandomAccessSource.kt new file mode 100644 index 000000000..6448cc483 --- /dev/null +++ b/skainet-io/skainet-io-core/src/androidNativeArm32Main/kotlin/sk/ainet/io/Posix32PreadRandomAccessSource.kt @@ -0,0 +1,115 @@ +package sk.ainet.io + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import platform.posix.O_RDONLY +import platform.posix.errno +import platform.posix.fstat +import platform.posix.pread +import platform.posix.stat +import platform.posix.strerror + +/** + * `androidNativeArm32`'s [RandomAccessSource], backed by POSIX `pread(2)`. + * + * Logic-identical to [PosixPreadRandomAccessSource] (native64Main) — `.convert()` + * already targets whatever width `pread`'s generated signature expects — but kept + * as a separate file/source set rather than shared, because androidNativeArm32's + * `ssize_t`/`off_t` are 32-bit where every other native target is 64-bit: putting + * both in one shared native source set fails Kotlin/Native's metadata compile + * ("numbers with different bit widths"), see native64Main's own doc comment and + * the `native64Targets` split in skainet-io-core's build.gradle.kts. + * + * 32-bit `off_t` without `_FILE_OFFSET_BITS=64` caps addressable file size at + * 2 GiB — acceptable for this target's real workloads (GGUF/safetensors reads + * on-device); large-file (100+ GB) loading stays a native64/JVM concern. + */ +@OptIn(ExperimentalForeignApi::class) +public class Posix32PreadRandomAccessSource private constructor( + private val fd: Int, + override val size: Long, +) : RandomAccessSource { + + private var closed = false + + override fun readAt(position: Long, length: Int): ByteArray { + require(position >= 0) { "Position must be non-negative: $position" } + require(length >= 0) { "Length must be non-negative: $length" } + require(position + length <= size) { + "Read beyond end of file: position=$position, length=$length, size=$size" + } + if (length == 0) return ByteArray(0) + + val buffer = ByteArray(length) + val bytesRead = readAt(position, buffer, 0, length) + return if (bytesRead < length) buffer.copyOf(bytesRead) else buffer + } + + override fun readAt(position: Long, buffer: ByteArray, offset: Int, length: Int): Int { + require(position >= 0) { "Position must be non-negative: $position" } + require(offset >= 0) { "Offset must be non-negative: $offset" } + require(length >= 0) { "Length must be non-negative: $length" } + require(offset + length <= buffer.size) { + "Buffer overflow: offset=$offset, length=$length, buffer.size=${buffer.size}" + } + check(!closed) { "Source is closed" } + if (length == 0) return 0 + + return buffer.usePinned { pinned -> + var totalRead = 0 + while (totalRead < length) { + val n = pread( + fd, + pinned.addressOf(offset + totalRead), + (length - totalRead).convert(), + (position + totalRead).convert(), + ) + if (n < 0) { + val cause = strerror(errno)?.toKString() ?: "errno=$errno" + error("pread failed at offset ${position + totalRead}: $cause") + } + if (n == 0) break // EOF + totalRead += n + } + totalRead + } + } + + override fun close() { + if (closed) return + closed = true + platform.posix.close(fd) + } + + public companion object { + /** + * Open [path] for read-only random access. Returns `null` if the file + * cannot be opened or stat'd, or exceeds the 32-bit `off_t` 2 GiB cap — + * matching [PosixPreadRandomAccessSource]'s contract so callers can fall + * back to the legacy sequential reader. + */ + public fun open(path: String): Posix32PreadRandomAccessSource? = memScoped { + val fd = platform.posix.open(path, O_RDONLY) + if (fd < 0) return@memScoped null + val st = alloc() + if (fstat(fd, st.ptr) != 0) { + platform.posix.close(fd) + return@memScoped null + } + val size = st.st_size + if (size < 0 || size > Int.MAX_VALUE.toLong()) { + // Negative == 32-bit off_t overflow; >2GiB is unreachable on a + // true 32-bit off_t anyway, but guard explicitly for clarity. + platform.posix.close(fd) + return@memScoped null + } + Posix32PreadRandomAccessSource(fd, size) + } + } +} diff --git a/skainet-io/skainet-io-gguf/build.gradle.kts b/skainet-io/skainet-io-gguf/build.gradle.kts index 0936f979c..050547413 100644 --- a/skainet-io/skainet-io-gguf/build.gradle.kts +++ b/skainet-io/skainet-io-gguf/build.gradle.kts @@ -35,6 +35,11 @@ kotlin { macosArm64 () linuxX64 () linuxArm64 () + // 32-bit: RandomAccessSourceFactory needs its own actual (see + // androidNativeArm32Main) — pread's ssize_t/off_t are Int here vs Long on + // every other native target, so it can't share nativeMain's actual with + // them (see the native64Main split below and skainet-io-core's own). + androidNativeArm32() js { browser() @@ -50,17 +55,47 @@ kotlin { nodejs() } + // 64-bit-only intermediate: holds the pread-based RandomAccessSourceFactory + // actual, whose ssize_t/off_t widths are uniform (Long) across every native + // target EXCEPT androidNativeArm32 (which gets its own actual instead — see + // androidNativeArm32Main). Mirrors skainet-io-core's identical split. + val native64Targets = listOf("iosArm64", "iosSimulatorArm64", "macosArm64", "linuxX64", "linuxArm64") + applyDefaultHierarchyTemplate() + sourceSets { + val nativeMain by getting val commonMain by getting { dependencies { implementation(libs.kotlinx.io.core) implementation(project(":skainet-lang:skainet-lang-core")) implementation(project(":skainet-io:skainet-io-core")) + } + } + // GgufExportFacade (host-side model export/compile tooling) needs + // skainet-compile-dag, which doesn't build for androidNativeArm32 + // (32-bit) — see PosixPreadRandomAccessSource's own split for the + // identical reasoning. Model export isn't an on-device concern anyway, + // so it lives here instead of commonMain: jvm/android/native64 (every + // target except androidNativeArm32) get it. + val exportMain by creating { + dependsOn(commonMain) + dependencies { implementation(project(":skainet-compile:skainet-compile-core")) implementation(project(":skainet-compile:skainet-compile-dag")) - } } + val native64Main by creating { + dependsOn(nativeMain) + dependsOn(exportMain) + } + native64Targets.forEach { t -> getByName("${t}Main").dependsOn(native64Main) } + val jvmMain by getting { dependsOn(exportMain) } + val androidMain by getting { dependsOn(exportMain) } + // js/wasmJs/wasmWasi: skainet-compile-dag supports these too — only + // androidNativeArm32 is actually excluded from exportMain. + val jsMain by getting { dependsOn(exportMain) } + val wasmJsMain by getting { dependsOn(exportMain) } + val wasmWasiMain by getting { dependsOn(exportMain) } val commonTest by getting { dependencies { implementation(libs.kotlin.test) diff --git a/skainet-io/skainet-io-gguf/src/androidNativeArm32Main/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.androidNativeArm32.kt b/skainet-io/skainet-io-gguf/src/androidNativeArm32Main/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.androidNativeArm32.kt new file mode 100644 index 000000000..75d282b6e --- /dev/null +++ b/skainet-io/skainet-io-gguf/src/androidNativeArm32Main/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.androidNativeArm32.kt @@ -0,0 +1,17 @@ +package sk.ainet.io.gguf + +import sk.ainet.io.Posix32PreadRandomAccessSource +import sk.ainet.io.RandomAccessSource + +/** + * `androidNativeArm32` implementation of [createRandomAccessSource] using POSIX + * `pread(2)` via [Posix32PreadRandomAccessSource] — the 32-bit-`off_t` sibling of + * [sk.ainet.io.PosixPreadRandomAccessSource] (native64Main), kept as a separate + * actual because this target can't share a source set with the 64-bit natives + * (mixed `ssize_t`/`off_t` widths fail Kotlin/Native's metadata compile). + * + * Returns `null` if the file cannot be opened, matching every other actual's + * contract so callers fall back to the legacy sequential reader. + */ +public actual fun createRandomAccessSource(filePath: String): RandomAccessSource? = + Posix32PreadRandomAccessSource.open(filePath) diff --git a/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/export/GGUFWriter.kt b/skainet-io/skainet-io-gguf/src/exportMain/kotlin/sk/ainet/io/gguf/export/GGUFWriter.kt similarity index 100% rename from skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/export/GGUFWriter.kt rename to skainet-io/skainet-io-gguf/src/exportMain/kotlin/sk/ainet/io/gguf/export/GGUFWriter.kt diff --git a/skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/export/GgufExportFacade.kt b/skainet-io/skainet-io-gguf/src/exportMain/kotlin/sk/ainet/io/gguf/export/GgufExportFacade.kt similarity index 100% rename from skainet-io/skainet-io-gguf/src/commonMain/kotlin/sk/ainet/io/gguf/export/GgufExportFacade.kt rename to skainet-io/skainet-io-gguf/src/exportMain/kotlin/sk/ainet/io/gguf/export/GgufExportFacade.kt diff --git a/skainet-io/skainet-io-gguf/src/nativeMain/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.native.kt b/skainet-io/skainet-io-gguf/src/native64Main/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.native64.kt similarity index 100% rename from skainet-io/skainet-io-gguf/src/nativeMain/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.native.kt rename to skainet-io/skainet-io-gguf/src/native64Main/kotlin/sk/ainet/io/gguf/RandomAccessSourceFactory.native64.kt diff --git a/skainet-lang/skainet-lang-dag/build.gradle.kts b/skainet-lang/skainet-lang-dag/build.gradle.kts index 448bc18c4..2113f9002 100644 --- a/skainet-lang/skainet-lang-dag/build.gradle.kts +++ b/skainet-lang/skainet-lang-dag/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { macosArm64() linuxX64() linuxArm64() + androidNativeArm32() jvm()