ruvector 0.2.32 → 0.2.34
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/LICENSE +21 -21
- package/README.md +2303 -2302
- package/bin/cli.js +10257 -10237
- package/bin/mcp-policy.js +95 -95
- package/bin/mcp-server.js +4066 -4054
- package/dist/core/onnx/bundled-parallel.mjs +169 -169
- package/dist/core/onnx/embed-worker.mjs +67 -67
- package/dist/core/onnx/loader.js +485 -461
- package/dist/core/onnx/package.json +3 -3
- package/dist/core/onnx/pkg/LICENSE +21 -21
- package/dist/core/onnx/pkg/loader.js +348 -348
- package/dist/core/onnx/pkg/package.json +3 -3
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.d.ts +112 -112
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.js +5 -5
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.js +638 -638
- package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.wasm.d.ts +29 -29
- package/dist/core/parallel-workers.js +439 -439
- package/dist/workers/benchmark.js +15 -15
- package/package.json +124 -123
- package/src/decompiler/api-prober.js +302 -302
- package/src/decompiler/index.js +463 -463
- package/src/decompiler/metrics.js +86 -86
- package/src/decompiler/model-decompiler.js +423 -423
- package/src/decompiler/module-splitter.js +498 -498
- package/src/decompiler/module-tree.js +142 -142
- package/src/decompiler/name-predictor.js +400 -400
- package/src/decompiler/npm-fetch.js +176 -176
- package/src/decompiler/reconstructor.js +499 -499
- package/src/decompiler/reference-tracker.js +285 -285
- package/src/decompiler/statement-parser.js +285 -285
- package/src/decompiler/style-improver.js +438 -438
- package/src/decompiler/subcategories.js +339 -339
- package/src/decompiler/validator.js +379 -379
- package/src/decompiler/witness.js +140 -140
- package/src/optimizer/context.js +149 -0
- package/src/optimizer/index.js +177 -0
- package/src/optimizer/settings-generator.js +176 -0
- package/src/optimizer/tool-schemas.js +190 -0
- package/wasm/package.json +26 -26
- package/wasm/ruvector_decompiler_wasm.d.ts +27 -27
- package/wasm/ruvector_decompiler_wasm.js +220 -220
- package/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts +16 -16
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bundled-WASM parallel embedder (issue #523 SOTA).
|
|
3
|
-
*
|
|
4
|
-
* A self-contained worker_threads pool — NO external dependency — that shards
|
|
5
|
-
* batches of text across CPU cores, each worker running the bundled ONNX WASM
|
|
6
|
-
* embedder over the SAME model bytes (shared via SharedArrayBuffer) and config.
|
|
7
|
-
* Output vectors are identical to the single-thread path (cosine-equivalent),
|
|
8
|
-
* so this is a pure throughput optimization with no quality change.
|
|
9
|
-
*
|
|
10
|
-
* Drop-in shape compatible with the optional `ruvector-onnx-embeddings-wasm/parallel`
|
|
11
|
-
* package: { numWorkers, dimension, init(), embedBatch(texts) -> number[][], shutdown() }.
|
|
12
|
-
*/
|
|
13
|
-
import { Worker } from 'node:worker_threads';
|
|
14
|
-
import * as os from 'node:os';
|
|
15
|
-
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import * as path from 'node:path';
|
|
17
|
-
|
|
18
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
|
|
20
|
-
export class ParallelEmbedder {
|
|
21
|
-
/**
|
|
22
|
-
* @param {object} opts
|
|
23
|
-
* @param {Uint8Array} opts.modelBytes raw ONNX model bytes (loaded once by caller)
|
|
24
|
-
* @param {string} opts.tokenizerJson
|
|
25
|
-
* @param {number} [opts.maxLength=256]
|
|
26
|
-
* @param {number} [opts.dimension=384]
|
|
27
|
-
* @param {number} [opts.numWorkers] defaults to min(cpus-2, 16), >=2
|
|
28
|
-
*/
|
|
29
|
-
constructor(opts = {}) {
|
|
30
|
-
this.numWorkers = opts.numWorkers || Math.max(2, Math.min((os.cpus().length || 4) - 2, 16));
|
|
31
|
-
this.dimension = opts.dimension || 384;
|
|
32
|
-
this._modelBytes = opts.modelBytes;
|
|
33
|
-
this._tokenizerJson = opts.tokenizerJson;
|
|
34
|
-
this._maxLength = opts.maxLength || 256;
|
|
35
|
-
this._requestTimeoutMs = opts.requestTimeoutMs ?? 30000;
|
|
36
|
-
this._workers = [];
|
|
37
|
-
this._pending = new Map(); // id -> { resolve, reject, worker, timer }
|
|
38
|
-
this._seq = 0;
|
|
39
|
-
this._shuttingDown = false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async init() {
|
|
43
|
-
if (!this._modelBytes || !this._tokenizerJson) {
|
|
44
|
-
throw new Error('ParallelEmbedder requires modelBytes and tokenizerJson');
|
|
45
|
-
}
|
|
46
|
-
// Share model bytes across all workers via a single SharedArrayBuffer.
|
|
47
|
-
const sab = new SharedArrayBuffer(this._modelBytes.length);
|
|
48
|
-
new Uint8Array(sab).set(this._modelBytes);
|
|
49
|
-
|
|
50
|
-
const workerUrl = new URL('./embed-worker.mjs', import.meta.url);
|
|
51
|
-
const readies = [];
|
|
52
|
-
|
|
53
|
-
for (let i = 0; i < this.numWorkers; i++) {
|
|
54
|
-
const w = new Worker(workerUrl, {
|
|
55
|
-
workerData: { modelSab: sab, tokenizerJson: this._tokenizerJson, maxLength: this._maxLength },
|
|
56
|
-
});
|
|
57
|
-
w.on('message', (m) => this._onMessage(m));
|
|
58
|
-
// If a worker dies (uncaught error or unexpected exit), fail every request
|
|
59
|
-
// currently routed to it instead of letting those promises hang forever.
|
|
60
|
-
w.on('error', (e) => this._failWorker(w, e instanceof Error ? e : new Error(String(e))));
|
|
61
|
-
w.on('exit', (code) => {
|
|
62
|
-
if (!this._shuttingDown && code !== 0) {
|
|
63
|
-
this._failWorker(w, new Error(`embed worker exited unexpectedly (code ${code})`));
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
this._workers.push(w);
|
|
67
|
-
readies.push(new Promise((resolve, reject) => {
|
|
68
|
-
const onReady = (m) => {
|
|
69
|
-
if (m.type === 'ready') { cleanup(); resolve(); }
|
|
70
|
-
else if (m.type === 'init-error') { cleanup(); reject(new Error('worker init failed: ' + m.error)); }
|
|
71
|
-
};
|
|
72
|
-
const onErr = (e) => { cleanup(); reject(e); };
|
|
73
|
-
const cleanup = () => { w.off('message', onReady); w.off('error', onErr); };
|
|
74
|
-
w.on('message', onReady);
|
|
75
|
-
w.once('error', onErr);
|
|
76
|
-
}));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await Promise.all(readies);
|
|
80
|
-
// Drop the main-thread reference; the SAB keeps the shared copy alive.
|
|
81
|
-
this._modelBytes = null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
_settle(id, fn) {
|
|
85
|
-
const p = this._pending.get(id);
|
|
86
|
-
if (!p) return;
|
|
87
|
-
this._pending.delete(id);
|
|
88
|
-
if (p.timer) clearTimeout(p.timer);
|
|
89
|
-
fn(p);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
_onMessage(m) {
|
|
93
|
-
if (m.type !== 'result' && m.type !== 'error') return;
|
|
94
|
-
this._settle(m.id, (p) => {
|
|
95
|
-
if (m.type === 'error') p.reject(new Error(m.error));
|
|
96
|
-
else p.resolve({ dim: m.dim, count: m.count, flat: new Float32Array(m.buffer) });
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Reject every in-flight request routed to a dead worker. */
|
|
101
|
-
_failWorker(worker, err) {
|
|
102
|
-
for (const [id, p] of this._pending) {
|
|
103
|
-
if (p.worker === worker) this._settle(id, () => p.reject(err));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
_send(worker, texts) {
|
|
108
|
-
const id = ++this._seq;
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
const entry = { resolve, reject, worker, timer: null };
|
|
111
|
-
if (this._requestTimeoutMs > 0) {
|
|
112
|
-
entry.timer = setTimeout(() => {
|
|
113
|
-
this._settle(id, () =>
|
|
114
|
-
reject(new Error(`embed request timed out after ${this._requestTimeoutMs}ms`)));
|
|
115
|
-
}, this._requestTimeoutMs);
|
|
116
|
-
// Don't keep the event loop alive solely for this timer.
|
|
117
|
-
if (typeof entry.timer.unref === 'function') entry.timer.unref();
|
|
118
|
-
}
|
|
119
|
-
this._pending.set(id, entry);
|
|
120
|
-
worker.postMessage({ type: 'embed', id, texts });
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Embed many texts across workers. Returns number[][] in input order.
|
|
126
|
-
*
|
|
127
|
-
* Texts are dispatched in bounded chunks (default 8) that workers pull as
|
|
128
|
-
* they finish (work-stealing), rather than one giant shard per worker:
|
|
129
|
-
* a large bulk batch (ADR-210 D3 ingest) would otherwise exceed the
|
|
130
|
-
* per-request timeout (~400ms/text in WASM x hundreds of texts), and a
|
|
131
|
-
* single slow worker would gate the whole batch.
|
|
132
|
-
*/
|
|
133
|
-
async embedBatch(texts, opts = {}) {
|
|
134
|
-
if (!texts || texts.length === 0) return [];
|
|
135
|
-
const chunkSize = Math.max(1, opts.chunkSize ?? 8);
|
|
136
|
-
const chunks = [];
|
|
137
|
-
for (let start = 0; start < texts.length; start += chunkSize) {
|
|
138
|
-
chunks.push({ start, texts: texts.slice(start, start + chunkSize) });
|
|
139
|
-
}
|
|
140
|
-
const out = new Array(texts.length);
|
|
141
|
-
let next = 0;
|
|
142
|
-
const drain = async (worker) => {
|
|
143
|
-
for (;;) {
|
|
144
|
-
const idx = next++;
|
|
145
|
-
if (idx >= chunks.length) return;
|
|
146
|
-
const { start, texts: chunkTexts } = chunks[idx];
|
|
147
|
-
const { dim, count, flat } = await this._send(worker, chunkTexts);
|
|
148
|
-
for (let j = 0; j < count; j++) {
|
|
149
|
-
out[start + j] = Array.from(flat.subarray(j * dim, (j + 1) * dim));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
await Promise.all(this._workers.map(drain));
|
|
154
|
-
return out;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async shutdown() {
|
|
158
|
-
this._shuttingDown = true;
|
|
159
|
-
// Reject anything still in flight so callers don't hang on shutdown.
|
|
160
|
-
for (const [id, p] of this._pending) {
|
|
161
|
-
this._settle(id, () => p.reject(new Error('ParallelEmbedder shut down')));
|
|
162
|
-
}
|
|
163
|
-
const ws = this._workers;
|
|
164
|
-
this._workers = [];
|
|
165
|
-
await Promise.all(ws.map((w) => w.terminate()));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export default ParallelEmbedder;
|
|
1
|
+
/**
|
|
2
|
+
* Bundled-WASM parallel embedder (issue #523 SOTA).
|
|
3
|
+
*
|
|
4
|
+
* A self-contained worker_threads pool — NO external dependency — that shards
|
|
5
|
+
* batches of text across CPU cores, each worker running the bundled ONNX WASM
|
|
6
|
+
* embedder over the SAME model bytes (shared via SharedArrayBuffer) and config.
|
|
7
|
+
* Output vectors are identical to the single-thread path (cosine-equivalent),
|
|
8
|
+
* so this is a pure throughput optimization with no quality change.
|
|
9
|
+
*
|
|
10
|
+
* Drop-in shape compatible with the optional `ruvector-onnx-embeddings-wasm/parallel`
|
|
11
|
+
* package: { numWorkers, dimension, init(), embedBatch(texts) -> number[][], shutdown() }.
|
|
12
|
+
*/
|
|
13
|
+
import { Worker } from 'node:worker_threads';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
export class ParallelEmbedder {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {Uint8Array} opts.modelBytes raw ONNX model bytes (loaded once by caller)
|
|
24
|
+
* @param {string} opts.tokenizerJson
|
|
25
|
+
* @param {number} [opts.maxLength=256]
|
|
26
|
+
* @param {number} [opts.dimension=384]
|
|
27
|
+
* @param {number} [opts.numWorkers] defaults to min(cpus-2, 16), >=2
|
|
28
|
+
*/
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
this.numWorkers = opts.numWorkers || Math.max(2, Math.min((os.cpus().length || 4) - 2, 16));
|
|
31
|
+
this.dimension = opts.dimension || 384;
|
|
32
|
+
this._modelBytes = opts.modelBytes;
|
|
33
|
+
this._tokenizerJson = opts.tokenizerJson;
|
|
34
|
+
this._maxLength = opts.maxLength || 256;
|
|
35
|
+
this._requestTimeoutMs = opts.requestTimeoutMs ?? 30000;
|
|
36
|
+
this._workers = [];
|
|
37
|
+
this._pending = new Map(); // id -> { resolve, reject, worker, timer }
|
|
38
|
+
this._seq = 0;
|
|
39
|
+
this._shuttingDown = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async init() {
|
|
43
|
+
if (!this._modelBytes || !this._tokenizerJson) {
|
|
44
|
+
throw new Error('ParallelEmbedder requires modelBytes and tokenizerJson');
|
|
45
|
+
}
|
|
46
|
+
// Share model bytes across all workers via a single SharedArrayBuffer.
|
|
47
|
+
const sab = new SharedArrayBuffer(this._modelBytes.length);
|
|
48
|
+
new Uint8Array(sab).set(this._modelBytes);
|
|
49
|
+
|
|
50
|
+
const workerUrl = new URL('./embed-worker.mjs', import.meta.url);
|
|
51
|
+
const readies = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < this.numWorkers; i++) {
|
|
54
|
+
const w = new Worker(workerUrl, {
|
|
55
|
+
workerData: { modelSab: sab, tokenizerJson: this._tokenizerJson, maxLength: this._maxLength },
|
|
56
|
+
});
|
|
57
|
+
w.on('message', (m) => this._onMessage(m));
|
|
58
|
+
// If a worker dies (uncaught error or unexpected exit), fail every request
|
|
59
|
+
// currently routed to it instead of letting those promises hang forever.
|
|
60
|
+
w.on('error', (e) => this._failWorker(w, e instanceof Error ? e : new Error(String(e))));
|
|
61
|
+
w.on('exit', (code) => {
|
|
62
|
+
if (!this._shuttingDown && code !== 0) {
|
|
63
|
+
this._failWorker(w, new Error(`embed worker exited unexpectedly (code ${code})`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this._workers.push(w);
|
|
67
|
+
readies.push(new Promise((resolve, reject) => {
|
|
68
|
+
const onReady = (m) => {
|
|
69
|
+
if (m.type === 'ready') { cleanup(); resolve(); }
|
|
70
|
+
else if (m.type === 'init-error') { cleanup(); reject(new Error('worker init failed: ' + m.error)); }
|
|
71
|
+
};
|
|
72
|
+
const onErr = (e) => { cleanup(); reject(e); };
|
|
73
|
+
const cleanup = () => { w.off('message', onReady); w.off('error', onErr); };
|
|
74
|
+
w.on('message', onReady);
|
|
75
|
+
w.once('error', onErr);
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await Promise.all(readies);
|
|
80
|
+
// Drop the main-thread reference; the SAB keeps the shared copy alive.
|
|
81
|
+
this._modelBytes = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_settle(id, fn) {
|
|
85
|
+
const p = this._pending.get(id);
|
|
86
|
+
if (!p) return;
|
|
87
|
+
this._pending.delete(id);
|
|
88
|
+
if (p.timer) clearTimeout(p.timer);
|
|
89
|
+
fn(p);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_onMessage(m) {
|
|
93
|
+
if (m.type !== 'result' && m.type !== 'error') return;
|
|
94
|
+
this._settle(m.id, (p) => {
|
|
95
|
+
if (m.type === 'error') p.reject(new Error(m.error));
|
|
96
|
+
else p.resolve({ dim: m.dim, count: m.count, flat: new Float32Array(m.buffer) });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Reject every in-flight request routed to a dead worker. */
|
|
101
|
+
_failWorker(worker, err) {
|
|
102
|
+
for (const [id, p] of this._pending) {
|
|
103
|
+
if (p.worker === worker) this._settle(id, () => p.reject(err));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_send(worker, texts) {
|
|
108
|
+
const id = ++this._seq;
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const entry = { resolve, reject, worker, timer: null };
|
|
111
|
+
if (this._requestTimeoutMs > 0) {
|
|
112
|
+
entry.timer = setTimeout(() => {
|
|
113
|
+
this._settle(id, () =>
|
|
114
|
+
reject(new Error(`embed request timed out after ${this._requestTimeoutMs}ms`)));
|
|
115
|
+
}, this._requestTimeoutMs);
|
|
116
|
+
// Don't keep the event loop alive solely for this timer.
|
|
117
|
+
if (typeof entry.timer.unref === 'function') entry.timer.unref();
|
|
118
|
+
}
|
|
119
|
+
this._pending.set(id, entry);
|
|
120
|
+
worker.postMessage({ type: 'embed', id, texts });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Embed many texts across workers. Returns number[][] in input order.
|
|
126
|
+
*
|
|
127
|
+
* Texts are dispatched in bounded chunks (default 8) that workers pull as
|
|
128
|
+
* they finish (work-stealing), rather than one giant shard per worker:
|
|
129
|
+
* a large bulk batch (ADR-210 D3 ingest) would otherwise exceed the
|
|
130
|
+
* per-request timeout (~400ms/text in WASM x hundreds of texts), and a
|
|
131
|
+
* single slow worker would gate the whole batch.
|
|
132
|
+
*/
|
|
133
|
+
async embedBatch(texts, opts = {}) {
|
|
134
|
+
if (!texts || texts.length === 0) return [];
|
|
135
|
+
const chunkSize = Math.max(1, opts.chunkSize ?? 8);
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (let start = 0; start < texts.length; start += chunkSize) {
|
|
138
|
+
chunks.push({ start, texts: texts.slice(start, start + chunkSize) });
|
|
139
|
+
}
|
|
140
|
+
const out = new Array(texts.length);
|
|
141
|
+
let next = 0;
|
|
142
|
+
const drain = async (worker) => {
|
|
143
|
+
for (;;) {
|
|
144
|
+
const idx = next++;
|
|
145
|
+
if (idx >= chunks.length) return;
|
|
146
|
+
const { start, texts: chunkTexts } = chunks[idx];
|
|
147
|
+
const { dim, count, flat } = await this._send(worker, chunkTexts);
|
|
148
|
+
for (let j = 0; j < count; j++) {
|
|
149
|
+
out[start + j] = Array.from(flat.subarray(j * dim, (j + 1) * dim));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
await Promise.all(this._workers.map(drain));
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async shutdown() {
|
|
158
|
+
this._shuttingDown = true;
|
|
159
|
+
// Reject anything still in flight so callers don't hang on shutdown.
|
|
160
|
+
for (const [id, p] of this._pending) {
|
|
161
|
+
this._settle(id, () => p.reject(new Error('ParallelEmbedder shut down')));
|
|
162
|
+
}
|
|
163
|
+
const ws = this._workers;
|
|
164
|
+
this._workers = [];
|
|
165
|
+
await Promise.all(ws.map((w) => w.terminate()));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default ParallelEmbedder;
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker-thread entry for the bundled-WASM parallel embedder (issue #523 SOTA).
|
|
3
|
-
*
|
|
4
|
-
* Each worker loads its own instance of the bundled ONNX WASM embedder from the
|
|
5
|
-
* SAME model bytes (shared via SharedArrayBuffer — no per-worker download) and
|
|
6
|
-
* the SAME config, so the vectors it produces are identical to the single-thread
|
|
7
|
-
* path (cosine-equivalent by construction).
|
|
8
|
-
*
|
|
9
|
-
* Protocol:
|
|
10
|
-
* workerData: { modelSab: SharedArrayBuffer, tokenizerJson: string, maxLength: number }
|
|
11
|
-
* → posts { type: 'ready' } once the WASM embedder is constructed
|
|
12
|
-
* message { type: 'embed', id, texts: string[] }
|
|
13
|
-
* → posts { type: 'result', id, dim, count, buffer } (Float32Array buffer, transferred)
|
|
14
|
-
* errors → { type: 'error', id, error }
|
|
15
|
-
*/
|
|
16
|
-
import { parentPort, workerData } from 'node:worker_threads';
|
|
17
|
-
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
18
|
-
import * as path from 'node:path';
|
|
19
|
-
import * as fs from 'node:fs';
|
|
20
|
-
|
|
21
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
|
|
23
|
-
let embedder = null;
|
|
24
|
-
|
|
25
|
-
async function init() {
|
|
26
|
-
const bgJsPath = path.join(__dirname, 'pkg', 'ruvector_onnx_embeddings_wasm_bg.js');
|
|
27
|
-
const wasmPath = path.join(__dirname, 'pkg', 'ruvector_onnx_embeddings_wasm_bg.wasm');
|
|
28
|
-
|
|
29
|
-
const wasmModule = await import(pathToFileURL(bgJsPath).href);
|
|
30
|
-
const wasmBytes = fs.readFileSync(wasmPath);
|
|
31
|
-
const wasmResult = await WebAssembly.instantiate(wasmBytes, {
|
|
32
|
-
'./ruvector_onnx_embeddings_wasm_bg.js': wasmModule,
|
|
33
|
-
});
|
|
34
|
-
const wasmExports = wasmResult.instance.exports;
|
|
35
|
-
if (typeof wasmModule.__wbg_set_wasm === 'function') wasmModule.__wbg_set_wasm(wasmExports);
|
|
36
|
-
if (typeof wasmExports.__wbindgen_start === 'function') wasmExports.__wbindgen_start();
|
|
37
|
-
|
|
38
|
-
// Reconstruct model bytes from the shared buffer (zero-copy view, then handed
|
|
39
|
-
// to wasm-bindgen which copies into WASM linear memory).
|
|
40
|
-
const modelBytes = new Uint8Array(workerData.modelSab);
|
|
41
|
-
|
|
42
|
-
const cfg = new wasmModule.WasmEmbedderConfig()
|
|
43
|
-
.setMaxLength(workerData.maxLength || 256)
|
|
44
|
-
.setNormalize(true)
|
|
45
|
-
.setPooling(0); // Mean pooling — matches the single-thread path.
|
|
46
|
-
|
|
47
|
-
embedder = wasmModule.WasmEmbedder.withConfig(modelBytes, workerData.tokenizerJson, cfg);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
parentPort.on('message', (msg) => {
|
|
51
|
-
if (msg.type !== 'embed') return;
|
|
52
|
-
try {
|
|
53
|
-
const dim = embedder.dimension();
|
|
54
|
-
const flat = embedder.embedBatch(msg.texts); // length = texts.length * dim
|
|
55
|
-
const arr = Float32Array.from(flat);
|
|
56
|
-
parentPort.postMessage(
|
|
57
|
-
{ type: 'result', id: msg.id, dim, count: msg.texts.length, buffer: arr.buffer },
|
|
58
|
-
[arr.buffer],
|
|
59
|
-
);
|
|
60
|
-
} catch (e) {
|
|
61
|
-
parentPort.postMessage({ type: 'error', id: msg.id, error: e?.message || String(e) });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
init()
|
|
66
|
-
.then(() => parentPort.postMessage({ type: 'ready' }))
|
|
67
|
-
.catch((e) => parentPort.postMessage({ type: 'init-error', error: e?.message || String(e) }));
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry for the bundled-WASM parallel embedder (issue #523 SOTA).
|
|
3
|
+
*
|
|
4
|
+
* Each worker loads its own instance of the bundled ONNX WASM embedder from the
|
|
5
|
+
* SAME model bytes (shared via SharedArrayBuffer — no per-worker download) and
|
|
6
|
+
* the SAME config, so the vectors it produces are identical to the single-thread
|
|
7
|
+
* path (cosine-equivalent by construction).
|
|
8
|
+
*
|
|
9
|
+
* Protocol:
|
|
10
|
+
* workerData: { modelSab: SharedArrayBuffer, tokenizerJson: string, maxLength: number }
|
|
11
|
+
* → posts { type: 'ready' } once the WASM embedder is constructed
|
|
12
|
+
* message { type: 'embed', id, texts: string[] }
|
|
13
|
+
* → posts { type: 'result', id, dim, count, buffer } (Float32Array buffer, transferred)
|
|
14
|
+
* errors → { type: 'error', id, error }
|
|
15
|
+
*/
|
|
16
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
17
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
let embedder = null;
|
|
24
|
+
|
|
25
|
+
async function init() {
|
|
26
|
+
const bgJsPath = path.join(__dirname, 'pkg', 'ruvector_onnx_embeddings_wasm_bg.js');
|
|
27
|
+
const wasmPath = path.join(__dirname, 'pkg', 'ruvector_onnx_embeddings_wasm_bg.wasm');
|
|
28
|
+
|
|
29
|
+
const wasmModule = await import(pathToFileURL(bgJsPath).href);
|
|
30
|
+
const wasmBytes = fs.readFileSync(wasmPath);
|
|
31
|
+
const wasmResult = await WebAssembly.instantiate(wasmBytes, {
|
|
32
|
+
'./ruvector_onnx_embeddings_wasm_bg.js': wasmModule,
|
|
33
|
+
});
|
|
34
|
+
const wasmExports = wasmResult.instance.exports;
|
|
35
|
+
if (typeof wasmModule.__wbg_set_wasm === 'function') wasmModule.__wbg_set_wasm(wasmExports);
|
|
36
|
+
if (typeof wasmExports.__wbindgen_start === 'function') wasmExports.__wbindgen_start();
|
|
37
|
+
|
|
38
|
+
// Reconstruct model bytes from the shared buffer (zero-copy view, then handed
|
|
39
|
+
// to wasm-bindgen which copies into WASM linear memory).
|
|
40
|
+
const modelBytes = new Uint8Array(workerData.modelSab);
|
|
41
|
+
|
|
42
|
+
const cfg = new wasmModule.WasmEmbedderConfig()
|
|
43
|
+
.setMaxLength(workerData.maxLength || 256)
|
|
44
|
+
.setNormalize(true)
|
|
45
|
+
.setPooling(0); // Mean pooling — matches the single-thread path.
|
|
46
|
+
|
|
47
|
+
embedder = wasmModule.WasmEmbedder.withConfig(modelBytes, workerData.tokenizerJson, cfg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
parentPort.on('message', (msg) => {
|
|
51
|
+
if (msg.type !== 'embed') return;
|
|
52
|
+
try {
|
|
53
|
+
const dim = embedder.dimension();
|
|
54
|
+
const flat = embedder.embedBatch(msg.texts); // length = texts.length * dim
|
|
55
|
+
const arr = Float32Array.from(flat);
|
|
56
|
+
parentPort.postMessage(
|
|
57
|
+
{ type: 'result', id: msg.id, dim, count: msg.texts.length, buffer: arr.buffer },
|
|
58
|
+
[arr.buffer],
|
|
59
|
+
);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
parentPort.postMessage({ type: 'error', id: msg.id, error: e?.message || String(e) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
init()
|
|
66
|
+
.then(() => parentPort.postMessage({ type: 'ready' }))
|
|
67
|
+
.catch((e) => parentPort.postMessage({ type: 'init-error', error: e?.message || String(e) }));
|