arcane-os 0.79.1 → 0.81.0
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 +43 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
- package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
- package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
- package/docs/reference/ai/browser-speech.md +11 -0
- package/docs/reference/ai/browser-wasm.md +21 -10
- package/docs/reference/core-native-packaging.md +9 -0
- package/docs/reference/inventory/package-api.json +8 -8
- package/docs/reference/model-assets.md +33 -0
- package/docs/reference/native-decisions.md +147 -17
- package/docs/reference/sdk-api.md +22 -6
- package/package.json +1 -1
- package/runtime/arcane/modules/AI.js +10 -5
- package/src/core/hosts/windows/ArcaneHost.cs +21 -5
- package/src/core/services/decisions.mjs +116 -43
- package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
- package/src/local-ai/decision-tokenizer.mjs +2 -2
- package/src/local-ai/decisions.mjs +123 -33
|
@@ -8,32 +8,52 @@ function sameExecutionTarget(left, right) {
|
|
|
8
8
|
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function sameActivation(left, right) {
|
|
12
|
+
if (!left || !right || !sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
|
|
13
|
+
if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
|
|
14
|
+
if (left.resourcePaths === right.resourcePaths) return true;
|
|
15
|
+
if (!left.resourcePaths || !right.resourcePaths) return false;
|
|
16
|
+
const entries = Object.entries(left.resourcePaths);
|
|
17
|
+
return entries.length === Object.keys(right.resourcePaths).length
|
|
18
|
+
&& entries.every(function sameResource([role, member]) { return right.resourcePaths[role] === member; });
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/** App-owned selection; shared typed-decision execution, without a renderer. */
|
|
12
22
|
export function createNativeDecisionService(configuration = {}, {appRoot = process.cwd()} = {}) {
|
|
13
23
|
const events = createArcaneEventSource({}, {source: 'core-decisions', eventTypes: ['decisions.state']});
|
|
14
24
|
const lifetime = new AbortController();
|
|
25
|
+
const loads = new Set();
|
|
26
|
+
const provisionalCleanupFailures = [];
|
|
15
27
|
let context;
|
|
28
|
+
let modelAssetOwner;
|
|
16
29
|
let model;
|
|
17
30
|
let loading;
|
|
18
31
|
let loadController;
|
|
19
32
|
let releasing;
|
|
20
33
|
let closing;
|
|
21
34
|
let unsubscribe;
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
let selectedActivation = {
|
|
36
|
+
family: configuration.family === undefined ? 'laya' : configuration.family,
|
|
37
|
+
model: configuration.model === undefined ? 'onnx-community/laya-typed-decisions-ONNX' : configuration.model,
|
|
38
|
+
revision: configuration.revision === undefined ? 'main' : configuration.revision,
|
|
39
|
+
dtype: configuration.dtype === undefined ? 'fp32' : configuration.dtype,
|
|
40
|
+
executionTarget: configuration.executionTarget,
|
|
41
|
+
...(configuration.assetProjectionId == null ? {} : {assetProjectionId: configuration.assetProjectionId, resourcePaths: configuration.resourcePaths && {...configuration.resourcePaths}})
|
|
42
|
+
};
|
|
43
|
+
let loadingActivation;
|
|
44
|
+
let loadedActivation;
|
|
25
45
|
|
|
26
46
|
function current() {
|
|
27
47
|
const snapshot = model?.current() ?? {
|
|
28
|
-
family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
|
|
48
|
+
family: configuration.family ?? 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
|
|
29
49
|
revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
|
|
30
50
|
state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
|
|
31
51
|
activeRequests: 0, progress: null, error: null, execution: null
|
|
32
52
|
};
|
|
33
|
-
const pending = Boolean(loading) && (!snapshot.loaded || !
|
|
53
|
+
const pending = Boolean(loading) && (!snapshot.loaded || !sameActivation(loadedActivation, loadingActivation));
|
|
34
54
|
return {
|
|
35
55
|
...snapshot, busy: snapshot.busy || pending,
|
|
36
|
-
pendingActivation: pending ? {
|
|
56
|
+
pendingActivation: pending ? {...loadingActivation} : snapshot.pendingActivation ?? null
|
|
37
57
|
};
|
|
38
58
|
}
|
|
39
59
|
|
|
@@ -51,57 +71,100 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
51
71
|
|
|
52
72
|
function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
|
|
53
73
|
|
|
54
|
-
function load({
|
|
74
|
+
function load({
|
|
75
|
+
family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
|
|
76
|
+
signal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
|
|
77
|
+
} = {}) {
|
|
55
78
|
operationSignal(signal).throwIfAborted();
|
|
56
79
|
if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
|
|
81
|
+
const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
|
|
82
|
+
const selection = {
|
|
83
|
+
family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
|
|
84
|
+
model: requestedModel === undefined ? selectedActivation.model : requestedModel,
|
|
85
|
+
revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
|
|
86
|
+
dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
|
|
87
|
+
executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
|
|
88
|
+
...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
|
|
89
|
+
};
|
|
90
|
+
if (loading && !loadController.signal.aborted && sameActivation(loadingActivation, selection)) return observeLoad(loading, loadController, signal);
|
|
91
|
+
if (!loading && model?.current().loaded && sameActivation(loadedActivation, selection)) return Promise.resolve(current());
|
|
60
92
|
const previousLoad = loading;
|
|
61
93
|
const previousController = loadController;
|
|
62
94
|
const controller = new AbortController();
|
|
63
95
|
const selectedSignal = AbortSignal.any([lifetime.signal, controller.signal]);
|
|
64
96
|
const precedingRelease = releasing;
|
|
97
|
+
let retained;
|
|
98
|
+
let retainError;
|
|
65
99
|
const task = Promise.resolve().then(async function loadSelectedModel() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
const replaced = new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision activation request was replaced.'});
|
|
101
|
+
try {
|
|
102
|
+
if (retainError) throw retainError;
|
|
103
|
+
selectedSignal.throwIfAborted();
|
|
104
|
+
if (loading !== task) throw replaced;
|
|
105
|
+
// Only explicit load waits for these owners. A provisional use
|
|
106
|
+
// protects incoming files while the preceding model retires.
|
|
107
|
+
const [localAI, modelAssets] = await Promise.all([
|
|
108
|
+
context.getService('local-ai'),
|
|
109
|
+
configuration.paths && selection.assetProjectionId === undefined ? undefined : context.getService('model-assets')
|
|
110
|
+
]);
|
|
111
|
+
selectedSignal.throwIfAborted();
|
|
112
|
+
if (loading !== task) throw replaced;
|
|
113
|
+
if (modelAssets) modelAssetOwner = modelAssets;
|
|
114
|
+
if (selection.assetProjectionId !== undefined && !retained) retained = modelAssets.retain(selection.assetProjectionId);
|
|
115
|
+
selectedSignal.throwIfAborted();
|
|
116
|
+
if (loading !== task) throw replaced;
|
|
117
|
+
previousController?.abort(replaced);
|
|
118
|
+
if (precedingRelease) await precedingRelease;
|
|
119
|
+
if (previousLoad) await Promise.allSettled([previousLoad]);
|
|
120
|
+
selectedSignal.throwIfAborted();
|
|
121
|
+
if (loading !== task) throw replaced;
|
|
122
|
+
if (model) await model.dispose();
|
|
123
|
+
selectedSignal.throwIfAborted();
|
|
124
|
+
if (loading !== task) throw replaced;
|
|
125
|
+
unsubscribe?.();
|
|
126
|
+
const paths = configuration.paths && Object.fromEntries(
|
|
127
|
+
Object.entries(configuration.paths).map(function nativePath([name, value]) { return [name, path.resolve(appRoot, value)]; })
|
|
128
|
+
);
|
|
129
|
+
model = createNativeDecisionModel({
|
|
130
|
+
...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
|
|
131
|
+
family: selection.family, model: selection.model, revision: selection.revision, dtype: selection.dtype,
|
|
132
|
+
executionTarget: selection.executionTarget, assetProjectionId: selection.assetProjectionId,
|
|
133
|
+
resourcePaths: selection.resourcePaths, signal: lifetime.signal
|
|
134
|
+
});
|
|
135
|
+
unsubscribe = model.subscribe(function modelStateChanged() { publish(); });
|
|
136
|
+
const result = await model.load({signal: selectedSignal});
|
|
137
|
+
selectedSignal.throwIfAborted();
|
|
138
|
+
loadedActivation = selection;
|
|
139
|
+
return result;
|
|
140
|
+
} finally {
|
|
141
|
+
previousController?.abort(replaced);
|
|
142
|
+
try { await retained?.release(); }
|
|
143
|
+
catch (error) {
|
|
144
|
+
provisionalCleanupFailures.push(error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
92
148
|
});
|
|
93
149
|
function settled() {
|
|
150
|
+
loads.delete(task);
|
|
94
151
|
if (loading !== task) return;
|
|
95
152
|
loading = undefined;
|
|
96
153
|
loadController = undefined;
|
|
97
154
|
publish();
|
|
98
155
|
}
|
|
99
156
|
task.then(settled, settled).catch(function reportStateFailure(error) { console.error('Native decision state publication failed.', error); });
|
|
157
|
+
loads.add(task);
|
|
100
158
|
loading = task;
|
|
101
159
|
loadController = controller;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
loadingActivation = selection;
|
|
161
|
+
selectedActivation = selection;
|
|
162
|
+
// A pending predecessor may hold the final native use while its workers
|
|
163
|
+
// exit. Take our use before making that predecessor's cleanup runnable.
|
|
164
|
+
if (selection.assetProjectionId !== undefined && modelAssetOwner) {
|
|
165
|
+
try { retained = modelAssetOwner.retain(selection.assetProjectionId); }
|
|
166
|
+
catch (error) { retainError = error; }
|
|
167
|
+
}
|
|
105
168
|
publish();
|
|
106
169
|
return observeLoad(task, controller, signal);
|
|
107
170
|
}
|
|
@@ -133,9 +196,12 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
133
196
|
// rather than wait for it to download and activate first.
|
|
134
197
|
loadController?.abort(new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision load was cancelled.'}));
|
|
135
198
|
const task = model?.unload() ?? Promise.resolve();
|
|
136
|
-
const
|
|
199
|
+
const acceptedLoads = [...loads];
|
|
137
200
|
releasing = Promise.resolve().then(async function releaseSelectedModel() {
|
|
138
|
-
const results = await Promise.allSettled([task,
|
|
201
|
+
const results = await Promise.allSettled([task, ...acceptedLoads]);
|
|
202
|
+
if (provisionalCleanupFailures.length) throw new AggregateError([
|
|
203
|
+
...(results[0].status === 'rejected' ? [results[0].reason] : []), ...provisionalCleanupFailures
|
|
204
|
+
], 'Releasing native decision resources failed.');
|
|
139
205
|
if (results[0].status === 'rejected') throw results[0].reason;
|
|
140
206
|
return current();
|
|
141
207
|
});
|
|
@@ -149,7 +215,8 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
149
215
|
const reason = new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision service is closing.'});
|
|
150
216
|
closing = Promise.resolve().then(async function closeDecisionService() {
|
|
151
217
|
const failures = [];
|
|
152
|
-
|
|
218
|
+
await Promise.allSettled([...loads]);
|
|
219
|
+
failures.push(...provisionalCleanupFailures);
|
|
153
220
|
try { await model?.dispose(); } catch (error) { failures.push(error); }
|
|
154
221
|
unsubscribe?.();
|
|
155
222
|
publish();
|
|
@@ -165,7 +232,13 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
165
232
|
start(serviceContext) { context = serviceContext; },
|
|
166
233
|
methods: {
|
|
167
234
|
'decisions.status': current,
|
|
168
|
-
'decisions.load': function loadRequest(parameters, request) {
|
|
235
|
+
'decisions.load': function loadRequest(parameters, request) {
|
|
236
|
+
return load({
|
|
237
|
+
family: parameters?.family, model: parameters?.model, revision: parameters?.revision, dtype: parameters?.dtype,
|
|
238
|
+
executionTarget: parameters?.executionTarget, assetProjectionId: parameters?.assetProjectionId,
|
|
239
|
+
resourcePaths: parameters?.resourcePaths, signal: request.signal
|
|
240
|
+
});
|
|
241
|
+
},
|
|
169
242
|
'decisions.evaluate': async function evaluateRequest({rows, runOptions}, request) {
|
|
170
243
|
const result = await evaluate(rows, {runOptions, signal: request.signal});
|
|
171
244
|
// Typed arrays retain their complete values through Core's
|
|
@@ -17,7 +17,7 @@ try {
|
|
|
17
17
|
};
|
|
18
18
|
parentPort.on('message', function encodeRequest({id, rows}) {
|
|
19
19
|
try {
|
|
20
|
-
const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows,
|
|
20
|
+
const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, workerData.family);
|
|
21
21
|
const inputs = createDecisionInputs(encoded, pad);
|
|
22
22
|
parentPort.postMessage({id, result: inputs});
|
|
23
23
|
} catch (error) {
|
|
@@ -6,7 +6,7 @@ function cancelled() {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/** One tokenizer worker owned by one native model activation. */
|
|
9
|
-
export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, signal, onError}) {
|
|
9
|
+
export function createDecisionTokenizer({family = 'laya', tokenizerPath, tokenizerConfigPath, signal, onError}) {
|
|
10
10
|
signal?.throwIfAborted();
|
|
11
11
|
const pending = new Map();
|
|
12
12
|
let nextId = 0;
|
|
@@ -14,7 +14,7 @@ export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, sig
|
|
|
14
14
|
let terminalError;
|
|
15
15
|
let exited = false;
|
|
16
16
|
const worker = new Worker(new URL('./decision-tokenizer-worker.mjs', import.meta.url), {
|
|
17
|
-
workerData: {tokenizerPath, tokenizerConfigPath}
|
|
17
|
+
workerData: {family, tokenizerPath, tokenizerConfigPath}
|
|
18
18
|
});
|
|
19
19
|
let resolveExit;
|
|
20
20
|
const exit = new Promise(function tokenizerExit(resolve) { resolveExit = resolve; });
|
|
@@ -5,7 +5,13 @@ import {decodeDecisionOutputs} from '../../browser-runtime/ai/decision-runtime.m
|
|
|
5
5
|
import {createDecisionTokenizer} from './decision-tokenizer.mjs';
|
|
6
6
|
|
|
7
7
|
const STATE_EVENT = 'ai.decisions.state';
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
function selectedGraph(family, dtype) {
|
|
10
|
+
if (family === 'laya' && dtype === 'fp32') return 'onnx/model.onnx';
|
|
11
|
+
if (family === 'laya' && dtype === 'fp16') return 'onnx/model_fp16.onnx';
|
|
12
|
+
if (family === 'julia' && dtype === 'fp32') return 'model.onnx';
|
|
13
|
+
throw new TypeError('Native decisions support Laya fp32/fp16 and Julia fp32 graph selections.');
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
function cancellation(message) {
|
|
11
17
|
return new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message});
|
|
@@ -17,18 +23,30 @@ function sameExecutionTarget(left, right) {
|
|
|
17
23
|
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function sameActivation(left, right) {
|
|
27
|
+
if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
|
|
28
|
+
if (!sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
|
|
29
|
+
if (left.resourcePaths === right.resourcePaths) return true;
|
|
30
|
+
if (!left.resourcePaths || !right.resourcePaths) return false;
|
|
31
|
+
const entries = Object.entries(left.resourcePaths);
|
|
32
|
+
return entries.length === Object.keys(right.resourcePaths).length
|
|
33
|
+
&& entries.every(function sameResource([role, member]) { return right.resourcePaths[role] === member; });
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
/**
|
|
21
|
-
* One explicitly activated
|
|
37
|
+
* One explicitly activated typed-decision model using an existing native ONNX owner.
|
|
22
38
|
* It does not create another inference runtime or load on construction.
|
|
23
39
|
*/
|
|
24
40
|
export function createNativeDecisionModel({
|
|
25
|
-
onnx, modelAssets, workingDirectory, paths,
|
|
26
|
-
model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
|
|
41
|
+
onnx, modelAssets, workingDirectory, paths, assetProjectionId, resourcePaths,
|
|
42
|
+
family = 'laya', model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
|
|
27
43
|
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
|
|
28
44
|
} = {}) {
|
|
29
|
-
|
|
45
|
+
selectedGraph(family, dtype);
|
|
30
46
|
const owner = {};
|
|
31
47
|
const events = createArcaneEventSource(owner, {source: 'native-decisions', eventTypes: [STATE_EVENT]});
|
|
48
|
+
const loads = new Set();
|
|
49
|
+
const provisionalCleanupFailures = [];
|
|
32
50
|
let activation;
|
|
33
51
|
let releaseTask;
|
|
34
52
|
let disposal;
|
|
@@ -37,18 +55,23 @@ export function createNativeDecisionModel({
|
|
|
37
55
|
let progress = null;
|
|
38
56
|
let lastError = null;
|
|
39
57
|
let execution = null;
|
|
40
|
-
let
|
|
58
|
+
let selectedActivation = {
|
|
59
|
+
family, model, revision, dtype, executionTarget,
|
|
60
|
+
...(assetProjectionId == null ? {} : {assetProjectionId, resourcePaths: resourcePaths && {...resourcePaths}})
|
|
61
|
+
};
|
|
62
|
+
let activeSelection = selectedActivation;
|
|
41
63
|
let pendingActivation;
|
|
42
64
|
|
|
43
65
|
function current() {
|
|
44
66
|
const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
|
|
45
67
|
return {
|
|
46
|
-
family:
|
|
68
|
+
family: activeSelection.family, model: activeSelection.model,
|
|
69
|
+
revision: activeSelection.revision, dtype: activeSelection.dtype, state,
|
|
47
70
|
loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
|
|
48
71
|
activeRequests: activation?.jobs.size ?? 0, progress,
|
|
49
72
|
error: lastError ? serializeCoreError(lastError) : null,
|
|
50
73
|
execution,
|
|
51
|
-
pendingActivation: pending ? {
|
|
74
|
+
pendingActivation: pending ? {...pending.selection} : null
|
|
52
75
|
};
|
|
53
76
|
}
|
|
54
77
|
|
|
@@ -131,12 +154,32 @@ export function createNativeDecisionModel({
|
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
async function prepareFiles(entry) {
|
|
157
|
+
if (entry.selection.assetProjectionId !== undefined) {
|
|
158
|
+
const mapping = entry.selection.resourcePaths;
|
|
159
|
+
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) {
|
|
160
|
+
throw new TypeError('Projected decisions require resourcePaths for model, tokenizer and tokenizerConfig.');
|
|
161
|
+
}
|
|
162
|
+
const files = {};
|
|
163
|
+
for (const role of ['model', 'tokenizer', 'tokenizerConfig']) {
|
|
164
|
+
const member = entry.retained.members.find(function selectedMember(value) { return value.path === mapping[role]; });
|
|
165
|
+
if (!member) throw new CoreError({
|
|
166
|
+
code: 'ARCANE_DECISION_RESOURCE_UNAVAILABLE',
|
|
167
|
+
message: `The retained decision projection has no member ${String(mapping[role])} for ${role}.`
|
|
168
|
+
});
|
|
169
|
+
files[role] = member.nativePath;
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
134
173
|
if (paths) return paths;
|
|
135
174
|
if (!modelAssets) throw new CoreError({code: 'ARCANE_DECISION_ASSETS_UNAVAILABLE', message: 'Native decisions need prepared paths or the Core model-assets owner.'});
|
|
136
|
-
const
|
|
175
|
+
const {family: selectedFamily, model: selectedModel, revision: selectedRevision, dtype: selectedDtype} = entry.selection;
|
|
176
|
+
const graph = selectedGraph(selectedFamily, selectedDtype);
|
|
177
|
+
const companion = selectedFamily === 'laya' ? `${graph}_data` : `${graph}.data`;
|
|
178
|
+
const members = [graph, companion, 'tokenizer.json', 'tokenizer_config.json'];
|
|
179
|
+
const base = `https://huggingface.co/${selectedModel}/resolve/${encodeURIComponent(selectedRevision)}/`;
|
|
137
180
|
const projection = await modelAssets.prepare({
|
|
138
181
|
id: entry.id, workingDirectory,
|
|
139
|
-
members:
|
|
182
|
+
members: members.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
|
|
140
183
|
signal: entry.controller.signal,
|
|
141
184
|
onProgress(value) { updateProgress(entry, value); }
|
|
142
185
|
});
|
|
@@ -146,12 +189,18 @@ export function createNativeDecisionModel({
|
|
|
146
189
|
await modelAssets.release(entry.id);
|
|
147
190
|
entry.prepared = false;
|
|
148
191
|
const files = Object.fromEntries(projection.members.map(function memberFile(member) { return [member.path, member.nativePath]; }));
|
|
149
|
-
return {model: files[
|
|
192
|
+
return {model: files[graph], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
|
|
150
193
|
}
|
|
151
194
|
|
|
152
|
-
function beginActivation(
|
|
153
|
-
const entry = {
|
|
195
|
+
function beginActivation(request) {
|
|
196
|
+
const entry = {
|
|
197
|
+
id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false,
|
|
198
|
+
selection: request.selection, executionTarget: request.selection.executionTarget, retained: request.retained
|
|
199
|
+
};
|
|
200
|
+
request.retained = undefined;
|
|
201
|
+
request.entry = entry;
|
|
154
202
|
activation = entry;
|
|
203
|
+
activeSelection = entry.selection;
|
|
155
204
|
state = 'loading';
|
|
156
205
|
progress = {phase: 'preparing-model'};
|
|
157
206
|
lastError = null;
|
|
@@ -161,6 +210,7 @@ export function createNativeDecisionModel({
|
|
|
161
210
|
entry.controller.signal.throwIfAborted();
|
|
162
211
|
updateProgress(entry, {phase: 'loading-model'});
|
|
163
212
|
entry.tokenizer = createDecisionTokenizer({
|
|
213
|
+
family: entry.selection.family,
|
|
164
214
|
tokenizerPath: files.tokenizer, tokenizerConfigPath: files.tokenizerConfig,
|
|
165
215
|
signal: entry.controller.signal,
|
|
166
216
|
onError(error) {
|
|
@@ -223,33 +273,57 @@ export function createNativeDecisionModel({
|
|
|
223
273
|
}).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
|
|
224
274
|
}
|
|
225
275
|
|
|
226
|
-
async function load({
|
|
276
|
+
async function load({
|
|
277
|
+
family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
|
|
278
|
+
signal: operationSignal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
|
|
279
|
+
} = {}) {
|
|
227
280
|
assertOpen(operationSignal);
|
|
228
|
-
const
|
|
229
|
-
|
|
281
|
+
const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
|
|
282
|
+
const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
|
|
283
|
+
const selection = {
|
|
284
|
+
family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
|
|
285
|
+
model: requestedModel === undefined ? selectedActivation.model : requestedModel,
|
|
286
|
+
revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
|
|
287
|
+
dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
|
|
288
|
+
executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
|
|
289
|
+
...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
|
|
290
|
+
};
|
|
291
|
+
selectedGraph(selection.family, selection.dtype);
|
|
292
|
+
if (pendingActivation && !pendingActivation.controller.signal.aborted && sameActivation(pendingActivation.selection, selection)) {
|
|
230
293
|
return observeLoad(pendingActivation, operationSignal);
|
|
231
294
|
}
|
|
232
|
-
if (!pendingActivation && activation &&
|
|
295
|
+
if (!pendingActivation && activation && sameActivation(activation.selection, selection)) {
|
|
233
296
|
return awaitActivation(activation, operationSignal);
|
|
234
297
|
}
|
|
235
298
|
const previous = pendingActivation;
|
|
236
|
-
const request = {
|
|
299
|
+
const request = {selection, controller: new AbortController()};
|
|
237
300
|
let precedingRelease;
|
|
238
301
|
// Retain the replacement before synchronous abort/state observers run.
|
|
239
302
|
// Only this model's retiring activation must finish before its successor.
|
|
240
303
|
request.task = Promise.resolve().then(async function loadSelectedTarget() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
304
|
+
try {
|
|
305
|
+
if (request.error) throw request.error;
|
|
306
|
+
request.controller.signal.throwIfAborted();
|
|
307
|
+
if (precedingRelease) await precedingRelease;
|
|
308
|
+
assertOpen(request.controller.signal);
|
|
309
|
+
if (pendingActivation !== request) throw cancellation('The decision activation request was replaced.');
|
|
310
|
+
request.entry ??= activation ?? beginActivation(request);
|
|
311
|
+
return await awaitActivation(request.entry, request.controller.signal);
|
|
312
|
+
} finally {
|
|
313
|
+
// A superseded request may own files without ever starting workers.
|
|
314
|
+
try { await request.retained?.release(); }
|
|
315
|
+
catch (error) {
|
|
316
|
+
provisionalCleanupFailures.push(error);
|
|
317
|
+
throw error;
|
|
318
|
+
} finally { request.retained = undefined; }
|
|
319
|
+
}
|
|
247
320
|
});
|
|
248
321
|
function settled(error) {
|
|
322
|
+
loads.delete(request.task);
|
|
249
323
|
request.detach?.();
|
|
250
324
|
if (pendingActivation !== request) return;
|
|
251
325
|
pendingActivation = undefined;
|
|
252
|
-
if (!request.entry && !activation && !disposed && state === 'loading') {
|
|
326
|
+
if (!request.entry && !activation && !disposed && (state === 'loading' || request.error)) {
|
|
253
327
|
state = request.controller.signal.aborted ? 'unloaded' : 'error';
|
|
254
328
|
progress = null;
|
|
255
329
|
lastError = request.controller.signal.aborted ? null : error;
|
|
@@ -257,14 +331,24 @@ export function createNativeDecisionModel({
|
|
|
257
331
|
publish();
|
|
258
332
|
}
|
|
259
333
|
request.task.then(function loaded() { settled(); }, function loadFailed(error) { settled(error); }).catch(reportCleanupFailure);
|
|
334
|
+
loads.add(request.task);
|
|
260
335
|
pendingActivation = request;
|
|
261
|
-
|
|
262
|
-
|
|
336
|
+
selectedActivation = selection;
|
|
337
|
+
try {
|
|
338
|
+
if (selection.assetProjectionId !== undefined) {
|
|
339
|
+
if (!modelAssets) throw new CoreError({code: 'ARCANE_DECISION_ASSETS_UNAVAILABLE', message: 'Projected decisions require the Core model-assets owner.'});
|
|
340
|
+
// retain is synchronous at the model-assets public boundary.
|
|
341
|
+
// Acquire before retiring the last native use of these files.
|
|
342
|
+
request.retained = modelAssets.retain(selection.assetProjectionId);
|
|
343
|
+
}
|
|
344
|
+
} catch (error) { request.error = error; }
|
|
345
|
+
const reason = cancellation('The decision activation was replaced.');
|
|
346
|
+
if (pendingActivation !== request) request.controller.abort(reason);
|
|
263
347
|
previous?.controller.abort(reason);
|
|
264
|
-
if (activation && !
|
|
348
|
+
if (!request.error && !request.controller.signal.aborted && activation && !sameActivation(activation.selection, selection)) stop(activation, reason);
|
|
265
349
|
precedingRelease = !activation ? releaseTask : undefined;
|
|
266
|
-
if (!activation && !precedingRelease) {
|
|
267
|
-
request.entry = beginActivation(
|
|
350
|
+
if (!request.error && !request.controller.signal.aborted && !activation && !precedingRelease) {
|
|
351
|
+
request.entry = beginActivation(request);
|
|
268
352
|
request.detach = observeCancellation(request.entry, request.controller.signal);
|
|
269
353
|
}
|
|
270
354
|
publish();
|
|
@@ -316,8 +400,11 @@ export function createNativeDecisionModel({
|
|
|
316
400
|
const pending = pendingActivation;
|
|
317
401
|
pending?.controller.abort(reason);
|
|
318
402
|
const release = activation ? stop(activation, reason) : releaseTask ?? Promise.resolve(current());
|
|
319
|
-
if (!
|
|
320
|
-
return Promise.allSettled([release,
|
|
403
|
+
if (!loads.size && !provisionalCleanupFailures.length) return release;
|
|
404
|
+
return Promise.allSettled([release, ...loads]).then(function releasedActivation(results) {
|
|
405
|
+
if (provisionalCleanupFailures.length) throw new AggregateError([
|
|
406
|
+
...(results[0].status === 'rejected' ? [results[0].reason] : []), ...provisionalCleanupFailures
|
|
407
|
+
], 'Releasing native decision resources failed.');
|
|
321
408
|
if (results[0].status === 'rejected') throw results[0].reason;
|
|
322
409
|
return current();
|
|
323
410
|
});
|
|
@@ -332,7 +419,10 @@ export function createNativeDecisionModel({
|
|
|
332
419
|
let release;
|
|
333
420
|
disposal = Promise.resolve().then(async function disposeNativeModel() {
|
|
334
421
|
try {
|
|
335
|
-
const results = await Promise.allSettled([release,
|
|
422
|
+
const results = await Promise.allSettled([release, ...loads]);
|
|
423
|
+
if (provisionalCleanupFailures.length) throw new AggregateError([
|
|
424
|
+
...(results[0].status === 'rejected' ? [results[0].reason] : []), ...provisionalCleanupFailures
|
|
425
|
+
], 'Releasing native decision resources failed.');
|
|
336
426
|
if (results[0].status === 'rejected') throw results[0].reason;
|
|
337
427
|
}
|
|
338
428
|
finally { state = 'disposed'; publish(); events.dispose(); }
|