@reftrixmcp/ml 0.3.0 → 0.5.1
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/config/worker-resource-limits.d.ts +89 -0
- package/dist/config/worker-resource-limits.d.ts.map +1 -0
- package/dist/config/worker-resource-limits.js +156 -0
- package/dist/config/worker-resource-limits.js.map +1 -0
- package/dist/dinov2/service.d.ts.map +1 -1
- package/dist/dinov2/service.js +70 -10
- package/dist/dinov2/service.js.map +1 -1
- package/dist/dinov2/worker-thread.js +72 -9
- package/dist/dinov2/worker-thread.js.map +1 -1
- package/dist/embeddings/service.d.ts.map +1 -1
- package/dist/embeddings/service.js +13 -1
- package/dist/embeddings/service.js.map +1 -1
- package/dist/embeddings/worker-thread.d.ts.map +1 -1
- package/dist/embeddings/worker-thread.js +8 -83
- package/dist/embeddings/worker-thread.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/onnx-availability.d.ts +29 -0
- package/dist/onnx-availability.d.ts.map +1 -0
- package/dist/onnx-availability.js +69 -0
- package/dist/onnx-availability.js.map +1 -0
- package/dist/onnx-provider-detect.d.ts +47 -0
- package/dist/onnx-provider-detect.d.ts.map +1 -0
- package/dist/onnx-provider-detect.js +111 -0
- package/dist/onnx-provider-detect.js.map +1 -0
- package/package.json +5 -3
- package/src/config/worker-resource-limits.ts +176 -0
- package/src/dinov2/service.ts +88 -10
- package/src/dinov2/worker-thread.ts +85 -10
- package/src/embeddings/service.ts +13 -1
- package/src/embeddings/worker-thread.ts +13 -102
- package/src/index.ts +15 -0
- package/src/onnx-availability.ts +78 -0
- package/src/onnx-provider-detect.ts +134 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onnx-provider-detect.js","sourceRoot":"","sources":["../src/onnx-provider-detect.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reftrixmcp/ml",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ML/Embedding services for Reftrix",
|
|
@@ -30,16 +30,18 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@huggingface/transformers": "^3.8.1",
|
|
33
|
-
"onnxruntime-node": "1.21.0",
|
|
34
33
|
"p-limit": "^3.1.0",
|
|
35
34
|
"zod": "^3.24.0"
|
|
36
35
|
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"onnxruntime-node": "1.21.0"
|
|
38
|
+
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/node": "^22.0.0",
|
|
39
41
|
"@vitest/coverage-v8": "^4.0.14",
|
|
40
42
|
"typescript": "^5.9.0",
|
|
41
43
|
"vitest": "^4.0.14",
|
|
42
|
-
"@reftrixmcp/config": "0.
|
|
44
|
+
"@reftrixmcp/config": "0.5.1"
|
|
43
45
|
},
|
|
44
46
|
"publishConfig": {
|
|
45
47
|
"access": "public"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025-2026 Reftrix Contributors
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ML Worker Thread Resource Limits (PR7e-β1)
|
|
6
|
+
*
|
|
7
|
+
* ONNX Runtime が CPU fallback (onnxruntime-node) に切り替わったとき、
|
|
8
|
+
* `execArgv: []` を指定した Worker Thread は親プロセスの
|
|
9
|
+
* `--max-old-space-size` を継承しないため、デフォルトでは V8 old space
|
|
10
|
+
* が 4GB 未満の既定値に制限されてしまう。その結果、e5-base / DINOv2 の
|
|
11
|
+
* CPU 推論で OOM クラッシュが発生し、Queue-based Backfill が永久に
|
|
12
|
+
* `in_progress` のまま滞留する。
|
|
13
|
+
*
|
|
14
|
+
* When ONNX Runtime falls back to CPU (`onnxruntime-node`), a Worker Thread
|
|
15
|
+
* spawned with `execArgv: []` does NOT inherit the parent's
|
|
16
|
+
* `--max-old-space-size` flag, so V8 old-space defaults to less than 4GB.
|
|
17
|
+
* This causes OOM crashes during e5-base / DINOv2 CPU inference and leaves
|
|
18
|
+
* Queue-based Backfill jobs stuck `in_progress` indefinitely.
|
|
19
|
+
*
|
|
20
|
+
* Fix: explicitly pass `resourceLimits` to `new Worker(...)` so that the
|
|
21
|
+
* worker gets an explicit old-space / young-space / code-range ceiling.
|
|
22
|
+
*
|
|
23
|
+
* 参照 / References:
|
|
24
|
+
* - ADR-0012 §3 BLOCKER 4
|
|
25
|
+
* - Node.js docs: https://nodejs.org/api/worker_threads.html#new-workerfilename-options
|
|
26
|
+
*
|
|
27
|
+
* @module config/worker-resource-limits
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default old-space ceiling (MB) for ML Worker Threads.
|
|
34
|
+
*
|
|
35
|
+
* 4096 MB は e5-base (~500MB arena) + DINOv2 (~800MB arena) + transformers.js
|
|
36
|
+
* pipeline overhead を同時に CPU でロードしても OOM にならない実測値。
|
|
37
|
+
* さらに β1 では Phase 5 親プロセス RSS 上限を 4096 MB に段階緩和するため、
|
|
38
|
+
* Worker old-space をそれと同水準に揃えることで、親プロセスと子 Worker の
|
|
39
|
+
* メモリ上限を対称化する。
|
|
40
|
+
*
|
|
41
|
+
* 4096 MB is empirically the minimum old-space ceiling that keeps e5-base
|
|
42
|
+
* (~500MB arena) + DINOv2 (~800MB arena) + transformers.js pipeline overhead
|
|
43
|
+
* from OOMing on CPU. β1 also raises the Phase 5 parent-process RSS ceiling
|
|
44
|
+
* to 4096 MB, so aligning the Worker old-space with the parent RSS keeps
|
|
45
|
+
* parent/child memory ceilings symmetric.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_ML_WORKER_MAX_OLD_SPACE_MB = 4096;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fixed secondary limits.
|
|
51
|
+
*
|
|
52
|
+
* young-space=512MB and code-range=256MB are Node.js defaults scaled for
|
|
53
|
+
* ONNX Runtime's large tensor allocations; exposing them as env vars would
|
|
54
|
+
* create too many configuration surfaces.
|
|
55
|
+
*/
|
|
56
|
+
const ML_WORKER_MAX_YOUNG_GENERATION_MB = 512;
|
|
57
|
+
const ML_WORKER_CODE_RANGE_MB = 256;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 許容レンジ
|
|
61
|
+
* - 下限 512 MB: e5-base の初期化 (tokenizer + model metadata) だけでも
|
|
62
|
+
* 約 300MB 必要で、ヘッドルームを確保する最小値
|
|
63
|
+
* - 上限 8192 MB: 単一 Worker Thread で 8GB を超える old-space は現実的な
|
|
64
|
+
* ユースケースが存在せず、over-provisioning 防御
|
|
65
|
+
*
|
|
66
|
+
* Allowed range:
|
|
67
|
+
* - Lower bound 512 MB: e5-base init (tokenizer + model metadata) alone needs
|
|
68
|
+
* ~300MB, so this is the minimum that preserves any headroom.
|
|
69
|
+
* - Upper bound 8192 MB: no realistic use case for > 8GB old-space in a single
|
|
70
|
+
* Worker Thread; guards against over-provisioning.
|
|
71
|
+
*/
|
|
72
|
+
const ML_WORKER_MAX_OLD_SPACE_MIN_MB = 512;
|
|
73
|
+
const ML_WORKER_MAX_OLD_SPACE_MAX_MB = 8192;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ML Worker resource-limits Zod schema
|
|
77
|
+
*/
|
|
78
|
+
export const MLWorkerResourceLimitsSchema = z.object({
|
|
79
|
+
/**
|
|
80
|
+
* Worker Thread の V8 old-space 上限(MB)
|
|
81
|
+
* V8 old-space ceiling (MB) for the Worker Thread
|
|
82
|
+
*/
|
|
83
|
+
maxOldGenerationSizeMb: z
|
|
84
|
+
.number()
|
|
85
|
+
.int()
|
|
86
|
+
.min(ML_WORKER_MAX_OLD_SPACE_MIN_MB)
|
|
87
|
+
.max(ML_WORKER_MAX_OLD_SPACE_MAX_MB)
|
|
88
|
+
.default(DEFAULT_ML_WORKER_MAX_OLD_SPACE_MB),
|
|
89
|
+
/**
|
|
90
|
+
* Worker Thread の V8 young-space 上限(MB)
|
|
91
|
+
* V8 young-space ceiling (MB) for the Worker Thread
|
|
92
|
+
*/
|
|
93
|
+
maxYoungGenerationSizeMb: z.number().int().positive().default(ML_WORKER_MAX_YOUNG_GENERATION_MB),
|
|
94
|
+
/**
|
|
95
|
+
* Worker Thread の code-range 上限(MB)
|
|
96
|
+
* Code-range ceiling (MB) for the Worker Thread
|
|
97
|
+
*/
|
|
98
|
+
codeRangeSizeMb: z.number().int().positive().default(ML_WORKER_CODE_RANGE_MB),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export type MLWorkerResourceLimits = z.infer<typeof MLWorkerResourceLimitsSchema>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 環境変数から ML Worker Thread の resource-limits をロードする。
|
|
105
|
+
* Load ML Worker Thread resource-limits from environment variables.
|
|
106
|
+
*
|
|
107
|
+
* 環境変数 / Environment variables:
|
|
108
|
+
* - `ML_WORKER_MAX_OLD_SPACE_MB`: V8 old-space ceiling (MB, integer, 512..8192)
|
|
109
|
+
*
|
|
110
|
+
* 不正値(NaN / Infinity / 負値 / 範囲外)は warn してデフォルトにフォールバック。
|
|
111
|
+
* Invalid values (NaN / Infinity / negative / out of range) warn and fall back
|
|
112
|
+
* to defaults.
|
|
113
|
+
*
|
|
114
|
+
* 副作用 / Side effects:
|
|
115
|
+
* - なし (純粋関数) / None (pure function)
|
|
116
|
+
* - 呼び出し側でログ出力すること / Caller is responsible for logging
|
|
117
|
+
*/
|
|
118
|
+
export function loadMLWorkerResourceLimits(): MLWorkerResourceLimits {
|
|
119
|
+
const raw = process.env["ML_WORKER_MAX_OLD_SPACE_MB"];
|
|
120
|
+
|
|
121
|
+
// 未設定 → Zod default
|
|
122
|
+
// Unset → Zod default
|
|
123
|
+
if (raw === undefined || raw === "") {
|
|
124
|
+
return MLWorkerResourceLimitsSchema.parse({});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parsed = Number(raw);
|
|
128
|
+
|
|
129
|
+
// NaN / Infinity 早期拒絶 (Zod でも弾けるが明示的に)
|
|
130
|
+
// Reject NaN / Infinity explicitly
|
|
131
|
+
if (!Number.isFinite(parsed)) {
|
|
132
|
+
return {
|
|
133
|
+
maxOldGenerationSizeMb: DEFAULT_ML_WORKER_MAX_OLD_SPACE_MB,
|
|
134
|
+
maxYoungGenerationSizeMb: ML_WORKER_MAX_YOUNG_GENERATION_MB,
|
|
135
|
+
codeRangeSizeMb: ML_WORKER_CODE_RANGE_MB,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = MLWorkerResourceLimitsSchema.safeParse({ maxOldGenerationSizeMb: parsed });
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
return {
|
|
142
|
+
maxOldGenerationSizeMb: DEFAULT_ML_WORKER_MAX_OLD_SPACE_MB,
|
|
143
|
+
maxYoungGenerationSizeMb: ML_WORKER_MAX_YOUNG_GENERATION_MB,
|
|
144
|
+
codeRangeSizeMb: ML_WORKER_CODE_RANGE_MB,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result.data;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 新しい Worker Thread を作成する際に渡す resourceLimits オプションを返す。
|
|
153
|
+
* Returns the `resourceLimits` option object to pass to `new Worker(...)`.
|
|
154
|
+
*
|
|
155
|
+
* 使用例 / Usage:
|
|
156
|
+
* ```ts
|
|
157
|
+
* import { getMLWorkerThreadOptions } from "@reftrixmcp/ml/config/worker-resource-limits";
|
|
158
|
+
* new Worker(scriptPath, { execArgv: [], ...getMLWorkerThreadOptions() });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function getMLWorkerThreadOptions(): {
|
|
162
|
+
resourceLimits: {
|
|
163
|
+
maxOldGenerationSizeMb: number;
|
|
164
|
+
maxYoungGenerationSizeMb: number;
|
|
165
|
+
codeRangeSizeMb: number;
|
|
166
|
+
};
|
|
167
|
+
} {
|
|
168
|
+
const limits = loadMLWorkerResourceLimits();
|
|
169
|
+
return {
|
|
170
|
+
resourceLimits: {
|
|
171
|
+
maxOldGenerationSizeMb: limits.maxOldGenerationSizeMb,
|
|
172
|
+
maxYoungGenerationSizeMb: limits.maxYoungGenerationSizeMb,
|
|
173
|
+
codeRangeSizeMb: limits.codeRangeSizeMb,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
package/src/dinov2/service.ts
CHANGED
|
@@ -23,6 +23,9 @@ import { Worker } from "node:worker_threads";
|
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import type { DINOv2WorkerMessage, DINOv2WorkerResponse } from "./worker-thread-types.js";
|
|
26
|
+
import { safeImportOnnx } from "../onnx-availability.js";
|
|
27
|
+
import { detectExecutionProvider, isLdLibraryPathSetAtOsLevel } from "../onnx-provider-detect.js";
|
|
28
|
+
import { getMLWorkerThreadOptions } from "../config/worker-resource-limits.js";
|
|
26
29
|
|
|
27
30
|
// =====================================================
|
|
28
31
|
// Constants
|
|
@@ -174,7 +177,13 @@ export class DINOv2Service {
|
|
|
174
177
|
|
|
175
178
|
// Worker threads inherit the parent's execArgv by default.
|
|
176
179
|
// Pass empty array to prevent parent flags from propagating.
|
|
177
|
-
|
|
180
|
+
//
|
|
181
|
+
// PR7e-β1: Add explicit resourceLimits to prevent OOM when ONNX
|
|
182
|
+
// falls back to CPU (onnxruntime-node). DINOv2 ViT-B/14 arena is
|
|
183
|
+
// ~800MB; without resourceLimits V8 old-space < 4GB triggers OOM
|
|
184
|
+
// during CPU inference. See ADR-0012 §3 BLOCKER 4.
|
|
185
|
+
const workerOptions = { execArgv: [], ...getMLWorkerThreadOptions() };
|
|
186
|
+
this.worker = new Worker(scriptPath, workerOptions);
|
|
178
187
|
|
|
179
188
|
this.worker.on("message", (response: DINOv2WorkerResponse) => {
|
|
180
189
|
this.handleWorkerResponse(response);
|
|
@@ -280,18 +289,67 @@ export class DINOv2Service {
|
|
|
280
289
|
// =====================================================
|
|
281
290
|
|
|
282
291
|
private async initializeInProcess(): Promise<void> {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
const provider = detectExecutionProvider("DINOv2");
|
|
293
|
+
|
|
294
|
+
// Safety check: LD_LIBRARY_PATH must be set at OS level for CUDA
|
|
295
|
+
let effectiveProvider = provider;
|
|
296
|
+
if (effectiveProvider === "cuda" && !isLdLibraryPathSetAtOsLevel()) {
|
|
297
|
+
console.warn(
|
|
298
|
+
"[DINOv2] CUDA requested but LD_LIBRARY_PATH not set at OS level. " + "Falling back to CPU."
|
|
299
|
+
);
|
|
300
|
+
effectiveProvider = "cpu";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const sessionOptions =
|
|
304
|
+
effectiveProvider === "cuda"
|
|
305
|
+
? {
|
|
306
|
+
executionProviders: ["CUDAExecutionProvider", "CPUExecutionProvider"],
|
|
307
|
+
enableMemPattern: false,
|
|
308
|
+
}
|
|
309
|
+
: {
|
|
310
|
+
executionProviders: ["cpu"],
|
|
311
|
+
enableCpuMemArena: false,
|
|
312
|
+
enableMemPattern: false,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const ortModule = await safeImportOnnx();
|
|
316
|
+
|
|
317
|
+
// Attempt session creation with resolved provider.
|
|
318
|
+
// If CUDA init fails, catch and retry with CPU.
|
|
319
|
+
try {
|
|
320
|
+
this.inProcessSession = await ortModule.InferenceSession.create(
|
|
321
|
+
this.modelPath,
|
|
322
|
+
sessionOptions
|
|
323
|
+
);
|
|
324
|
+
} catch (providerError) {
|
|
325
|
+
if (effectiveProvider !== "cpu") {
|
|
326
|
+
const errorMsg =
|
|
327
|
+
providerError instanceof Error ? providerError.message : String(providerError);
|
|
328
|
+
console.warn(
|
|
329
|
+
"[DINOv2] CUDA in-process session creation failed, falling back to CPU: %s",
|
|
330
|
+
errorMsg
|
|
331
|
+
);
|
|
332
|
+
this.inProcessSession = await ortModule.InferenceSession.create(this.modelPath, {
|
|
333
|
+
executionProviders: ["cpu"],
|
|
334
|
+
enableCpuMemArena: false,
|
|
335
|
+
enableMemPattern: false,
|
|
336
|
+
});
|
|
337
|
+
effectiveProvider = "cpu";
|
|
338
|
+
} else {
|
|
339
|
+
throw providerError;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
289
343
|
// eslint-disable-next-line no-console
|
|
290
|
-
console.log(
|
|
344
|
+
console.log(
|
|
345
|
+
"[DINOv2] Session initialized in-process (provider: %s):",
|
|
346
|
+
effectiveProvider,
|
|
347
|
+
this.modelPath
|
|
348
|
+
);
|
|
291
349
|
}
|
|
292
350
|
|
|
293
351
|
private async generateInProcess(imageBuffer: Buffer): Promise<number[]> {
|
|
294
|
-
const ortModule = await
|
|
352
|
+
const ortModule = await safeImportOnnx();
|
|
295
353
|
|
|
296
354
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
355
|
const sess = this.inProcessSession as any;
|
|
@@ -319,7 +377,18 @@ export class DINOv2Service {
|
|
|
319
377
|
}
|
|
320
378
|
|
|
321
379
|
const inputTensor = new ortModule.Tensor("float32", float32, [1, 3, height, width]);
|
|
322
|
-
|
|
380
|
+
|
|
381
|
+
// OOM-FIX-1: Use try/finally to ensure ONNX tensor disposal.
|
|
382
|
+
// ONNX Tensor holds native C++ memory that V8 GC cannot reclaim.
|
|
383
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
384
|
+
let output: any;
|
|
385
|
+
try {
|
|
386
|
+
output = await sess.run({ pixel_values: inputTensor });
|
|
387
|
+
} finally {
|
|
388
|
+
if (typeof inputTensor.dispose === "function") {
|
|
389
|
+
inputTensor.dispose();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
323
392
|
|
|
324
393
|
const lastHiddenState = output.last_hidden_state;
|
|
325
394
|
if (!lastHiddenState) {
|
|
@@ -334,6 +403,15 @@ export class DINOv2Service {
|
|
|
334
403
|
clsToken[i] = data[i]!;
|
|
335
404
|
}
|
|
336
405
|
|
|
406
|
+
// OOM-FIX-1: Dispose output tensors after data extraction
|
|
407
|
+
for (const key of Object.keys(output)) {
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
409
|
+
const tensor = output[key] as any;
|
|
410
|
+
if (typeof tensor?.dispose === "function") {
|
|
411
|
+
tensor.dispose();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
337
415
|
// L2 normalize
|
|
338
416
|
let norm = 0;
|
|
339
417
|
for (let i = 0; i < clsToken.length; i++) {
|
|
@@ -28,16 +28,20 @@ import type {
|
|
|
28
28
|
} from "./worker-thread-types.js";
|
|
29
29
|
|
|
30
30
|
// =====================================================
|
|
31
|
-
// Dynamic import of onnxruntime-node
|
|
31
|
+
// Dynamic import of onnxruntime-node (optional dependency)
|
|
32
32
|
// =====================================================
|
|
33
33
|
|
|
34
|
+
import { safeImportOnnx } from "../onnx-availability.js";
|
|
35
|
+
import { detectExecutionProvider, isLdLibraryPathSetAtOsLevel } from "../onnx-provider-detect.js";
|
|
36
|
+
import type { ExecutionProvider } from "../onnx-provider-detect.js";
|
|
37
|
+
|
|
34
38
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
35
39
|
type OrtModule = typeof import("onnxruntime-node");
|
|
36
40
|
let ort: OrtModule | null = null;
|
|
37
41
|
|
|
38
42
|
async function getOrt(): Promise<OrtModule> {
|
|
39
43
|
if (!ort) {
|
|
40
|
-
ort = await
|
|
44
|
+
ort = await safeImportOnnx();
|
|
41
45
|
}
|
|
42
46
|
return ort;
|
|
43
47
|
}
|
|
@@ -137,18 +141,71 @@ function l2Normalize(vec: Float32Array): Float32Array {
|
|
|
137
141
|
// Session management
|
|
138
142
|
// =====================================================
|
|
139
143
|
|
|
144
|
+
/** Resolved execution provider for the current session. */
|
|
145
|
+
let resolvedProvider: ExecutionProvider = "cpu";
|
|
146
|
+
|
|
140
147
|
async function initializeSession(modelPath: string): Promise<void> {
|
|
141
148
|
if (session) return;
|
|
142
149
|
|
|
150
|
+
const provider = detectExecutionProvider("DINOv2Worker");
|
|
151
|
+
|
|
152
|
+
// Safety check: if CUDA is requested but LD_LIBRARY_PATH was not set at
|
|
153
|
+
// the OS level (only set via loadEnvLocal at runtime), CUDA init will crash
|
|
154
|
+
// because dlopen() can't find CUDA shared libraries. Fall back to CPU.
|
|
155
|
+
let effectiveProvider = provider;
|
|
156
|
+
if (effectiveProvider === "cuda" && !isLdLibraryPathSetAtOsLevel()) {
|
|
157
|
+
console.warn(
|
|
158
|
+
"[DINOv2Worker] CUDA requested but LD_LIBRARY_PATH not set at OS level. " +
|
|
159
|
+
"dlopen() cannot find CUDA libraries. Falling back to CPU. " +
|
|
160
|
+
"To use CUDA, set LD_LIBRARY_PATH before starting the Node.js process."
|
|
161
|
+
);
|
|
162
|
+
effectiveProvider = "cpu";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sessionOptions =
|
|
166
|
+
effectiveProvider === "cuda"
|
|
167
|
+
? {
|
|
168
|
+
executionProviders: ["CUDAExecutionProvider", "CPUExecutionProvider"],
|
|
169
|
+
enableMemPattern: false,
|
|
170
|
+
}
|
|
171
|
+
: {
|
|
172
|
+
executionProviders: ["cpu"],
|
|
173
|
+
enableCpuMemArena: false,
|
|
174
|
+
enableMemPattern: false,
|
|
175
|
+
};
|
|
176
|
+
|
|
143
177
|
const ortModule = await getOrt();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
|
|
179
|
+
// Attempt to create session with the resolved provider.
|
|
180
|
+
// If CUDA init fails (e.g. library not found), catch and retry with CPU.
|
|
181
|
+
try {
|
|
182
|
+
session = (await ortModule.InferenceSession.create(
|
|
183
|
+
modelPath,
|
|
184
|
+
sessionOptions
|
|
185
|
+
)) as unknown as OrtSession;
|
|
186
|
+
resolvedProvider = effectiveProvider;
|
|
187
|
+
} catch (providerError) {
|
|
188
|
+
if (effectiveProvider !== "cpu") {
|
|
189
|
+
// CUDA init failed — fallback to CPU
|
|
190
|
+
const errorMsg =
|
|
191
|
+
providerError instanceof Error ? providerError.message : String(providerError);
|
|
192
|
+
console.warn(
|
|
193
|
+
"[DINOv2Worker] CUDA session creation failed, falling back to CPU: %s",
|
|
194
|
+
errorMsg
|
|
195
|
+
);
|
|
196
|
+
session = (await ortModule.InferenceSession.create(modelPath, {
|
|
197
|
+
executionProviders: ["cpu"],
|
|
198
|
+
enableCpuMemArena: false,
|
|
199
|
+
enableMemPattern: false,
|
|
200
|
+
})) as unknown as OrtSession;
|
|
201
|
+
resolvedProvider = "cpu";
|
|
202
|
+
} else {
|
|
203
|
+
throw providerError;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
149
206
|
|
|
150
207
|
// eslint-disable-next-line no-console
|
|
151
|
-
console.log("[DINOv2Worker] Session initialized:", modelPath);
|
|
208
|
+
console.log("[DINOv2Worker] Session initialized (provider: %s):", resolvedProvider, modelPath);
|
|
152
209
|
}
|
|
153
210
|
|
|
154
211
|
async function disposeSession(): Promise<void> {
|
|
@@ -187,8 +244,18 @@ async function runInference(
|
|
|
187
244
|
// Create input tensor [1, 3, height, width]
|
|
188
245
|
const inputTensor = new ortModule.Tensor("float32", inputData, [1, 3, height, width]);
|
|
189
246
|
|
|
190
|
-
// Run inference
|
|
191
|
-
|
|
247
|
+
// Run inference — use try/finally to ensure tensor disposal (OOM-FIX-1)
|
|
248
|
+
// ONNX Tensor holds native C++ memory that V8 GC cannot reclaim.
|
|
249
|
+
// Without explicit dispose(), ~1.4MB/inference leaks as native memory.
|
|
250
|
+
let output: Record<string, { data: Float32Array; dims: readonly number[] }>;
|
|
251
|
+
try {
|
|
252
|
+
output = await session.run({ pixel_values: inputTensor });
|
|
253
|
+
} finally {
|
|
254
|
+
// Dispose input tensor immediately after inference completes
|
|
255
|
+
if (typeof (inputTensor as unknown as { dispose?: () => void }).dispose === "function") {
|
|
256
|
+
(inputTensor as unknown as { dispose: () => void }).dispose();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
192
259
|
|
|
193
260
|
// Extract CLS token from last_hidden_state [1, 257, 768]
|
|
194
261
|
const lastHiddenState = output.last_hidden_state;
|
|
@@ -212,6 +279,14 @@ async function runInference(
|
|
|
212
279
|
clsToken[i] = data[i]!;
|
|
213
280
|
}
|
|
214
281
|
|
|
282
|
+
// Dispose output tensors after data extraction (OOM-FIX-1)
|
|
283
|
+
for (const key of Object.keys(output)) {
|
|
284
|
+
const tensor = output[key] as unknown as { dispose?: () => void };
|
|
285
|
+
if (typeof tensor?.dispose === "function") {
|
|
286
|
+
tensor.dispose();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
215
290
|
// L2 normalize with NaN/Infinity defense
|
|
216
291
|
const normalized = l2Normalize(clsToken);
|
|
217
292
|
|
|
@@ -21,6 +21,8 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import type { EmbeddingTextType, EmbeddingServiceConfig, CacheStats } from "./types.js";
|
|
23
23
|
import type { WorkerMessage, WorkerResponse } from "./worker-thread-types.js";
|
|
24
|
+
import { OnnxRuntimeUnavailableError } from "../onnx-availability.js";
|
|
25
|
+
import { getMLWorkerThreadOptions } from "../config/worker-resource-limits.js";
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Disposable pipeline interface (used only in in-process fallback mode).
|
|
@@ -306,7 +308,13 @@ export class EmbeddingService {
|
|
|
306
308
|
// worker_threads Workers, so we must explicitly pass an empty array
|
|
307
309
|
// to prevent the parent's flags (set by WorkerSupervisor fork())
|
|
308
310
|
// from propagating.
|
|
309
|
-
|
|
311
|
+
//
|
|
312
|
+
// PR7e-β1: Add explicit resourceLimits to prevent OOM when ONNX
|
|
313
|
+
// falls back to CPU (onnxruntime-node). Without resourceLimits,
|
|
314
|
+
// V8 old-space defaults to < 4GB and e5-base CPU inference OOMs.
|
|
315
|
+
// See ADR-0012 §3 BLOCKER 4 and config/worker-resource-limits.ts.
|
|
316
|
+
const workerOptions = { execArgv: [], ...getMLWorkerThreadOptions() };
|
|
317
|
+
this.worker = new Worker(scriptPath, workerOptions);
|
|
310
318
|
|
|
311
319
|
// Handle worker messages
|
|
312
320
|
this.worker.on("message", (response: WorkerResponse) => {
|
|
@@ -502,6 +510,10 @@ export class EmbeddingService {
|
|
|
502
510
|
}
|
|
503
511
|
} catch (error) {
|
|
504
512
|
this.initPromise = null;
|
|
513
|
+
const code = (error as { code?: string }).code;
|
|
514
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
515
|
+
throw new OnnxRuntimeUnavailableError("text embedding");
|
|
516
|
+
}
|
|
505
517
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
506
518
|
throw new Error(`Failed to load embedding model: ${message}`);
|
|
507
519
|
}
|
|
@@ -20,10 +20,14 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { parentPort } from "node:worker_threads";
|
|
23
|
-
import { createRequire } from "node:module";
|
|
24
|
-
import fs from "node:fs";
|
|
25
|
-
import nodePath from "node:path";
|
|
26
23
|
import type { WorkerMessage, WorkerResponse, WorkerErrorResponse } from "./worker-thread-types.js";
|
|
24
|
+
import { OnnxRuntimeUnavailableError } from "../onnx-availability.js";
|
|
25
|
+
import {
|
|
26
|
+
detectExecutionProvider,
|
|
27
|
+
verifyCudaAvailability,
|
|
28
|
+
isLdLibraryPathSetAtOsLevel,
|
|
29
|
+
} from "../onnx-provider-detect.js";
|
|
30
|
+
import type { ExecutionProvider } from "../onnx-provider-detect.js";
|
|
27
31
|
|
|
28
32
|
// =====================================================
|
|
29
33
|
// Pipeline types (mirrors service.ts DisposablePipeline)
|
|
@@ -38,83 +42,6 @@ interface DisposablePipeline {
|
|
|
38
42
|
dispose?: () => Promise<unknown[]>;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
// =====================================================
|
|
42
|
-
// GPU execution provider detection
|
|
43
|
-
// =====================================================
|
|
44
|
-
|
|
45
|
-
type ExecutionProvider = "cpu" | "cuda";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Detect the ONNX execution provider based on environment configuration.
|
|
49
|
-
*
|
|
50
|
-
* Checks ONNX_EXECUTION_PROVIDER env var and verifies that the CUDA
|
|
51
|
-
* provider shared library is available on disk. Falls back to CPU
|
|
52
|
-
* gracefully if the provider is not installed.
|
|
53
|
-
*/
|
|
54
|
-
function detectExecutionProvider(): ExecutionProvider {
|
|
55
|
-
const envProvider = process.env.ONNX_EXECUTION_PROVIDER;
|
|
56
|
-
|
|
57
|
-
if (envProvider === "cuda" || envProvider === "rocm") {
|
|
58
|
-
const cudaAvailable = verifyCudaAvailability();
|
|
59
|
-
if (cudaAvailable) {
|
|
60
|
-
return "cuda";
|
|
61
|
-
}
|
|
62
|
-
console.warn(
|
|
63
|
-
"[EmbeddingWorker] ONNX_EXECUTION_PROVIDER=%s but CUDA provider not available, falling back to CPU",
|
|
64
|
-
envProvider
|
|
65
|
-
);
|
|
66
|
-
return "cpu";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return "cpu";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Verify that CUDA provider (libonnxruntime_providers_cuda.so) is available.
|
|
74
|
-
*
|
|
75
|
-
* onnxruntime-node can download the CUDA provider shared library via
|
|
76
|
-
* `ONNXRUNTIME_NODE_INSTALL_CUDA=v12 node .../install.js`. This function
|
|
77
|
-
* checks for its presence on disk rather than trying to resolve a
|
|
78
|
-
* non-existent npm package (onnxruntime-gpu is Python-only).
|
|
79
|
-
*/
|
|
80
|
-
function verifyCudaAvailability(): boolean {
|
|
81
|
-
try {
|
|
82
|
-
const esmRequire = createRequire(import.meta.url);
|
|
83
|
-
const ortNodePath = esmRequire.resolve("onnxruntime-node");
|
|
84
|
-
// require.resolve returns e.g. .../onnxruntime-node/dist/index.js
|
|
85
|
-
// Walk up to package root by finding the directory containing package.json
|
|
86
|
-
let packageDir = nodePath.dirname(ortNodePath);
|
|
87
|
-
for (let i = 0; i < 5; i++) {
|
|
88
|
-
if (fs.existsSync(nodePath.join(packageDir, "package.json"))) break;
|
|
89
|
-
packageDir = nodePath.dirname(packageDir);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Search across napi versions (v3, v6, etc.) for CUDA provider
|
|
93
|
-
const binDir = nodePath.join(packageDir, "bin");
|
|
94
|
-
if (fs.existsSync(binDir)) {
|
|
95
|
-
const napiDirs = fs.readdirSync(binDir).filter((d) => d.startsWith("napi-v"));
|
|
96
|
-
for (const napiDir of napiDirs) {
|
|
97
|
-
const cudaProviderPath = nodePath.join(
|
|
98
|
-
binDir,
|
|
99
|
-
napiDir,
|
|
100
|
-
"linux",
|
|
101
|
-
"x64",
|
|
102
|
-
"libonnxruntime_providers_cuda.so"
|
|
103
|
-
);
|
|
104
|
-
if (fs.existsSync(cudaProviderPath)) {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
console.warn("[EmbeddingWorker] CUDA provider not found in: %s", binDir);
|
|
111
|
-
return false;
|
|
112
|
-
} catch {
|
|
113
|
-
console.warn("[EmbeddingWorker] Cannot verify CUDA provider: onnxruntime-node not found");
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
45
|
// =====================================================
|
|
119
46
|
// Worker state
|
|
120
47
|
// =====================================================
|
|
@@ -146,26 +73,6 @@ function normalizeVector(vector: number[]): number[] {
|
|
|
146
73
|
return vector.map((val) => val / norm);
|
|
147
74
|
}
|
|
148
75
|
|
|
149
|
-
/**
|
|
150
|
-
* Verify that LD_LIBRARY_PATH is set at the OS level (not just process.env).
|
|
151
|
-
*
|
|
152
|
-
* dlopen() reads LD_LIBRARY_PATH from the kernel environment (/proc/self/environ),
|
|
153
|
-
* NOT from process.env modifications made at runtime. If LD_LIBRARY_PATH was set
|
|
154
|
-
* after process startup (e.g. by loadEnvLocal()), dlopen() cannot find CUDA
|
|
155
|
-
* libraries and ONNX Runtime CUDA provider initialization will segfault or throw.
|
|
156
|
-
*
|
|
157
|
-
* Returns true only if LD_LIBRARY_PATH was present in the original process environment.
|
|
158
|
-
*/
|
|
159
|
-
function isLdLibraryPathSetAtOsLevel(): boolean {
|
|
160
|
-
try {
|
|
161
|
-
const procEnv = fs.readFileSync("/proc/self/environ", "utf-8");
|
|
162
|
-
return procEnv.includes("LD_LIBRARY_PATH");
|
|
163
|
-
} catch {
|
|
164
|
-
// /proc/self/environ not available (non-Linux) — assume set
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
76
|
async function initializePipeline(): Promise<void> {
|
|
170
77
|
if (pipeline) return;
|
|
171
78
|
if (initPromise) return initPromise;
|
|
@@ -175,7 +82,7 @@ async function initializePipeline(): Promise<void> {
|
|
|
175
82
|
// Only auto-detect from env on first init. If provider was explicitly
|
|
176
83
|
// set by a switch-provider message, respect that setting.
|
|
177
84
|
if (!providerExplicitlySet) {
|
|
178
|
-
resolvedProvider = detectExecutionProvider();
|
|
85
|
+
resolvedProvider = detectExecutionProvider("EmbeddingWorker");
|
|
179
86
|
}
|
|
180
87
|
|
|
181
88
|
// Safety check: if CUDA is requested but LD_LIBRARY_PATH was not set at
|
|
@@ -236,6 +143,10 @@ async function initializePipeline(): Promise<void> {
|
|
|
236
143
|
console.log("[EmbeddingWorker] ONNX pipeline ready (provider: %s)", resolvedProvider);
|
|
237
144
|
} catch (error) {
|
|
238
145
|
initPromise = null;
|
|
146
|
+
const code = (error as { code?: string }).code;
|
|
147
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
148
|
+
throw new OnnxRuntimeUnavailableError("text embedding");
|
|
149
|
+
}
|
|
239
150
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
240
151
|
throw new Error(`Failed to load embedding model in worker thread: ${message}`);
|
|
241
152
|
}
|
|
@@ -446,7 +357,7 @@ async function handleMessage(message: WorkerMessage): Promise<void> {
|
|
|
446
357
|
|
|
447
358
|
// If switching to CUDA, verify availability first
|
|
448
359
|
if (targetProvider === "cuda") {
|
|
449
|
-
const canUseCuda = verifyCudaAvailability();
|
|
360
|
+
const canUseCuda = verifyCudaAvailability("EmbeddingWorker");
|
|
450
361
|
if (!canUseCuda) {
|
|
451
362
|
// Cannot switch to CUDA — respond with current (cpu) provider
|
|
452
363
|
sendResponse({
|