@bendyline/gezel-inference 1.0.7 → 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/http/routes/v1-images.d.ts +2 -1
- package/dist/http/routes/v1-images.js +36 -9
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- 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 +19 -11
- 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/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +28 -16
- 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/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
- package/dist/providers/native/device-capacity-ledger.js +35 -20
- 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/process-identity.d.ts +38 -0
- package/dist/providers/native/process-identity.js +86 -0
- 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/dist/runtime-lock.d.ts +3 -27
- package/dist/runtime-lock.js +7 -80
- package/package.json +4 -4
|
@@ -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
|
}
|
|
@@ -7,5 +7,13 @@
|
|
|
7
7
|
* recipients.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isSuccessfulAsyncHandoff(toolName: string, output: string): boolean;
|
|
10
|
-
|
|
10
|
+
/** The gezel a successful handoff result names, when it names one. */
|
|
11
|
+
export declare function asyncHandoffRecipient(output: string): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The reply the person sees when the runtime ends a delegating turn, which the
|
|
14
|
+
* model also reads back as its own last message. The tool result already told
|
|
15
|
+
* the model to end its turn, so this line only says what happened. Each entry
|
|
16
|
+
* is one handoff's recipient, or undefined when its result named nobody.
|
|
17
|
+
*/
|
|
18
|
+
export declare function asyncHandoffClosing(recipients: readonly (string | undefined)[]): string;
|
|
11
19
|
export declare function immediateFileWriteClosing(paths: string[]): string;
|
|
@@ -10,10 +10,38 @@ export function isSuccessfulAsyncHandoff(toolName, output) {
|
|
|
10
10
|
return (!output.startsWith('ERROR:') &&
|
|
11
11
|
(toolName === 'message_gezel' || toolName.startsWith('delegate_')));
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The handoff results (core's `messageGezelText`, the MCP `delegate_*` tools)
|
|
15
|
+
* name the recipient after a fixed lead-in and again as "<name>'s turn".
|
|
16
|
+
* Requiring both means a reworded result, or a name holding a period, gives
|
|
17
|
+
* no name rather than half of one.
|
|
18
|
+
*/
|
|
19
|
+
const HANDOFF_RECIPIENT = /(?:Accepted the (?:message|handoff) for|Dispatched the (?:message|handoff) to|An identical file handoff is already pending with) (.+?)(?= \(|[.;])[\s\S]*?\1's turn/;
|
|
20
|
+
/** The gezel a successful handoff result names, when it names one. */
|
|
21
|
+
export function asyncHandoffRecipient(output) {
|
|
22
|
+
return HANDOFF_RECIPIENT.exec(output.slice(0, 2_000))?.[1]?.trim() || undefined;
|
|
23
|
+
}
|
|
24
|
+
function joinNames(names) {
|
|
25
|
+
if (names.length <= 2)
|
|
26
|
+
return names.join(' and ');
|
|
27
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The reply the person sees when the runtime ends a delegating turn, which the
|
|
31
|
+
* model also reads back as its own last message. The tool result already told
|
|
32
|
+
* the model to end its turn, so this line only says what happened. Each entry
|
|
33
|
+
* is one handoff's recipient, or undefined when its result named nobody.
|
|
34
|
+
*/
|
|
35
|
+
export function asyncHandoffClosing(recipients) {
|
|
36
|
+
const named = [...new Set(recipients.filter((name) => Boolean(name)))];
|
|
37
|
+
if (named.length > 0 && !recipients.includes(undefined)) {
|
|
38
|
+
return named.length === 1
|
|
39
|
+
? `${named[0]} is on it. Their reply will appear here.`
|
|
40
|
+
: `${joinNames(named)} are on it. Their replies will appear here.`;
|
|
41
|
+
}
|
|
42
|
+
return recipients.length > 1
|
|
43
|
+
? "I've passed this on. The replies will appear here."
|
|
44
|
+
: "I've passed this on. The reply will appear here.";
|
|
17
45
|
}
|
|
18
46
|
export function immediateFileWriteClosing(paths) {
|
|
19
47
|
const unique = [...new Set(paths.filter((path) => path.trim().length > 0))];
|
|
@@ -51,7 +51,7 @@ import { coerceToolCallArgs } from '../tool-arg-schema-coercion.js';
|
|
|
51
51
|
import { createToolExecutor } from '../tool-executor.js';
|
|
52
52
|
import { ToolFailureTracker } from '../tool-failure-tracker.js';
|
|
53
53
|
import { ToolRepeatTracker } from '../tool-repeat-tracker.js';
|
|
54
|
-
import { asyncHandoffClosing, immediateFileWriteClosing, isSuccessfulAsyncHandoff, } from './async-file-handoff.js';
|
|
54
|
+
import { asyncHandoffClosing, asyncHandoffRecipient, immediateFileWriteClosing, isSuccessfulAsyncHandoff, } from './async-file-handoff.js';
|
|
55
55
|
export { isSuccessfulAsyncHandoff };
|
|
56
56
|
import { setChatTemplateKwarg, } from './chat-protocol.js';
|
|
57
57
|
import { MlxEngineGate } from './engine-gate.js';
|
|
@@ -60,7 +60,7 @@ import { EngineLogRouter } from './engine-log-router.js';
|
|
|
60
60
|
import { MlxEngineStatsSampler } from './engine-stats-sampler.js';
|
|
61
61
|
import { REASONING_OPEN_HEADER, StreamingReasoningSplit, engineOpensReasoning, reopenTemplateReasoning, requestOpensReasoning, } from './reasoning-stream.js';
|
|
62
62
|
import { applyMlxRequestShape } from './request-shape.js';
|
|
63
|
-
import { PRE_FIRST_BYTE_BASE_MS, buildMidStreamDropMessage, buildPreFirstByteAbortMessage, computePreFirstByteBudgetMs, estimatePromptTokens, formatTps, isMidStreamConnectionDrop, mlxHttpError, } from './runtime-diagnostics.js';
|
|
63
|
+
import { MLX_IMAGE_RESERVED_CHARS, PRE_FIRST_BYTE_BASE_MS, buildMidStreamDropMessage, buildPreFirstByteAbortMessage, computePreFirstByteBudgetMs, estimatePromptTokens, formatTps, isMidStreamConnectionDrop, mlxHttpError, } from './runtime-diagnostics.js';
|
|
64
64
|
import { classifyMlxFatalErrorLine, classifyMlxStartupLine, } from './stdout-parser.js';
|
|
65
65
|
import { salvageTextualToolCalls } from './textual-tool-call-salvage.js';
|
|
66
66
|
import { APPEND_TO_FILE_CONTINUATION_TOOL, MlxToolCallAccumulator, applyCallableToolGrammar, callableRequestToolNames, chatCompletionToolName, externalToolsAsChatCompletions, hermesRequiredArgGrammarRequested, missingTopLevelRequiredToolArgs, toChatCompletionsTools, validatorReportedMissingRequiredArgs, } from './tool-call-protocol.js';
|
|
@@ -646,7 +646,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
646
646
|
total += m.content.length;
|
|
647
647
|
// The sidecar bounds images to 1024px. Reserve patch tokens without
|
|
648
648
|
// counting base64 bytes as language tokens or treating pixels as free.
|
|
649
|
-
total += (m.images?.length ?? 0) *
|
|
649
|
+
total += (m.images?.length ?? 0) * MLX_IMAGE_RESERVED_CHARS;
|
|
650
650
|
if (m.tool_calls) {
|
|
651
651
|
for (const tc of m.tool_calls)
|
|
652
652
|
total += tc.function.arguments.length + tc.function.name.length;
|
|
@@ -1427,7 +1427,8 @@ class MlxSession extends StreamingSessionBase {
|
|
|
1427
1427
|
// - STREAMING_IDLE_MS: budget between two consecutive SSE
|
|
1428
1428
|
// content events once tokens are flowing. Generous enough
|
|
1429
1429
|
// to cover a heavy reasoning gap on a 26B model.
|
|
1430
|
-
|
|
1430
|
+
// The request as sent: an image retired from it costs no prefill.
|
|
1431
|
+
const approxPromptTokens = estimatePromptTokens(body.messages, body.tools);
|
|
1431
1432
|
const PRE_FIRST_BYTE_IDLE_MS = computePreFirstByteBudgetMs(approxPromptTokens);
|
|
1432
1433
|
const PRE_FIRST_BYTE_PER_PROGRESS_MS = PRE_FIRST_BYTE_IDLE_MS;
|
|
1433
1434
|
const STREAMING_IDLE_MS = 300_000;
|
|
@@ -2168,6 +2169,11 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2168
2169
|
});
|
|
2169
2170
|
turnContent = salvaged.turnContent;
|
|
2170
2171
|
const { proseRepaired, xmlTagRepaired, claudeInvokeRepaired, glmRepaired, hermesRepaired, shellRepaired, envelopeRepaired, envelopeTruncated, unknownCall, truncatedSalvage, bareInvokeRepaired, } = salvaged;
|
|
2172
|
+
// A prose-shaped call such as lookup_record({}) was ordinary TEXT
|
|
2173
|
+
// to the Hermes grammar, not a constrained tool envelope. Missing
|
|
2174
|
+
// arguments in that salvage cannot establish a decoding violation.
|
|
2175
|
+
// Keep provenance by id through coercion and duplicate collapse.
|
|
2176
|
+
const proseCallIds = new Set(proseRepaired.map((call) => call.id));
|
|
2171
2177
|
const rawTurnContentBeforeReasoning = turnContent;
|
|
2172
2178
|
// Reasoning-tag extraction applies regardless of which (if any)
|
|
2173
2179
|
// salvage path fired. Qwen 3.5/3.6 leak `<think>…</think>`
|
|
@@ -2602,7 +2608,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2602
2608
|
tool_calls: toolCalls,
|
|
2603
2609
|
});
|
|
2604
2610
|
let abortDueToFailureLoop = null;
|
|
2605
|
-
|
|
2611
|
+
const asyncHandoffRecipients = [];
|
|
2606
2612
|
let terminalActionClosing = null;
|
|
2607
2613
|
const toolImages = [];
|
|
2608
2614
|
const immediateFileWritePaths = [];
|
|
@@ -2701,16 +2707,21 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2701
2707
|
this.deps.bridges.hasTool(call.function.name)) {
|
|
2702
2708
|
const missingRequired = missingTopLevelRequiredToolArgs(toolArgSchemas.get(call.function.name), args);
|
|
2703
2709
|
const grammarContractViolation = missingRequired.length > 0 &&
|
|
2710
|
+
!proseCallIds.has(call.id) &&
|
|
2704
2711
|
hermesRequiredArgGrammarRequested(body) &&
|
|
2705
2712
|
!this.forceSequentialToolGrammar;
|
|
2706
|
-
if (
|
|
2707
|
-
|
|
2708
|
-
|
|
2713
|
+
if (missingRequired.length > 0) {
|
|
2714
|
+
if (grammarContractViolation) {
|
|
2715
|
+
this.forceSequentialToolGrammar = true;
|
|
2716
|
+
log.warn(`turn#${seq}.${turn} required-argument grammar contract violation tool=${call.function.name} missing=${missingRequired.join(',')} — latching this session to sequential decode`);
|
|
2717
|
+
}
|
|
2709
2718
|
// Do not send a call that the advertised schema proves cannot
|
|
2710
|
-
// pass validation
|
|
2711
|
-
//
|
|
2712
|
-
//
|
|
2713
|
-
output =
|
|
2719
|
+
// pass validation, regardless of how it was parsed. A rejected
|
|
2720
|
+
// prose salvage gets ordinary corrective feedback and retains
|
|
2721
|
+
// speculation; native grammar violations keep the circuit breaker.
|
|
2722
|
+
output = grammarContractViolation
|
|
2723
|
+
? `ERROR: ${call.function.name} was not executed because the constrained decoder omitted required fields: ${missingRequired.join(', ')}. The runtime switched this session to sequential decoding; retry the call once.`
|
|
2724
|
+
: `ERROR: ${call.function.name} was not executed because its arguments are missing required fields: ${missingRequired.join(', ')}. Retry the call with all declared required fields.`;
|
|
2714
2725
|
}
|
|
2715
2726
|
else {
|
|
2716
2727
|
try {
|
|
@@ -2741,6 +2752,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2741
2752
|
// the advertised schema. Sequential decode may still recover on
|
|
2742
2753
|
// the next iteration, and the log now makes the skew explicit.
|
|
2743
2754
|
if (validatorReportedMissingRequiredArgs(output) &&
|
|
2755
|
+
!proseCallIds.has(call.id) &&
|
|
2744
2756
|
hermesRequiredArgGrammarRequested(body) &&
|
|
2745
2757
|
!this.forceSequentialToolGrammar) {
|
|
2746
2758
|
this.forceSequentialToolGrammar = true;
|
|
@@ -2755,7 +2767,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2755
2767
|
askedQuestionThisTurn = true;
|
|
2756
2768
|
}
|
|
2757
2769
|
if (isSuccessfulAsyncHandoff(call.function.name, output)) {
|
|
2758
|
-
|
|
2770
|
+
asyncHandoffRecipients.push(asyncHandoffRecipient(output));
|
|
2759
2771
|
}
|
|
2760
2772
|
if ((immediateFileWriteTurn || writeContinuationActive) &&
|
|
2761
2773
|
(call.function.name === 'write_file' || call.function.name === 'append_to_file') &&
|
|
@@ -2930,12 +2942,12 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2930
2942
|
`${writeContinuations > 0 ? ` continuations=${writeContinuations}` : ''}`);
|
|
2931
2943
|
return fullText;
|
|
2932
2944
|
}
|
|
2933
|
-
if (
|
|
2934
|
-
const closingText = asyncHandoffClosing(
|
|
2945
|
+
if (asyncHandoffRecipients.length > 0) {
|
|
2946
|
+
const closingText = asyncHandoffClosing(asyncHandoffRecipients);
|
|
2935
2947
|
this.messages.push({ role: 'assistant', content: closingText });
|
|
2936
2948
|
fullText = closingText;
|
|
2937
2949
|
log.info(`turn#${seq} END async-handoff-bail afterMs=${Date.now() - start} ` +
|
|
2938
|
-
`handoffs=${
|
|
2950
|
+
`handoffs=${asyncHandoffRecipients.length} loopTurns=${turn + 1}`);
|
|
2939
2951
|
return fullText;
|
|
2940
2952
|
}
|
|
2941
2953
|
if (forceProjectMacroBail) {
|
|
@@ -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(
|