@bendyline/gezel-inference 1.0.6 → 1.0.8
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/cache/controller.d.ts +2 -0
- package/dist/cache/controller.js +38 -5
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-loras/index.json +1 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/raw-index.json +1 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
- package/dist/catalog/image-models/index.json +1 -1
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
- package/dist/catalog/image-models/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/index.json +1 -1
- package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engine-components.js +2 -2
- package/dist/engines/electron-native-reuse.d.ts +7 -1
- package/dist/engines/electron-native-reuse.js +12 -4
- package/dist/engines/native-file-manifest.json +546 -406
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.js +1 -0
- package/dist/http/auth-context.d.ts +12 -0
- package/dist/http/routes/image-ensure-routes.d.ts +4 -0
- package/dist/http/routes/image-ensure-routes.js +102 -0
- package/dist/http/routes/v1-image-styles.d.ts +5 -0
- package/dist/http/routes/v1-image-styles.js +91 -0
- package/dist/http/routes/v1-images.d.ts +3 -1
- package/dist/http/routes/v1-images.js +48 -99
- package/dist/inference-service.js +2 -1
- 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/knowledge/worker-host.js +2 -0
- package/dist/machine-engine/inspect-runtime.js +12 -3
- package/dist/memory/embed-core.js +3 -0
- package/dist/memory/embeddings.js +2 -0
- package/dist/models/image-fit.js +2 -2
- package/dist/models/weight-facts.d.ts +5 -0
- package/dist/models/weight-facts.js +5 -2
- package/dist/providers/ds4/build-provider.js +1 -1
- package/dist/providers/ds4/residency.js +3 -3
- package/dist/providers/image/img2img-support.d.ts +36 -1
- package/dist/providers/image/img2img-support.js +30 -1
- package/dist/providers/image/local-engine.d.ts +1 -0
- package/dist/providers/image/local-engine.js +8 -2
- package/dist/providers/image/local-factory.d.ts +10 -1
- package/dist/providers/image/local-factory.js +33 -6
- package/dist/providers/image/mock.d.ts +8 -1
- package/dist/providers/image/mock.js +46 -1
- package/dist/providers/image/pull-registry.d.ts +25 -3
- package/dist/providers/image/pull-registry.js +96 -34
- package/dist/providers/image/sd-cpp.d.ts +63 -4
- package/dist/providers/image/sd-cpp.js +368 -38
- package/dist/providers/image/types.d.ts +76 -4
- package/dist/providers/image/types.js +8 -0
- package/dist/providers/llama-cpp/build-provider.js +3 -2
- package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
- package/dist/providers/llama-cpp/cache-adapter.js +18 -4
- package/dist/providers/llama-cpp/devices.js +2 -2
- package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
- package/dist/providers/llama-cpp/engine-flags.js +5 -2
- 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 +41 -20
- package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
- package/dist/providers/mlx/python/spec_decode.py +44 -5
- 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/native/capacity-broker.d.ts +1 -1
- package/dist/providers/native/capacity-broker.js +8 -8
- package/dist/providers/native/capacity-budget.js +5 -4
- package/dist/providers/native/device-capacity-ledger.d.ts +5 -1
- package/dist/providers/native/device-capacity-ledger.js +60 -14
- package/dist/providers/native/device-capacity.js +4 -3
- package/dist/providers/native/local-engine-runtime.js +4 -4
- package/dist/providers/native/process-identity.d.ts +40 -0
- package/dist/providers/native/process-identity.js +156 -0
- package/dist/relevance/relevance-core.js +2 -0
- package/dist/relevance/relevance-model.js +2 -0
- package/dist/relevance/settings.js +2 -2
- package/dist/runtime-lock.d.ts +19 -5
- package/dist/runtime-lock.js +116 -26
- package/dist/system/host-memory.d.ts +46 -0
- package/dist/system/host-memory.js +89 -0
- package/dist/system/memory.js +5 -4
- package/dist/transformers-cache.js +2 -0
- package/dist/utils/download-common.d.ts +1 -1
- package/package.json +8 -8
|
@@ -38,6 +38,12 @@ export declare function buildPreFirstByteAbortMessage(lastPrefill: {
|
|
|
38
38
|
} | null): string;
|
|
39
39
|
/** Build the user-facing message for an engine stream that died mid-turn. */
|
|
40
40
|
export declare function buildMidStreamDropMessage(charsReceived: number, plannedStop: boolean, engineStillRunning?: boolean): string;
|
|
41
|
+
/**
|
|
42
|
+
* Prompt characters one attached image stands for. The sidecar bounds images
|
|
43
|
+
* to 1024px, so their patch tokens are bounded too, and base64 bytes are never
|
|
44
|
+
* read as language tokens.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MLX_IMAGE_RESERVED_CHARS = 8192;
|
|
41
47
|
/** Estimate request tokens from message bodies and serialized tool schemas. */
|
|
42
48
|
export declare function estimatePromptTokens(messages: unknown, tools: unknown): number;
|
|
43
49
|
export declare const PRE_FIRST_BYTE_BASE_MS = 300000;
|
|
@@ -146,11 +146,28 @@ export function buildMidStreamDropMessage(charsReceived, plannedStop, engineStil
|
|
|
146
146
|
}
|
|
147
147
|
return `[Mac AI] the on-device engine dropped the connection mid-turn (${got}). This usually means the mlx server crashed, ran out of memory, or was restarted while the turn was streaming — this turn's work was lost. Retry the turn; if it keeps happening, restart the engine in Settings → On-device.`;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Prompt characters one attached image stands for. The sidecar bounds images
|
|
151
|
+
* to 1024px, so their patch tokens are bounded too, and base64 bytes are never
|
|
152
|
+
* read as language tokens.
|
|
153
|
+
*/
|
|
154
|
+
export const MLX_IMAGE_RESERVED_CHARS = 8192;
|
|
149
155
|
/** Estimate request tokens from message bodies and serialized tool schemas. */
|
|
150
156
|
export function estimatePromptTokens(messages, tools) {
|
|
151
157
|
let chars = 0;
|
|
152
158
|
try {
|
|
153
|
-
|
|
159
|
+
// Counted as base64, one generated picture read as ~1.1M tokens: an
|
|
160
|
+
// image gezel's six renders and reads put the estimate past 6M while the
|
|
161
|
+
// engine prefilled 14K (qwen3.8-27b, 2026-11-04).
|
|
162
|
+
let images = 0;
|
|
163
|
+
const text = JSON.stringify(messages, (key, value) => {
|
|
164
|
+
if (key === 'images' && Array.isArray(value)) {
|
|
165
|
+
images += value.length;
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
});
|
|
170
|
+
chars += (text?.length ?? 0) + images * MLX_IMAGE_RESERVED_CHARS;
|
|
154
171
|
}
|
|
155
172
|
catch {
|
|
156
173
|
/* circular or unstringifiable */
|
|
@@ -39,9 +39,10 @@ export declare function applyCallableToolGrammar(body: Record<string, unknown>,
|
|
|
39
39
|
*
|
|
40
40
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
41
41
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
42
|
-
* every declared required key is emitted before
|
|
43
|
-
* missing key
|
|
44
|
-
*
|
|
42
|
+
* every declared required key is emitted before a constrained call can close.
|
|
43
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
44
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
45
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
45
46
|
*/
|
|
46
47
|
export declare function missingTopLevelRequiredToolArgs(schema: Record<string, unknown> | undefined, args: Record<string, unknown>): string[];
|
|
47
48
|
export declare function hermesRequiredArgGrammarRequested(body: Record<string, unknown>): boolean;
|
|
@@ -59,9 +59,10 @@ export function applyCallableToolGrammar(body, bridges) {
|
|
|
59
59
|
*
|
|
60
60
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
61
61
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
62
|
-
* every declared required key is emitted before
|
|
63
|
-
* missing key
|
|
64
|
-
*
|
|
62
|
+
* every declared required key is emitted before a constrained call can close.
|
|
63
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
64
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
65
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
65
66
|
*/
|
|
66
67
|
export function missingTopLevelRequiredToolArgs(schema, args) {
|
|
67
68
|
const required = schema?.required;
|
|
@@ -31,7 +31,7 @@ export interface CapacityBrokerOptions {
|
|
|
31
31
|
* tests and for users who want to opt out of the budget entirely.
|
|
32
32
|
*/
|
|
33
33
|
budgetBytes?: number;
|
|
34
|
-
/** Test seam — defaults to {@link
|
|
34
|
+
/** Test seam — defaults to {@link totalMemoryBytes}. */
|
|
35
35
|
systemRamBytes?: () => number;
|
|
36
36
|
/**
|
|
37
37
|
* Whether this host shares one memory pool between CPU and GPU, which
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
21
|
-
import { freemem, totalmem } from 'node:os';
|
|
22
21
|
import { CONSTRAINED_LOCAL_CONTEXT_FLOOR_TOKENS, LLAMA_CPP_COMPUTE_HEADROOM, LOCAL_CONTEXT_FLOOR_TOKENS, MLX_COMPUTE_HEADROOM, createLogger, estimateLlamaCppResidentBytes, estimateMlxResidentBytes, localContextFloorTokens, } from '@bendyline/gezel';
|
|
22
|
+
import { capToContainer, freeMemoryBytes, totalMemoryBytes } from '../../system/host-memory.js';
|
|
23
23
|
import { autoAllowRamSpillover, computeCapacityBudget, fastMemoryBudgetBytes, getDetectedGpuVramBytes, } from './capacity-budget.js';
|
|
24
24
|
export { autoAllowRamSpillover, autoDetectBudgetBytes, computeCapacityBudget, getDetectedGpuVramBytes, pressureIdleGraceMs, setDetectedGpuVramBytes, } from './capacity-budget.js';
|
|
25
25
|
export { fastMemoryBudgetBytes };
|
|
@@ -156,7 +156,7 @@ export class CapacityBroker {
|
|
|
156
156
|
explicitAllowRamSpillover;
|
|
157
157
|
reservations = new Map();
|
|
158
158
|
constructor(opts = {}) {
|
|
159
|
-
this.systemRamBytes = (opts.systemRamBytes ??
|
|
159
|
+
this.systemRamBytes = (opts.systemRamBytes ?? totalMemoryBytes)();
|
|
160
160
|
this.unifiedMemory = opts.unifiedMemory;
|
|
161
161
|
this.gpuVramBytes = opts.gpuVramBytes;
|
|
162
162
|
this.reservedGpuBytes = opts.reservedGpuBytes;
|
|
@@ -467,24 +467,24 @@ export function availableSystemRamBytes() {
|
|
|
467
467
|
// vm_stat categories overlap (file-backed pages span active+inactive),
|
|
468
468
|
// so the raw sum can exceed physical RAM; cap at 90% of totalmem — the
|
|
469
469
|
// consumers already hold back their own OS reserves on top.
|
|
470
|
-
const cap =
|
|
470
|
+
const cap = totalMemoryBytes() * 0.9;
|
|
471
471
|
try {
|
|
472
472
|
if (process.platform === 'darwin') {
|
|
473
473
|
const out = execFileSync('/usr/bin/vm_stat', { encoding: 'utf8', timeout: 3_000 });
|
|
474
474
|
const parsed = parseVmStatAvailableBytes(out);
|
|
475
475
|
if (parsed !== null)
|
|
476
|
-
return Math.max(Math.min(parsed, cap),
|
|
476
|
+
return capToContainer(Math.max(Math.min(parsed, cap), freeMemoryBytes()));
|
|
477
477
|
}
|
|
478
478
|
else if (process.platform === 'linux') {
|
|
479
479
|
const parsed = parseMeminfoAvailableBytes(readFileSync('/proc/meminfo', 'utf8'));
|
|
480
480
|
if (parsed !== null)
|
|
481
|
-
return Math.max(Math.min(parsed, cap),
|
|
481
|
+
return capToContainer(Math.max(Math.min(parsed, cap), freeMemoryBytes()));
|
|
482
482
|
}
|
|
483
483
|
}
|
|
484
484
|
catch {
|
|
485
485
|
// Probe failure falls through to the conservative number.
|
|
486
486
|
}
|
|
487
|
-
return
|
|
487
|
+
return freeMemoryBytes();
|
|
488
488
|
}
|
|
489
489
|
/** Pure vm_stat parser (exported for tests). Returns null when the output
|
|
490
490
|
* doesn't look like vm_stat. */
|
|
@@ -664,7 +664,7 @@ export function llamaCppSlotCeiling(opts) {
|
|
|
664
664
|
* going to free 5 GiB, because nothing was holding it but the estimate.
|
|
665
665
|
* Planning and admission must read one number.
|
|
666
666
|
*/
|
|
667
|
-
export function liveRamOsReserveBytes(totalRamBytes =
|
|
667
|
+
export function liveRamOsReserveBytes(totalRamBytes = totalMemoryBytes()) {
|
|
668
668
|
return Math.min(4 * GIB, Math.max(2 * GIB, totalRamBytes * 0.1));
|
|
669
669
|
}
|
|
670
670
|
/**
|
|
@@ -769,7 +769,7 @@ export function minViableLocalContextTokens(opts) {
|
|
|
769
769
|
const override = envMinContextTokens();
|
|
770
770
|
if (override !== null)
|
|
771
771
|
return override;
|
|
772
|
-
const systemRamBytes = opts?.systemRamBytes ??
|
|
772
|
+
const systemRamBytes = opts?.systemRamBytes ?? totalMemoryBytes();
|
|
773
773
|
const budget = opts?.budget ?? computeCapacityBudget({ systemRamBytes });
|
|
774
774
|
return localContextFloorTokens({
|
|
775
775
|
totalRamBytes: systemRamBytes,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { arch, platform
|
|
1
|
+
import { arch, platform } from 'node:os';
|
|
2
|
+
import { totalMemoryBytes } from '../../system/host-memory.js';
|
|
2
3
|
const GIB = 1024 ** 3;
|
|
3
4
|
/**
|
|
4
5
|
* Ceiling on the auto-derived SYSTEM-RAM share, however much RAM the host
|
|
@@ -152,7 +153,7 @@ export function isUnifiedMemoryHost() {
|
|
|
152
153
|
* `GEZEL_CAPACITY_BUDGET_GB` overrides the total, including the setting.
|
|
153
154
|
*/
|
|
154
155
|
export function computeCapacityBudget(input = {}) {
|
|
155
|
-
const systemRamBytes = input.systemRamBytes ??
|
|
156
|
+
const systemRamBytes = input.systemRamBytes ?? totalMemoryBytes();
|
|
156
157
|
// Reading the ambient probe is for callers that know nothing about the
|
|
157
158
|
// host. One that passes `unifiedMemory` has already described it.
|
|
158
159
|
const measuredVram = input.gpuVramBytes !== undefined
|
|
@@ -269,7 +270,7 @@ function envOverrideBytes() {
|
|
|
269
270
|
* (Settings → the engine pages) is the dial for owners who want that traded
|
|
270
271
|
* either way.
|
|
271
272
|
*/
|
|
272
|
-
export function autoDetectBudgetBytes(systemRamBytes =
|
|
273
|
+
export function autoDetectBudgetBytes(systemRamBytes = totalMemoryBytes(), opts = {}) {
|
|
273
274
|
return computeCapacityBudget({ systemRamBytes, ...opts }).budgetBytes;
|
|
274
275
|
}
|
|
275
276
|
/**
|
|
@@ -277,7 +278,7 @@ export function autoDetectBudgetBytes(systemRamBytes = totalmem(), opts = {}) {
|
|
|
277
278
|
* the whole budget otherwise. KV slots, prompt caches, and anything else
|
|
278
279
|
* that must live where the compute happens size against this.
|
|
279
280
|
*/
|
|
280
|
-
export function fastMemoryBudgetBytes(systemRamBytes =
|
|
281
|
+
export function fastMemoryBudgetBytes(systemRamBytes = totalMemoryBytes(), opts = {}) {
|
|
281
282
|
return computeCapacityBudget({ systemRamBytes, ...opts }).fastBytes;
|
|
282
283
|
}
|
|
283
284
|
/** Fast pool at or below which a host is treated as accelerator-constrained. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { NativeCapacityCommand, NativeCapacityReply } from '@bendyline/gezel';
|
|
2
|
+
import { ProcessIdentityCache, processAlive, type readProcessIdentities } from './process-identity.js';
|
|
3
|
+
export { processAlive };
|
|
2
4
|
export interface DeviceCapacitySample {
|
|
3
5
|
budgetBytes: number;
|
|
4
6
|
gpuBudgetBytes: number;
|
|
@@ -13,7 +15,6 @@ export interface DeviceCapacitySample {
|
|
|
13
15
|
availableRamBytes?: number;
|
|
14
16
|
serializeLoads: boolean;
|
|
15
17
|
}
|
|
16
|
-
export declare function processAlive(pid: number): boolean;
|
|
17
18
|
/**
|
|
18
19
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
19
20
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -21,10 +22,13 @@ export declare function processAlive(pid: number): boolean;
|
|
|
21
22
|
*/
|
|
22
23
|
export declare class DeviceCapacityLedger {
|
|
23
24
|
private readonly options;
|
|
25
|
+
private readonly identityCache;
|
|
24
26
|
constructor(options: {
|
|
25
27
|
directory: string;
|
|
26
28
|
sample: () => Promise<DeviceCapacitySample>;
|
|
27
29
|
alive?: (pid: number) => boolean;
|
|
30
|
+
identities?: typeof readProcessIdentities;
|
|
31
|
+
identityCache?: ProcessIdentityCache;
|
|
28
32
|
});
|
|
29
33
|
execute(command: NativeCapacityCommand): Promise<NativeCapacityReply>;
|
|
30
34
|
private transact;
|
|
@@ -2,6 +2,8 @@ import { chmod, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CapacityDeniedError } from './capacity-broker.js';
|
|
5
|
+
import { ProcessIdentityCache, processAlive, sharedProcessIdentities, } from './process-identity.js';
|
|
6
|
+
export { processAlive };
|
|
5
7
|
// Keep the newer builtin behind a runtime require. The current tsup/esbuild
|
|
6
8
|
// target rewrites a static `node:sqlite` import to the nonexistent npm package
|
|
7
9
|
// `sqlite`, making the daemon bundle fail before it can write runtime state.
|
|
@@ -10,15 +12,6 @@ const { DatabaseSync } = nodeRequire('node:sqlite');
|
|
|
10
12
|
function gb(bytes) {
|
|
11
13
|
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
12
14
|
}
|
|
13
|
-
export function processAlive(pid) {
|
|
14
|
-
try {
|
|
15
|
-
process.kill(pid, 0);
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
return error.code !== 'ESRCH';
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
15
|
/**
|
|
23
16
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
24
17
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -26,8 +19,18 @@ export function processAlive(pid) {
|
|
|
26
19
|
*/
|
|
27
20
|
export class DeviceCapacityLedger {
|
|
28
21
|
options;
|
|
22
|
+
identityCache;
|
|
29
23
|
constructor(options) {
|
|
30
24
|
this.options = options;
|
|
25
|
+
// An injected reader still goes through a cache, so tests see the reads
|
|
26
|
+
// production makes. An injected `alive` alone checks liveness only.
|
|
27
|
+
this.identityCache =
|
|
28
|
+
options.identityCache ??
|
|
29
|
+
(options.identities
|
|
30
|
+
? new ProcessIdentityCache({ read: options.identities, alive: options.alive })
|
|
31
|
+
: options.alive
|
|
32
|
+
? undefined
|
|
33
|
+
: sharedProcessIdentities);
|
|
31
34
|
}
|
|
32
35
|
async execute(command) {
|
|
33
36
|
const sample = await this.options.sample();
|
|
@@ -38,9 +41,39 @@ export class DeviceCapacityLedger {
|
|
|
38
41
|
await chmod(path, 0o600);
|
|
39
42
|
db.exec('PRAGMA busy_timeout = 1000');
|
|
40
43
|
db.exec('CREATE TABLE IF NOT EXISTS claims (id TEXT PRIMARY KEY, claim TEXT NOT NULL)');
|
|
44
|
+
const snapshot = db
|
|
45
|
+
.prepare('SELECT claim FROM claims')
|
|
46
|
+
.all()
|
|
47
|
+
.map((row) => JSON.parse(String(row.claim)));
|
|
48
|
+
const record = [];
|
|
49
|
+
if (command.action === 'acquire' && !snapshot.some((claim) => claim.id === command.id))
|
|
50
|
+
record.push(command.ownerPid);
|
|
51
|
+
if (command.action === 'bind')
|
|
52
|
+
record.push(command.childPid);
|
|
53
|
+
// Stamps only decide which reservations are stale, and a stale one costs
|
|
54
|
+
// someone only while a request waits for memory. With no request
|
|
55
|
+
// waiting, liveness alone prunes, so the status check that runs before
|
|
56
|
+
// every engine request reads no stamps.
|
|
57
|
+
const confirm = new Map();
|
|
58
|
+
const stamp = (pid, identity) => {
|
|
59
|
+
if (pid !== undefined && identity !== undefined)
|
|
60
|
+
confirm.set(pid, [...(confirm.get(pid) ?? []), identity]);
|
|
61
|
+
};
|
|
62
|
+
if (command.action === 'acquire' || snapshot.some((claim) => claim.phase === 'waiting')) {
|
|
63
|
+
for (const claim of snapshot) {
|
|
64
|
+
stamp(claim.childPid, claim.childIdentity);
|
|
65
|
+
// An owner's stamp is only consulted while its claim is queued.
|
|
66
|
+
if (claim.phase === 'waiting')
|
|
67
|
+
stamp(claim.ownerPid, claim.ownerIdentity);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// OS probes may spawn a helper. Never hold SQLite's writer lock while awaiting one.
|
|
71
|
+
const identities = command.action !== 'release' && this.identityCache
|
|
72
|
+
? await this.identityCache.lookup(record, confirm).catch(() => new Map())
|
|
73
|
+
: new Map();
|
|
41
74
|
db.exec('BEGIN IMMEDIATE');
|
|
42
75
|
try {
|
|
43
|
-
const result = this.transact(db, command, sample);
|
|
76
|
+
const result = this.transact(db, command, sample, identities, new Map(snapshot.map((claim) => [claim.id, JSON.stringify(claim)])));
|
|
44
77
|
db.exec('COMMIT');
|
|
45
78
|
return result;
|
|
46
79
|
}
|
|
@@ -53,17 +86,27 @@ export class DeviceCapacityLedger {
|
|
|
53
86
|
db.close();
|
|
54
87
|
}
|
|
55
88
|
}
|
|
56
|
-
transact(db, command, sample) {
|
|
89
|
+
transact(db, command, sample, identities, observedClaims) {
|
|
57
90
|
const alive = this.options.alive ?? processAlive;
|
|
91
|
+
const sameProcess = (pid, identity) => {
|
|
92
|
+
if (!alive(pid))
|
|
93
|
+
return false;
|
|
94
|
+
const current = identities.get(pid);
|
|
95
|
+
return identity === undefined || current === undefined || current === identity;
|
|
96
|
+
};
|
|
58
97
|
const rows = db.prepare('SELECT claim FROM claims ORDER BY rowid').all();
|
|
59
98
|
const claims = rows.map((row) => JSON.parse(String(row.claim)));
|
|
60
99
|
const remove = db.prepare('DELETE FROM claims WHERE id = ?');
|
|
61
100
|
const save = db.prepare('UPDATE claims SET claim = ? WHERE id = ?');
|
|
62
101
|
const live = claims.filter((claim) => {
|
|
102
|
+
// Another coordinator can acquire/rebind while the OS probe is in flight.
|
|
103
|
+
// Its newer claim must not be compared with our earlier process snapshot.
|
|
104
|
+
if (observedClaims.get(claim.id) !== JSON.stringify(claim))
|
|
105
|
+
return true;
|
|
63
106
|
// An unbound grant with a dead owner could have spawned just before the
|
|
64
107
|
// owner died. Keep it conservatively; never guess that its bytes are free.
|
|
65
|
-
const stale = (claim.childPid !== undefined && !
|
|
66
|
-
(!
|
|
108
|
+
const stale = (claim.childPid !== undefined && !sameProcess(claim.childPid, claim.childIdentity)) ||
|
|
109
|
+
(!sameProcess(claim.ownerPid, claim.ownerIdentity) && claim.phase === 'waiting');
|
|
67
110
|
if (stale)
|
|
68
111
|
remove.run(claim.id);
|
|
69
112
|
return !stale;
|
|
@@ -80,6 +123,7 @@ export class DeviceCapacityLedger {
|
|
|
80
123
|
own = {
|
|
81
124
|
id: command.id,
|
|
82
125
|
ownerPid: command.ownerPid,
|
|
126
|
+
ownerIdentity: identities.get(command.ownerPid),
|
|
83
127
|
label: command.label,
|
|
84
128
|
bytes: command.bytes,
|
|
85
129
|
gpuBytes: command.gpuBytes,
|
|
@@ -95,8 +139,10 @@ export class DeviceCapacityLedger {
|
|
|
95
139
|
return { state: 'released', releaseRequested: false };
|
|
96
140
|
if (command.action === 'acquire' && command.priority === 'interactive')
|
|
97
141
|
own.priority = 'interactive';
|
|
98
|
-
if (command.action === 'bind' && own.phase !== 'waiting')
|
|
142
|
+
if (command.action === 'bind' && own.phase !== 'waiting') {
|
|
99
143
|
own.childPid = command.childPid;
|
|
144
|
+
own.childIdentity = identities.get(command.childPid);
|
|
145
|
+
}
|
|
100
146
|
if (command.action === 'ready' && own.phase === 'loading')
|
|
101
147
|
own.phase = 'ready';
|
|
102
148
|
const active = live.filter((c) => c.phase !== 'waiting');
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
-
import { homedir
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { DS4_FULL_RESIDENCY_HEADROOM_BYTES, GEZEL_VERSION, NativeCapacityCommandSchema, NativeCapacityReplySchema, awakeNow, createLogger, isUnstampedDevBuild, } from '@bendyline/gezel';
|
|
6
6
|
import { readSystemServiceRuntime, systemServiceHome } from '@bendyline/gezel-client/node';
|
|
7
7
|
import { ConfigStore } from '../../fs/config-store.js';
|
|
8
8
|
import { createPinnedFetch } from '../../remotes/pinned-fetch.js';
|
|
9
|
+
import { totalMemoryBytes } from '../../system/host-memory.js';
|
|
9
10
|
import { CapacityDeniedError, availableSystemRamBytes, liveRamOsReserveBytes, } from './capacity-broker.js';
|
|
10
11
|
import { DeviceCapacityLedger } from './device-capacity-ledger.js';
|
|
11
12
|
import { measuredCapacityBudget } from './measured-budget.js';
|
|
@@ -156,7 +157,7 @@ export function staticCapacityCeilings(budget, ramBytes, configuredBytes, platfo
|
|
|
156
157
|
}
|
|
157
158
|
export async function sampleDeviceCapacity(home) {
|
|
158
159
|
const budget = await measuredCapacityBudget();
|
|
159
|
-
const ram =
|
|
160
|
+
const ram = totalMemoryBytes();
|
|
160
161
|
const config = home ? await new ConfigStore(home).readConfig() : {};
|
|
161
162
|
const ceilings = staticCapacityCeilings(budget, ram, configuredEngineMemoryBytes(config));
|
|
162
163
|
// The ledger's RAM ceiling and the OS's currently reclaimable RAM answer
|
|
@@ -213,7 +214,7 @@ async function capacityExecutor(home) {
|
|
|
213
214
|
// broker that is merely unreachable is an outage, never permission to
|
|
214
215
|
// double-book the machine — so claim the authority before rethrowing.
|
|
215
216
|
observedMachineAuthorities.add(machineHome);
|
|
216
|
-
throw error;
|
|
217
|
+
throw new Error(`Local models can't start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
217
218
|
}
|
|
218
219
|
const skew = assessBrokerVersionSkew(identity.gezelVersion);
|
|
219
220
|
if (skew)
|
|
@@ -609,10 +609,10 @@ export class LocalEngineRuntime {
|
|
|
609
609
|
residentCtxTokens: ds4Block?.residentCtxTokens,
|
|
610
610
|
});
|
|
611
611
|
if (line) {
|
|
612
|
-
const {
|
|
612
|
+
const { totalMemoryBytes } = await import('../../system/host-memory.js');
|
|
613
613
|
bytes = ds4ProjectedResidentBytes(line, resolveDs4LaunchCtx({
|
|
614
614
|
configured: config.modelContextOverrides?.[`ds4:${modelId}`] ?? config.ds4NumCtx,
|
|
615
|
-
ramTieredCtx:
|
|
615
|
+
ramTieredCtx: totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072,
|
|
616
616
|
catalogMaxCtx: ds4Block?.maxLaunchCtx,
|
|
617
617
|
minViableContextTokens: minViableLocalContextTokens(),
|
|
618
618
|
}));
|
|
@@ -1046,8 +1046,8 @@ export class LocalEngineRuntime {
|
|
|
1046
1046
|
if (!installed && !hasExplicitSource && !(opts.allowUninstalled && ds4Source)) {
|
|
1047
1047
|
throw new ModelNotInstalledError(name, modelId);
|
|
1048
1048
|
}
|
|
1049
|
-
const {
|
|
1050
|
-
const ramTieredCtx =
|
|
1049
|
+
const { totalMemoryBytes } = await import('../../system/host-memory.js');
|
|
1050
|
+
const ramTieredCtx = totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072;
|
|
1051
1051
|
const ds4Override = config.modelContextOverrides?.[`ds4:${modelId}`];
|
|
1052
1052
|
const ds4Floor = ds4Override !== undefined ? Math.min(contextFloor, ds4Override) : contextFloor;
|
|
1053
1053
|
const effective = resolveDs4LaunchCtx({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare function processAlive(pid: number): boolean;
|
|
2
|
+
/** Birth stamps survive coordinator restarts and distinguish a reused PID. */
|
|
3
|
+
export declare function readProcessIdentities(pids: readonly number[]): Promise<ReadonlyMap<number, string>>;
|
|
4
|
+
/**
|
|
5
|
+
* How long a stamp read for a still-live PID is trusted. A PID can die and be
|
|
6
|
+
* reused between two liveness checks; this bounds how long such a dead
|
|
7
|
+
* engine's reservation outlives it. That is the safe direction: memory looks
|
|
8
|
+
* taken for a few seconds longer, never free twice.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VERIFIED_IDENTITY_TTL_MS = 10000;
|
|
11
|
+
export interface ProcessIdentityCacheOptions {
|
|
12
|
+
read?: (pids: readonly number[]) => Promise<ReadonlyMap<number, string>>;
|
|
13
|
+
alive?: (pid: number) => boolean;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
ttlMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
|
|
19
|
+
* ledger used to read every PID it holds on each command, which ran before
|
|
20
|
+
* every engine request. A stamp read for a PID that is still alive is reused
|
|
21
|
+
* for a short while, and the PIDs that do need reading go in one batch.
|
|
22
|
+
*
|
|
23
|
+
* A cached stamp may only confirm the stamp a reservation already recorded.
|
|
24
|
+
* A stamp about to be recorded, or one that disagrees, is read again: another
|
|
25
|
+
* coordinator may have recorded a newer process at that PID, and trusting an
|
|
26
|
+
* older stamp there would free a live engine's memory.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ProcessIdentityCache {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private readonly verified;
|
|
31
|
+
constructor(options?: ProcessIdentityCacheOptions);
|
|
32
|
+
/**
|
|
33
|
+
* `record`: PIDs whose stamp is about to be written onto a reservation.
|
|
34
|
+
* `confirm`: each PID's stamps as reservations recorded them.
|
|
35
|
+
* PIDs that are dead, or whose stamp cannot be read, are left out.
|
|
36
|
+
*/
|
|
37
|
+
lookup(record: Iterable<number>, confirm: ReadonlyMap<number, readonly string[]>): Promise<ReadonlyMap<number, string>>;
|
|
38
|
+
}
|
|
39
|
+
/** One cache per process: every ledger in it asks about the same OS processes. */
|
|
40
|
+
export declare const sharedProcessIdentities: ProcessIdentityCache;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const exec = promisify(execFile);
|
|
5
|
+
/**
|
|
6
|
+
* One PID the probe refuses blanks every row in its batch: macOS ps prints
|
|
7
|
+
* "process id too large" past XNU's PID_MAX, and Get-Process -Id binds Int32.
|
|
8
|
+
* A PID past the limit cannot be live, so it is left out like an absent one.
|
|
9
|
+
*/
|
|
10
|
+
const PROBE_PID_LIMIT = {
|
|
11
|
+
darwin: 99_999,
|
|
12
|
+
win32: 2_147_483_647,
|
|
13
|
+
};
|
|
14
|
+
export function processAlive(pid) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return error.code !== 'ESRCH';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Birth stamps survive coordinator restarts and distinguish a reused PID. */
|
|
24
|
+
export async function readProcessIdentities(pids) {
|
|
25
|
+
const limit = PROBE_PID_LIMIT[process.platform] ?? Number.MAX_SAFE_INTEGER;
|
|
26
|
+
const ids = [...new Set(pids)].filter((pid) => Number.isSafeInteger(pid) && pid > 0 && pid <= limit);
|
|
27
|
+
const identities = new Map();
|
|
28
|
+
if (!ids.length)
|
|
29
|
+
return identities;
|
|
30
|
+
try {
|
|
31
|
+
if (process.platform === 'linux') {
|
|
32
|
+
const boot = (await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim();
|
|
33
|
+
await Promise.all(ids.map(async (pid) => {
|
|
34
|
+
const value = await readFile(`/proc/${pid}/stat`, 'utf8').catch(() => null);
|
|
35
|
+
if (!value)
|
|
36
|
+
return;
|
|
37
|
+
// comm may contain spaces and parentheses; fields after its final ')' start at field 3.
|
|
38
|
+
const start = value
|
|
39
|
+
.slice(value.lastIndexOf(')') + 2)
|
|
40
|
+
.trim()
|
|
41
|
+
.split(/\s+/)[19];
|
|
42
|
+
if (start && /^\d+$/.test(start))
|
|
43
|
+
identities.set(pid, `${boot}:${start}`);
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const windows = process.platform === 'win32';
|
|
48
|
+
const { stdout } = await exec(windows ? 'powershell.exe' : 'ps', windows
|
|
49
|
+
? [
|
|
50
|
+
'-NoProfile',
|
|
51
|
+
'-NonInteractive',
|
|
52
|
+
'-Command',
|
|
53
|
+
`Get-Process -Id ${ids.join(',')} -ErrorAction SilentlyContinue | ForEach-Object { try { [string]$_.Id + ":" + $_.StartTime.ToUniversalTime().Ticks.ToString() } catch {} }`,
|
|
54
|
+
]
|
|
55
|
+
: ['-p', ids.join(','), '-o', 'pid=', '-o', 'lstart='], {
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
timeout: 3000,
|
|
58
|
+
maxBuffer: 256 * 1024,
|
|
59
|
+
env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' },
|
|
60
|
+
}).catch((error) => {
|
|
61
|
+
// ps (and Windows process lookup) may return live rows alongside a missing PID error.
|
|
62
|
+
if (typeof error.stdout === 'string')
|
|
63
|
+
return { stdout: error.stdout };
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
67
|
+
const match = windows
|
|
68
|
+
? /^\s*(\d+):(\d+)\s*$/.exec(line)
|
|
69
|
+
: /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
70
|
+
if (match && ids.includes(Number(match[1])))
|
|
71
|
+
identities.set(Number(match[1]), match[2]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Access denied or an unavailable OS probe is unknown, never proof of death.
|
|
77
|
+
}
|
|
78
|
+
return identities;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* How long a stamp read for a still-live PID is trusted. A PID can die and be
|
|
82
|
+
* reused between two liveness checks; this bounds how long such a dead
|
|
83
|
+
* engine's reservation outlives it. That is the safe direction: memory looks
|
|
84
|
+
* taken for a few seconds longer, never free twice.
|
|
85
|
+
*/
|
|
86
|
+
export const VERIFIED_IDENTITY_TTL_MS = 10_000;
|
|
87
|
+
/**
|
|
88
|
+
* Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
|
|
89
|
+
* ledger used to read every PID it holds on each command, which ran before
|
|
90
|
+
* every engine request. A stamp read for a PID that is still alive is reused
|
|
91
|
+
* for a short while, and the PIDs that do need reading go in one batch.
|
|
92
|
+
*
|
|
93
|
+
* A cached stamp may only confirm the stamp a reservation already recorded.
|
|
94
|
+
* A stamp about to be recorded, or one that disagrees, is read again: another
|
|
95
|
+
* coordinator may have recorded a newer process at that PID, and trusting an
|
|
96
|
+
* older stamp there would free a live engine's memory.
|
|
97
|
+
*/
|
|
98
|
+
export class ProcessIdentityCache {
|
|
99
|
+
options;
|
|
100
|
+
verified = new Map();
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `record`: PIDs whose stamp is about to be written onto a reservation.
|
|
106
|
+
* `confirm`: each PID's stamps as reservations recorded them.
|
|
107
|
+
* PIDs that are dead, or whose stamp cannot be read, are left out.
|
|
108
|
+
*/
|
|
109
|
+
async lookup(record, confirm) {
|
|
110
|
+
const alive = this.options.alive ?? processAlive;
|
|
111
|
+
const ttl = this.options.ttlMs ?? VERIFIED_IDENTITY_TTL_MS;
|
|
112
|
+
const now = (this.options.now ?? (() => performance.now()))();
|
|
113
|
+
for (const [pid, entry] of this.verified) {
|
|
114
|
+
if (now - entry.at >= ttl)
|
|
115
|
+
this.verified.delete(pid);
|
|
116
|
+
}
|
|
117
|
+
const live = (pid) => {
|
|
118
|
+
if (alive(pid))
|
|
119
|
+
return true;
|
|
120
|
+
// Forget a PID seen dead, so whatever process takes it next is read.
|
|
121
|
+
this.verified.delete(pid);
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
const identities = new Map();
|
|
125
|
+
const unread = new Set();
|
|
126
|
+
for (const pid of record)
|
|
127
|
+
if (live(pid))
|
|
128
|
+
unread.add(pid);
|
|
129
|
+
for (const [pid, stamps] of confirm) {
|
|
130
|
+
if (unread.has(pid) || !live(pid))
|
|
131
|
+
continue;
|
|
132
|
+
const cached = this.verified.get(pid)?.identity;
|
|
133
|
+
if (cached !== undefined && stamps.every((stamp) => stamp === cached)) {
|
|
134
|
+
identities.set(pid, cached);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
unread.add(pid);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (unread.size === 0)
|
|
141
|
+
return identities;
|
|
142
|
+
const read = await (this.options.read ?? readProcessIdentities)([...unread]).catch(() => new Map());
|
|
143
|
+
for (const pid of unread) {
|
|
144
|
+
const identity = read.get(pid);
|
|
145
|
+
if (identity === undefined) {
|
|
146
|
+
this.verified.delete(pid);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
this.verified.set(pid, { identity, at: now });
|
|
150
|
+
identities.set(pid, identity);
|
|
151
|
+
}
|
|
152
|
+
return identities;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** One cache per process: every ledger in it asks about the same OS processes. */
|
|
156
|
+
export const sharedProcessIdentities = new ProcessIdentityCache();
|
|
@@ -3,6 +3,7 @@ import { createReadStream } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createLogger } from '@bendyline/gezel';
|
|
5
5
|
import { transformersGraphOptions } from '@bendyline/gezel-knowledge';
|
|
6
|
+
import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
|
|
6
7
|
import { PipelineLoadError } from '../memory/embed-core.js';
|
|
7
8
|
/**
|
|
8
9
|
* Cross-encoder inference: load a pinned relevance model from its local
|
|
@@ -19,6 +20,7 @@ const BATCH = 8;
|
|
|
19
20
|
const MAX_LOADED = 2;
|
|
20
21
|
const loaded = new Map();
|
|
21
22
|
async function importTransformers() {
|
|
23
|
+
disableOnnxRuntimeTelemetry();
|
|
22
24
|
return (await import('@huggingface/transformers'));
|
|
23
25
|
}
|
|
24
26
|
export async function loadRelevanceModel(model, transformers) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
import { createLogger } from '@bendyline/gezel';
|
|
3
|
+
import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
|
|
3
4
|
import { retireModelWorker } from '../utils/retire-model-worker.js';
|
|
4
5
|
import { findServiceWorkerEntry } from '../utils/service-worker-entry.js';
|
|
5
6
|
import { disposeRelevanceModels, loadRelevanceModel, scoreRelevancePairs, } from './relevance-core.js';
|
|
@@ -185,6 +186,7 @@ class RelevanceModelHost {
|
|
|
185
186
|
return null;
|
|
186
187
|
}
|
|
187
188
|
try {
|
|
189
|
+
disableOnnxRuntimeTelemetry();
|
|
188
190
|
const worker = new Worker(entry);
|
|
189
191
|
worker.on('message', (reply) => {
|
|
190
192
|
const pending = this.pending.get(reply.id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { availableParallelism } from 'node:os';
|
|
2
1
|
import { KNOWLEDGE_FILTER_MIN_RELEVANCE, } from '@bendyline/gezel';
|
|
2
|
+
import { availableCpuCount } from '../system/host-memory.js';
|
|
3
3
|
import { relevanceModelDir } from './install.js';
|
|
4
4
|
import { DEFAULT_RELEVANCE_MODEL_ID, findRelevanceModel } from './registry.js';
|
|
5
5
|
const DEFAULT_BUDGETS = {
|
|
@@ -76,6 +76,6 @@ export function toResolvedModel(home, spec) {
|
|
|
76
76
|
maxTokens: spec.maxTokens,
|
|
77
77
|
queryMaxTokens: spec.queryMaxTokens,
|
|
78
78
|
scoreActivation: spec.scoreActivation,
|
|
79
|
-
intraOpNumThreads: Math.max(1, Math.min(4, Math.floor(
|
|
79
|
+
intraOpNumThreads: Math.max(1, Math.min(4, Math.floor(availableCpuCount() / 2))),
|
|
80
80
|
};
|
|
81
81
|
}
|