@bendyline/gezel-inference 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/manifest.json +46 -3
- package/dist/catalog/chat-models/ge/gemma4-12b-q4/versions/1.2.1/manifest.json +141 -0
- package/dist/catalog/chat-models/ge/gemma4-e4b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/or/ornith1.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/or/ornith1.5-9b-q8/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.5-9b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-27b-q4/manifest.json +5 -0
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/manifest.json +24 -7
- package/dist/catalog/chat-models/qw/qwen3.8-flash-next-q2/versions/1.0.3/manifest.json +47 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +5 -4
- package/dist/engines/electron-native-reuse.js +1 -1
- package/dist/engines/native-file-manifest.json +12344 -698
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +40 -36
- package/dist/engines/resolver.d.ts +18 -17
- package/dist/engines/resolver.js +36 -1
- package/dist/http/model-inventory-cache.d.ts +1 -1
- package/dist/http/routes/engines.js +3 -3
- package/dist/http/routes/v1-images.d.ts +2 -1
- package/dist/http/routes/v1-images.js +36 -9
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- package/dist/models/catalog-drift.d.ts +2 -0
- package/dist/models/catalog-drift.js +1 -1
- package/dist/models/catalog-payload-identity.d.ts +2 -2
- package/dist/models/catalog-payload-identity.js +10 -5
- package/dist/providers/active-install-message.js +1 -1
- package/dist/providers/catalog-model-config.d.ts +28 -0
- package/dist/providers/ds4/build-provider.js +35 -13
- package/dist/providers/ds4/residency.d.ts +77 -0
- package/dist/providers/ds4/residency.js +87 -6
- package/dist/providers/image/sd-cpp.js +19 -11
- package/dist/providers/llama-cpp/backend-health.d.ts +3 -2
- package/dist/providers/llama-cpp/backend-health.js +6 -3
- package/dist/providers/llama-cpp/backend-quarantine.js +1 -1
- package/dist/providers/llama-cpp/build-provider.js +168 -29
- package/dist/providers/llama-cpp/devices.js +4 -1
- package/dist/providers/llama-cpp/engine-flags.d.ts +8 -1
- package/dist/providers/llama-cpp/engine-flags.js +13 -8
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +4 -4
- package/dist/providers/llama-cpp/gpu-startup-watch.js +7 -5
- package/dist/providers/llama-cpp/models.d.ts +10 -0
- package/dist/providers/llama-cpp/models.js +64 -10
- package/dist/providers/llama-cpp/offload-planner.d.ts +6 -0
- package/dist/providers/llama-cpp/offload-planner.js +1 -1
- package/dist/providers/llama-cpp/spec-draft.d.ts +45 -23
- package/dist/providers/llama-cpp/spec-draft.js +113 -24
- package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +28 -16
- package/dist/providers/mlx/python/spec_decode.py +23 -2
- package/dist/providers/mlx/python/tool_args_json.py +59 -21
- package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/mlx/venv.d.ts +19 -1
- package/dist/providers/mlx/venv.js +21 -2
- package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
- package/dist/providers/native/device-capacity-ledger.js +35 -20
- package/dist/providers/native/device-capacity.d.ts +4 -0
- package/dist/providers/native/device-capacity.js +5 -0
- package/dist/providers/native/local-engine-runtime.d.ts +16 -1
- package/dist/providers/native/local-engine-runtime.js +142 -39
- package/dist/providers/native/process-identity.d.ts +38 -0
- package/dist/providers/native/process-identity.js +86 -0
- package/dist/providers/native/supervisor.d.ts +8 -1
- package/dist/providers/native/supervisor.js +16 -7
- package/dist/providers/types.d.ts +4 -0
- package/dist/remotes/types.d.ts +1 -1
- package/dist/runtime-lock.d.ts +3 -27
- package/dist/runtime-lock.js +7 -80
- package/package.json +4 -4
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
681
|
-
|
|
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 ${
|
|
1104
|
-
?
|
|
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:
|
|
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)
|
|
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,
|
|
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
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|