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