@bendyline/gezel-inference 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) 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/http/routes/v1-images.d.ts +2 -1
  23. package/dist/http/routes/v1-images.js +36 -9
  24. package/dist/knowledge/manager.d.ts +3 -27
  25. package/dist/knowledge/manager.js +8 -364
  26. package/dist/knowledge/unified-search.d.ts +61 -0
  27. package/dist/knowledge/unified-search.js +373 -0
  28. package/dist/models/catalog-drift.d.ts +2 -0
  29. package/dist/models/catalog-drift.js +1 -1
  30. package/dist/models/catalog-payload-identity.d.ts +2 -2
  31. package/dist/models/catalog-payload-identity.js +10 -5
  32. package/dist/providers/active-install-message.js +1 -1
  33. package/dist/providers/catalog-model-config.d.ts +28 -0
  34. package/dist/providers/ds4/build-provider.js +35 -13
  35. package/dist/providers/ds4/residency.d.ts +77 -0
  36. package/dist/providers/ds4/residency.js +87 -6
  37. package/dist/providers/image/sd-cpp.js +19 -11
  38. package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
  39. package/dist/providers/llama-cpp/backend-health.js +6 -3
  40. package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
  41. package/dist/providers/llama-cpp/build-provider.js +168 -29
  42. package/dist/providers/llama-cpp/devices.js +4 -1
  43. package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
  44. package/dist/providers/llama-cpp/engine-flags.js +13 -8
  45. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
  46. package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
  47. package/dist/providers/llama-cpp/models.d.ts +10 -0
  48. package/dist/providers/llama-cpp/models.js +64 -10
  49. package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
  50. package/dist/providers/llama-cpp/offload-planner.js +1 -1
  51. package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
  52. package/dist/providers/llama-cpp/spec-draft.js +113 -24
  53. package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
  54. package/dist/providers/mlx/async-file-handoff.js +32 -4
  55. package/dist/providers/mlx/provider.js +28 -16
  56. package/dist/providers/mlx/python/spec_decode.py +23 -2
  57. package/dist/providers/mlx/python/tool_args_json.py +59 -21
  58. package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
  59. package/dist/providers/mlx/runtime-diagnostics.js +18 -1
  60. package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
  61. package/dist/providers/mlx/tool-call-protocol.js +4 -3
  62. package/dist/providers/mlx/venv.d.ts +19 -1
  63. package/dist/providers/mlx/venv.js +21 -2
  64. package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
  65. package/dist/providers/native/device-capacity-ledger.js +35 -20
  66. package/dist/providers/native/device-capacity.d.ts +4 -0
  67. package/dist/providers/native/device-capacity.js +5 -0
  68. package/dist/providers/native/local-engine-runtime.d.ts +16 -1
  69. package/dist/providers/native/local-engine-runtime.js +142 -39
  70. package/dist/providers/native/process-identity.d.ts +38 -0
  71. package/dist/providers/native/process-identity.js +86 -0
  72. package/dist/providers/native/supervisor.d.ts +8 -1
  73. package/dist/providers/native/supervisor.js +16 -7
  74. package/dist/providers/types.d.ts +4 -0
  75. package/dist/remotes/types.d.ts +1 -1
  76. package/dist/runtime-lock.d.ts +3 -27
  77. package/dist/runtime-lock.js +7 -80
  78. package/package.json +4 -4
