@bendyline/gezel-inference 1.0.8 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
  2. package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
  3. package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
  4. package/dist/catalog/chat-models/index.json +1 -1
  5. package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
  6. package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
  7. package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
  8. package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
  9. package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
  10. package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
  11. package/dist/catalog/chat-models/raw-index.json +1 -1
  12. package/dist/catalog/provenance.json +1 -1
  13. package/dist/engines/electron-native-reuse.d.ts +5 -4
  14. package/dist/engines/electron-native-reuse.js +1 -1
  15. package/dist/engines/native-file-manifest.json +12344 -698
  16. package/dist/engines/native-manifest.d.ts +4 -4
  17. package/dist/engines/native-manifest.js +40 -36
  18. package/dist/engines/resolver.d.ts +18 -17
  19. package/dist/engines/resolver.js +36 -1
  20. package/dist/http/model-inventory-cache.d.ts +1 -1
  21. package/dist/http/routes/engines.js +3 -3
  22. package/dist/models/catalog-drift.d.ts +2 -0
  23. package/dist/models/catalog-drift.js +1 -1
  24. package/dist/models/catalog-payload-identity.d.ts +2 -2
  25. package/dist/models/catalog-payload-identity.js +10 -5
  26. package/dist/providers/active-install-message.js +1 -1
  27. package/dist/providers/catalog-model-config.d.ts +28 -0
  28. package/dist/providers/ds4/build-provider.js +35 -13
  29. package/dist/providers/ds4/residency.d.ts +77 -0
  30. package/dist/providers/ds4/residency.js +87 -6
  31. package/dist/providers/image/sd-cpp.js +3 -2
  32. package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
  33. package/dist/providers/llama-cpp/backend-health.js +6 -3
  34. package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
  35. package/dist/providers/llama-cpp/build-provider.js +168 -29
  36. package/dist/providers/llama-cpp/devices.js +4 -1
  37. package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
  38. package/dist/providers/llama-cpp/engine-flags.js +13 -8
  39. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
  40. package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
  41. package/dist/providers/llama-cpp/models.d.ts +10 -0
  42. package/dist/providers/llama-cpp/models.js +64 -10
  43. package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
  44. package/dist/providers/llama-cpp/offload-planner.js +1 -1
  45. package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
  46. package/dist/providers/llama-cpp/spec-draft.js +113 -24
  47. package/dist/providers/mlx/python/spec_decode.py +23 -2
  48. package/dist/providers/mlx/python/tool_args_json.py +59 -21
  49. package/dist/providers/mlx/venv.d.ts +19 -1
  50. package/dist/providers/mlx/venv.js +21 -2
  51. package/dist/providers/native/device-capacity.d.ts +4 -0
  52. package/dist/providers/native/device-capacity.js +5 -0
  53. package/dist/providers/native/local-engine-runtime.d.ts +16 -1
  54. package/dist/providers/native/local-engine-runtime.js +142 -39
  55. package/dist/providers/native/supervisor.d.ts +8 -1
  56. package/dist/providers/native/supervisor.js +16 -7
  57. package/dist/providers/types.d.ts +4 -0
  58. package/dist/remotes/types.d.ts +1 -1
  59. package/package.json +4 -4
@@ -6,6 +6,10 @@ export { DS4_FULL_RESIDENCY_HEADROOM_BYTES };
6
6
  * Conservative floor for the full-residency working set. Exclusivity is an
7
7
  * explicit device lease at process startup, not a consequence of this number:
8
8
  * the automatic model budget can exceed 96 GiB on larger machines.
9
+ *
10
+ * Only for a launch nobody measured: an entry with `fullResidencyMemory` is
11
+ * reserved at that working set ({@link planDs4MeasuredLaunch}). Flooring a
12
+ * 45 GiB Qwen3.8 launch here put it over a 96 GiB Mac's 80 GiB budget.
9
13
  */
10
14
  export const DS4_FULL_RESIDENCY_RESERVATION_BYTES = 96 * GB;
11
15
  /**
@@ -80,20 +84,97 @@ export function shouldUseDs4SsdStreaming(opts = {}) {
80
84
  * the old RAM-tier heuristic which incorrectly gave both models 64 GiB on a
81
85
  * 128 GiB Mac. Manual values are still accepted, but clamped so fixed model
82
86
  * state + the cache cannot consume the runtime/OS reserve.
87
+ *
88
+ * The 24 GiB headroom guards a footprint guessed as `residentBytes −
89
+ * cacheExpertsBytes`, which omits graph and staging memory ds4 also holds.
90
+ * An entry with a measured streamed footprint has no such gap, so its cache
91
+ * may use whatever this device's local-model budget leaves beside it. A fixed
92
+ * 24 GiB refused Qwen3.8 on 32 GiB Macs whose budget held it with room to spare.
83
93
  */
