@oh-my-pi/pi-mnemopi 18.13.1 → 18.13.2
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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.2] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed local embeddings using one spinning CPU thread per core, so a single recall query cost several CPU-seconds on a many-core machine ([#17109](https://github.com/can1357/oh-my-pi/pull/17109) by [@sjawhar](https://github.com/sjawhar))
|
|
10
|
+
|
|
5
11
|
## [18.13.0] - 2026-10-26
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -37,6 +37,11 @@ export declare function quarantineCorruptModelFile(message: string, cacheDir?: s
|
|
|
37
37
|
* retries once; fastembed re-downloads any file missing from the model's
|
|
38
38
|
* cache directory. Also the initializer the embed worker uses in its
|
|
39
39
|
* subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
40
|
+
*
|
|
41
|
+
* The ONNX sessions fastembed builds here from the model cache carry the
|
|
42
|
+
* embedding thread budget ({@link withEmbedSessionBudget}); it is off again by
|
|
43
|
+
* the time this returns, and never touches a session built from anywhere else,
|
|
44
|
+
* so a consumer sharing the same onnxruntime copy keeps its own defaults.
|
|
40
45
|
*/
|
|
41
46
|
export declare function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel>;
|
|
42
47
|
export declare function embeddingsDisabled(): boolean;
|
|
@@ -51,6 +56,8 @@ export declare function embeddingsDisabled(): boolean;
|
|
|
51
56
|
export declare function currentEmbeddingModel(): string;
|
|
52
57
|
export declare function isApiModel(modelName: string): boolean;
|
|
53
58
|
export declare function embeddingDimFor(modelName: string): number;
|
|
59
|
+
/** The bundled fastembed model id for a model name, or `null` when fastembed has no local model for it. */
|
|
60
|
+
export declare function fastembedModelName(modelName: string): StandardEmbeddingModel | null;
|
|
54
61
|
export declare function setEmbeddingProviderForTests(provider: EmbeddingProvider | null | undefined): void;
|
|
55
62
|
export declare const setEmbeddingProvider: typeof setEmbeddingProviderForTests;
|
|
56
63
|
export declare function setLocalModelInitializerForTests(initializer: LocalModelInitializer | null | undefined): void;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type RuntimeInstallSpec } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type * as Fastembed from "fastembed";
|
|
3
|
+
import type * as Ort from "onnxruntime-node";
|
|
3
4
|
type FastembedModule = typeof Fastembed;
|
|
5
|
+
type OrtModule = typeof Ort;
|
|
6
|
+
type OrtSessionOptions = Ort.InferenceSession.SessionOptions;
|
|
4
7
|
/** Runtime install inputs for the optional fastembed embedding stack. */
|
|
5
8
|
export interface FastembedRuntimeInstallPlan {
|
|
6
9
|
/** Cache directory key; changes when runtime resolution policy changes. */
|
|
@@ -37,5 +40,35 @@ export interface WindowsFastembedRuntime {
|
|
|
37
40
|
* unrelated `onnxruntime.dll` from the inherited system path.
|
|
38
41
|
*/
|
|
39
42
|
export declare function prepareWindowsFastembedRuntime({ fastembedEntry, fastembedPackageDir, arch, env, }: WindowsFastembedRuntimeOptions): Promise<WindowsFastembedRuntime>;
|
|
43
|
+
/**
|
|
44
|
+
* Session options for one embedding session: a bounded intra-op pool, a single
|
|
45
|
+
* inter-op thread, and no thread spinning. Every value the caller set wins,
|
|
46
|
+
* including inside `extra.session`, so a consumer that tunes onnxruntime itself
|
|
47
|
+
* keeps its own numbers.
|
|
48
|
+
*/
|
|
49
|
+
export declare function embedSessionOptions(base: OrtSessionOptions, parallelism: number): OrtSessionOptions;
|
|
50
|
+
/**
|
|
51
|
+
* Apply {@link embedSessionOptions} to sessions created through this ORT module
|
|
52
|
+
* from a model file under `cacheRoot`, until the returned function releases the
|
|
53
|
+
* scope. fastembed's `FlagEmbedding.init` passes only `executionProviders` and
|
|
54
|
+
* `graphOptimizationLevel` and exposes no way to reach the session options, so
|
|
55
|
+
* the module its dependency graph loads is the only seam. That module is
|
|
56
|
+
* onnxruntime-common's exported class, which an unrelated consumer shares when
|
|
57
|
+
* ORT is hoisted beside fastembed rather than nested inside it, so the budget
|
|
58
|
+
* is confined twice: only to model files under `cacheRoot`, and only while a
|
|
59
|
+
* scope is open. One wrapper serves every open scope (the newest open scope
|
|
60
|
+
* whose cache holds the model applies); releasing the last scope, in any
|
|
61
|
+
* order, puts the original `create` back unless something else has since
|
|
62
|
+
* wrapped it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyEmbedSessionBudget(ort: OrtModule, parallelism: number, cacheRoot: string): () => void;
|
|
65
|
+
/**
|
|
66
|
+
* Run `work` with the embedding thread budget on the ORT module paired with the
|
|
67
|
+
* loaded fastembed, for sessions built from model files under `cacheRoot`, and
|
|
68
|
+
* take it off again afterwards. Budgeting is best-effort: an ORT that cannot be
|
|
69
|
+
* found or loaded leaves the defaults in place rather than failing the
|
|
70
|
+
* embedding that asked for it. `loadOrt` is the test seam.
|
|
71
|
+
*/
|
|
72
|
+
export declare function withEmbedSessionBudget<T>(cacheRoot: string, work: () => Promise<T>, loadOrt?: () => OrtModule): Promise<T>;
|
|
40
73
|
export declare function loadFastembed(): Promise<FastembedModule>;
|
|
41
74
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "18.13.
|
|
4
|
+
"version": "18.13.2",
|
|
5
5
|
"description": "Local SQLite memory engine for omp agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@oh-my-pi/pi-ai": "18.13.
|
|
46
|
-
"@oh-my-pi/pi-catalog": "18.13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "18.13.
|
|
48
|
-
"@oh-my-pi/pi-utils": "18.13.
|
|
45
|
+
"@oh-my-pi/pi-ai": "18.13.2",
|
|
46
|
+
"@oh-my-pi/pi-catalog": "18.13.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "18.13.2",
|
|
48
|
+
"@oh-my-pi/pi-utils": "18.13.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"fastembed": "3.0.0",
|
package/src/core/embeddings.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "@oh-my-pi/pi-utils";
|
|
16
16
|
import { LRUCache } from "@oh-my-pi/pi-utils/lru";
|
|
17
17
|
import type { EmbeddingModel } from "fastembed";
|
|
18
|
-
import { loadFastembed } from "./fastembed-runtime";
|
|
18
|
+
import { loadFastembed, withEmbedSessionBudget } from "./fastembed-runtime";
|
|
19
19
|
import {
|
|
20
20
|
type EmbeddingOutput,
|
|
21
21
|
getMnemopiRuntimeOptions,
|
|
@@ -99,20 +99,27 @@ export async function quarantineCorruptModelFile(message: string, cacheDir?: str
|
|
|
99
99
|
* retries once; fastembed re-downloads any file missing from the model's
|
|
100
100
|
* cache directory. Also the initializer the embed worker uses in its
|
|
101
101
|
* subprocess; the in-process seam stays {@link setLocalModelInitializer}.
|
|
102
|
+
*
|
|
103
|
+
* The ONNX sessions fastembed builds here from the model cache carry the
|
|
104
|
+
* embedding thread budget ({@link withEmbedSessionBudget}); it is off again by
|
|
105
|
+
* the time this returns, and never touches a session built from anywhere else,
|
|
106
|
+
* so a consumer sharing the same onnxruntime copy keeps its own defaults.
|
|
102
107
|
*/
|
|
103
108
|
export async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
|
|
104
109
|
const cacheDir = options.cacheDir ?? getFastembedCacheDir();
|
|
105
110
|
const initOptions = options.cacheDir === undefined ? { ...options, cacheDir } : options;
|
|
106
111
|
const { FlagEmbedding } = await loadFastembed();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
return withEmbedSessionBudget(cacheDir, async () => {
|
|
113
|
+
try {
|
|
114
|
+
return await FlagEmbedding.init(initOptions);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : "";
|
|
117
|
+
if (/Protobuf parsing failed/i.test(message) && (await quarantineCorruptModelFile(message, cacheDir))) {
|
|
118
|
+
return FlagEmbedding.init(initOptions);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
113
121
|
}
|
|
114
|
-
|
|
115
|
-
}
|
|
122
|
+
});
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
function activeEmbeddingOptions() {
|
|
@@ -313,7 +320,8 @@ const KNOWN_MODEL_NAMES: Record<string, string> = {
|
|
|
313
320
|
"intfloat/multilingual-e5-large": "fast-multilingual-e5-large",
|
|
314
321
|
"sentence-transformers/all-MiniLM-L6-v2": "fast-all-MiniLM-L6-v2",
|
|
315
322
|
};
|
|
316
|
-
|
|
323
|
+
/** The bundled fastembed model id for a model name, or `null` when fastembed has no local model for it. */
|
|
324
|
+
export function fastembedModelName(modelName: string): StandardEmbeddingModel | null {
|
|
317
325
|
// Fastembed `EmbeddingModel` enum string values, inlined so resolving a model name
|
|
318
326
|
// (and `available()`) never imports `fastembed` — its module eagerly loads the
|
|
319
327
|
// `onnxruntime-node` native addon, which segfaults in some runtimes.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import {
|
|
4
5
|
ensureRuntimeInstalled,
|
|
@@ -9,9 +10,12 @@ import {
|
|
|
9
10
|
resolveRuntimeModule,
|
|
10
11
|
} from "@oh-my-pi/pi-utils";
|
|
11
12
|
import type * as Fastembed from "fastembed";
|
|
13
|
+
import type * as Ort from "onnxruntime-node";
|
|
12
14
|
import packageManifest from "../../package.json" with { type: "json" };
|
|
13
15
|
|
|
14
16
|
type FastembedModule = typeof Fastembed;
|
|
17
|
+
type OrtModule = typeof Ort;
|
|
18
|
+
type OrtSessionOptions = Ort.InferenceSession.SessionOptions;
|
|
15
19
|
|
|
16
20
|
/** Runtime install inputs for the optional fastembed embedding stack. */
|
|
17
21
|
export interface FastembedRuntimeInstallPlan {
|
|
@@ -58,6 +62,8 @@ export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan {
|
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
64
|
let fastembedLoad: Promise<FastembedModule> | null = null;
|
|
65
|
+
/** The fastembed installation {@link loadFastembed} resolved, and the `require` that sees its dependency graph. */
|
|
66
|
+
let loadedFastembed: { entry: string; fastembedPackageDir: string; require: NodeRequire } | null = null;
|
|
61
67
|
|
|
62
68
|
/** Inputs for selecting the Windows DLL directory paired with a fastembed installation. */
|
|
63
69
|
export interface WindowsFastembedRuntimeOptions {
|
|
@@ -81,6 +87,18 @@ export interface WindowsFastembedRuntime {
|
|
|
81
87
|
dllDir: string;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/** The `onnxruntime-node` installation fastembed's own dependency graph loads: nested first, then the hoisted copy beside it. */
|
|
91
|
+
function resolveFastembedOrt(fastembedPackageDir: string): { ortEntry: string | null; ortPackageDir: string } {
|
|
92
|
+
const nestedNodeModules = path.join(fastembedPackageDir, "node_modules");
|
|
93
|
+
const rootNodeModules = path.dirname(fastembedPackageDir);
|
|
94
|
+
const nestedOrtEntry = resolveRuntimeModule(nestedNodeModules, "onnxruntime-node");
|
|
95
|
+
const ortEntry = nestedOrtEntry ?? resolveRuntimeModule(rootNodeModules, "onnxruntime-node");
|
|
96
|
+
return {
|
|
97
|
+
ortEntry,
|
|
98
|
+
ortPackageDir: path.join(nestedOrtEntry ? nestedNodeModules : rootNodeModules, "onnxruntime-node"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/**
|
|
85
103
|
* Prepend the ORT DLL directory paired with fastembed before Bun loads its
|
|
86
104
|
* native binding. Compiled Windows binaries extract `.node` files to a
|
|
@@ -93,11 +111,7 @@ export async function prepareWindowsFastembedRuntime({
|
|
|
93
111
|
arch = process.arch,
|
|
94
112
|
env = process.env,
|
|
95
113
|
}: WindowsFastembedRuntimeOptions): Promise<WindowsFastembedRuntime> {
|
|
96
|
-
const
|
|
97
|
-
const rootNodeModules = path.dirname(fastembedPackageDir);
|
|
98
|
-
const nestedOrtEntry = resolveRuntimeModule(nestedNodeModules, "onnxruntime-node");
|
|
99
|
-
const ortEntry = nestedOrtEntry ?? resolveRuntimeModule(rootNodeModules, "onnxruntime-node");
|
|
100
|
-
const ortPackageDir = path.join(nestedOrtEntry ? nestedNodeModules : rootNodeModules, "onnxruntime-node");
|
|
114
|
+
const { ortEntry, ortPackageDir } = resolveFastembedOrt(fastembedPackageDir);
|
|
101
115
|
if (!ortEntry) {
|
|
102
116
|
throw new Error(`Cannot find module onnxruntime-node beside ${fastembedEntry}`);
|
|
103
117
|
}
|
|
@@ -120,6 +134,172 @@ export async function prepareWindowsFastembedRuntime({
|
|
|
120
134
|
return { ortEntry, ortPackageDir, dllDir };
|
|
121
135
|
}
|
|
122
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Upper bound on the intra-op thread pool of an embedding session.
|
|
139
|
+
*
|
|
140
|
+
* fastembed creates its ONNX session without any thread option, so
|
|
141
|
+
* onnxruntime applies its own defaults: one intra-op thread per physical core,
|
|
142
|
+
* with those threads spinning between operators. The real work of embedding a
|
|
143
|
+
* recall query is about half a CPU-second (fastembed pads every text to the
|
|
144
|
+
* model's maximum length), so on a many-core host most of what the default
|
|
145
|
+
* pool burns beyond that is spin and scheduling. Measured on a 32-core host,
|
|
146
|
+
* one short query cost 3.85 CPU-seconds and kept 31 cores busy for 124 ms;
|
|
147
|
+
* with the budget below it costs 0.56 CPU-seconds and finishes in 178 ms, and its
|
|
148
|
+
* vector points the same way (cosine 0.9999997 against the unbudgeted one;
|
|
149
|
+
* `bench/embed-session-threads.bench.ts` measures and checks both).
|
|
150
|
+
*
|
|
151
|
+
* The cap is a trade, not a free win: on an otherwise idle many-core host a
|
|
152
|
+
* single embedding finishes later (124 ms to 178 ms above, about 40% slower)
|
|
153
|
+
* for about 7x less CPU. It is fixed because the default pool's cost
|
|
154
|
+
* multiplies with every process embedding at the same time, while the extra
|
|
155
|
+
* latency is tens of milliseconds per retain or recall.
|
|
156
|
+
*/
|
|
157
|
+
const EMBED_INTRA_OP_THREAD_CAP = 4;
|
|
158
|
+
|
|
159
|
+
/** Narrow an `extra` sub-object for merging; anything else is replaced wholesale. */
|
|
160
|
+
function extraSection(value: unknown): Record<string, unknown> | undefined {
|
|
161
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Session options for one embedding session: a bounded intra-op pool, a single
|
|
166
|
+
* inter-op thread, and no thread spinning. Every value the caller set wins,
|
|
167
|
+
* including inside `extra.session`, so a consumer that tunes onnxruntime itself
|
|
168
|
+
* keeps its own numbers.
|
|
169
|
+
*/
|
|
170
|
+
export function embedSessionOptions(base: OrtSessionOptions, parallelism: number): OrtSessionOptions {
|
|
171
|
+
const extra = extraSection(base.extra);
|
|
172
|
+
const session = extraSection(extra?.session);
|
|
173
|
+
return {
|
|
174
|
+
...base,
|
|
175
|
+
intraOpNumThreads: base.intraOpNumThreads ?? Math.max(1, Math.min(EMBED_INTRA_OP_THREAD_CAP, parallelism)),
|
|
176
|
+
interOpNumThreads: base.interOpNumThreads ?? 1,
|
|
177
|
+
extra: {
|
|
178
|
+
...extra,
|
|
179
|
+
session: {
|
|
180
|
+
...session,
|
|
181
|
+
intra_op: { allow_spinning: "0", ...extraSection(session?.intra_op) },
|
|
182
|
+
inter_op: { allow_spinning: "0", ...extraSection(session?.inter_op) },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** One open budget: the host's core count it sizes for, and the model directory its sessions load from. */
|
|
189
|
+
interface EmbedBudgetScope {
|
|
190
|
+
parallelism: number;
|
|
191
|
+
cacheRoot: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The single budgeting wrapper installed on an ORT session factory, and the scopes it currently serves. */
|
|
195
|
+
interface BudgetedFactory {
|
|
196
|
+
original: typeof Ort.InferenceSession.create;
|
|
197
|
+
wrapper: typeof Ort.InferenceSession.create;
|
|
198
|
+
scopes: EmbedBudgetScope[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const budgetedFactories = new WeakMap<object, BudgetedFactory>();
|
|
202
|
+
|
|
203
|
+
/** Whether `model` is a model file under `cacheRoot`, the only sessions an embedding budget may touch. */
|
|
204
|
+
function isEmbeddingModelPath(model: unknown, cacheRoot: string): boolean {
|
|
205
|
+
return typeof model === "string" && path.resolve(model).startsWith(path.resolve(cacheRoot) + path.sep);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Apply {@link embedSessionOptions} to sessions created through this ORT module
|
|
210
|
+
* from a model file under `cacheRoot`, until the returned function releases the
|
|
211
|
+
* scope. fastembed's `FlagEmbedding.init` passes only `executionProviders` and
|
|
212
|
+
* `graphOptimizationLevel` and exposes no way to reach the session options, so
|
|
213
|
+
* the module its dependency graph loads is the only seam. That module is
|
|
214
|
+
* onnxruntime-common's exported class, which an unrelated consumer shares when
|
|
215
|
+
* ORT is hoisted beside fastembed rather than nested inside it, so the budget
|
|
216
|
+
* is confined twice: only to model files under `cacheRoot`, and only while a
|
|
217
|
+
* scope is open. One wrapper serves every open scope (the newest open scope
|
|
218
|
+
* whose cache holds the model applies); releasing the last scope, in any
|
|
219
|
+
* order, puts the original `create` back unless something else has since
|
|
220
|
+
* wrapped it.
|
|
221
|
+
*/
|
|
222
|
+
export function applyEmbedSessionBudget(ort: OrtModule, parallelism: number, cacheRoot: string): () => void {
|
|
223
|
+
const factory = ort.InferenceSession;
|
|
224
|
+
let budgeted = budgetedFactories.get(factory);
|
|
225
|
+
if (budgeted === undefined) {
|
|
226
|
+
const original = factory.create;
|
|
227
|
+
const create = original.bind(factory);
|
|
228
|
+
const scopes: EmbedBudgetScope[] = [];
|
|
229
|
+
const wrapper = ((
|
|
230
|
+
model: string | ArrayBufferLike | Uint8Array,
|
|
231
|
+
...rest: unknown[]
|
|
232
|
+
): Promise<Ort.InferenceSession> => {
|
|
233
|
+
// A budgeted call is always the `(path, options?)` form: only a path
|
|
234
|
+
// can name a model file under a scope's cache. Every other call,
|
|
235
|
+
// including the buffer forms, reaches ORT exactly as it was made.
|
|
236
|
+
// The newest scope that owns this model wins, so two scopes on
|
|
237
|
+
// different caches each budget their own sessions.
|
|
238
|
+
const scope = scopes.findLast(open => isEmbeddingModelPath(model, open.cacheRoot));
|
|
239
|
+
if (scope === undefined) {
|
|
240
|
+
return create(model as string, ...(rest as [OrtSessionOptions]));
|
|
241
|
+
}
|
|
242
|
+
return create(
|
|
243
|
+
model as string,
|
|
244
|
+
embedSessionOptions((rest[0] as OrtSessionOptions | undefined) ?? {}, scope.parallelism),
|
|
245
|
+
);
|
|
246
|
+
}) as typeof factory.create;
|
|
247
|
+
budgeted = { original, wrapper, scopes };
|
|
248
|
+
budgetedFactories.set(factory, budgeted);
|
|
249
|
+
factory.create = wrapper;
|
|
250
|
+
}
|
|
251
|
+
const held = budgeted;
|
|
252
|
+
const scope: EmbedBudgetScope = { parallelism, cacheRoot };
|
|
253
|
+
held.scopes.push(scope);
|
|
254
|
+
return () => {
|
|
255
|
+
const index = held.scopes.indexOf(scope);
|
|
256
|
+
if (index === -1) return;
|
|
257
|
+
held.scopes.splice(index, 1);
|
|
258
|
+
if (held.scopes.length > 0) return;
|
|
259
|
+
// The last scope is gone: forget the factory so the next scope installs
|
|
260
|
+
// a fresh wrapper, and restore the original only when ours is still the
|
|
261
|
+
// installed `create` — something wrapped on top of it owns the chain.
|
|
262
|
+
budgetedFactories.delete(factory);
|
|
263
|
+
if (factory.create === held.wrapper) factory.create = held.original;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The ORT module paired with the fastembed {@link loadFastembed} resolved. */
|
|
268
|
+
function pairedFastembedOrt(): OrtModule {
|
|
269
|
+
const loaded = loadedFastembed;
|
|
270
|
+
if (loaded === null) throw new Error("fastembed is not loaded");
|
|
271
|
+
const { ortEntry } = resolveFastembedOrt(loaded.fastembedPackageDir);
|
|
272
|
+
if (ortEntry === null) throw new Error(`Cannot find module onnxruntime-node beside ${loaded.entry}`);
|
|
273
|
+
return loaded.require(ortEntry);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Run `work` with the embedding thread budget on the ORT module paired with the
|
|
278
|
+
* loaded fastembed, for sessions built from model files under `cacheRoot`, and
|
|
279
|
+
* take it off again afterwards. Budgeting is best-effort: an ORT that cannot be
|
|
280
|
+
* found or loaded leaves the defaults in place rather than failing the
|
|
281
|
+
* embedding that asked for it. `loadOrt` is the test seam.
|
|
282
|
+
*/
|
|
283
|
+
export async function withEmbedSessionBudget<T>(
|
|
284
|
+
cacheRoot: string,
|
|
285
|
+
work: () => Promise<T>,
|
|
286
|
+
loadOrt: () => OrtModule = pairedFastembedOrt,
|
|
287
|
+
): Promise<T> {
|
|
288
|
+
let release: (() => void) | undefined;
|
|
289
|
+
try {
|
|
290
|
+
release = applyEmbedSessionBudget(loadOrt(), os.availableParallelism(), cacheRoot);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.debug("mnemopi: onnxruntime not budgetable; embedding sessions keep ORT thread defaults", {
|
|
293
|
+
error: String(error),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
return await work();
|
|
298
|
+
} finally {
|
|
299
|
+
release?.();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
123
303
|
export function loadFastembed(): Promise<FastembedModule> {
|
|
124
304
|
fastembedLoad ??= loadFastembedOnce().catch(error => {
|
|
125
305
|
fastembedLoad = null;
|
|
@@ -153,6 +333,8 @@ async function loadResolvedFastembed(entry: string, fastembedPackageDir: string)
|
|
|
153
333
|
requireFastembed(ortEntry);
|
|
154
334
|
}
|
|
155
335
|
const loaded: FastembedModule = requireFastembed(entry);
|
|
336
|
+
// Where the ORT paired with this fastembed lives, for `withEmbedSessionBudget`.
|
|
337
|
+
loadedFastembed = { entry, fastembedPackageDir, require: requireFastembed };
|
|
156
338
|
return loaded;
|
|
157
339
|
}
|
|
158
340
|
|