@@ -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
  /**
@@ -49,7 +49,7 @@ function gpuResidentThreads(hostCpuCount) {
49
49
  : GPU_RESIDENT_THREADS;
50
50
  return Math.max(1, Math.min(GPU_RESIDENT_THREADS, available));
51
51
  }
52
- const GPU_BACKENDS = new Set(['cuda', 'vulkan', 'metal']);
52
+ const GPU_BACKENDS = new Set(['cuda', 'rocm', 'vulkan', 'metal']);
53
53
  /**
54
54
  * `llamaCppExtraArgs` keys that either choose a thread pool or may move
55
55
  * weights to the CPU; any of them makes the GPU-resident default stand down.
@@ -223,7 +223,7 @@ export function buildLlamaCppEngineArgs(input) {
223
223
  // MTP is ON by default for a GGUF that carries the head, having cleared the
224
224
  // A/B gate this comment used to wait for (reports/llama-mtp-eval-20260829.md):
225
225
  // +12.7% / +4.1% / +15.6% / +19.3% decode at 5.5k / 21.7k / 54k / 87k
226
- // context, never negative, byte-identical greedy output, +1.35 GB resident.
226
+ // context, never negative, +1.35 GB resident.
227
227
  // The gain GROWS with context — long-context decode is bound by reading the
228
228
  // KV cache, and verifying K drafted tokens costs one pass over it instead of
229
229
  // K — which is the regime agent turns actually run in.
@@ -232,6 +232,8 @@ export function buildLlamaCppEngineArgs(input) {
232
232
  // FATALLY when draft-mtp is selected for a model with no MTP tensors, so
233
233
  // the default may only ever apply where the metadata confirms the head.
234
234
  // Explicit config and manifest `spec.type` still win, including 'none'.
235
+ // `perModel` arrives with its `spec.byBackend` already applied by
236
+ // `resolveSpecDraft`, which also decided whether a drafter is in play.
235
237
  const specType = config.llamaCppSpecType ?? perModel?.spec?.type ?? (ggufHasMtp ? 'draft-mtp' : undefined);
236
238
  const safeSpecType = specType === 'draft-mtp' && !ggufHasMtp ? undefined : specType;
237
239
  if (safeSpecType && safeSpecType !== 'none') {
@@ -244,12 +246,15 @@ export function buildLlamaCppEngineArgs(input) {
244
246
  const nMax = config.llamaCppSpecDraftNMax ?? perModel?.spec?.nMax;
245
247
  if (typeof nMax === 'number')
246
248
  args.push('--spec-draft-n-max', String(nMax));
247
- // v0.7.0 supports probabilistic selection only for MTP/simple drafts
248
- // (DFlash/LiLiCorr ignore it).
249
- // Leave the upstream greedy default untouched unless explicitly set.
250
- if ((safeSpecType === 'draft-mtp' || safeSpecType === 'draft-simple') &&
251
- config.llamaCppSpecDraftSampling) {
252
- args.push('--spec-draft-sampling', config.llamaCppSpecDraftSampling);
249
+ // MTP, simple and DFlash2 honor probabilistic selection; a DFlash 1 or
250
+ // LiLiCorr drafter takes the flag and ignores it. Leave the upstream greedy
251
+ // default untouched unless someone chose otherwise.
252
+ const draftSampling = config.llamaCppSpecDraftSampling ?? perModel?.spec?.sampling;
253
+ if ((safeSpecType === 'draft-mtp' ||
254
+ safeSpecType === 'draft-simple' ||
255
+ safeSpecType === 'draft-dflash') &&
256
+ draftSampling) {
257
+ args.push('--spec-draft-sampling', draftSampling);
253
258
  }
254
259
  }
255
260
  // ── Server-side output parsing (`--reasoning-format`) ─────────────
@@ -7,8 +7,8 @@
7
7
  * word. A CUDA backend that loads but cannot initialize prints one error line
8
8
  * and registers no devices. Either way the engine starts, answers, and runs
9
9
  * every token on the CPU — at a fraction of the speed the person's hardware
10
- * and Settings promise — while gezel reports the CUDA or Vulkan build it
11
- * launched.
10
+ * and Settings promise — while gezel reports the CUDA, ROCm or Vulkan build
11
+ * it launched.
12
12
  *
13
13
  * llama.cpp names every GPU it will use while loading a model
14
14
  * (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
@@ -16,14 +16,14 @@
16
16
  * without its GPU. The explicit lines say why when there is a why to say.
17
17
  */
18
18
  export type GpuUnusedReason =
19
- /** The CUDA runtime loaded and refused to start: `failed to initialize CUDA`. */
19
+ /** The CUDA or ROCm runtime loaded and refused to start: `failed to initialize CUDA`. */
20
20
  'init-failed'
21
21
  /** The backend loaded and found no device it could use. */
22
22
  | 'no-devices'
