@plasius/gpu-model-runtime 0.1.1 → 0.2.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/README.md +153 -4
- package/dist/index.cjs +873 -137
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -2
- package/dist/index.d.ts +136 -2
- package/dist/index.js +868 -137
- package/dist/index.js.map +1 -1
- package/package.json +17 -10
package/dist/index.js
CHANGED
|
@@ -29,8 +29,117 @@ function createModelCacheKey(contentHash, formatId, adapterOptions) {
|
|
|
29
29
|
return `${formatId}:${contentHash}:${JSON.stringify(stableValue(adapterOptions ?? null))}`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/types.ts
|
|
33
|
+
var DEFAULT_MODEL_MAX_BYTES = 64 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
// src/bytes.ts
|
|
36
|
+
var ModelResourceTooLargeError = class extends Error {
|
|
37
|
+
code = "MODEL_RESOURCE_TOO_LARGE";
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Model resource exceeds maxBytes");
|
|
40
|
+
this.name = "ModelResourceTooLargeError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function byteLimit(value = DEFAULT_MODEL_MAX_BYTES) {
|
|
44
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError("maxBytes must be a positive safe integer");
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function checkSize(size, maxBytes) {
|
|
48
|
+
if (size > maxBytes) throw new ModelResourceTooLargeError();
|
|
49
|
+
}
|
|
50
|
+
function checkAbort(signal) {
|
|
51
|
+
if (signal?.aborted) throw new DOMException("The model load was aborted", "AbortError");
|
|
52
|
+
}
|
|
53
|
+
function acquisitionController(signal, timeoutMs = 3e4, parent) {
|
|
54
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
55
|
+
throw new RangeError("timeoutMs must be a positive timer-safe integer");
|
|
56
|
+
}
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const abort = () => controller.abort();
|
|
59
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
60
|
+
parent?.addEventListener("abort", abort, { once: true });
|
|
61
|
+
if (signal?.aborted || parent?.aborted) controller.abort();
|
|
62
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
63
|
+
return {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
dispose() {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
signal?.removeEventListener("abort", abort);
|
|
68
|
+
parent?.removeEventListener("abort", abort);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function abortable(work, signal) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const abort = () => {
|
|
75
|
+
signal.removeEventListener("abort", abort);
|
|
76
|
+
reject(new DOMException("The model load was aborted", "AbortError"));
|
|
77
|
+
};
|
|
78
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
79
|
+
if (signal.aborted) abort();
|
|
80
|
+
work.then(
|
|
81
|
+
(value) => {
|
|
82
|
+
signal.removeEventListener("abort", abort);
|
|
83
|
+
resolve(value);
|
|
84
|
+
},
|
|
85
|
+
(error) => {
|
|
86
|
+
signal.removeEventListener("abort", abort);
|
|
87
|
+
reject(error);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function cleanup(work) {
|
|
93
|
+
try {
|
|
94
|
+
void Promise.resolve(work()).catch(() => {
|
|
95
|
+
});
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function readBoundedStream(source, maxBytes, signal) {
|
|
100
|
+
let bytes = new Uint8Array(0);
|
|
101
|
+
let length = 0;
|
|
102
|
+
let chunks = 0;
|
|
103
|
+
const reader = "getReader" in source ? source.getReader() : void 0;
|
|
104
|
+
const iterator = reader ? void 0 : source[Symbol.asyncIterator]();
|
|
105
|
+
try {
|
|
106
|
+
while (true) {
|
|
107
|
+
checkAbort(signal);
|
|
108
|
+
if (++chunks % 256 === 0) await abortable(new Promise((resolve) => setTimeout(resolve, 0)), signal);
|
|
109
|
+
const next = await abortable(reader ? reader.read() : iterator.next(), signal);
|
|
110
|
+
checkAbort(signal);
|
|
111
|
+
if (next.done) break;
|
|
112
|
+
if (!(next.value instanceof Uint8Array)) throw new TypeError("Model stream chunks must be Uint8Array");
|
|
113
|
+
const chunk = next.value;
|
|
114
|
+
if (chunk.byteLength > maxBytes - length) throw new ModelResourceTooLargeError();
|
|
115
|
+
const required = length + chunk.byteLength;
|
|
116
|
+
if (required > bytes.length) {
|
|
117
|
+
const grown = new Uint8Array(Math.min(maxBytes, Math.max(required, bytes.length * 2, 4096)));
|
|
118
|
+
grown.set(bytes.subarray(0, length));
|
|
119
|
+
bytes = grown;
|
|
120
|
+
}
|
|
121
|
+
bytes.set(chunk, length);
|
|
122
|
+
length = required;
|
|
123
|
+
}
|
|
124
|
+
return bytes.length === length ? bytes : bytes.slice(0, length);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (reader) cleanup(() => reader.cancel());
|
|
127
|
+
else cleanup(() => iterator?.return?.());
|
|
128
|
+
throw error;
|
|
129
|
+
} finally {
|
|
130
|
+
reader?.releaseLock();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function boundedBytes(input, maxBytes, signal) {
|
|
134
|
+
checkAbort(signal);
|
|
135
|
+
checkSize(input instanceof Blob ? input.size : input.byteLength, maxBytes);
|
|
136
|
+
if (input instanceof Blob) return readBoundedStream(input.stream(), maxBytes, signal);
|
|
137
|
+
return new Uint8Array(input);
|
|
138
|
+
}
|
|
139
|
+
|
|
32
140
|
// src/fetch.ts
|
|
33
141
|
var DEFAULT_POLICY = {
|
|
142
|
+
maxBytes: DEFAULT_MODEL_MAX_BYTES,
|
|
34
143
|
maxAttempts: 3,
|
|
35
144
|
baseDelayMs: 100,
|
|
36
145
|
maxDelayMs: 1e3,
|
|
@@ -61,6 +170,7 @@ async function sha256Hex(bytes) {
|
|
|
61
170
|
}
|
|
62
171
|
async function fetchResource(url, options) {
|
|
63
172
|
const policy = { ...DEFAULT_POLICY, ...options.policy };
|
|
173
|
+
const maxBytes = Math.min(byteLimit(policy.maxBytes), byteLimit(options.resolveOptions?.maxBytes ?? policy.maxBytes));
|
|
64
174
|
const headers = new Headers(options.headers);
|
|
65
175
|
if (options.resolveOptions?.range) {
|
|
66
176
|
const { start, end } = options.resolveOptions.range;
|
|
@@ -68,13 +178,21 @@ async function fetchResource(url, options) {
|
|
|
68
178
|
}
|
|
69
179
|
let lastError;
|
|
70
180
|
for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) {
|
|
71
|
-
const controller =
|
|
181
|
+
const controller = acquisitionController(options.resolveOptions?.signal, policy.timeoutMs);
|
|
72
182
|
try {
|
|
73
|
-
|
|
183
|
+
checkAbort(controller.signal);
|
|
184
|
+
const response = await abortable(options.fetch(url, {
|
|
74
185
|
headers,
|
|
75
186
|
signal: controller.signal
|
|
76
|
-
})
|
|
187
|
+
}).then((response2) => {
|
|
188
|
+
if (controller.signal.aborted) {
|
|
189
|
+
cleanup(() => response2.body?.cancel());
|
|
190
|
+
checkAbort(controller.signal);
|
|
191
|
+
}
|
|
192
|
+
return response2;
|
|
193
|
+
}), controller.signal);
|
|
77
194
|
if (!response.ok) {
|
|
195
|
+
cleanup(() => response.body?.cancel());
|
|
78
196
|
if (!RETRYABLE_STATUSES.has(response.status)) {
|
|
79
197
|
throw new NonRetryableResourceError(`Model resource request failed with HTTP ${response.status}`);
|
|
80
198
|
}
|
|
@@ -82,11 +200,20 @@ async function fetchResource(url, options) {
|
|
|
82
200
|
throw new NonRetryableResourceError(`Model resource request failed with HTTP ${response.status}`);
|
|
83
201
|
}
|
|
84
202
|
lastError = new Error(`Retryable model resource response: HTTP ${response.status}`);
|
|
85
|
-
await waitForRetry(options.sleep, attempt, policy,
|
|
203
|
+
await waitForRetry(options.sleep, attempt, policy, controller.signal);
|
|
86
204
|
continue;
|
|
87
205
|
}
|
|
88
|
-
|
|
89
|
-
|
|
206
|
+
let bytes;
|
|
207
|
+
try {
|
|
208
|
+
const declared = response.headers.get("content-length");
|
|
209
|
+
if (declared !== null && /^\d+$/u.test(declared)) checkSize(Number(declared), maxBytes);
|
|
210
|
+
bytes = response.body ? await readBoundedStream(response.body, maxBytes, controller.signal) : new Uint8Array(0);
|
|
211
|
+
checkAbort(controller.signal);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (!response.body?.locked) cleanup(() => response.body?.cancel());
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
await abortable(verifyIntegrity(bytes, options.integrity, options.hashBytes), controller.signal);
|
|
90
217
|
return {
|
|
91
218
|
bytes,
|
|
92
219
|
contentType: response.headers.get("content-type") ?? void 0,
|
|
@@ -96,14 +223,14 @@ async function fetchResource(url, options) {
|
|
|
96
223
|
if (isAbortError(error) || options.resolveOptions?.signal?.aborted) {
|
|
97
224
|
throw createAbortError();
|
|
98
225
|
}
|
|
99
|
-
if (error instanceof NonRetryableResourceError) {
|
|
226
|
+
if (error instanceof NonRetryableResourceError || error instanceof ModelResourceTooLargeError) {
|
|
100
227
|
throw error;
|
|
101
228
|
}
|
|
102
229
|
lastError = error;
|
|
103
230
|
if (attempt === policy.maxAttempts) {
|
|
104
231
|
throw error;
|
|
105
232
|
}
|
|
106
|
-
await waitForRetry(options.sleep, attempt, policy,
|
|
233
|
+
await waitForRetry(options.sleep, attempt, policy, controller.signal);
|
|
107
234
|
} finally {
|
|
108
235
|
controller.dispose();
|
|
109
236
|
}
|
|
@@ -140,20 +267,7 @@ function bytesToBase64(bytes) {
|
|
|
140
267
|
}
|
|
141
268
|
async function waitForRetry(sleep, attempt, policy, signal) {
|
|
142
269
|
const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (attempt - 1));
|
|
143
|
-
await sleep(delay, signal);
|
|
144
|
-
}
|
|
145
|
-
function createRequestController(signal, timeoutMs) {
|
|
146
|
-
const controller = new AbortController();
|
|
147
|
-
const onAbort = () => controller.abort();
|
|
148
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
149
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
150
|
-
return {
|
|
151
|
-
signal: controller.signal,
|
|
152
|
-
dispose: () => {
|
|
153
|
-
clearTimeout(timer);
|
|
154
|
-
signal?.removeEventListener("abort", onAbort);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
270
|
+
await abortable(sleep(delay, signal), signal);
|
|
157
271
|
}
|
|
158
272
|
function createAbortError() {
|
|
159
273
|
return new DOMException("The model load was aborted", "AbortError");
|
|
@@ -189,6 +303,7 @@ var AdapterRegistry = class {
|
|
|
189
303
|
}
|
|
190
304
|
};
|
|
191
305
|
var PackageResourceResolver = class {
|
|
306
|
+
parentSignal;
|
|
192
307
|
memoryResources;
|
|
193
308
|
baseUrl;
|
|
194
309
|
sourceKind;
|
|
@@ -199,48 +314,58 @@ var PackageResourceResolver = class {
|
|
|
199
314
|
headers;
|
|
200
315
|
blobStorageUrlResolver;
|
|
201
316
|
constructor(options) {
|
|
317
|
+
this.parentSignal = options.signal;
|
|
202
318
|
this.sourceKind = options.sourceKind;
|
|
203
319
|
this.baseUrl = options.package?.baseUrl ?? options.baseUrl;
|
|
204
320
|
this.memoryResources = options.package?.resources ?? {};
|
|
205
321
|
this.fetch = options.fetch;
|
|
206
322
|
this.hashBytes = options.hashBytes;
|
|
207
323
|
this.sleep = options.sleep;
|
|
208
|
-
this.fetchPolicy = options.fetchPolicy;
|
|
324
|
+
this.fetchPolicy = { ...options.fetchPolicy };
|
|
209
325
|
this.headers = options.headers;
|
|
210
326
|
this.blobStorageUrlResolver = options.blobStorageUrlResolver;
|
|
211
327
|
}
|
|
212
328
|
async resolve(reference, options = {}) {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
329
|
+
const inheritedLimit = byteLimit(this.fetchPolicy?.maxBytes);
|
|
330
|
+
const maxBytes = Math.min(inheritedLimit, byteLimit(options.maxBytes ?? inheritedLimit));
|
|
331
|
+
const controller = acquisitionController(options.signal, this.fetchPolicy?.timeoutMs, this.parentSignal);
|
|
332
|
+
try {
|
|
333
|
+
checkAbort(controller.signal);
|
|
334
|
+
const requested = typeof reference === "string" ? { path: reference } : reference;
|
|
335
|
+
const memoryKey = normalizePackagePath(requested.path);
|
|
336
|
+
const memoryResource = this.memoryResources[memoryKey] ?? this.memoryResources[`./${memoryKey}`];
|
|
337
|
+
if (memoryResource) {
|
|
338
|
+
const bytes = await boundedBytes(memoryResource.bytes, maxBytes, controller.signal);
|
|
339
|
+
await abortable(verifyResourceIntegrity(bytes, requested.integrity ?? memoryResource.integrity, this.hashBytes), controller.signal);
|
|
340
|
+
return {
|
|
341
|
+
path: memoryKey,
|
|
342
|
+
bytes,
|
|
343
|
+
contentType: requested.contentType ?? memoryResource.contentType,
|
|
344
|
+
contentHash: await abortable(this.hashBytes(bytes), controller.signal),
|
|
345
|
+
rangeSupported: false
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const url = await abortable(this.resolveUrl(requested.path), controller.signal);
|
|
349
|
+
checkAbort(controller.signal);
|
|
350
|
+
const result = await fetchResource(url, {
|
|
351
|
+
fetch: this.fetch,
|
|
352
|
+
hashBytes: this.hashBytes,
|
|
353
|
+
sleep: this.sleep,
|
|
354
|
+
headers: this.headers,
|
|
355
|
+
integrity: requested.integrity,
|
|
356
|
+
policy: this.fetchPolicy,
|
|
357
|
+
resolveOptions: { ...options, maxBytes, signal: controller.signal }
|
|
358
|
+
});
|
|
219
359
|
return {
|
|
220
360
|
path: memoryKey,
|
|
221
|
-
bytes,
|
|
222
|
-
contentType: requested.contentType ??
|
|
223
|
-
contentHash: await this.hashBytes(bytes),
|
|
224
|
-
rangeSupported:
|
|
361
|
+
bytes: result.bytes,
|
|
362
|
+
contentType: requested.contentType ?? result.contentType,
|
|
363
|
+
contentHash: await abortable(this.hashBytes(result.bytes), controller.signal),
|
|
364
|
+
rangeSupported: result.rangeSupported
|
|
225
365
|
};
|
|
366
|
+
} finally {
|
|
367
|
+
controller.dispose();
|
|
226
368
|
}
|
|
227
|
-
const url = await this.resolveUrl(requested.path);
|
|
228
|
-
const result = await fetchResource(url, {
|
|
229
|
-
fetch: this.fetch,
|
|
230
|
-
hashBytes: this.hashBytes,
|
|
231
|
-
sleep: this.sleep,
|
|
232
|
-
headers: this.headers,
|
|
233
|
-
integrity: requested.integrity,
|
|
234
|
-
policy: this.fetchPolicy,
|
|
235
|
-
resolveOptions: options
|
|
236
|
-
});
|
|
237
|
-
return {
|
|
238
|
-
path: memoryKey,
|
|
239
|
-
bytes: result.bytes,
|
|
240
|
-
contentType: requested.contentType ?? result.contentType,
|
|
241
|
-
contentHash: await this.hashBytes(result.bytes),
|
|
242
|
-
rangeSupported: result.rangeSupported
|
|
243
|
-
};
|
|
244
369
|
}
|
|
245
370
|
async resolveUrl(reference) {
|
|
246
371
|
if (/^[a-z][a-z\d+.-]*:/iu.test(reference)) {
|
|
@@ -299,7 +424,7 @@ var ModelRuntime = class {
|
|
|
299
424
|
fileName: input.fileName,
|
|
300
425
|
sourceKind: source.kind
|
|
301
426
|
}, options.formatId);
|
|
302
|
-
const cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, options.adapterOptions);
|
|
427
|
+
const cacheKey = createModelCacheKey(input.contentHash, adapter.formatId, { adapterOptions: options.adapterOptions ?? null, maxBytes: byteLimit(options.fetchPolicy?.maxBytes) });
|
|
303
428
|
const useCache = options.useCache ?? true;
|
|
304
429
|
if (useCache) {
|
|
305
430
|
const cached = await this.cache.get(cacheKey);
|
|
@@ -345,54 +470,64 @@ var ModelRuntime = class {
|
|
|
345
470
|
return adapter.load(input, context);
|
|
346
471
|
}
|
|
347
472
|
async resolveSource(source, options) {
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
473
|
+
const maxBytes = byteLimit(options.fetchPolicy?.maxBytes);
|
|
474
|
+
const controller = acquisitionController(options.signal, options.fetchPolicy?.timeoutMs);
|
|
475
|
+
try {
|
|
476
|
+
checkAbort(controller.signal);
|
|
477
|
+
const fileName = sourceFileName(source);
|
|
478
|
+
const baseUrl = "url" in source ? source.url : void 0;
|
|
479
|
+
const headers = "headers" in source ? source.headers : void 0;
|
|
480
|
+
let bytes;
|
|
481
|
+
let contentType = "mimeTypeHint" in source ? source.mimeTypeHint : void 0;
|
|
482
|
+
let rangeSupported = false;
|
|
483
|
+
if (source.kind === "file-path") {
|
|
484
|
+
if (!this.readFile) {
|
|
485
|
+
throw new Error("file-path sources require a readFile dependency");
|
|
486
|
+
}
|
|
487
|
+
bytes = await abortable(this.readFile(source.path, controller.signal, { maxBytes }), controller.signal);
|
|
488
|
+
checkSize(bytes.byteLength, maxBytes);
|
|
489
|
+
} else if (source.kind === "url" || source.kind === "blob-storage-url") {
|
|
490
|
+
const result = await fetchResource(ensureHttpUrl(source.url), {
|
|
491
|
+
fetch: this.fetch,
|
|
492
|
+
hashBytes: this.hashBytes,
|
|
493
|
+
sleep: this.sleep,
|
|
494
|
+
headers,
|
|
495
|
+
integrity: options.integrity,
|
|
496
|
+
policy: options.fetchPolicy,
|
|
497
|
+
resolveOptions: { signal: controller.signal }
|
|
498
|
+
});
|
|
499
|
+
bytes = result.bytes;
|
|
500
|
+
contentType ??= result.contentType;
|
|
501
|
+
rangeSupported = result.rangeSupported;
|
|
502
|
+
} else {
|
|
503
|
+
bytes = await sourceToBytes(source, maxBytes, controller.signal);
|
|
357
504
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
505
|
+
checkAbort(controller.signal);
|
|
506
|
+
const contentHash = await abortable(this.hashBytes(bytes), controller.signal);
|
|
507
|
+
const resourceResolver = new PackageResourceResolver({
|
|
508
|
+
sourceKind: source.kind,
|
|
509
|
+
signal: options.signal,
|
|
510
|
+
baseUrl,
|
|
511
|
+
package: source.package,
|
|
361
512
|
fetch: this.fetch,
|
|
362
513
|
hashBytes: this.hashBytes,
|
|
363
514
|
sleep: this.sleep,
|
|
515
|
+
fetchPolicy: options.fetchPolicy,
|
|
364
516
|
headers,
|
|
365
|
-
|
|
366
|
-
policy: options.fetchPolicy,
|
|
367
|
-
resolveOptions: { signal: options.signal }
|
|
517
|
+
blobStorageUrlResolver: this.blobStorageUrlResolver
|
|
368
518
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
hashBytes: this.hashBytes,
|
|
382
|
-
sleep: this.sleep,
|
|
383
|
-
fetchPolicy: options.fetchPolicy,
|
|
384
|
-
headers,
|
|
385
|
-
blobStorageUrlResolver: this.blobStorageUrlResolver
|
|
386
|
-
});
|
|
387
|
-
return {
|
|
388
|
-
source,
|
|
389
|
-
bytes,
|
|
390
|
-
contentType,
|
|
391
|
-
fileName,
|
|
392
|
-
contentHash,
|
|
393
|
-
rangeSupported,
|
|
394
|
-
resourceResolver
|
|
395
|
-
};
|
|
519
|
+
return {
|
|
520
|
+
source,
|
|
521
|
+
bytes,
|
|
522
|
+
contentType,
|
|
523
|
+
fileName,
|
|
524
|
+
contentHash,
|
|
525
|
+
rangeSupported,
|
|
526
|
+
resourceResolver
|
|
527
|
+
};
|
|
528
|
+
} finally {
|
|
529
|
+
controller.dispose();
|
|
530
|
+
}
|
|
396
531
|
}
|
|
397
532
|
};
|
|
398
533
|
function scoreRegistration(registration, input) {
|
|
@@ -424,50 +559,11 @@ function normalizePackagePath(path) {
|
|
|
424
559
|
}
|
|
425
560
|
return segments.join("/");
|
|
426
561
|
}
|
|
427
|
-
async function sourceToBytes(source) {
|
|
428
|
-
if (source.kind === "blob")
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return new Uint8Array(source.bytes);
|
|
433
|
-
}
|
|
434
|
-
if (source.kind === "uint8-array") {
|
|
435
|
-
return new Uint8Array(source.bytes);
|
|
436
|
-
}
|
|
437
|
-
const chunks = [];
|
|
438
|
-
if (Symbol.asyncIterator in Object(source.stream)) {
|
|
439
|
-
for await (const chunk of source.stream) {
|
|
440
|
-
chunks.push(new Uint8Array(chunk));
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
const reader = source.stream.getReader();
|
|
444
|
-
try {
|
|
445
|
-
while (true) {
|
|
446
|
-
const next = await reader.read();
|
|
447
|
-
if (next.done) break;
|
|
448
|
-
chunks.push(new Uint8Array(next.value));
|
|
449
|
-
}
|
|
450
|
-
} finally {
|
|
451
|
-
reader.releaseLock();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
455
|
-
const bytes = new Uint8Array(length);
|
|
456
|
-
let offset = 0;
|
|
457
|
-
for (const chunk of chunks) {
|
|
458
|
-
bytes.set(chunk, offset);
|
|
459
|
-
offset += chunk.byteLength;
|
|
460
|
-
}
|
|
461
|
-
return bytes;
|
|
462
|
-
}
|
|
463
|
-
async function toBytes(input) {
|
|
464
|
-
if (input instanceof Blob) {
|
|
465
|
-
return new Uint8Array(await input.arrayBuffer());
|
|
466
|
-
}
|
|
467
|
-
if (input instanceof Uint8Array) {
|
|
468
|
-
return new Uint8Array(input);
|
|
469
|
-
}
|
|
470
|
-
return new Uint8Array(input);
|
|
562
|
+
async function sourceToBytes(source, maxBytes, signal) {
|
|
563
|
+
if (source.kind === "blob") return boundedBytes(source.blob, maxBytes, signal);
|
|
564
|
+
if (source.kind === "array-buffer" || source.kind === "uint8-array") return boundedBytes(source.bytes, maxBytes, signal);
|
|
565
|
+
if (source.byteLengthHint !== void 0) checkSize(source.byteLengthHint, maxBytes);
|
|
566
|
+
return readBoundedStream(source.stream, maxBytes, signal);
|
|
471
567
|
}
|
|
472
568
|
async function verifyResourceIntegrity(bytes, integrity, hashBytes) {
|
|
473
569
|
if (!integrity) return;
|
|
@@ -507,6 +603,636 @@ function ensureHttpUrl(value) {
|
|
|
507
603
|
return url.href;
|
|
508
604
|
}
|
|
509
605
|
|
|
606
|
+
// src/model-residency.ts
|
|
607
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
608
|
+
var PARTITION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u;
|
|
609
|
+
var ModelResidencyError = class extends Error {
|
|
610
|
+
code;
|
|
611
|
+
constructor(code, message) {
|
|
612
|
+
super(message);
|
|
613
|
+
this.name = "ModelResidencyError";
|
|
614
|
+
this.code = code;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
function requireSafeNonNegativeInteger(value, name) {
|
|
618
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
619
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function requirePositiveSafeInteger(value, name) {
|
|
623
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
624
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function requirePriority(priority) {
|
|
628
|
+
if (!Number.isFinite(priority) || priority < 0) {
|
|
629
|
+
throw new RangeError("priority must be a non-negative finite number");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function requireLod(lod) {
|
|
633
|
+
if (!Number.isInteger(lod) || lod < 0 || lod > 3) {
|
|
634
|
+
throw new RangeError("lod must be an integer from 0 to 3");
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function createModelResidencyKey(assetRef, lod, partitionId) {
|
|
638
|
+
if (!SHA256_PATTERN.test(assetRef.contentHash)) {
|
|
639
|
+
throw new Error("ModelAssetRef.contentHash must be a lowercase SHA-256 value");
|
|
640
|
+
}
|
|
641
|
+
requireLod(lod);
|
|
642
|
+
if (partitionId !== void 0 && !PARTITION_ID_PATTERN.test(partitionId)) {
|
|
643
|
+
throw new Error("partitionId must be a bounded runtime token");
|
|
644
|
+
}
|
|
645
|
+
return `${assetRef.contentHash}:lod${lod}:${partitionId ?? "whole"}`;
|
|
646
|
+
}
|
|
647
|
+
function closedError() {
|
|
648
|
+
return new ModelResidencyError(
|
|
649
|
+
"manager-closed",
|
|
650
|
+
"The model residency manager is closed"
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
function abortedError() {
|
|
654
|
+
return new ModelResidencyError(
|
|
655
|
+
"acquire-aborted",
|
|
656
|
+
"The model residency acquisition was aborted"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
function signalIsAborted(signal) {
|
|
660
|
+
return signal?.aborted === true;
|
|
661
|
+
}
|
|
662
|
+
var ModelResidencyManager = class {
|
|
663
|
+
#budget;
|
|
664
|
+
#load;
|
|
665
|
+
#loadTimeoutMs;
|
|
666
|
+
#entries = /* @__PURE__ */ new Map();
|
|
667
|
+
#loadTasks = /* @__PURE__ */ new Set();
|
|
668
|
+
#disposalTasks = /* @__PURE__ */ new Set();
|
|
669
|
+
#metrics = {
|
|
670
|
+
acquireCount: 0,
|
|
671
|
+
cacheHits: 0,
|
|
672
|
+
deduplicatedAcquires: 0,
|
|
673
|
+
loadsStarted: 0,
|
|
674
|
+
loadsCompleted: 0,
|
|
675
|
+
loadsFailed: 0,
|
|
676
|
+
loadsAborted: 0,
|
|
677
|
+
loadsTimedOut: 0,
|
|
678
|
+
evictions: 0,
|
|
679
|
+
disposals: 0,
|
|
680
|
+
disposalFailures: 0
|
|
681
|
+
};
|
|
682
|
+
#nextIdentity = 1;
|
|
683
|
+
#clock = 1;
|
|
684
|
+
#inFlightLoads = 0;
|
|
685
|
+
#closed = false;
|
|
686
|
+
#admissionTail = Promise.resolve();
|
|
687
|
+
#shutdownPromise;
|
|
688
|
+
constructor(options) {
|
|
689
|
+
requirePositiveSafeInteger(
|
|
690
|
+
options.budget.maxCpuBytes,
|
|
691
|
+
"budget.maxCpuBytes"
|
|
692
|
+
);
|
|
693
|
+
requirePositiveSafeInteger(
|
|
694
|
+
options.budget.maxGpuBytes,
|
|
695
|
+
"budget.maxGpuBytes"
|
|
696
|
+
);
|
|
697
|
+
requirePositiveSafeInteger(
|
|
698
|
+
options.budget.maxInFlightLoads,
|
|
699
|
+
"budget.maxInFlightLoads"
|
|
700
|
+
);
|
|
701
|
+
if (options.loadTimeoutMs !== void 0) {
|
|
702
|
+
requirePositiveSafeInteger(options.loadTimeoutMs, "loadTimeoutMs");
|
|
703
|
+
}
|
|
704
|
+
this.#budget = Object.freeze({ ...options.budget });
|
|
705
|
+
this.#load = options.load;
|
|
706
|
+
this.#loadTimeoutMs = options.loadTimeoutMs;
|
|
707
|
+
}
|
|
708
|
+
/** Acquire one shared resource lease, loading it at most once per cache key. */
|
|
709
|
+
acquire(options) {
|
|
710
|
+
try {
|
|
711
|
+
if (this.#closed) throw closedError();
|
|
712
|
+
requirePriority(options.priority);
|
|
713
|
+
requireSafeNonNegativeInteger(
|
|
714
|
+
options.estimatedCpuBytes,
|
|
715
|
+
"estimatedCpuBytes"
|
|
716
|
+
);
|
|
717
|
+
requireSafeNonNegativeInteger(
|
|
718
|
+
options.estimatedGpuBytes,
|
|
719
|
+
"estimatedGpuBytes"
|
|
720
|
+
);
|
|
721
|
+
const key = createModelResidencyKey(
|
|
722
|
+
options.assetRef,
|
|
723
|
+
options.lod,
|
|
724
|
+
options.partitionId
|
|
725
|
+
);
|
|
726
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
727
|
+
this.#metrics.acquireCount += 1;
|
|
728
|
+
const existing = this.#entries.get(key);
|
|
729
|
+
if (existing !== void 0) return this.#acquireExisting(existing, options);
|
|
730
|
+
return this.#scheduleAdmission(key, options);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
return Promise.reject(error);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
#acquireExisting(entry, options) {
|
|
736
|
+
this.#metrics.deduplicatedAcquires += 1;
|
|
737
|
+
if (entry.state === "resident") {
|
|
738
|
+
this.#metrics.cacheHits += 1;
|
|
739
|
+
return Promise.resolve(
|
|
740
|
+
this.#createLease(
|
|
741
|
+
entry,
|
|
742
|
+
options.priority,
|
|
743
|
+
options.pinned ?? false
|
|
744
|
+
)
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return this.#addWaiter(entry, options);
|
|
748
|
+
}
|
|
749
|
+
#scheduleAdmission(key, options) {
|
|
750
|
+
let resolveAcquisition;
|
|
751
|
+
let rejectAcquisition;
|
|
752
|
+
const acquisition = new Promise(
|
|
753
|
+
(resolve, reject) => {
|
|
754
|
+
resolveAcquisition = resolve;
|
|
755
|
+
rejectAcquisition = reject;
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
const admission = this.#admissionTail.then(async () => {
|
|
759
|
+
if (this.#closed) throw closedError();
|
|
760
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
761
|
+
const existing = this.#entries.get(key);
|
|
762
|
+
if (existing !== void 0) {
|
|
763
|
+
void this.#acquireExisting(existing, options).then(
|
|
764
|
+
resolveAcquisition,
|
|
765
|
+
rejectAcquisition
|
|
766
|
+
);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
await this.#ensureCapacityForReservation(
|
|
770
|
+
options.estimatedCpuBytes,
|
|
771
|
+
options.estimatedGpuBytes
|
|
772
|
+
);
|
|
773
|
+
if (this.#closed) throw closedError();
|
|
774
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
775
|
+
const entry = {
|
|
776
|
+
key,
|
|
777
|
+
assetRef: options.assetRef,
|
|
778
|
+
lod: options.lod,
|
|
779
|
+
...options.partitionId === void 0 ? {} : { partitionId: options.partitionId },
|
|
780
|
+
estimatedCpuBytes: options.estimatedCpuBytes,
|
|
781
|
+
estimatedGpuBytes: options.estimatedGpuBytes,
|
|
782
|
+
queueOrder: this.#nextIdentity++,
|
|
783
|
+
state: "queued",
|
|
784
|
+
priority: options.priority,
|
|
785
|
+
retainedPriority: options.priority,
|
|
786
|
+
lastUsed: this.#clock++,
|
|
787
|
+
abandoned: false,
|
|
788
|
+
loadSlotReleased: true,
|
|
789
|
+
waiters: /* @__PURE__ */ new Map(),
|
|
790
|
+
leases: /* @__PURE__ */ new Map()
|
|
791
|
+
};
|
|
792
|
+
this.#entries.set(key, entry);
|
|
793
|
+
void this.#addWaiter(entry, options).then(
|
|
794
|
+
resolveAcquisition,
|
|
795
|
+
rejectAcquisition
|
|
796
|
+
);
|
|
797
|
+
this.#pumpQueue();
|
|
798
|
+
});
|
|
799
|
+
this.#admissionTail = admission.catch(() => void 0);
|
|
800
|
+
void admission.catch(rejectAcquisition);
|
|
801
|
+
return acquisition;
|
|
802
|
+
}
|
|
803
|
+
/** Dispose every currently unreferenced, unpinned resident cache entry. */
|
|
804
|
+
async evictUnused() {
|
|
805
|
+
const tasks = [];
|
|
806
|
+
for (const entry of this.#evictionCandidates()) {
|
|
807
|
+
tasks.push(this.#evict(entry));
|
|
808
|
+
}
|
|
809
|
+
await Promise.all(tasks);
|
|
810
|
+
}
|
|
811
|
+
/** Wait until queued/loading work and its resulting disposal have settled. */
|
|
812
|
+
async waitForIdle() {
|
|
813
|
+
await this.#admissionTail;
|
|
814
|
+
do {
|
|
815
|
+
this.#pumpQueue();
|
|
816
|
+
const tasks = [...this.#loadTasks];
|
|
817
|
+
if (tasks.length > 0) {
|
|
818
|
+
await Promise.allSettled(tasks);
|
|
819
|
+
}
|
|
820
|
+
} while (this.#loadTasks.size > 0 || this.#queuedCount() > 0);
|
|
821
|
+
await Promise.allSettled([...this.#disposalTasks]);
|
|
822
|
+
}
|
|
823
|
+
/** Abort pending work and dispose every resident or late-completing resource. */
|
|
824
|
+
shutdown() {
|
|
825
|
+
if (this.#shutdownPromise !== void 0) return this.#shutdownPromise;
|
|
826
|
+
this.#closed = true;
|
|
827
|
+
const error = closedError();
|
|
828
|
+
const disposalTasks = [];
|
|
829
|
+
for (const entry of [...this.#entries.values()]) {
|
|
830
|
+
if (entry.state === "resident") {
|
|
831
|
+
this.#entries.delete(entry.key);
|
|
832
|
+
disposalTasks.push(this.#disposeEntry(entry));
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
this.#rejectWaiters(entry, error);
|
|
836
|
+
if (entry.state === "queued") {
|
|
837
|
+
this.#entries.delete(entry.key);
|
|
838
|
+
} else {
|
|
839
|
+
this.#abandonLoadingEntry(entry, error, false);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
this.#shutdownPromise = (async () => {
|
|
843
|
+
await Promise.allSettled(disposalTasks);
|
|
844
|
+
await this.waitForIdle();
|
|
845
|
+
await Promise.allSettled([...this.#disposalTasks]);
|
|
846
|
+
})();
|
|
847
|
+
return this.#shutdownPromise;
|
|
848
|
+
}
|
|
849
|
+
/** Return immutable byte, queue, cache, and lifetime metrics. */
|
|
850
|
+
snapshot() {
|
|
851
|
+
let cpuBytes = 0;
|
|
852
|
+
let gpuBytes = 0;
|
|
853
|
+
let reservedCpuBytes = 0;
|
|
854
|
+
let reservedGpuBytes = 0;
|
|
855
|
+
let referenceCount = 0;
|
|
856
|
+
let pinnedCount = 0;
|
|
857
|
+
let residentCount = 0;
|
|
858
|
+
let pendingCount = 0;
|
|
859
|
+
const entries = [];
|
|
860
|
+
for (const entry of this.#entries.values()) {
|
|
861
|
+
const pinned = [...entry.leases.values()].some(
|
|
862
|
+
(lease) => lease.active && lease.pinned
|
|
863
|
+
);
|
|
864
|
+
const references = [...entry.leases.values()].filter(
|
|
865
|
+
(lease) => lease.active
|
|
866
|
+
).length;
|
|
867
|
+
referenceCount += references;
|
|
868
|
+
if (pinned) pinnedCount += 1;
|
|
869
|
+
if (entry.state === "resident") {
|
|
870
|
+
residentCount += 1;
|
|
871
|
+
cpuBytes += entry.loaded.cpuBytes;
|
|
872
|
+
gpuBytes += entry.loaded.gpuBytes;
|
|
873
|
+
} else {
|
|
874
|
+
pendingCount += 1;
|
|
875
|
+
reservedCpuBytes += entry.estimatedCpuBytes;
|
|
876
|
+
reservedGpuBytes += entry.estimatedGpuBytes;
|
|
877
|
+
}
|
|
878
|
+
entries.push(
|
|
879
|
+
Object.freeze({
|
|
880
|
+
key: entry.key,
|
|
881
|
+
state: entry.state,
|
|
882
|
+
priority: entry.priority,
|
|
883
|
+
references,
|
|
884
|
+
pinned,
|
|
885
|
+
cpuBytes: entry.state === "resident" ? entry.loaded.cpuBytes : entry.estimatedCpuBytes,
|
|
886
|
+
gpuBytes: entry.state === "resident" ? entry.loaded.gpuBytes : entry.estimatedGpuBytes
|
|
887
|
+
})
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
891
|
+
return Object.freeze({
|
|
892
|
+
...this.#budget,
|
|
893
|
+
cpuBytes,
|
|
894
|
+
gpuBytes,
|
|
895
|
+
reservedCpuBytes,
|
|
896
|
+
reservedGpuBytes,
|
|
897
|
+
residentCount,
|
|
898
|
+
pendingCount,
|
|
899
|
+
referenceCount,
|
|
900
|
+
pinnedCount,
|
|
901
|
+
inFlightLoads: this.#inFlightLoads,
|
|
902
|
+
queuedLoads: this.#queuedCount(),
|
|
903
|
+
...this.#metrics,
|
|
904
|
+
residentKeys: Object.freeze(
|
|
905
|
+
entries.filter((entry) => entry.state === "resident").map((entry) => entry.key)
|
|
906
|
+
),
|
|
907
|
+
entries: Object.freeze(entries)
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
#addWaiter(entry, options) {
|
|
911
|
+
const id = this.#nextIdentity++;
|
|
912
|
+
let resolvePromise;
|
|
913
|
+
let rejectPromise;
|
|
914
|
+
const promise = new Promise(
|
|
915
|
+
(resolve, reject) => {
|
|
916
|
+
resolvePromise = resolve;
|
|
917
|
+
rejectPromise = reject;
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
const waiter = {
|
|
921
|
+
id,
|
|
922
|
+
priority: options.priority,
|
|
923
|
+
pinned: options.pinned ?? false,
|
|
924
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
925
|
+
resolve: resolvePromise,
|
|
926
|
+
reject: rejectPromise
|
|
927
|
+
};
|
|
928
|
+
if (options.signal !== void 0) {
|
|
929
|
+
waiter.onAbort = () => this.#abortWaiter(entry, waiter);
|
|
930
|
+
options.signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
931
|
+
}
|
|
932
|
+
entry.waiters.set(id, waiter);
|
|
933
|
+
entry.retainedPriority = Math.max(entry.retainedPriority, options.priority);
|
|
934
|
+
this.#recalculatePriority(entry);
|
|
935
|
+
return promise;
|
|
936
|
+
}
|
|
937
|
+
#abortWaiter(entry, waiter) {
|
|
938
|
+
entry.waiters.delete(waiter.id);
|
|
939
|
+
this.#detachWaiterSignal(waiter);
|
|
940
|
+
waiter.reject(abortedError());
|
|
941
|
+
this.#recalculatePriority(entry);
|
|
942
|
+
if (entry.waiters.size > 0) return;
|
|
943
|
+
if (entry.state === "queued") {
|
|
944
|
+
this.#entries.delete(entry.key);
|
|
945
|
+
this.#pumpQueue();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this.#abandonLoadingEntry(entry, abortedError(), true);
|
|
949
|
+
}
|
|
950
|
+
#createLease(entry, priority, pinned) {
|
|
951
|
+
const id = this.#nextIdentity++;
|
|
952
|
+
const record = {
|
|
953
|
+
id,
|
|
954
|
+
active: true,
|
|
955
|
+
priority,
|
|
956
|
+
pinned
|
|
957
|
+
};
|
|
958
|
+
entry.leases.set(id, record);
|
|
959
|
+
entry.retainedPriority = priority;
|
|
960
|
+
entry.lastUsed = this.#clock++;
|
|
961
|
+
this.#recalculatePriority(entry);
|
|
962
|
+
const lease = {
|
|
963
|
+
key: entry.key,
|
|
964
|
+
assetRef: entry.assetRef,
|
|
965
|
+
lod: entry.lod,
|
|
966
|
+
...entry.partitionId === void 0 ? {} : { partitionId: entry.partitionId },
|
|
967
|
+
resource: entry.loaded.resource,
|
|
968
|
+
release: () => {
|
|
969
|
+
if (!record.active) return;
|
|
970
|
+
record.active = false;
|
|
971
|
+
entry.leases.delete(id);
|
|
972
|
+
entry.lastUsed = this.#clock++;
|
|
973
|
+
this.#recalculatePriority(entry);
|
|
974
|
+
},
|
|
975
|
+
setPriority: (nextPriority) => {
|
|
976
|
+
if (!record.active || this.#entries.get(entry.key) !== entry) return;
|
|
977
|
+
requirePriority(nextPriority);
|
|
978
|
+
record.priority = nextPriority;
|
|
979
|
+
entry.retainedPriority = nextPriority;
|
|
980
|
+
entry.lastUsed = this.#clock++;
|
|
981
|
+
this.#recalculatePriority(entry);
|
|
982
|
+
},
|
|
983
|
+
setPinned: (nextPinned) => {
|
|
984
|
+
if (!record.active || this.#entries.get(entry.key) !== entry) return;
|
|
985
|
+
record.pinned = nextPinned;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
return Object.freeze(lease);
|
|
989
|
+
}
|
|
990
|
+
#recalculatePriority(entry) {
|
|
991
|
+
let priority = entry.retainedPriority;
|
|
992
|
+
for (const waiter of entry.waiters.values()) {
|
|
993
|
+
priority = Math.max(priority, waiter.priority);
|
|
994
|
+
}
|
|
995
|
+
for (const lease of entry.leases.values()) {
|
|
996
|
+
priority = Math.max(priority, lease.priority);
|
|
997
|
+
}
|
|
998
|
+
entry.priority = priority;
|
|
999
|
+
}
|
|
1000
|
+
#pumpQueue() {
|
|
1001
|
+
if (this.#closed) return;
|
|
1002
|
+
while (this.#inFlightLoads < this.#budget.maxInFlightLoads) {
|
|
1003
|
+
const candidate = [...this.#entries.values()].filter(
|
|
1004
|
+
(entry) => entry.state === "queued" && !entry.abandoned && entry.waiters.size > 0
|
|
1005
|
+
).sort(
|
|
1006
|
+
(left, right) => right.priority - left.priority || left.queueOrder - right.queueOrder
|
|
1007
|
+
)[0];
|
|
1008
|
+
if (candidate === void 0) return;
|
|
1009
|
+
this.#startLoad(candidate);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
#startLoad(entry) {
|
|
1013
|
+
entry.state = "loading";
|
|
1014
|
+
entry.loadSlotReleased = false;
|
|
1015
|
+
entry.controller = new AbortController();
|
|
1016
|
+
this.#inFlightLoads += 1;
|
|
1017
|
+
this.#metrics.loadsStarted += 1;
|
|
1018
|
+
if (this.#loadTimeoutMs !== void 0) {
|
|
1019
|
+
entry.timeout = setTimeout(() => {
|
|
1020
|
+
this.#metrics.loadsTimedOut += 1;
|
|
1021
|
+
this.#abandonLoadingEntry(
|
|
1022
|
+
entry,
|
|
1023
|
+
new ModelResidencyError(
|
|
1024
|
+
"load-timeout",
|
|
1025
|
+
`Model load exceeded ${this.#loadTimeoutMs} ms`
|
|
1026
|
+
),
|
|
1027
|
+
true
|
|
1028
|
+
);
|
|
1029
|
+
}, this.#loadTimeoutMs);
|
|
1030
|
+
}
|
|
1031
|
+
let adapterPromise;
|
|
1032
|
+
try {
|
|
1033
|
+
adapterPromise = this.#load({
|
|
1034
|
+
assetRef: entry.assetRef,
|
|
1035
|
+
lod: entry.lod,
|
|
1036
|
+
...entry.partitionId === void 0 ? {} : { partitionId: entry.partitionId },
|
|
1037
|
+
priority: entry.priority,
|
|
1038
|
+
signal: entry.controller.signal
|
|
1039
|
+
});
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
adapterPromise = Promise.reject(error);
|
|
1042
|
+
}
|
|
1043
|
+
const task = Promise.resolve(adapterPromise).then((result) => this.#completeLoad(entry, result)).catch((error) => this.#failLoad(entry, error)).finally(() => {
|
|
1044
|
+
if (entry.timeout !== void 0) clearTimeout(entry.timeout);
|
|
1045
|
+
entry.timeout = void 0;
|
|
1046
|
+
entry.controller = void 0;
|
|
1047
|
+
this.#releaseLoadSlot(entry);
|
|
1048
|
+
entry.loadTask = void 0;
|
|
1049
|
+
});
|
|
1050
|
+
entry.loadTask = task;
|
|
1051
|
+
this.#loadTasks.add(task);
|
|
1052
|
+
}
|
|
1053
|
+
async #completeLoad(entry, result) {
|
|
1054
|
+
if (entry.abandoned) {
|
|
1055
|
+
await this.#disposeLoaded(result);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
this.#validateLoadedResource(entry, result);
|
|
1060
|
+
const evictions = this.#ensureCapacityForLoadedResult(entry, result);
|
|
1061
|
+
await Promise.all(evictions);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
await this.#disposeLoaded(result);
|
|
1064
|
+
this.#entries.delete(entry.key);
|
|
1065
|
+
this.#rejectWaiters(entry, error);
|
|
1066
|
+
this.#metrics.loadsFailed += 1;
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
entry.state = "resident";
|
|
1070
|
+
entry.loaded = result;
|
|
1071
|
+
entry.lastUsed = this.#clock++;
|
|
1072
|
+
this.#metrics.loadsCompleted += 1;
|
|
1073
|
+
const waiters = [...entry.waiters.values()];
|
|
1074
|
+
entry.waiters.clear();
|
|
1075
|
+
for (const waiter of waiters) {
|
|
1076
|
+
this.#detachWaiterSignal(waiter);
|
|
1077
|
+
waiter.resolve(this.#createLease(entry, waiter.priority, waiter.pinned));
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
#failLoad(entry, error) {
|
|
1081
|
+
if (entry.abandoned) return;
|
|
1082
|
+
this.#entries.delete(entry.key);
|
|
1083
|
+
this.#metrics.loadsFailed += 1;
|
|
1084
|
+
this.#rejectWaiters(entry, error);
|
|
1085
|
+
}
|
|
1086
|
+
#abandonLoadingEntry(entry, error, rejectWaiters) {
|
|
1087
|
+
entry.abandoned = true;
|
|
1088
|
+
this.#metrics.loadsAborted += 1;
|
|
1089
|
+
if (entry.timeout !== void 0) clearTimeout(entry.timeout);
|
|
1090
|
+
entry.timeout = void 0;
|
|
1091
|
+
this.#entries.delete(entry.key);
|
|
1092
|
+
if (rejectWaiters) this.#rejectWaiters(entry, error);
|
|
1093
|
+
entry.controller?.abort(error);
|
|
1094
|
+
this.#releaseLoadSlot(entry);
|
|
1095
|
+
}
|
|
1096
|
+
#releaseLoadSlot(entry) {
|
|
1097
|
+
if (entry.loadSlotReleased) return;
|
|
1098
|
+
entry.loadSlotReleased = true;
|
|
1099
|
+
this.#inFlightLoads -= 1;
|
|
1100
|
+
this.#loadTasks.delete(entry.loadTask);
|
|
1101
|
+
this.#pumpQueue();
|
|
1102
|
+
}
|
|
1103
|
+
#rejectWaiters(entry, error) {
|
|
1104
|
+
const waiters = [...entry.waiters.values()];
|
|
1105
|
+
entry.waiters.clear();
|
|
1106
|
+
for (const waiter of waiters) {
|
|
1107
|
+
this.#detachWaiterSignal(waiter);
|
|
1108
|
+
waiter.reject(error);
|
|
1109
|
+
}
|
|
1110
|
+
this.#recalculatePriority(entry);
|
|
1111
|
+
}
|
|
1112
|
+
#detachWaiterSignal(waiter) {
|
|
1113
|
+
if (waiter.signal !== void 0 && waiter.onAbort !== void 0) {
|
|
1114
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1115
|
+
}
|
|
1116
|
+
waiter.onAbort = void 0;
|
|
1117
|
+
}
|
|
1118
|
+
#validateLoadedResource(entry, result) {
|
|
1119
|
+
if (result === null || typeof result !== "object") {
|
|
1120
|
+
throw new ModelResidencyError(
|
|
1121
|
+
"invalid-load-result",
|
|
1122
|
+
"The model loader returned an invalid resource"
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof result.dispose !== "function") {
|
|
1126
|
+
throw new ModelResidencyError(
|
|
1127
|
+
"invalid-load-result",
|
|
1128
|
+
"The model loader must return a disposer"
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
requireSafeNonNegativeInteger(result.cpuBytes, "loaded.cpuBytes");
|
|
1132
|
+
requireSafeNonNegativeInteger(result.gpuBytes, "loaded.gpuBytes");
|
|
1133
|
+
if (result.contentHash !== entry.assetRef.contentHash) {
|
|
1134
|
+
throw new ModelResidencyError(
|
|
1135
|
+
"content-hash-mismatch",
|
|
1136
|
+
"The loaded model content hash does not match its canonical reference"
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async #ensureCapacityForReservation(cpuBytes, gpuBytes) {
|
|
1141
|
+
if (cpuBytes > this.#budget.maxCpuBytes || gpuBytes > this.#budget.maxGpuBytes) {
|
|
1142
|
+
throw this.#budgetError();
|
|
1143
|
+
}
|
|
1144
|
+
let usage = this.#totalAccountedBytes();
|
|
1145
|
+
for (const candidate of this.#evictionCandidates()) {
|
|
1146
|
+
if (usage.cpu + cpuBytes <= this.#budget.maxCpuBytes && usage.gpu + gpuBytes <= this.#budget.maxGpuBytes) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
await this.#evict(candidate);
|
|
1150
|
+
usage = this.#totalAccountedBytes();
|
|
1151
|
+
}
|
|
1152
|
+
if (usage.cpu + cpuBytes > this.#budget.maxCpuBytes || usage.gpu + gpuBytes > this.#budget.maxGpuBytes) {
|
|
1153
|
+
throw this.#budgetError();
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
#ensureCapacityForLoadedResult(loadingEntry, result) {
|
|
1157
|
+
const usage = this.#totalAccountedBytes();
|
|
1158
|
+
usage.cpu += result.cpuBytes - loadingEntry.estimatedCpuBytes;
|
|
1159
|
+
usage.gpu += result.gpuBytes - loadingEntry.estimatedGpuBytes;
|
|
1160
|
+
const disposals = [];
|
|
1161
|
+
for (const candidate of this.#evictionCandidates()) {
|
|
1162
|
+
if (usage.cpu <= this.#budget.maxCpuBytes && usage.gpu <= this.#budget.maxGpuBytes) {
|
|
1163
|
+
return disposals;
|
|
1164
|
+
}
|
|
1165
|
+
usage.cpu -= candidate.loaded.cpuBytes;
|
|
1166
|
+
usage.gpu -= candidate.loaded.gpuBytes;
|
|
1167
|
+
disposals.push(this.#evict(candidate));
|
|
1168
|
+
}
|
|
1169
|
+
if (usage.cpu > this.#budget.maxCpuBytes || usage.gpu > this.#budget.maxGpuBytes) {
|
|
1170
|
+
throw this.#budgetError();
|
|
1171
|
+
}
|
|
1172
|
+
return disposals;
|
|
1173
|
+
}
|
|
1174
|
+
#budgetError() {
|
|
1175
|
+
return new ModelResidencyError(
|
|
1176
|
+
"budget-exceeded",
|
|
1177
|
+
"Pinned, referenced, or pending models leave insufficient residency budget"
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
#totalAccountedBytes() {
|
|
1181
|
+
let cpu = 0;
|
|
1182
|
+
let gpu = 0;
|
|
1183
|
+
for (const entry of this.#entries.values()) {
|
|
1184
|
+
if (entry.state === "resident") {
|
|
1185
|
+
cpu += entry.loaded.cpuBytes;
|
|
1186
|
+
gpu += entry.loaded.gpuBytes;
|
|
1187
|
+
} else {
|
|
1188
|
+
cpu += entry.estimatedCpuBytes;
|
|
1189
|
+
gpu += entry.estimatedGpuBytes;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return { cpu, gpu };
|
|
1193
|
+
}
|
|
1194
|
+
#evictionCandidates() {
|
|
1195
|
+
return [...this.#entries.values()].filter(
|
|
1196
|
+
(entry) => entry.state === "resident" && entry.leases.size === 0
|
|
1197
|
+
).sort(
|
|
1198
|
+
(left, right) => left.priority - right.priority || left.lastUsed - right.lastUsed
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
#evict(entry) {
|
|
1202
|
+
this.#entries.delete(entry.key);
|
|
1203
|
+
this.#metrics.evictions += 1;
|
|
1204
|
+
return this.#disposeEntry(entry);
|
|
1205
|
+
}
|
|
1206
|
+
#disposeEntry(entry) {
|
|
1207
|
+
const result = entry.loaded;
|
|
1208
|
+
entry.loaded = void 0;
|
|
1209
|
+
return this.#disposeLoaded(result);
|
|
1210
|
+
}
|
|
1211
|
+
#disposeLoaded(result) {
|
|
1212
|
+
this.#metrics.disposals += 1;
|
|
1213
|
+
let disposal;
|
|
1214
|
+
try {
|
|
1215
|
+
disposal = Promise.resolve(result.dispose());
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
disposal = Promise.reject(error);
|
|
1218
|
+
}
|
|
1219
|
+
const tracked = disposal.catch(() => {
|
|
1220
|
+
this.#metrics.disposalFailures += 1;
|
|
1221
|
+
}).finally(() => {
|
|
1222
|
+
this.#disposalTasks.delete(tracked);
|
|
1223
|
+
});
|
|
1224
|
+
this.#disposalTasks.add(tracked);
|
|
1225
|
+
return tracked;
|
|
1226
|
+
}
|
|
1227
|
+
#queuedCount() {
|
|
1228
|
+
let count = 0;
|
|
1229
|
+
for (const entry of this.#entries.values()) {
|
|
1230
|
+
if (entry.state === "queued") count += 1;
|
|
1231
|
+
}
|
|
1232
|
+
return count;
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
|
|
510
1236
|
// src/index.ts
|
|
511
1237
|
var packageName = "@plasius/gpu-model-runtime";
|
|
512
1238
|
var packageBootstrap = Object.freeze({
|
|
@@ -516,10 +1242,15 @@ var packageBootstrap = Object.freeze({
|
|
|
516
1242
|
});
|
|
517
1243
|
export {
|
|
518
1244
|
AdapterRegistry,
|
|
1245
|
+
DEFAULT_MODEL_MAX_BYTES,
|
|
519
1246
|
MemoryModelCache,
|
|
1247
|
+
ModelResidencyError,
|
|
1248
|
+
ModelResidencyManager,
|
|
1249
|
+
ModelResourceTooLargeError,
|
|
520
1250
|
ModelRuntime,
|
|
521
1251
|
PackageResourceResolver,
|
|
522
1252
|
createModelCacheKey,
|
|
1253
|
+
createModelResidencyKey,
|
|
523
1254
|
defaultSleep,
|
|
524
1255
|
fetchResource,
|
|
525
1256
|
packageBootstrap,
|