@qvac/harness 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/README.md +46 -0
- package/dist/harness.bundle +6 -5
- package/dist-lib/index.d.ts +14 -1
- package/dist-lib/index.js +173 -51
- package/dist-lib/lib/connect.d.ts +3 -1
- package/dist-lib/lib/connect.js +18 -0
- package/dist-lib/lib/contract-types.d.ts +13 -0
- package/dist-lib/lib/models/model-catalog.d.ts +2 -0
- package/dist-lib/lib/models/model-catalog.js +7 -0
- package/dist-lib/lib/models/model-pool.d.ts +6 -2
- package/dist-lib/lib/models/model-pool.js +67 -11
- package/dist-lib/lib/serve.js +12 -0
- package/dist-lib/schema/harness.js +25 -0
- package/dist-lib/spec/bind.js +6 -0
- package/dist-lib/spec/calls.js +2 -0
- package/dist-lib/spec/hrpc/hrpc.json +26 -0
- package/dist-lib/spec/hrpc/index.js +27 -3
- package/dist-lib/spec/hrpc/messages.js +68 -2
- package/dist-lib/spec/hyperschema/index.js +68 -2
- package/dist-lib/spec/hyperschema/schema.json +48 -1
- package/dist-lib/spec/hyperschema/types.d.ts +17 -1
- package/package.json +2 -1
package/dist-lib/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type EmbedContext } from './lib/capabilities/embed.ts';
|
|
|
10
10
|
import { type KvCacheDeletionResult } from './lib/capabilities/kv-cache.ts';
|
|
11
11
|
import type { LlmGenerationParams } from './lib/models/llm-presets.ts';
|
|
12
12
|
import { type SpeakContext, type SpeakResult, type SpeakStreamContext, type TranscribeContext } from './lib/capabilities/voice.ts';
|
|
13
|
-
import type { CapabilityInfo, CredentialEntry, HarnessDiagnostics, InfraModel, SkillsOptions, SkillWriteResult } from './lib/contract-types.ts';
|
|
13
|
+
import type { CapabilityInfo, CredentialEntry, HarnessDiagnostics, InfraModel, LoadModelResult, SkillsOptions, SkillWriteResult, UnloadModelContext, UnloadModelResult } from './lib/contract-types.ts';
|
|
14
14
|
import { type CustomModelEntry } from './lib/models/model-catalog.ts';
|
|
15
15
|
import { type ListModelsOptions, type ModelInfo } from './lib/models/model-list.ts';
|
|
16
16
|
import { type DeleteModelSummary } from './lib/models/model-delete.ts';
|
|
@@ -138,6 +138,8 @@ export declare class Harness extends ReadyResource {
|
|
|
138
138
|
private _tools;
|
|
139
139
|
private _sdk;
|
|
140
140
|
private readonly _loads;
|
|
141
|
+
private readonly _pinned;
|
|
142
|
+
private readonly _replying;
|
|
141
143
|
private readonly _modelStatus;
|
|
142
144
|
private _models;
|
|
143
145
|
private _embedModels;
|
|
@@ -211,6 +213,7 @@ export declare class Harness extends ReadyResource {
|
|
|
211
213
|
setCustomModels(entries: ReadonlyArray<CustomModelEntry>): void;
|
|
212
214
|
setModelIdleTimeout(ms: number): void;
|
|
213
215
|
previewModelSource(context: PreviewModelSourceContext): Promise<ModelSourcePreview>;
|
|
216
|
+
private _assessFit;
|
|
214
217
|
describe(): Promise<HarnessInfo>;
|
|
215
218
|
diagnostics(): HarnessDiagnostics;
|
|
216
219
|
private _loadedModels;
|
|
@@ -261,6 +264,16 @@ export declare class Harness extends ReadyResource {
|
|
|
261
264
|
capability(): Promise<CapabilityInfo>;
|
|
262
265
|
warmModel(context: WarmModelOptions): Promise<void>;
|
|
263
266
|
private _warmModel;
|
|
267
|
+
loadModel(context: WarmModelOptions): Promise<LoadModelResult>;
|
|
268
|
+
private _loadModel;
|
|
269
|
+
unloadModel(context: UnloadModelContext): Promise<UnloadModelResult>;
|
|
270
|
+
private _unloadModel;
|
|
271
|
+
private _loadAndRelease;
|
|
272
|
+
private _unloadIdleGenerators;
|
|
273
|
+
private _fits;
|
|
274
|
+
private _yieldPins;
|
|
275
|
+
private _residency;
|
|
276
|
+
private _completionLoadOptions;
|
|
264
277
|
invoke(context: InvokeContext): AsyncGenerator<CompletionEvent>;
|
|
265
278
|
private _invoke;
|
|
266
279
|
completion(context: CompletionContext): AsyncGenerator<CompletionEvent>;
|
package/dist-lib/index.js
CHANGED
|
@@ -21,10 +21,11 @@ import { buildWavBuffer } from './lib/wav-encode.js';
|
|
|
21
21
|
import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, 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, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, infraModelsFor, modelNameToCapabilityProfile, musicModelSet, setCustomModels } from './lib/models/model-catalog.js';
|
|
24
|
+
import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
|
|
25
25
|
import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
|
|
26
26
|
import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
|
|
27
|
-
import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
|
|
27
|
+
import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
|
|
28
|
+
import { defaultContextSize } from './lib/models/context.js';
|
|
28
29
|
import { resolveMainGpu } from './lib/models/gpu-selection.js';
|
|
29
30
|
import { createIdleReaper } from './lib/models/idle-unload.js';
|
|
30
31
|
import { openclCacheDir } from './lib/models/opencl-cache.js';
|
|
@@ -81,6 +82,7 @@ const MUSIC_EVICT_WAIT_MS = 15_000;
|
|
|
81
82
|
const WEDGED_EVICT_WAIT_MS = 10_000;
|
|
82
83
|
const MODEL_ROOM_WAIT_MS = 300_000;
|
|
83
84
|
const WARM_ROOM_WAIT_MS = 2_000;
|
|
85
|
+
const UNLOAD_LEASE_WAIT_MS = 10_000;
|
|
84
86
|
const DEFAULT_COMPLETION_MODEL_CAP = 1;
|
|
85
87
|
const WEDGED_EVICT_LEASED_MS = 120_000;
|
|
86
88
|
const TTS_MODEL_CONFIG = {
|
|
@@ -93,6 +95,8 @@ export class Harness extends ReadyResource {
|
|
|
93
95
|
_tools;
|
|
94
96
|
_sdk;
|
|
95
97
|
_loads = createLoadTracker();
|
|
98
|
+
_pinned = new Set();
|
|
99
|
+
_replying = [];
|
|
96
100
|
_modelStatus = createModelStatusCache();
|
|
97
101
|
_models;
|
|
98
102
|
_embedModels;
|
|
@@ -520,19 +524,18 @@ export class Harness extends ReadyResource {
|
|
|
520
524
|
this._idle.configure(ms);
|
|
521
525
|
}
|
|
522
526
|
previewModelSource(context) {
|
|
523
|
-
return previewModelSource(context, {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const model = result.models[0];
|
|
530
|
-
return {
|
|
531
|
-
fit: model?.verdict ?? result.verdict,
|
|
532
|
-
fitReasons: [...(model?.reasons ?? result.reasons)]
|
|
533
|
-
};
|
|
534
|
-
}
|
|
527
|
+
return previewModelSource(context, { assessFit: (query) => this._assessFit(query) });
|
|
528
|
+
}
|
|
529
|
+
async _assessFit({ sha256Checksum, name, contextTokens }) {
|
|
530
|
+
const sdk = await this._sdk.load();
|
|
531
|
+
const result = await sdk.assessModelFit({
|
|
532
|
+
models: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
|
|
535
533
|
});
|
|
534
|
+
const model = result.models[0];
|
|
535
|
+
return {
|
|
536
|
+
fit: model?.verdict ?? result.verdict,
|
|
537
|
+
fitReasons: [...(model?.reasons ?? result.reasons)]
|
|
538
|
+
};
|
|
536
539
|
}
|
|
537
540
|
async describe() {
|
|
538
541
|
if (!this.opened)
|
|
@@ -595,7 +598,7 @@ export class Harness extends ReadyResource {
|
|
|
595
598
|
const key = modelKey(model, loadOpts);
|
|
596
599
|
const pool = this._models;
|
|
597
600
|
if (!isResident(pool, key)) {
|
|
598
|
-
await this._makeRoomForCompletion(signal);
|
|
601
|
+
await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
|
|
599
602
|
}
|
|
600
603
|
try {
|
|
601
604
|
const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
|
|
@@ -630,7 +633,7 @@ export class Harness extends ReadyResource {
|
|
|
630
633
|
}
|
|
631
634
|
_createPool() {
|
|
632
635
|
const loadSdk = this._sdk.load;
|
|
633
|
-
return createModelPool(loadSdk, (state) => this._notifyModel(state
|
|
636
|
+
return createModelPool(loadSdk, (state) => this._notifyModel(state), {
|
|
634
637
|
deviceConfig: this._deviceConfig(),
|
|
635
638
|
loads: this._loads
|
|
636
639
|
});
|
|
@@ -663,11 +666,18 @@ export class Harness extends ReadyResource {
|
|
|
663
666
|
loads: this._loads
|
|
664
667
|
});
|
|
665
668
|
}
|
|
666
|
-
_notifyModel(state
|
|
667
|
-
const catalogChanged =
|
|
669
|
+
_notifyModel(state) {
|
|
670
|
+
const catalogChanged = state.status === 'ready' || state.status === 'unloaded';
|
|
668
671
|
if (catalogChanged) {
|
|
669
672
|
invalidateModelStatusCache(this._modelStatus);
|
|
670
673
|
}
|
|
674
|
+
if (state.status === 'unloaded') {
|
|
675
|
+
for (const key of this._pinned) {
|
|
676
|
+
if (!isResident(this._models, key)) {
|
|
677
|
+
this._pinned.delete(key);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
671
681
|
this.emit('model', { ...state, catalogChanged, loaded: this._loadedModels() });
|
|
672
682
|
}
|
|
673
683
|
async _collectRetiredEmbedPool(pool) {
|
|
@@ -874,7 +884,7 @@ export class Harness extends ReadyResource {
|
|
|
874
884
|
const pool = this._embedModels;
|
|
875
885
|
const faults = this._embedFaults;
|
|
876
886
|
const generation = faults.generation;
|
|
877
|
-
const key = faults.key(model);
|
|
887
|
+
const key = faults.key(residentSrc(model, 'embeddings'));
|
|
878
888
|
const handle = pool.open(key, model);
|
|
879
889
|
let loaded = null;
|
|
880
890
|
let job = null;
|
|
@@ -929,7 +939,8 @@ export class Harness extends ReadyResource {
|
|
|
929
939
|
await this.ready();
|
|
930
940
|
if (this.suspended)
|
|
931
941
|
throw new Error('harness is suspended');
|
|
932
|
-
const
|
|
942
|
+
const addon = category === 'tts' ? 'tts' : 'parakeet';
|
|
943
|
+
const name = residentSrc(model || (await this._defaultVoiceSrc(category)), addon);
|
|
933
944
|
const handle = pool.open(name, name);
|
|
934
945
|
let job = null;
|
|
935
946
|
let releaseAfter = null;
|
|
@@ -997,7 +1008,7 @@ export class Harness extends ReadyResource {
|
|
|
997
1008
|
await this.ready();
|
|
998
1009
|
if (this.suspended)
|
|
999
1010
|
throw new Error('harness is suspended');
|
|
1000
|
-
const name = model || (await this._defaultVoiceSrc('tts'));
|
|
1011
|
+
const name = residentSrc(model || (await this._defaultVoiceSrc('tts')), 'tts');
|
|
1001
1012
|
const pool = this._ttsModels;
|
|
1002
1013
|
const handle = pool.open(name, name);
|
|
1003
1014
|
const chunks = pushQueue();
|
|
@@ -1123,29 +1134,143 @@ export class Harness extends ReadyResource {
|
|
|
1123
1134
|
await this.ready();
|
|
1124
1135
|
if (this.suspended)
|
|
1125
1136
|
return;
|
|
1126
|
-
const
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
if (!isResident(pool, key)) {
|
|
1137
|
-
if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS))) {
|
|
1137
|
+
const target = this._residency(model, agent);
|
|
1138
|
+
const resident = isResident(target.pool, target.key);
|
|
1139
|
+
hlog('warm model', { model, resident });
|
|
1140
|
+
if (target.pool === this._models && !resident) {
|
|
1141
|
+
const roomed = await this._makeRoomForCompletion(model, {
|
|
1142
|
+
ctxSize: target.ctxSize,
|
|
1143
|
+
signal,
|
|
1144
|
+
deadlineMs: WARM_ROOM_WAIT_MS
|
|
1145
|
+
});
|
|
1146
|
+
if (!roomed) {
|
|
1138
1147
|
return;
|
|
1139
1148
|
}
|
|
1140
|
-
await this.
|
|
1149
|
+
await this._unloadIdleGenerators();
|
|
1150
|
+
}
|
|
1151
|
+
await this._loadAndRelease(target, signal);
|
|
1152
|
+
}
|
|
1153
|
+
loadModel(context) {
|
|
1154
|
+
return this._busy(() => this._loadModel(context));
|
|
1155
|
+
}
|
|
1156
|
+
async _loadModel({ model, agent, signal }) {
|
|
1157
|
+
if (!this.opened)
|
|
1158
|
+
await this.ready();
|
|
1159
|
+
if (this.suspended) {
|
|
1160
|
+
throw new Error('harness is suspended');
|
|
1161
|
+
}
|
|
1162
|
+
const target = this._residency(model, agent);
|
|
1163
|
+
const resident = isResident(target.pool, target.key);
|
|
1164
|
+
hlog('load model', { model, resident });
|
|
1165
|
+
if (target.pool !== this._models) {
|
|
1166
|
+
await this._loadAndRelease(target, signal);
|
|
1167
|
+
return { loaded: true };
|
|
1168
|
+
}
|
|
1169
|
+
if (!resident) {
|
|
1170
|
+
if (!(await this._fits(model, target.ctxSize))) {
|
|
1171
|
+
return { loaded: false, reason: 'memory' };
|
|
1172
|
+
}
|
|
1173
|
+
await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
|
|
1174
|
+
await this._unloadIdleGenerators();
|
|
1175
|
+
}
|
|
1176
|
+
this._pinned.add(target.key);
|
|
1177
|
+
try {
|
|
1178
|
+
await this._loadAndRelease(target, signal);
|
|
1179
|
+
}
|
|
1180
|
+
catch (err) {
|
|
1181
|
+
this._pinned.delete(target.key);
|
|
1182
|
+
throw err;
|
|
1183
|
+
}
|
|
1184
|
+
return { loaded: true };
|
|
1185
|
+
}
|
|
1186
|
+
unloadModel(context) {
|
|
1187
|
+
return this._busy(() => this._unloadModel(context));
|
|
1188
|
+
}
|
|
1189
|
+
async _unloadModel({ model }) {
|
|
1190
|
+
if (!this.opened)
|
|
1191
|
+
await this.ready();
|
|
1192
|
+
const target = this._residency(model);
|
|
1193
|
+
const name = modelOf(target.key);
|
|
1194
|
+
const unloaded = await evictModel(target.pool, name, { deadlineMs: UNLOAD_LEASE_WAIT_MS });
|
|
1195
|
+
hlog('unload model', { model, unloaded });
|
|
1196
|
+
if (unloaded) {
|
|
1197
|
+
for (const key of this._pinned) {
|
|
1198
|
+
if (modelOf(key) === name) {
|
|
1199
|
+
this._pinned.delete(key);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1141
1202
|
}
|
|
1203
|
+
return { unloaded };
|
|
1204
|
+
}
|
|
1205
|
+
async _loadAndRelease(target, signal) {
|
|
1142
1206
|
try {
|
|
1143
|
-
await raceAbort(
|
|
1207
|
+
await raceAbort(target.open().load(), signal);
|
|
1144
1208
|
}
|
|
1145
1209
|
finally {
|
|
1146
|
-
releaseModel(pool, key);
|
|
1210
|
+
releaseModel(target.pool, target.key);
|
|
1147
1211
|
}
|
|
1148
1212
|
}
|
|
1213
|
+
async _unloadIdleGenerators() {
|
|
1214
|
+
await this._imageRuntime?.unloadIdle().catch(noop);
|
|
1215
|
+
await this._musicRuntime?.unloadIdle().catch(noop);
|
|
1216
|
+
}
|
|
1217
|
+
async _fits(model, ctxSize) {
|
|
1218
|
+
const asset = curatedModelAsset(model, 'llm');
|
|
1219
|
+
if (!asset) {
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
const assessed = await this._assessFit({
|
|
1223
|
+
sha256Checksum: asset.sha256Checksum,
|
|
1224
|
+
name: asset.name,
|
|
1225
|
+
contextTokens: ctxSize || defaultContextSize(asset)
|
|
1226
|
+
}).catch((err) => {
|
|
1227
|
+
hlog('fit estimate failed', { model, err: errorMessage(err) });
|
|
1228
|
+
return null;
|
|
1229
|
+
});
|
|
1230
|
+
return assessed?.fit !== 'likely-too-large';
|
|
1231
|
+
}
|
|
1232
|
+
async _yieldPins(model, ctxSize) {
|
|
1233
|
+
for (const key of this._pinned) {
|
|
1234
|
+
if (leaseCount(this._models, key) > 0 || this._replying.includes(key)) {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (await this._fits(model, ctxSize))
|
|
1238
|
+
return;
|
|
1239
|
+
this._pinned.delete(key);
|
|
1240
|
+
await evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_WAIT_MS });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
_residency(model, agent) {
|
|
1244
|
+
const addon = modelAddonOf(model);
|
|
1245
|
+
if (addon === 'llm') {
|
|
1246
|
+
const loadOpts = this._completionLoadOptions(model, agent);
|
|
1247
|
+
const key = modelKey(model, loadOpts);
|
|
1248
|
+
return {
|
|
1249
|
+
pool: this._models,
|
|
1250
|
+
key,
|
|
1251
|
+
ctxSize: loadOpts.ctxSize,
|
|
1252
|
+
open: () => this._models.open(key, model, loadOpts)
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (addon === 'embeddings') {
|
|
1256
|
+
const key = this._embedFaults.key(residentSrc(model, addon));
|
|
1257
|
+
return { pool: this._embedModels, key, open: () => this._embedModels.open(key, model) };
|
|
1258
|
+
}
|
|
1259
|
+
if (addon === 'tts' || addon === 'parakeet') {
|
|
1260
|
+
const pool = addon === 'tts' ? this._ttsModels : this._transcriptionModels;
|
|
1261
|
+
const key = residentSrc(model, addon);
|
|
1262
|
+
return { pool, key, open: () => pool.open(key, key) };
|
|
1263
|
+
}
|
|
1264
|
+
throw new Error(`${model} loads for each generation and is not kept in memory`);
|
|
1265
|
+
}
|
|
1266
|
+
_completionLoadOptions(model, agent) {
|
|
1267
|
+
return resolveLoadOptions({
|
|
1268
|
+
ctxSize: agent?.ctxSize ?? undefined,
|
|
1269
|
+
kvCacheK: agent?.kvCacheK ?? undefined,
|
|
1270
|
+
kvCacheV: agent?.kvCacheV ?? undefined,
|
|
1271
|
+
projectionModel: this._projectionFor(model)
|
|
1272
|
+
}, this._deviceConfig());
|
|
1273
|
+
}
|
|
1149
1274
|
invoke(context) {
|
|
1150
1275
|
return this._busyStream(() => this._invoke(context));
|
|
1151
1276
|
}
|
|
@@ -1184,27 +1309,20 @@ export class Harness extends ReadyResource {
|
|
|
1184
1309
|
await this.ready();
|
|
1185
1310
|
if (this.suspended)
|
|
1186
1311
|
throw new Error('harness is suspended');
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1189
|
-
ctxSize: agent?.ctxSize ?? undefined,
|
|
1190
|
-
kvCacheK: agent?.kvCacheK ?? undefined,
|
|
1191
|
-
kvCacheV: agent?.kvCacheV ?? undefined,
|
|
1192
|
-
projectionModel
|
|
1193
|
-
}, this._deviceConfig());
|
|
1312
|
+
const loadOpts = this._completionLoadOptions(model, agent);
|
|
1313
|
+
const projectionModel = loadOpts.projectionModel;
|
|
1194
1314
|
const key = modelKey(model, loadOpts);
|
|
1195
1315
|
const pool = this._models;
|
|
1196
1316
|
const resident = isResident(pool, key);
|
|
1197
1317
|
hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
|
|
1198
1318
|
if (!resident) {
|
|
1199
|
-
await this._makeRoomForCompletion(signal);
|
|
1200
|
-
await this.
|
|
1201
|
-
await this._musicRuntime?.unloadIdle().catch(noop);
|
|
1319
|
+
await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
|
|
1320
|
+
await this._unloadIdleGenerators();
|
|
1202
1321
|
}
|
|
1203
1322
|
const acquireRound = async () => {
|
|
1204
1323
|
if (!isResident(pool, key)) {
|
|
1205
|
-
await this._makeRoomForCompletion(signal);
|
|
1206
|
-
await this.
|
|
1207
|
-
await this._musicRuntime?.unloadIdle().catch(noop);
|
|
1324
|
+
await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
|
|
1325
|
+
await this._unloadIdleGenerators();
|
|
1208
1326
|
}
|
|
1209
1327
|
try {
|
|
1210
1328
|
const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
|
|
@@ -1228,6 +1346,7 @@ export class Harness extends ReadyResource {
|
|
|
1228
1346
|
held = false;
|
|
1229
1347
|
releaseModel(pool, key);
|
|
1230
1348
|
};
|
|
1349
|
+
this._replying.push(key);
|
|
1231
1350
|
try {
|
|
1232
1351
|
const { modelId, ctxSize } = await raceAbort(handle.load(), signal);
|
|
1233
1352
|
hlog('model ready', { model });
|
|
@@ -1287,6 +1406,7 @@ export class Harness extends ReadyResource {
|
|
|
1287
1406
|
}
|
|
1288
1407
|
finally {
|
|
1289
1408
|
hlog('completion end', { model });
|
|
1409
|
+
this._replying.splice(this._replying.indexOf(key), 1);
|
|
1290
1410
|
releaseHeld();
|
|
1291
1411
|
void this._imageRuntime?.unloadIdle().catch(noop);
|
|
1292
1412
|
void this._musicRuntime?.unloadIdle().catch(noop);
|
|
@@ -1315,11 +1435,12 @@ export class Harness extends ReadyResource {
|
|
|
1315
1435
|
}
|
|
1316
1436
|
};
|
|
1317
1437
|
}
|
|
1318
|
-
async _makeRoomForCompletion(signal, deadlineMs = MODEL_ROOM_WAIT_MS) {
|
|
1438
|
+
async _makeRoomForCompletion(model, { ctxSize, signal, deadlineMs = MODEL_ROOM_WAIT_MS } = {}) {
|
|
1319
1439
|
const roomed = await awaitModelCapacity(this._models, {
|
|
1320
1440
|
cap: this._completionModelCap,
|
|
1321
1441
|
deadlineMs,
|
|
1322
|
-
signal
|
|
1442
|
+
signal,
|
|
1443
|
+
evictable: (key) => !this._pinned.has(key) || modelOf(key) === model
|
|
1323
1444
|
});
|
|
1324
1445
|
if (!roomed) {
|
|
1325
1446
|
hlog('model room wait gave up', {
|
|
@@ -1327,6 +1448,7 @@ export class Harness extends ReadyResource {
|
|
|
1327
1448
|
loaded: loadedModels(this._models).length
|
|
1328
1449
|
});
|
|
1329
1450
|
}
|
|
1451
|
+
await this._yieldPins(model, ctxSize);
|
|
1330
1452
|
return roomed;
|
|
1331
1453
|
}
|
|
1332
1454
|
async _evictWedgedModel(model, key) {
|
|
@@ -3,7 +3,7 @@ import { type Transport } from './rpc-session.ts';
|
|
|
3
3
|
import { type KvCacheDeletionResult } from './capabilities/kv-cache.ts';
|
|
4
4
|
import type { CustomModelEntry } from './models/model-catalog.ts';
|
|
5
5
|
import { type ModelSourcePreview, type PreviewModelSourceContext } from './capabilities/preview-model-source.ts';
|
|
6
|
-
import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, WarmModelContext } from './contract-types.ts';
|
|
6
|
+
import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, LoadModelContext, LoadModelResult, UnloadModelContext, UnloadModelResult, WarmModelContext } from './contract-types.ts';
|
|
7
7
|
import { type EmbedDevice } from './models/embed-config.ts';
|
|
8
8
|
import type { DeleteModelSummary } from './models/model-delete.ts';
|
|
9
9
|
import type { CopyBaseResult, DeleteOverlayResult, ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud.ts';
|
|
@@ -46,6 +46,8 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
|
|
|
46
46
|
skills(agentId?: string | null, modelName?: string | null, options?: SkillsOptions): Promise<ResolvedSkillCatalogEntry[]>;
|
|
47
47
|
tools(): Promise<ToolCatalogEntry[]>;
|
|
48
48
|
warmModel(context: WarmModelContext): Promise<void>;
|
|
49
|
+
loadModel(context: LoadModelContext): Promise<LoadModelResult>;
|
|
50
|
+
unloadModel(context: UnloadModelContext): Promise<UnloadModelResult>;
|
|
49
51
|
list(options?: ListModelsOptions): Promise<ModelInfo[]>;
|
|
50
52
|
deleteModel(name: string): Promise<DeleteModelSummary>;
|
|
51
53
|
clearKvCache(): Promise<KvCacheDeletionResult>;
|
package/dist-lib/lib/connect.js
CHANGED
|
@@ -540,6 +540,24 @@ export class RemoteHarness extends EventEmitter {
|
|
|
540
540
|
agent: context.agent ? knobs(context.agent) : undefined
|
|
541
541
|
}), context.signal);
|
|
542
542
|
}
|
|
543
|
+
async loadModel(context) {
|
|
544
|
+
const { client } = await this._ensure();
|
|
545
|
+
const response = await abortable(client.loadModel({
|
|
546
|
+
model: context.model,
|
|
547
|
+
agent: context.agent ? knobs(context.agent) : undefined
|
|
548
|
+
}), context.signal);
|
|
549
|
+
return {
|
|
550
|
+
loaded: response.loaded === true,
|
|
551
|
+
reason: response.reason === 'memory' ? 'memory' : undefined
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async unloadModel(context) {
|
|
555
|
+
const { client } = await this._ensure();
|
|
556
|
+
const response = await client.unloadModel({
|
|
557
|
+
model: context.model
|
|
558
|
+
});
|
|
559
|
+
return { unloaded: response.unloaded === true };
|
|
560
|
+
}
|
|
543
561
|
async list(options = {}) {
|
|
544
562
|
const { client } = await this._ensure();
|
|
545
563
|
const response = await client.listModels({
|
|
@@ -113,6 +113,17 @@ export interface WarmModelContext {
|
|
|
113
113
|
agent?: AgentKnobs;
|
|
114
114
|
signal?: CompletionContext['signal'];
|
|
115
115
|
}
|
|
116
|
+
export type LoadModelContext = WarmModelContext;
|
|
117
|
+
export interface LoadModelResult {
|
|
118
|
+
loaded: boolean;
|
|
119
|
+
reason?: 'memory';
|
|
120
|
+
}
|
|
121
|
+
export interface UnloadModelContext {
|
|
122
|
+
model: string;
|
|
123
|
+
}
|
|
124
|
+
export interface UnloadModelResult {
|
|
125
|
+
unloaded: boolean;
|
|
126
|
+
}
|
|
116
127
|
export interface DownloadContext {
|
|
117
128
|
model: string;
|
|
118
129
|
addon?: string;
|
|
@@ -224,6 +235,8 @@ export interface HarnessContract {
|
|
|
224
235
|
}>;
|
|
225
236
|
list?(options?: ListModelsOptions): Promise<ModelInfo[]>;
|
|
226
237
|
warmModel?(context: WarmModelContext): Promise<void>;
|
|
238
|
+
loadModel?(context: LoadModelContext): Promise<LoadModelResult>;
|
|
239
|
+
unloadModel?(context: UnloadModelContext): Promise<UnloadModelResult>;
|
|
227
240
|
deleteModel?(name: string): Promise<DeleteModelSummary>;
|
|
228
241
|
clearKvCache?(): Promise<KvCacheDeletionResult>;
|
|
229
242
|
deleteKvCache?(kvCacheKey: string): Promise<KvCacheDeletionResult>;
|
|
@@ -95,6 +95,8 @@ export interface CuratedListable {
|
|
|
95
95
|
}
|
|
96
96
|
export declare function curatedListables(): CuratedListable[];
|
|
97
97
|
export declare function curatedModelAsset(name: string, addon: ModelRegistryEntry['addon']): SdkModelAsset | undefined;
|
|
98
|
+
export declare function modelAddonOf(model: string): ModelRegistryEntry['addon'];
|
|
99
|
+
export declare function residentSrc(model: string, addon: ModelRegistryEntry['addon']): string;
|
|
98
100
|
export declare const CUSTOM_TASK_TYPES: readonly ['llm'];
|
|
99
101
|
export type CustomTaskType = (typeof CUSTOM_TASK_TYPES)[number];
|
|
100
102
|
export interface CustomModelEntry {
|
|
@@ -332,6 +332,13 @@ export function curatedListables() {
|
|
|
332
332
|
export function curatedModelAsset(name, addon) {
|
|
333
333
|
return curatedListables().find((entry) => entry.asset.addon === addon && (entry.name === name || entry.asset.name === name))?.asset;
|
|
334
334
|
}
|
|
335
|
+
export function modelAddonOf(model) {
|
|
336
|
+
const known = [...curatedListables(), ...musicListables()].find((entry) => entry.name === model || entry.asset.name === model || entry.asset.src === model);
|
|
337
|
+
return known?.asset.addon ?? 'llm';
|
|
338
|
+
}
|
|
339
|
+
export function residentSrc(model, addon) {
|
|
340
|
+
return curatedModelAsset(model, addon)?.src ?? model;
|
|
341
|
+
}
|
|
335
342
|
export const CUSTOM_TASK_TYPES = ['llm'];
|
|
336
343
|
let CUSTOM_MODELS = new Map();
|
|
337
344
|
export function setCustomModels(entries) {
|
|
@@ -75,6 +75,7 @@ declare class ModelPuddle extends ResourcePuddle<string, ModelHandle> {
|
|
|
75
75
|
readonly resources: Map<string, PuddleEntry>;
|
|
76
76
|
}
|
|
77
77
|
type PoolLike = ModelPuddle;
|
|
78
|
+
export declare function modelOf(key: string): string;
|
|
78
79
|
export declare function releaseModel(pool: PoolLike, key: string): void;
|
|
79
80
|
export declare function loadedModels(pool: PoolLike): LoadedModel[];
|
|
80
81
|
export interface ResidentLease {
|
|
@@ -86,6 +87,7 @@ export declare function leaseResidentModel(pool: PoolLike, wanted?: (model: stri
|
|
|
86
87
|
export declare function isResident(pool: PoolLike, key: string): boolean;
|
|
87
88
|
export declare function leaseCount(pool: PoolLike, key: string): number;
|
|
88
89
|
export declare function evictIdle(pool: PoolLike): Promise<void>;
|
|
90
|
+
export declare function evictIdleWhere(pool: PoolLike, wanted: (key: string) => boolean): Promise<void>;
|
|
89
91
|
export interface DeadlineOptions {
|
|
90
92
|
deadlineMs: number;
|
|
91
93
|
tickMs?: number;
|
|
@@ -95,9 +97,11 @@ export interface ModelCapacityOptions extends DeadlineOptions {
|
|
|
95
97
|
signal?: {
|
|
96
98
|
aborted?: boolean;
|
|
97
99
|
} | undefined;
|
|
100
|
+
evictable?: (key: string) => boolean;
|
|
98
101
|
}
|
|
99
|
-
export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal }: ModelCapacityOptions): Promise<boolean>;
|
|
100
|
-
export declare function evictModelWithin(pool: PoolLike, key: string,
|
|
102
|
+
export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal, evictable }: ModelCapacityOptions): Promise<boolean>;
|
|
103
|
+
export declare function evictModelWithin(pool: PoolLike, key: string, options: DeadlineOptions): Promise<boolean>;
|
|
104
|
+
export declare function evictModel(pool: PoolLike, model: string, options: DeadlineOptions): Promise<boolean>;
|
|
101
105
|
export type LoadSdk = () => Promise<ModelLifecycleSdk>;
|
|
102
106
|
export type EmbedLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySearch'> & Partial<CancelSdk> & {
|
|
103
107
|
loadModel(params: {
|
|
@@ -69,6 +69,9 @@ class ModelPuddle extends ResourcePuddle {
|
|
|
69
69
|
function entryOf(pool, key) {
|
|
70
70
|
return pool.resources.get(key);
|
|
71
71
|
}
|
|
72
|
+
export function modelOf(key) {
|
|
73
|
+
return key.split('\0')[0];
|
|
74
|
+
}
|
|
72
75
|
export function releaseModel(pool, key) {
|
|
73
76
|
if (pool.destroyed)
|
|
74
77
|
return;
|
|
@@ -123,8 +126,43 @@ export async function evictIdle(pool) {
|
|
|
123
126
|
await pool.gc();
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
export async function
|
|
127
|
-
|
|
129
|
+
export async function evictIdleWhere(pool, wanted) {
|
|
130
|
+
for (let tick = 0; tick <= pool.gcIdleTicks; tick++) {
|
|
131
|
+
if (pool.destroyed)
|
|
132
|
+
return;
|
|
133
|
+
const held = [];
|
|
134
|
+
for (const entry of pool.resources.values()) {
|
|
135
|
+
if (entry.refs === 0 && !wanted(entry.id)) {
|
|
136
|
+
held.push(entry.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (pool.idle === held.length) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const key of held) {
|
|
143
|
+
pool.open(key);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await pool.gc();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
for (const key of held) {
|
|
150
|
+
releaseSettled(pool, key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function residentCount(pool, wanted) {
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (const entry of pool.resources.values()) {
|
|
158
|
+
if (wanted(entry.id) && entry.value.peek() !== null) {
|
|
159
|
+
count++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return count;
|
|
163
|
+
}
|
|
164
|
+
export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250, signal, evictable = () => true }) {
|
|
165
|
+
const roomed = () => residentCount(pool, evictable) < cap;
|
|
128
166
|
let done = false;
|
|
129
167
|
let timer = null;
|
|
130
168
|
const expired = new Promise((resolve) => {
|
|
@@ -134,7 +172,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
134
172
|
}, deadlineMs);
|
|
135
173
|
});
|
|
136
174
|
const evicting = (async () => {
|
|
137
|
-
await
|
|
175
|
+
await evictIdleWhere(pool, evictable);
|
|
138
176
|
while (!roomed()) {
|
|
139
177
|
if (done || signal?.aborted)
|
|
140
178
|
return false;
|
|
@@ -142,7 +180,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
142
180
|
if (done || signal?.aborted) {
|
|
143
181
|
return false;
|
|
144
182
|
}
|
|
145
|
-
await
|
|
183
|
+
await evictIdleWhere(pool, evictable);
|
|
146
184
|
}
|
|
147
185
|
return true;
|
|
148
186
|
})();
|
|
@@ -156,18 +194,36 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
|
|
|
156
194
|
}
|
|
157
195
|
}
|
|
158
196
|
}
|
|
159
|
-
export
|
|
197
|
+
export function evictModelWithin(pool, key, options) {
|
|
198
|
+
return evictWhereWithin(pool, (candidate) => candidate === key, options);
|
|
199
|
+
}
|
|
200
|
+
export function evictModel(pool, model, options) {
|
|
201
|
+
return evictWhereWithin(pool, (key) => modelOf(key) === model, options);
|
|
202
|
+
}
|
|
203
|
+
async function evictWhereWithin(pool, wanted, { deadlineMs, tickMs = 250 }) {
|
|
204
|
+
let done = false;
|
|
160
205
|
const evicting = (async () => {
|
|
161
|
-
while (
|
|
162
|
-
await
|
|
163
|
-
if (
|
|
206
|
+
while (residentCount(pool, wanted) > 0) {
|
|
207
|
+
await evictIdleWhere(pool, wanted);
|
|
208
|
+
if (residentCount(pool, wanted) === 0) {
|
|
164
209
|
break;
|
|
165
210
|
}
|
|
166
211
|
await new Promise((resolve) => setTimeout(resolve, tickMs));
|
|
212
|
+
if (done) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
167
215
|
}
|
|
168
216
|
return true;
|
|
169
217
|
})();
|
|
170
|
-
|
|
218
|
+
try {
|
|
219
|
+
return await raceTimeout(evicting, { ms: deadlineMs, fallback: false });
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
done = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function definedFields(config) {
|
|
226
|
+
return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
|
|
171
227
|
}
|
|
172
228
|
function llmVerbosity() {
|
|
173
229
|
const raw = Number(process.env.QVAC_LLM_VERBOSITY ?? 0);
|
|
@@ -253,7 +309,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
253
309
|
const openclCache = deviceConfig.device !== 'cpu' && deviceConfig.openclCacheDir
|
|
254
310
|
? deviceConfig.openclCacheDir()
|
|
255
311
|
: undefined;
|
|
256
|
-
const modelConfig = {
|
|
312
|
+
const modelConfig = definedFields({
|
|
257
313
|
verbosity: llmVerbosity(),
|
|
258
314
|
tools: true,
|
|
259
315
|
stop_sequences: RUNAWAY_STOP_SEQUENCES,
|
|
@@ -265,7 +321,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
|
|
|
265
321
|
gpu_layers: deviceConfig.gpuLayers,
|
|
266
322
|
'main-gpu': mainGpu,
|
|
267
323
|
openclCacheDir: openclCache || undefined
|
|
268
|
-
};
|
|
324
|
+
});
|
|
269
325
|
const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
|
|
270
326
|
hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
|
|
271
327
|
const loadStarted = Date.now();
|