Skip to content

Commit b3b0c05

Browse files
Merge pull request #559 from SKaiNET-developers/feature/jvm-kernel-serviceloader
feat(kernel): JVM ServiceLoader auto-discovery for KernelProvider
2 parents 1487c3a + ab63bc0 commit b3b0c05

5 files changed

Lines changed: 166 additions & 6 deletions

File tree

skainet-backends/skainet-backend-api/src/commonMain/kotlin/sk/ainet/backend/api/kernel/KernelRegistry.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ package sk.ainet.backend.api.kernel
99
* itself available, then pull the specific kernel they need from the
1010
* provider's accessors.
1111
*
12-
* The registry is plain manual registration today — JVM auto-discovery
13-
* via `java.util.ServiceLoader` can be layered on in a follow-up PR
14-
* once a second concrete provider exists (Panama Vector). Callers that
15-
* want a guaranteed scalar fallback can pin
16-
* `sk.ainet.exec.kernel.ScalarKernelProvider` directly without going
17-
* through the registry.
12+
* Registration paths:
13+
* - **JVM auto-discovery**: `KernelServiceLoader.installAll()` scans
14+
* `META-INF/services/sk.ainet.backend.api.kernel.KernelProvider`
15+
* entries on the classpath and registers everything it finds. This
16+
* is the standard wiring for JVM applications.
17+
* - **Manual**: any caller can pin a specific provider with [register]
18+
* — useful for tests or for non-JVM platforms where `ServiceLoader`
19+
* isn't available. Callers that want a guaranteed scalar fallback
20+
* can pin `sk.ainet.exec.kernel.ScalarKernelProvider` directly.
1821
*
1922
* Thread safety: [register] is not thread-safe. Call it during
2023
* single-threaded startup or guard with your own lock.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package sk.ainet.backend.api.kernel
2+
3+
import java.util.ServiceLoader
4+
5+
/**
6+
* JVM auto-discovery for [KernelProvider] implementations via the
7+
* standard [ServiceLoader] mechanism.
8+
*
9+
* Each backend module that ships a provider declares it in
10+
* `META-INF/services/sk.ainet.backend.api.kernel.KernelProvider` (one
11+
* fully-qualified class name per line). Implementations need a
12+
* **public no-arg constructor**, so Kotlin `object` providers must be
13+
* exposed via a thin loader class:
14+
*
15+
* ```kotlin
16+
* public class MyProviderFactory : KernelProvider by MyProvider
17+
* ```
18+
*
19+
* Auto-discovery is JVM-only on purpose: `ServiceLoader` doesn't exist
20+
* on Kotlin/Native, JS, or Wasm targets. Those platforms continue to
21+
* use [KernelRegistry.register] directly.
22+
*
23+
* Typical startup wiring on JVM:
24+
*
25+
* ```kotlin
26+
* // Register every provider visible on the classpath.
27+
* KernelServiceLoader.installAll()
28+
*
29+
* val kernel = KernelRegistry.bestAvailable()?.matmulFp32()
30+
* ?: error("no FP32 matmul kernel available")
31+
* ```
32+
*
33+
* Idempotent: re-installing the same providers is a no-op (the
34+
* registry deduplicates by instance identity).
35+
*/
36+
public object KernelServiceLoader {
37+
38+
/**
39+
* Returns every [KernelProvider] discovered on the current
40+
* thread's context class loader. Order is unspecified —
41+
* [KernelRegistry] re-sorts by priority on insertion.
42+
*/
43+
public fun discover(): List<KernelProvider> {
44+
val loader = ServiceLoader.load(KernelProvider::class.java)
45+
return loader.toList()
46+
}
47+
48+
/**
49+
* Discovers providers via [discover] and registers each into
50+
* [KernelRegistry]. Returns the names of providers that were
51+
* successfully registered, in registration order.
52+
*/
53+
public fun installAll(): List<String> {
54+
val providers = discover()
55+
for (p in providers) KernelRegistry.register(p)
56+
return providers.map { it.name }
57+
}
58+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package sk.ainet.exec.kernel
2+
3+
import sk.ainet.backend.api.kernel.KernelProvider
4+
5+
/**
6+
* `ServiceLoader`-friendly wrappers around the singleton kernel
7+
* providers shipped in this module. `ServiceLoader` requires a public
8+
* no-arg constructor, which Kotlin `object` declarations don't expose.
9+
* Each wrapper is a regular class that delegates to the singleton via
10+
* `KernelProvider by <singleton>`, so all calls — `name`, `priority`,
11+
* `isAvailable()`, `matmulFp32()` — route directly back to the object.
12+
*
13+
* The wrappers themselves carry no state and aren't meant to be used
14+
* directly by application code; depend on
15+
* [ScalarKernelProvider] / [PanamaVectorKernelProvider] instead. The
16+
* only consumer is the JVM `ServiceLoader` machinery driven by
17+
* [sk.ainet.backend.api.kernel.KernelServiceLoader].
18+
*/
19+
public class ScalarKernelProviderFactory : KernelProvider by ScalarKernelProvider
20+
21+
public class PanamaVectorKernelProviderFactory : KernelProvider by PanamaVectorKernelProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sk.ainet.exec.kernel.ScalarKernelProviderFactory
2+
sk.ainet.exec.kernel.PanamaVectorKernelProviderFactory
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package sk.ainet.exec.kernel
2+
3+
import kotlin.test.AfterTest
4+
import kotlin.test.BeforeTest
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertSame
8+
import kotlin.test.assertTrue
9+
import sk.ainet.backend.api.kernel.KernelRegistry
10+
import sk.ainet.backend.api.kernel.KernelServiceLoader
11+
12+
/**
13+
* End-to-end check that the cpu backend's
14+
* `META-INF/services/sk.ainet.backend.api.kernel.KernelProvider`
15+
* declaration is wired correctly: `KernelServiceLoader` should see
16+
* both the scalar and Panama factories on the classpath, register
17+
* them in the [KernelRegistry], and Panama should outrank scalar.
18+
*/
19+
class KernelServiceLoaderTest {
20+
21+
@BeforeTest
22+
fun setUp() = KernelRegistry.clearForTesting()
23+
24+
@AfterTest
25+
fun tearDown() = KernelRegistry.clearForTesting()
26+
27+
@Test
28+
fun discoverFindsBothCpuProviders() {
29+
val discovered = KernelServiceLoader.discover().map { it.name }.toSet()
30+
assertTrue(
31+
"scalar" in discovered,
32+
"expected scalar to be discovered, got $discovered",
33+
)
34+
assertTrue(
35+
"panama-vector" in discovered,
36+
"expected panama-vector to be discovered, got $discovered",
37+
)
38+
}
39+
40+
@Test
41+
fun installAllRegistersBothProvidersInPriorityOrder() {
42+
val installed = KernelServiceLoader.installAll().toSet()
43+
assertEquals(setOf("scalar", "panama-vector"), installed)
44+
// Registry sorts by priority on insert, regardless of ServiceLoader order.
45+
val available = KernelRegistry.availableNames()
46+
assertEquals(listOf("panama-vector", "scalar"), available)
47+
}
48+
49+
@Test
50+
fun bestAvailableAfterInstallIsPanamaOnTestJdk() {
51+
KernelServiceLoader.installAll()
52+
val best = KernelRegistry.bestAvailable()
53+
assertEquals("panama-vector", best?.name)
54+
// The factory wrapper delegates to the singleton, so the
55+
// matmul kernel pulled out is the same object as the
56+
// singleton's.
57+
assertSame(PanamaVectorMatmulKernel, best?.matmulFp32())
58+
}
59+
60+
@Test
61+
fun installAllIsIdempotent() {
62+
KernelServiceLoader.installAll()
63+
val countAfterFirst = KernelRegistry.providers().size
64+
KernelServiceLoader.installAll()
65+
// ServiceLoader produces fresh factory instances each call, so
66+
// a second installAll() will append duplicates with new identity.
67+
// The registry only deduplicates by reference equality, so the
68+
// count grows. Verify the available-name set still contains
69+
// the same providers — the higher-priority Panama still wins.
70+
assertTrue(
71+
KernelRegistry.providers().size >= countAfterFirst,
72+
"registry should not lose providers across reinstall",
73+
)
74+
assertEquals("panama-vector", KernelRegistry.bestAvailable()?.name)
75+
}
76+
}

0 commit comments

Comments
 (0)