Skip to content

Commit 07b884d

Browse files
Merge branch 'develop' into dependabot/gradle/kotest-6.1.10
2 parents 4c73cf1 + 8171c5c commit 07b884d

99 files changed

Lines changed: 14240 additions & 562 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,10 @@ subprojects {
5151

5252
tasks.withType<Test>().configureEach {
5353
maxHeapSize = "8192m"
54+
useJUnitPlatform {
55+
if (!project.hasProperty("includeIntegration")) {
56+
excludeTags("integration")
57+
}
58+
}
5459
}
5560
}

docs/adding-compute-backend.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Adding a Compute Backend
2+
3+
This guide explains how to implement and register a new compute backend
4+
(e.g. Metal GPU, MLX, CUDA, Vulkan) for the SKaiNET inference engine.
5+
6+
## Architecture Overview
7+
8+
```
9+
llm-core (commonMain)
10+
└── BackendProvider ← interface you implement
11+
└── BackendRegistry ← expect object (discovery)
12+
13+
llm-core (jvmMain)
14+
└── BackendRegistry.jvm ← ServiceLoader-based discovery
15+
16+
llm-core (registryBasedMain)
17+
└── BackendRegistry ← manual registry (native, JS, Wasm, Android)
18+
19+
llm-runtime/kllama
20+
└── CpuBackendProvider ← reference implementation
21+
└── META-INF/services/... ← JVM SPI registration
22+
└── BackendActual.kt ← native registration per platform
23+
```
24+
25+
Backends are discovered differently per target:
26+
27+
| Target | Mechanism |
28+
|-------------------|------------------------------------------------------|
29+
| JVM | `java.util.ServiceLoader` — auto-discovers from JAR |
30+
| Native (macOS, Linux, iOS) | `registerPlatformBackends()` at startup |
31+
| Android, JS, Wasm | `BackendRegistry.register()` called by host app |
32+
33+
The registry auto-selects the **highest priority available** backend.
34+
Users can override with `--backend=NAME` on the CLI.
35+
36+
## Step 1: Implement `BackendProvider`
37+
38+
Create a class that implements `BackendProvider` from
39+
`llm-core/src/commonMain/kotlin/sk/ainet/apps/llm/backend/BackendProvider.kt`:
40+
41+
```kotlin
42+
package sk.ainet.apps.mybackend
43+
44+
import sk.ainet.apps.llm.backend.BackendProvider
45+
import sk.ainet.context.ExecutionContext
46+
47+
class MetalBackendProvider : BackendProvider {
48+
override val name: String = "metal"
49+
override val displayName: String = "Metal GPU"
50+
override val priority: Int = 100 // GPU > CPU (0)
51+
52+
override fun isAvailable(): Boolean {
53+
// Runtime check: is the hardware/driver present?
54+
// Return false if not — the registry will skip this backend.
55+
return try {
56+
MetalExecutionContext()
57+
true
58+
} catch (_: Throwable) {
59+
false
60+
}
61+
}
62+
63+
override fun createContext(): ExecutionContext {
64+
return MetalExecutionContext()
65+
}
66+
}
67+
```
68+
69+
**Priority guidelines:**
70+
71+
| Backend | Priority |
72+
|------------|----------|
73+
| CPU | 0 |
74+
| GPU (Metal, Vulkan) | 100 |
75+
| Specialized accelerator | 200 |
76+
77+
`isAvailable()` must be safe to call on any platform — return `false`
78+
if the hardware or native library is not present.
79+
80+
## Step 2: Register the backend
81+
82+
Registration differs by target.
83+
84+
### JVM — ServiceLoader (automatic)
85+
86+
Create a service file in your module's JVM resources:
87+
88+
```
89+
src/jvmMain/resources/META-INF/services/sk.ainet.apps.llm.backend.BackendProvider
90+
```
91+
92+
Contents (one fully-qualified class name per line):
93+
94+
```
95+
sk.ainet.apps.mybackend.MetalBackendProvider
96+
```
97+
98+
That's it. Adding the JAR to the classpath makes it discoverable.
99+
The Shadow JAR's `mergeServiceFiles()` handles combining service files
100+
from multiple JARs.
101+
102+
### Native — manual registration
103+
104+
In the platform-specific `BackendActual.kt`, add a `register()` call
105+
inside `registerPlatformBackends()`:
106+
107+
```kotlin
108+
// llm-runtime/kllama/src/macosMain/kotlin/.../BackendActual.kt
109+
110+
internal actual fun registerPlatformBackends() {
111+
BackendRegistry.register(CpuBackendProvider())
112+
BackendRegistry.register(MetalBackendProvider()) // ← add this
113+
}
114+
```
115+
116+
This is called once at CLI startup before backend selection happens.
117+
118+
### Android / JS / Wasm
119+
120+
Call `BackendRegistry.register()` from your application's initialization
121+
code before any inference calls:
122+
123+
```kotlin
124+
// In your app's startup
125+
BackendRegistry.register(CpuBackendProvider())
126+
BackendRegistry.register(MyGpuBackendProvider())
127+
```
128+
129+
## Step 3: Add the dependency
130+
131+
### As a separate module
132+
133+
If the backend lives in its own Gradle module or external JAR:
134+
135+
```kotlin
136+
// build.gradle.kts of the consuming module
137+
sourceSets {
138+
val jvmMain by getting {
139+
dependencies {
140+
implementation("sk.ainet.core:skainet-backend-metal:0.17.0")
141+
}
142+
}
143+
val macosMain by getting {
144+
dependencies {
145+
implementation("sk.ainet.core:skainet-backend-metal:0.17.0")
146+
}
147+
}
148+
}
149+
```
150+
151+
### Native bridge libraries
152+
153+
If the backend wraps a native C/C++ library (Metal, MLX, Vulkan),
154+
configure linker opts in the native binary block:
155+
156+
```kotlin
157+
macosArm64 {
158+
binaries {
159+
executable {
160+
linkerOpts(
161+
"-L/path/to/bridge", "-lmetal_bridge",
162+
"-framework", "Metal",
163+
"-framework", "MetalPerformanceShaders",
164+
"-framework", "Accelerate",
165+
)
166+
}
167+
}
168+
}
169+
```
170+
171+
## Step 4: Verify
172+
173+
### List backends
174+
175+
```bash
176+
# JVM
177+
./gradlew :llm-runtime:kllama:runJvm --args="--list-backends"
178+
179+
# Native (macOS)
180+
./llm-runtime/kllama/build/bin/macosArm64/debugExecutable/kllama.kexe --list-backends
181+
```
182+
183+
Expected output:
184+
185+
```
186+
Available backends:
187+
metal Metal GPU (priority=100, available)
188+
cpu CPU (SIMD) (priority=0, available)
189+
```
190+
191+
### Run with a specific backend
192+
193+
```bash
194+
./gradlew :llm-runtime:kllama:runJvm \
195+
--args="--backend=metal -m model.gguf 'Hello'"
196+
```
197+
198+
### Auto-selection
199+
200+
Without `--backend`, the registry picks the highest-priority available
201+
backend automatically (Metal over CPU in this example).
202+
203+
## Reference: Existing implementation
204+
205+
The CPU backend serves as the reference implementation:
206+
207+
- **Provider:** `llm-runtime/kllama/src/commonMain/kotlin/sk/ainet/apps/kllama/CpuBackendProvider.kt`
208+
- **JVM SPI file:** `llm-runtime/kllama/src/jvmMain/resources/META-INF/services/sk.ainet.apps.llm.backend.BackendProvider`
209+
- **Native registration:** `llm-runtime/kllama/src/{macosMain,linuxMain,iosMain}/kotlin/.../BackendActual.kt`
210+
- **Interface:** `llm-core/src/commonMain/kotlin/sk/ainet/apps/llm/backend/BackendProvider.kt`
211+
- **Registry:** `llm-core/src/commonMain/kotlin/sk/ainet/apps/llm/backend/BackendRegistry.kt`
212+
213+
## File checklist for a new backend
214+
215+
```
216+
[ ] BackendProvider implementation class
217+
[ ] JVM: META-INF/services file listing the provider class
218+
[ ] Native: register() call in platform BackendActual.kt
219+
[ ] build.gradle.kts: dependency declaration
220+
[ ] Native: linkerOpts if wrapping C/C++ bridge
221+
[ ] Verify: --list-backends shows the new backend
222+
[ ] Verify: --backend=NAME runs inference with it
223+
```

0 commit comments

Comments
 (0)