arcane-os 0.78.0 → 0.79.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 +21 -0
- package/README.md +6 -1
- package/browser-runtime/ai/core-onnx.mjs +6 -2
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/publishing.md +20 -3
- 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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {inspect} from 'node:util';
|
|
4
|
+
import Is from 'strong-type';
|
|
5
|
+
import {ArcaneError, ERROR_CODES} from '../errors.mjs';
|
|
6
|
+
import {runProcess} from '../process.mjs';
|
|
7
|
+
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One lazy physical-device inventory per host lifetime. Concurrent callers share
|
|
12
|
+
* discovery; refresh explicitly replaces the cached observation. Returned records
|
|
13
|
+
* are ordinary independent snapshots, not engine-placement or affinity claims.
|
|
14
|
+
* Cancellation rejects with AbortError/ARCANE_CANCELLED. The last departing
|
|
15
|
+
* caller stops discovery; dispose waits for the owned process to finish stopping.
|
|
16
|
+
*/
|
|
17
|
+
export function createExecutionDeviceCatalog({signal} = {}) {
|
|
18
|
+
requireSignal(signal);
|
|
19
|
+
const lifetime = new AbortController();
|
|
20
|
+
const waiting = new Set();
|
|
21
|
+
let cached = null;
|
|
22
|
+
let active = null;
|
|
23
|
+
let closing = null;
|
|
24
|
+
|
|
25
|
+
function requireActive(requestSignal) {
|
|
26
|
+
requireSignal(requestSignal);
|
|
27
|
+
if (requestSignal?.aborted) throw cancellation(requestSignal.reason);
|
|
28
|
+
if (lifetime.signal.aborted) throw cancellation(lifetime.signal.reason);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startDiscovery() {
|
|
32
|
+
const operation = {
|
|
33
|
+
controller: new AbortController(),
|
|
34
|
+
callers: 0,
|
|
35
|
+
finished: false,
|
|
36
|
+
task: null
|
|
37
|
+
};
|
|
38
|
+
active = operation;
|
|
39
|
+
operation.task = discoverDevices(operation.controller.signal).then(
|
|
40
|
+
function discoveryCompleted(value) {
|
|
41
|
+
operation.finished = true;
|
|
42
|
+
if (active === operation) active = null;
|
|
43
|
+
if (operation.controller.signal.aborted) {
|
|
44
|
+
return {error: cancellation(operation.controller.signal.reason)};
|
|
45
|
+
}
|
|
46
|
+
cached = value;
|
|
47
|
+
return {value};
|
|
48
|
+
},
|
|
49
|
+
function discoveryFailed(error) {
|
|
50
|
+
operation.finished = true;
|
|
51
|
+
if (active === operation) active = null;
|
|
52
|
+
return {error};
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
return operation;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function waitForDiscovery(operation, requestSignal, ownsQuery = true) {
|
|
59
|
+
return new Promise(
|
|
60
|
+
function waitForOwnedDiscovery(resolve, reject) {
|
|
61
|
+
let settled = false;
|
|
62
|
+
if (ownsQuery) operation.callers += 1;
|
|
63
|
+
|
|
64
|
+
function releaseCaller() {
|
|
65
|
+
requestSignal?.removeEventListener('abort', cancelCaller);
|
|
66
|
+
waiting.delete(cancelCaller);
|
|
67
|
+
if (ownsQuery) operation.callers -= 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cancelCaller() {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
const reason = requestSignal?.aborted
|
|
74
|
+
? requestSignal.reason
|
|
75
|
+
: lifetime.signal.reason;
|
|
76
|
+
const error = cancellation(reason);
|
|
77
|
+
releaseCaller();
|
|
78
|
+
if (ownsQuery && operation.callers === 0 && !operation.finished) {
|
|
79
|
+
operation.controller.abort(error);
|
|
80
|
+
}
|
|
81
|
+
reject(error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
waiting.add(cancelCaller);
|
|
85
|
+
requestSignal?.addEventListener(
|
|
86
|
+
'abort', cancelCaller,
|
|
87
|
+
{once: true}
|
|
88
|
+
);
|
|
89
|
+
operation.task.then(
|
|
90
|
+
function deliverDiscovery(outcome) {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
if (requestSignal?.aborted || lifetime.signal.aborted) {
|
|
93
|
+
cancelCaller();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
settled = true;
|
|
97
|
+
releaseCaller();
|
|
98
|
+
// A replacement query waits only for the cancelled process
|
|
99
|
+
// to retire; its old cancellation is not the new request's.
|
|
100
|
+
if (ownsQuery && outcome.error) reject(outcome.error);
|
|
101
|
+
else resolve(outcome.value);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
if (requestSignal?.aborted || lifetime.signal.aborted) cancelCaller();
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function devices({refresh = false, signal: requestSignal} = {}) {
|
|
110
|
+
requireActive(requestSignal);
|
|
111
|
+
if (refresh) cached = null;
|
|
112
|
+
while (active?.controller.signal.aborted) {
|
|
113
|
+
await waitForDiscovery(active, requestSignal, false);
|
|
114
|
+
requireActive(requestSignal);
|
|
115
|
+
}
|
|
116
|
+
if (cached && !refresh) return structuredClone(cached);
|
|
117
|
+
const operation = active ?? startDiscovery();
|
|
118
|
+
const inventory = await waitForDiscovery(operation, requestSignal);
|
|
119
|
+
requireActive(requestSignal);
|
|
120
|
+
return structuredClone(inventory);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function resolveTarget({executionTarget = null, refresh = false, signal: requestSignal} = {}) {
|
|
124
|
+
requireActive(requestSignal);
|
|
125
|
+
if (executionTarget === null) {
|
|
126
|
+
return {
|
|
127
|
+
requestedTarget: null,
|
|
128
|
+
resolvedDevice: null,
|
|
129
|
+
resolution: 'automatic',
|
|
130
|
+
reason: 'engine-default-required'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (!is.string(executionTarget?.deviceId) || executionTarget.deviceId === '') {
|
|
134
|
+
throw new TypeError('An execution target requires a deviceId or an explicit null automatic selection.');
|
|
135
|
+
}
|
|
136
|
+
const requestedTarget = {deviceId: executionTarget.deviceId};
|
|
137
|
+
const inventory = await devices(
|
|
138
|
+
{refresh, signal: requestSignal}
|
|
139
|
+
);
|
|
140
|
+
requireActive(requestSignal);
|
|
141
|
+
const device = inventory.devices.find(
|
|
142
|
+
function matchesRequestedDevice(candidate) {
|
|
143
|
+
return candidate.present && candidate.deviceId === requestedTarget.deviceId;
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
if (device) {
|
|
147
|
+
return {requestedTarget, resolvedDevice: device, resolution: 'matched', reason: null};
|
|
148
|
+
}
|
|
149
|
+
const cpuRequested = requestedTarget.deviceId === 'cpu';
|
|
150
|
+
const unresolvedIdentity = !cpuRequested && inventory.devices.some(
|
|
151
|
+
function hasUnresolvedIdentity(candidate) {
|
|
152
|
+
return candidate.present && candidate.deviceId === null;
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
const incompleteInventory = inventory.state === 'unavailable' || inventory.issues.some(
|
|
156
|
+
function enumerationUnavailable(issue) {
|
|
157
|
+
const relevant = cpuRequested
|
|
158
|
+
? issue.source === 'cpu'
|
|
159
|
+
: issue.source === 'gpu' || issue.source === 'npu';
|
|
160
|
+
return relevant && (issue.code === 'enumeration-unavailable' || issue.code === 'enumeration-failed');
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
const reason = incompleteInventory
|
|
164
|
+
? 'inventory-unavailable'
|
|
165
|
+
: unresolvedIdentity ? 'identity-unresolved' : 'device-not-present';
|
|
166
|
+
return {
|
|
167
|
+
requestedTarget,
|
|
168
|
+
resolvedDevice: null,
|
|
169
|
+
resolution: reason === 'device-not-present' ? 'unavailable' : 'unsupported',
|
|
170
|
+
reason
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function dispose() {
|
|
175
|
+
if (closing) return closing;
|
|
176
|
+
const operation = active;
|
|
177
|
+
signal?.removeEventListener('abort', cancelLifetime);
|
|
178
|
+
cached = null;
|
|
179
|
+
lifetime.abort(signal?.aborted ? signal.reason : cancellation('Execution-device catalog disposed.'));
|
|
180
|
+
for (const cancelCaller of waiting) cancelCaller();
|
|
181
|
+
operation?.controller.abort(lifetime.signal.reason);
|
|
182
|
+
closing = operation
|
|
183
|
+
? operation.task.then(
|
|
184
|
+
function discoveryDisposed(outcome) {
|
|
185
|
+
if (outcome.error && outcome.error.code !== ERROR_CODES.cancelled) throw outcome.error;
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
: Promise.resolve();
|
|
189
|
+
return closing;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cancelLifetime() {
|
|
193
|
+
// The retained closing promise owns the process drain even when a parent
|
|
194
|
+
// aborts without immediately awaiting dispose(). Discovery settles it.
|
|
195
|
+
dispose();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
signal?.addEventListener(
|
|
199
|
+
'abort', cancelLifetime,
|
|
200
|
+
{once: true}
|
|
201
|
+
);
|
|
202
|
+
if (signal?.aborted) cancelLifetime();
|
|
203
|
+
return {devices, resolveTarget, dispose};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function discoverDevices(signal) {
|
|
207
|
+
if (signal.aborted) throw cancellation(signal.reason);
|
|
208
|
+
if (process.platform !== 'win32') {
|
|
209
|
+
return unavailableInventory(
|
|
210
|
+
'platform-unavailable',
|
|
211
|
+
`Physical execution-device inventory is unavailable on ${process.platform}.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
const script = await readFile(
|
|
217
|
+
new URL('./execution-devices-windows.ps1', import.meta.url),
|
|
218
|
+
{encoding: 'utf8', signal}
|
|
219
|
+
);
|
|
220
|
+
if (signal.aborted) throw cancellation(signal.reason);
|
|
221
|
+
const command = process.env.SystemRoot
|
|
222
|
+
? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
223
|
+
: 'powershell.exe';
|
|
224
|
+
async function* discoveryInput() {
|
|
225
|
+
// The process owner's iterable input path observes stdin failures
|
|
226
|
+
// and owns its drain if PowerShell exits before accepting the script.
|
|
227
|
+
yield `& {\n${script}\n}\n\n`;
|
|
228
|
+
}
|
|
229
|
+
result = await runProcess(
|
|
230
|
+
command,
|
|
231
|
+
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '-'],
|
|
232
|
+
{
|
|
233
|
+
signal,
|
|
234
|
+
input: discoveryInput(),
|
|
235
|
+
captureOutput: true,
|
|
236
|
+
emitOutputEvents: false
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
if (signal.aborted) throw cancellation(signal.reason);
|
|
240
|
+
const inventory = JSON.parse(result.stdout);
|
|
241
|
+
if (!inventory || !is.array(inventory.devices) || !is.array(inventory.issues)) {
|
|
242
|
+
throw new TypeError('Windows execution-device discovery returned no inventory record.');
|
|
243
|
+
}
|
|
244
|
+
if (result.stderr !== '') {
|
|
245
|
+
inventory.issues.push(
|
|
246
|
+
{source: 'identity', code: 'discovery-diagnostics', message: result.stderr, deviceId: null}
|
|
247
|
+
);
|
|
248
|
+
if (inventory.state === 'ready') inventory.state = 'partial';
|
|
249
|
+
}
|
|
250
|
+
return inventory;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (signal.aborted) throw cancellation(error);
|
|
253
|
+
const details = inspect(
|
|
254
|
+
error,
|
|
255
|
+
{depth: null, maxArrayLength: null, maxStringLength: null, compact: false}
|
|
256
|
+
);
|
|
257
|
+
const output = result ? `\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` : '';
|
|
258
|
+
return unavailableInventory('discovery-failed', `${details}${output}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function unavailableInventory(code, message) {
|
|
263
|
+
return {
|
|
264
|
+
platform: process.platform,
|
|
265
|
+
state: 'unavailable',
|
|
266
|
+
devices: [],
|
|
267
|
+
issues: [
|
|
268
|
+
{source: 'identity', code, message, deviceId: null}
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function cancellation(reason) {
|
|
274
|
+
const error = new ArcaneError(
|
|
275
|
+
ERROR_CODES.cancelled,
|
|
276
|
+
'Execution-device discovery was cancelled.',
|
|
277
|
+
{cause: reason, exitCode: 130}
|
|
278
|
+
);
|
|
279
|
+
error.name = 'AbortError';
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function requireSignal(signal) {
|
|
284
|
+
if (signal === undefined || signal === null) return;
|
|
285
|
+
if (!is.boolean(signal.aborted) || !is.function(signal.addEventListener) || !is.function(signal.removeEventListener)) {
|
|
286
|
+
throw new TypeError('Execution-device discovery requires an AbortSignal when signal is supplied.');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -20,13 +20,15 @@ async function createSession(request) {
|
|
|
20
20
|
const selectProviders = supplied === undefined
|
|
21
21
|
|| (supplied !== null && is.object(supplied) && supplied.executionProviders === undefined);
|
|
22
22
|
const options = selectProviders ? {...supplied, executionProviders: ['cpu']} : supplied;
|
|
23
|
-
if (request.executionPreference !== 'gpu' || !selectProviders) {
|
|
24
|
-
session = await ort.InferenceSession.create(request.model, options);
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
const execution = {
|
|
29
|
-
preference: '
|
|
24
|
+
preference: request.executionPreference ?? 'cpu',
|
|
25
|
+
requestedTarget: request.executionTarget ?? null,
|
|
26
|
+
resolvedDevice: null,
|
|
27
|
+
resolution: 'automatic',
|
|
28
|
+
reason: selectProviders ? 'engine-default-required' : 'caller-session-options',
|
|
29
|
+
...request.targetResolution,
|
|
30
|
+
configuredTarget: null,
|
|
31
|
+
observedTarget: null,
|
|
30
32
|
supportedBackends: [],
|
|
31
33
|
selectedProviders: [],
|
|
32
34
|
attempts: [],
|
|
@@ -34,19 +36,153 @@ async function createSession(request) {
|
|
|
34
36
|
discoveryError: null
|
|
35
37
|
};
|
|
36
38
|
const failures = [];
|
|
39
|
+
|
|
40
|
+
function targetFailure(code, reason, message) {
|
|
41
|
+
execution.reason = reason;
|
|
42
|
+
const error = new Error(message);
|
|
43
|
+
error.code = code;
|
|
44
|
+
error.execution = execution;
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function providerName(provider) {
|
|
49
|
+
return is.string(provider) ? provider : provider?.name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function configure(selectedOptions, source, finalAttempt = true) {
|
|
53
|
+
const executionProviders = selectedOptions?.executionProviders ?? null;
|
|
54
|
+
const names = is.array(executionProviders) ? executionProviders.map(providerName) : [];
|
|
55
|
+
const configuredTarget = {source, executionProviders};
|
|
56
|
+
const attempt = {executionProviders: names, configuredTarget, status: 'loading', error: null};
|
|
57
|
+
execution.attempts.push(attempt);
|
|
58
|
+
try {
|
|
59
|
+
session = await ort.InferenceSession.create(request.model, selectedOptions);
|
|
60
|
+
attempt.status = 'configured';
|
|
61
|
+
execution.selectedProviders = names;
|
|
62
|
+
execution.configuredTarget = configuredTarget;
|
|
63
|
+
// Session creation accepts configuration. Node exposes no physical
|
|
64
|
+
// placement observation; GPU and CPU graph partitioning may coexist.
|
|
65
|
+
return execution;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
attempt.status = 'failed';
|
|
68
|
+
const record = describeError(error);
|
|
69
|
+
attempt.error = record;
|
|
70
|
+
if (finalAttempt) {
|
|
71
|
+
// Preserve primitive and non-extensible provider failures while
|
|
72
|
+
// adding SDK diagnostics without mutating the provider's value.
|
|
73
|
+
throw Object.assign(new Error(record.message, {cause: record.cause}), record, {execution});
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasBackend(name) {
|
|
80
|
+
return execution.supportedBackends.some(
|
|
81
|
+
function supportedBackend(backend) { return backend.name === name; }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function physicalOptions(device) {
|
|
86
|
+
let provider;
|
|
87
|
+
if (device.kind === 'cpu' && device.deviceId === 'cpu') {
|
|
88
|
+
provider = 'cpu';
|
|
89
|
+
} else if (device.kind === 'gpu' && hasBackend('dml')
|
|
90
|
+
&& is.integer(device.addresses?.dxgiAdapterIndex) && device.addresses.dxgiAdapterIndex >= 0) {
|
|
91
|
+
provider = {name: 'dml', deviceId: device.addresses.dxgiAdapterIndex};
|
|
92
|
+
} else {
|
|
93
|
+
throw targetFailure(
|
|
94
|
+
'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED', 'provider-device-mapping-unavailable',
|
|
95
|
+
'The installed ONNX binding cannot map this physical device to a supported execution provider.'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!selectProviders) {
|
|
99
|
+
const providers = supplied?.executionProviders;
|
|
100
|
+
if (!is.array(providers) || providers.length === 0) {
|
|
101
|
+
throw targetFailure(
|
|
102
|
+
'LOCAL_AI_EXECUTION_TARGET_CONFLICT', 'session-options-do-not-select-target',
|
|
103
|
+
'The explicit session options do not select the requested physical execution target.'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const expectedName = providerName(provider);
|
|
107
|
+
const primary = providers[0];
|
|
108
|
+
const matches = providerName(primary) === expectedName
|
|
109
|
+
&& (expectedName === 'cpu' || (primary?.deviceId ?? 0) === provider.deviceId)
|
|
110
|
+
&& providers.every(
|
|
111
|
+
function compatibleProvider(value, index) { return index === 0 || providerName(value) === 'cpu'; }
|
|
112
|
+
);
|
|
113
|
+
if (!matches) {
|
|
114
|
+
throw targetFailure(
|
|
115
|
+
'LOCAL_AI_EXECUTION_TARGET_CONFLICT', 'session-options-target-conflict',
|
|
116
|
+
'The explicit execution providers conflict with the physical target or have no established mapping to it.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const selectedOptions = selectProviders ? {
|
|
121
|
+
...supplied,
|
|
122
|
+
executionProviders: provider === 'cpu' ? ['cpu'] : [provider, 'cpu']
|
|
123
|
+
} : {...supplied};
|
|
124
|
+
if (providerName(provider) === 'dml') {
|
|
125
|
+
if (selectedOptions.enableMemPattern === undefined) selectedOptions.enableMemPattern = false;
|
|
126
|
+
if (selectedOptions.executionMode === undefined) selectedOptions.executionMode = 'sequential';
|
|
127
|
+
}
|
|
128
|
+
return selectedOptions;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const explicitDevice = request.executionTarget !== undefined && request.executionTarget !== null;
|
|
132
|
+
if (explicitDevice && (execution.resolution !== 'matched' || !execution.resolvedDevice)) {
|
|
133
|
+
throw targetFailure(
|
|
134
|
+
execution.resolution === 'unsupported' ? 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED' : 'LOCAL_AI_EXECUTION_TARGET_UNAVAILABLE',
|
|
135
|
+
execution.reason ?? 'physical-device-unavailable',
|
|
136
|
+
'The requested physical execution device could not be resolved.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (request.executionTarget === null && !selectProviders
|
|
140
|
+
&& is.array(supplied?.executionProviders) && supplied.executionProviders.some(
|
|
141
|
+
function fixedDevice(provider) { return !is.string(provider) && provider?.deviceId !== undefined; }
|
|
142
|
+
)) {
|
|
143
|
+
throw targetFailure(
|
|
144
|
+
'LOCAL_AI_EXECUTION_TARGET_CONFLICT', 'automatic-target-fixed-session-device',
|
|
145
|
+
'An automatic execution target conflicts with an explicit provider deviceId in session options.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (explicitDevice && execution.resolvedDevice.kind === 'cpu') {
|
|
149
|
+
return configure(physicalOptions(execution.resolvedDevice), 'execution-target');
|
|
150
|
+
}
|
|
151
|
+
if (!explicitDevice && (request.executionPreference !== 'gpu' || !selectProviders)) {
|
|
152
|
+
return configure(options, selectProviders ? 'cpu-default' : 'session-options');
|
|
153
|
+
}
|
|
37
154
|
try {
|
|
38
155
|
execution.supportedBackends = ort.listSupportedBackends();
|
|
39
156
|
} catch (error) {
|
|
40
157
|
execution.discoveryError = describeError(error);
|
|
41
158
|
failures.push(error);
|
|
42
159
|
}
|
|
160
|
+
if (explicitDevice) {
|
|
161
|
+
return configure(physicalOptions(execution.resolvedDevice), 'execution-target');
|
|
162
|
+
}
|
|
163
|
+
if (request.executionTarget === null && hasBackend('dml')) {
|
|
164
|
+
const candidates = (request.deviceInventory?.devices ?? []).filter(
|
|
165
|
+
function supportedPhysicalGPU(device) {
|
|
166
|
+
return device.kind === 'gpu' && device.present === true && device.isHardware === true
|
|
167
|
+
&& is.string(device.deviceId) && is.finite(device.dedicatedMemoryMiB)
|
|
168
|
+
&& device.dedicatedMemoryMiB > 0 && is.integer(device.addresses?.dxgiAdapterIndex)
|
|
169
|
+
&& device.addresses.dxgiAdapterIndex >= 0;
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
candidates.sort(
|
|
173
|
+
function largestDedicatedMemory(first, second) { return second.dedicatedMemoryMiB - first.dedicatedMemoryMiB; }
|
|
174
|
+
);
|
|
175
|
+
if (candidates.length) {
|
|
176
|
+
execution.resolvedDevice = candidates[0];
|
|
177
|
+
execution.reason = 'largest-supported-dedicated-memory-gpu';
|
|
178
|
+
return configure(physicalOptions(execution.resolvedDevice), 'automatic-physical-target');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
43
181
|
// The inventory advertises compiled support. `bundled` describes packaging,
|
|
44
182
|
// not whether an installed provider library or physical device can load.
|
|
45
183
|
const candidates = ['cuda', 'tensorrt', 'dml', 'coreml', 'webgpu'].filter(
|
|
46
184
|
function advertisedProvider(name) {
|
|
47
|
-
return
|
|
48
|
-
function supportedBackend(backend) { return backend.name === name; }
|
|
49
|
-
);
|
|
185
|
+
return hasBackend(name);
|
|
50
186
|
}
|
|
51
187
|
);
|
|
52
188
|
candidates.push('cpu');
|
|
@@ -59,19 +195,13 @@ async function createSession(request) {
|
|
|
59
195
|
if (selectedOptions.enableMemPattern === undefined) selectedOptions.enableMemPattern = false;
|
|
60
196
|
if (selectedOptions.executionMode === undefined) selectedOptions.executionMode = 'sequential';
|
|
61
197
|
}
|
|
62
|
-
const attempt = {executionProviders, status: 'loading', error: null};
|
|
63
|
-
execution.attempts.push(attempt);
|
|
64
198
|
execution.fallback = provider === 'cpu';
|
|
65
199
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
execution
|
|
69
|
-
|
|
70
|
-
// ORT can assign graph nodes to CPU; it does not prove GPU execution.
|
|
71
|
-
return execution;
|
|
200
|
+
// Keep intermediate native errors untouched: a provider may reuse
|
|
201
|
+
// one Error object for multiple attempts. The final failure owns
|
|
202
|
+
// the execution record after every attempt has been described.
|
|
203
|
+
return await configure(selectedOptions, 'gpu-preference', false);
|
|
72
204
|
} catch (error) {
|
|
73
|
-
attempt.status = 'failed';
|
|
74
|
-
attempt.error = describeError(error);
|
|
75
205
|
failures.push(error);
|
|
76
206
|
}
|
|
77
207
|
}
|