@qvac/harness 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -1
- package/README.md +49 -0
- package/dist-lib/index.d.ts +31 -2
- package/dist-lib/index.js +365 -53
- package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-bare.js +22 -25
- package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
- package/dist-lib/lib/audio-transcode-node.js +1 -3
- package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
- package/dist-lib/lib/capabilities/audio-resample.js +7 -8
- package/dist-lib/lib/capabilities/completion.d.ts +5 -2
- package/dist-lib/lib/capabilities/completion.js +22 -4
- package/dist-lib/lib/capabilities/download.d.ts +3 -1
- package/dist-lib/lib/capabilities/download.js +1 -1
- package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
- package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
- package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
- package/dist-lib/lib/capabilities/voice.d.ts +5 -1
- package/dist-lib/lib/capabilities/voice.js +94 -26
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +14 -1
- package/dist-lib/lib/contract-types.d.ts +10 -0
- package/dist-lib/lib/models/download-gate.d.ts +20 -0
- package/dist-lib/lib/models/download-gate.js +73 -0
- package/dist-lib/lib/models/memory-cap.d.ts +13 -0
- package/dist-lib/lib/models/memory-cap.js +21 -0
- package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
- package/dist-lib/lib/models/model-catalog-constants.js +9 -1
- package/dist-lib/lib/models/model-catalog.d.ts +6 -2
- package/dist-lib/lib/models/model-catalog.js +35 -3
- package/dist-lib/lib/models/model-handle.d.ts +8 -1
- package/dist-lib/lib/models/model-handle.js +20 -2
- package/dist-lib/lib/models/model-list.d.ts +8 -1
- package/dist-lib/lib/models/model-list.js +23 -0
- package/dist-lib/lib/models/model-pool.d.ts +33 -5
- package/dist-lib/lib/models/model-pool.js +97 -29
- package/dist-lib/lib/serve.js +18 -2
- package/dist-lib/lib/tools/generate-image.d.ts +4 -2
- package/dist-lib/lib/tools/generate-image.js +5 -2
- package/dist-lib/lib/tools/generate-music.d.ts +4 -2
- package/dist-lib/lib/tools/generate-music.js +5 -2
- package/dist-lib/lib/tools/translate.d.ts +3 -1
- package/dist-lib/lib/tools/translate.js +6 -1
- package/dist-lib/schema/harness.js +41 -3
- package/dist-lib/spec/bind.js +9 -0
- package/dist-lib/spec/calls.js +3 -0
- package/dist-lib/spec/hrpc/hrpc.json +39 -0
- package/dist-lib/spec/hrpc/index.js +39 -3
- package/dist-lib/spec/hrpc/messages.js +93 -10
- package/dist-lib/spec/hyperschema/index.js +93 -10
- package/dist-lib/spec/hyperschema/schema.json +57 -1
- package/dist-lib/spec/hyperschema/types.d.ts +19 -2
- package/package.json +4 -4
package/dist-lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import ScopeLock from 'scope-lock';
|
|
2
3
|
import { platform, totalmem } from 'os';
|
|
3
4
|
import process from 'process';
|
|
4
5
|
import ReadyResource from 'ready-resource';
|
|
@@ -17,14 +18,15 @@ import { download } from './lib/capabilities/download.js';
|
|
|
17
18
|
import { createEmbedLane } from './lib/capabilities/embed.js';
|
|
18
19
|
import { deleteAllKvCaches, deleteKvCacheKey } from './lib/capabilities/kv-cache.js';
|
|
19
20
|
import { pcmForTranscription } from './lib/capabilities/audio-resample.js';
|
|
20
|
-
import {
|
|
21
|
-
import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
|
|
21
|
+
import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeAudio, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
|
|
22
22
|
import { pushQueue } from './lib/queue.js';
|
|
23
23
|
import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
|
|
24
|
-
import { audioGenDownloadMetadata, categoryModelConfig, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
|
|
25
|
-
import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
|
|
24
|
+
import { audioGenDownloadMetadata, categoryModelConfig, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, assetBytes, curatedModelAsset, customContextLength, customModelSrc, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, catalogModelName, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
|
|
25
|
+
import { createModelStatusCache, customModelRows, invalidateModelStatusCache, isModelSourceCached, mergeCustomRows, listModels } from './lib/models/model-list.js';
|
|
26
|
+
import { createDownloadGate } from './lib/models/download-gate.js';
|
|
26
27
|
import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
|
|
27
|
-
import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
|
|
28
|
+
import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictIdleWithin, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledEntries, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
|
|
29
|
+
import { memoryCapBytes, nextVictim, VictimRank } from './lib/models/memory-cap.js';
|
|
28
30
|
import { defaultContextSize } from './lib/models/context.js';
|
|
29
31
|
import { resolveMainGpu } from './lib/models/gpu-selection.js';
|
|
30
32
|
import { createIdleReaper } from './lib/models/idle-unload.js';
|
|
@@ -41,7 +43,7 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
|
|
|
41
43
|
import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
|
|
42
44
|
import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
|
|
43
45
|
import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
|
|
44
|
-
import { errorMessage } from './lib/guards.js';
|
|
46
|
+
import { errorMessage, isRecord } from './lib/guards.js';
|
|
45
47
|
import { hlog } from './lib/log.js';
|
|
46
48
|
import { harness } from './spec/index.js';
|
|
47
49
|
import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
|
|
@@ -75,7 +77,6 @@ export { harnessHooks } from './lib/hooks/registry.js';
|
|
|
75
77
|
export { EgressGuardHook, SkillCatalogGuardHook } from './lib/hooks/built-in/index.js';
|
|
76
78
|
const HarnessState = harness.getEnum('@harness/state');
|
|
77
79
|
const BIN_VERSION_PROBE_TTL_MS = 45_000;
|
|
78
|
-
const TRANSCRIBE_STREAM_MIN_BYTES = 16_000 * 2 * 60;
|
|
79
80
|
const TRANSCRIPTION_MODEL_LABEL = 'transcription model';
|
|
80
81
|
const DIFFUSION_EVICT_WAIT_MS = 15_000;
|
|
81
82
|
const MUSIC_EVICT_WAIT_MS = 15_000;
|
|
@@ -91,6 +92,23 @@ const TTS_MODEL_CONFIG = {
|
|
|
91
92
|
ttsSpeed: 1.175,
|
|
92
93
|
ttsNumInferenceSteps: 5
|
|
93
94
|
};
|
|
95
|
+
function fitSrc(modelSrc) {
|
|
96
|
+
if (typeof modelSrc === 'string')
|
|
97
|
+
return modelSrc;
|
|
98
|
+
const { src, name, sha256Checksum } = modelSrc;
|
|
99
|
+
return { src, name, sha256Checksum };
|
|
100
|
+
}
|
|
101
|
+
function downloadBytes(modelSrc) {
|
|
102
|
+
if (typeof modelSrc === 'string') {
|
|
103
|
+
return assetBytes(modelSrc);
|
|
104
|
+
}
|
|
105
|
+
return isRecord(modelSrc) && typeof modelSrc.expectedSize === 'number'
|
|
106
|
+
? modelSrc.expectedSize
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
function stagesBytes(srcs) {
|
|
110
|
+
return srcs.reduce((sum, src) => sum + (assetBytes(src) ?? 0), 0);
|
|
111
|
+
}
|
|
94
112
|
export class Harness extends ReadyResource {
|
|
95
113
|
_tools;
|
|
96
114
|
_sdk;
|
|
@@ -98,6 +116,8 @@ export class Harness extends ReadyResource {
|
|
|
98
116
|
_pinned = new Set();
|
|
99
117
|
_replying = [];
|
|
100
118
|
_modelStatus = createModelStatusCache();
|
|
119
|
+
_downloads;
|
|
120
|
+
_hold;
|
|
101
121
|
_models;
|
|
102
122
|
_embedModels;
|
|
103
123
|
_retiredEmbedModels;
|
|
@@ -140,6 +160,11 @@ export class Harness extends ReadyResource {
|
|
|
140
160
|
_capabilityInflight = null;
|
|
141
161
|
_modelFaults = new Map();
|
|
142
162
|
_idle;
|
|
163
|
+
_sizeLoads = false;
|
|
164
|
+
_capBytes = null;
|
|
165
|
+
_reserved = new Map();
|
|
166
|
+
_admission = new ScopeLock();
|
|
167
|
+
_byHand = new Set();
|
|
143
168
|
constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
|
|
144
169
|
super();
|
|
145
170
|
this._sdk = sdkLoader ?? createSdkLoader();
|
|
@@ -164,11 +189,19 @@ export class Harness extends ReadyResource {
|
|
|
164
189
|
device: embedConfig.device,
|
|
165
190
|
gpuLayers: embedConfig.gpuLayers
|
|
166
191
|
});
|
|
192
|
+
this._downloads = createDownloadGate({
|
|
193
|
+
isCached: (model, addon) => this._isModelCached(model, addon),
|
|
194
|
+
onHeld: ({ model, addon }) => this._notifyModel({ status: 'held', model, addon }),
|
|
195
|
+
onDropped: ({ model, addon }) => this._notifyModel({ status: 'error', model, addon, message: 'aborted' })
|
|
196
|
+
});
|
|
197
|
+
const hold = (model, addon, signal) => this._downloads.hold(catalogModelName(model, addon), addon, signal);
|
|
198
|
+
this._hold = hold;
|
|
167
199
|
const imageTransfers = new Map();
|
|
168
200
|
const image = typeof imageModel === 'string' ? { model: imageModel } : imageModel;
|
|
169
201
|
this._imageRuntime = image
|
|
170
202
|
? createDiffusionRuntime({
|
|
171
203
|
...image,
|
|
204
|
+
hold,
|
|
172
205
|
loads: this._loads,
|
|
173
206
|
loadSdk: () => this._loadDiffusionSdk(),
|
|
174
207
|
onProgress: (model, progress) => {
|
|
@@ -191,9 +224,13 @@ export class Harness extends ReadyResource {
|
|
|
191
224
|
invalidateModelStatusCache(this._modelStatus);
|
|
192
225
|
this.emit('model', { ...transfer, status: 'downloaded' });
|
|
193
226
|
},
|
|
194
|
-
onBeforeLoad: async (signal) => {
|
|
227
|
+
onBeforeLoad: async (signal, srcs) => {
|
|
195
228
|
if (this._models.destroyed)
|
|
196
229
|
return;
|
|
230
|
+
if (this._capBytes !== null) {
|
|
231
|
+
await this._makeRoomForGenerator(srcs, signal);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
197
234
|
const evicted = await awaitModelCapacity(this._models, {
|
|
198
235
|
cap: 1,
|
|
199
236
|
deadlineMs: DIFFUSION_EVICT_WAIT_MS,
|
|
@@ -212,12 +249,17 @@ export class Harness extends ReadyResource {
|
|
|
212
249
|
this._musicRuntime = music
|
|
213
250
|
? createMusicRuntime({
|
|
214
251
|
...music,
|
|
252
|
+
hold,
|
|
215
253
|
loads: this._loads,
|
|
216
254
|
loadSdk: () => this._loadMusicSdk(),
|
|
217
255
|
onProgress: (model, progress) => this.emit('model', progressState(model, progress)),
|
|
218
|
-
onBeforeLoad: async (signal) => {
|
|
256
|
+
onBeforeLoad: async (signal, srcs) => {
|
|
219
257
|
if (this._models.destroyed)
|
|
220
258
|
return;
|
|
259
|
+
if (this._capBytes !== null) {
|
|
260
|
+
await this._makeRoomForGenerator(srcs, signal);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
221
263
|
const evicted = await awaitModelCapacity(this._models, {
|
|
222
264
|
cap: 1,
|
|
223
265
|
deadlineMs: MUSIC_EVICT_WAIT_MS,
|
|
@@ -233,6 +275,7 @@ export class Harness extends ReadyResource {
|
|
|
233
275
|
})
|
|
234
276
|
: null;
|
|
235
277
|
this._translationRuntime = createTranslationRuntime({
|
|
278
|
+
hold,
|
|
236
279
|
loads: this._loads,
|
|
237
280
|
loadSdk: () => this._loadTranslationSdk(),
|
|
238
281
|
onProgress: (model, progress) => this.emit('model', progressState(model, progress))
|
|
@@ -523,13 +566,45 @@ export class Harness extends ReadyResource {
|
|
|
523
566
|
setModelIdleTimeout(ms) {
|
|
524
567
|
this._idle.configure(ms);
|
|
525
568
|
}
|
|
569
|
+
async setModelMemoryCap(percent) {
|
|
570
|
+
this._sizeLoads = true;
|
|
571
|
+
const { mobile, memoryBudget } = await this.capability();
|
|
572
|
+
this._capBytes =
|
|
573
|
+
percent > 0 && !mobile && memoryBudget
|
|
574
|
+
? memoryCapBytes(percent, memoryBudget.availableBytes)
|
|
575
|
+
: null;
|
|
576
|
+
hlog('memory cap', { percent, capBytes: this._capBytes });
|
|
577
|
+
if (this._capBytes !== null) {
|
|
578
|
+
await this._busy(() => this._admit(() => this._makeRoom(0, { generators: true })));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async memoryUsage() {
|
|
582
|
+
return { usedBytes: this._settledBytes() };
|
|
583
|
+
}
|
|
584
|
+
setDownloadsAllowed({ allowed }) {
|
|
585
|
+
this._downloads.setAllowed(allowed);
|
|
586
|
+
}
|
|
587
|
+
async _isModelCached(model, addon) {
|
|
588
|
+
const row = (await this.list({ type: addon })).find((entry) => entry.name === model);
|
|
589
|
+
if (row) {
|
|
590
|
+
return row.isCached;
|
|
591
|
+
}
|
|
592
|
+
const sdk = await this._sdk.load();
|
|
593
|
+
return isModelSourceCached(sdk, { model, addon, modelsDir: this._modelsDir });
|
|
594
|
+
}
|
|
526
595
|
previewModelSource(context) {
|
|
527
596
|
return previewModelSource(context, { assessFit: (query) => this._assessFit(query) });
|
|
528
597
|
}
|
|
529
|
-
async _assessFit({ sha256Checksum, name, contextTokens }) {
|
|
598
|
+
async _assessFit({ src, sha256Checksum, name, contextTokens }) {
|
|
530
599
|
const sdk = await this._sdk.load();
|
|
531
600
|
const result = await sdk.assessModelFit({
|
|
532
|
-
models: [
|
|
601
|
+
models: [
|
|
602
|
+
{
|
|
603
|
+
modelSrc: { src, sha256Checksum, name },
|
|
604
|
+
modelType: 'llm',
|
|
605
|
+
modelConfig: { ctx_size: contextTokens }
|
|
606
|
+
}
|
|
607
|
+
]
|
|
533
608
|
});
|
|
534
609
|
const model = result.models[0];
|
|
535
610
|
return {
|
|
@@ -644,7 +719,9 @@ export class Harness extends ReadyResource {
|
|
|
644
719
|
const loadSdk = this._sdk.load;
|
|
645
720
|
return createModelPool(loadSdk, (state) => this._notifyModel(state), {
|
|
646
721
|
deviceConfig: this._deviceConfig(),
|
|
647
|
-
loads: this._loads
|
|
722
|
+
loads: this._loads,
|
|
723
|
+
beforeLoad: (load) => this._beforeLoad(load),
|
|
724
|
+
hold: this._hold
|
|
648
725
|
});
|
|
649
726
|
}
|
|
650
727
|
_createEmbedPool() {
|
|
@@ -653,7 +730,9 @@ export class Harness extends ReadyResource {
|
|
|
653
730
|
faults: this._embedFaults,
|
|
654
731
|
batchSize: this._embedBatchSize,
|
|
655
732
|
openclCacheDir,
|
|
656
|
-
loads: this._loads
|
|
733
|
+
loads: this._loads,
|
|
734
|
+
beforeLoad: (load) => this._beforeLoad(load),
|
|
735
|
+
hold: this._hold
|
|
657
736
|
});
|
|
658
737
|
}
|
|
659
738
|
_createTtsPool() {
|
|
@@ -663,7 +742,9 @@ export class Harness extends ReadyResource {
|
|
|
663
742
|
addon: 'tts',
|
|
664
743
|
modelConfig: TTS_MODEL_CONFIG,
|
|
665
744
|
modelConfigFor: categoryModelConfig,
|
|
666
|
-
loads: this._loads
|
|
745
|
+
loads: this._loads,
|
|
746
|
+
beforeLoad: (load) => this._beforeLoad(load),
|
|
747
|
+
hold: this._hold
|
|
667
748
|
});
|
|
668
749
|
}
|
|
669
750
|
_createTranscriptionPool() {
|
|
@@ -673,7 +754,9 @@ export class Harness extends ReadyResource {
|
|
|
673
754
|
addon: 'parakeet',
|
|
674
755
|
modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
|
|
675
756
|
openclCacheDir,
|
|
676
|
-
loads: this._loads
|
|
757
|
+
loads: this._loads,
|
|
758
|
+
beforeLoad: (load) => this._beforeLoad(load),
|
|
759
|
+
hold: this._hold
|
|
677
760
|
});
|
|
678
761
|
}
|
|
679
762
|
_notifyModel(state) {
|
|
@@ -703,6 +786,10 @@ export class Harness extends ReadyResource {
|
|
|
703
786
|
releaseModel(pool, key);
|
|
704
787
|
await this._collectRetiredEmbedPool(pool);
|
|
705
788
|
}
|
|
789
|
+
_collectRetiredEmbedPoolAfter(settled, pool) {
|
|
790
|
+
const collect = () => this._collectRetiredEmbedPool(pool);
|
|
791
|
+
settled.then(collect, collect).catch(noop);
|
|
792
|
+
}
|
|
706
793
|
_releaseEmbedResourceAfter(settled, pool, key) {
|
|
707
794
|
settled
|
|
708
795
|
.then(() => this._releaseEmbedResource(pool, key), () => this._releaseEmbedResource(pool, key))
|
|
@@ -758,11 +845,14 @@ export class Harness extends ReadyResource {
|
|
|
758
845
|
}
|
|
759
846
|
this._state = HarnessState.SUSPEND;
|
|
760
847
|
hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
|
|
848
|
+
this._downloads.abandon();
|
|
761
849
|
const pending = this._sdk.peek();
|
|
762
850
|
if (pending) {
|
|
763
851
|
this._loads.drain(await pending);
|
|
764
852
|
}
|
|
765
853
|
await this._releaseRuntimes();
|
|
854
|
+
this._reserved.clear();
|
|
855
|
+
this._byHand.clear();
|
|
766
856
|
await this._sdkLifecycle('suspend');
|
|
767
857
|
invalidateModelStatusCache(this._modelStatus);
|
|
768
858
|
hlog('suspended');
|
|
@@ -856,6 +946,7 @@ export class Harness extends ReadyResource {
|
|
|
856
946
|
async _close() {
|
|
857
947
|
hlog('closing');
|
|
858
948
|
this._idle.close();
|
|
949
|
+
this._downloads.abandon();
|
|
859
950
|
this._overlayWatcher?.stop();
|
|
860
951
|
this._overlayWatcher = null;
|
|
861
952
|
const pending = this._sdk.peek();
|
|
@@ -881,7 +972,12 @@ export class Harness extends ReadyResource {
|
|
|
881
972
|
projection,
|
|
882
973
|
companions: multiFile?.companions,
|
|
883
974
|
assetSizes: multiFile?.assetSizes
|
|
884
|
-
}, () =>
|
|
975
|
+
}, (event) => {
|
|
976
|
+
invalidateModelStatusCache(this._modelStatus);
|
|
977
|
+
if (event.type === 'complete') {
|
|
978
|
+
this._downloads.downloaded();
|
|
979
|
+
}
|
|
980
|
+
});
|
|
885
981
|
}
|
|
886
982
|
embed(context) {
|
|
887
983
|
return this._busy(() => this._embed(context));
|
|
@@ -902,6 +998,7 @@ export class Harness extends ReadyResource {
|
|
|
902
998
|
let loaded = null;
|
|
903
999
|
let job = null;
|
|
904
1000
|
let releaseAfter = null;
|
|
1001
|
+
let collectAfter = null;
|
|
905
1002
|
try {
|
|
906
1003
|
loaded = await raceAbort(handle.load(), signal);
|
|
907
1004
|
const sdk = await this._sdk.loadEmbed();
|
|
@@ -912,7 +1009,8 @@ export class Harness extends ReadyResource {
|
|
|
912
1009
|
}
|
|
913
1010
|
catch (err) {
|
|
914
1011
|
if (err instanceof AbortedError) {
|
|
915
|
-
releaseAfter = job
|
|
1012
|
+
releaseAfter = job;
|
|
1013
|
+
collectAfter = job === null && this._retiredEmbedModels.has(pool) ? handle.peek() : null;
|
|
916
1014
|
}
|
|
917
1015
|
if (isSdkGpuDeviceLostError(err)) {
|
|
918
1016
|
faults.recordFault(generation);
|
|
@@ -930,6 +1028,10 @@ export class Harness extends ReadyResource {
|
|
|
930
1028
|
if (releaseAfter) {
|
|
931
1029
|
this._releaseEmbedResourceAfter(releaseAfter, pool, key);
|
|
932
1030
|
}
|
|
1031
|
+
else if (collectAfter) {
|
|
1032
|
+
releaseModel(pool, key);
|
|
1033
|
+
this._collectRetiredEmbedPoolAfter(collectAfter, pool);
|
|
1034
|
+
}
|
|
933
1035
|
else {
|
|
934
1036
|
await this._releaseEmbedResource(pool, key);
|
|
935
1037
|
}
|
|
@@ -992,16 +1094,7 @@ export class Harness extends ReadyResource {
|
|
|
992
1094
|
return this._busy(() => this._transcribe(context));
|
|
993
1095
|
}
|
|
994
1096
|
async _transcribe({ model, audio, signal }) {
|
|
995
|
-
return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal },
|
|
996
|
-
const pcm = await pcmForTranscription(audio);
|
|
997
|
-
if (signal?.aborted) {
|
|
998
|
-
throw new AbortedError();
|
|
999
|
-
}
|
|
1000
|
-
if (pcm && pcm.length >= TRANSCRIBE_STREAM_MIN_BYTES) {
|
|
1001
|
-
return transcribeStreaming(sdk, pcm, { modelId: id, signal });
|
|
1002
|
-
}
|
|
1003
|
-
return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
|
|
1004
|
-
});
|
|
1097
|
+
return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, (sdk, id) => transcribeAudio(sdk, pcmForTranscription(audio), { modelId: id, signal, audio }));
|
|
1005
1098
|
}
|
|
1006
1099
|
detectLanguage(context) {
|
|
1007
1100
|
return detectLanguage(context);
|
|
@@ -1033,21 +1126,25 @@ export class Harness extends ReadyResource {
|
|
|
1033
1126
|
}
|
|
1034
1127
|
};
|
|
1035
1128
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
1036
|
-
const job =
|
|
1129
|
+
const job = (async () => {
|
|
1037
1130
|
try {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1131
|
+
const loadedId = (await raceAbort(handle.load(), signal)).modelId;
|
|
1132
|
+
modelId = loadedId;
|
|
1133
|
+
const voiceSdk = (await this._sdk.loadVoice());
|
|
1134
|
+
sdk = voiceSdk;
|
|
1135
|
+
await this._voiceLane(async () => {
|
|
1136
|
+
if (signal?.aborted) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
for await (const chunk of speakStreamOnce(voiceSdk, loadedId, text)) {
|
|
1140
|
+
chunks.push(chunk);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1046
1143
|
}
|
|
1047
1144
|
finally {
|
|
1048
1145
|
chunks.end();
|
|
1049
1146
|
}
|
|
1050
|
-
});
|
|
1147
|
+
})();
|
|
1051
1148
|
let settled = false;
|
|
1052
1149
|
try {
|
|
1053
1150
|
yield* chunks;
|
|
@@ -1075,9 +1172,11 @@ export class Harness extends ReadyResource {
|
|
|
1075
1172
|
const loaded = this._settledModels();
|
|
1076
1173
|
const loading = this._loadingModels();
|
|
1077
1174
|
const custom = await customModelRows(options, { modelsDir: this._modelsDir, loaded });
|
|
1175
|
+
const memory = this._memoryByModel();
|
|
1078
1176
|
return mergeCustomRows(rows, custom).map((row) => ({
|
|
1079
1177
|
...row,
|
|
1080
|
-
isLoading: loading.has(row.name)
|
|
1178
|
+
isLoading: loading.has(row.name),
|
|
1179
|
+
memoryBytes: memory.get(row.name) ?? memory.get(residentSrc(row.name, row.type))
|
|
1081
1180
|
}));
|
|
1082
1181
|
}
|
|
1083
1182
|
async deleteModel(name) {
|
|
@@ -1159,7 +1258,7 @@ export class Harness extends ReadyResource {
|
|
|
1159
1258
|
if (!roomed) {
|
|
1160
1259
|
return;
|
|
1161
1260
|
}
|
|
1162
|
-
await this.
|
|
1261
|
+
await this._yieldGenerators();
|
|
1163
1262
|
}
|
|
1164
1263
|
await this._loadAndRelease(target, signal);
|
|
1165
1264
|
}
|
|
@@ -1175,20 +1274,20 @@ export class Harness extends ReadyResource {
|
|
|
1175
1274
|
const target = this._residency(model, agent);
|
|
1176
1275
|
const resident = isResident(target.pool, target.key);
|
|
1177
1276
|
hlog('load model', { model, resident });
|
|
1277
|
+
if (!resident && !(await this._admitByHand(model, target))) {
|
|
1278
|
+
return { loaded: false, reason: 'memory' };
|
|
1279
|
+
}
|
|
1178
1280
|
if (target.pool !== this._models) {
|
|
1179
|
-
await this.
|
|
1281
|
+
await this._loadByHand(target, signal);
|
|
1180
1282
|
return { loaded: true };
|
|
1181
1283
|
}
|
|
1182
1284
|
if (!resident) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
|
|
1187
|
-
await this._unloadIdleGenerators();
|
|
1285
|
+
await evictIdleWhere(this._models, this._replacedBy(model));
|
|
1286
|
+
await this._yieldGenerators();
|
|
1188
1287
|
}
|
|
1189
1288
|
this._pinned.add(target.key);
|
|
1190
1289
|
try {
|
|
1191
|
-
await this.
|
|
1290
|
+
await this._loadByHand(target, signal);
|
|
1192
1291
|
}
|
|
1193
1292
|
catch (err) {
|
|
1194
1293
|
this._pinned.delete(target.key);
|
|
@@ -1228,11 +1327,15 @@ export class Harness extends ReadyResource {
|
|
|
1228
1327
|
await this._musicRuntime?.unloadIdle().catch(noop);
|
|
1229
1328
|
}
|
|
1230
1329
|
async _fits(model, ctxSize) {
|
|
1330
|
+
return (await this._curatedFit(model, ctxSize))?.fit !== 'likely-too-large';
|
|
1331
|
+
}
|
|
1332
|
+
async _curatedFit(model, ctxSize) {
|
|
1231
1333
|
const asset = curatedModelAsset(model, 'llm');
|
|
1232
1334
|
if (!asset) {
|
|
1233
|
-
return
|
|
1335
|
+
return null;
|
|
1234
1336
|
}
|
|
1235
|
-
|
|
1337
|
+
return this._assessFit({
|
|
1338
|
+
src: asset.src,
|
|
1236
1339
|
sha256Checksum: asset.sha256Checksum,
|
|
1237
1340
|
name: asset.name,
|
|
1238
1341
|
contextTokens: ctxSize || defaultContextSize(asset)
|
|
@@ -1240,7 +1343,205 @@ export class Harness extends ReadyResource {
|
|
|
1240
1343
|
hlog('fit estimate failed', { model, err: errorMessage(err) });
|
|
1241
1344
|
return null;
|
|
1242
1345
|
});
|
|
1243
|
-
|
|
1346
|
+
}
|
|
1347
|
+
async _admitByHand(model, target) {
|
|
1348
|
+
if (this._capBytes === null) {
|
|
1349
|
+
return target.pool !== this._models || this._fits(model, target.ctxSize);
|
|
1350
|
+
}
|
|
1351
|
+
return this._admit(async () => {
|
|
1352
|
+
if (isResident(target.pool, target.key) || this._byHand.has(target.key)) {
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
const sized = await this._sizeByHand(model, target);
|
|
1356
|
+
const capBytes = this._capBytes;
|
|
1357
|
+
const settled = this._pools().flatMap((pool) => settledEntries(pool));
|
|
1358
|
+
const replacedBy = this._replacedBy(model);
|
|
1359
|
+
const replaced = target.pool === this._models
|
|
1360
|
+
? settledEntries(this._models).filter(({ key, refs }) => refs === 0 && replacedBy(key))
|
|
1361
|
+
: [];
|
|
1362
|
+
const used = this._usedBytes() - replaced.reduce((sum, { bytes }) => sum + (bytes ?? 0), 0);
|
|
1363
|
+
const alone = this._reserved.size === 0 && settled.length === replaced.length;
|
|
1364
|
+
const fits = capBytes === null || alone || sized.bytes === undefined || used + sized.bytes <= capBytes;
|
|
1365
|
+
if (sized.tooLarge || !fits) {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
this._byHand.add(target.key);
|
|
1369
|
+
if (sized.bytes !== undefined) {
|
|
1370
|
+
this._reserved.set(target.key, sized.bytes);
|
|
1371
|
+
}
|
|
1372
|
+
return true;
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
_replacedBy(model) {
|
|
1376
|
+
return (key) => modelOf(key) === model && !this._replying.includes(key);
|
|
1377
|
+
}
|
|
1378
|
+
_sizeByHand(model, target) {
|
|
1379
|
+
const addon = modelAddonOf(model);
|
|
1380
|
+
const modelSrc = curatedModelAsset(model, addon) ?? customModelSrc(model, addon) ?? model;
|
|
1381
|
+
const contextTokens = target.ctxSize || defaultContextSize(modelSrc, customContextLength(model));
|
|
1382
|
+
return this._sizeLoad({
|
|
1383
|
+
key: target.key,
|
|
1384
|
+
model,
|
|
1385
|
+
modelSrc,
|
|
1386
|
+
modelType: target.modelType,
|
|
1387
|
+
modelConfig: target.pool === this._models
|
|
1388
|
+
? { ctx_size: contextTokens, projectionModelSrc: this._projectionFor(model) }
|
|
1389
|
+
: undefined
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
async _loadByHand(target, signal) {
|
|
1393
|
+
const loading = target.open().load();
|
|
1394
|
+
void loading.then(noop, noop).then(() => {
|
|
1395
|
+
if (this._byHand.delete(target.key)) {
|
|
1396
|
+
this._reserved.delete(target.key);
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
try {
|
|
1400
|
+
await raceAbort(loading, signal);
|
|
1401
|
+
}
|
|
1402
|
+
finally {
|
|
1403
|
+
releaseModel(target.pool, target.key);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async _admit(run) {
|
|
1407
|
+
await this._admission.lock();
|
|
1408
|
+
try {
|
|
1409
|
+
return await run();
|
|
1410
|
+
}
|
|
1411
|
+
finally {
|
|
1412
|
+
this._admission.unlock();
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
async _makeRoomForGenerator(srcs, signal) {
|
|
1416
|
+
if (signal?.aborted) {
|
|
1417
|
+
throw new AbortedError();
|
|
1418
|
+
}
|
|
1419
|
+
await this._makeRoom(stagesBytes(srcs), { replying: true });
|
|
1420
|
+
if (signal?.aborted) {
|
|
1421
|
+
throw new AbortedError();
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async _yieldGenerators() {
|
|
1425
|
+
if (this._capBytes === null) {
|
|
1426
|
+
await this._unloadIdleGenerators();
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
async _beforeLoad(load) {
|
|
1430
|
+
if (!this._sizeLoads)
|
|
1431
|
+
return undefined;
|
|
1432
|
+
const bytes = this._byHand.delete(load.key)
|
|
1433
|
+
? this._reserved.get(load.key)
|
|
1434
|
+
: await this._admit(() => this._reserve(load));
|
|
1435
|
+
return { bytes, release: () => this._reserved.delete(load.key) };
|
|
1436
|
+
}
|
|
1437
|
+
async _reserve(load) {
|
|
1438
|
+
const sized = await this._sizeLoad(load);
|
|
1439
|
+
if (this._capBytes !== null) {
|
|
1440
|
+
await this._makeRoom(sized.bytes ?? 0, {
|
|
1441
|
+
generators: true,
|
|
1442
|
+
tooLarge: sized.tooLarge,
|
|
1443
|
+
recheck: async () => (await this._sizeLoad(load)).tooLarge
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
if (sized.bytes !== undefined) {
|
|
1447
|
+
this._reserved.set(load.key, sized.bytes);
|
|
1448
|
+
}
|
|
1449
|
+
return sized.bytes;
|
|
1450
|
+
}
|
|
1451
|
+
async _sizeLoad({ model, modelSrc, modelType, modelConfig }) {
|
|
1452
|
+
const downloaded = downloadBytes(modelSrc);
|
|
1453
|
+
const { projectionModelSrc, ...config } = modelConfig ?? {};
|
|
1454
|
+
const projection = downloadBytes(projectionModelSrc) ?? 0;
|
|
1455
|
+
try {
|
|
1456
|
+
const sdk = await this._sdk.load();
|
|
1457
|
+
const { models } = await sdk.assessModelFit({
|
|
1458
|
+
models: [{ modelSrc: fitSrc(modelSrc), modelType, modelConfig: config }]
|
|
1459
|
+
});
|
|
1460
|
+
const [fit] = models;
|
|
1461
|
+
const bytes = fit?.estimate?.upperBoundBytes ?? fit?.floorBytes ?? downloaded;
|
|
1462
|
+
return {
|
|
1463
|
+
bytes: bytes === undefined ? undefined : bytes + projection,
|
|
1464
|
+
tooLarge: fit?.verdict === 'likely-too-large'
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
catch (err) {
|
|
1468
|
+
hlog('fit estimate failed', { model, err: errorMessage(err) });
|
|
1469
|
+
return {
|
|
1470
|
+
bytes: downloaded === undefined ? undefined : downloaded + projection,
|
|
1471
|
+
tooLarge: false
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async _makeRoom(need, room) {
|
|
1476
|
+
const capBytes = this._capBytes;
|
|
1477
|
+
if (capBytes === null) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const tried = new Set();
|
|
1481
|
+
let tooLarge = room.tooLarge ?? false;
|
|
1482
|
+
while (this._usedBytes() + need > capBytes || tooLarge) {
|
|
1483
|
+
const victim = nextVictim(this._capVictims(room).filter(({ id }) => !tried.has(id)));
|
|
1484
|
+
if (!victim) {
|
|
1485
|
+
hlog('model over memory cap', { need, used: this._usedBytes(), capBytes, tooLarge });
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
tried.add(victim.id);
|
|
1489
|
+
await victim.evict();
|
|
1490
|
+
if (tooLarge && room.recheck) {
|
|
1491
|
+
tooLarge = await room.recheck();
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
_capVictims({ replying = false, generators = false }) {
|
|
1496
|
+
const victims = [];
|
|
1497
|
+
if (generators) {
|
|
1498
|
+
victims.push({
|
|
1499
|
+
id: 'generators',
|
|
1500
|
+
rank: VictimRank.generator,
|
|
1501
|
+
lastUsed: 0,
|
|
1502
|
+
evict: () => this._unloadIdleGenerators()
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
for (const [index, pool] of this._pools().entries()) {
|
|
1506
|
+
for (const { key, refs, lastUsed } of settledEntries(pool)) {
|
|
1507
|
+
const inReply = this._replying.includes(key);
|
|
1508
|
+
if (refs > 0 || (inReply && !replying)) {
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
victims.push({
|
|
1512
|
+
id: `${index}:${key}`,
|
|
1513
|
+
rank: this._victimRank(pool, key, inReply),
|
|
1514
|
+
lastUsed,
|
|
1515
|
+
evict: () => evictIdleWithin(pool, key, { deadlineMs: WEDGED_EVICT_WAIT_MS })
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return victims;
|
|
1520
|
+
}
|
|
1521
|
+
_victimRank(pool, key, inReply) {
|
|
1522
|
+
if (pool !== this._models)
|
|
1523
|
+
return VictimRank.companion;
|
|
1524
|
+
if (inReply)
|
|
1525
|
+
return VictimRank.replying;
|
|
1526
|
+
return this._pinned.has(key) ? VictimRank.pinned : VictimRank.requested;
|
|
1527
|
+
}
|
|
1528
|
+
_usedBytes() {
|
|
1529
|
+
return (this._settledBytes() + [...this._reserved.values()].reduce((sum, bytes) => sum + bytes, 0));
|
|
1530
|
+
}
|
|
1531
|
+
_settledBytes() {
|
|
1532
|
+
return this._pools()
|
|
1533
|
+
.flatMap((pool) => settledEntries(pool))
|
|
1534
|
+
.reduce((sum, { bytes }) => sum + (bytes ?? 0), 0);
|
|
1535
|
+
}
|
|
1536
|
+
_memoryByModel() {
|
|
1537
|
+
const memory = new Map();
|
|
1538
|
+
for (const { key, bytes } of this._pools().flatMap((pool) => settledEntries(pool))) {
|
|
1539
|
+
if (bytes === undefined)
|
|
1540
|
+
continue;
|
|
1541
|
+
const model = modelOf(key);
|
|
1542
|
+
memory.set(model, (memory.get(model) ?? 0) + bytes);
|
|
1543
|
+
}
|
|
1544
|
+
return memory;
|
|
1244
1545
|
}
|
|
1245
1546
|
async _yieldPins(model, ctxSize) {
|
|
1246
1547
|
for (const key of this._pinned) {
|
|
@@ -1261,18 +1562,25 @@ export class Harness extends ReadyResource {
|
|
|
1261
1562
|
return {
|
|
1262
1563
|
pool: this._models,
|
|
1263
1564
|
key,
|
|
1565
|
+
modelType: 'llamacpp-completion',
|
|
1264
1566
|
ctxSize: loadOpts.ctxSize,
|
|
1265
1567
|
open: () => this._models.open(key, model, loadOpts)
|
|
1266
1568
|
};
|
|
1267
1569
|
}
|
|
1268
1570
|
if (addon === 'embeddings') {
|
|
1269
1571
|
const key = this._embedFaults.key(residentSrc(model, addon));
|
|
1270
|
-
return {
|
|
1572
|
+
return {
|
|
1573
|
+
pool: this._embedModels,
|
|
1574
|
+
key,
|
|
1575
|
+
modelType: 'llamacpp-embedding',
|
|
1576
|
+
open: () => this._embedModels.open(key, model)
|
|
1577
|
+
};
|
|
1271
1578
|
}
|
|
1272
1579
|
if (addon === 'tts' || addon === 'parakeet') {
|
|
1273
1580
|
const pool = addon === 'tts' ? this._ttsModels : this._transcriptionModels;
|
|
1274
1581
|
const key = residentSrc(model, addon);
|
|
1275
|
-
|
|
1582
|
+
const modelType = addon === 'tts' ? 'tts-ggml' : 'parakeet-transcription';
|
|
1583
|
+
return { pool, key, modelType, open: () => pool.open(key, key) };
|
|
1276
1584
|
}
|
|
1277
1585
|
throw new Error(`${model} loads for each generation and is not kept in memory`);
|
|
1278
1586
|
}
|
|
@@ -1330,12 +1638,12 @@ export class Harness extends ReadyResource {
|
|
|
1330
1638
|
hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
|
|
1331
1639
|
if (!resident) {
|
|
1332
1640
|
await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
|
|
1333
|
-
await this.
|
|
1641
|
+
await this._yieldGenerators();
|
|
1334
1642
|
}
|
|
1335
1643
|
const acquireRound = async () => {
|
|
1336
1644
|
if (!isResident(pool, key)) {
|
|
1337
1645
|
await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
|
|
1338
|
-
await this.
|
|
1646
|
+
await this._yieldGenerators();
|
|
1339
1647
|
}
|
|
1340
1648
|
try {
|
|
1341
1649
|
const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
|
|
@@ -1449,6 +1757,10 @@ export class Harness extends ReadyResource {
|
|
|
1449
1757
|
};
|
|
1450
1758
|
}
|
|
1451
1759
|
async _makeRoomForCompletion(model, { ctxSize, signal, deadlineMs = MODEL_ROOM_WAIT_MS } = {}) {
|
|
1760
|
+
if (this._capBytes !== null) {
|
|
1761
|
+
await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1452
1764
|
const roomed = await awaitModelCapacity(this._models, {
|
|
1453
1765
|
cap: this._completionModelCap,
|
|
1454
1766
|
deadlineMs,
|