84
94
  export function planDs4ExpertCache(opts) {
85
95
  const totalRamBytes = opts.totalRamBytes ?? totalMemoryBytes();
86
96
  const catalogCacheBytes = opts.catalogCacheBytes ?? 0;
87
- const fixedResidentBytes = Math.max(0, (opts.catalogResidentBytes ?? 0) - catalogCacheBytes);
88
97
  const fallbackGb = totalRamBytes >= 120 * GB ? 64 : totalRamBytes >= 88 * GB ? 48 : 32;
89
98
  const requestedGb = opts.configuredGb ?? (catalogCacheBytes > 0 ? catalogCacheBytes / GB : fallbackGb);
90
- const safeCacheBytes = totalRamBytes - fixedResidentBytes - DS4_FULL_RESIDENCY_HEADROOM_BYTES;
99
+ const modelBudget = opts.measuredFixedBytes !== undefined && opts.budgetBytes !== undefined;
100
+ const safeCacheBytes = modelBudget
101
+ ? opts.budgetBytes - opts.measuredFixedBytes
102
+ : totalRamBytes -
103
+ Math.max(0, (opts.catalogResidentBytes ?? 0) - catalogCacheBytes) -
104
+ DS4_FULL_RESIDENCY_HEADROOM_BYTES;
91
105
  const safeCacheGb = Math.floor(safeCacheBytes / GB);
92
- if (safeCacheGb < 1) {
93
- return { cacheGb: 1, requestedGb, clamped: true, safe: false };
106
+ const minCacheGb = Math.max(1, Math.ceil((opts.minCacheBytes ?? 0) / GB));
107
+ const limit = modelBudget ? 'model-budget' : 'headroom';
108
+ if (safeCacheGb < minCacheGb) {
109
+ return { cacheGb: minCacheGb, requestedGb, clamped: true, safe: false, limit };
110
+ }
111
+ const cacheGb = Math.max(minCacheGb, Math.min(requestedGb, safeCacheGb));
112
+ return { cacheGb, requestedGb, clamped: cacheGb !== requestedGb, safe: true, limit };
113
+ }
114
+ /** Evaluate a measured line at a window, never below its context-free part. */
115
+ export function ds4MeasuredBytesAt(line, ctxTokens) {
116
+ const contextFree = Math.max(0, line.bytes - line.bytesPerToken * line.ctxTokens);
117
+ return Math.round(contextFree + line.bytesPerToken * Math.max(0, ctxTokens));
118
+ }
119
+ function fullResidencyLine(source) {
120
+ const m = source?.fullResidencyMemory;
121
+ return m
122
+ ? { bytes: m.workingSetBytes, ctxTokens: m.ctxTokens, bytesPerToken: m.bytesPerToken }
123
+ : undefined;
124
+ }
125
+ function streamingLine(source) {
126
+ const m = source?.streamingMemory;
127
+ return m
128
+ ? { bytes: m.fixedBytes, ctxTokens: m.ctxTokens, bytesPerToken: m.bytesPerToken }
129
+ : undefined;
130
+ }
131
+ /** Measured streamed footprint without the expert cache, at a window; undefined when unmeasured. */
132
+ export function ds4StreamingFixedBytes(source, ctxTokens) {
133
+ const line = streamingLine(source);
134
+ return line ? ds4MeasuredBytesAt(line, ctxTokens) : undefined;
135
+ }
136
+ /**
137
+ * Price a launch from the entry's measurement for the chosen mode. Undefined
138
+ * when that mode is unmeasured (or a streamed launch has no budget to size
139
+ * against), so callers keep their catalog-`residentBytes` path, including the
140
+ * 96 GiB full-residency floor that stands in for a missing measurement.
141
+ */
142
+ export function planDs4MeasuredLaunch(opts) {
143
+ const sidecarBytes = opts.sidecarBytes ?? 0;
144
+ if (!opts.ssdStreaming) {
145
+ const line = fullResidencyLine(opts.source);
146
+ if (!line)
147
+ return undefined;
148
+ const contextFreeBytes = ds4MeasuredBytesAt(line, 0) + sidecarBytes;
149
+ return {
150
+ workingSetBytes: contextFreeBytes + line.bytesPerToken * Math.max(0, opts.numCtx),
151
+ contextFreeBytes,
152
+ bytesPerToken: line.bytesPerToken,
153
+ };
94
154
  }
95
- const cacheGb = Math.max(1, Math.min(requestedGb, safeCacheGb));
96
- return { cacheGb, requestedGb, clamped: cacheGb !== requestedGb, safe: true };
155
+ const line = streamingLine(opts.source);
156
+ if (!line || opts.budgetBytes === undefined)
157
+ return undefined;
158
+ const fixedBytes = ds4MeasuredBytesAt(line, opts.numCtx) + sidecarBytes;
159
+ const cache = planDs4ExpertCache({
160
+ ...(opts.configuredCacheGb !== undefined ? { configuredGb: opts.configuredCacheGb } : {}),
161
+ ...(opts.source?.cacheExpertsBytes !== undefined
162
+ ? { catalogCacheBytes: opts.source.cacheExpertsBytes }
163
+ : {}),
164
+ totalRamBytes: opts.totalRamBytes,
165
+ measuredFixedBytes: fixedBytes,
166
+ budgetBytes: opts.budgetBytes,
167
+ ...(opts.source?.streamingMemory?.minCacheBytes !== undefined
168
+ ? { minCacheBytes: opts.source.streamingMemory.minCacheBytes }
169
+ : {}),
170
+ });
171
+ const cacheBytes = cache.cacheGb * GB;
172
+ return {
173
+ workingSetBytes: fixedBytes + cacheBytes,
174
+ contextFreeBytes: ds4MeasuredBytesAt(line, 0) + sidecarBytes + cacheBytes,
175
+ bytesPerToken: line.bytesPerToken,
176
+ cache,
177
+ };
97
178
  }
98
179
  /** Effective capacity-broker reservation for the chosen DS4 residency mode. */
99
180
  export function ds4ResidentBytesForMode(catalogResidentBytes, ssdStreaming, ssdStreamingSupported = true) {
@@ -285,8 +285,9 @@ export class StableDiffusionCppProvider {
285
285
  this.launchState.modelId = modelId;
286
286
  await this.supervisor.stop();
287
287
  }
288
- // A render the caller abandons keeps running inside sd-server, which
289
- // cannot interrupt one it has started, so cancelling stops the engine.
288
+ // A render the caller abandons keeps running inside sd-server, whose
289
+ // `/sdapi` endpoints cannot interrupt one they have started, so
290
+ // cancelling stops the engine.
290
291
  const request = new AbortController();
291
292
  const stopRender = () => {
292
293
  request.abort();
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Whether llama-server is getting the hardware this machine has, as the
3
3
  * engine process sees it: the CUDA build discovery passed over for the
4
- * driver (`GEZEL_LLAMA_CUDA_BLOCKER`), a GPU build that started without its
4
+ * driver (`GEZEL_LLAMA_CUDA_BLOCKER`), the ROCm build it passed over for this
5
+ * machine's AMD GPUs (`GEZEL_LLAMA_ROCM_BLOCKER`), a GPU build that started without its
5
6
  * GPU (gpu-startup-watch.ts), and Linux's out-of-memory killer stopping the
6
7
  * engine. Each is invisible otherwise — the engine keeps answering, slower,
7
8
  * or a chat fails with a crash that reads like any other.
@@ -24,7 +25,7 @@ export declare function noteHostOutOfMemory(input: {
24
25
  model?: string | undefined;
25
26
  whileStarting: boolean;
26
27
  }, now?: Date): void;
27
- declare const BACKENDS: readonly ["cuda", "vulkan", "metal", "cpu"];
28
+ declare const BACKENDS: readonly ["cuda", "rocm", "vulkan", "metal", "cpu"];
28
29
  /**
29
30
  * The backend this process's on-device llama-server runs, for catalog
30
31
  * backend exclusions: the build discovery resolved, else what the probe
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Whether llama-server is getting the hardware this machine has, as the
3
3
  * engine process sees it: the CUDA build discovery passed over for the
4
- * driver (`GEZEL_LLAMA_CUDA_BLOCKER`), a GPU build that started without its
4
+ * driver (`GEZEL_LLAMA_CUDA_BLOCKER`), the ROCm build it passed over for this
5
+ * machine's AMD GPUs (`GEZEL_LLAMA_ROCM_BLOCKER`), a GPU build that started without its
5
6
  * GPU (gpu-startup-watch.ts), and Linux's out-of-memory killer stopping the
6
7
  * engine. Each is invisible otherwise — the engine keeps answering, slower,
7
8
  * or a chat fails with a crash that reads like any other.
@@ -9,7 +10,7 @@
9
10
  * Process-wide like the backend env it sits beside: every llama provider in
10
11
  * this process launches the same build.
11
12
  */
12
- import { missingMachineRuntime, readLlamaCudaBlocker, vcRedistributableUrl, } from '@bendyline/gezel/native';
13
+ import { missingMachineRuntime, readLlamaCudaBlocker, readRocmBlocker, vcRedistributableUrl, } from '@bendyline/gezel/native';
13
14
  /**
14
15
  * Reading an engine's import tables takes tens of milliseconds of synchronous
15
16
  * file reads, and the answer only changes when someone installs the runtime.
@@ -61,7 +62,7 @@ export function noteHostOutOfMemory(input, now = new Date()) {
61
62
  whileStarting: input.whileStarting,
62
63
  };
63
64
  }
64
- const BACKENDS = ['cuda', 'vulkan', 'metal', 'cpu'];
65
+ const BACKENDS = ['cuda', 'rocm', 'vulkan', 'metal', 'cpu'];
65
66
  /**
66
67
  * The backend this process's on-device llama-server runs, for catalog
67
68
  * backend exclusions: the build discovery resolved, else what the probe
@@ -85,6 +86,7 @@ export function llamaBackendHealthView() {
85
86
  const runningRaw = process.env.GEZEL_LLAMA_SERVER_BACKEND;
86
87
  const running = BACKENDS.find((backend) => backend === runningRaw);
87
88
  const cudaBlocker = readLlamaCudaBlocker();
89
+ const rocmBlocker = readRocmBlocker();
88
90
  // A verdict about a build the process has since moved off (a quarantine
89
91
  // fallback, a lifted quarantine) no longer describes what runs.
90
92
  const unused = gpuUnused && gpuUnused.backend === running ? gpuUnused : null;
@@ -92,6 +94,7 @@ export function llamaBackendHealthView() {
92
94
  return {
93
95
  ...(running ? { running } : {}),
94
96
  ...(cudaBlocker ? { cudaBlocker } : {}),
97
+ ...(rocmBlocker ? { rocmBlocker } : {}),
95
98
  ...(unused ? { gpuUnused: unused } : {}),
96
99
  ...(missingRuntime ? { missingRuntime } : {}),
97
100
  ...(outOfMemory ? { outOfMemory } : {}),
@@ -11,7 +11,7 @@
11
11
  * next engine launch instead of the next app start.
12
12
  */
13
13
  import { LLAMA_QUARANTINE_STRIKES, clearLlamaQuarantine, createLlamaBuildCheck, isBinaryQuarantined, llamaVariantBinaryPath, publishLlamaCudaBlocker, readLlamaQuarantine, resolveLlamaFallbackBinary, resolveLlamaVariantBinary, } from '@bendyline/gezel/native';
14
- const BACKENDS = ['cuda', 'vulkan', 'metal', 'cpu'];
14
+ const BACKENDS = ['cuda', 'rocm', 'vulkan', 'metal', 'cpu'];
15
15
  export function isLlamaBackend(value) {
16
16
  return BACKENDS.some((backend) => backend === value);
17
17
  }
@@ -1,10 +1,11 @@
1
- import { delimiter, dirname, join } from 'node:path';
2
- import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, llamaCppBackendExclusion, llamaCppBackendLabel, } from '@bendyline/gezel';
3
- import { LLAMA_QUARANTINE_STRIKES, clearLlamaStartupStrikes, recordLlamaStartupCrash, unrunnableBuildExit, } from '@bendyline/gezel/native';
1
+ import { basename, delimiter, dirname, join } from 'node:path';
2
+ import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, llamaCppBackendExclusion, llamaCppBackendLabel, llamaCppDraftResidentBytes, llamaCppSpecNeedsDraftModel, } from '@bendyline/gezel';
3
+ import { LLAMA_QUARANTINE_STRIKES, clearLlamaStartupStrikes, recordLlamaStartupCrash, rocmLaunchEnv, unrunnableBuildExit, } from '@bendyline/gezel/native';
4
4
  import { gezelPaths } from '@bendyline/gezel/paths';
5
5
  import { mmprojBudgetBytes, nativeVisionEnabledFor } from '../../chat/vision-capability.js';
6
6
  import { effectiveEngineRelease, isEnginePinned } from '../../engines/native-manifest.js';
7
7
  import { engineInstallPhase } from '../../engines/registry.js';
8
+ import { llamaDownloadVariant } from '../../engines/resolver.js';
8
9
  import { availableCpuCount, containerCpuLimit } from '../../system/host-memory.js';
9
10
  import { localDeviceSettingsLabel, noModelYetMessage, notReadyError, } from '../active-install-message.js';
10
11
  import { resolveCatalogLlamaCppEngineConfig, resolveCatalogReasoningBudget, } from '../catalog-model-config.js';
@@ -27,7 +28,7 @@ import { isGemmaModel, planLlamaCppKv, resolveLlamaCppKvCacheType, } from './kv-
27
28
  import { degradeDenseFfnOffloadDecision, degradeMoeOffloadDecision, estimateExactPerSlotKvBytesF16, estimateKvReserveBytes, estimateWindowedKvLinearization, planDenseFfnOffload, planMoeOffload, shedSlotsForGpuResidency, } from './offload-planner.js';
28
29
  import { LlamaCppProvider } from './provider.js';
29
30
  import { reasoningLaunchOverridesFromEnv } from './reasoning-launch.js';
30
- import { resolveSpecDraft } from './spec-draft.js';
31
+ import { brokenDrafters, drafterCostsContext, drafterCostsGpuResidency, llamaCppSidecarLoads, resolveSpecDraft, } from './spec-draft.js';
31
32
  const log = createLogger('chat');
32
33
  function activeLlamaInstall(models) {
33
34
  return models?.getActiveInstalls().find((i) => i.phase !== undefined) ?? null;
@@ -50,7 +51,7 @@ export function ensureLlamaEngineStatus(registry, config) {
50
51
  const override = config.llamaCppBackendOverride;
51
52
  const backend = override && override !== 'auto'
52
53
  ? override
53
- : process.env.GEZEL_LLAMA_DETECTED_BACKEND || undefined;
54
+ : llamaDownloadVariant(process.env.GEZEL_LLAMA_DETECTED_BACKEND || undefined);
54
55
  const { snapshot } = registry.ensure('llama-server', backend);
55
56
  if (snapshot.error) {
56
57
  return { detail: `On-device engine couldn't be downloaded: ${snapshot.error}`, failed: true };
@@ -283,10 +284,13 @@ export async function buildLlamaCppProvider(opts) {
283
284
  }
284
285
  // Install refuses an excluded backend; this catches a model installed
285
286
  // before the exclusion was authored, or copied in by hand.
287
+ let catalogDraftResidentBytes;
286
288
  if (modelCatalogInfo && opts.catalog) {
287
289
  const engineBackend = binaryBackend ?? currentLlamaCppBackend();
288
290
  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);
291
+ const catalogSource = detail?.manifest.kind === 'chat-model' ? detail.manifest.llamaCpp : undefined;
292
+ catalogDraftResidentBytes = llamaCppDraftResidentBytes(catalogSource?.draftModel, modelCatalogInfo.draftModelPath);
293
+ const excluded = llamaCppBackendExclusion(catalogSource, engineBackend);
290
294
  if (excluded && engineBackend) {
291
295
  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
296
  err.isActionable = true;
@@ -338,6 +342,41 @@ export async function buildLlamaCppProvider(opts) {
338
342
  const manifestEngineConfig = opts.catalog
339
343
  ? await resolveCatalogLlamaCppEngineConfig(opts.catalog, opts.modelOverride?.modelId ?? defaultModelId)
340
344
  : undefined;
345
+ // Speculative-decoding resolution — the catalog's per-backend overlay, and
346
+ // whether the drafter it names is on disk. Settled here, above the memory
347
+ // planning, for the same reason as vision: the launch and the budget must
348
+ // agree on whether a drafter is resident. See providers/llama-cpp/spec-draft.ts.
349
+ const llamaBuildMetadata = binary ? await readLlamaCppBuildMetadata(binary) : null;
350
+ const llamaBackend = llamaBuildMetadata?.backend ?? process.env.GEZEL_LLAMA_SERVER_BACKEND;
351
+ const resolveSpec = (installedDraftModelPath) => resolveSpecDraft({
352
+ perModel: manifestEngineConfig,
353
+ backend: llamaBackend,
354
+ configSpecType: config.llamaCppSpecType,
355
+ configDraftModelPath: config.llamaCppDraftModelPath,
356
+ installedDraftModelPath,
357
+ resolveDraftPath: (id) => opts.llamaCppModels ? opts.llamaCppModels.resolveModelPath(id) : Promise.resolve(null),
358
+ });
359
+ const brokenDrafterKey = `${binary ?? ''}|${modelCatalogInfo?.draftModelPath ?? ''}`;
360
+ let drafterInUse = brokenDrafters.has(brokenDrafterKey)
361
+ ? undefined
362
+ : modelCatalogInfo?.draftModelPath;
363
+ let specDraft = await resolveSpec(drafterInUse);
364
+ if (specDraft.log)
365
+ log[specDraft.log.level](specDraft.log.message);
366
+ let resolvedManifestEngineConfig = specDraft.perModel;
367
+ const specTypeNow = () => config.llamaCppSpecType ?? resolvedManifestEngineConfig?.spec?.type;
368
+ const draftBudgetBytes = () => (drafterInUse ? (modelCatalogInfo?.draftModelSizeBytes ?? 0) : 0);
369
+ // A drafter the catalog chose, which this launch can run without. One someone
370
+ // forced stays theirs to answer for.
371
+ const catalogDrafterLoaded = () => !!drafterInUse &&
372
+ !config.llamaCppSpecType &&
373
+ !config.llamaCppDraftModelPath &&
374
+ llamaCppSpecNeedsDraftModel(resolvedManifestEngineConfig?.spec?.type);
375
+ const leaveDrafterOut = async () => {
376
+ drafterInUse = undefined;
377
+ specDraft = await resolveSpec(undefined);
378
+ resolvedManifestEngineConfig = specDraft.perModel;
379
+ };
341
380
  const PREFERRED_CTX_DEFAULT = 65_536;
342
381
  // Explicit numeric values win: env, then the per-model override, then the
343
382
  // machine-wide config. Otherwise Adaptive uses the per-model manifest
@@ -413,6 +452,13 @@ export async function buildLlamaCppProvider(opts) {
413
452
  headerSummary = null;
414
453
  }
415
454
  }
455
+ if (!llamaCppSidecarLoads({
456
+ specType: specTypeNow(),
457
+ targetHasMtp: (headerSummary?.nextnPredictLayers ?? 0) > 0,
458
+ perBackend: !!manifestEngineConfig?.spec?.byBackend,
459
+ })) {
460
+ drafterInUse = undefined;
461
+ }
416
462
  const exactPerSlotKvF16 = headerSummary
417
463
  ? estimateExactPerSlotKvBytesF16({
418
464
  blockCount: headerSummary.blockCount,
@@ -462,8 +508,10 @@ export async function buildLlamaCppProvider(opts) {
462
508
  // contract in planLlamaCppKv. Priced from the cached exact GGUF walk above,
463
509
  // including any v0.4 lazy-tensor deduction.
464
510
  const PLAN_REFERENCE_CTX = 4096;
465
- const planResidentBytes = estimateLlamaCppResidentBytes(planWeightsBytes, {
511
+ const planResidentBytes = () => estimateLlamaCppResidentBytes(planWeightsBytes, {
466
512
  mmprojBytes: visionBudgetBytes,
513
+ draftBytes: draftBudgetBytes(),
514
+ draftResidentBytes: catalogDraftResidentBytes,
467
515
  });
468
516
  const planCapacity = hostCapacity;
469
517
  const planKvBytesPerToken = (kv) => {
@@ -506,7 +554,7 @@ export async function buildLlamaCppProvider(opts) {
506
554
  slots: slotCount,
507
555
  minimumPerTurnCtxTokens: ctxTokens,
508
556
  kvBytesPerToken: planKvBytesPerToken(kv),
509
- weightsResidentBytes: planResidentBytes,
557
+ weightsResidentBytes: planResidentBytes(),
510
558
  budgetBytes: brokerSnap?.enforced ? brokerSnap.budgetBytes : planCapacity.budgetBytes,
511
559
  committedOtherBytes,
512
560
  freeSystemRamBytes: availableSystemRamBytes(),
@@ -514,7 +562,51 @@ export async function buildLlamaCppProvider(opts) {
514
562
  });
515
563
  return probe.minimumSatisfied && probe.slots >= slotCount;
516
564
  };
517
- const kvPlan = planLlamaCppKv({
565
+ // One slot's KV at `ctxTokens`, priced the way the launch reservation
566
+ // prices it below: the windowed cache unless `--swa-full` is pinned.
567
+ const planSlotKvReserveBytes = (kv, ctxTokens) => {
568
+ if (!headerSummary)
569
+ return planKvBytesPerToken(kv) * ctxTokens;
570
+ const geometry = {
571
+ blockCount: headerSummary.blockCount,
572
+ embeddingLength: headerSummary.embeddingLength,
573
+ headCount: headerSummary.headCount,
574
+ headCountKv: headerSummary.headCountKv,
575
+ headCountKvPerLayer: headerSummary.headCountKvPerLayer,
576
+ slidingWindowPattern: headerSummary.slidingWindowPattern,
577
+ sharedKvLayers: headerSummary.sharedKvLayers,
578
+ loopCount: headerSummary.loopCount,
579
+ keyLength: headerSummary.keyLength,
580
+ valueLength: headerSummary.valueLength,
581
+ keyLengthSwa: headerSummary.keyLengthSwa,
582
+ valueLengthSwa: headerSummary.valueLengthSwa,
583
+ fullAttentionInterval: headerSummary.fullAttentionInterval,
584
+ ssmInnerSize: headerSummary.ssmInnerSize,
585
+ ssmStateSize: headerSummary.ssmStateSize,
586
+ ssmConvKernel: headerSummary.ssmConvKernel,
587
+ kvCacheType: kv,
588
+ };
589
+ const windowed = (config.llamaCppSwaFull ?? manifestEngineConfig?.swaFull ?? false)
590
+ ? undefined
591
+ : estimateWindowedKvLinearization({
592
+ ...geometry,
593
+ slidingWindow: headerSummary.slidingWindow,
594
+ });
595
+ return windowed
596
+ ? windowed.fixedBytes + windowed.bytesPerToken * ctxTokens
597
+ : (estimateKvReserveBytes({ ...geometry, ctxTokens }) ?? planKvBytesPerToken(kv) * ctxTokens);
598
+ };
599
+ // What a discrete card leaves this launch now: free graphics memory as
600
+ // device admission measures it, within the card's usable share. Undefined
601
+ // off discrete cards, where there is no separate pool to run out of.
602
+ const planGpuPoolBytes = async () => {
603
+ const kind = brokerSnap?.enforced ? brokerSnap.pools.kind : hostCapacity.kind;
604
+ if (kind !== 'discrete-gpu')
605
+ return undefined;
606
+ const freeGpu = await measureAvailableGpuBytes().catch(() => undefined);
607
+ return Math.max(0, Math.min(budgetBytes - committedOtherBytes, freeGpu ?? Number.POSITIVE_INFINITY));
608
+ };
609
+ const planKv = () => planLlamaCppKv({
518
610
  architecture: modelCatalogInfo?.architecture,
519
611
  modelId: defaultModelId ?? undefined,
520
612
  override: config.llamaCppKvCacheType,
@@ -527,6 +619,42 @@ export async function buildLlamaCppProvider(opts) {
527
619
  fitsAt: planFitsAt,
528
620
  maxSlots: defaultLocalEngineSlots(budgetBytes),
529
621
  });
622
+ let kvPlan = planKv();
623
+ // A drafter buys speed; context is what a session runs out of. When a
624
+ // drafter the catalog chose would cap the target window or cost its KV
625
+ // precision, it goes and the model decodes the way it does without one.
626
+ // Growth above the target, sized later, simply has less room. On a
627
+ // discrete card it must also leave the model wholly on the GPU in the
628
+ // memory free now, which is what device admission charges the launch.
629
+ if (draftBudgetBytes() > 0 && !config.llamaCppSpecType && !config.llamaCppDraftModelPath) {
630
+ const targetCtx = kvPlan.ctxCapTokens ?? effectiveNumCtx;
631
+ const fitsWithDrafter = planFitsAt(kvPlan.kvCacheType, targetCtx, 1);
632
+ const drafter = drafterInUse;
633
+ drafterInUse = undefined;
634
+ const withoutDrafter = planKv();
635
+ drafterInUse = drafter;
636
+ let drafterCost;
637
+ if (drafterCostsContext(kvPlan, withoutDrafter, fitsWithDrafter)) {
638
+ drafterCost = `beside it the ${effectiveNumCtx}-token window would not keep its size and KV precision`;
639
+ }
640
+ else {
641
+ const gpuPoolBytes = await planGpuPoolBytes();
642
+ const withDrafterBytes = planResidentBytes() + planSlotKvReserveBytes(kvPlan.kvCacheType, targetCtx);
643
+ if (drafterCostsGpuResidency(withDrafterBytes, gpuPoolBytes)) {
644
+ drafterCost = `beside it the model needs ${(withDrafterBytes / 1024 ** 3).toFixed(1)} GiB of graphics memory and ${((gpuPoolBytes ?? 0) / 1024 ** 3).toFixed(1)} GiB is available`;
645
+ }
646
+ }
647
+ if (drafterCost) {
648
+ log.info(`[llama-cpp] ${modelCatalogInfo?.id ?? defaultModelId ?? 'model'}: leaving the speculative drafter out; ${drafterCost}`);
649
+ await leaveDrafterOut();
650
+ kvPlan = withoutDrafter;
651
+ }
652
+ }
653
+ // What a loaded drafter holds on the GPU, as the resident estimate prices it.
654
+ const drafterGpuBytes = () => estimateLlamaCppResidentBytes(0, {
655
+ draftBytes: draftBudgetBytes(),
656
+ draftResidentBytes: catalogDraftResidentBytes,
657
+ }) - estimateLlamaCppResidentBytes(0);
530
658
  kvCacheType = kvPlan.kvCacheType;
531
659
  if (kvPlan.upgraded) {
532
660
  log.info(`[llama-cpp] ${modelCatalogInfo?.id ?? defaultModelId ?? 'model'}: trading f16 KV for q8_0 to fit a second engine slot (single-slot SWA session alternation re-prefills wholesale; KV A/B 2026-08-03 showed no measurable q8_0 fidelity cost)`);
@@ -552,7 +680,7 @@ export async function buildLlamaCppProvider(opts) {
552
680
  // concurrent requests correctly with zero engine errors, so the failure this
553
681
  // clamp guards against looks stale — but one probe is not grounds for
554
682
  // deleting a safety measure, so it stays for anyone who opted in explicitly.
555
- const selectedSpecType = config.llamaCppSpecType ?? manifestEngineConfig?.spec?.type;
683
+ const selectedSpecType = specTypeNow();
556
684
  if (selectedSpecType === 'draft-mtp' && slots > 1) {
557
685
  log.info(`[llama-cpp] limiting ${modelCatalogInfo?.id ?? 'MTP model'} to one slot because draft-mtp is not reliable with --parallel > 1 in the bundled engine`);
558
686
  slots = 1;
@@ -614,18 +742,6 @@ export async function buildLlamaCppProvider(opts) {
614
742
  : catalogReasoningBudgetTokens !== undefined
615
743
  ? 'catalog'
616
744
  : 'unbounded';
617
- // Speculative-decoding draft resolution — a manifest names its draft by
618
- // catalog id; resolve it to the installed GGUF path (or disable) before the
619
- // argv is built. See providers/llama-cpp/spec-draft.ts for the full rules.
620
- const specDraft = await resolveSpecDraft({
621
- perModel: manifestEngineConfig,
622
- configSpecType: config.llamaCppSpecType,
623
- configDraftModelPath: config.llamaCppDraftModelPath,
624
- resolveDraftPath: (id) => opts.llamaCppModels ? opts.llamaCppModels.resolveModelPath(id) : Promise.resolve(null),
625
- });
626
- const resolvedManifestEngineConfig = specDraft.perModel;
627
- if (specDraft.log)
628
- log[specDraft.log.level](specDraft.log.message);
629
745
  // Phase v4 — hardware-aware structured-weight offload. Probe device VRAM (cached to
630
746
  // `engines/llama-cpp/devices.json` via `--list-devices`) and read the
631
747
  // model's MoE metadata from its GGUF header; the planner decides
@@ -654,7 +770,6 @@ export async function buildLlamaCppProvider(opts) {
654
770
  // cross-check for an explicit draft-mtp request.
655
771
  let ggufHasMtp = false;
656
772
  let mtpLayerCount = 0;
657
- const llamaBuildMetadata = binary ? await readLlamaCppBuildMetadata(binary) : null;
658
773
  let llamaGpuDevice = null;
659
774
  let nvidiaRuntimeDevice;
660
775
  let llamaDevices = [];
@@ -677,8 +792,10 @@ export async function buildLlamaCppProvider(opts) {
677
792
  offloadBlockCount = summary.blockCount;
678
793
  mtpLayerCount = summary.nextnPredictLayers ?? 0;
679
794
  ggufHasMtp = mtpLayerCount > 0;
680
- if (!ggufHasMtp && modelCatalogInfo?.draftModelPath) {
681
- const draftSummary = await readGgufSummaryAsync(modelCatalogInfo.draftModelPath);
795
+ // Only a companion this launch loads can supply the head: one left out
796
+ // for memory would turn MTP on with nothing to run it.
797
+ if (!ggufHasMtp && drafterInUse) {
798
+ const draftSummary = await readGgufSummaryAsync(drafterInUse);
682
799
  mtpLayerCount = draftSummary.nextnPredictLayers ?? 0;
683
800
  ggufHasMtp = mtpLayerCount > 0;
684
801
  }
@@ -687,6 +804,8 @@ export async function buildLlamaCppProvider(opts) {
687
804
  const residentWeightBytes = Math.max(0, approxBytes - lazyTensorBytes);
688
805
  const residentBytes = estimateLlamaCppResidentBytes(residentWeightBytes, {
689
806
  mmprojBytes: visionBudgetBytes,
807
+ draftBytes: draftBudgetBytes(),
808
+ draftResidentBytes: catalogDraftResidentBytes,
690
809
  });
691
810
  if (lazyTensorBytes > 0) {
692
811
  log.info(`[llama-cpp] ${modelCatalogInfo?.id ?? defaultModelId ?? 'model'}: lazy-reading ${(lazyTensorBytes / 1024 ** 3).toFixed(1)} GiB of row-addressable embeddings from the GGUF; budgeting ${(residentWeightBytes / 1024 ** 3).toFixed(1)} GiB as resident model weights`);
@@ -1025,6 +1144,7 @@ export async function buildLlamaCppProvider(opts) {
1025
1144
  split,
1026
1145
  ...(summary.blockCount !== undefined ? { blockCount: summary.blockCount } : {}),
1027
1146
  ...(kvReserveBytes !== undefined ? { kvReserveBytes } : {}),
1147
+ additionalGpuBytes: drafterGpuBytes(),
1028
1148
  }
1029
1149
  : {}),
1030
1150
  });
@@ -1063,7 +1183,7 @@ export async function buildLlamaCppProvider(opts) {
1063
1183
  ...(denseFfnSplit ? { split: denseFfnSplit } : {}),
1064
1184
  ...(summary.blockCount !== undefined ? { blockCount: summary.blockCount } : {}),
1065
1185
  ...(kvReserveBytes !== undefined ? { kvReserveBytes } : {}),
1066
- additionalGpuBytes: visionBudgetBytes,
1186
+ additionalGpuBytes: visionBudgetBytes + drafterGpuBytes(),
1067
1187
  ramBudgetBytes: brokerSnap?.enforced
1068
1188
  ? brokerSnap.pools.ramShareBytes
1069
1189
  : hostCapacity.ramShareBytes,
@@ -1100,8 +1220,8 @@ export async function buildLlamaCppProvider(opts) {
1100
1220
  // metadata gate remains load-bearing even though supported GGUFs are
1101
1221
  // now default-on.
1102
1222
  if (ggufHasMtp) {
1103
- log.info(`[llama-cpp] ${modelCatalogInfo?.id ?? 'model'} ships an MTP head (nextn_predict_layers=${mtpLayerCount}); speculative decoding ${config.llamaCppSpecType || manifestEngineConfig?.spec?.type
1104
- ? 'follows the explicit setting'
1223
+ log.info(`[llama-cpp] ${modelCatalogInfo?.id ?? 'model'} ships an MTP head (nextn_predict_layers=${mtpLayerCount}); speculative decoding ${specTypeNow()
1224
+ ? `runs ${specTypeNow()} (${config.llamaCppSpecType ? 'Settings' : 'catalog'})`
1105
1225
  : 'is ON by default (+4-19% decode; disable with llamaCppSpecType: "none")'}.`);
1106
1226
  }
1107
1227
  }
@@ -1183,6 +1303,8 @@ export async function buildLlamaCppProvider(opts) {
1183
1303
  providerHolder.current?.getCacheAdapter()?.resetEngineState();
1184
1304
  if (snapshot.expected)
1185
1305
  return;
1306
+ if (catalogDrafterLoaded())
1307
+ brokenDrafters.add(brokenDrafterKey);
1186
1308
  logFile.writeIncident(snapshot);
1187
1309
  if (snapshot.panicKind === 'host-out-of-memory') {
1188
1310
  noteHostOutOfMemory({
@@ -1264,6 +1386,15 @@ export async function buildLlamaCppProvider(opts) {
1264
1386
  // restarts of this provider, so a recovered engine doesn't re-OOM on its
1265
1387
  // next boot.
1266
1388
  recoverStartup: ({ panicKind }) => {
1389
+ // The drafter is the one part of this launch it can do without, and
1390
+ // drafters have changed format between engine releases. Without this a
1391
+ // bad one reads as a build that cannot run, and three of those quarantine
1392
+ // the whole GPU backend; `resolveLaunch` drops it from the retry.
1393
+ if (catalogDrafterLoaded()) {
1394
+ brokenDrafters.add(brokenDrafterKey);
1395
+ log.warn(`[llama-cpp] ${panicKind ?? 'failed start'} while starting ${modelCatalogInfo?.id ?? 'model'} with its speculative drafter — retrying without it`);
1396
+ return true;
1397
+ }
1267
1398
  if (panicKind !== 'cuda-out-of-memory' && panicKind !== 'vulkan-out-of-memory') {
1268
1399
  return false;
1269
1400
  }
@@ -1318,6 +1449,9 @@ export async function buildLlamaCppProvider(opts) {
1318
1449
  return true;
1319
1450
  },
1320
1451
  resolveLaunch: async () => {
1452
+ if (catalogDrafterLoaded() && brokenDrafters.has(brokenDrafterKey)) {
1453
+ await leaveDrafterOut();
1454
+ }
1321
1455
  gpuWatch = new GpuStartupWatch(binaryBackend);
1322
1456
  // Again at every launch, not only at build: llama.cpp from the 0.1.51
1323
1457
  // release refuses a --slot-save-path that does not exist ("not a
@@ -1345,7 +1479,7 @@ export async function buildLlamaCppProvider(opts) {
1345
1479
  kvCacheType,
1346
1480
  slots,
1347
1481
  ggufHasMtp,
1348
- installedDraftModelPath: modelCatalogInfo?.draftModelPath,
1482
+ installedDraftModelPath: drafterInUse,
1349
1483
  // Opt-in A/B lever: `GEZEL_LLAMA_REASONING_FORMAT=none`
1350
1484
  // disables llama-server's chat-template output parsing so mangled
1351
1485
  // model output reaches `delta.content` for provider-side salvage.
@@ -1483,6 +1617,8 @@ export async function buildLlamaCppProvider(opts) {
1483
1617
  ubatchSize: lastArgValue(resolvedAdvancedArgs, '--ubatch-size') ?? 'default',
1484
1618
  flashAttention: lastArgValue(resolvedAdvancedArgs, '--flash-attn') ?? 'default',
1485
1619
  threads: lastArgValue(resolvedAdvancedArgs, '--threads') ?? 'default',
1620
+ specType: lastArgValue(resolvedAdvancedArgs, '--spec-type') ?? 'default',
1621
+ specDraftModel: basename(lastArgValue(resolvedAdvancedArgs, '--spec-draft-model') ?? 'none'),
1486
1622
  reasoningBudgetTokens: reasoningBudgetTokens ?? 'unbounded',
1487
1623
  reasoningBudgetSource,
1488
1624
  reasoningPreserve,
@@ -1493,6 +1629,9 @@ export async function buildLlamaCppProvider(opts) {
1493
1629
  // Equivalent to `--api-key`, but out of `argv` so it does not show
1494
1630
  // up in `ps` on a shared machine.
1495
1631
  LLAMA_API_KEY: engineApiKey(),
1632
+ // A ROCm build's kernel packs, rocBLAS routing, and the AMD GPUs it
1633
+ // has no code for kept out of sight (rocm-devices.ts).
1634
+ ...rocmLaunchEnv(home, binary, llamaBuildMetadata?.backend ?? binaryBackend),
1496
1635
  },
1497
1636
  baseUrl: `http://127.0.0.1:${port}`,
1498
1637
  };
@@ -19,7 +19,7 @@ import { execFile } from 'node:child_process';
19
19
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
20
20
  import { delimiter, dirname, join } from 'node:path';
21
21
  import { promisify } from 'node:util';
22
- import { windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
22
+ import { rocmLaunchEnv, windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
23
23
  import { gezelHome } from '@bendyline/gezel/paths';
24
24
  import { totalMemoryBytes } from '../../system/host-memory.js';
25
25
  const execFileAsync = promisify(execFile);
@@ -191,6 +191,9 @@ export async function probeLlamaDevices(opts) {
191
191
  env: {
192
192
  ...process.env,
193
193
  PATH: inheritedPath ? `${binDir}${delimiter}${inheritedPath}` : binDir,
194
+ // A ROCm build lists only the GPUs its launch will see, so the VRAM
195
+ // the planner reads is the VRAM the engine gets.
196
+ ...rocmLaunchEnv(home, binaryPath),
194
197
  },
195
198
  // This is an awaited probe, not an independent process. Hide its
196
199
  // Windows console without detaching it from the daemon.
@@ -64,6 +64,12 @@ export interface PerModelLlamaCppEngineConfig {
64
64
  mtp?: boolean;
65
65
  draftModelId?: string;
66
66
  nMax?: number;
67
+ sampling?: 'greedy' | 'probabilistic';
68
+ byBackend?: Partial<Record<string, {
69
+ type?: string;
70
+ nMax?: number;
71
+ sampling?: 'greedy' | 'probabilistic';
72
+ }>>;
67
73
  };
68
74
  }
69
75
  /** Phase v2 hardware-aware offload decision (lowest precedence). */
@@ -106,7 +112,8 @@ export interface EngineFlagInput {
106
112
  /**
107
113
  * Absolute path to a catalog-installed speculative draft sidecar.
108
114
  * Separate MTP assistants (Gemma) and other draft algorithms need this;
109
- * combined MTP GGUFs (Qwen) intentionally leave it unset.
115
+ * combined MTP GGUFs (Qwen) leave it unset, even with a DFlash drafter on
116
+ * disk: `--spec-draft-model` would make MTP load the wrong file.
110
117
  */
111
118
  installedDraftModelPath?: string | undefined;
112
119
  /**