@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +31 -2
  4. package/dist-lib/index.js +365 -53
  5. package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
  6. package/dist-lib/lib/audio-transcode-bare.js +22 -25
  7. package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
  8. package/dist-lib/lib/audio-transcode-node.js +1 -3
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
  10. package/dist-lib/lib/capabilities/audio-resample.js +7 -8
  11. package/dist-lib/lib/capabilities/completion.d.ts +5 -2
  12. package/dist-lib/lib/capabilities/completion.js +22 -4
  13. package/dist-lib/lib/capabilities/download.d.ts +3 -1
  14. package/dist-lib/lib/capabilities/download.js +1 -1
  15. package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
  16. package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
  17. package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
  18. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  19. package/dist-lib/lib/capabilities/voice.js +94 -26
  20. package/dist-lib/lib/connect.d.ts +4 -1
  21. package/dist-lib/lib/connect.js +14 -1
  22. package/dist-lib/lib/contract-types.d.ts +10 -0
  23. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  24. package/dist-lib/lib/models/download-gate.js +73 -0
  25. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  26. package/dist-lib/lib/models/memory-cap.js +21 -0
  27. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  28. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  29. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  30. package/dist-lib/lib/models/model-catalog.js +35 -3
  31. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  32. package/dist-lib/lib/models/model-handle.js +20 -2
  33. package/dist-lib/lib/models/model-list.d.ts +8 -1
  34. package/dist-lib/lib/models/model-list.js +23 -0
  35. package/dist-lib/lib/models/model-pool.d.ts +33 -5
  36. package/dist-lib/lib/models/model-pool.js +97 -29
  37. package/dist-lib/lib/serve.js +18 -2
  38. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  39. package/dist-lib/lib/tools/generate-image.js +5 -2
  40. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  41. package/dist-lib/lib/tools/generate-music.js +5 -2
  42. package/dist-lib/lib/tools/translate.d.ts +3 -1
  43. package/dist-lib/lib/tools/translate.js +6 -1
  44. package/dist-lib/schema/harness.js +41 -3
  45. package/dist-lib/spec/bind.js +9 -0
  46. package/dist-lib/spec/calls.js +3 -0
  47. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  48. package/dist-lib/spec/hrpc/index.js +39 -3
  49. package/dist-lib/spec/hrpc/messages.js +93 -10
  50. package/dist-lib/spec/hyperschema/index.js +93 -10
  51. package/dist-lib/spec/hyperschema/schema.json +57 -1
  52. package/dist-lib/spec/hyperschema/types.d.ts +19 -2
  53. 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 { buildWavBuffer } from './lib/wav-encode.js';
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: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
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
- }, () => invalidateModelStatusCache(this._modelStatus));
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 ?? (this._retiredEmbedModels.has(pool) ? handle.peek() : null);
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 }, async (sdk, id) => {
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 = this._voiceLane(async () => {
1129
+ const job = (async () => {
1037
1130
  try {
1038
- modelId = (await handle.load()).modelId;
1039
- sdk = (await this._sdk.loadVoice());
1040
- if (signal?.aborted) {
1041
- return;
1042
- }
1043
- for await (const chunk of speakStreamOnce(sdk, modelId, text)) {
1044
- chunks.push(chunk);
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._unloadIdleGenerators();
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._loadAndRelease(target, signal);
1281
+ await this._loadByHand(target, signal);
1180
1282
  return { loaded: true };
1181
1283
  }
1182
1284
  if (!resident) {
1183
- if (!(await this._fits(model, target.ctxSize))) {
1184
- return { loaded: false, reason: 'memory' };
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._loadAndRelease(target, signal);
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 true;
1335
+ return null;
1234
1336
  }
1235
- const assessed = await this._assessFit({
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
- return assessed?.fit !== 'likely-too-large';
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 { pool: this._embedModels, key, open: () => this._embedModels.open(key, model) };
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
- return { pool, key, open: () => pool.open(key, key) };
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._unloadIdleGenerators();
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._unloadIdleGenerators();
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,