@plasius/gpu-model-runtime 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -3
- package/dist/index.cjs +636 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -1
- package/dist/index.d.ts +118 -1
- package/dist/index.js +633 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -22,9 +22,12 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AdapterRegistry: () => AdapterRegistry,
|
|
24
24
|
MemoryModelCache: () => MemoryModelCache,
|
|
25
|
+
ModelResidencyError: () => ModelResidencyError,
|
|
26
|
+
ModelResidencyManager: () => ModelResidencyManager,
|
|
25
27
|
ModelRuntime: () => ModelRuntime,
|
|
26
28
|
PackageResourceResolver: () => PackageResourceResolver,
|
|
27
29
|
createModelCacheKey: () => createModelCacheKey,
|
|
30
|
+
createModelResidencyKey: () => createModelResidencyKey,
|
|
28
31
|
defaultSleep: () => defaultSleep,
|
|
29
32
|
fetchResource: () => fetchResource,
|
|
30
33
|
packageBootstrap: () => packageBootstrap,
|
|
@@ -543,6 +546,636 @@ function ensureHttpUrl(value) {
|
|
|
543
546
|
return url.href;
|
|
544
547
|
}
|
|
545
548
|
|
|
549
|
+
// src/model-residency.ts
|
|
550
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
551
|
+
var PARTITION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u;
|
|
552
|
+
var ModelResidencyError = class extends Error {
|
|
553
|
+
code;
|
|
554
|
+
constructor(code, message) {
|
|
555
|
+
super(message);
|
|
556
|
+
this.name = "ModelResidencyError";
|
|
557
|
+
this.code = code;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
function requireSafeNonNegativeInteger(value, name) {
|
|
561
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
562
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function requirePositiveSafeInteger(value, name) {
|
|
566
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
567
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function requirePriority(priority) {
|
|
571
|
+
if (!Number.isFinite(priority) || priority < 0) {
|
|
572
|
+
throw new RangeError("priority must be a non-negative finite number");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function requireLod(lod) {
|
|
576
|
+
if (!Number.isInteger(lod) || lod < 0 || lod > 3) {
|
|
577
|
+
throw new RangeError("lod must be an integer from 0 to 3");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function createModelResidencyKey(assetRef, lod, partitionId) {
|
|
581
|
+
if (!SHA256_PATTERN.test(assetRef.contentHash)) {
|
|
582
|
+
throw new Error("ModelAssetRef.contentHash must be a lowercase SHA-256 value");
|
|
583
|
+
}
|
|
584
|
+
requireLod(lod);
|
|
585
|
+
if (partitionId !== void 0 && !PARTITION_ID_PATTERN.test(partitionId)) {
|
|
586
|
+
throw new Error("partitionId must be a bounded runtime token");
|
|
587
|
+
}
|
|
588
|
+
return `${assetRef.contentHash}:lod${lod}:${partitionId ?? "whole"}`;
|
|
589
|
+
}
|
|
590
|
+
function closedError() {
|
|
591
|
+
return new ModelResidencyError(
|
|
592
|
+
"manager-closed",
|
|
593
|
+
"The model residency manager is closed"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
function abortedError() {
|
|
597
|
+
return new ModelResidencyError(
|
|
598
|
+
"acquire-aborted",
|
|
599
|
+
"The model residency acquisition was aborted"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function signalIsAborted(signal) {
|
|
603
|
+
return signal?.aborted === true;
|
|
604
|
+
}
|
|
605
|
+
var ModelResidencyManager = class {
|
|
606
|
+
#budget;
|
|
607
|
+
#load;
|
|
608
|
+
#loadTimeoutMs;
|
|
609
|
+
#entries = /* @__PURE__ */ new Map();
|
|
610
|
+
#loadTasks = /* @__PURE__ */ new Set();
|
|
611
|
+
#disposalTasks = /* @__PURE__ */ new Set();
|
|
612
|
+
#metrics = {
|
|
613
|
+
acquireCount: 0,
|
|
614
|
+
cacheHits: 0,
|
|
615
|
+
deduplicatedAcquires: 0,
|
|
616
|
+
loadsStarted: 0,
|
|
617
|
+
loadsCompleted: 0,
|
|
618
|
+
loadsFailed: 0,
|
|
619
|
+
loadsAborted: 0,
|
|
620
|
+
loadsTimedOut: 0,
|
|
621
|
+
evictions: 0,
|
|
622
|
+
disposals: 0,
|
|
623
|
+
disposalFailures: 0
|
|
624
|
+
};
|
|
625
|
+
#nextIdentity = 1;
|
|
626
|
+
#clock = 1;
|
|
627
|
+
#inFlightLoads = 0;
|
|
628
|
+
#closed = false;
|
|
629
|
+
#admissionTail = Promise.resolve();
|
|
630
|
+
#shutdownPromise;
|
|
631
|
+
constructor(options) {
|
|
632
|
+
requirePositiveSafeInteger(
|
|
633
|
+
options.budget.maxCpuBytes,
|
|
634
|
+
"budget.maxCpuBytes"
|
|
635
|
+
);
|
|
636
|
+
requirePositiveSafeInteger(
|
|
637
|
+
options.budget.maxGpuBytes,
|
|
638
|
+
"budget.maxGpuBytes"
|
|
639
|
+
);
|
|
640
|
+
requirePositiveSafeInteger(
|
|
641
|
+
options.budget.maxInFlightLoads,
|
|
642
|
+
"budget.maxInFlightLoads"
|
|
643
|
+
);
|
|
644
|
+
if (options.loadTimeoutMs !== void 0) {
|
|
645
|
+
requirePositiveSafeInteger(options.loadTimeoutMs, "loadTimeoutMs");
|
|
646
|
+
}
|
|
647
|
+
this.#budget = Object.freeze({ ...options.budget });
|
|
648
|
+
this.#load = options.load;
|
|
649
|
+
this.#loadTimeoutMs = options.loadTimeoutMs;
|
|
650
|
+
}
|
|
651
|
+
/** Acquire one shared resource lease, loading it at most once per cache key. */
|
|
652
|
+
acquire(options) {
|
|
653
|
+
try {
|
|
654
|
+
if (this.#closed) throw closedError();
|
|
655
|
+
requirePriority(options.priority);
|
|
656
|
+
requireSafeNonNegativeInteger(
|
|
657
|
+
options.estimatedCpuBytes,
|
|
658
|
+
"estimatedCpuBytes"
|
|
659
|
+
);
|
|
660
|
+
requireSafeNonNegativeInteger(
|
|
661
|
+
options.estimatedGpuBytes,
|
|
662
|
+
"estimatedGpuBytes"
|
|
663
|
+
);
|
|
664
|
+
const key = createModelResidencyKey(
|
|
665
|
+
options.assetRef,
|
|
666
|
+
options.lod,
|
|
667
|
+
options.partitionId
|
|
668
|
+
);
|
|
669
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
670
|
+
this.#metrics.acquireCount += 1;
|
|
671
|
+
const existing = this.#entries.get(key);
|
|
672
|
+
if (existing !== void 0) return this.#acquireExisting(existing, options);
|
|
673
|
+
return this.#scheduleAdmission(key, options);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
return Promise.reject(error);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
#acquireExisting(entry, options) {
|
|
679
|
+
this.#metrics.deduplicatedAcquires += 1;
|
|
680
|
+
if (entry.state === "resident") {
|
|
681
|
+
this.#metrics.cacheHits += 1;
|
|
682
|
+
return Promise.resolve(
|
|
683
|
+
this.#createLease(
|
|
684
|
+
entry,
|
|
685
|
+
options.priority,
|
|
686
|
+
options.pinned ?? false
|
|
687
|
+
)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
return this.#addWaiter(entry, options);
|
|
691
|
+
}
|
|
692
|
+
#scheduleAdmission(key, options) {
|
|
693
|
+
let resolveAcquisition;
|
|
694
|
+
let rejectAcquisition;
|
|
695
|
+
const acquisition = new Promise(
|
|
696
|
+
(resolve, reject) => {
|
|
697
|
+
resolveAcquisition = resolve;
|
|
698
|
+
rejectAcquisition = reject;
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
const admission = this.#admissionTail.then(async () => {
|
|
702
|
+
if (this.#closed) throw closedError();
|
|
703
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
704
|
+
const existing = this.#entries.get(key);
|
|
705
|
+
if (existing !== void 0) {
|
|
706
|
+
void this.#acquireExisting(existing, options).then(
|
|
707
|
+
resolveAcquisition,
|
|
708
|
+
rejectAcquisition
|
|
709
|
+
);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await this.#ensureCapacityForReservation(
|
|
713
|
+
options.estimatedCpuBytes,
|
|
714
|
+
options.estimatedGpuBytes
|
|
715
|
+
);
|
|
716
|
+
if (this.#closed) throw closedError();
|
|
717
|
+
if (signalIsAborted(options.signal)) throw abortedError();
|
|
718
|
+
const entry = {
|
|
719
|
+
key,
|
|
720
|
+
assetRef: options.assetRef,
|
|
721
|
+
lod: options.lod,
|
|
722
|
+
...options.partitionId === void 0 ? {} : { partitionId: options.partitionId },
|
|
723
|
+
estimatedCpuBytes: options.estimatedCpuBytes,
|
|
724
|
+
estimatedGpuBytes: options.estimatedGpuBytes,
|
|
725
|
+
queueOrder: this.#nextIdentity++,
|
|
726
|
+
state: "queued",
|
|
727
|
+
priority: options.priority,
|
|
728
|
+
retainedPriority: options.priority,
|
|
729
|
+
lastUsed: this.#clock++,
|
|
730
|
+
abandoned: false,
|
|
731
|
+
loadSlotReleased: true,
|
|
732
|
+
waiters: /* @__PURE__ */ new Map(),
|
|
733
|
+
leases: /* @__PURE__ */ new Map()
|
|
734
|
+
};
|
|
735
|
+
this.#entries.set(key, entry);
|
|
736
|
+
void this.#addWaiter(entry, options).then(
|
|
737
|
+
resolveAcquisition,
|
|
738
|
+
rejectAcquisition
|
|
739
|
+
);
|
|
740
|
+
this.#pumpQueue();
|
|
741
|
+
});
|
|
742
|
+
this.#admissionTail = admission.catch(() => void 0);
|
|
743
|
+
void admission.catch(rejectAcquisition);
|
|
744
|
+
return acquisition;
|
|
745
|
+
}
|
|
746
|
+
/** Dispose every currently unreferenced, unpinned resident cache entry. */
|
|
747
|
+
async evictUnused() {
|
|
748
|
+
const tasks = [];
|
|
749
|
+
for (const entry of this.#evictionCandidates()) {
|
|
750
|
+
tasks.push(this.#evict(entry));
|
|
751
|
+
}
|
|
752
|
+
await Promise.all(tasks);
|
|
753
|
+
}
|
|
754
|
+
/** Wait until queued/loading work and its resulting disposal have settled. */
|
|
755
|
+
async waitForIdle() {
|
|
756
|
+
await this.#admissionTail;
|
|
757
|
+
do {
|
|
758
|
+
this.#pumpQueue();
|
|
759
|
+
const tasks = [...this.#loadTasks];
|
|
760
|
+
if (tasks.length > 0) {
|
|
761
|
+
await Promise.allSettled(tasks);
|
|
762
|
+
}
|
|
763
|
+
} while (this.#loadTasks.size > 0 || this.#queuedCount() > 0);
|
|
764
|
+
await Promise.allSettled([...this.#disposalTasks]);
|
|
765
|
+
}
|
|
766
|
+
/** Abort pending work and dispose every resident or late-completing resource. */
|
|
767
|
+
shutdown() {
|
|
768
|
+
if (this.#shutdownPromise !== void 0) return this.#shutdownPromise;
|
|
769
|
+
this.#closed = true;
|
|
770
|
+
const error = closedError();
|
|
771
|
+
const disposalTasks = [];
|
|
772
|
+
for (const entry of [...this.#entries.values()]) {
|
|
773
|
+
if (entry.state === "resident") {
|
|
774
|
+
this.#entries.delete(entry.key);
|
|
775
|
+
disposalTasks.push(this.#disposeEntry(entry));
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
this.#rejectWaiters(entry, error);
|
|
779
|
+
if (entry.state === "queued") {
|
|
780
|
+
this.#entries.delete(entry.key);
|
|
781
|
+
} else {
|
|
782
|
+
this.#abandonLoadingEntry(entry, error, false);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
this.#shutdownPromise = (async () => {
|
|
786
|
+
await Promise.allSettled(disposalTasks);
|
|
787
|
+
await this.waitForIdle();
|
|
788
|
+
await Promise.allSettled([...this.#disposalTasks]);
|
|
789
|
+
})();
|
|
790
|
+
return this.#shutdownPromise;
|
|
791
|
+
}
|
|
792
|
+
/** Return immutable byte, queue, cache, and lifetime metrics. */
|
|
793
|
+
snapshot() {
|
|
794
|
+
let cpuBytes = 0;
|
|
795
|
+
let gpuBytes = 0;
|
|
796
|
+
let reservedCpuBytes = 0;
|
|
797
|
+
let reservedGpuBytes = 0;
|
|
798
|
+
let referenceCount = 0;
|
|
799
|
+
let pinnedCount = 0;
|
|
800
|
+
let residentCount = 0;
|
|
801
|
+
let pendingCount = 0;
|
|
802
|
+
const entries = [];
|
|
803
|
+
for (const entry of this.#entries.values()) {
|
|
804
|
+
const pinned = [...entry.leases.values()].some(
|
|
805
|
+
(lease) => lease.active && lease.pinned
|
|
806
|
+
);
|
|
807
|
+
const references = [...entry.leases.values()].filter(
|
|
808
|
+
(lease) => lease.active
|
|
809
|
+
).length;
|
|
810
|
+
referenceCount += references;
|
|
811
|
+
if (pinned) pinnedCount += 1;
|
|
812
|
+
if (entry.state === "resident") {
|
|
813
|
+
residentCount += 1;
|
|
814
|
+
cpuBytes += entry.loaded.cpuBytes;
|
|
815
|
+
gpuBytes += entry.loaded.gpuBytes;
|
|
816
|
+
} else {
|
|
817
|
+
pendingCount += 1;
|
|
818
|
+
reservedCpuBytes += entry.estimatedCpuBytes;
|
|
819
|
+
reservedGpuBytes += entry.estimatedGpuBytes;
|
|
820
|
+
}
|
|
821
|
+
entries.push(
|
|
822
|
+
Object.freeze({
|
|
823
|
+
key: entry.key,
|
|
824
|
+
state: entry.state,
|
|
825
|
+
priority: entry.priority,
|
|
826
|
+
references,
|
|
827
|
+
pinned,
|
|
828
|
+
cpuBytes: entry.state === "resident" ? entry.loaded.cpuBytes : entry.estimatedCpuBytes,
|
|
829
|
+
gpuBytes: entry.state === "resident" ? entry.loaded.gpuBytes : entry.estimatedGpuBytes
|
|
830
|
+
})
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
entries.sort((left, right) => left.key.localeCompare(right.key));
|
|
834
|
+
return Object.freeze({
|
|
835
|
+
...this.#budget,
|
|
836
|
+
cpuBytes,
|
|
837
|
+
gpuBytes,
|
|
838
|
+
reservedCpuBytes,
|
|
839
|
+
reservedGpuBytes,
|
|
840
|
+
residentCount,
|
|
841
|
+
pendingCount,
|
|
842
|
+
referenceCount,
|
|
843
|
+
pinnedCount,
|
|
844
|
+
inFlightLoads: this.#inFlightLoads,
|
|
845
|
+
queuedLoads: this.#queuedCount(),
|
|
846
|
+
...this.#metrics,
|
|
847
|
+
residentKeys: Object.freeze(
|
|
848
|
+
entries.filter((entry) => entry.state === "resident").map((entry) => entry.key)
|
|
849
|
+
),
|
|
850
|
+
entries: Object.freeze(entries)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
#addWaiter(entry, options) {
|
|
854
|
+
const id = this.#nextIdentity++;
|
|
855
|
+
let resolvePromise;
|
|
856
|
+
let rejectPromise;
|
|
857
|
+
const promise = new Promise(
|
|
858
|
+
(resolve, reject) => {
|
|
859
|
+
resolvePromise = resolve;
|
|
860
|
+
rejectPromise = reject;
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
const waiter = {
|
|
864
|
+
id,
|
|
865
|
+
priority: options.priority,
|
|
866
|
+
pinned: options.pinned ?? false,
|
|
867
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
868
|
+
resolve: resolvePromise,
|
|
869
|
+
reject: rejectPromise
|
|
870
|
+
};
|
|
871
|
+
if (options.signal !== void 0) {
|
|
872
|
+
waiter.onAbort = () => this.#abortWaiter(entry, waiter);
|
|
873
|
+
options.signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
874
|
+
}
|
|
875
|
+
entry.waiters.set(id, waiter);
|
|
876
|
+
entry.retainedPriority = Math.max(entry.retainedPriority, options.priority);
|
|
877
|
+
this.#recalculatePriority(entry);
|
|
878
|
+
return promise;
|
|
879
|
+
}
|
|
880
|
+
#abortWaiter(entry, waiter) {
|
|
881
|
+
entry.waiters.delete(waiter.id);
|
|
882
|
+
this.#detachWaiterSignal(waiter);
|
|
883
|
+
waiter.reject(abortedError());
|
|
884
|
+
this.#recalculatePriority(entry);
|
|
885
|
+
if (entry.waiters.size > 0) return;
|
|
886
|
+
if (entry.state === "queued") {
|
|
887
|
+
this.#entries.delete(entry.key);
|
|
888
|
+
this.#pumpQueue();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
this.#abandonLoadingEntry(entry, abortedError(), true);
|
|
892
|
+
}
|
|
893
|
+
#createLease(entry, priority, pinned) {
|
|
894
|
+
const id = this.#nextIdentity++;
|
|
895
|
+
const record = {
|
|
896
|
+
id,
|
|
897
|
+
active: true,
|
|
898
|
+
priority,
|
|
899
|
+
pinned
|
|
900
|
+
};
|
|
901
|
+
entry.leases.set(id, record);
|
|
902
|
+
entry.retainedPriority = priority;
|
|
903
|
+
entry.lastUsed = this.#clock++;
|
|
904
|
+
this.#recalculatePriority(entry);
|
|
905
|
+
const lease = {
|
|
906
|
+
key: entry.key,
|
|
907
|
+
assetRef: entry.assetRef,
|
|
908
|
+
lod: entry.lod,
|
|
909
|
+
...entry.partitionId === void 0 ? {} : { partitionId: entry.partitionId },
|
|
910
|
+
resource: entry.loaded.resource,
|
|
911
|
+
release: () => {
|
|
912
|
+
if (!record.active) return;
|
|
913
|
+
record.active = false;
|
|
914
|
+
entry.leases.delete(id);
|
|
915
|
+
entry.lastUsed = this.#clock++;
|
|
916
|
+
this.#recalculatePriority(entry);
|
|
917
|
+
},
|
|
918
|
+
setPriority: (nextPriority) => {
|
|
919
|
+
if (!record.active || this.#entries.get(entry.key) !== entry) return;
|
|
920
|
+
requirePriority(nextPriority);
|
|
921
|
+
record.priority = nextPriority;
|
|
922
|
+
entry.retainedPriority = nextPriority;
|
|
923
|
+
entry.lastUsed = this.#clock++;
|
|
924
|
+
this.#recalculatePriority(entry);
|
|
925
|
+
},
|
|
926
|
+
setPinned: (nextPinned) => {
|
|
927
|
+
if (!record.active || this.#entries.get(entry.key) !== entry) return;
|
|
928
|
+
record.pinned = nextPinned;
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
return Object.freeze(lease);
|
|
932
|
+
}
|
|
933
|
+
#recalculatePriority(entry) {
|
|
934
|
+
let priority = entry.retainedPriority;
|
|
935
|
+
for (const waiter of entry.waiters.values()) {
|
|
936
|
+
priority = Math.max(priority, waiter.priority);
|
|
937
|
+
}
|
|
938
|
+
for (const lease of entry.leases.values()) {
|
|
939
|
+
priority = Math.max(priority, lease.priority);
|
|
940
|
+
}
|
|
941
|
+
entry.priority = priority;
|
|
942
|
+
}
|
|
943
|
+
#pumpQueue() {
|
|
944
|
+
if (this.#closed) return;
|
|
945
|
+
while (this.#inFlightLoads < this.#budget.maxInFlightLoads) {
|
|
946
|
+
const candidate = [...this.#entries.values()].filter(
|
|
947
|
+
(entry) => entry.state === "queued" && !entry.abandoned && entry.waiters.size > 0
|
|
948
|
+
).sort(
|
|
949
|
+
(left, right) => right.priority - left.priority || left.queueOrder - right.queueOrder
|
|
950
|
+
)[0];
|
|
951
|
+
if (candidate === void 0) return;
|
|
952
|
+
this.#startLoad(candidate);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
#startLoad(entry) {
|
|
956
|
+
entry.state = "loading";
|
|
957
|
+
entry.loadSlotReleased = false;
|
|
958
|
+
entry.controller = new AbortController();
|
|
959
|
+
this.#inFlightLoads += 1;
|
|
960
|
+
this.#metrics.loadsStarted += 1;
|
|
961
|
+
if (this.#loadTimeoutMs !== void 0) {
|
|
962
|
+
entry.timeout = setTimeout(() => {
|
|
963
|
+
this.#metrics.loadsTimedOut += 1;
|
|
964
|
+
this.#abandonLoadingEntry(
|
|
965
|
+
entry,
|
|
966
|
+
new ModelResidencyError(
|
|
967
|
+
"load-timeout",
|
|
968
|
+
`Model load exceeded ${this.#loadTimeoutMs} ms`
|
|
969
|
+
),
|
|
970
|
+
true
|
|
971
|
+
);
|
|
972
|
+
}, this.#loadTimeoutMs);
|
|
973
|
+
}
|
|
974
|
+
let adapterPromise;
|
|
975
|
+
try {
|
|
976
|
+
adapterPromise = this.#load({
|
|
977
|
+
assetRef: entry.assetRef,
|
|
978
|
+
lod: entry.lod,
|
|
979
|
+
...entry.partitionId === void 0 ? {} : { partitionId: entry.partitionId },
|
|
980
|
+
priority: entry.priority,
|
|
981
|
+
signal: entry.controller.signal
|
|
982
|
+
});
|
|
983
|
+
} catch (error) {
|
|
984
|
+
adapterPromise = Promise.reject(error);
|
|
985
|
+
}
|
|
986
|
+
const task = Promise.resolve(adapterPromise).then((result) => this.#completeLoad(entry, result)).catch((error) => this.#failLoad(entry, error)).finally(() => {
|
|
987
|
+
if (entry.timeout !== void 0) clearTimeout(entry.timeout);
|
|
988
|
+
entry.timeout = void 0;
|
|
989
|
+
entry.controller = void 0;
|
|
990
|
+
this.#releaseLoadSlot(entry);
|
|
991
|
+
entry.loadTask = void 0;
|
|
992
|
+
});
|
|
993
|
+
entry.loadTask = task;
|
|
994
|
+
this.#loadTasks.add(task);
|
|
995
|
+
}
|
|
996
|
+
async #completeLoad(entry, result) {
|
|
997
|
+
if (entry.abandoned) {
|
|
998
|
+
await this.#disposeLoaded(result);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
this.#validateLoadedResource(entry, result);
|
|
1003
|
+
const evictions = this.#ensureCapacityForLoadedResult(entry, result);
|
|
1004
|
+
await Promise.all(evictions);
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
await this.#disposeLoaded(result);
|
|
1007
|
+
this.#entries.delete(entry.key);
|
|
1008
|
+
this.#rejectWaiters(entry, error);
|
|
1009
|
+
this.#metrics.loadsFailed += 1;
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
entry.state = "resident";
|
|
1013
|
+
entry.loaded = result;
|
|
1014
|
+
entry.lastUsed = this.#clock++;
|
|
1015
|
+
this.#metrics.loadsCompleted += 1;
|
|
1016
|
+
const waiters = [...entry.waiters.values()];
|
|
1017
|
+
entry.waiters.clear();
|
|
1018
|
+
for (const waiter of waiters) {
|
|
1019
|
+
this.#detachWaiterSignal(waiter);
|
|
1020
|
+
waiter.resolve(this.#createLease(entry, waiter.priority, waiter.pinned));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
#failLoad(entry, error) {
|
|
1024
|
+
if (entry.abandoned) return;
|
|
1025
|
+
this.#entries.delete(entry.key);
|
|
1026
|
+
this.#metrics.loadsFailed += 1;
|
|
1027
|
+
this.#rejectWaiters(entry, error);
|
|
1028
|
+
}
|
|
1029
|
+
#abandonLoadingEntry(entry, error, rejectWaiters) {
|
|
1030
|
+
entry.abandoned = true;
|
|
1031
|
+
this.#metrics.loadsAborted += 1;
|
|
1032
|
+
if (entry.timeout !== void 0) clearTimeout(entry.timeout);
|
|
1033
|
+
entry.timeout = void 0;
|
|
1034
|
+
this.#entries.delete(entry.key);
|
|
1035
|
+
if (rejectWaiters) this.#rejectWaiters(entry, error);
|
|
1036
|
+
entry.controller?.abort(error);
|
|
1037
|
+
this.#releaseLoadSlot(entry);
|
|
1038
|
+
}
|
|
1039
|
+
#releaseLoadSlot(entry) {
|
|
1040
|
+
if (entry.loadSlotReleased) return;
|
|
1041
|
+
entry.loadSlotReleased = true;
|
|
1042
|
+
this.#inFlightLoads -= 1;
|
|
1043
|
+
this.#loadTasks.delete(entry.loadTask);
|
|
1044
|
+
this.#pumpQueue();
|
|
1045
|
+
}
|
|
1046
|
+
#rejectWaiters(entry, error) {
|
|
1047
|
+
const waiters = [...entry.waiters.values()];
|
|
1048
|
+
entry.waiters.clear();
|
|
1049
|
+
for (const waiter of waiters) {
|
|
1050
|
+
this.#detachWaiterSignal(waiter);
|
|
1051
|
+
waiter.reject(error);
|
|
1052
|
+
}
|
|
1053
|
+
this.#recalculatePriority(entry);
|
|
1054
|
+
}
|
|
1055
|
+
#detachWaiterSignal(waiter) {
|
|
1056
|
+
if (waiter.signal !== void 0 && waiter.onAbort !== void 0) {
|
|
1057
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1058
|
+
}
|
|
1059
|
+
waiter.onAbort = void 0;
|
|
1060
|
+
}
|
|
1061
|
+
#validateLoadedResource(entry, result) {
|
|
1062
|
+
if (result === null || typeof result !== "object") {
|
|
1063
|
+
throw new ModelResidencyError(
|
|
1064
|
+
"invalid-load-result",
|
|
1065
|
+
"The model loader returned an invalid resource"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof result.dispose !== "function") {
|
|
1069
|
+
throw new ModelResidencyError(
|
|
1070
|
+
"invalid-load-result",
|
|
1071
|
+
"The model loader must return a disposer"
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
requireSafeNonNegativeInteger(result.cpuBytes, "loaded.cpuBytes");
|
|
1075
|
+
requireSafeNonNegativeInteger(result.gpuBytes, "loaded.gpuBytes");
|
|
1076
|
+
if (result.contentHash !== entry.assetRef.contentHash) {
|
|
1077
|
+
throw new ModelResidencyError(
|
|
1078
|
+
"content-hash-mismatch",
|
|
1079
|
+
"The loaded model content hash does not match its canonical reference"
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async #ensureCapacityForReservation(cpuBytes, gpuBytes) {
|
|
1084
|
+
if (cpuBytes > this.#budget.maxCpuBytes || gpuBytes > this.#budget.maxGpuBytes) {
|
|
1085
|
+
throw this.#budgetError();
|
|
1086
|
+
}
|
|
1087
|
+
let usage = this.#totalAccountedBytes();
|
|
1088
|
+
for (const candidate of this.#evictionCandidates()) {
|
|
1089
|
+
if (usage.cpu + cpuBytes <= this.#budget.maxCpuBytes && usage.gpu + gpuBytes <= this.#budget.maxGpuBytes) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
await this.#evict(candidate);
|
|
1093
|
+
usage = this.#totalAccountedBytes();
|
|
1094
|
+
}
|
|
1095
|
+
if (usage.cpu + cpuBytes > this.#budget.maxCpuBytes || usage.gpu + gpuBytes > this.#budget.maxGpuBytes) {
|
|
1096
|
+
throw this.#budgetError();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
#ensureCapacityForLoadedResult(loadingEntry, result) {
|
|
1100
|
+
const usage = this.#totalAccountedBytes();
|
|
1101
|
+
usage.cpu += result.cpuBytes - loadingEntry.estimatedCpuBytes;
|
|
1102
|
+
usage.gpu += result.gpuBytes - loadingEntry.estimatedGpuBytes;
|
|
1103
|
+
const disposals = [];
|
|
1104
|
+
for (const candidate of this.#evictionCandidates()) {
|
|
1105
|
+
if (usage.cpu <= this.#budget.maxCpuBytes && usage.gpu <= this.#budget.maxGpuBytes) {
|
|
1106
|
+
return disposals;
|
|
1107
|
+
}
|
|
1108
|
+
usage.cpu -= candidate.loaded.cpuBytes;
|
|
1109
|
+
usage.gpu -= candidate.loaded.gpuBytes;
|
|
1110
|
+
disposals.push(this.#evict(candidate));
|
|
1111
|
+
}
|
|
1112
|
+
if (usage.cpu > this.#budget.maxCpuBytes || usage.gpu > this.#budget.maxGpuBytes) {
|
|
1113
|
+
throw this.#budgetError();
|
|
1114
|
+
}
|
|
1115
|
+
return disposals;
|
|
1116
|
+
}
|
|
1117
|
+
#budgetError() {
|
|
1118
|
+
return new ModelResidencyError(
|
|
1119
|
+
"budget-exceeded",
|
|
1120
|
+
"Pinned, referenced, or pending models leave insufficient residency budget"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
#totalAccountedBytes() {
|
|
1124
|
+
let cpu = 0;
|
|
1125
|
+
let gpu = 0;
|
|
1126
|
+
for (const entry of this.#entries.values()) {
|
|
1127
|
+
if (entry.state === "resident") {
|
|
1128
|
+
cpu += entry.loaded.cpuBytes;
|
|
1129
|
+
gpu += entry.loaded.gpuBytes;
|
|
1130
|
+
} else {
|
|
1131
|
+
cpu += entry.estimatedCpuBytes;
|
|
1132
|
+
gpu += entry.estimatedGpuBytes;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return { cpu, gpu };
|
|
1136
|
+
}
|
|
1137
|
+
#evictionCandidates() {
|
|
1138
|
+
return [...this.#entries.values()].filter(
|
|
1139
|
+
(entry) => entry.state === "resident" && entry.leases.size === 0
|
|
1140
|
+
).sort(
|
|
1141
|
+
(left, right) => left.priority - right.priority || left.lastUsed - right.lastUsed
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
#evict(entry) {
|
|
1145
|
+
this.#entries.delete(entry.key);
|
|
1146
|
+
this.#metrics.evictions += 1;
|
|
1147
|
+
return this.#disposeEntry(entry);
|
|
1148
|
+
}
|
|
1149
|
+
#disposeEntry(entry) {
|
|
1150
|
+
const result = entry.loaded;
|
|
1151
|
+
entry.loaded = void 0;
|
|
1152
|
+
return this.#disposeLoaded(result);
|
|
1153
|
+
}
|
|
1154
|
+
#disposeLoaded(result) {
|
|
1155
|
+
this.#metrics.disposals += 1;
|
|
1156
|
+
let disposal;
|
|
1157
|
+
try {
|
|
1158
|
+
disposal = Promise.resolve(result.dispose());
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
disposal = Promise.reject(error);
|
|
1161
|
+
}
|
|
1162
|
+
const tracked = disposal.catch(() => {
|
|
1163
|
+
this.#metrics.disposalFailures += 1;
|
|
1164
|
+
}).finally(() => {
|
|
1165
|
+
this.#disposalTasks.delete(tracked);
|
|
1166
|
+
});
|
|
1167
|
+
this.#disposalTasks.add(tracked);
|
|
1168
|
+
return tracked;
|
|
1169
|
+
}
|
|
1170
|
+
#queuedCount() {
|
|
1171
|
+
let count = 0;
|
|
1172
|
+
for (const entry of this.#entries.values()) {
|
|
1173
|
+
if (entry.state === "queued") count += 1;
|
|
1174
|
+
}
|
|
1175
|
+
return count;
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
|
|
546
1179
|
// src/index.ts
|
|
547
1180
|
var packageName = "@plasius/gpu-model-runtime";
|
|
548
1181
|
var packageBootstrap = Object.freeze({
|
|
@@ -554,9 +1187,12 @@ var packageBootstrap = Object.freeze({
|
|
|
554
1187
|
0 && (module.exports = {
|
|
555
1188
|
AdapterRegistry,
|
|
556
1189
|
MemoryModelCache,
|
|
1190
|
+
ModelResidencyError,
|
|
1191
|
+
ModelResidencyManager,
|
|
557
1192
|
ModelRuntime,
|
|
558
1193
|
PackageResourceResolver,
|
|
559
1194
|
createModelCacheKey,
|
|
1195
|
+
createModelResidencyKey,
|
|
560
1196
|
defaultSleep,
|
|
561
1197
|
fetchResource,
|
|
562
1198
|
packageBootstrap,
|