arcane-os 0.78.0 → 0.79.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +6 -1
- package/browser-runtime/ai/core-onnx.mjs +6 -2
- package/browser-runtime/ai/speech-worker-runtime.mjs +1 -1
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/publishing.md +20 -3
- package/docs/reference/ai/browser-speech.md +7 -1
- package/docs/reference/execution-devices.md +157 -0
- package/docs/reference/inventory/package-api.json +39 -11
- package/docs/reference/local-ai.md +104 -10
- package/docs/reference/native-decisions.md +65 -10
- package/package.json +4 -2
- package/src/core/services/decisions.mjs +51 -15
- package/src/core/services/execution-devices.mjs +32 -0
- package/src/core/services/local-ai.mjs +12 -5
- package/src/local-ai/decisions.mjs +93 -15
- package/src/local-ai/dev.mjs +7 -3
- package/src/local-ai/execution-devices-windows.ps1 +833 -0
- package/src/local-ai/execution-devices.mjs +288 -0
- package/src/local-ai/onnx-worker.mjs +149 -19
- package/src/local-ai/onnx.mjs +87 -27
- package/src/native/portable-layout.mjs +8 -1
|
@@ -58,7 +58,7 @@ name to `decisions`. It exposes `load`, `evaluate`, `classify` (an alias for
|
|
|
58
58
|
| Method | Parameters and result |
|
|
59
59
|
| --- | --- |
|
|
60
60
|
| `decisions.status` | Current lifecycle snapshot; does not load anything. |
|
|
61
|
-
| `decisions.load` |
|
|
61
|
+
| `decisions.load` | `{executionTarget?}`, where the target is `{deviceId:string}` or `null`; explicit activation or target replacement, returning the ready lifecycle snapshot. The request signal cancels activation. |
|
|
62
62
|
| `decisions.evaluate` | `{rows, runOptions?}`; returns `{decisions, outputs}`. The request signal cancels the active model operation. |
|
|
63
63
|
| `decisions.unload` | Releases the activation. Service-lifetime cleanup continues after the caller disconnects. |
|
|
64
64
|
|
|
@@ -111,12 +111,66 @@ is loaded by this API.
|
|
|
111
111
|
This graph selection requires `dtype:'fp32'`, which is also the default; another
|
|
112
112
|
dtype reports the incompatible selection rather than substituting a graph.
|
|
113
113
|
`sessionOptions` and `runOptions` pass to the existing native ONNX API unchanged.
|
|
114
|
-
`executionPreference` defaults to `gpu`
|
|
115
|
-
provider creation is attempted, followed by
|
|
116
|
-
`sessionOptions.executionProviders` takes
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
114
|
+
With no `executionTarget` selection, `executionPreference` defaults to `gpu`
|
|
115
|
+
here: actual advertised native GPU provider creation is attempted, followed by
|
|
116
|
+
an honest CPU fallback. Explicit `sessionOptions.executionProviders` takes
|
|
117
|
+
precedence on that unchanged path. The underlying generic ONNX factory
|
|
118
|
+
continues to default to CPU. Accepted provider configuration is not evidence
|
|
119
|
+
that GPU nodes executed. Full provider-attempt diagnostics remain in
|
|
120
|
+
`execution`; see [native ONNX sessions](local-ai.md#native-onnx-sessions).
|
|
121
|
+
|
|
122
|
+
## Select an activation's execution device
|
|
123
|
+
|
|
124
|
+
Both factories accept `executionTarget: {deviceId:string}` or `null` in their
|
|
125
|
+
configuration. Direct model and service calls accept
|
|
126
|
+
`load({executionTarget, signal})`; the Core `decisions.load` method carries the
|
|
127
|
+
same target in its parameters. Use an actual stable ID from the shared
|
|
128
|
+
[physical device catalog](execution-devices.md), or the documented whole-host
|
|
129
|
+
CPU ID:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
await decisions.load({executionTarget: {deviceId: 'cpu'}});
|
|
133
|
+
// A different explicit selection drains the current activation and replaces it.
|
|
134
|
+
await decisions.load({executionTarget: null});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Omitting `executionTarget`, or supplying `undefined`, retains the most recent
|
|
138
|
+
explicit selection, initially the factory's selection. When neither supplies a
|
|
139
|
+
target, existing constructor, session-option and provider-default behavior
|
|
140
|
+
remains unchanged. Explicit `null` requests automatic device resolution at the
|
|
141
|
+
new activation boundary. It remains distinct from omission. The selection
|
|
142
|
+
stays available for a later explicit load after unloading or a failed load;
|
|
143
|
+
the SDK creates no preference store and does not change application preferences.
|
|
144
|
+
|
|
145
|
+
The decision owner forwards the target to its existing ONNX owner. That owner
|
|
146
|
+
resolves the physical ID, supported provider and provider-specific address.
|
|
147
|
+
An unavailable or unsupported explicit target reports its actual failure; it
|
|
148
|
+
does not silently select another adapter or a cloud provider. GPU selection
|
|
149
|
+
does not require every graph node to execute on the GPU: supported CPU graph
|
|
150
|
+
partitioning remains an engine concern. See the native ONNX contract for exact
|
|
151
|
+
target routing and automatic-selection behavior.
|
|
152
|
+
|
|
153
|
+
The existing `current().execution` field remains the complete ONNX session load
|
|
154
|
+
record for that activation. Its `execution` member carries `requestedTarget`,
|
|
155
|
+
`resolvedDevice`, `resolution`, `configuredTarget` and `observedTarget`, alongside
|
|
156
|
+
provider-attempt diagnostics. These distinguish requested selection, physical
|
|
157
|
+
resolution, accepted configuration and actual execution evidence. An unknown
|
|
158
|
+
`observedTarget` remains `null`; successful configuration alone does not establish
|
|
159
|
+
which device executed graph nodes.
|
|
160
|
+
|
|
161
|
+
`pendingActivation` is separately `null` or `{executionTarget}` while a load or
|
|
162
|
+
replacement is pending; its `executionTarget` property is omitted at JSON
|
|
163
|
+
transport when the selection is `undefined`. A pending target never overwrites the previous
|
|
164
|
+
activation's `execution` record. The old record remains attributable to the
|
|
165
|
+
retiring activation until its cleanup completes, then the new activation owns
|
|
166
|
+
its own record. A request is not a claim that the requested device is running.
|
|
167
|
+
|
|
168
|
+
Concurrent loads for the same target share their pending activation. A
|
|
169
|
+
different target supersedes the earlier pending request, which rejects with
|
|
170
|
+
cancellation instead of returning another target's ready result. Replacement
|
|
171
|
+
joins this model's actual tokenizer/native cleanup before creating its
|
|
172
|
+
successor. Independent model owners retain their independent workers and
|
|
173
|
+
lifetimes; there is no all-model loading barrier or trial inference.
|
|
120
174
|
|
|
121
175
|
## Complete inputs and outputs
|
|
122
176
|
|
|
@@ -148,14 +202,15 @@ runtime; sharing the encoding implementation alone does not establish it.
|
|
|
148
202
|
## Lifetime and cancellation
|
|
149
203
|
|
|
150
204
|
`current()` returns `{family, model, revision, dtype, state, loaded, busy,
|
|
151
|
-
activeRequests, progress, error, execution}`. States include
|
|
205
|
+
activeRequests, progress, error, execution, pendingActivation}`. States include
|
|
206
|
+
`unloaded`,
|
|
152
207
|
`loading`, `ready`, `unloading`, `disposing`, `disposed` and `error`. Semantic
|
|
153
208
|
progress includes model preparation, tokenization and evaluation; downloaded
|
|
154
209
|
members report files, not byte progress. Complete technical errors belong in
|
|
155
210
|
developer diagnostics, while the application owns its user-facing status.
|
|
156
211
|
|
|
157
|
-
Concurrent callers share one explicit activation.
|
|
158
|
-
evaluation cancels that activation and its outstanding operations, because the
|
|
212
|
+
Concurrent callers selecting the same target share one explicit activation.
|
|
213
|
+
Cancelling a load or evaluation cancels that activation and its outstanding operations, because the
|
|
159
214
|
native session and tokenizer have one owner. Use separately owned models when
|
|
160
215
|
independent cancellation is required. The construction `signal` governs the
|
|
161
216
|
whole model lifetime. `unload()` permits another explicit load after successful
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.1",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"./core/preferences": "./src/core/services/preferences.mjs",
|
|
70
70
|
"./core/diarization": "./src/diarization/service.mjs",
|
|
71
71
|
"./core/local-ai": "./src/core/services/local-ai.mjs",
|
|
72
|
+
"./core/execution-devices": "./src/core/services/execution-devices.mjs",
|
|
72
73
|
"./core/decisions": "./src/core/services/decisions.mjs",
|
|
73
74
|
"./core/image": "./src/core/services/image.mjs",
|
|
74
75
|
"./core/speech": "./src/core/services/speech.mjs",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"./diarization/build": "./src/diarization/build.mjs",
|
|
78
79
|
"./local-ai": "./src/local-ai/install.mjs",
|
|
79
80
|
"./local-ai/onnx": "./src/local-ai/onnx.mjs",
|
|
81
|
+
"./local-ai/execution-devices": "./src/local-ai/execution-devices.mjs",
|
|
80
82
|
"./local-ai/decisions": "./src/local-ai/decisions.mjs",
|
|
81
83
|
"./local-ai/image": "./src/local-ai/image-runtime.mjs",
|
|
82
84
|
"./local-ai/whisper": "./src/local-ai/whisper/index.mjs",
|
|
@@ -123,7 +125,7 @@
|
|
|
123
125
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
124
126
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
125
127
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
126
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
128
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/execution-devices.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
127
129
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
128
130
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/ai-audio-output.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
129
131
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -4,6 +4,10 @@ import {createArcaneEventSource} from '../../event-manager.mjs';
|
|
|
4
4
|
import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
5
5
|
import {encodeTensorMap} from '../../../browser-runtime/ai/onnx-tensors.mjs';
|
|
6
6
|
|
|
7
|
+
function sameExecutionTarget(left, right) {
|
|
8
|
+
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
/** App-owned selection; shared typed-decision execution, without a renderer. */
|
|
8
12
|
export function createNativeDecisionService(configuration = {}, {appRoot = process.cwd()} = {}) {
|
|
9
13
|
const events = createArcaneEventSource({}, {source: 'core-decisions', eventTypes: ['decisions.state']});
|
|
@@ -15,14 +19,22 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
15
19
|
let releasing;
|
|
16
20
|
let closing;
|
|
17
21
|
let unsubscribe;
|
|
22
|
+
let selectedTarget = configuration.executionTarget;
|
|
23
|
+
let loadingTarget;
|
|
24
|
+
let loadedTarget;
|
|
18
25
|
|
|
19
26
|
function current() {
|
|
20
|
-
|
|
27
|
+
const snapshot = model?.current() ?? {
|
|
21
28
|
family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
|
|
22
29
|
revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
|
|
23
30
|
state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
|
|
24
31
|
activeRequests: 0, progress: null, error: null, execution: null
|
|
25
32
|
};
|
|
33
|
+
const pending = Boolean(loading) && (!snapshot.loaded || !sameExecutionTarget(loadedTarget, loadingTarget));
|
|
34
|
+
return {
|
|
35
|
+
...snapshot, busy: snapshot.busy || pending,
|
|
36
|
+
pendingActivation: pending ? {executionTarget: loadingTarget} : snapshot.pendingActivation ?? null
|
|
37
|
+
};
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
function publish(value = current()) {
|
|
@@ -39,20 +51,24 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
39
51
|
|
|
40
52
|
function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
|
|
41
53
|
|
|
42
|
-
function load({signal} = {}) {
|
|
54
|
+
function load({signal, executionTarget: requestedTarget} = {}) {
|
|
43
55
|
operationSignal(signal).throwIfAborted();
|
|
44
56
|
if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
const
|
|
57
|
+
const target = requestedTarget === undefined ? selectedTarget : requestedTarget;
|
|
58
|
+
if (loading && !loadController.signal.aborted && sameExecutionTarget(loadingTarget, target)) return observeLoad(loading, loadController, signal);
|
|
59
|
+
if (!loading && model?.current().loaded && sameExecutionTarget(loadedTarget, target)) return Promise.resolve(current());
|
|
60
|
+
const previousLoad = loading;
|
|
61
|
+
const previousController = loadController;
|
|
62
|
+
const controller = new AbortController();
|
|
49
63
|
const selectedSignal = AbortSignal.any([lifetime.signal, controller.signal]);
|
|
50
64
|
const precedingRelease = releasing;
|
|
51
|
-
|
|
65
|
+
const task = Promise.resolve().then(async function loadSelectedModel() {
|
|
52
66
|
selectedSignal.throwIfAborted();
|
|
53
67
|
if (precedingRelease) await precedingRelease;
|
|
68
|
+
if (previousLoad) await Promise.allSettled([previousLoad]);
|
|
54
69
|
selectedSignal.throwIfAborted();
|
|
55
70
|
if (model) await model.dispose();
|
|
71
|
+
selectedSignal.throwIfAborted();
|
|
56
72
|
unsubscribe?.();
|
|
57
73
|
// Only the explicit load waits for its native dependencies. Merely
|
|
58
74
|
// registering this service does not download a model or delay UI.
|
|
@@ -65,23 +81,43 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
65
81
|
Object.entries(configuration.paths).map(function nativePath([name, value]) { return [name, path.resolve(appRoot, value)]; })
|
|
66
82
|
);
|
|
67
83
|
model = createNativeDecisionModel({
|
|
68
|
-
...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
|
|
84
|
+
...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets, executionTarget: target,
|
|
69
85
|
signal: lifetime.signal
|
|
70
86
|
});
|
|
71
|
-
unsubscribe = model.subscribe(publish);
|
|
72
|
-
|
|
87
|
+
unsubscribe = model.subscribe(function modelStateChanged() { publish(); });
|
|
88
|
+
const result = await model.load({signal: selectedSignal});
|
|
89
|
+
selectedSignal.throwIfAborted();
|
|
90
|
+
loadedTarget = target;
|
|
91
|
+
return result;
|
|
73
92
|
});
|
|
74
|
-
function settled() {
|
|
75
|
-
|
|
93
|
+
function settled() {
|
|
94
|
+
if (loading !== task) return;
|
|
95
|
+
loading = undefined;
|
|
96
|
+
loadController = undefined;
|
|
97
|
+
publish();
|
|
98
|
+
}
|
|
99
|
+
task.then(settled, settled).catch(function reportStateFailure(error) { console.error('Native decision state publication failed.', error); });
|
|
100
|
+
loading = task;
|
|
101
|
+
loadController = controller;
|
|
102
|
+
loadingTarget = target;
|
|
103
|
+
selectedTarget = target;
|
|
104
|
+
previousController?.abort(new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision activation request was replaced.'}));
|
|
76
105
|
publish();
|
|
77
|
-
return observeLoad(
|
|
106
|
+
return observeLoad(task, controller, signal);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
function observeLoad(task, controller, signal) {
|
|
81
110
|
function abort() { controller.abort(signal.reason); }
|
|
82
111
|
signal?.addEventListener('abort', abort, {once: true});
|
|
83
112
|
if (signal?.aborted) abort();
|
|
84
|
-
return task.
|
|
113
|
+
return task.then(function loadedActivation(result) {
|
|
114
|
+
controller.signal.throwIfAborted();
|
|
115
|
+
const snapshot = current();
|
|
116
|
+
if ((loading && loadController !== controller) || !snapshot.loaded || snapshot.execution !== result.execution) {
|
|
117
|
+
throw new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision activation was replaced.'});
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}).finally(function detachLoadCaller() { signal?.removeEventListener('abort', abort); });
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
async function evaluate(rows, {signal, runOptions} = {}) {
|
|
@@ -129,7 +165,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
129
165
|
start(serviceContext) { context = serviceContext; },
|
|
130
166
|
methods: {
|
|
131
167
|
'decisions.status': current,
|
|
132
|
-
'decisions.load': function loadRequest(parameters, request) { return load({signal: request.signal}); },
|
|
168
|
+
'decisions.load': function loadRequest(parameters, request) { return load({executionTarget: parameters?.executionTarget, signal: request.signal}); },
|
|
133
169
|
'decisions.evaluate': async function evaluateRequest({rows, runOptions}, request) {
|
|
134
170
|
const result = await evaluate(rows, {runOptions, signal: request.signal});
|
|
135
171
|
// Typed arrays retain their complete values through Core's
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {createExecutionDeviceCatalog} from '../../local-ai/execution-devices.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Independent of model-service startup. Core gives this service ownership of
|
|
5
|
+
* the shared catalog's disposal; model engines borrowing it do not dispose it.
|
|
6
|
+
*/
|
|
7
|
+
export function createExecutionDeviceService({catalog = createExecutionDeviceCatalog()} = {}) {
|
|
8
|
+
function devices({refresh = false} = {}, {signal} = {}) {
|
|
9
|
+
return catalog.devices(
|
|
10
|
+
{refresh, signal}
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveTarget({executionTarget = null, refresh = false} = {}, {signal} = {}) {
|
|
15
|
+
return catalog.resolveTarget(
|
|
16
|
+
{executionTarget, refresh, signal}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dispose() {
|
|
21
|
+
return catalog.dispose();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: 'execution-devices',
|
|
26
|
+
methods: {
|
|
27
|
+
'localai.devices': devices,
|
|
28
|
+
'localai.resolveTarget': resolveTarget
|
|
29
|
+
},
|
|
30
|
+
dispose
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -121,7 +121,7 @@ function collectCompletion(completion, chunk) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Core owns native processes; applications own runtime and model selection. */
|
|
124
|
-
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare} = {}) {
|
|
124
|
+
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare, executionDevices} = {}) {
|
|
125
125
|
const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
|
|
126
126
|
const lifetime = new AbortController();
|
|
127
127
|
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
@@ -173,7 +173,9 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
173
173
|
|
|
174
174
|
function startONNX() {
|
|
175
175
|
if (!onnxRecord?.modulePath) return;
|
|
176
|
-
onnx = createONNXRuntime(
|
|
176
|
+
onnx = createONNXRuntime(
|
|
177
|
+
{modulePath: onnxRecord.modulePath, signal: lifetimeSignal, onEvent, executionDevices}
|
|
178
|
+
);
|
|
177
179
|
stopONNXSubscription = onnx.subscribe(function onnxChanged() { publish(); });
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -630,9 +632,14 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
630
632
|
return snapshot();
|
|
631
633
|
},
|
|
632
634
|
'onnx.status': function currentONNX() { return onnxStatus(); },
|
|
633
|
-
'onnx.load': function loadONNX({id, model, sessionOptions}, request) {
|
|
634
|
-
return requireONNX().load(
|
|
635
|
-
|
|
635
|
+
'onnx.load': function loadONNX({id, model, sessionOptions, executionPreference, executionTarget}, request) {
|
|
636
|
+
return requireONNX().load(
|
|
637
|
+
{
|
|
638
|
+
id, model: path.resolve(appRoot ?? process.cwd(), model), sessionOptions,
|
|
639
|
+
executionPreference, executionTarget,
|
|
640
|
+
signal: AbortSignal.any([lifetimeSignal, request.signal])
|
|
641
|
+
}
|
|
642
|
+
);
|
|
636
643
|
},
|
|
637
644
|
'onnx.run': async function runONNX({id, feeds, fetches, runOptions}, request) {
|
|
638
645
|
const outputs = await requireONNX().run({id, feeds: decodeTensorMap(feeds),
|
|
@@ -13,6 +13,10 @@ function cancellation(message) {
|
|
|
13
13
|
|
|
14
14
|
function reportCleanupFailure(error) { console.error('Native decision cleanup failed.', error); }
|
|
15
15
|
|
|
16
|
+
function sameExecutionTarget(left, right) {
|
|
17
|
+
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* One explicitly activated Laya FP32 model using an existing native ONNX owner.
|
|
18
22
|
* It does not create another inference runtime or load on construction.
|
|
@@ -20,7 +24,7 @@ function reportCleanupFailure(error) { console.error('Native decision cleanup fa
|
|
|
20
24
|
export function createNativeDecisionModel({
|
|
21
25
|
onnx, modelAssets, workingDirectory, paths,
|
|
22
26
|
model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
|
|
23
|
-
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', signal
|
|
27
|
+
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
|
|
24
28
|
} = {}) {
|
|
25
29
|
if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
|
|
26
30
|
const owner = {};
|
|
@@ -33,14 +37,18 @@ export function createNativeDecisionModel({
|
|
|
33
37
|
let progress = null;
|
|
34
38
|
let lastError = null;
|
|
35
39
|
let execution = null;
|
|
40
|
+
let selectedTarget = executionTarget;
|
|
41
|
+
let pendingActivation;
|
|
36
42
|
|
|
37
43
|
function current() {
|
|
44
|
+
const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
|
|
38
45
|
return {
|
|
39
46
|
family: 'laya', model, revision, dtype, state,
|
|
40
|
-
loaded: state === 'ready', busy: state === 'loading' || Boolean(activation?.jobs.size),
|
|
47
|
+
loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
|
|
41
48
|
activeRequests: activation?.jobs.size ?? 0, progress,
|
|
42
49
|
error: lastError ? serializeCoreError(lastError) : null,
|
|
43
|
-
execution
|
|
50
|
+
execution,
|
|
51
|
+
pendingActivation: pending ? {executionTarget: pending.executionTarget} : null
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -141,8 +149,8 @@ export function createNativeDecisionModel({
|
|
|
141
149
|
return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
function beginActivation() {
|
|
145
|
-
const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false};
|
|
152
|
+
function beginActivation(target) {
|
|
153
|
+
const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false, executionTarget: target};
|
|
146
154
|
activation = entry;
|
|
147
155
|
state = 'loading';
|
|
148
156
|
progress = {phase: 'preparing-model'};
|
|
@@ -161,7 +169,7 @@ export function createNativeDecisionModel({
|
|
|
161
169
|
});
|
|
162
170
|
entry.onnxLoadStarted = true;
|
|
163
171
|
const loaded = onnx.load({
|
|
164
|
-
id: entry.id, model: files.model, sessionOptions, executionPreference,
|
|
172
|
+
id: entry.id, model: files.model, sessionOptions, executionPreference, executionTarget: entry.executionTarget,
|
|
165
173
|
signal: entry.controller.signal
|
|
166
174
|
});
|
|
167
175
|
const results = await Promise.allSettled([loaded, entry.tokenizer.ready]);
|
|
@@ -193,19 +201,76 @@ export function createNativeDecisionModel({
|
|
|
193
201
|
return entry;
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
async function
|
|
197
|
-
assertOpen(operationSignal);
|
|
198
|
-
if (releaseTask && !activation) await releaseTask;
|
|
199
|
-
assertOpen(operationSignal);
|
|
200
|
-
const entry = activation ?? beginActivation();
|
|
204
|
+
async function awaitActivation(entry, operationSignal) {
|
|
201
205
|
const detach = observeCancellation(entry, operationSignal);
|
|
202
206
|
try {
|
|
203
207
|
await entry.loading;
|
|
204
208
|
entry.controller.signal.throwIfAborted();
|
|
209
|
+
if (activation !== entry) throw cancellation('The decision activation was replaced.');
|
|
205
210
|
return current();
|
|
206
211
|
} finally { detach(); }
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
function observeLoad(request, operationSignal) {
|
|
215
|
+
function abort() { request.controller.abort(operationSignal.reason); }
|
|
216
|
+
operationSignal?.addEventListener('abort', abort, {once: true});
|
|
217
|
+
if (operationSignal?.aborted) abort();
|
|
218
|
+
return request.task.then(function loadedActivation() {
|
|
219
|
+
request.controller.signal.throwIfAborted();
|
|
220
|
+
request.entry.controller.signal.throwIfAborted();
|
|
221
|
+
if (activation !== request.entry) throw cancellation('The decision activation was replaced.');
|
|
222
|
+
return current();
|
|
223
|
+
}).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function load({signal: operationSignal, executionTarget: requestedTarget} = {}) {
|
|
227
|
+
assertOpen(operationSignal);
|
|
228
|
+
const target = requestedTarget === undefined ? selectedTarget : requestedTarget;
|
|
229
|
+
if (pendingActivation && !pendingActivation.controller.signal.aborted && sameExecutionTarget(pendingActivation.executionTarget, target)) {
|
|
230
|
+
return observeLoad(pendingActivation, operationSignal);
|
|
231
|
+
}
|
|
232
|
+
if (!pendingActivation && activation && sameExecutionTarget(activation.executionTarget, target)) {
|
|
233
|
+
return awaitActivation(activation, operationSignal);
|
|
234
|
+
}
|
|
235
|
+
const previous = pendingActivation;
|
|
236
|
+
const request = {executionTarget: target, controller: new AbortController()};
|
|
237
|
+
let precedingRelease;
|
|
238
|
+
// Retain the replacement before synchronous abort/state observers run.
|
|
239
|
+
// Only this model's retiring activation must finish before its successor.
|
|
240
|
+
request.task = Promise.resolve().then(async function loadSelectedTarget() {
|
|
241
|
+
request.controller.signal.throwIfAborted();
|
|
242
|
+
if (precedingRelease) await precedingRelease;
|
|
243
|
+
assertOpen(request.controller.signal);
|
|
244
|
+
if (pendingActivation !== request) throw cancellation('The decision activation request was replaced.');
|
|
245
|
+
request.entry ??= activation ?? beginActivation(request.executionTarget);
|
|
246
|
+
return awaitActivation(request.entry, request.controller.signal);
|
|
247
|
+
});
|
|
248
|
+
function settled(error) {
|
|
249
|
+
request.detach?.();
|
|
250
|
+
if (pendingActivation !== request) return;
|
|
251
|
+
pendingActivation = undefined;
|
|
252
|
+
if (!request.entry && !activation && !disposed && state === 'loading') {
|
|
253
|
+
state = request.controller.signal.aborted ? 'unloaded' : 'error';
|
|
254
|
+
progress = null;
|
|
255
|
+
lastError = request.controller.signal.aborted ? null : error;
|
|
256
|
+
}
|
|
257
|
+
publish();
|
|
258
|
+
}
|
|
259
|
+
request.task.then(function loaded() { settled(); }, function loadFailed(error) { settled(error); }).catch(reportCleanupFailure);
|
|
260
|
+
pendingActivation = request;
|
|
261
|
+
selectedTarget = target;
|
|
262
|
+
const reason = cancellation('The decision execution target was replaced.');
|
|
263
|
+
previous?.controller.abort(reason);
|
|
264
|
+
if (activation && !sameExecutionTarget(activation.executionTarget, target)) stop(activation, reason);
|
|
265
|
+
precedingRelease = !activation ? releaseTask : undefined;
|
|
266
|
+
if (!activation && !precedingRelease) {
|
|
267
|
+
request.entry = beginActivation(target);
|
|
268
|
+
request.detach = observeCancellation(request.entry, request.controller.signal);
|
|
269
|
+
}
|
|
270
|
+
publish();
|
|
271
|
+
return observeLoad(request, operationSignal);
|
|
272
|
+
}
|
|
273
|
+
|
|
209
274
|
async function evaluate(rows, {signal: operationSignal, runOptions} = {}) {
|
|
210
275
|
assertOpen(operationSignal);
|
|
211
276
|
const entry = activation;
|
|
@@ -247,21 +312,34 @@ export function createNativeDecisionModel({
|
|
|
247
312
|
}
|
|
248
313
|
|
|
249
314
|
function unload() {
|
|
250
|
-
|
|
251
|
-
|
|
315
|
+
const reason = cancellation('The native decision model was unloaded.');
|
|
316
|
+
const pending = pendingActivation;
|
|
317
|
+
pending?.controller.abort(reason);
|
|
318
|
+
const release = activation ? stop(activation, reason) : releaseTask ?? Promise.resolve(current());
|
|
319
|
+
if (!pending) return release;
|
|
320
|
+
return Promise.allSettled([release, pending.task]).then(function releasedActivation(results) {
|
|
321
|
+
if (results[0].status === 'rejected') throw results[0].reason;
|
|
322
|
+
return current();
|
|
323
|
+
});
|
|
252
324
|
}
|
|
253
325
|
|
|
254
326
|
function dispose() {
|
|
255
327
|
if (disposal) return disposal;
|
|
256
328
|
disposed = true;
|
|
257
329
|
signal?.removeEventListener('abort', lifetimeAborted);
|
|
330
|
+
const pending = pendingActivation;
|
|
331
|
+
const reason = cancellation('The native decision model was disposed.');
|
|
258
332
|
let release;
|
|
259
333
|
disposal = Promise.resolve().then(async function disposeNativeModel() {
|
|
260
|
-
try {
|
|
334
|
+
try {
|
|
335
|
+
const results = await Promise.allSettled([release, pending?.task]);
|
|
336
|
+
if (results[0].status === 'rejected') throw results[0].reason;
|
|
337
|
+
}
|
|
261
338
|
finally { state = 'disposed'; publish(); events.dispose(); }
|
|
262
339
|
return current();
|
|
263
340
|
});
|
|
264
|
-
|
|
341
|
+
pending?.controller.abort(reason);
|
|
342
|
+
release = activation ? stop(activation, reason) : releaseTask;
|
|
265
343
|
if (!activation && !release) { state = 'disposed'; publish(); }
|
|
266
344
|
return disposal;
|
|
267
345
|
}
|
package/src/local-ai/dev.mjs
CHANGED
|
@@ -4,19 +4,23 @@ import {createLocalAIService} from '../core/services/local-ai.mjs';
|
|
|
4
4
|
import {createLocalImageService} from '../core/services/image.mjs';
|
|
5
5
|
import {createModelAssetService} from '../core/services/model-assets.mjs';
|
|
6
6
|
import {createSpeechService} from '../core/services/speech.mjs';
|
|
7
|
+
import {createExecutionDeviceService} from '../core/services/execution-devices.mjs';
|
|
7
8
|
import {normalizeLocalAIConfig} from './config.mjs';
|
|
8
9
|
import {discoverLocalAIRuntimes} from './discover.mjs';
|
|
9
10
|
import {ensureLocalAIRuntimes} from './install.mjs';
|
|
10
11
|
import {createWhisperRuntime} from './whisper/index.mjs';
|
|
12
|
+
import {createExecutionDeviceCatalog} from './execution-devices.mjs';
|
|
11
13
|
|
|
12
14
|
/** Local engines share the ordinary app-service development transport. */
|
|
13
15
|
export function createDevelopmentLocalAI({
|
|
14
16
|
config, appRoot, directory, application, version, signal, onEvent,
|
|
15
17
|
services = [], serviceModules = [], context = {}
|
|
16
18
|
} = {}) {
|
|
19
|
+
const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
|
|
17
20
|
const lifetime = new AbortController();
|
|
18
21
|
signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
19
|
-
|
|
22
|
+
// Discovery starts on demand, independently of model runtime preparation.
|
|
23
|
+
const executionDevices = createExecutionDeviceCatalog({signal});
|
|
20
24
|
const localConfig = {
|
|
21
25
|
...selected,
|
|
22
26
|
runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
|
|
@@ -26,7 +30,7 @@ export function createDevelopmentLocalAI({
|
|
|
26
30
|
const service = createLocalAIService(
|
|
27
31
|
localConfig,
|
|
28
32
|
{
|
|
29
|
-
appRoot, signal, onEvent,
|
|
33
|
+
appRoot, signal, onEvent, executionDevices,
|
|
30
34
|
async prepare({signal, onEvent: report}) {
|
|
31
35
|
const discovered = await discoverLocalAIRuntimes({config: localConfig, appRoot, signal});
|
|
32
36
|
const installed = await ensureLocalAIRuntimes(
|
|
@@ -36,7 +40,7 @@ export function createDevelopmentLocalAI({
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
);
|
|
39
|
-
const definitions = [service, ...services];
|
|
43
|
+
const definitions = [createExecutionDeviceService({catalog: executionDevices}), service, ...services];
|
|
40
44
|
let modelAssets;
|
|
41
45
|
let imageService;
|
|
42
46
|
let speechService;
|