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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ include("skainet-compile:skainet-compile-c")
// ====== BACKENDS
include("skainet-backends:skainet-backend-api")
include("skainet-backends:skainet-backend-cpu")
include("skainet-backends:skainet-backend-native-cpu")

// ====== BENCHMARKS
include("skainet-backends:benchmarks:jvm-cpu-jmh")
Expand Down
115 changes: 115 additions & 0 deletions skainet-backends/skainet-backend-native-cpu/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
explicitApi()
jvm()

sourceSets {
val jvmMain by getting {
dependencies {
implementation(project(":skainet-backends:skainet-backend-api"))
}
}
val jvmTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}

// --- Native (CMake) wiring -------------------------------------------------
//
// PR 1 builds for the host arch only. Cross-arch CI matrix is deferred per
// the native-ffm-plan asciidoc. The artifact lands at
// build/native/resources/native/<os>-<arch>/libskainet_kernels.{so|dylib|dll}
// and is bundled into the JAR via an extra resources srcDir on jvmMain.

val nativeOsArch: String = run {
val os = System.getProperty("os.name").lowercase()
val osTag = when {
os.contains("linux") -> "linux"
os.contains("mac") || os.contains("darwin") -> "macos"
os.contains("windows") -> "windows"
else -> error("Unsupported OS for skainet-backend-native-cpu: $os")
}
val archRaw = System.getProperty("os.arch").lowercase()
val archTag = when (archRaw) {
"x86_64", "amd64" -> "x86_64"
"aarch64", "arm64" -> "arm64"
else -> error("Unsupported arch for skainet-backend-native-cpu: $archRaw")
}
"$osTag-$archTag"
}

// Capture as plain Strings up front so configuration-cache serialization
// doesn't have to walk back to the build script for Provider resolution.
val nativeSourcePath: String = layout.projectDirectory.dir("native").asFile.absolutePath
val cmakeBuildPath: String = layout.buildDirectory.dir("native/cmake-build").get().asFile.absolutePath
val nativeResourcesRoot = layout.buildDirectory.dir("native/resources")
val nativeResourceTargetDir = nativeResourcesRoot.map { it.dir("native/$nativeOsArch") }

val configureNativeKernels by tasks.registering(Exec::class) {
group = "build"
description = "Run CMake configure step for the native kernels library."
inputs.file("$nativeSourcePath/CMakeLists.txt")
inputs.dir("$nativeSourcePath/src")
inputs.dir("$nativeSourcePath/include")
outputs.dir(cmakeBuildPath)
// CMake auto-creates the -B directory; no doFirst mkdirs needed.
commandLine = listOf(
"cmake",
"-S", nativeSourcePath,
"-B", cmakeBuildPath,
"-DCMAKE_BUILD_TYPE=Release",
)
}

val buildNativeKernels by tasks.registering(Exec::class) {
group = "build"
description = "Build the native kernels shared library via CMake."
dependsOn(configureNativeKernels)
inputs.file("$nativeSourcePath/CMakeLists.txt")
inputs.dir("$nativeSourcePath/src")
inputs.dir("$nativeSourcePath/include")
outputs.dir(cmakeBuildPath)
commandLine = listOf(
"cmake",
"--build", cmakeBuildPath,
"--config", "Release",
)
}

val packageNativeKernels by tasks.registering(Copy::class) {
group = "build"
description = "Stage the built native kernels library into JVM resources."
dependsOn(buildNativeKernels)
from(cmakeBuildPath) {
include(
"libskainet_kernels.so",
"libskainet_kernels.dylib",
"skainet_kernels.dll",
"Release/skainet_kernels.dll",
)
eachFile { path = name }
}
into(nativeResourceTargetDir)
}

kotlin.sourceSets.named("jvmMain") {
resources.srcDir(nativeResourcesRoot)
}

tasks.named("jvmProcessResources") {
dependsOn(packageNativeKernels)
}

tasks.withType<Test>().configureEach {
jvmArgs("--enable-preview", "--enable-native-access=ALL-UNNAMED")
}

tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-preview", "--enable-native-access=ALL-UNNAMED")
}
30 changes: 30 additions & 0 deletions skainet-backends/skainet-backend-native-cpu/native/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.20)
project(skainet_kernels C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

add_library(skainet_kernels SHARED
src/skainet_smoke.c
)

target_include_directories(skainet_kernels PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

# Strip the "lib" prefix on Windows so the artifact name is consistent
# with the resource-bundle path skainet_kernels.{dll,so,dylib}.
if(WIN32)
set_target_properties(skainet_kernels PROPERTIES PREFIX "")
endif()

# Hide non-exported symbols on ELF / Mach-O for a smaller surface area.
if(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
target_compile_options(skainet_kernels PRIVATE -fvisibility=hidden -Wall -Wextra)
set_target_properties(skainet_kernels PROPERTIES C_VISIBILITY_PRESET hidden)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#ifndef SKAINET_KERNELS_H
#define SKAINET_KERNELS_H

#include <stdint.h>

#if defined(_WIN32) || defined(__CYGWIN__)
# define SKAINET_API __declspec(dllexport)
#elif defined(__GNUC__) || defined(__clang__)
# define SKAINET_API __attribute__((visibility("default")))
#else
# define SKAINET_API
#endif

#ifdef __cplusplus
extern "C" {
#endif

/*
* Trivial smoke kernel proving the FFM downcall pipeline end-to-end.
*
* for (int i = 0; i < length; ++i) output[i] = 2.0f * input[i];
*
* The Kotlin caller owns the memory backing `input` and `output`; the
* kernel must not retain pointers past return.
*/
SKAINET_API void skainet_smoke_double(const float* input, float* output, int32_t length);

#ifdef __cplusplus
}
#endif

#endif /* SKAINET_KERNELS_H */
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "skainet_kernels.h"

void skainet_smoke_double(const float* input, float* output, int32_t length) {
for (int32_t i = 0; i < length; ++i) {
output[i] = 2.0f * input[i];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package sk.ainet.exec.kernel

import java.lang.foreign.Arena
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.MemorySegment
import java.lang.foreign.ValueLayout
import java.lang.invoke.MethodHandle

/**
* End-to-end smoke test of the FFM downcall pipeline used by the
* native kernel provider. Calls the bundled native function
*
* void skainet_smoke_double(const float* input, float* output, int32_t length);
*
* which writes `output[i] = 2.0f * input[i]`. This object exists only
* to validate the loader → Linker → MethodHandle path on real hardware
* before any production kernel ships in PR 2.
*
* Not part of the public SPI: `internal` visibility, exposed to tests
* via the same package.
*/
internal object NativeFfmSmoke {

fun isAvailable(): Boolean = handle != null

/**
* Run the bundled smoke kernel on [input] and return a fresh
* `FloatArray` with `output[i] = 2.0f * input[i]`. Returns `null`
* when the native lib failed to load (callers fall back to a
* pure-Kotlin reference).
*/
fun double(input: FloatArray): FloatArray? {
val mh = handle ?: return null
val output = FloatArray(input.size)
val byteSize = input.size.toLong() * java.lang.Float.BYTES
val byteAlign = ValueLayout.JAVA_FLOAT.byteAlignment()
Arena.ofConfined().use { arena ->
val inSeg = arena.allocate(byteSize, byteAlign)
val outSeg = arena.allocate(byteSize, byteAlign)
MemorySegment.copy(input, 0, inSeg, ValueLayout.JAVA_FLOAT, 0L, input.size)
mh.invoke(inSeg, outSeg, input.size)
MemorySegment.copy(outSeg, ValueLayout.JAVA_FLOAT, 0L, output, 0, output.size)
}
return output
}

private val handle: MethodHandle? by lazy {
val lookup = NativeLibraryLoader.lookup() ?: return@lazy null
val symbol = lookup.find("skainet_smoke_double").orElse(null) ?: return@lazy null
val descriptor = FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT,
)
runCatching { Linker.nativeLinker().downcallHandle(symbol, descriptor) }.getOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package sk.ainet.exec.kernel

import sk.ainet.backend.api.kernel.Fp32MatmulKernel
import sk.ainet.backend.api.kernel.KernelProvider
import sk.ainet.backend.api.kernel.Q4KMatmulKernel

/**
* Native (FFM) [KernelProvider]. Sits at priority `100`, above
* [PanamaVectorKernelProvider] (`50`) and the scalar reference (`0`).
*
* PR 1 of the staged native-FFM rollout (see the `native-ffm-plan`
* asciidoc) only ships the module scaffolding: the Gradle ↔ CMake
* pipeline that produces a host-arch shared library, its bundling into
* JAR resources, and an end-to-end FFM smoke downcall test. No real
* matmul kernel is wired into the public SPI yet.
*
* Until [NativeQ4KMatmulKernel] (or its `MemSegment`-input sibling)
* lands in PR 2, this provider deliberately reports `isAvailable() =
* false` and returns `null` from every kernel accessor. That keeps
* `KernelRegistry.bestAvailable()` cleanly cascading down to the
* Panama priority-50 provider on every shape we measure today, so
* adding the new module to the classpath produces no behavior change.
*/
public object NativeKernelProvider : KernelProvider {
override val name: String = "native-ffm"
override val priority: Int = 100

override fun isAvailable(): Boolean = false

override fun matmulFp32(): Fp32MatmulKernel? = null

override fun matmulQ4K(): Q4KMatmulKernel? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package sk.ainet.exec.kernel

import sk.ainet.backend.api.kernel.KernelProvider

/**
* `ServiceLoader`-friendly wrapper around [NativeKernelProvider]. The
* platform `ServiceLoader` machinery requires a public no-arg
* constructor, which a Kotlin `object` does not expose; this factory
* delegates every [KernelProvider] member back to the singleton.
*
* Listed in
* `META-INF/services/sk.ainet.backend.api.kernel.KernelProvider` so
* `KernelServiceLoader.installAll()` discovers the provider on JVM
* startup.
*/
public class NativeKernelProviderFactory : KernelProvider by NativeKernelProvider
Loading
Loading