@reftrixmcp/ml 0.1.7 → 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 +13 -0
- package/dist/dinov2/service.d.ts.map +1 -1
- package/dist/dinov2/service.js +86 -8
- package/dist/dinov2/service.js.map +1 -1
- package/dist/dinov2/worker-thread.js +72 -7
- package/dist/dinov2/worker-thread.js.map +1 -1
- package/dist/embeddings/service.d.ts +19 -0
- package/dist/embeddings/service.d.ts.map +1 -1
- package/dist/embeddings/service.js +87 -1
- package/dist/embeddings/service.js.map +1 -1
- package/dist/embeddings/worker-thread-types.d.ts +1 -1
- package/dist/embeddings/worker-thread.d.ts.map +1 -1
- package/dist/embeddings/worker-thread.js +10 -85
- 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 +8 -3
- package/src/config/worker-resource-limits.ts +176 -0
- package/src/dinov2/service.ts +105 -8
- package/src/dinov2/worker-thread.ts +85 -8
- package/src/embeddings/service.ts +99 -1
- package/src/embeddings/worker-thread-types.ts +1 -1
- package/src/embeddings/worker-thread.ts +15 -104
- package/src/index.ts +15 -0
- package/src/onnx-availability.ts +78 -0
- package/src/onnx-provider-detect.ts +134 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
/**
|
|
4
|
+
* Cached availability checker for onnxruntime-node (optionalDependency).
|
|
5
|
+
* When unavailable, ML features degrade gracefully.
|
|
6
|
+
*
|
|
7
|
+
* @module onnx-availability
|
|
8
|
+
*/
|
|
9
|
+
let _available = null;
|
|
10
|
+
let _unavailableReason = null;
|
|
11
|
+
/**
|
|
12
|
+
* Check if onnxruntime-node is available (async, caches result).
|
|
13
|
+
*/
|
|
14
|
+
export async function isOnnxRuntimeAvailable() {
|
|
15
|
+
if (_available !== null)
|
|
16
|
+
return _available;
|
|
17
|
+
try {
|
|
18
|
+
await import("onnxruntime-node");
|
|
19
|
+
_available = true;
|
|
20
|
+
_unavailableReason = null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
_available = false;
|
|
24
|
+
_unavailableReason = "onnxruntime-node is not installed";
|
|
25
|
+
}
|
|
26
|
+
return _available;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Synchronous check — returns null if not yet probed.
|
|
30
|
+
*/
|
|
31
|
+
export function isOnnxRuntimeAvailableSync() {
|
|
32
|
+
return _available;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the reason onnxruntime-node is unavailable, or null if available.
|
|
36
|
+
*/
|
|
37
|
+
export function getOnnxUnavailableReason() {
|
|
38
|
+
return _unavailableReason;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when ML features require onnxruntime-node but it is not installed.
|
|
42
|
+
*/
|
|
43
|
+
export class OnnxRuntimeUnavailableError extends Error {
|
|
44
|
+
constructor(context) {
|
|
45
|
+
super(`ML features require onnxruntime-node which is not available. ` +
|
|
46
|
+
`Install with: pnpm add onnxruntime-node` +
|
|
47
|
+
(context ? ` (${context})` : ""));
|
|
48
|
+
this.name = "OnnxRuntimeUnavailableError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Safely import onnxruntime-node with a sanitized error on failure.
|
|
53
|
+
*/
|
|
54
|
+
export async function safeImportOnnx() {
|
|
55
|
+
try {
|
|
56
|
+
return await import("onnxruntime-node");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new OnnxRuntimeUnavailableError();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* For testing: reset cached state.
|
|
64
|
+
*/
|
|
65
|
+
export function _resetForTesting() {
|
|
66
|
+
_available = null;
|
|
67
|
+
_unavailableReason = null;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=onnx-availability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onnx-availability.js","sourceRoot":"","sources":["../src/onnx-availability.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,yCAAyC;AAEzC;;;;;GAKG;AAEH,IAAI,UAAU,GAAmB,IAAI,CAAC;AACtC,IAAI,kBAAkB,GAAkB,IAAI,CAAC;AAE7C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC1C,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACjC,UAAU,GAAG,IAAI,CAAC;QAClB,kBAAkB,GAAG,IAAI,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,UAAU,GAAG,KAAK,CAAC;QACnB,kBAAkB,GAAG,mCAAmC,CAAC;IAC3D,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACpD,YAAY,OAAgB;QAC1B,KAAK,CACH,+DAA+D;YAC7D,yCAAyC;YACzC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CACnC,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC5C,CAAC;CACF;AAKD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,2BAA2B,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,UAAU,GAAG,IAAI,CAAC;IAClB,kBAAkB,GAAG,IAAI,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONNX Execution Provider Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for detecting the ONNX execution provider (CPU vs CUDA)
|
|
5
|
+
* based on environment configuration and CUDA shared library availability.
|
|
6
|
+
*
|
|
7
|
+
* Used by both the e5-base embedding worker thread and the DINOv2 worker thread
|
|
8
|
+
* to avoid code duplication. Extracted from embeddings/worker-thread.ts in v0.4.0.
|
|
9
|
+
*
|
|
10
|
+
* @module onnx-provider-detect
|
|
11
|
+
*/
|
|
12
|
+
export type ExecutionProvider = "cpu" | "cuda";
|
|
13
|
+
/**
|
|
14
|
+
* Detect the ONNX execution provider based on environment configuration.
|
|
15
|
+
*
|
|
16
|
+
* Checks ONNX_EXECUTION_PROVIDER env var and verifies that the CUDA
|
|
17
|
+
* provider shared library is available on disk. Falls back to CPU
|
|
18
|
+
* gracefully if the provider is not installed.
|
|
19
|
+
*
|
|
20
|
+
* @param logPrefix - Log prefix for warning messages (e.g. "EmbeddingWorker", "DINOv2Worker")
|
|
21
|
+
*/
|
|
22
|
+
export declare function detectExecutionProvider(logPrefix?: string): ExecutionProvider;
|
|
23
|
+
/**
|
|
24
|
+
* Verify that CUDA provider (libonnxruntime_providers_cuda.so) is available.
|
|
25
|
+
*
|
|
26
|
+
* onnxruntime-node can download the CUDA provider shared library via
|
|
27
|
+
* `ONNXRUNTIME_NODE_INSTALL_CUDA=v12 node .../install.js`. This function
|
|
28
|
+
* checks for its presence on disk rather than trying to resolve a
|
|
29
|
+
* non-existent npm package (onnxruntime-gpu is Python-only).
|
|
30
|
+
*
|
|
31
|
+
* @param logPrefix - Log prefix for warning messages
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyCudaAvailability(logPrefix?: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Verify that LD_LIBRARY_PATH is set at the OS level (not just process.env).
|
|
36
|
+
*
|
|
37
|
+
* dlopen() reads LD_LIBRARY_PATH from the kernel environment (/proc/self/environ),
|
|
38
|
+
* NOT from process.env modifications made at runtime. If LD_LIBRARY_PATH was set
|
|
39
|
+
* after process startup (e.g. by loadEnvLocal()), dlopen() cannot find CUDA
|
|
40
|
+
* libraries and ONNX Runtime CUDA provider initialization will segfault or throw.
|
|
41
|
+
*
|
|
42
|
+
* Returns true only if LD_LIBRARY_PATH was present in the original process environment.
|
|
43
|
+
*
|
|
44
|
+
* @returns true if LD_LIBRARY_PATH is set at OS level, or if /proc/self/environ is unavailable
|
|
45
|
+
*/
|
|
46
|
+
export declare function isLdLibraryPathSetAtOsLevel(): boolean;
|
|
47
|
+
//# sourceMappingURL=onnx-provider-detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onnx-provider-detect.d.ts","sourceRoot":"","sources":["../src/onnx-provider-detect.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AAUH,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,MAAM,CAAC;AAM/C;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAkB7E;AAMD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAqClE;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAQrD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* ONNX Execution Provider Detection Utilities
|
|
4
|
+
*
|
|
5
|
+
* Shared logic for detecting the ONNX execution provider (CPU vs CUDA)
|
|
6
|
+
* based on environment configuration and CUDA shared library availability.
|
|
7
|
+
*
|
|
8
|
+
* Used by both the e5-base embedding worker thread and the DINOv2 worker thread
|
|
9
|
+
* to avoid code duplication. Extracted from embeddings/worker-thread.ts in v0.4.0.
|
|
10
|
+
*
|
|
11
|
+
* @module onnx-provider-detect
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import nodePath from "node:path";
|
|
16
|
+
// =====================================================
|
|
17
|
+
// Provider Detection
|
|
18
|
+
// =====================================================
|
|
19
|
+
/**
|
|
20
|
+
* Detect the ONNX execution provider based on environment configuration.
|
|
21
|
+
*
|
|
22
|
+
* Checks ONNX_EXECUTION_PROVIDER env var and verifies that the CUDA
|
|
23
|
+
* provider shared library is available on disk. Falls back to CPU
|
|
24
|
+
* gracefully if the provider is not installed.
|
|
25
|
+
*
|
|
26
|
+
* @param logPrefix - Log prefix for warning messages (e.g. "EmbeddingWorker", "DINOv2Worker")
|
|
27
|
+
*/
|
|
28
|
+
export function detectExecutionProvider(logPrefix) {
|
|
29
|
+
const prefix = logPrefix ?? "OnnxProvider";
|
|
30
|
+
const envProvider = process.env.ONNX_EXECUTION_PROVIDER;
|
|
31
|
+
if (envProvider === "cuda" || envProvider === "rocm") {
|
|
32
|
+
const cudaAvailable = verifyCudaAvailability(logPrefix);
|
|
33
|
+
if (cudaAvailable) {
|
|
34
|
+
return "cuda";
|
|
35
|
+
}
|
|
36
|
+
console.warn("[%s] ONNX_EXECUTION_PROVIDER=%s but CUDA provider not available, falling back to CPU", prefix, envProvider);
|
|
37
|
+
return "cpu";
|
|
38
|
+
}
|
|
39
|
+
return "cpu";
|
|
40
|
+
}
|
|
41
|
+
// =====================================================
|
|
42
|
+
// CUDA Availability Verification
|
|
43
|
+
// =====================================================
|
|
44
|
+
/**
|
|
45
|
+
* Verify that CUDA provider (libonnxruntime_providers_cuda.so) is available.
|
|
46
|
+
*
|
|
47
|
+
* onnxruntime-node can download the CUDA provider shared library via
|
|
48
|
+
* `ONNXRUNTIME_NODE_INSTALL_CUDA=v12 node .../install.js`. This function
|
|
49
|
+
* checks for its presence on disk rather than trying to resolve a
|
|
50
|
+
* non-existent npm package (onnxruntime-gpu is Python-only).
|
|
51
|
+
*
|
|
52
|
+
* @param logPrefix - Log prefix for warning messages
|
|
53
|
+
*/
|
|
54
|
+
export function verifyCudaAvailability(logPrefix) {
|
|
55
|
+
const prefix = logPrefix ?? "OnnxProvider";
|
|
56
|
+
try {
|
|
57
|
+
const esmRequire = createRequire(import.meta.url);
|
|
58
|
+
const ortNodePath = esmRequire.resolve("onnxruntime-node");
|
|
59
|
+
// require.resolve returns e.g. .../onnxruntime-node/dist/index.js
|
|
60
|
+
// Walk up to package root by finding the directory containing package.json
|
|
61
|
+
let packageDir = nodePath.dirname(ortNodePath);
|
|
62
|
+
for (let i = 0; i < 5; i++) {
|
|
63
|
+
if (fs.existsSync(nodePath.join(packageDir, "package.json")))
|
|
64
|
+
break;
|
|
65
|
+
packageDir = nodePath.dirname(packageDir);
|
|
66
|
+
}
|
|
67
|
+
// Search across napi versions (v3, v6, etc.) for CUDA provider
|
|
68
|
+
const binDir = nodePath.join(packageDir, "bin");
|
|
69
|
+
if (fs.existsSync(binDir)) {
|
|
70
|
+
const napiDirs = fs.readdirSync(binDir).filter((d) => d.startsWith("napi-v"));
|
|
71
|
+
for (const napiDir of napiDirs) {
|
|
72
|
+
const cudaProviderPath = nodePath.join(binDir, napiDir, "linux", "x64", "libonnxruntime_providers_cuda.so");
|
|
73
|
+
if (fs.existsSync(cudaProviderPath)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.warn("[%s] CUDA provider not found in: %s", prefix, binDir);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
console.warn("[%s] Cannot verify CUDA provider: onnxruntime-node not found", prefix);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// =====================================================
|
|
87
|
+
// LD_LIBRARY_PATH Check
|
|
88
|
+
// =====================================================
|
|
89
|
+
/**
|
|
90
|
+
* Verify that LD_LIBRARY_PATH is set at the OS level (not just process.env).
|
|
91
|
+
*
|
|
92
|
+
* dlopen() reads LD_LIBRARY_PATH from the kernel environment (/proc/self/environ),
|
|
93
|
+
* NOT from process.env modifications made at runtime. If LD_LIBRARY_PATH was set
|
|
94
|
+
* after process startup (e.g. by loadEnvLocal()), dlopen() cannot find CUDA
|
|
95
|
+
* libraries and ONNX Runtime CUDA provider initialization will segfault or throw.
|
|
96
|
+
*
|
|
97
|
+
* Returns true only if LD_LIBRARY_PATH was present in the original process environment.
|
|
98
|
+
*
|
|
99
|
+
* @returns true if LD_LIBRARY_PATH is set at OS level, or if /proc/self/environ is unavailable
|
|
100
|
+
*/
|
|
101
|
+
export function isLdLibraryPathSetAtOsLevel() {
|
|
102
|
+
try {
|
|
103
|
+
const procEnv = fs.readFileSync("/proc/self/environ", "utf-8");
|
|
104
|
+
return procEnv.includes("LD_LIBRARY_PATH");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// /proc/self/environ not available (non-Linux) — assume set
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=onnx-provider-detect.js.map
|
|
@@ -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.1
|
|
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,21 @@
|
|
|
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.1
|
|
44
|
+
"@reftrixmcp/config": "0.5.1"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
43
48
|
},
|
|
44
49
|
"repository": {
|
|
45
50
|
"type": "git",
|
|
@@ -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,16 +289,67 @@ export class DINOv2Service {
|
|
|
280
289
|
// =====================================================
|
|
281
290
|
|
|
282
291
|
private async initializeInProcess(): Promise<void> {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
|
|
287
343
|
// eslint-disable-next-line no-console
|
|
288
|
-
console.log(
|
|
344
|
+
console.log(
|
|
345
|
+
"[DINOv2] Session initialized in-process (provider: %s):",
|
|
346
|
+
effectiveProvider,
|
|
347
|
+
this.modelPath
|
|
348
|
+
);
|
|
289
349
|
}
|
|
290
350
|
|
|
291
351
|
private async generateInProcess(imageBuffer: Buffer): Promise<number[]> {
|
|
292
|
-
const ortModule = await
|
|
352
|
+
const ortModule = await safeImportOnnx();
|
|
293
353
|
|
|
294
354
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
355
|
const sess = this.inProcessSession as any;
|
|
@@ -317,7 +377,18 @@ export class DINOv2Service {
|
|
|
317
377
|
}
|
|
318
378
|
|
|
319
379
|
const inputTensor = new ortModule.Tensor("float32", float32, [1, 3, height, width]);
|
|
320
|
-
|
|
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
|
+
}
|
|
321
392
|
|
|
322
393
|
const lastHiddenState = output.last_hidden_state;
|
|
323
394
|
if (!lastHiddenState) {
|
|
@@ -332,6 +403,15 @@ export class DINOv2Service {
|
|
|
332
403
|
clsToken[i] = data[i]!;
|
|
333
404
|
}
|
|
334
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
|
+
|
|
335
415
|
// L2 normalize
|
|
336
416
|
let norm = 0;
|
|
337
417
|
for (let i = 0; i < clsToken.length; i++) {
|
|
@@ -469,6 +549,23 @@ export class DINOv2Service {
|
|
|
469
549
|
this._isInitialized = false;
|
|
470
550
|
}
|
|
471
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Recycle the ONNX session to free accumulated native memory.
|
|
554
|
+
*
|
|
555
|
+
* Performs dispose() followed by initialize() to reset the ONNX Runtime
|
|
556
|
+
* internal memory allocations. The modelPath is reused from the original
|
|
557
|
+
* configuration (DINO-3: no re-read of process.env["DINOV2_MODEL_PATH"]).
|
|
558
|
+
*
|
|
559
|
+
* If re-initialization fails, the service is left in a disposed state.
|
|
560
|
+
* Callers should handle this gracefully (Graceful Degradation).
|
|
561
|
+
*
|
|
562
|
+
* @throws {Error} If re-initialization fails after disposal
|
|
563
|
+
*/
|
|
564
|
+
async recycle(): Promise<void> {
|
|
565
|
+
await this.dispose();
|
|
566
|
+
await this.initialize();
|
|
567
|
+
}
|
|
568
|
+
|
|
472
569
|
/**
|
|
473
570
|
* Terminate the worker thread completely and clean up.
|
|
474
571
|
* After this, the service cannot be used until a new instance is created.
|