@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.
Files changed (40) hide show
  1. package/dist/config/worker-resource-limits.d.ts +89 -0
  2. package/dist/config/worker-resource-limits.d.ts.map +1 -0
  3. package/dist/config/worker-resource-limits.js +156 -0
  4. package/dist/config/worker-resource-limits.js.map +1 -0
  5. package/dist/dinov2/service.d.ts +13 -0
  6. package/dist/dinov2/service.d.ts.map +1 -1
  7. package/dist/dinov2/service.js +86 -8
  8. package/dist/dinov2/service.js.map +1 -1
  9. package/dist/dinov2/worker-thread.js +72 -7
  10. package/dist/dinov2/worker-thread.js.map +1 -1
  11. package/dist/embeddings/service.d.ts +19 -0
  12. package/dist/embeddings/service.d.ts.map +1 -1
  13. package/dist/embeddings/service.js +87 -1
  14. package/dist/embeddings/service.js.map +1 -1
  15. package/dist/embeddings/worker-thread-types.d.ts +1 -1
  16. package/dist/embeddings/worker-thread.d.ts.map +1 -1
  17. package/dist/embeddings/worker-thread.js +10 -85
  18. package/dist/embeddings/worker-thread.js.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/onnx-availability.d.ts +29 -0
  24. package/dist/onnx-availability.d.ts.map +1 -0
  25. package/dist/onnx-availability.js +69 -0
  26. package/dist/onnx-availability.js.map +1 -0
  27. package/dist/onnx-provider-detect.d.ts +47 -0
  28. package/dist/onnx-provider-detect.d.ts.map +1 -0
  29. package/dist/onnx-provider-detect.js +111 -0
  30. package/dist/onnx-provider-detect.js.map +1 -0
  31. package/package.json +8 -3
  32. package/src/config/worker-resource-limits.ts +176 -0
  33. package/src/dinov2/service.ts +105 -8
  34. package/src/dinov2/worker-thread.ts +85 -8
  35. package/src/embeddings/service.ts +99 -1
  36. package/src/embeddings/worker-thread-types.ts +1 -1
  37. package/src/embeddings/worker-thread.ts +15 -104
  38. package/src/index.ts +15 -0
  39. package/src/onnx-availability.ts +78 -0
  40. 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.7",
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.7"
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
+ }
@@ -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
- this.worker = new Worker(scriptPath, { execArgv: [] });
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 ortModule = await import("onnxruntime-node");
284
- this.inProcessSession = await ortModule.InferenceSession.create(this.modelPath, {
285
- executionProviders: ["cpu"],
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("[DINOv2] Session initialized in-process:", this.modelPath);
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 import("onnxruntime-node");
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
- const output = await sess.run({ pixel_values: inputTensor });
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.