@bendyline/gezel-inference 1.0.8 → 1.0.9
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/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
- package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +5 -4
- package/dist/engines/electron-native-reuse.js +1 -1
- package/dist/engines/native-file-manifest.json +12344 -698
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +40 -36
- package/dist/engines/resolver.d.ts +18 -17
- package/dist/engines/resolver.js +36 -1
- package/dist/http/model-inventory-cache.d.ts +1 -1
- package/dist/http/routes/engines.js +3 -3
- package/dist/models/catalog-drift.d.ts +2 -0
- package/dist/models/catalog-drift.js +1 -1
- package/dist/models/catalog-payload-identity.d.ts +2 -2
- package/dist/models/catalog-payload-identity.js +10 -5
- package/dist/providers/active-install-message.js +1 -1
- package/dist/providers/catalog-model-config.d.ts +28 -0
- package/dist/providers/ds4/build-provider.js +35 -13
- package/dist/providers/ds4/residency.d.ts +77 -0
- package/dist/providers/ds4/residency.js +87 -6
- package/dist/providers/image/sd-cpp.js +3 -2
- package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
- package/dist/providers/llama-cpp/backend-health.js +6 -3
- package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
- package/dist/providers/llama-cpp/build-provider.js +168 -29
- package/dist/providers/llama-cpp/devices.js +4 -1
- package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
- package/dist/providers/llama-cpp/engine-flags.js +13 -8
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
- package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
- package/dist/providers/llama-cpp/models.d.ts +10 -0
- package/dist/providers/llama-cpp/models.js +64 -10
- package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
- package/dist/providers/llama-cpp/offload-planner.js +1 -1
- package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
- package/dist/providers/llama-cpp/spec-draft.js +113 -24
- package/dist/providers/mlx/python/spec_decode.py +23 -2
- package/dist/providers/mlx/python/tool_args_json.py +59 -21
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- package/dist/providers/native/device-capacity.d.ts +4 -0
- package/dist/providers/native/device-capacity.js +5 -0
- package/dist/providers/native/local-engine-runtime.d.ts +16 -1
- package/dist/providers/native/local-engine-runtime.js +142 -39
- package/dist/providers/native/supervisor.d.ts +8 -1
- package/dist/providers/native/supervisor.js +16 -7
- package/dist/providers/types.d.ts +4 -0
- package/dist/remotes/types.d.ts +1 -1
- package/package.json +4 -4
|
@@ -49,7 +49,7 @@ function gpuResidentThreads(hostCpuCount) {
|
|
|
49
49
|
: GPU_RESIDENT_THREADS;
|
|
50
50
|
return Math.max(1, Math.min(GPU_RESIDENT_THREADS, available));
|
|
51
51
|
}
|
|
52
|
-
const GPU_BACKENDS = new Set(['cuda', 'vulkan', 'metal']);
|
|
52
|
+
const GPU_BACKENDS = new Set(['cuda', 'rocm', 'vulkan', 'metal']);
|
|
53
53
|
/**
|
|
54
54
|
* `llamaCppExtraArgs` keys that either choose a thread pool or may move
|
|
55
55
|
* weights to the CPU; any of them makes the GPU-resident default stand down.
|
|
@@ -223,7 +223,7 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
223
223
|
// MTP is ON by default for a GGUF that carries the head, having cleared the
|
|
224
224
|
// A/B gate this comment used to wait for (reports/llama-mtp-eval-20260829.md):
|
|
225
225
|
// +12.7% / +4.1% / +15.6% / +19.3% decode at 5.5k / 21.7k / 54k / 87k
|
|
226
|
-
// context, never negative,
|
|
226
|
+
// context, never negative, +1.35 GB resident.
|
|
227
227
|
// The gain GROWS with context — long-context decode is bound by reading the
|
|
228
228
|
// KV cache, and verifying K drafted tokens costs one pass over it instead of
|
|
229
229
|
// K — which is the regime agent turns actually run in.
|
|
@@ -232,6 +232,8 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
232
232
|
// FATALLY when draft-mtp is selected for a model with no MTP tensors, so
|
|
233
233
|
// the default may only ever apply where the metadata confirms the head.
|
|
234
234
|
// Explicit config and manifest `spec.type` still win, including 'none'.
|
|
235
|
+
// `perModel` arrives with its `spec.byBackend` already applied by
|
|
236
|
+
// `resolveSpecDraft`, which also decided whether a drafter is in play.
|
|
235
237
|
const specType = config.llamaCppSpecType ?? perModel?.spec?.type ?? (ggufHasMtp ? 'draft-mtp' : undefined);
|
|
236
238
|
const safeSpecType = specType === 'draft-mtp' && !ggufHasMtp ? undefined : specType;
|
|
237
239
|
if (safeSpecType && safeSpecType !== 'none') {
|
|
@@ -244,12 +246,15 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
244
246
|
const nMax = config.llamaCppSpecDraftNMax ?? perModel?.spec?.nMax;
|
|
245
247
|
if (typeof nMax === 'number')
|
|
246
248
|
args.push('--spec-draft-n-max', String(nMax));
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
// MTP, simple and DFlash2 honor probabilistic selection; a DFlash 1 or
|
|
250
|
+
// LiLiCorr drafter takes the flag and ignores it. Leave the upstream greedy
|
|
251
|
+
// default untouched unless someone chose otherwise.
|
|
252
|
+
const draftSampling = config.llamaCppSpecDraftSampling ?? perModel?.spec?.sampling;
|
|
253
|
+
if ((safeSpecType === 'draft-mtp' ||
|
|
254
|
+
safeSpecType === 'draft-simple' ||
|
|
255
|
+
safeSpecType === 'draft-dflash') &&
|
|
256
|
+
draftSampling) {
|
|
257
|
+
args.push('--spec-draft-sampling', draftSampling);
|
|
253
258
|
}
|
|
254
259
|
}
|
|
255
260
|
// ── Server-side output parsing (`--reasoning-format`) ─────────────
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* word. A CUDA backend that loads but cannot initialize prints one error line
|
|
8
8
|
* and registers no devices. Either way the engine starts, answers, and runs
|
|
9
9
|
* every token on the CPU — at a fraction of the speed the person's hardware
|
|
10
|
-
* and Settings promise — while gezel reports the CUDA or Vulkan build
|
|
11
|
-
* launched.
|
|
10
|
+
* and Settings promise — while gezel reports the CUDA, ROCm or Vulkan build
|
|
11
|
+
* it launched.
|
|
12
12
|
*
|
|
13
13
|
* llama.cpp names every GPU it will use while loading a model
|
|
14
14
|
* (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
* without its GPU. The explicit lines say why when there is a why to say.
|
|
17
17
|
*/
|
|
18
18
|
export type GpuUnusedReason =
|
|
19
|
-
/** The CUDA runtime loaded and refused to start: `failed to initialize CUDA`. */
|
|
19
|
+
/** The CUDA or ROCm runtime loaded and refused to start: `failed to initialize CUDA`. */
|
|
20
20
|
'init-failed'
|
|
21
21
|
/** The backend loaded and found no device it could use. */
|
|
22
22
|
| 'no-devices'
|
|
23
23
|
/** No sign of the GPU backend at all: it failed to load, silently. */
|
|
24
24
|
| 'not-loaded';
|
|
25
25
|
export interface GpuUnused {
|
|
26
|
-
backend: 'cuda' | 'vulkan';
|
|
26
|
+
backend: 'cuda' | 'rocm' | 'vulkan';
|
|
27
27
|
reason: GpuUnusedReason;
|
|
28
28
|
/** The engine's own words, when it gave any. */
|
|
29
29
|
detail?: string;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* word. A CUDA backend that loads but cannot initialize prints one error line
|
|
8
8
|
* and registers no devices. Either way the engine starts, answers, and runs
|
|
9
9
|
* every token on the CPU — at a fraction of the speed the person's hardware
|
|
10
|
-
* and Settings promise — while gezel reports the CUDA or Vulkan build
|
|
11
|
-
* launched.
|
|
10
|
+
* and Settings promise — while gezel reports the CUDA, ROCm or Vulkan build
|
|
11
|
+
* it launched.
|
|
12
12
|
*
|
|
13
13
|
* llama.cpp names every GPU it will use while loading a model
|
|
14
14
|
* (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* without its GPU. The explicit lines say why when there is a why to say.
|
|
17
17
|
*/
|
|
18
18
|
const USING_GPU = /\busing device (?:CUDA|Vulkan|ROCm|SYCL|MTL)\d+\b/;
|
|
19
|
-
|
|
19
|
+
/** ggml's ROCm backend is its CUDA code under HIP, and names itself `ROCm` here. */
|
|
20
|
+
const CUDA_INIT_FAILED = /ggml_cuda_init: failed to initialize (?:CUDA|ROCm):\s*(.+?)\s*$/;
|
|
20
21
|
const VULKAN_NO_DEVICES = /ggml_vulkan: No devices found/;
|
|
21
22
|
/**
|
|
22
23
|
* Lines llama.cpp prints at the same verbosity as `using device`, while
|
|
@@ -46,9 +47,10 @@ export class GpuStartupWatch {
|
|
|
46
47
|
/** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
|
|
47
48
|
verdict() {
|
|
48
49
|
const backend = this.backend;
|
|
49
|
-
if ((backend !== 'cuda' && backend !== 'vulkan') || this.usingGpu)
|
|
50
|
+
if ((backend !== 'cuda' && backend !== 'rocm' && backend !== 'vulkan') || this.usingGpu) {
|
|
50
51
|
return null;
|
|
51
|
-
|
|
52
|
+
}
|
|
53
|
+
if ((backend === 'cuda' || backend === 'rocm') && this.initFailure) {
|
|
52
54
|
return { backend, reason: 'init-failed', detail: this.initFailure };
|
|
53
55
|
}
|
|
54
56
|
if (backend === 'vulkan' && this.noDevices)
|
|
@@ -73,6 +73,8 @@ export interface InstalledLlamaCppModel {
|
|
|
73
73
|
* `--spec-draft-model <path>` for the selected draft algorithm.
|
|
74
74
|
*/
|
|
75
75
|
draftModelPath?: string;
|
|
76
|
+
/** On-disk size of {@link draftModelPath}, for the memory plan. */
|
|
77
|
+
draftModelSizeBytes?: number;
|
|
76
78
|
/** From the GGUF metadata — useful for the supervisor's `-c` flag (Phase 3). */
|
|
77
79
|
contextWindow?: number;
|
|
78
80
|
/** Quantization tag (Q4_K_M / Q8_0 / …) lifted from the catalog source block. */
|
|
@@ -389,6 +391,14 @@ export declare class LlamaCppModelManager {
|
|
|
389
391
|
* a recorded digest plus an unchanged stat is identity.
|
|
390
392
|
*/
|
|
391
393
|
private planReusableFiles;
|
|
394
|
+
/**
|
|
395
|
+
* A drafter is trained against a model, not a quant, so every quant of that
|
|
396
|
+
* model pins the same file. Link a copy another installed quant verified
|
|
397
|
+
* rather than downloading a gigabyte again. A hard link keeps each model's
|
|
398
|
+
* directory complete on its own: deleting one quant leaves the other's
|
|
399
|
+
* drafter in place. A volume that cannot link just downloads.
|
|
400
|
+
*/
|
|
401
|
+
private adoptSiblingDraftModels;
|
|
392
402
|
/** Warn once per model directory per process, then stay quiet. */
|
|
393
403
|
private warnSkip;
|
|
394
404
|
private loadInstalled;
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { createHash } from 'node:crypto';
|
|
35
35
|
import { createReadStream } from 'node:fs';
|
|
36
|
-
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
36
|
+
import { link, mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
37
37
|
import { basename, join, resolve } from 'node:path';
|
|
38
|
-
import { createLogger, llamaCppBackendExclusion, llamaCppBackendLabel } from '@bendyline/gezel';
|
|
38
|
+
import { createLogger, llamaCppBackendExclusion, llamaCppBackendLabel, llamaCppDraftModelApplies, } from '@bendyline/gezel';
|
|
39
39
|
import { listBundleModelFiles, publishStagedModel, safeBundleModelPath, } from '../../models/bundle-storage.js';
|
|
40
40
|
import { evaluateCatalogDrift } from '../../models/catalog-drift.js';
|
|
41
41
|
import { catalogPayloadFingerprint, describeCatalogPayload, } from '../../models/catalog-payload-identity.js';
|
|
@@ -291,6 +291,7 @@ export class LlamaCppModelManager {
|
|
|
291
291
|
modelDir,
|
|
292
292
|
manifest: detail.manifest,
|
|
293
293
|
installedVersion,
|
|
294
|
+
llamaCppBackend: this.resolveBackend(),
|
|
294
295
|
installed: {
|
|
295
296
|
...(parsed.huggingfaceRepo ? { huggingfaceRepo: parsed.huggingfaceRepo } : {}),
|
|
296
297
|
...(parsed.payloadFingerprint ? { payloadFingerprint: parsed.payloadFingerprint } : {}),
|
|
@@ -474,7 +475,7 @@ export class LlamaCppModelManager {
|
|
|
474
475
|
await assertModelStorePathSafe(this.modelsRoot, itemDir);
|
|
475
476
|
await mkdir(itemDir, { recursive: true });
|
|
476
477
|
await assertModelStorePathSafe(this.modelsRoot, itemDir);
|
|
477
|
-
const plan = planDownloads(src, includeMmproj);
|
|
478
|
+
const plan = planDownloads(src, includeMmproj, this.resolveBackend());
|
|
478
479
|
if (plan.length === 0) {
|
|
479
480
|
yield {
|
|
480
481
|
type: 'error',
|
|
@@ -495,6 +496,7 @@ export class LlamaCppModelManager {
|
|
|
495
496
|
// the start, keep their digests, and are left untouched by the publish
|
|
496
497
|
// rename below.
|
|
497
498
|
const reusable = await this.planReusableFiles(itemDir, plan);
|
|
499
|
+
await this.adoptSiblingDraftModels(catalogId, itemDir, plan, reusable);
|
|
498
500
|
let bytesCompleted = 0;
|
|
499
501
|
for (const entry of plan) {
|
|
500
502
|
const sha = reusable.get(entry.destFilename);
|
|
@@ -709,7 +711,7 @@ export class LlamaCppModelManager {
|
|
|
709
711
|
// that only edits metadata can be recognized as such without re-reading a
|
|
710
712
|
// byte. Skipped for a skipSha install: those bytes are knowingly not the
|
|
711
713
|
// ones the catalog describes.
|
|
712
|
-
const catalogPayload = describeCatalogPayload(manifest, this.engine);
|
|
714
|
+
const catalogPayload = describeCatalogPayload(manifest, this.engine, this.resolveBackend());
|
|
713
715
|
const payloadFingerprint = catalogPayload && !skipSha ? catalogPayloadFingerprint(catalogPayload) : undefined;
|
|
714
716
|
// Recorded next to the catalog's tag, never instead of it: the catalog
|
|
715
717
|
// names the build a user chose, the header names what the bytes are.
|
|
@@ -795,6 +797,48 @@ export class LlamaCppModelManager {
|
|
|
795
797
|
}
|
|
796
798
|
return reusable;
|
|
797
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* A drafter is trained against a model, not a quant, so every quant of that
|
|
802
|
+
* model pins the same file. Link a copy another installed quant verified
|
|
803
|
+
* rather than downloading a gigabyte again. A hard link keeps each model's
|
|
804
|
+
* directory complete on its own: deleting one quant leaves the other's
|
|
805
|
+
* drafter in place. A volume that cannot link just downloads.
|
|
806
|
+
*/
|
|
807
|
+
async adoptSiblingDraftModels(catalogId, itemDir, plan, reusable) {
|
|
808
|
+
const drafters = plan.filter((entry) => entry.role === 'draft-model' && !reusable.has(entry.destFilename));
|
|
809
|
+
if (drafters.length === 0)
|
|
810
|
+
return;
|
|
811
|
+
const siblings = await readdir(this.modelsRoot).catch(() => []);
|
|
812
|
+
for (const entry of drafters) {
|
|
813
|
+
for (const sibling of siblings) {
|
|
814
|
+
if (sibling === catalogId)
|
|
815
|
+
continue;
|
|
816
|
+
const siblingDir = join(this.modelsRoot, sibling);
|
|
817
|
+
const recorded = await readFile(join(siblingDir, 'manifest.json'), 'utf8')
|
|
818
|
+
.then((raw) => JSON.parse(raw).fileSha256)
|
|
819
|
+
.catch(() => undefined);
|
|
820
|
+
if (recorded?.[entry.destFilename]?.toLowerCase() !== entry.sha256.toLowerCase())
|
|
821
|
+
continue;
|
|
822
|
+
const source = join(siblingDir, entry.destFilename);
|
|
823
|
+
const size = await stat(source)
|
|
824
|
+
.then((info) => (info.isFile() ? info.size : -1))
|
|
825
|
+
.catch(() => -1);
|
|
826
|
+
if (size !== entry.sizeBytes)
|
|
827
|
+
continue;
|
|
828
|
+
const dest = join(itemDir, entry.destFilename);
|
|
829
|
+
try {
|
|
830
|
+
await rm(dest, { force: true });
|
|
831
|
+
await link(source, dest);
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
reusable.set(entry.destFilename, entry.sha256.toLowerCase());
|
|
837
|
+
log.info(`[models] [${this.engine}] "${catalogId}": linked ${entry.destFilename} from "${sibling}"`);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
798
842
|
/** Warn once per model directory per process, then stay quiet. */
|
|
799
843
|
warnSkip(id, reason) {
|
|
800
844
|
if (this.skipWarned.has(id))
|
|
@@ -854,6 +898,11 @@ export class LlamaCppModelManager {
|
|
|
854
898
|
.then((s) => s.size)
|
|
855
899
|
.catch(() => 0)
|
|
856
900
|
: 0;
|
|
901
|
+
const draftModelSizeBytes = parsed.draftModelFilename
|
|
902
|
+
? await stat(join(root, id, parsed.draftModelFilename))
|
|
903
|
+
.then((s) => s.size)
|
|
904
|
+
.catch(() => 0)
|
|
905
|
+
: 0;
|
|
857
906
|
// Absent on every copy installed before the field existed, and on those
|
|
858
907
|
// the catalog tag is the only thing the table has to show.
|
|
859
908
|
const ggufQuantization = parsed.ggufQuantization ??
|
|
@@ -884,7 +933,10 @@ export class LlamaCppModelManager {
|
|
|
884
933
|
}
|
|
885
934
|
: {}),
|
|
886
935
|
...(parsed.draftModelFilename
|
|
887
|
-
? {
|
|
936
|
+
? {
|
|
937
|
+
draftModelPath: join(root, id, parsed.draftModelFilename),
|
|
938
|
+
...(draftModelSizeBytes > 0 ? { draftModelSizeBytes } : {}),
|
|
939
|
+
}
|
|
888
940
|
: {}),
|
|
889
941
|
...(parsed.contextWindow ? { contextWindow: parsed.contextWindow } : {}),
|
|
890
942
|
...(parsed.quantization ? { quantization: parsed.quantization } : {}),
|
|
@@ -928,7 +980,9 @@ function planDownloads(src,
|
|
|
928
980
|
* This option only governs llama.cpp's `mmproj`; a declared ds4
|
|
929
981
|
* `visionEncoder` is part of that model's install and is always fetched.
|
|
930
982
|
*/
|
|
931
|
-
includeMmproj = true
|
|
983
|
+
includeMmproj = true,
|
|
984
|
+
/** Decides a drafter limited to some llama-server backends. */
|
|
985
|
+
backend) {
|
|
932
986
|
const out = [];
|
|
933
987
|
if (src.shards && src.shards.length > 0) {
|
|
934
988
|
src.shards.forEach((shard, i) => {
|
|
@@ -976,10 +1030,10 @@ includeMmproj = true) {
|
|
|
976
1030
|
});
|
|
977
1031
|
}
|
|
978
1032
|
// Draft companions are part of the model's selected decoding path, not
|
|
979
|
-
// an optional modality. Download them whenever the catalog ships one
|
|
980
|
-
// an explicitly selected `draft-mtp`/other draft mode can
|
|
981
|
-
// after install without a second download.
|
|
982
|
-
if (src.draftModel) {
|
|
1033
|
+
// an optional modality. Download them whenever the catalog ships one for
|
|
1034
|
+
// this backend, so an explicitly selected `draft-mtp`/other draft mode can
|
|
1035
|
+
// work immediately after install without a second download.
|
|
1036
|
+
if (src.draftModel && llamaCppDraftModelApplies(src.draftModel, backend)) {
|
|
983
1037
|
out.push({
|
|
984
1038
|
...(src.draftModel.huggingfaceRepo
|
|
985
1039
|
? { huggingfaceRepo: src.draftModel.huggingfaceRepo }
|
|
@@ -94,6 +94,12 @@ export interface MoeOffloadInput {
|
|
|
94
94
|
* consulted on the exact path; 0 / absent = weights-only budgeting.
|
|
95
95
|
*/
|
|
96
96
|
kvReserveBytes?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Weights outside the GGUF split that stay on the GPU, such as a
|
|
99
|
+
* speculative drafter. The exact path adds them to its reserves;
|
|
100
|
+
* `residentBytes` already counts them.
|
|
101
|
+
*/
|
|
102
|
+
additionalGpuBytes?: number;
|
|
97
103
|
}
|
|
98
104
|
export interface MoeOffloadDecision {
|
|
99
105
|
/** `--n-gpu-layers` (−1 = all). Undefined = leave to the engine. */
|
|
@@ -167,7 +167,7 @@ function planFromSplit(input, split, margin) {
|
|
|
167
167
|
const kvReserve = Math.max(0, input.kvReserveBytes ?? 0);
|
|
168
168
|
const expertTotal = split.expertBytesByLayer.reduce((sum, v) => sum + v, 0);
|
|
169
169
|
const weightsTotal = split.nonExpertBytes + expertTotal;
|
|
170
|
-
const reserves = margin + kvReserve + COMPUTE_RESERVE_BYTES;
|
|
170
|
+
const reserves = margin + kvReserve + COMPUTE_RESERVE_BYTES + Math.max(0, input.additionalGpuBytes ?? 0);
|
|
171
171
|
if (weightsTotal + reserves <= input.vramBytes) {
|
|
172
172
|
return {
|
|
173
173
|
fullGpuResidency: true,
|
|
@@ -1,37 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Speculative-decoding draft resolution, extracted from the chat/manager
|
|
3
|
-
* launcher so it can be unit-tested in isolation.
|
|
4
|
-
*
|
|
5
|
-
* A model manifest names its `draft-simple` draft by CATALOG id
|
|
6
|
-
* (`spec.draftModelId`) so it stays portable across installs — a user's GGUF
|
|
7
|
-
* path differs from any hardcoded one. But engine-flags passes
|
|
8
|
-
* `--spec-draft-model` verbatim and a `draft-simple` spec with no draft model
|
|
9
|
-
* is a FATAL llama-server launch, so the id must be resolved to the installed
|
|
10
|
-
* GGUF path (or the spec disabled) BEFORE the argv is built. This module owns
|
|
11
|
-
* exactly that decision:
|
|
12
|
-
*
|
|
13
|
-
* - global `config.llamaCppDraftModelPath` set → operator's own path wins,
|
|
14
|
-
* nothing to resolve.
|
|
15
|
-
* - manifest draft resolves to an installed path → rewrite `draftModelId`
|
|
16
|
-
* to that path.
|
|
17
|
-
* - manifest-driven spec whose draft isn't installed → STRIP the spec so
|
|
18
|
-
* the launch falls back to plain decoding instead of crashing. The
|
|
19
|
-
* +decode win is opportunistic on the draft being present, never a hard
|
|
20
|
-
* dependency.
|
|
21
|
-
* - operator explicitly forced `config.llamaCppSpecType=draft-simple` but
|
|
22
|
-
* no draft path resolves → leave it and warn; the operator owns it.
|
|
23
|
-
*/
|
|
24
1
|
import type { PerModelLlamaCppEngineConfig } from './engine-flags.js';
|
|
25
2
|
export interface SpecDraftResolveInput {
|
|
26
3
|
/** The catalog manifest's `tuning.engine.llamaCpp` block, if any. */
|
|
27
4
|
perModel: PerModelLlamaCppEngineConfig | undefined;
|
|
5
|
+
/** The llama-server build's backend, for the manifest's `spec.byBackend`. */
|
|
6
|
+
backend?: string | undefined;
|
|
28
7
|
/** Global `config.llamaCppSpecType`, if the operator set one. */
|
|
29
8
|
configSpecType?: string | undefined;
|
|
30
9
|
/** Global `config.llamaCppDraftModelPath`, if the operator set one. */
|
|
31
10
|
configDraftModelPath?: string | undefined;
|
|
11
|
+
/** The catalog `draftModel` sidecar on disk beside the weights, if any. */
|
|
12
|
+
installedDraftModelPath?: string | undefined;
|
|
32
13
|
/** Resolve a catalog draft id → installed GGUF path (null if not installed). */
|
|
33
14
|
resolveDraftPath: (id: string) => Promise<string | null>;
|
|
34
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether the sidecar beside the weights is what this launch loads. A separate
|
|
18
|
+
* MTP assistant loads whenever MTP runs. A DFlash drafter sits beside a target
|
|
19
|
+
* that may have its own head, or on a backend its catalog entry did not turn it
|
|
20
|
+
* on for; handing it to MTP through `--spec-draft-model` would load the wrong
|
|
21
|
+
* file. `perBackend` is the manifest carrying a `spec.byBackend` table.
|
|
22
|
+
*/
|
|
23
|
+
export declare function llamaCppSidecarLoads(input: {
|
|
24
|
+
specType: string | undefined;
|
|
25
|
+
targetHasMtp: boolean;
|
|
26
|
+
perBackend: boolean;
|
|
27
|
+
}): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Engine and drafter pairs that took an engine down, so a later launch in this
|
|
30
|
+
* daemon goes without instead of failing the same way first. Keyed by both,
|
|
31
|
+
* because a drafter that one engine build cannot load may suit the next.
|
|
32
|
+
*/
|
|
33
|
+
export declare const brokenDrafters: Set<string>;
|
|
34
|
+
/** The parts of a KV plan a drafter must not change. */
|
|
35
|
+
export interface DrafterKvPlan {
|
|
36
|
+
kvCacheType: string;
|
|
37
|
+
ctxCapTokens?: number | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a drafter would cost the session something it runs out of. The
|
|
41
|
+
* target window and KV precision outlast a faster decode: a drafter that
|
|
42
|
+
* makes the plan cap the target, drop to a coarser KV cache, or stop fitting
|
|
43
|
+
* at all is left out. Adaptive growth above the target and slots pay for it,
|
|
44
|
+
* as they do for every resident cost.
|
|
45
|
+
*/
|
|
46
|
+
export declare function drafterCostsContext(withDrafter: DrafterKvPlan, withoutDrafter: DrafterKvPlan, fitsWithDrafter: boolean): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a drafter would push the launch past the graphics memory it has.
|
|
49
|
+
* `gpuPoolBytes` is what a discrete card leaves this launch now (free memory
|
|
50
|
+
* as device admission measures it, within the card's usable share), and
|
|
51
|
+
* undefined elsewhere. Checked against the card's total alone, a drafter
|
|
52
|
+
* stayed while admission refused the model it was meant to speed up: the
|
|
53
|
+
* 74B Bolt and the 27B on a 32 GB card beside a desktop holding 6.5 GB, both
|
|
54
|
+
* of which start without it (2026-11-07).
|
|
55
|
+
*/
|
|
56
|
+
export declare function drafterCostsGpuResidency(withDrafterBytes: number, gpuPoolBytes: number | undefined): boolean;
|
|
35
57
|
export interface SpecDraftResolution {
|
|
36
58
|
/** The perModel to hand to `buildLlamaCppEngineArgs` (spec rewritten or stripped). */
|
|
37
59
|
perModel: PerModelLlamaCppEngineConfig | undefined;
|
|
@@ -1,44 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speculative-decoding draft resolution, extracted from the chat/manager
|
|
3
|
+
* launcher so it can be unit-tested in isolation.
|
|
4
|
+
*
|
|
5
|
+
* A catalog entry can ask for a separate draft GGUF two ways: a `draftModel`
|
|
6
|
+
* sidecar installed beside the weights, or a `spec.draftModelId` naming another
|
|
7
|
+
* catalog model. engine-flags passes `--spec-draft-model` verbatim, and a draft
|
|
8
|
+
* algorithm with no draft model is a FATAL llama-server launch, so this has to
|
|
9
|
+
* be settled BEFORE the argv is built. This module owns that decision, and
|
|
10
|
+
* applies the catalog's per-backend overlay first so everything downstream of
|
|
11
|
+
* it reads one spec:
|
|
12
|
+
*
|
|
13
|
+
* - global `config.llamaCppDraftModelPath` set → operator's own path wins,
|
|
14
|
+
* nothing to resolve.
|
|
15
|
+
* - sidecar installed → use it.
|
|
16
|
+
* - manifest draft id resolves to an installed path → rewrite `draftModelId`
|
|
17
|
+
* to that path.
|
|
18
|
+
* - manifest-driven spec with no draft on disk → fall back so the launch
|
|
19
|
+
* decodes the way it would without the drafter instead of crashing. A live
|
|
20
|
+
* Gilde update reaches this: the new manifest takes effect at once, and the
|
|
21
|
+
* drafter arrives only when the person updates the model. The +decode win
|
|
22
|
+
* is opportunistic on the draft being present, never a hard dependency.
|
|
23
|
+
* - operator explicitly forced a draft `config.llamaCppSpecType` but no draft
|
|
24
|
+
* path resolves → leave it and warn; the operator owns it.
|
|
25
|
+
*/
|
|
26
|
+
import { llamaCppSpecForBackend, llamaCppSpecNeedsDraftModel } from '@bendyline/gezel';
|
|
27
|
+
/**
|
|
28
|
+
* Whether the sidecar beside the weights is what this launch loads. A separate
|
|
29
|
+
* MTP assistant loads whenever MTP runs. A DFlash drafter sits beside a target
|
|
30
|
+
* that may have its own head, or on a backend its catalog entry did not turn it
|
|
31
|
+
* on for; handing it to MTP through `--spec-draft-model` would load the wrong
|
|
32
|
+
* file. `perBackend` is the manifest carrying a `spec.byBackend` table.
|
|
33
|
+
*/
|
|
34
|
+
export function llamaCppSidecarLoads(input) {
|
|
35
|
+
const { specType, targetHasMtp, perBackend } = input;
|
|
36
|
+
if (llamaCppSpecNeedsDraftModel(specType))
|
|
37
|
+
return true;
|
|
38
|
+
if (specType && specType !== 'draft-mtp')
|
|
39
|
+
return false;
|
|
40
|
+
return !targetHasMtp && !perBackend;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Engine and drafter pairs that took an engine down, so a later launch in this
|
|
44
|
+
* daemon goes without instead of failing the same way first. Keyed by both,
|
|
45
|
+
* because a drafter that one engine build cannot load may suit the next.
|
|
46
|
+
*/
|
|
47
|
+
export const brokenDrafters = new Set();
|
|
48
|
+
/**
|
|
49
|
+
* Whether a drafter would cost the session something it runs out of. The
|
|
50
|
+
* target window and KV precision outlast a faster decode: a drafter that
|
|
51
|
+
* makes the plan cap the target, drop to a coarser KV cache, or stop fitting
|
|
52
|
+
* at all is left out. Adaptive growth above the target and slots pay for it,
|
|
53
|
+
* as they do for every resident cost.
|
|
54
|
+
*/
|
|
55
|
+
export function drafterCostsContext(withDrafter, withoutDrafter, fitsWithDrafter) {
|
|
56
|
+
return (!fitsWithDrafter ||
|
|
57
|
+
withDrafter.kvCacheType !== withoutDrafter.kvCacheType ||
|
|
58
|
+
withDrafter.ctxCapTokens !== withoutDrafter.ctxCapTokens);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether a drafter would push the launch past the graphics memory it has.
|
|
62
|
+
* `gpuPoolBytes` is what a discrete card leaves this launch now (free memory
|
|
63
|
+
* as device admission measures it, within the card's usable share), and
|
|
64
|
+
* undefined elsewhere. Checked against the card's total alone, a drafter
|
|
65
|
+
* stayed while admission refused the model it was meant to speed up: the
|
|
66
|
+
* 74B Bolt and the 27B on a 32 GB card beside a desktop holding 6.5 GB, both
|
|
67
|
+
* of which start without it (2026-11-07).
|
|
68
|
+
*/
|
|
69
|
+
export function drafterCostsGpuResidency(withDrafterBytes, gpuPoolBytes) {
|
|
70
|
+
return gpuPoolBytes !== undefined && withDrafterBytes > gpuPoolBytes;
|
|
71
|
+
}
|
|
72
|
+
/** The base spec with no overlay, or `perModel` with no spec at all. */
|
|
73
|
+
function withoutOverlay(perModel) {
|
|
74
|
+
const base = llamaCppSpecForBackend(perModel.spec, undefined);
|
|
75
|
+
return base ? { ...perModel, spec: base } : perModel;
|
|
76
|
+
}
|
|
77
|
+
function withoutSpec(perModel) {
|
|
78
|
+
const { spec: _spec, ...rest } = perModel;
|
|
79
|
+
return rest;
|
|
80
|
+
}
|
|
1
81
|
export async function resolveSpecDraft(input) {
|
|
2
|
-
const { perModel, configSpecType, configDraftModelPath
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
// draft path, a non-draft-simple spec, or no draft id → nothing to do.
|
|
6
|
-
if (effectiveSpecType !== 'draft-simple' ||
|
|
7
|
-
!perModel?.spec?.draftModelId ||
|
|
8
|
-
configDraftModelPath) {
|
|
82
|
+
const { perModel, backend, configSpecType, configDraftModelPath } = input;
|
|
83
|
+
const { installedDraftModelPath, resolveDraftPath } = input;
|
|
84
|
+
if (!perModel?.spec)
|
|
9
85
|
return { perModel, log: null };
|
|
86
|
+
const overlay = backend ? perModel.spec.byBackend?.[backend] : undefined;
|
|
87
|
+
// A person who forced another algorithm has not asked for this overlay's
|
|
88
|
+
// tuning: MTP must not inherit a sampling mode chosen for a drafter.
|
|
89
|
+
const overlayStands = !configSpecType || !overlay?.type || overlay.type === configSpecType;
|
|
90
|
+
const spec = llamaCppSpecForBackend(perModel.spec, overlayStands ? backend : undefined);
|
|
91
|
+
const applied = perModel.spec.byBackend ? { ...perModel, spec } : perModel;
|
|
92
|
+
const effectiveSpecType = configSpecType ?? spec?.type;
|
|
93
|
+
if (!llamaCppSpecNeedsDraftModel(effectiveSpecType) ||
|
|
94
|
+
configDraftModelPath ||
|
|
95
|
+
installedDraftModelPath) {
|
|
96
|
+
return { perModel: applied, log: null };
|
|
10
97
|
}
|
|
11
|
-
const draftId =
|
|
12
|
-
const draftPath = await resolveDraftPath(draftId).catch(() => null);
|
|
13
|
-
if (draftPath) {
|
|
98
|
+
const draftId = spec?.draftModelId;
|
|
99
|
+
const draftPath = draftId ? await resolveDraftPath(draftId).catch(() => null) : null;
|
|
100
|
+
if (draftPath && spec) {
|
|
14
101
|
return {
|
|
15
|
-
perModel: { ...
|
|
102
|
+
perModel: { ...applied, spec: { ...spec, draftModelId: draftPath } },
|
|
16
103
|
log: {
|
|
17
104
|
level: 'info',
|
|
18
105
|
message: `[llama-cpp] speculative decoding: draft "${draftId}" → ${draftPath}`,
|
|
19
106
|
},
|
|
20
107
|
};
|
|
21
108
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
109
|
+
const name = draftId ? `"${draftId}"` : 'sidecar';
|
|
110
|
+
if (configSpecType) {
|
|
111
|
+
// Operator forced the spec type via global config but gave no resolvable
|
|
112
|
+
// draft — leave it and let llama-server surface the error rather than
|
|
113
|
+
// silently overriding an explicit request.
|
|
26
114
|
return {
|
|
27
|
-
perModel:
|
|
115
|
+
perModel: applied,
|
|
28
116
|
log: {
|
|
29
|
-
level: '
|
|
30
|
-
message: `[llama-cpp]
|
|
117
|
+
level: 'warn',
|
|
118
|
+
message: `[llama-cpp] config.llamaCppSpecType=${configSpecType} but draft ${name} is not installed and no llamaCppDraftModelPath is set; the launch may fail.`,
|
|
31
119
|
},
|
|
32
120
|
};
|
|
33
121
|
}
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
122
|
+
// Manifest-driven spec, draft absent → the overlay's base if that decodes
|
|
123
|
+
// without one, else no spec at all.
|
|
124
|
+
const base = overlay ? withoutOverlay(perModel) : undefined;
|
|
125
|
+
const fallback = base && !llamaCppSpecNeedsDraftModel(base.spec?.type) ? base : withoutSpec(perModel);
|
|
37
126
|
return {
|
|
38
|
-
perModel,
|
|
127
|
+
perModel: fallback,
|
|
39
128
|
log: {
|
|
40
|
-
level: '
|
|
41
|
-
message: `[llama-cpp]
|
|
129
|
+
level: 'info',
|
|
130
|
+
message: `[llama-cpp] speculative decoding (manifest draft ${name}) skipped: draft model not installed; running without it.`,
|
|
42
131
|
},
|
|
43
132
|
};
|
|
44
133
|
}
|
|
@@ -682,6 +682,21 @@ def abort_verify_round(verify) -> None:
|
|
|
682
682
|
abort()
|
|
683
683
|
|
|
684
684
|
|
|
685
|
+
def shared_kv_after_round(verify, rejected: int, slice_after_reject):
|
|
686
|
+
"""The drafter's shared-KV view for the next round.
|
|
687
|
+
|
|
688
|
+
mlx-vlm 0.7.12 lets a target rebuild that view from its committed caches
|
|
689
|
+
(Gemma 4's ``shared_kv_from_cache``): ``verify.commit`` then empties the
|
|
690
|
+
verify window and records ``committed_shared_kv``. Slicing the emptied
|
|
691
|
+
window would hand the drafter no context, which costs acceptance, never
|
|
692
|
+
exactness. Targets without the hook (the Qwen 3.5 family) keep the slice.
|
|
693
|
+
"""
|
|
694
|
+
committed = getattr(verify, "committed_shared_kv", None)
|
|
695
|
+
if committed is not None:
|
|
696
|
+
return committed
|
|
697
|
+
return slice_after_reject(verify.shared_kv_states, rejected)
|
|
698
|
+
|
|
699
|
+
|
|
685
700
|
def assisted_rounds(model, spec: SpecState, cache, hidden_tokens: List[int],
|
|
686
701
|
last_output, first_bonus: int, max_tokens: int, *,
|
|
687
702
|
sampler, processors, history):
|
|
@@ -734,9 +749,13 @@ def assisted_rounds(model, spec: SpecState, cache, hidden_tokens: List[int],
|
|
|
734
749
|
hidden = _mtp_draft_hidden(lm, hidden)
|
|
735
750
|
|
|
736
751
|
kv_offset = _mtp_cache_offset_max(cache)
|
|
752
|
+
rebuild_shared_kv = getattr(lm, "shared_kv_from_cache", None)
|
|
753
|
+
if callable(rebuild_shared_kv):
|
|
754
|
+
shared_kv = rebuild_shared_kv(cache)
|
|
737
755
|
drafter.set_shared_kv(
|
|
738
756
|
shared_kv, kv_offset, position=_mtp_draft_position(kv_offset), kv_valid_len=kv_offset
|
|
739
757
|
)
|
|
758
|
+
release_shared_kv = getattr(drafter, "release_shared_kv", None)
|
|
740
759
|
|
|
741
760
|
b = int(first_bonus)
|
|
742
761
|
emitted = 1
|
|
@@ -753,6 +772,8 @@ def assisted_rounds(model, spec: SpecState, cache, hidden_tokens: List[int],
|
|
|
753
772
|
b, hidden, None, bs, draw, token_dtype, **draft_kwargs
|
|
754
773
|
)
|
|
755
774
|
mx.async_eval(draft_tokens)
|
|
775
|
+
if callable(release_shared_kv):
|
|
776
|
+
release_shared_kv()
|
|
756
777
|
|
|
757
778
|
verify_input = mx.concatenate(
|
|
758
779
|
[mx.array([[b]], dtype=token_dtype), draft_tokens], axis=1
|
|
@@ -806,8 +827,8 @@ def assisted_rounds(model, spec: SpecState, cache, hidden_tokens: List[int],
|
|
|
806
827
|
hidden = _mtp_draft_hidden(lm, verify.hidden[:, accepted : accepted + 1, :])
|
|
807
828
|
b = new_tokens[-1] if new_tokens else b
|
|
808
829
|
|
|
809
|
-
shared_kv =
|
|
810
|
-
verify
|
|
830
|
+
shared_kv = shared_kv_after_round(
|
|
831
|
+
verify, bs - (accepted + 1), _slice_shared_kv_after_reject
|
|
811
832
|
)
|
|
812
833
|
kv_offset += accepted + 1
|
|
813
834
|
drafter.set_shared_kv(
|