inference-worker.mjs
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠ breaking
24 days ago
| 1 | import './inference-egress-guard.mjs'; |
| 2 | import fs from 'node:fs/promises'; |
| 3 | import path from 'node:path'; |
| 4 | import { pathToFileURL } from 'node:url'; |
| 5 | |
| 6 | const runtimeRoot = path.resolve(process.argv[2] ?? ''); |
| 7 | if (!path.isAbsolute(runtimeRoot)) throw new Error('PRIVATE_MODEL_UNAVAILABLE'); |
| 8 | |
| 9 | let model; |
| 10 | let tokenizer; |
| 11 | let ort; |
| 12 | let loadMilliseconds; |
| 13 | |
| 14 | async function load() { |
| 15 | if (model) return; |
| 16 | const start = performance.now(); |
| 17 | const ortRoot = path.join(runtimeRoot, 'node_modules/onnxruntime-web/dist'); |
| 18 | ort = await import(pathToFileURL(path.join(ortRoot, 'ort.wasm.min.mjs'))); |
| 19 | globalThis[Symbol.for('onnxruntime')] = ort; |
| 20 | const transformersPath = path.join( |
| 21 | runtimeRoot, |
| 22 | 'node_modules/@huggingface/transformers/dist/transformers.web.js' |
| 23 | ); |
| 24 | const { BertTokenizer, env } = await import(pathToFileURL(transformersPath)); |
| 25 | env.allowRemoteModels = false; |
| 26 | env.useFSCache = false; |
| 27 | env.useBrowserCache = false; |
| 28 | ort.env.wasm.numThreads = 1; |
| 29 | ort.env.wasm.proxy = false; |
| 30 | ort.env.wasm.wasmPaths = `${ortRoot}/`; |
| 31 | ort.env.wasm.wasmBinary = await fs.readFile(path.join(ortRoot, 'ort-wasm-simd-threaded.wasm')); |
| 32 | const modelRoot = path.join(runtimeRoot, 'models/all-MiniLM-L6-v2'); |
| 33 | tokenizer = new BertTokenizer( |
| 34 | JSON.parse(await fs.readFile(path.join(modelRoot, 'tokenizer.json'), 'utf8')), |
| 35 | JSON.parse(await fs.readFile(path.join(modelRoot, 'tokenizer_config.json'), 'utf8')) |
| 36 | ); |
| 37 | model = await ort.InferenceSession.create( |
| 38 | await fs.readFile(path.join(modelRoot, 'onnx/model_quantized.onnx')), |
| 39 | { executionProviders: ['wasm'] } |
| 40 | ); |
| 41 | loadMilliseconds = performance.now() - start; |
| 42 | } |
| 43 | |
| 44 | async function embed(texts) { |
| 45 | if (!Array.isArray(texts) || texts.length === 0 || texts.length > 96 || |
| 46 | texts.some((text) => typeof text !== 'string' || Buffer.byteLength(text) > 4096)) { |
| 47 | throw new Error('PRIVATE_INFERENCE_INPUT'); |
| 48 | } |
| 49 | await load(); |
| 50 | const inputs = tokenizer(texts, { padding: true, truncation: false }); |
| 51 | if (inputs.input_ids.dims[1] > 256) throw new Error('PRIVATE_CONTEXT_LIMIT'); |
| 52 | const feeds = Object.fromEntries(model.inputNames.map((name) => [ |
| 53 | name, |
| 54 | new ort.Tensor('int64', inputs[name].data, inputs[name].dims), |
| 55 | ])); |
| 56 | const outputs = await model.run(feeds); |
| 57 | const hidden = outputs.last_hidden_state; |
| 58 | const [batch, length, dimensions] = hidden.dims; |
| 59 | const vectors = []; |
| 60 | for (let row = 0; row < batch; row += 1) { |
| 61 | const vector = Array(dimensions).fill(0); |
| 62 | let count = 0; |
| 63 | for (let token = 0; token < length; token += 1) { |
| 64 | if (inputs.attention_mask.data[row * length + token] === 0n) continue; |
| 65 | count += 1; |
| 66 | for (let column = 0; column < dimensions; column += 1) { |
| 67 | vector[column] += hidden.data[(row * length + token) * dimensions + column]; |
| 68 | } |
| 69 | } |
| 70 | for (let column = 0; column < dimensions; column += 1) vector[column] /= count; |
| 71 | const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)); |
| 72 | vectors.push(vector.map((value) => value / norm)); |
| 73 | } |
| 74 | return vectors; |
| 75 | } |
| 76 | |
| 77 | let queue = Promise.resolve(); |
| 78 | process.on('message', (message) => { |
| 79 | queue = queue.then(async () => { |
| 80 | try { |
| 81 | if (message?.command !== 'embed' || Object.keys(message.arguments ?? {}).join(',') !== 'texts') { |
| 82 | throw new Error('PRIVATE_INFERENCE_INPUT'); |
| 83 | } |
| 84 | const vectors = await embed(message.arguments.texts); |
| 85 | process.send?.({ id: message.id, ok: true, result: { vectors, loadMilliseconds } }); |
| 86 | } catch (error) { |
| 87 | const safe = String(error?.message ?? '').startsWith('PRIVATE_') |
| 88 | ? error.message |
| 89 | : 'PRIVATE_MODEL_UNAVAILABLE'; |
| 90 | process.send?.({ id: message?.id ?? 'invalid', ok: false, error: safe }); |
| 91 | } |
| 92 | }); |
| 93 | }); |
| 94 | process.send?.({ ready: true, pid: process.pid }); |
File History
1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠
24 days ago