From a9efb6211bea8611a74499e195234bf2a29b266e Mon Sep 17 00:00:00 2001 From: michalharakal Date: Sat, 17 Oct 2026 14:27:58 +0200 Subject: [PATCH] =?UTF-8?q?fix(laya):=20LayaModelLoader.load(Path,=20?= =?UTF-8?q?=E2=80=A6)=20is=20a=20member=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.62.0 made the JVM convenience an extension on the common object, which needs an explicit import of sk.ainet.models.laya.load; every existing caller of LayaModelLoader.load(dir, ctx, exactGelu) failed to compile. LayaModelLoader is an expect object now: the common suspend load is unchanged on all targets (actuals for the JVM, Android, native, JS, wasmJs and wasmWasi), the JVM actual adds the Path overload as a member, @JvmOverloads restoring the 0.61 Java signature. API dump regenerated; every laya target compiles, jvmTest green. --- CHANGELOG.md | 11 ++++ llm-inference/laya/api/jvm/laya.api | 9 ++- .../sk/ainet/models/laya/LayaModelLoader.kt | 17 +++++ .../sk/ainet/models/laya/LayaModelLoader.kt | 38 +++++++---- .../sk/ainet/models/laya/LayaModelLoader.kt | 17 +++++ .../sk/ainet/models/laya/LayaModelLoader.kt | 64 +++++++++++-------- .../sk/ainet/models/laya/LayaModelLoader.kt | 17 +++++ .../sk/ainet/models/laya/LayaModelLoader.kt | 17 +++++ .../sk/ainet/models/laya/LayaModelLoader.kt | 17 +++++ 9 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 llm-inference/laya/src/androidMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt create mode 100644 llm-inference/laya/src/jsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt create mode 100644 llm-inference/laya/src/nativeMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt create mode 100644 llm-inference/laya/src/wasmJsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt create mode 100644 llm-inference/laya/src/wasmWasiMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a1b95b..685ce1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **`LayaModelLoader.load(directory: Path, …)` is a member again.** 0.62.0 had turned the JVM + convenience into an extension function on the common `LayaModelLoader` object; an extension needs + an explicit `import sk.ainet.models.laya.load`, so every existing Kotlin caller of + `LayaModelLoader.load(dir, ctx, exactGelu = true)` stopped compiling against 0.62.0 — the opposite + of the source compatibility 0.62.0 claimed. `LayaModelLoader` is now an `expect` object: the common + `load(files, weights, ctx, …)` is unchanged on every target, and the JVM `actual` adds + `load(directory, ctx, exactGelu, weightFormFor)` as a member, `@JvmOverloads` so that Java callers + get the 0.61 `load(Path, ExecutionContext, boolean)` signature back as well. + ## [0.62.0] — 2026-10-17 **Laya on the packed Q8_0 lane.** Against engine **0.61.0** (SKEEP-007, the browser lane: diff --git a/llm-inference/laya/api/jvm/laya.api b/llm-inference/laya/api/jvm/laya.api index f2a5fafb..031682ea 100644 --- a/llm-inference/laya/api/jvm/laya.api +++ b/llm-inference/laya/api/jvm/laya.api @@ -306,13 +306,12 @@ public final class sk/ainet/models/laya/LayaLinear : sk/ainet/lang/nn/Module, sk public final class sk/ainet/models/laya/LayaModelLoader { public static final field INSTANCE Lsk/ainet/models/laya/LayaModelLoader; + public final fun load (Ljava/nio/file/Path;Lsk/ainet/context/ExecutionContext;)Lsk/ainet/models/laya/LayaAgent; + public final fun load (Ljava/nio/file/Path;Lsk/ainet/context/ExecutionContext;Z)Lsk/ainet/models/laya/LayaAgent; + public final fun load (Ljava/nio/file/Path;Lsk/ainet/context/ExecutionContext;ZLkotlin/jvm/functions/Function1;)Lsk/ainet/models/laya/LayaAgent; public final fun load (Lsk/ainet/models/laya/LayaCheckpointFiles;Lkotlin/jvm/functions/Function0;Lsk/ainet/context/ExecutionContext;ZLsk/ainet/lang/memory/plan/WeightForm;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun load$default (Lsk/ainet/models/laya/LayaModelLoader;Lsk/ainet/models/laya/LayaCheckpointFiles;Lkotlin/jvm/functions/Function0;Lsk/ainet/context/ExecutionContext;ZLsk/ainet/lang/memory/plan/WeightForm;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; -} - -public final class sk/ainet/models/laya/LayaModelLoaderKt { - public static final fun load (Lsk/ainet/models/laya/LayaModelLoader;Ljava/nio/file/Path;Lsk/ainet/context/ExecutionContext;ZLkotlin/jvm/functions/Function1;)Lsk/ainet/models/laya/LayaAgent; public static synthetic fun load$default (Lsk/ainet/models/laya/LayaModelLoader;Ljava/nio/file/Path;Lsk/ainet/context/ExecutionContext;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lsk/ainet/models/laya/LayaAgent; + public static synthetic fun load$default (Lsk/ainet/models/laya/LayaModelLoader;Lsk/ainet/models/laya/LayaCheckpointFiles;Lkotlin/jvm/functions/Function0;Lsk/ainet/context/ExecutionContext;ZLsk/ainet/lang/memory/plan/WeightForm;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class sk/ainet/models/laya/LayaNetwork { diff --git a/llm-inference/laya/src/androidMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/androidMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt new file mode 100644 index 00000000..4996dd25 --- /dev/null +++ b/llm-inference/laya/src/androidMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -0,0 +1,17 @@ +package sk.ainet.models.laya + +import sk.ainet.context.ExecutionContext +import sk.ainet.io.RandomAccessSource +import sk.ainet.lang.memory.plan.WeightForm +import sk.ainet.lang.types.FP32 + +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) +} diff --git a/llm-inference/laya/src/commonMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/commonMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt index 8f90d26c..eb94b792 100644 --- a/llm-inference/laya/src/commonMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt +++ b/llm-inference/laya/src/commonMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -16,7 +16,9 @@ import sk.ainet.lang.types.FP32 * * Common code since engine 0.61 (SKEEP-007): the JVM, Android and browser builds all run this * function; only how the bytes are obtained differs (a file, an app's files directory, a download - * held in memory). The FP32 reference path (REQ-EXE-008) is the default: F16 weights are up-cast + * held in memory). It is an `expect` object so that the JVM can add its `load(directory: Path, …)` + * convenience as a *member*: callers reach it as `LayaModelLoader.load(dir, ctx)` without an + * import, as they did before 0.62.0. The FP32 reference path (REQ-EXE-008) is the default: F16 weights are up-cast * on load, which is what the Python reference does on CPU. * * [weightForm] / [weightFormFor] are passed through to the engine's SafeTensors loader. With @@ -24,7 +26,7 @@ import sk.ainet.lang.types.FP32 * Q8_0 at load — about a quarter of the FP32 bytes, served by the packed kernels on every tier — * and the decision-quality gate (`LayaQuantizedParityTest`) is the contract that lane keeps. */ -public object LayaModelLoader { +public expect object LayaModelLoader { public suspend fun load( files: LayaCheckpointFiles, weights: () -> RandomAccessSource, @@ -32,17 +34,27 @@ public object LayaModelLoader { exactGelu: Boolean = true, weightForm: WeightForm? = null, weightFormFor: ((String) -> WeightForm?)? = null, - ): LayaAgent { - val preprocessor = LayaPreprocessor.fromFiles(files) - val raw = ArrayList>() - SafeTensorsParametersLoader(sourceProvider = weights, weightForm = weightForm, weightFormFor = weightFormFor) - .load(ctx, FP32::class) { name, tensor -> - raw += WeightTensor(name, tensor.shape.dimensions.toList(), tensor) - } - val prepared = LayaWeightPreparation.prepare(raw, preprocessor.encoderConfig, preprocessor.agentConfig, ctx, FP32::class) - val model = LayaDecisionModel.build(preprocessor.encoderConfig, preprocessor.agentConfig, prepared, ctx, FP32::class, exactGelu) - return LayaAgent(preprocessor, model) - } + ): LayaAgent +} + +/** The six loading steps every platform's [LayaModelLoader.load] runs. */ +internal suspend fun loadLayaAgent( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, +): LayaAgent { + val preprocessor = LayaPreprocessor.fromFiles(files) + val raw = ArrayList>() + SafeTensorsParametersLoader(sourceProvider = weights, weightForm = weightForm, weightFormFor = weightFormFor) + .load(ctx, FP32::class) { name, tensor -> + raw += WeightTensor(name, tensor.shape.dimensions.toList(), tensor) + } + val prepared = LayaWeightPreparation.prepare(raw, preprocessor.encoderConfig, preprocessor.agentConfig, ctx, FP32::class) + val model = LayaDecisionModel.build(preprocessor.encoderConfig, preprocessor.agentConfig, prepared, ctx, FP32::class, exactGelu) + return LayaAgent(preprocessor, model) } /** diff --git a/llm-inference/laya/src/jsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/jsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt new file mode 100644 index 00000000..4996dd25 --- /dev/null +++ b/llm-inference/laya/src/jsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -0,0 +1,17 @@ +package sk.ainet.models.laya + +import sk.ainet.context.ExecutionContext +import sk.ainet.io.RandomAccessSource +import sk.ainet.lang.memory.plan.WeightForm +import sk.ainet.lang.types.FP32 + +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) +} diff --git a/llm-inference/laya/src/jvmMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/jvmMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt index b7ad17b5..05f1cf95 100644 --- a/llm-inference/laya/src/jvmMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt +++ b/llm-inference/laya/src/jvmMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -3,35 +3,49 @@ package sk.ainet.models.laya import kotlinx.coroutines.runBlocking import sk.ainet.context.ExecutionContext import sk.ainet.io.JvmRandomAccessSource +import sk.ainet.io.RandomAccessSource import sk.ainet.lang.memory.plan.WeightForm import sk.ainet.lang.types.FP32 import java.nio.file.Files import java.nio.file.Path -/** - * Loads a checkpoint directory (`rl_agent_config.json`, `encoder/config.json`, `tokenizer/`, - * `model.safetensors`) into a [LayaAgent] — the JVM convenience over the common - * [LayaModelLoader.load]. Blocking; call off the UI thread. - * - * [weightFormFor] selects the lane: `null` is the FP32 reference path (REQ-EXE-008, F16 up-cast on - * load), [LayaWeightForms.q8_0For] the packed Q8_0 lane. - */ -public fun LayaModelLoader.load( - directory: Path, - ctx: ExecutionContext, - exactGelu: Boolean = true, - weightFormFor: ((String) -> WeightForm?)? = null, -): LayaAgent { - val weights = directory.resolve("model.safetensors") - require(Files.exists(weights)) { "no model.safetensors in $directory" } - val files = LayaCheckpointFiles.read(directory) - return runBlocking { - load( - files = files, - weights = { JvmRandomAccessSource.open(weights.toString()) }, - ctx = ctx, - exactGelu = exactGelu, - weightFormFor = weightFormFor, - ) +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) + + /** + * Loads a checkpoint directory (`rl_agent_config.json`, `encoder/config.json`, `tokenizer/`, + * `model.safetensors`) into a [LayaAgent]: the JVM convenience over the common [load]. + * Blocking; call off the UI thread. + * + * [weightFormFor] selects the lane: `null` is the FP32 reference path (REQ-EXE-008, F16 up-cast on + * load), [LayaWeightForms.q8_0For] the packed Q8_0 lane. `@JvmOverloads` keeps the 0.61 + * `load(Path, ExecutionContext, boolean)` signature for Java callers. + */ + @JvmOverloads + public fun load( + directory: Path, + ctx: ExecutionContext, + exactGelu: Boolean = true, + weightFormFor: ((String) -> WeightForm?)? = null, + ): LayaAgent { + val weights = directory.resolve("model.safetensors") + require(Files.exists(weights)) { "no model.safetensors in $directory" } + val files = LayaCheckpointFiles.read(directory) + return runBlocking { + load( + files = files, + weights = { JvmRandomAccessSource.open(weights.toString()) }, + ctx = ctx, + exactGelu = exactGelu, + weightFormFor = weightFormFor, + ) + } } } diff --git a/llm-inference/laya/src/nativeMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/nativeMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt new file mode 100644 index 00000000..4996dd25 --- /dev/null +++ b/llm-inference/laya/src/nativeMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -0,0 +1,17 @@ +package sk.ainet.models.laya + +import sk.ainet.context.ExecutionContext +import sk.ainet.io.RandomAccessSource +import sk.ainet.lang.memory.plan.WeightForm +import sk.ainet.lang.types.FP32 + +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) +} diff --git a/llm-inference/laya/src/wasmJsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/wasmJsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt new file mode 100644 index 00000000..4996dd25 --- /dev/null +++ b/llm-inference/laya/src/wasmJsMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -0,0 +1,17 @@ +package sk.ainet.models.laya + +import sk.ainet.context.ExecutionContext +import sk.ainet.io.RandomAccessSource +import sk.ainet.lang.memory.plan.WeightForm +import sk.ainet.lang.types.FP32 + +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) +} diff --git a/llm-inference/laya/src/wasmWasiMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt b/llm-inference/laya/src/wasmWasiMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt new file mode 100644 index 00000000..4996dd25 --- /dev/null +++ b/llm-inference/laya/src/wasmWasiMain/kotlin/sk/ainet/models/laya/LayaModelLoader.kt @@ -0,0 +1,17 @@ +package sk.ainet.models.laya + +import sk.ainet.context.ExecutionContext +import sk.ainet.io.RandomAccessSource +import sk.ainet.lang.memory.plan.WeightForm +import sk.ainet.lang.types.FP32 + +public actual object LayaModelLoader { + public actual suspend fun load( + files: LayaCheckpointFiles, + weights: () -> RandomAccessSource, + ctx: ExecutionContext, + exactGelu: Boolean, + weightForm: WeightForm?, + weightFormFor: ((String) -> WeightForm?)?, + ): LayaAgent = loadLayaAgent(files, weights, ctx, exactGelu, weightForm, weightFormFor) +}