23
23
  /** No sign of the GPU backend at all: it failed to load, silently. */
24
24
  | 'not-loaded';
25
25
  export interface GpuUnused {
26
- backend: 'cuda' | 'vulkan';
26
+ backend: 'cuda' | 'rocm' | 'vulkan';
27
27
  reason: GpuUnusedReason;
28
28
  /** The engine's own words, when it gave any. */
29
29
  detail?: string;
@@ -7,8 +7,8 @@
7
7
  * word. A CUDA backend that loads but cannot initialize prints one error line
8
8
  * and registers no devices. Either way the engine starts, answers, and runs
9
9
  * every token on the CPU — at a fraction of the speed the person's hardware
10
- * and Settings promise — while gezel reports the CUDA or Vulkan build it
11
- * launched.
10
+ * and Settings promise — while gezel reports the CUDA, ROCm or Vulkan build
11
+ * it launched.
12
12
  *
13
13
  * llama.cpp names every GPU it will use while loading a model
14
14
  * (`using device CUDA0 (NVIDIA GB10) …`), integrated GPUs included. A GPU
@@ -16,7 +16,8 @@
16
16
  * without its GPU. The explicit lines say why when there is a why to say.
17
17
  */
18
18
  const USING_GPU = /\busing device (?:CUDA|Vulkan|ROCm|SYCL|MTL)\d+\b/;
19
- const CUDA_INIT_FAILED = /ggml_cuda_init: failed to initialize CUDA:\s*(.+?)\s*$/;
19
+ /** ggml's ROCm backend is its CUDA code under HIP, and names itself `ROCm` here. */
20
+ const CUDA_INIT_FAILED = /ggml_cuda_init: failed to initialize (?:CUDA|ROCm):\s*(.+?)\s*$/;
20
21
  const VULKAN_NO_DEVICES = /ggml_vulkan: No devices found/;
21
22
  /**
22
23
  * Lines llama.cpp prints at the same verbosity as `using device`, while
@@ -46,9 +47,10 @@ export class GpuStartupWatch {
46
47
  /** At readiness: what kept the GPU out, or null when it is in use or this is no GPU build. */
47
48
  verdict() {
48
49
  const backend = this.backend;
49
- if ((backend !== 'cuda' && backend !== 'vulkan') || this.usingGpu)
50
+ if ((backend !== 'cuda' && backend !== 'rocm' && backend !== 'vulkan') || this.usingGpu) {
50
51
  return null;
51
- if (backend === 'cuda' && this.initFailure) {
52
+ }
53
+ if ((backend === 'cuda' || backend === 'rocm') && this.initFailure) {
52
54
  return { backend, reason: 'init-failed', detail: this.initFailure };
53
55
  }
54
56
  if (backend === 'vulkan' && this.noDevices)
@@ -73,6 +73,8 @@ export interface InstalledLlamaCppModel {
73
73
  * `--spec-draft-model <path>` for the selected draft algorithm.
74
74
  */
75
75
  draftModelPath?: string;
76
+ /** On-disk size of {@link draftModelPath}, for the memory plan. */
77
+ draftModelSizeBytes?: number;
76
78
  /** From the GGUF metadata — useful for the supervisor's `-c` flag (Phase 3). */
77
79
  contextWindow?: number;
78
80
  /** Quantization tag (Q4_K_M / Q8_0 / …) lifted from the catalog source block. */
@@ -389,6 +391,14 @@ export declare class LlamaCppModelManager {
389
391
  * a recorded digest plus an unchanged stat is identity.
390
392
  */
391
393
  private planReusableFiles;
394
+ /**
395
+ * A drafter is trained against a model, not a quant, so every quant of that
396
+ * model pins the same file. Link a copy another installed quant verified
397
+ * rather than downloading a gigabyte again. A hard link keeps each model's
398
+ * directory complete on its own: deleting one quant leaves the other's
399
+ * drafter in place. A volume that cannot link just downloads.
400
+ */
401
+ private adoptSiblingDraftModels;
392
402
  /** Warn once per model directory per process, then stay quiet. */
393
403
  private warnSkip;
394
404
  private loadInstalled;