@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
package/dist/runtime-lock.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type LockIdentity, currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace } from '@bendyline/gezel/native';
|
|
2
|
+
export type { LockIdentity };
|
|
3
|
+
export { currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace };
|
|
1
4
|
/**
|
|
2
5
|
* Thrown when a gezel home already has a live daemon. The CLI's `gezel
|
|
3
6
|
* start` / the supervisor surface this as an actionable message rather
|
|
@@ -6,7 +9,8 @@
|
|
|
6
9
|
export declare class SingleInstanceError extends Error {
|
|
7
10
|
readonly holderPid: number;
|
|
8
11
|
readonly lockPath: string;
|
|
9
|
-
|
|
12
|
+
readonly holderHost?: string | undefined;
|
|
13
|
+
constructor(holderPid: number, lockPath: string, holderHost?: string | undefined);
|
|
10
14
|
}
|
|
11
15
|
export interface RuntimeLock {
|
|
12
16
|
release(): Promise<void>;
|
|
@@ -15,15 +19,25 @@ export interface RuntimeLock {
|
|
|
15
19
|
* Acquire an exclusive single-instance lock for a gezel home so two daemons
|
|
16
20
|
* can't share one `~/.gezel/` and race its on-disk state (config, sessions,
|
|
17
21
|
* tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
|
|
18
|
-
* `<runtimeDir>/lock` holding the owner
|
|
22
|
+
* `<runtimeDir>/lock` holding the owner's identity; a stale lock is
|
|
19
23
|
* reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
|
|
20
24
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* While held, the lock file's mtime is touched every few seconds: that
|
|
26
|
+
* heartbeat is how a daemon in another container (another pid namespace)
|
|
27
|
+
* is told apart from a dead one.
|
|
28
|
+
*
|
|
29
|
+
* The returned `release()` stops the heartbeat and removes the lock — call
|
|
30
|
+
* it on graceful shutdown. A hard crash leaves a stale lock the next start
|
|
31
|
+
* reclaims.
|
|
24
32
|
*/
|
|
25
33
|
export declare function acquireSingleInstanceLock(opts: {
|
|
26
34
|
runtimeDir: string;
|
|
27
35
|
lockPath: string;
|
|
28
36
|
isAlive?: (pid: number) => boolean;
|
|
37
|
+
identity?: LockIdentity;
|
|
38
|
+
startTicksOf?: (pid: number) => string | undefined;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
sleep?: (ms: number) => Promise<void>;
|
|
41
|
+
heartbeatIntervalMs?: number;
|
|
42
|
+
staleAfterMs?: number;
|
|
29
43
|
}): Promise<RuntimeLock>;
|
package/dist/runtime-lock.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, open, readFile, rm, stat, utimes } from 'node:fs/promises';
|
|
2
|
+
import { LOCK_HEARTBEAT_INTERVAL_MS, LOCK_STALE_AFTER_MS, currentLockIdentity, foreignHolderMessage, parseLockFile, procStartTicks, sharesPidNamespace, } from '@bendyline/gezel/native';
|
|
3
|
+
export { currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace };
|
|
2
4
|
/**
|
|
3
5
|
* Thrown when a gezel home already has a live daemon. The CLI's `gezel
|
|
4
6
|
* start` / the supervisor surface this as an actionable message rather
|
|
@@ -7,15 +9,21 @@ import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
|
7
9
|
export class SingleInstanceError extends Error {
|
|
8
10
|
holderPid;
|
|
9
11
|
lockPath;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
holderHost;
|
|
13
|
+
constructor(holderPid, lockPath, holderHost) {
|
|
14
|
+
super(holderHost !== undefined
|
|
15
|
+
? foreignHolderMessage({ pid: holderPid, hostname: holderHost })
|
|
16
|
+
: holderPid > 0
|
|
17
|
+
? `Another gezel daemon is already running for this home (pid ${holderPid}). Stop it first with \`gezel stop\`, or use a different GEZEL_HOME.`
|
|
18
|
+
: `Could not acquire the single-instance lock at ${lockPath}.`);
|
|
14
19
|
this.holderPid = holderPid;
|
|
15
20
|
this.lockPath = lockPath;
|
|
21
|
+
this.holderHost = holderHost;
|
|
16
22
|
this.name = 'SingleInstanceError';
|
|
17
23
|
}
|
|
18
24
|
}
|
|
25
|
+
// A holder's identity (core's lock-identity.ts) is what let a one-shot
|
|
26
|
+
// `docker run … gezel do` stop reclaiming a serving container's live lock.
|
|
19
27
|
function defaultIsAlive(pid) {
|
|
20
28
|
try {
|
|
21
29
|
process.kill(pid, 0);
|
|
@@ -27,55 +35,104 @@ function defaultIsAlive(pid) {
|
|
|
27
35
|
return err.code === 'EPERM';
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The pid stays on the first line so a daemon from before the identity
|
|
40
|
+
* record (which parses the leading integer) still reads it as held instead
|
|
41
|
+
* of stale.
|
|
42
|
+
*/
|
|
43
|
+
function serializeIdentity(identity) {
|
|
44
|
+
return `${identity.pid}\n${JSON.stringify({ v: 1, ...identity })}\n`;
|
|
45
|
+
}
|
|
46
|
+
async function readHolder(lockPath) {
|
|
47
|
+
try {
|
|
48
|
+
return parseLockFile(await readFile(lockPath, 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function lockMtimeMs(lockPath) {
|
|
31
55
|
try {
|
|
32
|
-
|
|
33
|
-
return Number.isFinite(pid) ? pid : null;
|
|
56
|
+
return (await stat(lockPath)).mtimeMs;
|
|
34
57
|
}
|
|
35
58
|
catch {
|
|
36
59
|
return null;
|
|
37
60
|
}
|
|
38
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A holder outside our pid namespace is alive while its heartbeat moves.
|
|
64
|
+
* A fresh-looking mtime may be a holder that died moments ago (a killed
|
|
65
|
+
* container restarting onto its own volume), so wait until the heartbeat
|
|
66
|
+
* either advances (alive) or ages past the stale bound (dead).
|
|
67
|
+
*/
|
|
68
|
+
async function foreignHolderIsAlive(lockPath, opts) {
|
|
69
|
+
const first = await lockMtimeMs(lockPath);
|
|
70
|
+
if (first === null)
|
|
71
|
+
return false;
|
|
72
|
+
for (;;) {
|
|
73
|
+
if (opts.now() - first > opts.staleAfterMs)
|
|
74
|
+
return false;
|
|
75
|
+
await opts.sleep(1_000);
|
|
76
|
+
const current = await lockMtimeMs(lockPath);
|
|
77
|
+
if (current === null)
|
|
78
|
+
return false;
|
|
79
|
+
if (current !== first)
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
39
83
|
/**
|
|
40
84
|
* Acquire an exclusive single-instance lock for a gezel home so two daemons
|
|
41
85
|
* can't share one `~/.gezel/` and race its on-disk state (config, sessions,
|
|
42
86
|
* tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
|
|
43
|
-
* `<runtimeDir>/lock` holding the owner
|
|
87
|
+
* `<runtimeDir>/lock` holding the owner's identity; a stale lock is
|
|
44
88
|
* reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
|
|
45
89
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
90
|
+
* While held, the lock file's mtime is touched every few seconds: that
|
|
91
|
+
* heartbeat is how a daemon in another container (another pid namespace)
|
|
92
|
+
* is told apart from a dead one.
|
|
93
|
+
*
|
|
94
|
+
* The returned `release()` stops the heartbeat and removes the lock — call
|
|
95
|
+
* it on graceful shutdown. A hard crash leaves a stale lock the next start
|
|
96
|
+
* reclaims.
|
|
49
97
|
*/
|
|
50
98
|
export async function acquireSingleInstanceLock(opts) {
|
|
51
99
|
const isAlive = opts.isAlive ?? defaultIsAlive;
|
|
100
|
+
const me = opts.identity ?? currentLockIdentity();
|
|
101
|
+
const startTicksOf = opts.startTicksOf ?? procStartTicks;
|
|
102
|
+
const now = opts.now ?? Date.now;
|
|
103
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
104
|
+
const staleAfterMs = opts.staleAfterMs ?? LOCK_STALE_AFTER_MS;
|
|
52
105
|
await mkdir(opts.runtimeDir, { recursive: true });
|
|
53
106
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
54
107
|
try {
|
|
55
108
|
const handle = await open(opts.lockPath, 'wx'); // fails with EEXIST if present
|
|
56
109
|
try {
|
|
57
|
-
await handle.writeFile(
|
|
110
|
+
await handle.writeFile(serializeIdentity(me));
|
|
58
111
|
}
|
|
59
112
|
finally {
|
|
60
113
|
await handle.close();
|
|
61
114
|
}
|
|
62
|
-
return
|
|
63
|
-
async release() {
|
|
64
|
-
// Only remove the lock if it's still ours — defends against a
|
|
65
|
-
// narrow race where another process reclaimed our stale lock.
|
|
66
|
-
const holder = await readHolderPid(opts.lockPath);
|
|
67
|
-
if (holder === null || holder === process.pid) {
|
|
68
|
-
await rm(opts.lockPath, { force: true }).catch(() => { });
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
};
|
|
115
|
+
return holdLock(opts.lockPath, me, opts.heartbeatIntervalMs ?? LOCK_HEARTBEAT_INTERVAL_MS);
|
|
72
116
|
}
|
|
73
117
|
catch (err) {
|
|
74
118
|
if (err.code !== 'EEXIST')
|
|
75
119
|
throw err;
|
|
76
|
-
const holder = await
|
|
77
|
-
if (holder
|
|
78
|
-
|
|
120
|
+
const holder = await readHolder(opts.lockPath);
|
|
121
|
+
if (holder) {
|
|
122
|
+
if (!sharesPidNamespace(holder, me)) {
|
|
123
|
+
if (await foreignHolderIsAlive(opts.lockPath, { now, sleep, staleAfterMs })) {
|
|
124
|
+
throw new SingleInstanceError(holder.pid, opts.lockPath, holder.hostname ?? 'unknown');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (holder.pid !== me.pid && isAlive(holder.pid)) {
|
|
128
|
+
// A recorded start time that no longer matches means the pid was
|
|
129
|
+
// reused by an unrelated process after the holder died.
|
|
130
|
+
const reused = holder.startTicks !== undefined &&
|
|
131
|
+
startTicksOf(holder.pid) !== undefined &&
|
|
132
|
+
startTicksOf(holder.pid) !== holder.startTicks;
|
|
133
|
+
if (!reused)
|
|
134
|
+
throw new SingleInstanceError(holder.pid, opts.lockPath);
|
|
135
|
+
}
|
|
79
136
|
}
|
|
80
137
|
// Stale (dead holder / our own pid / unreadable) — reclaim and retry.
|
|
81
138
|
await rm(opts.lockPath, { force: true }).catch(() => { });
|
|
@@ -83,3 +140,36 @@ export async function acquireSingleInstanceLock(opts) {
|
|
|
83
140
|
}
|
|
84
141
|
throw new SingleInstanceError(-1, opts.lockPath);
|
|
85
142
|
}
|
|
143
|
+
function holdLock(lockPath, me, intervalMs) {
|
|
144
|
+
const isOurs = (holder) => holder === null ||
|
|
145
|
+
(me.instance !== undefined && holder.instance !== undefined
|
|
146
|
+
? holder.instance === me.instance
|
|
147
|
+
: holder.pid === me.pid);
|
|
148
|
+
let timer = setInterval(() => {
|
|
149
|
+
void (async () => {
|
|
150
|
+
const holder = await readHolder(lockPath);
|
|
151
|
+
if (holder === null || !isOurs(holder)) {
|
|
152
|
+
// Someone else owns the file now; touching it would keep their lock
|
|
153
|
+
// looking alive on our behalf.
|
|
154
|
+
clearInterval(timer);
|
|
155
|
+
timer = undefined;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const at = new Date();
|
|
159
|
+
await utimes(lockPath, at, at).catch(() => { });
|
|
160
|
+
})();
|
|
161
|
+
}, intervalMs);
|
|
162
|
+
timer.unref?.();
|
|
163
|
+
return {
|
|
164
|
+
async release() {
|
|
165
|
+
if (timer)
|
|
166
|
+
clearInterval(timer);
|
|
167
|
+
timer = undefined;
|
|
168
|
+
// Only remove the lock if it's still ours — defends against a
|
|
169
|
+
// narrow race where another process reclaimed our stale lock.
|
|
170
|
+
if (isOurs(await readHolder(lockPath))) {
|
|
171
|
+
await rm(lockPath, { force: true }).catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory and CPU as this process may actually use them. `os.totalmem()` and
|
|
3
|
+
* `os.availableParallelism()` describe the host; inside a container started
|
|
4
|
+
* with `--memory 8g --cpus 4` on a 64 GB, 16-core machine they still say 64
|
|
5
|
+
* and 16, so a model sized from them gets the container killed by its own
|
|
6
|
+
* cgroup. Every sizing decision reads these instead.
|
|
7
|
+
*/
|
|
8
|
+
export interface HostLimitsProbe {
|
|
9
|
+
totalmem: () => number;
|
|
10
|
+
freemem: () => number;
|
|
11
|
+
constrainedMemory: () => number | undefined;
|
|
12
|
+
availableMemory: () => number | undefined;
|
|
13
|
+
availableParallelism: () => number;
|
|
14
|
+
/** Every logical CPU the host has, whatever this process may use. */
|
|
15
|
+
hostCpuCount: () => number;
|
|
16
|
+
readFile: (path: string) => string | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The cgroup memory limit when it is below the host's RAM, else null.
|
|
20
|
+
* Node reads cgroup v1 and v2; "no limit" comes back as 0 or as a value at
|
|
21
|
+
* or above physical RAM, depending on the libuv release.
|
|
22
|
+
*/
|
|
23
|
+
export declare function containerMemoryLimitBytes(probe?: HostLimitsProbe): number | null;
|
|
24
|
+
/** RAM this process may use: host RAM, or the container's limit when lower. */
|
|
25
|
+
export declare function totalMemoryBytes(probe?: HostLimitsProbe): number;
|
|
26
|
+
/** Free RAM, never more than the container's limit leaves. */
|
|
27
|
+
export declare function freeMemoryBytes(probe?: HostLimitsProbe): number;
|
|
28
|
+
/**
|
|
29
|
+
* Caps a host-wide free-memory reading (`MemAvailable`, `vm_stat`) to what
|
|
30
|
+
* the container still has. Unchanged outside a container.
|
|
31
|
+
*/
|
|
32
|
+
export declare function capToContainer(hostFreeBytes: number, probe?: HostLimitsProbe): number;
|
|
33
|
+
/**
|
|
34
|
+
* CPUs this process may keep busy: the scheduler's CPU set, further capped
|
|
35
|
+
* by a cgroup v2 `cpu.max` quota (or v1 `cfs_quota_us`) rounded up.
|
|
36
|
+
*/
|
|
37
|
+
export declare function availableCpuCount(probe?: HostLimitsProbe): number;
|
|
38
|
+
export declare function cgroupCpuQuota(probe?: HostLimitsProbe): number | null;
|
|
39
|
+
/**
|
|
40
|
+
* The CPUs a container (a `--cpus` quota or a `--cpuset-cpus` set) leaves this
|
|
41
|
+
* process when that is fewer than the host has, else null. Engines that size
|
|
42
|
+
* their own thread pool from the host's cores, as llama.cpp does, oversubscribe
|
|
43
|
+
* such a limit badly: 32 threads under a 4-CPU quota spend most of each period
|
|
44
|
+
* throttled. Measured in a 4-CPU container: a 0.8B model decoded at 3.7 t/s.
|
|
45
|
+
*/
|
|
46
|
+
export declare function containerCpuLimit(probe?: HostLimitsProbe): number | null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { availableParallelism, cpus, freemem, totalmem } from 'node:os';
|
|
3
|
+
function readTrimmed(path) {
|
|
4
|
+
try {
|
|
5
|
+
return readFileSync(path, 'utf8').trim();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const nodeProbe = {
|
|
12
|
+
totalmem,
|
|
13
|
+
freemem,
|
|
14
|
+
constrainedMemory: () => typeof process.constrainedMemory === 'function' ? process.constrainedMemory() : undefined,
|
|
15
|
+
availableMemory: () => typeof process.availableMemory === 'function' ? process.availableMemory() : undefined,
|
|
16
|
+
availableParallelism,
|
|
17
|
+
hostCpuCount: () => cpus().length,
|
|
18
|
+
readFile: (path) => (process.platform === 'linux' ? readTrimmed(path) : undefined),
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The cgroup memory limit when it is below the host's RAM, else null.
|
|
22
|
+
* Node reads cgroup v1 and v2; "no limit" comes back as 0 or as a value at
|
|
23
|
+
* or above physical RAM, depending on the libuv release.
|
|
24
|
+
*/
|
|
25
|
+
export function containerMemoryLimitBytes(probe = nodeProbe) {
|
|
26
|
+
const host = probe.totalmem();
|
|
27
|
+
const limit = probe.constrainedMemory();
|
|
28
|
+
if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0 && limit < host) {
|
|
29
|
+
return limit;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** RAM this process may use: host RAM, or the container's limit when lower. */
|
|
34
|
+
export function totalMemoryBytes(probe = nodeProbe) {
|
|
35
|
+
return containerMemoryLimitBytes(probe) ?? probe.totalmem();
|
|
36
|
+
}
|
|
37
|
+
/** Free RAM, never more than the container's limit leaves. */
|
|
38
|
+
export function freeMemoryBytes(probe = nodeProbe) {
|
|
39
|
+
return capToContainer(probe.freemem(), probe);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Caps a host-wide free-memory reading (`MemAvailable`, `vm_stat`) to what
|
|
43
|
+
* the container still has. Unchanged outside a container.
|
|
44
|
+
*/
|
|
45
|
+
export function capToContainer(hostFreeBytes, probe = nodeProbe) {
|
|
46
|
+
if (containerMemoryLimitBytes(probe) === null)
|
|
47
|
+
return hostFreeBytes;
|
|
48
|
+
const available = probe.availableMemory();
|
|
49
|
+
if (typeof available !== 'number' || !Number.isFinite(available) || available < 0) {
|
|
50
|
+
return hostFreeBytes;
|
|
51
|
+
}
|
|
52
|
+
return Math.min(hostFreeBytes, available);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* CPUs this process may keep busy: the scheduler's CPU set, further capped
|
|
56
|
+
* by a cgroup v2 `cpu.max` quota (or v1 `cfs_quota_us`) rounded up.
|
|
57
|
+
*/
|
|
58
|
+
export function availableCpuCount(probe = nodeProbe) {
|
|
59
|
+
const cpus = Math.max(1, probe.availableParallelism());
|
|
60
|
+
const quota = cgroupCpuQuota(probe);
|
|
61
|
+
return quota === null ? cpus : Math.max(1, Math.min(cpus, Math.ceil(quota)));
|
|
62
|
+
}
|
|
63
|
+
export function cgroupCpuQuota(probe = nodeProbe) {
|
|
64
|
+
const v2 = probe.readFile('/sys/fs/cgroup/cpu.max');
|
|
65
|
+
if (v2) {
|
|
66
|
+
const [max, period] = v2.split(/\s+/);
|
|
67
|
+
const quota = Number(max);
|
|
68
|
+
const per = Number(period);
|
|
69
|
+
if (max !== 'max' && quota > 0 && per > 0)
|
|
70
|
+
return quota / per;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const v1Quota = Number(probe.readFile('/sys/fs/cgroup/cpu/cpu.cfs_quota_us'));
|
|
74
|
+
const v1Period = Number(probe.readFile('/sys/fs/cgroup/cpu/cpu.cfs_period_us'));
|
|
75
|
+
if (v1Quota > 0 && v1Period > 0)
|
|
76
|
+
return v1Quota / v1Period;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The CPUs a container (a `--cpus` quota or a `--cpuset-cpus` set) leaves this
|
|
81
|
+
* process when that is fewer than the host has, else null. Engines that size
|
|
82
|
+
* their own thread pool from the host's cores, as llama.cpp does, oversubscribe
|
|
83
|
+
* such a limit badly: 32 threads under a 4-CPU quota spend most of each period
|
|
84
|
+
* throttled. Measured in a 4-CPU container: a 0.8B model decoded at 3.7 t/s.
|
|
85
|
+
*/
|
|
86
|
+
export function containerCpuLimit(probe = nodeProbe) {
|
|
87
|
+
const allowed = availableCpuCount(probe);
|
|
88
|
+
return allowed < probe.hostCpuCount() ? allowed : null;
|
|
89
|
+
}
|
package/dist/system/memory.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { platform as osPlatform } from 'node:os';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { readDeviceSystemFacts, windowsHeadlessSpawnOptions, } from '@bendyline/gezel/native';
|
|
5
5
|
import { detectLlamaGpuVram, } from '../providers/llama-cpp/devices.js';
|
|
6
6
|
import { autoDetectBudgetBytes, computeCapacityBudget, setDetectedGpuVramBytes, } from '../providers/native/capacity-budget.js';
|
|
7
7
|
import { readAmdgpuAdapters } from './amdgpu-adapters.js';
|
|
8
8
|
import { gpuCarveOutBytes, gpuVendorFromPciId, linuxGpuAddressableBytes, linuxGpuCarveOutBytes, matchGpuAdapter, } from './gpu-carve-out.js';
|
|
9
|
+
import { freeMemoryBytes, totalMemoryBytes } from './host-memory.js';
|
|
9
10
|
const exec = promisify(execFile);
|
|
10
11
|
let cachedMemoryProfile = null;
|
|
11
12
|
let pendingMemoryProfile = null;
|
|
@@ -25,7 +26,7 @@ let pendingMemoryProfile = null;
|
|
|
25
26
|
*/
|
|
26
27
|
export async function detectMemoryProfile() {
|
|
27
28
|
const plat = osPlatform();
|
|
28
|
-
const totalRam =
|
|
29
|
+
const totalRam = totalMemoryBytes();
|
|
29
30
|
if (plat === 'darwin') {
|
|
30
31
|
setDetectedGpuVramBytes(null);
|
|
31
32
|
return {
|
|
@@ -311,7 +312,7 @@ export function sampleMachineMemoryUsage(opts) {
|
|
|
311
312
|
const residentModels = opts.residentModels ?? [];
|
|
312
313
|
const sampledAt = opts.sampledAt ?? new Date().toISOString();
|
|
313
314
|
if (mainMemory) {
|
|
314
|
-
const freeRamBytes = clamp(finiteNonNegative(opts.darwinSystemMemory?.freeBytes ?? opts.freeRamBytes ??
|
|
315
|
+
const freeRamBytes = clamp(finiteNonNegative(opts.darwinSystemMemory?.freeBytes ?? opts.freeRamBytes ?? freeMemoryBytes()), 0, totalRamBytes);
|
|
315
316
|
const cachedBytes = opts.darwinSystemMemory
|
|
316
317
|
? clamp(finiteNonNegative(opts.darwinSystemMemory.cachedBytes), 0, totalRamBytes - freeRamBytes)
|
|
317
318
|
: null;
|
|
@@ -512,7 +513,7 @@ function clamp(value, min, max) {
|
|
|
512
513
|
* held back only ~3 GiB for the OS, so a 95 GB chat model and a 10 GB image
|
|
513
514
|
* model were co-admitted and the image engine died with a CUDA OOM at init.
|
|
514
515
|
*/
|
|
515
|
-
export function parseNvidiaMemoryProbe(stdout, systemRamBytes =
|
|
516
|
+
export function parseNvidiaMemoryProbe(stdout, systemRamBytes = totalMemoryBytes()) {
|
|
516
517
|
const devices = [];
|
|
517
518
|
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
518
519
|
// GPU names can theoretically contain commas. Split on the final field,
|
|
@@ -19,6 +19,7 @@ import { createReadStream } from 'node:fs';
|
|
|
19
19
|
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
20
20
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { createLogger } from '@bendyline/gezel';
|
|
22
|
+
import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
|
|
22
23
|
import { isPathInside } from './fs/safe-paths.js';
|
|
23
24
|
const log = createLogger('service');
|
|
24
25
|
/**
|
|
@@ -330,6 +331,7 @@ export function isMissingModule(err, specifier) {
|
|
|
330
331
|
return new RegExp(String.raw `Cannot find (?:package|module) '${escaped}'`).test(err.message);
|
|
331
332
|
}
|
|
332
333
|
async function defaultLoadEnv() {
|
|
334
|
+
disableOnnxRuntimeTelemetry();
|
|
333
335
|
const mod = (await import('@huggingface/transformers'));
|
|
334
336
|
return mod.env;
|
|
335
337
|
}
|
|
@@ -14,7 +14,7 @@ export declare const downloadLog: import("@bendyline/gezel").Logger;
|
|
|
14
14
|
* transparent about who we are is the right posture for a third-party client
|
|
15
15
|
* fetching public weights on the user's behalf.
|
|
16
16
|
*/
|
|
17
|
-
export declare const GEZEL_DOWNLOAD_UA
|
|
17
|
+
export declare const GEZEL_DOWNLOAD_UA: string;
|
|
18
18
|
/**
|
|
19
19
|
* Headers for an HF control-plane request. An optional operator token raises
|
|
20
20
|
* public-model rate limits, but is never forwarded to Xet/CAS/CDN hosts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/gezel-inference",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Gezel's on-device inference runtime: native engines, local model management, embeddings, knowledge search, and OpenAI-compatible routes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -73,16 +73,16 @@
|
|
|
73
73
|
"!dist/**/__pycache__"
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@bendyline/gezel": "1.2.
|
|
77
|
-
"@bendyline/gezel-catalog": "1.2.
|
|
78
|
-
"@bendyline/gezel-client": "1.2.
|
|
79
|
-
"@bendyline/gezel-knowledge": "1.2.
|
|
80
|
-
"hono": "4.13.
|
|
76
|
+
"@bendyline/gezel": "1.2.13",
|
|
77
|
+
"@bendyline/gezel-catalog": "1.2.13",
|
|
78
|
+
"@bendyline/gezel-client": "1.2.13",
|
|
79
|
+
"@bendyline/gezel-knowledge": "1.2.10",
|
|
80
|
+
"hono": "4.13.14",
|
|
81
81
|
"tar": "7.5.21",
|
|
82
|
-
"undici": "8.
|
|
82
|
+
"undici": "8.12.3",
|
|
83
83
|
"yauzl": "^3.4.0",
|
|
84
84
|
"yazl": "^3.3.1",
|
|
85
|
-
"zod": "
|
|
85
|
+
"zod": "4.6.7"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"@huggingface/transformers": "^4.3.1"
|