@bendyline/gezel-inference 1.0.1 → 1.0.2
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/engines/native-file-manifest.json +404 -404
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +34 -34
- package/dist/http/openai-compat/engine-failure.d.ts +25 -0
- package/dist/http/openai-compat/engine-failure.js +76 -0
- package/dist/http/openai-compat/responses-streaming.js +2 -1
- package/dist/http/openai-compat/responses-translate.d.ts +1 -1
- package/dist/http/openai-compat/translate.d.ts +1 -1
- package/dist/http/routes/engines.js +35 -0
- package/dist/http/routes/v1-chat.js +10 -1
- package/dist/http/routes/v1-models.js +6 -4
- package/dist/http/routes/v1-responses.js +7 -1
- package/dist/knowledge/catalog-host.d.ts +9 -1
- package/dist/knowledge/catalog-host.js +2 -0
- package/dist/knowledge/manager.d.ts +14 -0
- package/dist/knowledge/manager.js +53 -6
- package/dist/knowledge/search-worker.d.ts +1 -1
- package/dist/knowledge/spatial-query.d.ts +2 -3
- package/dist/knowledge/spatial-query.js +12 -6
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/memory/embed-core.d.ts +16 -0
- package/dist/memory/embed-core.js +31 -0
- package/dist/memory/embed-worker.d.ts +7 -3
- package/dist/memory/embed-worker.js +13 -4
- package/dist/memory/embeddings.d.ts +17 -2
- package/dist/memory/embeddings.js +36 -7
- package/dist/models/ensure.d.ts +2 -0
- package/dist/models/ensure.js +9 -0
- package/dist/perf/responsiveness.d.ts +14 -1
- package/dist/perf/responsiveness.js +77 -7
- package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
- package/dist/providers/llama-cpp/backend-health.js +64 -0
- package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
- package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
- package/dist/providers/llama-cpp/build-provider.js +141 -144
- package/dist/providers/llama-cpp/cache-adapter.js +2 -1
- package/dist/providers/llama-cpp/devices.d.ts +11 -2
- package/dist/providers/llama-cpp/devices.js +10 -4
- package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
- package/dist/providers/llama-cpp/engine-flags.js +12 -18
- package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
- package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
- package/dist/providers/llama-cpp/models.d.ts +6 -0
- package/dist/providers/llama-cpp/models.js +19 -1
- package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
- package/dist/providers/llama-cpp/offload-planner.js +3 -14
- package/dist/providers/llama-cpp/provider.d.ts +8 -0
- package/dist/providers/llama-cpp/provider.js +4 -1
- package/dist/providers/mlx/provider.js +3 -4
- package/dist/providers/mlx/python/qwen4_ple.py +9 -2
- package/dist/providers/native/capacity-broker.d.ts +21 -15
- package/dist/providers/native/capacity-broker.js +16 -10
- package/dist/providers/native/capacity-budget.d.ts +35 -3
- package/dist/providers/native/capacity-budget.js +49 -5
- package/dist/providers/native/device-capacity.d.ts +1 -1
- package/dist/providers/native/device-capacity.js +23 -5
- package/dist/providers/native/local-engine-runtime.js +16 -19
- package/dist/providers/native/measured-budget.js +2 -0
- package/dist/providers/native/supervisor.d.ts +25 -0
- package/dist/providers/native/supervisor.js +59 -7
- package/dist/system/amdgpu-adapters.d.ts +18 -0
- package/dist/system/amdgpu-adapters.js +55 -0
- package/dist/system/gpu-carve-out.d.ts +84 -0
- package/dist/system/gpu-carve-out.js +111 -0
- package/dist/system/memory.d.ts +17 -1
- package/dist/system/memory.js +94 -13
- package/dist/transformers-cache.d.ts +2 -0
- package/dist/transformers-cache.js +6 -3
- package/package.json +5 -5
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { totalmem } from 'node:os';
|
|
20
21
|
import { delimiter, dirname, join } from 'node:path';
|
|
21
22
|
import { promisify } from 'node:util';
|
|
22
23
|
import { windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
|
|
@@ -124,20 +125,25 @@ function cachePath(home) {
|
|
|
124
125
|
/**
|
|
125
126
|
* Probe device memory by running `<binaryPath> --list-devices`, caching
|
|
126
127
|
* the result. Reads the cache when present (VRAM capacity is stable per
|
|
127
|
-
* machine)
|
|
128
|
+
* machine) and the OS still reports the RAM it was probed with; pass
|
|
129
|
+
* `force` to re-probe. Any failure (spawn error, no
|
|
128
130
|
* cache, unparseable output) resolves to an empty device list — the
|
|
129
131
|
* planner then simply does nothing and the engine's own `--fit`/`-ngl
|
|
130
|
-
* auto` takes over. `nowIso`
|
|
132
|
+
* auto` takes over. `nowIso` and `systemRamBytes` are injectable for
|
|
133
|
+
* deterministic tests.
|
|
131
134
|
*/
|
|
132
135
|
export async function probeLlamaDevices(opts) {
|
|
133
136
|
const { binaryPath, home, force } = opts;
|
|
134
137
|
const now = opts.nowIso ?? (() => new Date().toISOString());
|
|
138
|
+
const systemRamBytes = opts.systemRamBytes ?? totalmem();
|
|
135
139
|
const path = cachePath(home);
|
|
136
140
|
if (!force && existsSync(path)) {
|
|
137
141
|
try {
|
|
138
142
|
const cached = JSON.parse(readFileSync(path, 'utf8'));
|
|
139
|
-
|
|
143
|
+
// A cache without the RAM figure predates the check and re-probes once.
|
|
144
|
+
if (Array.isArray(cached.devices) && cached.systemRamBytes === systemRamBytes) {
|
|
140
145
|
return { ...cached, cached: true };
|
|
146
|
+
}
|
|
141
147
|
}
|
|
142
148
|
catch {
|
|
143
149
|
// fall through to a fresh probe
|
|
@@ -165,7 +171,7 @@ export async function probeLlamaDevices(opts) {
|
|
|
165
171
|
// Leave devices empty — caller treats "no devices" as "no offload target".
|
|
166
172
|
return { devices, probedAt: now(), cached: false };
|
|
167
173
|
}
|
|
168
|
-
const probe = { devices, probedAt: now(), cached: false };
|
|
174
|
+
const probe = { devices, probedAt: now(), cached: false, systemRamBytes };
|
|
169
175
|
try {
|
|
170
176
|
mkdirSync(dirname(path), { recursive: true });
|
|
171
177
|
writeFileSync(path, JSON.stringify(probe, null, 2), 'utf8');
|
|
@@ -126,24 +126,6 @@ export interface EngineFlagInput {
|
|
|
126
126
|
* when the client also replays `reasoning_content`.
|
|
127
127
|
*/
|
|
128
128
|
reasoningPreserve?: boolean | undefined;
|
|
129
|
-
/**
|
|
130
|
-
* The model's GGUF `general.architecture`, for the Gemma-family auto
|
|
131
|
-
* defaults (sliding-window full cache). Undefined disables the arch
|
|
132
|
-
* heuristic; an explicit config/manifest `swaFull` still applies.
|
|
133
|
-
*/
|
|
134
|
-
architecture?: string | undefined;
|
|
135
|
-
/** The resolved model id, a fallback for `architecture` (see isGemmaModel). */
|
|
136
|
-
modelId?: string | undefined;
|
|
137
|
-
/**
|
|
138
|
-
* Whether resident weights plus the full SWA KV cache fit the host's
|
|
139
|
-
* fast-memory pool (VRAM on a discrete GPU). The exact estimate honors
|
|
140
|
-
* per-layer SWA dimensions and shared-KV layers. `false` declines the
|
|
141
|
-
* auto-default so a low-memory device keeps the model's full context on
|
|
142
|
-
* the windowed cache instead of spilling or OOMing. `undefined` (unknown
|
|
143
|
-
* host/metadata) and `true` retain the compatibility default. An explicit
|
|
144
|
-
* config/manifest `swaFull` wins regardless.
|
|
145
|
-
*/
|
|
146
|
-
swaFullAutoFits?: boolean | undefined;
|
|
147
129
|
/**
|
|
148
130
|
* The llama-server build's backend (`cuda`, `vulkan`, `metal`, `cpu`).
|
|
149
131
|
* Only a GPU backend can receive the GPU-resident thread default.
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* dependency; the real `GezelConfig` / `LlamaCppEngineConfig` satisfy
|
|
22
22
|
* these shapes by construction.
|
|
23
23
|
*/
|
|
24
|
-
import { isGemmaModel } from './kv-cache-type.js';
|
|
25
24
|
/** Default `--cache-reuse` chunk when neither config nor manifest set it. */
|
|
26
25
|
export const DEFAULT_CACHE_REUSE = 256;
|
|
27
26
|
/**
|
|
@@ -101,7 +100,7 @@ function normalizeFlagKey(key) {
|
|
|
101
100
|
* `args` array.
|
|
102
101
|
*/
|
|
103
102
|
export function buildLlamaCppEngineArgs(input) {
|
|
104
|
-
const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve,
|
|
103
|
+
const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, } = input;
|
|
105
104
|
const args = [];
|
|
106
105
|
// ── GPU-layer offload (`--n-gpu-layers`) ──────────────────────────
|
|
107
106
|
const nGpuLayers = config.llamaCppNGpuLayers ?? perModel?.nGpuLayers ?? planner?.nGpuLayers;
|
|
@@ -176,26 +175,20 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
176
175
|
//
|
|
177
176
|
// So the engine already declines it precisely when it cannot honour it,
|
|
178
177
|
// which is what the warning is for. Withholding the flag ourselves only
|
|
179
|
-
// cost reuse on multi-slot dense engines.
|
|
180
|
-
//
|
|
181
|
-
// whether Gemma gets any cross-request prefix reuse at all.
|
|
178
|
+
// cost reuse on multi-slot dense engines. Gemma therefore gets it only
|
|
179
|
+
// with a pinned `--swa-full` below.
|
|
182
180
|
const explicitCacheReuse = config.llamaCppCacheReuse ?? perModel?.cacheReuse;
|
|
183
181
|
const cacheReuse = explicitCacheReuse ?? DEFAULT_CACHE_REUSE;
|
|
184
182
|
if (cacheReuse > 0)
|
|
185
183
|
args.push('--cache-reuse', String(cacheReuse));
|
|
186
184
|
// ── SWA full cache (`--swa-full`) ─────────────────────────────────
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
// fast memory, Gemma launches with the windowed cache instead.
|
|
195
|
-
const swaFull = config.llamaCppSwaFull ??
|
|
196
|
-
perModel?.swaFull ??
|
|
197
|
-
(swaFullAutoFits !== false && isGemmaModel({ architecture, modelId }));
|
|
198
|
-
if (swaFull)
|
|
185
|
+
// Opt-in only: an explicit config/manifest `swaFull`. Gemma used to get
|
|
186
|
+
// it by default for cross-request prefix reuse, until a paired A/B on
|
|
187
|
+
// gemma4-31b (2026-10-15) showed the full cache decodes 17-25% slower at
|
|
188
|
+
// every context depth, because decode reads each sliding-window layer's
|
|
189
|
+
// whole cache, and costs 51 GB more at 64K. Trials ran 19% longer in
|
|
190
|
+
// total despite the better reuse.
|
|
191
|
+
if (config.llamaCppSwaFull ?? perModel?.swaFull)
|
|
199
192
|
args.push('--swa-full');
|
|
200
193
|
// ── Thread / batch overrides ──────────────────────────────────────
|
|
201
194
|
// The GPU-resident default needs positive evidence that no model compute
|
|
@@ -248,7 +241,8 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
248
241
|
const nMax = config.llamaCppSpecDraftNMax ?? perModel?.spec?.nMax;
|
|
249
242
|
if (typeof nMax === 'number')
|
|
250
243
|
args.push('--spec-draft-n-max', String(nMax));
|
|
251
|
-
// v0.
|
|
244
|
+
// v0.7.0 supports probabilistic selection only for MTP/simple drafts
|
|
245
|
+
// (DFlash/LiLiCorr ignore it).
|
|
252
246
|
// Leave the upstream greedy default untouched unless explicitly set.
|
|
253
247
|
if ((safeSpecType === 'draft-mtp' || safeSpecType === 'draft-simple') &&
|
|
254
248
|
config.llamaCppSpecDraftSampling) {
|
|
@@ -32,8 +32,9 @@ const GgufValueType = {
|
|
|
32
32
|
INT64: 11,
|
|
33
33
|
FLOAT64: 12,
|
|
34
34
|
};
|
|
35
|
-
// `general.file_type` enum from llama.cpp/
|
|
36
|
-
//
|
|
35
|
+
// `general.file_type` enum (`llama_ftype`) from llama.cpp's include/llama.h.
|
|
36
|
+
// Copy new values from upstream rather than guessing: this table once mapped
|
|
37
|
+
// 30 to BF16 (upstream 30 is IQ4_XS, BF16 is 32) and had no NVFP4 at all.
|
|
37
38
|
// Anything unrecognised is reported by number.
|
|
38
39
|
const FILE_TYPE_NAMES = {
|
|
39
40
|
0: 'ALL_F32',
|
|
@@ -54,8 +55,24 @@ const FILE_TYPE_NAMES = {
|
|
|
54
55
|
18: 'MOSTLY_Q6_K',
|
|
55
56
|
19: 'MOSTLY_IQ2_XXS',
|
|
56
57
|
20: 'MOSTLY_IQ2_XS',
|
|
58
|
+
21: 'MOSTLY_Q2_K_S',
|
|
59
|
+
22: 'MOSTLY_IQ3_XS',
|
|
57
60
|
23: 'MOSTLY_IQ3_XXS',
|
|
58
|
-
|
|
61
|
+
24: 'MOSTLY_IQ1_S',
|
|
62
|
+
25: 'MOSTLY_IQ4_NL',
|
|
63
|
+
26: 'MOSTLY_IQ3_S',
|
|
64
|
+
27: 'MOSTLY_IQ3_M',
|
|
65
|
+
28: 'MOSTLY_IQ2_S',
|
|
66
|
+
29: 'MOSTLY_IQ2_M',
|
|
67
|
+
30: 'MOSTLY_IQ4_XS',
|
|
68
|
+
31: 'MOSTLY_IQ1_M',
|
|
69
|
+
32: 'MOSTLY_BF16',
|
|
70
|
+
36: 'MOSTLY_TQ1_0',
|
|
71
|
+
37: 'MOSTLY_TQ2_0',
|
|
72
|
+
38: 'MOSTLY_MXFP4_MOE',
|
|
73
|
+
39: 'MOSTLY_NVFP4',
|
|
74
|
+
40: 'MOSTLY_Q1_0',
|
|
75
|
+
41: 'MOSTLY_Q2_0',
|
|
59
76
|
};
|
|
60
77
|
/**
|
|
61
78
|
* The quantization tag a GGUF declares about itself, in the shape the model
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did a GPU build of llama-server actually get its GPU? Read from the
|
|
3
|
+
* engine's own startup output, because nothing else says.
|
|
4
|
+
*
|
|
5
|
+
* The bundled builds load their GPU backend with dlopen (`GGML_BACKEND_DL`),
|
|
6
|
+
* and in a release build ggml skips a backend that fails to load without a
|
|
7
|
+
* word. A CUDA backend that loads but cannot initialize prints one error line
|
|
8
|
+
* and registers no devices. Either way the engine starts, answers, and runs
|
|
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 it
|
|
11
|
+
* launched.
|
|
12
|
+
*
|
|
13
|
+
* llama.cpp names every GPU it will use while loading a model
|
|
14
|
+
* (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
|
|
15
|
+
* build that reached readiness after loading a model without that line ran
|
|
16
|
+
* without its GPU. The explicit lines say why when there is a why to say.
|
|
17
|
+
*/
|
|
18
|
+
export type GpuUnusedReason =
|
|
19
|
+
/** The CUDA runtime loaded and refused to start: `failed to initialize CUDA`. */
|
|
20
|
+
'init-failed'
|
|
21
|
+
/** The backend loaded and found no device it could use. */
|
|
22
|
+
| 'no-devices'
|
|
23
|
+
/** No sign of the GPU backend at all: it failed to load, silently. */
|
|
24
|
+
| 'not-loaded';
|
|
25
|
+
export interface GpuUnused {
|
|
26
|
+
backend: 'cuda' | 'vulkan';
|
|
27
|
+
reason: GpuUnusedReason;
|
|
28
|
+
/** The engine's own words, when it gave any. */
|
|
29
|
+
detail?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class GpuStartupWatch {
|
|
32
|
+
private readonly backend;
|
|
33
|
+
private usingGpu;
|
|
34
|
+
private sawModelLoad;
|
|
35
|
+
private initFailure;
|
|
36
|
+
private noDevices;
|
|
37
|
+
constructor(backend: string | undefined);
|
|
38
|
+
observe(line: string): void;
|
|
39
|
+
/** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
|
|
40
|
+
verdict(): GpuUnused | null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did a GPU build of llama-server actually get its GPU? Read from the
|
|
3
|
+
* engine's own startup output, because nothing else says.
|
|
4
|
+
*
|
|
5
|
+
* The bundled builds load their GPU backend with dlopen (`GGML_BACKEND_DL`),
|
|
6
|
+
* and in a release build ggml skips a backend that fails to load without a
|
|
7
|
+
* word. A CUDA backend that loads but cannot initialize prints one error line
|
|
8
|
+
* and registers no devices. Either way the engine starts, answers, and runs
|
|
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 it
|
|
11
|
+
* launched.
|
|
12
|
+
*
|
|
13
|
+
* llama.cpp names every GPU it will use while loading a model
|
|
14
|
+
* (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
|
|
15
|
+
* build that reached readiness after loading a model without that line ran
|
|
16
|
+
* without its GPU. The explicit lines say why when there is a why to say.
|
|
17
|
+
*/
|
|
18
|
+
const USING_GPU = /\busing device (?:CUDA|Vulkan|ROCm|SYCL|MTL)\d+\b/;
|
|
19
|
+
const CUDA_INIT_FAILED = /ggml_cuda_init: failed to initialize CUDA:\s*(.+?)\s*$/;
|
|
20
|
+
const VULKAN_NO_DEVICES = /ggml_vulkan: No devices found/;
|
|
21
|
+
/**
|
|
22
|
+
* Lines llama.cpp prints at the same verbosity as `using device`, while
|
|
23
|
+
* loading a model. Without one the output is too quiet to conclude anything.
|
|
24
|
+
*/
|
|
25
|
+
const MODEL_LOAD = /\b(?:llama_model_loader:|load_tensors:|print_info:|llama_model_load_from_file_impl:)/;
|
|
26
|
+
export class GpuStartupWatch {
|
|
27
|
+
backend;
|
|
28
|
+
usingGpu = false;
|
|
29
|
+
sawModelLoad = false;
|
|
30
|
+
initFailure;
|
|
31
|
+
noDevices = false;
|
|
32
|
+
constructor(backend) {
|
|
33
|
+
this.backend = backend;
|
|
34
|
+
}
|
|
35
|
+
observe(line) {
|
|
36
|
+
if (USING_GPU.test(line))
|
|
37
|
+
this.usingGpu = true;
|
|
38
|
+
if (MODEL_LOAD.test(line))
|
|
39
|
+
this.sawModelLoad = true;
|
|
40
|
+
const failure = CUDA_INIT_FAILED.exec(line);
|
|
41
|
+
if (failure?.[1])
|
|
42
|
+
this.initFailure = failure[1];
|
|
43
|
+
if (VULKAN_NO_DEVICES.test(line))
|
|
44
|
+
this.noDevices = true;
|
|
45
|
+
}
|
|
46
|
+
/** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
|
|
47
|
+
verdict() {
|
|
48
|
+
const backend = this.backend;
|
|
49
|
+
if ((backend !== 'cuda' && backend !== 'vulkan') || this.usingGpu)
|
|
50
|
+
return null;
|
|
51
|
+
if (backend === 'cuda' && this.initFailure) {
|
|
52
|
+
return { backend, reason: 'init-failed', detail: this.initFailure };
|
|
53
|
+
}
|
|
54
|
+
if (backend === 'vulkan' && this.noDevices)
|
|
55
|
+
return { backend, reason: 'no-devices' };
|
|
56
|
+
return this.sawModelLoad ? { backend, reason: 'not-loaded' } : null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -236,6 +236,11 @@ export interface LlamaCppModelManagerOptions {
|
|
|
236
236
|
engine: 'llama-cpp' | 'ds4';
|
|
237
237
|
id: string;
|
|
238
238
|
}) => void;
|
|
239
|
+
/**
|
|
240
|
+
* The llama-server backend installs are judged against for a catalog
|
|
241
|
+
* source's `excludeBackends`. Test seam — defaults to the process's own.
|
|
242
|
+
*/
|
|
243
|
+
resolveBackend?: () => string | undefined;
|
|
239
244
|
}
|
|
240
245
|
export declare class LlamaCppModelManager {
|
|
241
246
|
private readonly modelsRoot;
|
|
@@ -244,6 +249,7 @@ export declare class LlamaCppModelManager {
|
|
|
244
249
|
private readonly fetchImpl;
|
|
245
250
|
private readonly engine;
|
|
246
251
|
private readonly onInstalled?;
|
|
252
|
+
private readonly resolveBackend;
|
|
247
253
|
/**
|
|
248
254
|
* In-flight installs. `install()` registers an entry on first yield
|
|
249
255
|
* and clears it in the iterator's `finally` block, so consumers
|
|
@@ -35,13 +35,14 @@ import { createHash } from 'node:crypto';
|
|
|
35
35
|
import { createReadStream } from 'node:fs';
|
|
36
36
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
37
37
|
import { basename, join, resolve } from 'node:path';
|
|
38
|
-
import { createLogger } from '@bendyline/gezel';
|
|
38
|
+
import { createLogger, llamaCppBackendExclusion, llamaCppBackendLabel } 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';
|
|
42
42
|
import { MODEL_HASH_READ_BUFFER_BYTES, assertModelStorePathSafe, findModelRoot, findRenamedModelId, hashModelPayloadFiles, inspectModelDirectory, listIncompleteModelDownloads, listOverlayModelIds, makeSharedModelReadable, modelExistsOnlyReadOnly, modelStorageRoots, pruneModelPayloadFiles, readOnlyModelError, removeModelDir, verifyReadOnlyModelPayload, } from '../../models/storage-roots.js';
|
|
43
43
|
import { checkDiskSpace, describeDiskShortfall } from '../../utils/disk-space.js';
|
|
44
44
|
import { downloadWithRetry } from '../../utils/download-with-retry.js';
|
|
45
|
+
import { currentLlamaCppBackend } from './backend-health.js';
|
|
45
46
|
import { ggufQuantizationTag, readGgufSummary } from './gguf-metadata.js';
|
|
46
47
|
import { backfillGgufQuantization } from './gguf-quantization-backfill.js';
|
|
47
48
|
/**
|
|
@@ -58,6 +59,7 @@ export class LlamaCppModelManager {
|
|
|
58
59
|
fetchImpl;
|
|
59
60
|
engine;
|
|
60
61
|
onInstalled;
|
|
62
|
+
resolveBackend;
|
|
61
63
|
/**
|
|
62
64
|
* In-flight installs. `install()` registers an entry on first yield
|
|
63
65
|
* and clears it in the iterator's `finally` block, so consumers
|
|
@@ -78,6 +80,7 @@ export class LlamaCppModelManager {
|
|
|
78
80
|
this.catalog = opts.catalog;
|
|
79
81
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
80
82
|
this.onInstalled = opts.onInstalled;
|
|
83
|
+
this.resolveBackend = opts.resolveBackend ?? currentLlamaCppBackend;
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
83
86
|
* The catalog source block this manager downloads from — `ds4` for the
|
|
@@ -435,6 +438,17 @@ export class LlamaCppModelManager {
|
|
|
435
438
|
};
|
|
436
439
|
return;
|
|
437
440
|
}
|
|
441
|
+
if (this.engine === 'llama-cpp') {
|
|
442
|
+
const backend = this.resolveBackend();
|
|
443
|
+
const excluded = llamaCppBackendExclusion(manifest.llamaCpp, backend);
|
|
444
|
+
if (excluded && backend) {
|
|
445
|
+
yield {
|
|
446
|
+
type: 'error',
|
|
447
|
+
error: `chat-model "${catalogId}" is not available on the ${llamaCppBackendLabel(backend)} llama.cpp engine: ${excluded}`,
|
|
448
|
+
};
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
438
452
|
tracked.totalBytes = installSrc.approxSizeBytes;
|
|
439
453
|
yield* this.runInstall(catalogId, manifest, tracked, skipSha, includeMmproj);
|
|
440
454
|
}
|
|
@@ -964,6 +978,10 @@ includeMmproj = true) {
|
|
|
964
978
|
// after install without a second download.
|
|
965
979
|
if (src.draftModel) {
|
|
966
980
|
out.push({
|
|
981
|
+
...(src.draftModel.huggingfaceRepo
|
|
982
|
+
? { huggingfaceRepo: src.draftModel.huggingfaceRepo }
|
|
983
|
+
: {}),
|
|
984
|
+
...(src.draftModel.revision ? { revision: src.draftModel.revision } : {}),
|
|
967
985
|
repoPath: src.draftModel.filename,
|
|
968
986
|
destFilename: basename(src.draftModel.filename),
|
|
969
987
|
sha256: src.draftModel.sha256,
|
|
@@ -286,10 +286,9 @@ export interface WindowedKvLinearization {
|
|
|
286
286
|
* than the layer ratio — global layers carry fewer KV heads at wider head
|
|
287
287
|
* dims (31b: 10 layers × 4 heads × 512+512 vs 50 layers × 16 heads ×
|
|
288
288
|
* 256+256) — so full-attention math overstates the windowed cache ~14×
|
|
289
|
-
* and using it for admission over-clamps or over-denies
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* this way).
|
|
289
|
+
* and using it for admission over-clamps or over-denies launches the
|
|
290
|
+
* windowed cache holds easily (the 2026-08-04 gemma4-26b sweep shipped
|
|
291
|
+
* 19–56K windows on a 65536 request this way).
|
|
293
292
|
*
|
|
294
293
|
* Returns undefined when the header lacks the SWA layout (no window, no
|
|
295
294
|
* pattern, length mismatches) so callers fall back to their conservative
|
|
@@ -323,17 +322,3 @@ export declare function estimateExactPerSlotKvBytesF16(input: Omit<WindowedKvInp
|
|
|
323
322
|
*/
|
|
324
323
|
export declare function estimateLinearHybridKvLinearization(input: WindowedKvInput): WindowedKvLinearization | undefined;
|
|
325
324
|
export declare function estimateWindowedKvLinearization(input: WindowedKvInput): WindowedKvLinearization | undefined;
|
|
326
|
-
export interface SwaFullFastMemoryFitInput {
|
|
327
|
-
residentWeightsBytes: number;
|
|
328
|
-
fullKvBytes: number;
|
|
329
|
-
/** Already safety-discounted fast-memory budget (VRAM on a discrete GPU). */
|
|
330
|
-
fastBudgetBytes: number;
|
|
331
|
-
/** Fast-memory reservations held by models that are already resident. */
|
|
332
|
-
committedOtherBytes?: number | undefined;
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Whether Auto may keep `--swa-full` without spilling the model or KV cache
|
|
336
|
-
* out of the fast pool. The capacity budget already reserves driver/OS
|
|
337
|
-
* headroom, so this comparison must not discount it a second time.
|
|
338
|
-
*/
|
|
339
|
-
export declare function fitsSwaFullInFastMemory(input: SwaFullFastMemoryFitInput): boolean;
|
|
@@ -406,10 +406,9 @@ const SWA_UBATCH_MARGIN_TOKENS = 2048;
|
|
|
406
406
|
* than the layer ratio — global layers carry fewer KV heads at wider head
|
|
407
407
|
* dims (31b: 10 layers × 4 heads × 512+512 vs 50 layers × 16 heads ×
|
|
408
408
|
* 256+256) — so full-attention math overstates the windowed cache ~14×
|
|
409
|
-
* and using it for admission over-clamps or over-denies
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* this way).
|
|
409
|
+
* and using it for admission over-clamps or over-denies launches the
|
|
410
|
+
* windowed cache holds easily (the 2026-08-04 gemma4-26b sweep shipped
|
|
411
|
+
* 19–56K windows on a 65536 request this way).
|
|
413
412
|
*
|
|
414
413
|
* Returns undefined when the header lacks the SWA layout (no window, no
|
|
415
414
|
* pattern, length mismatches) so callers fall back to their conservative
|
|
@@ -512,13 +511,3 @@ export function estimateWindowedKvLinearization(input) {
|
|
|
512
511
|
fixedBytes: Math.round(swaElemsPerToken * bytesPerElement * (slidingWindow + SWA_UBATCH_MARGIN_TOKENS) * loops),
|
|
513
512
|
};
|
|
514
513
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Whether Auto may keep `--swa-full` without spilling the model or KV cache
|
|
517
|
-
* out of the fast pool. The capacity budget already reserves driver/OS
|
|
518
|
-
* headroom, so this comparison must not discount it a second time.
|
|
519
|
-
*/
|
|
520
|
-
export function fitsSwaFullInFastMemory(input) {
|
|
521
|
-
const required = Math.max(0, input.residentWeightsBytes) + Math.max(0, input.fullKvBytes);
|
|
522
|
-
const available = Math.max(0, input.fastBudgetBytes - Math.max(0, input.committedOtherBytes ?? 0));
|
|
523
|
-
return Number.isFinite(required) && Number.isFinite(available) && required <= available;
|
|
524
|
-
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* lazily respawn its process (see {@link shutdown}).
|
|
23
23
|
*/
|
|
24
24
|
import { type DisableThinkingRequestShape, LlamaCppSession, type ReasoningEffortRequestShape, type ToolGrammarFallback } from '@bendyline/gezel/local-loop';
|
|
25
|
+
import type { StartupCrashVerdict } from '@bendyline/gezel/native';
|
|
25
26
|
import type { GpuArbiter } from '../gpu-arbiter.js';
|
|
26
27
|
import type { NativeEngineLifecycleSnapshot, NativeEngineSupervisor } from '../native/supervisor.js';
|
|
27
28
|
import { ProviderQueue } from '../queue.js';
|
|
@@ -55,6 +56,7 @@ export declare class LlamaCppProvider implements LLMProvider {
|
|
|
55
56
|
*/
|
|
56
57
|
private toolGrammarFloor;
|
|
57
58
|
private readonly supervisor?;
|
|
59
|
+
private readonly startupCrashVerdict?;
|
|
58
60
|
/** Explicit base URL when no supervisor is managing the process. */
|
|
59
61
|
private readonly externalBaseUrl?;
|
|
60
62
|
private readonly defaultModel;
|
|
@@ -242,6 +244,12 @@ export declare class LlamaCppProvider implements LLMProvider {
|
|
|
242
244
|
* `baseUrl`.
|
|
243
245
|
*/
|
|
244
246
|
supervisor?: NativeEngineSupervisor;
|
|
247
|
+
/**
|
|
248
|
+
* What a startup crash did to its build's standing (strike or
|
|
249
|
+
* quarantine), looked up by incident id when a start fails, so the error
|
|
250
|
+
* says what the next request will actually do.
|
|
251
|
+
*/
|
|
252
|
+
startupCrashVerdict?: (incidentId: string) => StartupCrashVerdict | undefined;
|
|
245
253
|
/**
|
|
246
254
|
* Fixed base URL of an already-running llama-server. Used in
|
|
247
255
|
* dev iteration and LAN-shared setups. Mutually exclusive with
|
|
@@ -57,6 +57,7 @@ export class LlamaCppProvider {
|
|
|
57
57
|
*/
|
|
58
58
|
toolGrammarFloor = null;
|
|
59
59
|
supervisor;
|
|
60
|
+
startupCrashVerdict;
|
|
60
61
|
/** Explicit base URL when no supervisor is managing the process. */
|
|
61
62
|
externalBaseUrl;
|
|
62
63
|
defaultModel;
|
|
@@ -247,6 +248,8 @@ export class LlamaCppProvider {
|
|
|
247
248
|
}
|
|
248
249
|
if (opts.supervisor)
|
|
249
250
|
this.supervisor = opts.supervisor;
|
|
251
|
+
if (opts.startupCrashVerdict)
|
|
252
|
+
this.startupCrashVerdict = opts.startupCrashVerdict;
|
|
250
253
|
if (opts.baseUrl)
|
|
251
254
|
this.externalBaseUrl = opts.baseUrl.replace(/\/+$/, '');
|
|
252
255
|
// Only register an evictor when we own the lifecycle. External-baseUrl
|
|
@@ -848,7 +851,7 @@ export class LlamaCppProvider {
|
|
|
848
851
|
catch (err) {
|
|
849
852
|
const nativeExit = this.supervisor.lastExitSnapshot?.();
|
|
850
853
|
if (nativeExit && !nativeExit.expected && nativeExit.exitedAt >= ensureStartedAt) {
|
|
851
|
-
throw new NativeEngineCrashedError(nativeExit, err);
|
|
854
|
+
throw new NativeEngineCrashedError(nativeExit, err, this.startupCrashVerdict?.(nativeExit.incidentId));
|
|
852
855
|
}
|
|
853
856
|
throw err;
|
|
854
857
|
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
29
29
|
import { randomUUID } from 'node:crypto';
|
|
30
|
-
import { AwakeBudget, autoCompactedNotice, createAwakeTimeout, createLogger, leaksUntaggedReasoning, turnCancelledMessage, } from '@bendyline/gezel';
|
|
30
|
+
import { AwakeBudget, autoCompactedNotice, createAwakeTimeout, createLogger, isProjectStartingCall, leaksUntaggedReasoning, turnCancelledMessage, } from '@bendyline/gezel';
|
|
31
31
|
import { leadingSystemMessages, standaloneTurnMessages, toolSurfaceSize, } from '@bendyline/gezel/local-loop';
|
|
32
32
|
import { computeToolBudgetChars } from '@bendyline/gezel/local-loop';
|
|
33
33
|
import { extractReasoningWithProfile, profileBehaviorConfig, profileHasBehavior, } from '../../model-profile/runtime.js';
|
|
@@ -2656,7 +2656,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2656
2656
|
output =
|
|
2657
2657
|
"You already posted a question card to the user earlier in this turn. The second `ask_user_question` call was suppressed so the user only sees one card. END YOUR TURN NOW — the user's answer to the first question will arrive as the next user message; this turn produces no further visible content.";
|
|
2658
2658
|
}
|
|
2659
|
-
else if ((call.function.name
|
|
2659
|
+
else if (isProjectStartingCall(call.function.name, args) &&
|
|
2660
2660
|
startedProjectOrJobThisTurn) {
|
|
2661
2661
|
// Success-led phrasing — wild-caught on gemma4-26b/MLX,
|
|
2662
2662
|
// the previous "DO NOT call..." wording got misread by
|
|
@@ -2753,8 +2753,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2753
2753
|
// Successful start_project / start_job → arm the
|
|
2754
2754
|
// per-turn macro guard so subsequent variants get
|
|
2755
2755
|
// intercepted above before reaching the bridge.
|
|
2756
|
-
if ((call.function.name
|
|
2757
|
-
!output.startsWith('ERROR:')) {
|
|
2756
|
+
if (isProjectStartingCall(call.function.name, args) && !output.startsWith('ERROR:')) {
|
|
2758
2757
|
startedProjectOrJobThisTurn = { tool: call.function.name, firstResult: output };
|
|
2759
2758
|
}
|
|
2760
2759
|
}
|
|
@@ -14,7 +14,6 @@ so concurrent replicas cannot observe a half-built view.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import fcntl
|
|
18
17
|
import hashlib
|
|
19
18
|
import json
|
|
20
19
|
import os
|
|
@@ -23,6 +22,13 @@ import uuid
|
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
from typing import Callable, Optional, Tuple
|
|
25
24
|
|
|
25
|
+
try:
|
|
26
|
+
import fcntl
|
|
27
|
+
except ImportError:
|
|
28
|
+
# Windows has no fcntl. The MLX engine never runs there, but this module's
|
|
29
|
+
# stdlib suite does (python-suites.test.ts), so it must still import.
|
|
30
|
+
fcntl = None
|
|
31
|
+
|
|
26
32
|
|
|
27
33
|
PLE_MARKER = ".ple.ple_embedding.ngram_embedding.shards."
|
|
28
34
|
VIEW_MARKER = "GEZEL_PLE_VIEW.json"
|
|
@@ -122,7 +128,8 @@ def prepare_external_ple_view(
|
|
|
122
128
|
lock_path = target.parent / f".{target.name}.lock"
|
|
123
129
|
try:
|
|
124
130
|
with lock_path.open("a+b") as lock:
|
|
125
|
-
fcntl
|
|
131
|
+
if fcntl is not None:
|
|
132
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
126
133
|
if _valid_view(target, signature):
|
|
127
134
|
return str(target), "reused", True
|
|
128
135
|
|
|
@@ -46,6 +46,14 @@ export interface CapacityBrokerOptions {
|
|
|
46
46
|
* {@link computeCapacityBudget}. Omit to read the last published probe.
|
|
47
47
|
*/
|
|
48
48
|
gpuVramBytes?: number | null;
|
|
49
|
+
/**
|
|
50
|
+
* An integrated GPU's firmware carve-out — see
|
|
51
|
+
* {@link CapacityBudgetInput.reservedGpuBytes}. Travels with
|
|
52
|
+
* `gpuVramBytes`/`unifiedMemory` as one description of the host.
|
|
53
|
+
*/
|
|
54
|
+
reservedGpuBytes?: number | null;
|
|
55
|
+
/** What an integrated GPU can reach — see {@link CapacityBudgetInput.gpuAddressableBytes}. */
|
|
56
|
+
gpuAddressableBytes?: number | null;
|
|
49
57
|
/**
|
|
50
58
|
* May co-resident models spill into system RAM? `null`/omitted follows
|
|
51
59
|
* {@link autoAllowRamSpillover} for this host. See
|
|
@@ -192,6 +200,8 @@ export declare class CapacityBroker {
|
|
|
192
200
|
private readonly systemRamBytes;
|
|
193
201
|
private readonly unifiedMemory;
|
|
194
202
|
private readonly gpuVramBytes;
|
|
203
|
+
private readonly reservedGpuBytes;
|
|
204
|
+
private readonly gpuAddressableBytes;
|
|
195
205
|
private explicitAllowRamSpillover;
|
|
196
206
|
private readonly reservations;
|
|
197
207
|
constructor(opts?: CapacityBrokerOptions);
|
|
@@ -503,11 +513,9 @@ export declare const MIN_VIABLE_LOCAL_CONTEXT_TOKENS = 65536;
|
|
|
503
513
|
/** The same minimum on a host that has to trade context for memory. */
|
|
504
514
|
export declare const MIN_VIABLE_CONSTRAINED_CONTEXT_TOKENS = 32768;
|
|
505
515
|
/**
|
|
506
|
-
* The most KV cache
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* numeric context, a pinned `swaFull`) are the operator's call and bypass it;
|
|
510
|
-
* it never shortens the ladder-settled base window either.
|
|
516
|
+
* The most KV cache adaptive growth plans for one llama.cpp engine. An
|
|
517
|
+
* explicit numeric context is the operator's call and skips growth
|
|
518
|
+
* altogether, and the ceiling never shortens the ladder-settled base window.
|
|
511
519
|
*
|
|
512
520
|
* Past this a bigger cache buys nothing: llama.cpp's prefill and decode cost
|
|
513
521
|
* follows the tokens actually present, not the reservation, so the rest only
|
|
@@ -650,10 +658,10 @@ export interface CtxMemoryClampInput {
|
|
|
650
658
|
*/
|
|
651
659
|
freeSystemRamBytes?: number;
|
|
652
660
|
/**
|
|
653
|
-
* Usable VRAM on a discrete card
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
661
|
+
* Usable VRAM on a discrete card. On a unified/shared-pool host it MUST be
|
|
662
|
+
* only an integrated GPU's firmware carve-out, which `freeSystemRamBytes`
|
|
663
|
+
* never sees; any "VRAM" that is the same physical RAM would double-count.
|
|
664
|
+
* `computeCapacityBudget` reports exactly that — pass its value.
|
|
657
665
|
*/
|
|
658
666
|
vramBytes: number;
|
|
659
667
|
minPerTurnCtxTokens?: number;
|
|
@@ -751,7 +759,7 @@ export interface AdaptiveCtxGrowthInput {
|
|
|
751
759
|
fastBudgetBytes: number;
|
|
752
760
|
committedOtherBytes?: number;
|
|
753
761
|
budgetKind: CapacityBudget['kind'];
|
|
754
|
-
/** Usable VRAM on a discrete card (`liveBudget.vramBytes`);
|
|
762
|
+
/** Usable VRAM on a discrete card (`liveBudget.vramBytes`); ignored elsewhere. */
|
|
755
763
|
vramBytes: number;
|
|
756
764
|
/**
|
|
757
765
|
* Graphics memory free right now on a discrete card, as device admission
|
|
@@ -796,11 +804,9 @@ export interface AdaptiveCtxGrowthResult {
|
|
|
796
804
|
* settled slots and granted the un-grown request.
|
|
797
805
|
*
|
|
798
806
|
* Deliberately a second clamp pass rather than a bigger first request:
|
|
799
|
-
* the first ladder pass sizes slot ceilings
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* `--swa-full` for models whose full cache fits at 64K and grow KV into
|
|
803
|
-
* system-RAM spill on discrete cards.
|
|
807
|
+
* the first ladder pass sizes slot ceilings and RAM-spillover admission,
|
|
808
|
+
* both of which must stay anchored on the practical 64K target — growing
|
|
809
|
+
* the request there would grow KV into system-RAM spill on discrete cards.
|
|
804
810
|
*
|
|
805
811
|
* Slot trade: when `allowSlotTrade` is set and the target is out of reach
|
|
806
812
|
* at the settled slot count, growth walks the count down — taking the
|