@reftrixmcp/ml 0.3.0 → 0.6.0
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 +19 -0
- package/dist/embeddings/service.d.ts.map +1 -1
- package/dist/embeddings/service.js +56 -1
- package/dist/embeddings/service.js.map +1 -1
- package/dist/embeddings/worker-thread-device.d.ts +59 -0
- package/dist/embeddings/worker-thread-device.d.ts.map +1 -0
- package/dist/embeddings/worker-thread-device.js +63 -0
- package/dist/embeddings/worker-thread-device.js.map +1 -0
- package/dist/embeddings/worker-thread.d.ts.map +1 -1
- package/dist/embeddings/worker-thread.js +22 -85
- package/dist/embeddings/worker-thread.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -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 -5
- 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 +63 -1
- package/src/embeddings/worker-thread-device.ts +68 -0
- package/src/embeddings/worker-thread.ts +30 -104
- package/src/index.ts +27 -0
- package/src/onnx-availability.ts +78 -0
- package/src/onnx-provider-detect.ts +134 -0
|
@@ -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,9 @@ 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";
|
|
26
|
+
import { detectExecutionProvider, isLdLibraryPathSetAtOsLevel } from "../onnx-provider-detect.js";
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Disposable pipeline interface (used only in in-process fallback mode).
|
|
@@ -306,7 +309,13 @@ export class EmbeddingService {
|
|
|
306
309
|
// worker_threads Workers, so we must explicitly pass an empty array
|
|
307
310
|
// to prevent the parent's flags (set by WorkerSupervisor fork())
|
|
308
311
|
// from propagating.
|
|
309
|
-
|
|
312
|
+
//
|
|
313
|
+
// PR7e-β1: Add explicit resourceLimits to prevent OOM when ONNX
|
|
314
|
+
// falls back to CPU (onnxruntime-node). Without resourceLimits,
|
|
315
|
+
// V8 old-space defaults to < 4GB and e5-base CPU inference OOMs.
|
|
316
|
+
// See ADR-0012 §3 BLOCKER 4 and config/worker-resource-limits.ts.
|
|
317
|
+
const workerOptions = { execArgv: [], ...getMLWorkerThreadOptions() };
|
|
318
|
+
this.worker = new Worker(scriptPath, workerOptions);
|
|
310
319
|
|
|
311
320
|
// Handle worker messages
|
|
312
321
|
this.worker.on("message", (response: WorkerResponse) => {
|
|
@@ -445,10 +454,59 @@ export class EmbeddingService {
|
|
|
445
454
|
* If the configured device is 'cuda' but initialization fails (e.g. because
|
|
446
455
|
* LD_LIBRARY_PATH was not set at the OS level), automatically falls back to CPU.
|
|
447
456
|
*/
|
|
457
|
+
/**
|
|
458
|
+
* Resolve the safe execution device for the in-process pipeline.
|
|
459
|
+
*
|
|
460
|
+
* PR-1 GPU-COORD H regression fix (FIND-IMPL-PR1-H-NEW-01): the Phase 5 fork
|
|
461
|
+
* child's VRAM probe wires `ONNX_EXECUTION_PROVIDER=cuda` when free VRAM is
|
|
462
|
+
* available, but that does NOT guarantee the CUDA EP shared library
|
|
463
|
+
* (`libonnxruntime_providers_cuda.so`) is installed on the host. Previously
|
|
464
|
+
* the in-process path trusted `this.config.device` (set straight from the env
|
|
465
|
+
* var in the constructor) and passed `device:"cuda"` to transformers, which
|
|
466
|
+
* raw-throws on the native dlopen failure when the EP `.so` is absent.
|
|
467
|
+
*
|
|
468
|
+
* This gate mirrors the DINOv2 in-process path (`dinov2/worker-thread.ts`
|
|
469
|
+
* `initializeSession`): it resolves via `detectExecutionProvider` (which calls
|
|
470
|
+
* `verifyCudaAvailability` for an EP `.so` filesystem check) and also requires
|
|
471
|
+
* `LD_LIBRARY_PATH` at the OS level. If either fails, it returns "cpu" so the
|
|
472
|
+
* pipeline is never asked to create a CUDA session it cannot back. When the
|
|
473
|
+
* configured device is not "cuda", the configured device is returned as-is.
|
|
474
|
+
*/
|
|
475
|
+
private resolveInProcessDevice(): "cpu" | "cuda" {
|
|
476
|
+
if (this.config.device !== "cuda") {
|
|
477
|
+
return this.config.device === undefined ? "cpu" : (this.config.device as "cpu" | "cuda");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// CUDA requested — gate on EP `.so` availability and OS-level LD_LIBRARY_PATH.
|
|
481
|
+
const resolved = detectExecutionProvider("EmbeddingService");
|
|
482
|
+
if (resolved === "cuda" && !isLdLibraryPathSetAtOsLevel()) {
|
|
483
|
+
console.warn(
|
|
484
|
+
"[ML] CUDA requested but LD_LIBRARY_PATH not set at OS level. " +
|
|
485
|
+
"dlopen() cannot find CUDA libraries. Falling back to CPU."
|
|
486
|
+
);
|
|
487
|
+
return "cpu";
|
|
488
|
+
}
|
|
489
|
+
if (resolved !== "cuda") {
|
|
490
|
+
console.warn(
|
|
491
|
+
"[ML] CUDA requested but CUDA EP not available (libonnxruntime_providers_cuda.so absent). " +
|
|
492
|
+
"Falling back to CPU."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return resolved;
|
|
496
|
+
}
|
|
497
|
+
|
|
448
498
|
private async initializeInProcess(): Promise<void> {
|
|
449
499
|
if (this.pipeline) return;
|
|
450
500
|
if (this.initPromise) return this.initPromise;
|
|
451
501
|
|
|
502
|
+
// Resolve the safe device BEFORE init so we never pass an unbacked CUDA
|
|
503
|
+
// device to the pipeline (FIND-IMPL-PR1-H-NEW-01 — gate parity with DINOv2).
|
|
504
|
+
const effectiveDevice = this.resolveInProcessDevice();
|
|
505
|
+
if (effectiveDevice !== this.config.device) {
|
|
506
|
+
this.config = { ...this.config, device: effectiveDevice };
|
|
507
|
+
this.currentProvider = effectiveDevice;
|
|
508
|
+
}
|
|
509
|
+
|
|
452
510
|
this.initPromise = (async (): Promise<void> => {
|
|
453
511
|
try {
|
|
454
512
|
if (process.env.NODE_ENV === "development") {
|
|
@@ -502,6 +560,10 @@ export class EmbeddingService {
|
|
|
502
560
|
}
|
|
503
561
|
} catch (error) {
|
|
504
562
|
this.initPromise = null;
|
|
563
|
+
const code = (error as { code?: string }).code;
|
|
564
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
565
|
+
throw new OnnxRuntimeUnavailableError("text embedding");
|
|
566
|
+
}
|
|
505
567
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
506
568
|
throw new Error(`Failed to load embedding model: ${message}`);
|
|
507
569
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
/**
|
|
3
|
+
* Worker-thread ONNX device gate helpers
|
|
4
|
+
*
|
|
5
|
+
* Pure, side-effect-free helpers that decide the effective ONNX execution
|
|
6
|
+
* device for the e5-base embedding worker thread. Extracted from
|
|
7
|
+
* `worker-thread.ts` so the gate logic is directly unit-testable — the worker
|
|
8
|
+
* module itself only runs as a Worker Thread (it requires `parentPort` at
|
|
9
|
+
* import time), so its gate could otherwise not be exercised in-place.
|
|
10
|
+
*
|
|
11
|
+
* The contract mirrors the in-process gate
|
|
12
|
+
* (`service.ts` `resolveInProcessDevice`, FIND-IMPL-PR1-H-NEW-01): the effective
|
|
13
|
+
* device must HONOR the already-resolved provider (the result of
|
|
14
|
+
* `detectExecutionProvider()` + the OS-level `LD_LIBRARY_PATH` check) and must
|
|
15
|
+
* NOT fall back to a raw `config.device` value (which is set straight from the
|
|
16
|
+
* `ONNX_EXECUTION_PROVIDER` env var). Letting `config.device="cuda"` override a
|
|
17
|
+
* gate decision of `"cpu"` passes an unbacked CUDA device to transformers, which
|
|
18
|
+
* raw-throws on the native `dlopen()` failure when the EP `.so` is absent
|
|
19
|
+
* (root-cause decision `019ec56b`).
|
|
20
|
+
*
|
|
21
|
+
* @module embeddings/worker-thread-device
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ExecutionProvider } from "../onnx-provider-detect.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the effective ONNX device for the worker-thread pipeline.
|
|
28
|
+
*
|
|
29
|
+
* `resolvedProvider` is already the result of the init gate
|
|
30
|
+
* (`detectExecutionProvider()` — a CUDA EP `.so` filesystem check — plus the
|
|
31
|
+
* OS-level `LD_LIBRARY_PATH` check in `initializePipeline`). This helper simply
|
|
32
|
+
* honors that decision. It deliberately does NOT consult `config.device`: doing
|
|
33
|
+
* so was the line-97 bug that let a `"cuda"` env override a `"cpu"` gate result.
|
|
34
|
+
*
|
|
35
|
+
* Parity: `service.ts` `resolveInProcessDevice()` likewise returns the gate
|
|
36
|
+
* result (`resolved`), never a raw configured device when CUDA is unbacked.
|
|
37
|
+
*
|
|
38
|
+
* @param resolvedProvider - The provider already resolved by the init gate.
|
|
39
|
+
* @returns The effective device to pass to the transformers pipeline.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveWorkerEffectiveDevice(
|
|
42
|
+
resolvedProvider: ExecutionProvider
|
|
43
|
+
): ExecutionProvider {
|
|
44
|
+
return resolvedProvider;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether a `switch-provider` request to CUDA may proceed.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors the init-time AND gate (`initializePipeline`, line ~88): CUDA may be
|
|
51
|
+
* used only if BOTH the CUDA EP shared library is present
|
|
52
|
+
* (`verifyCudaAvailability`) AND `LD_LIBRARY_PATH` was set at the OS level
|
|
53
|
+
* (`isLdLibraryPathSetAtOsLevel`). Pre-fix, `switch-provider` gated on
|
|
54
|
+
* `verifyCudaAvailability` alone — so a host with the EP `.so` but an unset
|
|
55
|
+
* `LD_LIBRARY_PATH` would let `resolvedProvider="cuda"` through, producing an
|
|
56
|
+
* unbacked CUDA device and a `dlopen()` raw throw (UB-6 init parity).
|
|
57
|
+
*
|
|
58
|
+
* The two checks are passed in as functions so this helper stays pure and
|
|
59
|
+
* deterministically testable (the production caller passes the real
|
|
60
|
+
* `verifyCudaAvailability` / `isLdLibraryPathSetAtOsLevel`).
|
|
61
|
+
*
|
|
62
|
+
* @param verifyCuda - CUDA EP `.so` filesystem availability check.
|
|
63
|
+
* @param isLdSet - OS-level `LD_LIBRARY_PATH` presence check.
|
|
64
|
+
* @returns true only if both checks pass (CUDA switch permitted).
|
|
65
|
+
*/
|
|
66
|
+
export function canSwitchToCuda(verifyCuda: () => boolean, isLdSet: () => boolean): boolean {
|
|
67
|
+
return verifyCuda() && isLdSet();
|
|
68
|
+
}
|
|
@@ -20,10 +20,15 @@
|
|
|
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";
|
|
31
|
+
import { resolveWorkerEffectiveDevice, canSwitchToCuda } from "./worker-thread-device.js";
|
|
27
32
|
|
|
28
33
|
// =====================================================
|
|
29
34
|
// Pipeline types (mirrors service.ts DisposablePipeline)
|
|
@@ -38,83 +43,6 @@ interface DisposablePipeline {
|
|
|
38
43
|
dispose?: () => Promise<unknown[]>;
|
|
39
44
|
}
|
|
40
45
|
|
|
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
46
|
// =====================================================
|
|
119
47
|
// Worker state
|
|
120
48
|
// =====================================================
|
|
@@ -146,26 +74,6 @@ function normalizeVector(vector: number[]): number[] {
|
|
|
146
74
|
return vector.map((val) => val / norm);
|
|
147
75
|
}
|
|
148
76
|
|
|
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
77
|
async function initializePipeline(): Promise<void> {
|
|
170
78
|
if (pipeline) return;
|
|
171
79
|
if (initPromise) return initPromise;
|
|
@@ -175,7 +83,7 @@ async function initializePipeline(): Promise<void> {
|
|
|
175
83
|
// Only auto-detect from env on first init. If provider was explicitly
|
|
176
84
|
// set by a switch-provider message, respect that setting.
|
|
177
85
|
if (!providerExplicitlySet) {
|
|
178
|
-
resolvedProvider = detectExecutionProvider();
|
|
86
|
+
resolvedProvider = detectExecutionProvider("EmbeddingWorker");
|
|
179
87
|
}
|
|
180
88
|
|
|
181
89
|
// Safety check: if CUDA is requested but LD_LIBRARY_PATH was not set at
|
|
@@ -190,7 +98,13 @@ async function initializePipeline(): Promise<void> {
|
|
|
190
98
|
resolvedProvider = "cpu";
|
|
191
99
|
}
|
|
192
100
|
|
|
193
|
-
|
|
101
|
+
// Honor the gate decision. resolvedProvider is already the result of
|
|
102
|
+
// detectExecutionProvider() (CUDA EP .so check) + the LD_LIBRARY_PATH OS-level
|
|
103
|
+
// check above (line 88-95). config.device (set straight from the env var) must
|
|
104
|
+
// NOT override the gate — otherwise a "cuda" env on a host without the EP .so
|
|
105
|
+
// passes device:"cuda" to transformers, which raw-throws on dlopen. Parity with
|
|
106
|
+
// the in-process resolveInProcessDevice() gate (service.ts:472, FIND-IMPL-PR1-H-NEW-01).
|
|
107
|
+
const effectiveDevice: ExecutionProvider = resolveWorkerEffectiveDevice(resolvedProvider);
|
|
194
108
|
|
|
195
109
|
// eslint-disable-next-line no-console
|
|
196
110
|
console.log("[EmbeddingWorker] Initializing ONNX pipeline", {
|
|
@@ -236,6 +150,10 @@ async function initializePipeline(): Promise<void> {
|
|
|
236
150
|
console.log("[EmbeddingWorker] ONNX pipeline ready (provider: %s)", resolvedProvider);
|
|
237
151
|
} catch (error) {
|
|
238
152
|
initPromise = null;
|
|
153
|
+
const code = (error as { code?: string }).code;
|
|
154
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
155
|
+
throw new OnnxRuntimeUnavailableError("text embedding");
|
|
156
|
+
}
|
|
239
157
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
240
158
|
throw new Error(`Failed to load embedding model in worker thread: ${message}`);
|
|
241
159
|
}
|
|
@@ -444,9 +362,17 @@ async function handleMessage(message: WorkerMessage): Promise<void> {
|
|
|
444
362
|
case "switch-provider": {
|
|
445
363
|
const targetProvider = message.provider;
|
|
446
364
|
|
|
447
|
-
// If switching to CUDA, verify availability first
|
|
365
|
+
// If switching to CUDA, verify availability first. Mirror the init-time
|
|
366
|
+
// AND gate (line ~88): CUDA may proceed only if BOTH the EP .so is present
|
|
367
|
+
// AND LD_LIBRARY_PATH was set at the OS level. Gating on
|
|
368
|
+
// verifyCudaAvailability ALONE (the pre-fix gap, UB-6) would let a host
|
|
369
|
+
// with the EP .so but unset LD_LIBRARY_PATH pass device:"cuda" through →
|
|
370
|
+
// dlopen raw throw. canSwitchToCuda ANDs both checks (init parity).
|
|
448
371
|
if (targetProvider === "cuda") {
|
|
449
|
-
const canUseCuda =
|
|
372
|
+
const canUseCuda = canSwitchToCuda(
|
|
373
|
+
() => verifyCudaAvailability("EmbeddingWorker"),
|
|
374
|
+
isLdLibraryPathSetAtOsLevel
|
|
375
|
+
);
|
|
450
376
|
if (!canUseCuda) {
|
|
451
377
|
// Cannot switch to CUDA — respond with current (cpu) provider
|
|
452
378
|
sendResponse({
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,33 @@ export { DINOv2Service, DINOV2_EMBEDDING_DIMENSION, DINOV2_INPUT_SIZE } from "./
|
|
|
49
49
|
export type { DINOv2ServiceConfig } from "./dinov2/index.js";
|
|
50
50
|
export type { DINOv2WorkerMessage, DINOv2WorkerResponse } from "./dinov2/index.js";
|
|
51
51
|
|
|
52
|
+
// ONNX Runtime availability checker
|
|
53
|
+
export {
|
|
54
|
+
isOnnxRuntimeAvailable,
|
|
55
|
+
OnnxRuntimeUnavailableError,
|
|
56
|
+
safeImportOnnx,
|
|
57
|
+
} from "./onnx-availability.js";
|
|
58
|
+
|
|
59
|
+
// ONNX execution provider detection (CUDA EP `.so` availability gate)
|
|
60
|
+
// Exported so the Phase 5 fork-child VRAM probe can gate CUDA selection on the
|
|
61
|
+
// CUDA EP shared-library availability (FIND-IMPL-PR1-H-NEW-01), not just on
|
|
62
|
+
// free VRAM. `verifyCudaAvailability` is a pure filesystem check (no GPU/native
|
|
63
|
+
// init), safe to call from the fork-child leaf path.
|
|
64
|
+
export {
|
|
65
|
+
detectExecutionProvider,
|
|
66
|
+
verifyCudaAvailability,
|
|
67
|
+
isLdLibraryPathSetAtOsLevel,
|
|
68
|
+
} from "./onnx-provider-detect.js";
|
|
69
|
+
export type { ExecutionProvider } from "./onnx-provider-detect.js";
|
|
70
|
+
|
|
71
|
+
// ML Worker Thread resource limits (PR7e-β1)
|
|
72
|
+
export {
|
|
73
|
+
loadMLWorkerResourceLimits,
|
|
74
|
+
getMLWorkerThreadOptions,
|
|
75
|
+
MLWorkerResourceLimitsSchema,
|
|
76
|
+
} from "./config/worker-resource-limits.js";
|
|
77
|
+
export type { MLWorkerResourceLimits } from "./config/worker-resource-limits.js";
|
|
78
|
+
|
|
52
79
|
// Search utilities exports (SearchService removed in v0.1.0)
|
|
53
80
|
export { calculateRRF, mergeWithRRF, normalizeRRFScore, toRankedItems } from "./search/index.js";
|
|
54
81
|
export { executeHybridSearch } from "./search/index.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cached availability checker for onnxruntime-node (optionalDependency).
|
|
6
|
+
* When unavailable, ML features degrade gracefully.
|
|
7
|
+
*
|
|
8
|
+
* @module onnx-availability
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let _available: boolean | null = null;
|
|
12
|
+
let _unavailableReason: string | null = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if onnxruntime-node is available (async, caches result).
|
|
16
|
+
*/
|
|
17
|
+
export async function isOnnxRuntimeAvailable(): Promise<boolean> {
|
|
18
|
+
if (_available !== null) return _available;
|
|
19
|
+
try {
|
|
20
|
+
await import("onnxruntime-node");
|
|
21
|
+
_available = true;
|
|
22
|
+
_unavailableReason = null;
|
|
23
|
+
} catch {
|
|
24
|
+
_available = false;
|
|
25
|
+
_unavailableReason = "onnxruntime-node is not installed";
|
|
26
|
+
}
|
|
27
|
+
return _available;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Synchronous check — returns null if not yet probed.
|
|
32
|
+
*/
|
|
33
|
+
export function isOnnxRuntimeAvailableSync(): boolean | null {
|
|
34
|
+
return _available;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the reason onnxruntime-node is unavailable, or null if available.
|
|
39
|
+
*/
|
|
40
|
+
export function getOnnxUnavailableReason(): string | null {
|
|
41
|
+
return _unavailableReason;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Error thrown when ML features require onnxruntime-node but it is not installed.
|
|
46
|
+
*/
|
|
47
|
+
export class OnnxRuntimeUnavailableError extends Error {
|
|
48
|
+
constructor(context?: string) {
|
|
49
|
+
super(
|
|
50
|
+
`ML features require onnxruntime-node which is not available. ` +
|
|
51
|
+
`Install with: pnpm add onnxruntime-node` +
|
|
52
|
+
(context ? ` (${context})` : "")
|
|
53
|
+
);
|
|
54
|
+
this.name = "OnnxRuntimeUnavailableError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
59
|
+
type OnnxRuntimeModule = typeof import("onnxruntime-node");
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Safely import onnxruntime-node with a sanitized error on failure.
|
|
63
|
+
*/
|
|
64
|
+
export async function safeImportOnnx(): Promise<OnnxRuntimeModule> {
|
|
65
|
+
try {
|
|
66
|
+
return await import("onnxruntime-node");
|
|
67
|
+
} catch {
|
|
68
|
+
throw new OnnxRuntimeUnavailableError();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* For testing: reset cached state.
|
|
74
|
+
*/
|
|
75
|
+
export function _resetForTesting(): void {
|
|
76
|
+
_available = null;
|
|
77
|
+
_unavailableReason = null;
|
|
78
|
+
}
|