@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine process's side of the llama-server quarantine
|
|
3
|
+
* (`llama-quarantine.ts` in `@bendyline/gezel/native`): which build this
|
|
4
|
+
* process launches, kept in the env discovery stamps at startup
|
|
5
|
+
* (`GEZEL_LLAMA_SERVER_BIN`, `GEZEL_LLAMA_SERVER_BACKEND`,
|
|
6
|
+
* `GEZEL_LLAMA_QUARANTINED`), and the view and undo behind the engine's
|
|
7
|
+
* Settings tab.
|
|
8
|
+
*
|
|
9
|
+
* Discovery reads the quarantine only at startup. These move the env inside
|
|
10
|
+
* a running process, so a quarantine and its undo both take effect on the
|
|
11
|
+
* next engine launch instead of the next app start.
|
|
12
|
+
*/
|
|
13
|
+
import type { LlamaCppQuarantineResponse } from '@bendyline/gezel';
|
|
14
|
+
import { type LlamaBackend, type ResolvedLlamaBinary } from '@bendyline/gezel/native';
|
|
15
|
+
export declare function isLlamaBackend(value: string): value is LlamaBackend;
|
|
16
|
+
/**
|
|
17
|
+
* Point this process at the build that replaces `backend`'s, just
|
|
18
|
+
* quarantined, so the pool's rebuild of the failed engine launches it. Null,
|
|
19
|
+
* env untouched, when nothing usable is bundled beside `binary`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function adoptLlamaFallback(home: string, binary: string, backend: LlamaBackend): ResolvedLlamaBinary | null;
|
|
22
|
+
/**
|
|
23
|
+
* The quarantined builds that still apply. An entry pins its binary's bytes,
|
|
24
|
+
* so one whose build an update replaced is left out — discovery ignores it
|
|
25
|
+
* too. Without a resolved build to measure from, entries are shown as written.
|
|
26
|
+
*/
|
|
27
|
+
export declare function llamaQuarantineView(home: string): LlamaCppQuarantineResponse;
|
|
28
|
+
/**
|
|
29
|
+
* Lift `backend`'s quarantine at a person's request. Unless they pinned a
|
|
30
|
+
* backend, also point this process back at the best build the hardware
|
|
31
|
+
* supports, so the next engine launch tries it; `restored` is that build when
|
|
32
|
+
* the env moved. A pinned backend is launched as pinned and was never routed
|
|
33
|
+
* around, so there is nothing to move back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function liftLlamaQuarantine(home: string, backend: LlamaBackend, opts: {
|
|
36
|
+
pinned: boolean;
|
|
37
|
+
}): {
|
|
38
|
+
cleared: boolean;
|
|
39
|
+
restored: ResolvedLlamaBinary | null;
|
|
40
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine process's side of the llama-server quarantine
|
|
3
|
+
* (`llama-quarantine.ts` in `@bendyline/gezel/native`): which build this
|
|
4
|
+
* process launches, kept in the env discovery stamps at startup
|
|
5
|
+
* (`GEZEL_LLAMA_SERVER_BIN`, `GEZEL_LLAMA_SERVER_BACKEND`,
|
|
6
|
+
* `GEZEL_LLAMA_QUARANTINED`), and the view and undo behind the engine's
|
|
7
|
+
* Settings tab.
|
|
8
|
+
*
|
|
9
|
+
* Discovery reads the quarantine only at startup. These move the env inside
|
|
10
|
+
* a running process, so a quarantine and its undo both take effect on the
|
|
11
|
+
* next engine launch instead of the next app start.
|
|
12
|
+
*/
|
|
13
|
+
import { LLAMA_QUARANTINE_STRIKES, clearLlamaQuarantine, createLlamaBuildCheck, isBinaryQuarantined, llamaVariantBinaryPath, publishLlamaCudaBlocker, readLlamaQuarantine, resolveLlamaFallbackBinary, resolveLlamaVariantBinary, } from '@bendyline/gezel/native';
|
|
14
|
+
const BACKENDS = ['cuda', 'vulkan', 'metal', 'cpu'];
|
|
15
|
+
export function isLlamaBackend(value) {
|
|
16
|
+
return BACKENDS.some((backend) => backend === value);
|
|
17
|
+
}
|
|
18
|
+
function envBackend(name) {
|
|
19
|
+
const raw = process.env[name];
|
|
20
|
+
return raw !== undefined && isLlamaBackend(raw) ? raw : undefined;
|
|
21
|
+
}
|
|
22
|
+
function updateQuarantinedEnv(update) {
|
|
23
|
+
const backends = new Set(process.env.GEZEL_LLAMA_QUARANTINED?.split(',').filter(Boolean));
|
|
24
|
+
update(backends);
|
|
25
|
+
if (backends.size > 0)
|
|
26
|
+
process.env.GEZEL_LLAMA_QUARANTINED = [...backends].join(',');
|
|
27
|
+
else
|
|
28
|
+
delete process.env.GEZEL_LLAMA_QUARANTINED;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Point this process at the build that replaces `backend`'s, just
|
|
32
|
+
* quarantined, so the pool's rebuild of the failed engine launches it. Null,
|
|
33
|
+
* env untouched, when nothing usable is bundled beside `binary`.
|
|
34
|
+
*/
|
|
35
|
+
export function adoptLlamaFallback(home, binary, backend) {
|
|
36
|
+
const quarantine = readLlamaQuarantine(home);
|
|
37
|
+
const fallback = resolveLlamaFallbackBinary(binary, backend, (candidate, path) => !isBinaryQuarantined(quarantine, candidate, path));
|
|
38
|
+
if (!fallback)
|
|
39
|
+
return null;
|
|
40
|
+
process.env.GEZEL_LLAMA_SERVER_BIN = fallback.path;
|
|
41
|
+
process.env.GEZEL_LLAMA_SERVER_BACKEND = fallback.backend;
|
|
42
|
+
updateQuarantinedEnv((backends) => backends.add(backend));
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The quarantined builds that still apply. An entry pins its binary's bytes,
|
|
47
|
+
* so one whose build an update replaced is left out — discovery ignores it
|
|
48
|
+
* too. Without a resolved build to measure from, entries are shown as written.
|
|
49
|
+
*/
|
|
50
|
+
export function llamaQuarantineView(home) {
|
|
51
|
+
const binary = process.env.GEZEL_LLAMA_SERVER_BIN;
|
|
52
|
+
const running = envBackend('GEZEL_LLAMA_SERVER_BACKEND');
|
|
53
|
+
const entries = readLlamaQuarantine(home).filter((entry) => {
|
|
54
|
+
if (!isLlamaBackend(entry.backend))
|
|
55
|
+
return false;
|
|
56
|
+
if (!binary || !running)
|
|
57
|
+
return true;
|
|
58
|
+
const path = llamaVariantBinaryPath(binary, running, entry.backend);
|
|
59
|
+
return path === null || isBinaryQuarantined([entry], entry.backend, path);
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
entries: entries.map(({ backend, signal, reason, at, crashes }) => ({
|
|
63
|
+
backend,
|
|
64
|
+
signal,
|
|
65
|
+
reason,
|
|
66
|
+
at,
|
|
67
|
+
...(crashes !== undefined ? { crashes } : {}),
|
|
68
|
+
})),
|
|
69
|
+
strikeThreshold: LLAMA_QUARANTINE_STRIKES,
|
|
70
|
+
...(running ? { running } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lift `backend`'s quarantine at a person's request. Unless they pinned a
|
|
75
|
+
* backend, also point this process back at the best build the hardware
|
|
76
|
+
* supports, so the next engine launch tries it; `restored` is that build when
|
|
77
|
+
* the env moved. A pinned backend is launched as pinned and was never routed
|
|
78
|
+
* around, so there is nothing to move back.
|
|
79
|
+
*/
|
|
80
|
+
export function liftLlamaQuarantine(home, backend, opts) {
|
|
81
|
+
const cleared = clearLlamaQuarantine(home, backend);
|
|
82
|
+
updateQuarantinedEnv((backends) => backends.delete(backend));
|
|
83
|
+
const binary = process.env.GEZEL_LLAMA_SERVER_BIN;
|
|
84
|
+
const running = envBackend('GEZEL_LLAMA_SERVER_BACKEND');
|
|
85
|
+
if (opts.pinned || !binary || !running)
|
|
86
|
+
return { cleared, restored: null };
|
|
87
|
+
// Discovery's own check: a CUDA build the driver cannot run stays passed
|
|
88
|
+
// over, whatever else was lifted.
|
|
89
|
+
const check = createLlamaBuildCheck({
|
|
90
|
+
quarantine: readLlamaQuarantine(home),
|
|
91
|
+
platform: process.platform,
|
|
92
|
+
});
|
|
93
|
+
const best = resolveLlamaVariantBinary(binary, running, envBackend('GEZEL_LLAMA_DETECTED_BACKEND') ?? backend, check.isUsable);
|
|
94
|
+
publishLlamaCudaBlocker(check.cudaBlocker());
|
|
95
|
+
if (!best || best.backend === running)
|
|
96
|
+
return { cleared, restored: null };
|
|
97
|
+
process.env.GEZEL_LLAMA_SERVER_BIN = best.path;
|
|
98
|
+
process.env.GEZEL_LLAMA_SERVER_BACKEND = best.backend;
|
|
99
|
+
return { cleared, restored: best };
|
|
100
|
+
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { availableParallelism } from 'node:os';
|
|
2
2
|
import { delimiter, dirname, join } from 'node:path';
|
|
3
|
-
import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, } from '@bendyline/gezel';
|
|
4
|
-
import {
|
|
3
|
+
import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, llamaCppBackendExclusion, llamaCppBackendLabel, } from '@bendyline/gezel';
|
|
4
|
+
import { LLAMA_QUARANTINE_STRIKES, clearLlamaStartupStrikes, recordLlamaStartupCrash, unrunnableBuildExit, } from '@bendyline/gezel/native';
|
|
5
5
|
import { gezelPaths } from '@bendyline/gezel/paths';
|
|
6
6
|
import { mmprojBudgetBytes, nativeVisionEnabledFor } from '../../chat/vision-capability.js';
|
|
7
7
|
import { effectiveEngineRelease, isEnginePinned } from '../../engines/native-manifest.js';
|
|
8
8
|
import { engineInstallPhase } from '../../engines/registry.js';
|
|
9
9
|
import { localDeviceSettingsLabel, noModelYetMessage, notReadyError, } from '../active-install-message.js';
|
|
10
10
|
import { resolveCatalogLlamaCppEngineConfig, resolveCatalogReasoningBudget, } from '../catalog-model-config.js';
|
|
11
|
-
import { CapacityDeniedError, NOT_ENOUGH_MEMORY_MESSAGE,
|
|
11
|
+
import { CapacityDeniedError, NOT_ENOUGH_MEMORY_MESSAGE, availableSystemRamBytes, formatContextCapacityDenial, minViableLocalContextTokens, planAdaptiveContextGrowth, pressureIdleGraceMs, resolveLlamaCppContextRequirement, } from '../native/capacity-broker.js';
|
|
12
12
|
import { measureAvailableGpuBytes } from '../native/device-capacity.js';
|
|
13
13
|
import { engineApiKey, withEngineApiKey } from '../native/engine-api-key.js';
|
|
14
14
|
import { makeEngineKey } from '../native/engine-key.js';
|
|
15
15
|
import { pickFreePort } from '../native/port.js';
|
|
16
16
|
import { NativeEngineSupervisor } from '../native/supervisor.js';
|
|
17
17
|
import { patientFetch } from '../patient-fetch.js';
|
|
18
|
+
import { currentLlamaCppBackend, noteGpuStartup, noteHostOutOfMemory } from './backend-health.js';
|
|
19
|
+
import { adoptLlamaFallback } from './backend-quarantine.js';
|
|
18
20
|
import { lastArgValue, readLlamaCppBuildMetadata } from './build-metadata.js';
|
|
19
21
|
import { matchNvidiaRuntimeDevice, maxGpuVramBytes, pickBestGpuDevice, probeLlamaDevices, probeNvidiaRuntimeDevices, } from './devices.js';
|
|
20
22
|
import { buildLlamaCppEngineArgs } from './engine-flags.js';
|
|
21
23
|
import { readGgufSummaryAsync } from './gguf-metadata-async.js';
|
|
22
24
|
import { effectiveLazyTensorBytes } from './gguf-metadata.js';
|
|
25
|
+
import { GpuStartupWatch } from './gpu-startup-watch.js';
|
|
23
26
|
import { isGemmaModel, planLlamaCppKv, resolveLlamaCppKvCacheType, } from './kv-cache-type.js';
|
|
24
|
-
import { degradeDenseFfnOffloadDecision, degradeMoeOffloadDecision, estimateExactPerSlotKvBytesF16, estimateKvReserveBytes, estimateWindowedKvLinearization,
|
|
27
|
+
import { degradeDenseFfnOffloadDecision, degradeMoeOffloadDecision, estimateExactPerSlotKvBytesF16, estimateKvReserveBytes, estimateWindowedKvLinearization, planDenseFfnOffload, planMoeOffload, } from './offload-planner.js';
|
|
25
28
|
import { LlamaCppProvider } from './provider.js';
|
|
26
29
|
import { reasoningLaunchOverridesFromEnv } from './reasoning-launch.js';
|
|
27
30
|
import { resolveSpecDraft } from './spec-draft.js';
|
|
@@ -69,6 +72,15 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
69
72
|
? undefined
|
|
70
73
|
: (process.env.GEZEL_LLAMA_SERVER_URL ?? config.llamaCppBaseUrl);
|
|
71
74
|
const binary = process.env.GEZEL_LLAMA_SERVER_BIN;
|
|
75
|
+
// Read together with the binary: after a quarantine the env points at the
|
|
76
|
+
// fallback build, and a crash of THIS build must be charged to its own
|
|
77
|
+
// backend, not to whatever the env names by then.
|
|
78
|
+
const binaryBackend = process.env.GEZEL_LLAMA_SERVER_BACKEND;
|
|
79
|
+
// What the latest startup crash did to this build's standing, for the error
|
|
80
|
+
// the failed start raises. `onExit` runs before `ensureRunning` rejects.
|
|
81
|
+
let lastStartupCrash = null;
|
|
82
|
+
// Fresh for each launch: whether this start of a GPU build got its GPU.
|
|
83
|
+
let gpuWatch = null;
|
|
72
84
|
// Preservation is a two-sided contract: launch llama-server with the
|
|
73
85
|
// explicit preserve/no-preserve flag AND replay captured
|
|
74
86
|
// `reasoning_content` only in the preserve case. v0.4.0 changed the server
|
|
@@ -269,6 +281,18 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
269
281
|
}
|
|
270
282
|
throw notReadyError(noModelYetMessage('Local model', activeLlamaInstall(opts.llamaCppModels)), 'model-not-ready');
|
|
271
283
|
}
|
|
284
|
+
// Install refuses an excluded backend; this catches a model installed
|
|
285
|
+
// before the exclusion was authored, or copied in by hand.
|
|
286
|
+
if (modelCatalogInfo && opts.catalog) {
|
|
287
|
+
const engineBackend = binaryBackend ?? currentLlamaCppBackend();
|
|
288
|
+
const detail = await opts.catalog.get('chat-model', modelCatalogInfo.id).catch(() => null);
|
|
289
|
+
const excluded = llamaCppBackendExclusion(detail?.manifest.kind === 'chat-model' ? detail.manifest.llamaCpp : undefined, engineBackend);
|
|
290
|
+
if (excluded && engineBackend) {
|
|
291
|
+
const err = new Error(`Local model: ${modelCatalogInfo.name} cannot run on the ${llamaCppBackendLabel(engineBackend)} llama.cpp engine. ${excluded} Pick another local model in Settings → ${localDeviceSettingsLabel()}.`);
|
|
292
|
+
err.isActionable = true;
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
272
296
|
// Per Phase 0 measurements on a cold Mac: Metal library compile is
|
|
273
297
|
// ~14s and model load adds model-size-dependent time. Default
|
|
274
298
|
// startupTimeoutMs is 60s which squeaks through for a 7B on a warm
|
|
@@ -621,13 +645,6 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
621
645
|
// back to the engine's own `--fit` / `-ngl auto`.
|
|
622
646
|
let offloadDecision;
|
|
623
647
|
let offloadBlockCount;
|
|
624
|
-
// Whether weights + full-attention KV at the requested context fit the
|
|
625
|
-
// fast-memory pool — VRAM on a discrete GPU. Gates the Gemma
|
|
626
|
-
// `--swa-full` auto-default (see `EngineFlagInput.swaFullAutoFits`). Set
|
|
627
|
-
// false to keep the full context on the windowed cache when Full would
|
|
628
|
-
// spill or require a slot/context reduction; undefined means it fits or
|
|
629
|
-
// the exact decision could not be computed.
|
|
630
|
-
let swaFullAutoFits;
|
|
631
648
|
// Broker-ledger reservation for this launch: resident weights + the KV
|
|
632
649
|
// the engine will actually allocate at the granted window and cache
|
|
633
650
|
// mode. Undefined when the GGUF is unreadable — the pool builder then
|
|
@@ -709,21 +726,16 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
709
726
|
// under GEZEL_LLAMA_NUM_CTX — eval runs lift ceilings deliberately
|
|
710
727
|
// and accept the memory consequences.
|
|
711
728
|
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
// ungated: admission re-plans with the windowed-KV linearization
|
|
723
|
-
// (SWA layers = fixed window-capped bytes, global layers = the only
|
|
724
|
-
// per-token cost), so a host too small even for the windowed cache
|
|
725
|
-
// still denies or clamps honestly instead of OOMing one machine
|
|
726
|
-
// class below the 2026-08-05 incident.
|
|
729
|
+
// A sliding-window model runs llama.cpp's windowed cache unless
|
|
730
|
+
// `swaFull` is pinned, and admission prices what runs: it re-plans
|
|
731
|
+
// with the windowed-KV linearization (SWA layers = fixed window-capped
|
|
732
|
+
// bytes, global layers = the only per-token cost), so a host too small
|
|
733
|
+
// even for the windowed cache still denies or clamps honestly instead
|
|
734
|
+
// of OOMing one machine class below the 2026-08-05 incident.
|
|
735
|
+
// Shared-KV layers and SWA-specific head dimensions are load-bearing
|
|
736
|
+
// in that math — E4B owns only 24 KV caches for 42 logical layers, and
|
|
737
|
+
// pricing all 42 at global dimensions overstated its two-slot 64K KV
|
|
738
|
+
// allocation as 21 GiB instead of llama.cpp's actual 7 GiB.
|
|
727
739
|
{
|
|
728
740
|
const REFERENCE_CTX = 4096;
|
|
729
741
|
const exactKvAtReference = estimateKvReserveBytes({
|
|
@@ -769,23 +781,14 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
769
781
|
? {
|
|
770
782
|
unifiedMemory: liveMemory.gpuMemoryKind === 'integrated' ||
|
|
771
783
|
liveMemory.gpuMemoryKind === 'unified',
|
|
784
|
+
reservedGpuBytes: liveMemory.gpuReservedBytes ?? null,
|
|
785
|
+
gpuAddressableBytes: liveMemory.gpuAddressableBytes ?? null,
|
|
772
786
|
}
|
|
773
787
|
: {}),
|
|
774
788
|
});
|
|
775
789
|
const fastBudgetBytes = brokerSnap?.enforced
|
|
776
790
|
? (opts.broker?.fastBudgetBytes() ?? liveBudget.fastBytes)
|
|
777
791
|
: liveBudget.fastBytes;
|
|
778
|
-
const fullKvBytesAtRequested = exactKvAtReference !== undefined
|
|
779
|
-
? (exactKvAtReference / REFERENCE_CTX) * effectiveNumCtx * slots
|
|
780
|
-
: undefined;
|
|
781
|
-
const fullCacheFitsFastMemory = fullKvBytesAtRequested !== undefined
|
|
782
|
-
? fitsSwaFullInFastMemory({
|
|
783
|
-
residentWeightsBytes: residentBytes,
|
|
784
|
-
fullKvBytes: fullKvBytesAtRequested,
|
|
785
|
-
fastBudgetBytes,
|
|
786
|
-
committedOtherBytes,
|
|
787
|
-
})
|
|
788
|
-
: undefined;
|
|
789
792
|
const admission = planCtxTokensForMemory({
|
|
790
793
|
requestedPerTurnCtxTokens: effectiveNumCtx,
|
|
791
794
|
slots,
|
|
@@ -797,25 +800,13 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
797
800
|
freeSystemRamBytes: availableSystemRamBytes(),
|
|
798
801
|
vramBytes: liveBudget.vramBytes,
|
|
799
802
|
});
|
|
803
|
+
// `--swa-full` runs only when pinned; every other launch of a
|
|
804
|
+
// sliding-window model gets llama.cpp's windowed cache.
|
|
800
805
|
const explicitSwaFull = config.llamaCppSwaFull ?? manifestEngineConfig?.swaFull;
|
|
801
|
-
const
|
|
802
|
-
isGemmaModel({
|
|
803
|
-
architecture: modelCatalogInfo?.architecture,
|
|
804
|
-
modelId: defaultModelId ?? undefined,
|
|
805
|
-
});
|
|
806
|
-
// A full-size cache past the automatic KV ceiling is declined even
|
|
807
|
-
// where it fits: gemma4-31b's is 55 GB at 64K, 11x its windowed
|
|
808
|
-
// cache, for a cross-request reuse gain nobody had measured.
|
|
809
|
-
const kvCeilingBytes = autoKvCacheMaxBytes();
|
|
810
|
-
const fullCacheOverCeiling = fullKvBytesAtRequested !== undefined && fullKvBytesAtRequested > kvCeilingBytes;
|
|
811
|
-
const fullKvOverBudget = fullCacheFitsFastMemory === false ||
|
|
812
|
-
fullCacheOverCeiling ||
|
|
813
|
-
!admission.minimumSatisfied ||
|
|
814
|
-
admission.slots < slots ||
|
|
815
|
-
admission.clamped;
|
|
806
|
+
const fullKvOverBudget = !admission.minimumSatisfied || admission.slots < slots || admission.clamped;
|
|
816
807
|
// The plan whose ladder (deny → shed slots → clamp) is enforced
|
|
817
808
|
// below. Defaults to the full-attention plan; the windowed-cache
|
|
818
|
-
//
|
|
809
|
+
// branch replaces it with windowed math or — only when the GGUF
|
|
819
810
|
// hides its SWA layout — null (no safe estimate; launch untouched,
|
|
820
811
|
// the pre-windowed-admission behavior).
|
|
821
812
|
let ladderPlan = admission;
|
|
@@ -833,17 +824,17 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
833
824
|
// full-attention), and the strict minimum rides inside
|
|
834
825
|
// `contextRequirement.minimumPerTurnCtxTokens`, so the windowed
|
|
835
826
|
// re-plan below sheds slots or denies but never shortens the window.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
// context
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
827
|
+
if (explicitSwaFull !== true) {
|
|
828
|
+
// The admission above is full-attention KV math, which overstates
|
|
829
|
+
// the windowed cache's real allocation (Gemma 4: global layers are
|
|
830
|
+
// both fewer AND cheaper per token than SWA layers — ~11× on 31b
|
|
831
|
+
// at 64K). Re-plan with the windowed linearization: SWA layers
|
|
832
|
+
// become a fixed, context-independent reservation; only global
|
|
833
|
+
// layers scale. Without it a 31b-class Gemma on a ~32 GB host
|
|
834
|
+
// would launch with NO context admission at all — the same
|
|
835
|
+
// lazy-Metal-wiring OOM this gate exists to prevent, one machine
|
|
836
|
+
// class down — and growth would price every token at the full
|
|
837
|
+
// slope.
|
|
847
838
|
const windowed = estimateWindowedKvLinearization({
|
|
848
839
|
blockCount: summary.blockCount,
|
|
849
840
|
embeddingLength: summary.embeddingLength,
|
|
@@ -864,42 +855,6 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
864
855
|
ssmConvKernel: summary.ssmConvKernel,
|
|
865
856
|
kvCacheType,
|
|
866
857
|
});
|
|
867
|
-
if (swaFullAutoDefault) {
|
|
868
|
-
// Declining `--swa-full` dominates every full-math remedy:
|
|
869
|
-
// the context window is capability, the full cache is a
|
|
870
|
-
// session-switch performance trade. Weights-level admission
|
|
871
|
-
// stays with the capacity broker.
|
|
872
|
-
swaFullAutoFits = false;
|
|
873
|
-
const fastFitDetail = fullCacheFitsFastMemory === false && fullKvBytesAtRequested !== undefined
|
|
874
|
-
? `projected weights + full KV ${((residentBytes + fullKvBytesAtRequested) / 1024 ** 3).toFixed(1)} GiB exceeds available fast memory ${(Math.max(0, fastBudgetBytes - committedOtherBytes) / 1024 ** 3).toFixed(1)} GiB`
|
|
875
|
-
: undefined;
|
|
876
|
-
const overCeilingOnly = fullCacheOverCeiling &&
|
|
877
|
-
fullCacheFitsFastMemory !== false &&
|
|
878
|
-
admission.minimumSatisfied &&
|
|
879
|
-
admission.slots >= slots &&
|
|
880
|
-
!admission.clamped;
|
|
881
|
-
if (overCeilingOnly && fullKvBytesAtRequested !== undefined) {
|
|
882
|
-
// A policy choice on a machine that could hold the cache, not
|
|
883
|
-
// a shortage: info, not warn.
|
|
884
|
-
log.info([
|
|
885
|
-
`[llama-cpp] ${modelCatalogInfo?.id ?? 'model'}: the full SWA cache at the requested `,
|
|
886
|
-
`${effectiveNumCtx * slots}-token total context would take `,
|
|
887
|
-
`${(fullKvBytesAtRequested / 1024 ** 3).toFixed(1)} GiB, past the `,
|
|
888
|
-
`${(kvCeilingBytes / 1024 ** 3).toFixed(1)} GiB automatic KV ceiling — keeping the full `,
|
|
889
|
-
'context and declining the --swa-full auto-default instead; the windowed KV cache ',
|
|
890
|
-
'holds it (cross-request prefix reuse unavailable).',
|
|
891
|
-
].join(''));
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
log.warn([
|
|
895
|
-
`[llama-cpp] ${modelCatalogInfo?.id ?? 'model'}: the full SWA working set at the requested `,
|
|
896
|
-
`${effectiveNumCtx * slots}-token total context does not fit fast memory — keeping the full `,
|
|
897
|
-
'context and declining the --swa-full auto-default instead; the windowed KV cache ',
|
|
898
|
-
'is what actually fits (cross-request prefix reuse unavailable). ',
|
|
899
|
-
`Fit detail: ${fastFitDetail ?? admission.reason ?? 'over budget at the requested slot count'}`,
|
|
900
|
-
].join(''));
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
858
|
if (windowed) {
|
|
904
859
|
ladderPlan = planCtxTokensForMemory({
|
|
905
860
|
requestedPerTurnCtxTokens: effectiveNumCtx,
|
|
@@ -917,16 +872,23 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
917
872
|
fixedPerSlotBytes: windowed.fixedBytes,
|
|
918
873
|
};
|
|
919
874
|
}
|
|
920
|
-
else if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
875
|
+
else if (fullKvOverBudget &&
|
|
876
|
+
(explicitSwaFull === false
|
|
877
|
+
? (summary.slidingWindow ?? 0) > 0
|
|
878
|
+
: isGemmaModel({
|
|
879
|
+
architecture: modelCatalogInfo?.architecture,
|
|
880
|
+
modelId: defaultModelId ?? undefined,
|
|
881
|
+
}))) {
|
|
882
|
+
// A sliding-window model whose GGUF hides the layer layout,
|
|
883
|
+
// over budget on full math: that math over-states the real cache
|
|
884
|
+
// and there is no exact substitute — leave the launch alone
|
|
885
|
+
// rather than over-clamp.
|
|
924
886
|
ladderPlan = null;
|
|
925
887
|
ladderKvLinearization = null;
|
|
926
888
|
}
|
|
927
|
-
// else:
|
|
928
|
-
//
|
|
929
|
-
//
|
|
889
|
+
// else: no SWA layers at all, or a layout-less model that fits on
|
|
890
|
+
// full math — the full-attention plan stands (it IS the real
|
|
891
|
+
// allocation, or safely above it).
|
|
930
892
|
}
|
|
931
893
|
if (ladderPlan !== null && envNumCtx === undefined) {
|
|
932
894
|
const windowedNote = ladderPlan === admission ? '' : ' (windowed KV admission)';
|
|
@@ -989,19 +951,12 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
989
951
|
}
|
|
990
952
|
}
|
|
991
953
|
// Price the KV the engine will ACTUALLY allocate. Mirrors the
|
|
992
|
-
// `--swa-full`
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
const effectiveSwaFull = config.llamaCppSwaFull ??
|
|
999
|
-
manifestEngineConfig?.swaFull ??
|
|
1000
|
-
(swaFullAutoFits !== false &&
|
|
1001
|
-
isGemmaModel({
|
|
1002
|
-
architecture: modelCatalogInfo?.architecture,
|
|
1003
|
-
modelId: defaultModelId ?? undefined,
|
|
1004
|
-
}));
|
|
954
|
+
// opt-in `--swa-full` in buildLlamaCppEngineArgs: an SWA model on the
|
|
955
|
+
// default windowed cache carries only its global layers per token
|
|
956
|
+
// plus a fixed window block per slot. Feeding the offload planner
|
|
957
|
+
// full-attention math for a windowed launch over-biased Gemma experts
|
|
958
|
+
// into system RAM for a cache that never materializes.
|
|
959
|
+
const effectiveSwaFull = config.llamaCppSwaFull ?? manifestEngineConfig?.swaFull ?? false;
|
|
1005
960
|
const windowedKvPlan = effectiveSwaFull
|
|
1006
961
|
? undefined
|
|
1007
962
|
: estimateWindowedKvLinearization({
|
|
@@ -1202,34 +1157,77 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
1202
1157
|
if (snapshot.expected)
|
|
1203
1158
|
return;
|
|
1204
1159
|
logFile.writeIncident(snapshot);
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
// Deliberately narrow: only SIGILL, and only before readiness.
|
|
1212
|
-
// A SIGSEGV during startup can be a corrupt model file, and any
|
|
1213
|
-
// crash after readiness is attributable to the work rather than
|
|
1214
|
-
// the binary — quarantining a backend over either would demote
|
|
1215
|
-
// users to slow engines for transient reasons.
|
|
1216
|
-
if (snapshot.signal !== 'SIGILL' || snapshot.reachedReady)
|
|
1160
|
+
if (snapshot.panicKind === 'host-out-of-memory') {
|
|
1161
|
+
noteHostOutOfMemory({
|
|
1162
|
+
model: modelCatalogInfo?.id ?? defaultModelId,
|
|
1163
|
+
whileStarting: !snapshot.reachedReady,
|
|
1164
|
+
});
|
|
1165
|
+
log.warn(`[llama-server] Linux's out-of-memory killer stopped the engine (${modelCatalogInfo?.id ?? 'model'}, incident=${snapshot.incidentId}, ${snapshot.reachedReady ? 'while serving' : 'while loading'})`);
|
|
1217
1166
|
return;
|
|
1218
|
-
|
|
1219
|
-
|
|
1167
|
+
}
|
|
1168
|
+
// A build that dies before it ever answers /health, without saying
|
|
1169
|
+
// why, may not be able to run here at all — and relaunching it on every
|
|
1170
|
+
// request just reproduces the crash while a working lower-tier build
|
|
1171
|
+
// sits in the next directory. Count it; a few in a row write it down so
|
|
1172
|
+
// the next launch routes around it. Which exits qualify (a silent
|
|
1173
|
+
// SIGILL; a GPU build's silent crash status on Windows or signal on
|
|
1174
|
+
// Linux; a CUDA build with no code for this GPU) is decided in
|
|
1175
|
+
// unrunnable-build.ts, how many in llama-quarantine.ts.
|
|
1176
|
+
const backend = binaryBackend;
|
|
1177
|
+
const unrunnable = unrunnableBuildExit(snapshot, backend);
|
|
1178
|
+
if (!backend || !unrunnable)
|
|
1220
1179
|
return;
|
|
1221
|
-
const entry =
|
|
1180
|
+
const { strikes, entry } = recordLlamaStartupCrash(home, {
|
|
1222
1181
|
backend,
|
|
1223
1182
|
binaryPath: binary,
|
|
1224
|
-
signal:
|
|
1225
|
-
reason:
|
|
1226
|
-
'this machine does not support, or faulted inside GPU library startup',
|
|
1183
|
+
signal: unrunnable.signal,
|
|
1184
|
+
reason: unrunnable.reason,
|
|
1227
1185
|
});
|
|
1228
|
-
if (entry) {
|
|
1229
|
-
|
|
1186
|
+
if (!entry) {
|
|
1187
|
+
if (strikes === 0)
|
|
1188
|
+
return;
|
|
1189
|
+
lastStartupCrash = {
|
|
1190
|
+
incidentId: snapshot.incidentId,
|
|
1191
|
+
verdict: { kind: 'strike', strikes, threshold: LLAMA_QUARANTINE_STRIKES },
|
|
1192
|
+
};
|
|
1193
|
+
log.warn(`[llama-server] the ${backend} build crashed before it was ready (${unrunnable.signal}, incident=${snapshot.incidentId}); ${strikes} of ${LLAMA_QUARANTINE_STRIKES} in a row switch it off`);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
log.warn(`[llama-server] quarantined the ${backend} build after ${strikes} crashes in a row (${unrunnable.signal}, incident=${snapshot.incidentId}); the next launch will fall back to another backend`);
|
|
1197
|
+
// Discovery reads the quarantine only at startup. Point the env at the
|
|
1198
|
+
// fallback now, so the pool's rebuild of this failed engine on the next
|
|
1199
|
+
// request launches it instead of the build just written down. A pinned
|
|
1200
|
+
// backend is a decision, and discovery never routes around one either;
|
|
1201
|
+
// an env that has already moved on belongs to someone else's fallback.
|
|
1202
|
+
const pinned = config.llamaCppBackendOverride !== undefined && config.llamaCppBackendOverride !== 'auto';
|
|
1203
|
+
const fallback = pinned || process.env.GEZEL_LLAMA_SERVER_BIN !== binary
|
|
1204
|
+
? null
|
|
1205
|
+
: adoptLlamaFallback(home, binary, backend);
|
|
1206
|
+
lastStartupCrash = {
|
|
1207
|
+
incidentId: snapshot.incidentId,
|
|
1208
|
+
verdict: { kind: 'quarantined', ...(fallback ? { switchedTo: fallback.backend } : {}) },
|
|
1209
|
+
};
|
|
1210
|
+
if (fallback) {
|
|
1211
|
+
log.warn(`[llama-server] the next launch uses the ${fallback.backend} build: ${fallback.path}`);
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
// Any start that reaches readiness proves the build runs here, so the
|
|
1215
|
+
// crashes before it were not in a row. It does not prove the build got
|
|
1216
|
+
// its GPU: a GPU backend that cannot load or initialize leaves the engine
|
|
1217
|
+
// on the CPU without crashing (gpu-startup-watch.ts).
|
|
1218
|
+
onReady: () => {
|
|
1219
|
+
if (binaryBackend)
|
|
1220
|
+
clearLlamaStartupStrikes(home, binaryBackend);
|
|
1221
|
+
const unused = gpuWatch?.verdict() ?? null;
|
|
1222
|
+
noteGpuStartup(unused);
|
|
1223
|
+
if (unused) {
|
|
1224
|
+
log.warn(`[llama-server] the ${unused.backend} build started without its GPU (${unused.reason}${unused.detail ? `: ${unused.detail}` : ''}); every token runs on the CPU`);
|
|
1230
1225
|
}
|
|
1231
1226
|
},
|
|
1232
|
-
onRawLine: (line) =>
|
|
1227
|
+
onRawLine: (line) => {
|
|
1228
|
+
gpuWatch?.observe(line);
|
|
1229
|
+
providerHolder.current?.onStdoutLine(line);
|
|
1230
|
+
},
|
|
1233
1231
|
// GPU-OOM recovery ladder: when a start dies of VRAM exhaustion and the
|
|
1234
1232
|
// planner's offload decision was in play, degrade it one step
|
|
1235
1233
|
// (partial split → all expert/FFN weights to RAM → engine-owned fit) and let the
|
|
@@ -1275,6 +1273,7 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
1275
1273
|
return true;
|
|
1276
1274
|
},
|
|
1277
1275
|
resolveLaunch: async () => {
|
|
1276
|
+
gpuWatch = new GpuStartupWatch(binaryBackend);
|
|
1278
1277
|
const port = cachedPort ?? (await pickFreePort());
|
|
1279
1278
|
cachedPort = port;
|
|
1280
1279
|
// Prepend the binary's own directory to PATH so the OS finds the
|
|
@@ -1301,9 +1300,6 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
1301
1300
|
// model output reaches `delta.content` for provider-side salvage.
|
|
1302
1301
|
reasoningFormat: process.env.GEZEL_LLAMA_REASONING_FORMAT?.trim() || undefined,
|
|
1303
1302
|
reasoningPreserve,
|
|
1304
|
-
architecture: modelCatalogInfo?.architecture,
|
|
1305
|
-
modelId: defaultModelId ?? undefined,
|
|
1306
|
-
swaFullAutoFits,
|
|
1307
1303
|
backend: llamaBuildMetadata?.backend ?? process.env.GEZEL_LLAMA_SERVER_BACKEND,
|
|
1308
1304
|
hostCpuCount: availableParallelism(),
|
|
1309
1305
|
});
|
|
@@ -1454,6 +1450,7 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
1454
1450
|
supervisor,
|
|
1455
1451
|
logFile,
|
|
1456
1452
|
slotSavePath,
|
|
1453
|
+
startupCrashVerdict: (incidentId) => lastStartupCrash?.incidentId === incidentId ? lastStartupCrash.verdict : undefined,
|
|
1457
1454
|
// Lets `listModels()` enumerate every installed model (not just
|
|
1458
1455
|
// the resident one) so `/v1/models` + pickers see the full set
|
|
1459
1456
|
// the engine pool can serve.
|
|
@@ -306,7 +306,8 @@ export class LlamaCppCacheAdapter {
|
|
|
306
306
|
if (!entry || typeof entry !== 'object')
|
|
307
307
|
continue;
|
|
308
308
|
const slot = entry.id;
|
|
309
|
-
|
|
309
|
+
// The slot's resident tokens (prompt plus generated); there is no `n_cache_tokens`.
|
|
310
|
+
const cachedTokens = entry.n_prompt_tokens;
|
|
310
311
|
if (typeof slot !== 'number' || typeof cachedTokens !== 'number' || cachedTokens <= 0) {
|
|
311
312
|
continue;
|
|
312
313
|
}
|
|
@@ -31,6 +31,12 @@ export interface LlamaDeviceProbe {
|
|
|
31
31
|
probedAt: string;
|
|
32
32
|
/** True when returned from the on-disk cache rather than a fresh spawn. */
|
|
33
33
|
cached: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The RAM the OS reported when the probe ran. Changing an integrated GPU's
|
|
36
|
+
* firmware carve-out changes this figure and the GPU's own, so a mismatch
|
|
37
|
+
* means the cached devices describe a machine that no longer exists.
|
|
38
|
+
*/
|
|
39
|
+
systemRamBytes?: number;
|
|
34
40
|
}
|
|
35
41
|
export interface NvidiaRuntimeDevice {
|
|
36
42
|
index: number;
|
|
@@ -72,16 +78,19 @@ export declare function pickBestGpuDevice(devices: LlamaDevice[]): LlamaDevice |
|
|
|
72
78
|
/**
|
|
73
79
|
* Probe device memory by running `<binaryPath> --list-devices`, caching
|
|
74
80
|
* the result. Reads the cache when present (VRAM capacity is stable per
|
|
75
|
-
* machine)
|
|
81
|
+
* machine) and the OS still reports the RAM it was probed with; pass
|
|
82
|
+
* `force` to re-probe. Any failure (spawn error, no
|
|
76
83
|
* cache, unparseable output) resolves to an empty device list — the
|
|
77
84
|
* planner then simply does nothing and the engine's own `--fit`/`-ngl
|
|
78
|
-
* auto` takes over. `nowIso`
|
|
85
|
+
* auto` takes over. `nowIso` and `systemRamBytes` are injectable for
|
|
86
|
+
* deterministic tests.
|
|
79
87
|
*/
|
|
80
88
|
export declare function probeLlamaDevices(opts: {
|
|
81
89
|
binaryPath: string;
|
|
82
90
|
home: string;
|
|
83
91
|
force?: boolean;
|
|
84
92
|
nowIso?: () => string;
|
|
93
|
+
systemRamBytes?: number;
|
|
85
94
|
}): Promise<LlamaDeviceProbe>;
|
|
86
95
|
/**
|
|
87
96
|
* Best-effort VRAM for a non-NVIDIA GPU (AMD Radeon, Intel Arc, any Vulkan
|