Spaces:
Running
Running
File size: 4,924 Bytes
3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 6dcf170 3bbbb17 6dcf170 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6dcf170 3bbbb17 6dcf170 3bbbb17 6dcf170 3bbbb17 6dcf170 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e 3bbbb17 6c3af4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | /**
* The main thread's side of the bi-encoder. It owns no model and runs no
* inference: it spawns `biEncoderWorker.ts`, forwards scoring requests to it
* and matches the replies back to their callers.
*
* The four exported functions keep the signatures they had when the session
* lived here, so no caller changed. What changed is that a scoring pass no
* longer stops the event loop: a 200-passage pool blocked the main thread for
* ~815 ms before and stays under 20 ms now (see docs/page-content.md).
*/
import path from "node:path";
import { Worker } from "node:worker_threads";
import type {
BiEncoderRequest,
BiEncoderResponse,
} from "./biEncoderWorkerProtocol.ts";
import { createModelLogger } from "./utils/onnxModelLoader.ts";
const printMessage = createModelLogger(path.basename(import.meta.url));
let worker: Worker | null = null;
let isReady = false;
let nextRequestId = 0;
/** Resolvers for requests the worker has not answered yet, keyed by their id. */
const pendingScores = new Map<number, (scores: number[]) => void>();
/**
* Answers every in-flight request with empty scores. Empty is the signal
* `pageContentService` already reads as "dense scoring is unavailable, rank
* lexically", so a worker that dies degrades the ranking instead of leaving
* page-content reads waiting for a reply that is never coming.
*/
function resolvePendingEmpty() {
for (const resolve of pendingScores.values()) resolve([]);
pendingScores.clear();
}
/**
* Drops a worker that died. Guarded on identity because a terminated worker's
* `exit` still arrives after `startBiEncoderService` has installed its
* replacement, and that late event must not mark the new one dead.
*/
function forgetWorker(deadWorker: Worker) {
if (worker !== deadWorker) return;
worker = null;
isReady = false;
resolvePendingEmpty();
}
export async function startBiEncoderService() {
if (worker) await stopBiEncoderService();
const biEncoderWorker = new Worker(
new URL("./biEncoderWorker.ts", import.meta.url),
);
worker = biEncoderWorker;
biEncoderWorker.on("message", (message: BiEncoderResponse) => {
if (message.type === "ready") return;
const resolve = pendingScores.get(message.id);
if (!resolve) return;
pendingScores.delete(message.id);
if (message.type === "failed") {
printMessage(`Bi-encoder scoring failed: ${message.message}`);
resolve([]);
return;
}
resolve(message.scores);
});
biEncoderWorker.on("error", (error) => {
printMessage(`Bi-encoder worker failed: ${error.message}`);
forgetWorker(biEncoderWorker);
});
biEncoderWorker.on("exit", () => forgetWorker(biEncoderWorker));
await new Promise<void>((resolve, reject) => {
const stopListening = () => {
biEncoderWorker.off("message", onMessage);
biEncoderWorker.off("error", onError);
biEncoderWorker.off("exit", onExit);
};
const onMessage = (message: BiEncoderResponse) => {
if (message.type !== "ready") return;
stopListening();
resolve();
};
const onError = (error: Error) => {
stopListening();
reject(error);
};
const onExit = (code: number) => {
stopListening();
reject(
new Error(
`The bi-encoder worker exited with code ${code} before it was ready`,
),
);
};
biEncoderWorker.on("message", onMessage);
biEncoderWorker.on("error", onError);
biEncoderWorker.on("exit", onExit);
});
isReady = true;
printMessage("Bi-encoder service ready!");
}
export async function stopBiEncoderService() {
const runningWorker = worker;
isReady = false;
worker = null;
resolvePendingEmpty();
await runningWorker?.terminate();
}
export async function getBiEncoderStatus() {
return isReady;
}
/**
* Returns dense (semantic) scores for passages given a query.
* Falls back to empty array when the worker is not running.
*/
export async function scorePassages(
query: string,
passages: string[],
): Promise<number[]> {
const runningWorker = worker;
if (!runningWorker || !isReady || passages.length === 0) {
return [];
}
// Ids are per-process and monotonic because the service can have several
// scoring passes in flight at once: two concurrent page-content reads each
// send their own request, and the replies can come back in either order.
const id = nextRequestId++;
return new Promise<number[]>((resolve) => {
pendingScores.set(id, resolve);
const request: BiEncoderRequest = { type: "score", id, query, passages };
try {
runningWorker.postMessage(request);
} catch (error) {
// The worker was terminated between the check above and this send.
pendingScores.delete(id);
printMessage(
`Could not reach the bi-encoder worker: ${error instanceof Error ? error.message : String(error)}`,
);
resolve([]);
}
});
}
|