@modular-prompt/driver 0.16.0 → 0.17.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/README.md +93 -10
- package/dist/cache-controller.d.ts +4 -0
- package/dist/cache-controller.d.ts.map +1 -1
- package/dist/driver-registry/config-based-factory.d.ts.map +1 -1
- package/dist/driver-registry/config-based-factory.js +6 -0
- package/dist/driver-registry/config-based-factory.js.map +1 -1
- package/dist/driver-registry/factory-helper.d.ts.map +1 -1
- package/dist/driver-registry/factory-helper.js +9 -2
- package/dist/driver-registry/factory-helper.js.map +1 -1
- package/dist/driver-registry/index.d.ts +1 -1
- package/dist/driver-registry/index.d.ts.map +1 -1
- package/dist/driver-registry/types.d.ts +15 -1
- package/dist/driver-registry/types.d.ts.map +1 -1
- package/dist/formatter/converter.d.ts.map +1 -1
- package/dist/formatter/converter.js +31 -2
- package/dist/formatter/converter.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/local-inference/adapters.d.ts +6 -0
- package/dist/local-inference/adapters.d.ts.map +1 -1
- package/dist/local-inference/driver.d.ts.map +1 -1
- package/dist/local-inference/driver.js +45 -24
- package/dist/local-inference/driver.js.map +1 -1
- package/dist/local-inference/process-client.d.ts +4 -2
- package/dist/local-inference/process-client.d.ts.map +1 -1
- package/dist/local-inference/process-client.js +24 -8
- package/dist/local-inference/process-client.js.map +1 -1
- package/dist/local-inference/process-communication.d.ts +9 -2
- package/dist/local-inference/process-communication.d.ts.map +1 -1
- package/dist/local-inference/process-communication.js +37 -5
- package/dist/local-inference/process-communication.js.map +1 -1
- package/dist/local-inference/protocol.d.ts +4 -0
- package/dist/local-inference/protocol.d.ts.map +1 -1
- package/dist/local-inference/request-queue.d.ts +1 -1
- package/dist/local-inference/request-queue.d.ts.map +1 -1
- package/dist/local-inference/request-queue.js +26 -7
- package/dist/local-inference/request-queue.js.map +1 -1
- package/dist/local-inference/stream-utils.d.ts +6 -0
- package/dist/local-inference/stream-utils.d.ts.map +1 -1
- package/dist/local-inference/stream-utils.js.map +1 -1
- package/dist/mlx-ml/mlx-cache-controller.d.ts +9 -0
- package/dist/mlx-ml/mlx-cache-controller.d.ts.map +1 -1
- package/dist/mlx-ml/mlx-cache-controller.js +158 -33
- package/dist/mlx-ml/mlx-cache-controller.js.map +1 -1
- package/dist/mlx-ml/mlx-cache-support.d.ts +2 -2
- package/dist/mlx-ml/mlx-cache-support.d.ts.map +1 -1
- package/dist/mlx-ml/mlx-cache-support.js +8 -3
- package/dist/mlx-ml/mlx-cache-support.js.map +1 -1
- package/dist/mlx-ml/mlx-driver.d.ts +0 -1
- package/dist/mlx-ml/mlx-driver.d.ts.map +1 -1
- package/dist/mlx-ml/mlx-driver.js +1 -8
- package/dist/mlx-ml/mlx-driver.js.map +1 -1
- package/dist/mlx-ml/process/index.d.ts +1 -1
- package/dist/mlx-ml/process/index.d.ts.map +1 -1
- package/dist/mlx-ml/process/index.js +2 -2
- package/dist/mlx-ml/process/index.js.map +1 -1
- package/dist/models-config/index.d.ts +1 -1
- package/dist/models-config/index.d.ts.map +1 -1
- package/dist/models-config/index.js +1 -1
- package/dist/models-config/index.js.map +1 -1
- package/dist/models-config/resolve.d.ts +9 -1
- package/dist/models-config/resolve.d.ts.map +1 -1
- package/dist/models-config/resolve.js +94 -2
- package/dist/models-config/resolve.js.map +1 -1
- package/dist/models-config/types.d.ts +3 -1
- package/dist/models-config/types.d.ts.map +1 -1
- package/dist/pytorch/process/index.d.ts +4 -2
- package/dist/pytorch/process/index.d.ts.map +1 -1
- package/dist/pytorch/process/index.js +24 -7
- package/dist/pytorch/process/index.js.map +1 -1
- package/dist/pytorch/pytorch-cache-controller.d.ts +84 -0
- package/dist/pytorch/pytorch-cache-controller.d.ts.map +1 -0
- package/dist/pytorch/pytorch-cache-controller.js +742 -0
- package/dist/pytorch/pytorch-cache-controller.js.map +1 -0
- package/dist/pytorch/pytorch-cache-support.d.ts +23 -0
- package/dist/pytorch/pytorch-cache-support.d.ts.map +1 -0
- package/dist/pytorch/pytorch-cache-support.js +47 -0
- package/dist/pytorch/pytorch-cache-support.js.map +1 -0
- package/dist/pytorch/pytorch-driver.d.ts +8 -1
- package/dist/pytorch/pytorch-driver.d.ts.map +1 -1
- package/dist/pytorch/pytorch-driver.js +40 -0
- package/dist/pytorch/pytorch-driver.js.map +1 -1
- package/dist/runtime/check.d.ts.map +1 -1
- package/dist/runtime/check.js +8 -6
- package/dist/runtime/check.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/manifest-core.d.mts +1 -0
- package/dist/runtime/manifest-core.mjs +1 -0
- package/dist/runtime/manifest-core.mjs.map +1 -1
- package/dist/runtime/manifest.d.ts +2 -0
- package/dist/runtime/manifest.d.ts.map +1 -1
- package/dist/runtime/manifest.js.map +1 -1
- package/dist/runtime/paths-core.d.mts +15 -1
- package/dist/runtime/paths-core.d.mts.map +1 -1
- package/dist/runtime/paths-core.mjs +50 -5
- package/dist/runtime/paths-core.mjs.map +1 -1
- package/dist/runtime/paths.d.ts +2 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +2 -2
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/pytorch-template-core.d.mts +11 -0
- package/dist/runtime/pytorch-template-core.d.mts.map +1 -0
- package/dist/runtime/pytorch-template-core.mjs +54 -0
- package/dist/runtime/pytorch-template-core.mjs.map +1 -0
- package/dist/runtime/setup-commands-core.d.mts +3 -0
- package/dist/runtime/setup-commands-core.d.mts.map +1 -1
- package/dist/runtime/setup-commands-core.mjs +4 -0
- package/dist/runtime/setup-commands-core.mjs.map +1 -1
- package/dist/runtime/setup-commands.d.ts +1 -1
- package/dist/runtime/setup-commands.d.ts.map +1 -1
- package/dist/runtime/setup-commands.js +1 -1
- package/dist/runtime/setup-commands.js.map +1 -1
- package/docs/DRIVER_API.md +455 -0
- package/docs/LOCAL_MODEL_SETUP.md +765 -0
- package/docs/mlx-api-selection.md +301 -0
- package/package.json +9 -5
- package/scripts/runtime-cli.bin.test.ts +142 -0
- package/scripts/runtime-cli.js +305 -35
- package/scripts/runtime-cli.test.ts +163 -0
- package/src/mlx-ml/python/__main__.py +1 -1
- package/src/mlx-ml/python/backends/base.py +88 -18
- package/src/mlx-ml/python/backends/mlx_lm.py +28 -3
- package/src/mlx-ml/python/backends/mlx_vlm.py +679 -2
- package/src/mlx-ml/python/handlers/cache.py +4 -0
- package/src/mlx-ml/python/handlers/generate.py +33 -10
- package/src/mlx-ml/python/handlers/tokenize.py +1 -4
- package/src/mlx-ml/python/pyproject.toml +1 -1
- package/src/mlx-ml/python/server.py +2 -0
- package/src/mlx-ml/python/uv.lock +8 -8
- package/src/pytorch/templates/cpu-minimal/backends/base.py +139 -0
- package/src/pytorch/templates/cpu-minimal/backends/transformers_lm.py +1167 -0
- package/src/pytorch/{python → templates/cpu-minimal}/handlers/__init__.py +1 -0
- package/src/pytorch/templates/cpu-minimal/handlers/cache.py +88 -0
- package/src/pytorch/templates/cpu-minimal/handlers/generate.py +157 -0
- package/src/pytorch/{python → templates/cpu-minimal}/pyproject.toml +2 -2
- package/src/pytorch/{python → templates/cpu-minimal}/server.py +20 -2
- package/src/pytorch/templates/cpu-minimal/tests/test_cache_handler.py +284 -0
- package/src/pytorch/templates/cpu-minimal/tests/test_capabilities.py +14 -0
- package/src/pytorch/templates/cpu-minimal/tests/test_server.py +141 -0
- package/src/pytorch/templates/cpu-minimal/tests/test_transformers_errors.py +89 -0
- package/src/pytorch/templates/cpu-minimal/tests/test_transformers_lm_cache.py +554 -0
- package/src/pytorch/templates/cpu-minimal/utils/__init__.py +0 -0
- package/src/pytorch/{python → templates/cpu-minimal}/utils/token_utils.py +2 -2
- package/src/pytorch/templates/cpu-minimal/utils/transformers_errors.py +54 -0
- package/src/pytorch/{python → templates/cpu-minimal}/uv.lock +149 -109
- package/src/pytorch/templates/cuda/__main__.py +19 -0
- package/src/pytorch/templates/cuda/backends/__init__.py +3 -0
- package/src/pytorch/{python → templates/cuda}/backends/base.py +54 -6
- package/src/pytorch/templates/cuda/backends/transformers_lm.py +379 -0
- package/src/pytorch/templates/cuda/handlers/__init__.py +7 -0
- package/src/pytorch/templates/cuda/handlers/cache.py +93 -0
- package/src/pytorch/templates/cuda/handlers/cancel.py +53 -0
- package/src/pytorch/templates/cuda/handlers/capabilities.py +6 -0
- package/src/pytorch/templates/cuda/handlers/completion.py +15 -0
- package/src/pytorch/templates/cuda/handlers/format_test.py +70 -0
- package/src/pytorch/templates/cuda/handlers/generate.py +152 -0
- package/src/pytorch/templates/cuda/handlers/render.py +40 -0
- package/src/pytorch/templates/cuda/handlers/tokenize.py +63 -0
- package/src/pytorch/templates/cuda/pyproject.toml +37 -0
- package/src/pytorch/templates/cuda/server.py +158 -0
- package/src/pytorch/templates/cuda/tests/__init__.py +0 -0
- package/src/pytorch/templates/cuda/tests/test_cache_handler.py +207 -0
- package/src/pytorch/templates/cuda/tests/test_capabilities.py +14 -0
- package/src/pytorch/templates/cuda/tests/test_server.py +145 -0
- package/src/pytorch/templates/cuda/tests/test_transformers_errors.py +89 -0
- package/src/pytorch/templates/cuda/tests/test_transformers_lm_cache.py +288 -0
- package/src/pytorch/templates/cuda/utils/__init__.py +0 -0
- package/src/pytorch/templates/cuda/utils/chat_template_constraints.py +164 -0
- package/src/pytorch/templates/cuda/utils/prompt_builder.py +54 -0
- package/src/pytorch/templates/cuda/utils/template_render.py +80 -0
- package/src/pytorch/templates/cuda/utils/token_utils.py +376 -0
- package/src/pytorch/templates/cuda/utils/transformers_errors.py +54 -0
- package/src/pytorch/templates/cuda/uv.lock +734 -0
- package/src/pytorch/python/backends/transformers_lm.py +0 -127
- package/src/pytorch/python/handlers/generate.py +0 -68
- /package/src/pytorch/{python → templates/cpu-minimal}/__main__.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/backends/__init__.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/cancel.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/capabilities.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/completion.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/format_test.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/render.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/handlers/tokenize.py +0 -0
- /package/src/pytorch/{python/utils → templates/cpu-minimal/tests}/__init__.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/utils/chat_template_constraints.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/utils/prompt_builder.py +0 -0
- /package/src/pytorch/{python → templates/cpu-minimal}/utils/template_render.py +0 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join, relative, resolve as resolvePath, sep } from 'node:path';
|
|
5
|
+
import { mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { lock as lockFile } from 'proper-lockfile';
|
|
7
|
+
import { Logger } from '@modular-prompt/utils';
|
|
8
|
+
import { formatPromptAsMessages } from '../formatter/converter.js';
|
|
9
|
+
import { convertMessages, convertToolDefinitions } from '../mlx-ml/mlx-message-utils.js';
|
|
10
|
+
const logger = new Logger({ prefix: 'PyTorch', context: 'cache' });
|
|
11
|
+
const CACHE_FILE_EXTENSION = '.pytorch-cache';
|
|
12
|
+
const PYTORCH_BACKEND = 'pytorch';
|
|
13
|
+
/**
|
|
14
|
+
* PyTorch Transformers backend 用の PromptCacheController。
|
|
15
|
+
*
|
|
16
|
+
* キャッシュ本体の形式・検証・寿命は Python backend が所有する。
|
|
17
|
+
* このクラスは、プロンプト要素から安定した ref を生成し、LIP の
|
|
18
|
+
* cache_prefill / generate と PromptCacheController の usage 契約を橋渡しする。
|
|
19
|
+
*/
|
|
20
|
+
export class PyTorchCacheController {
|
|
21
|
+
cacheByHash = new Map();
|
|
22
|
+
inflightRequests = new Map();
|
|
23
|
+
process;
|
|
24
|
+
cacheDir;
|
|
25
|
+
managedDir;
|
|
26
|
+
cacheDirReady = false;
|
|
27
|
+
closed = false;
|
|
28
|
+
bound = false;
|
|
29
|
+
modelKind = 'lm';
|
|
30
|
+
cleanupHandler;
|
|
31
|
+
messageProcessor;
|
|
32
|
+
formatterOptions = {};
|
|
33
|
+
lastHandle;
|
|
34
|
+
lastElementHashes;
|
|
35
|
+
lastHandleModel;
|
|
36
|
+
lastHandleFormatterOptionsHash;
|
|
37
|
+
lastHandleToolsHash;
|
|
38
|
+
lastHandleReasoningEffort;
|
|
39
|
+
cacheIndex = { version: 1, entries: [] };
|
|
40
|
+
/** Token counts returned by the backend for process-local refs. */
|
|
41
|
+
cacheTokenCounts = new Map();
|
|
42
|
+
stats = {
|
|
43
|
+
totalQueries: 0,
|
|
44
|
+
memoryHit: 0,
|
|
45
|
+
diskHit: 0,
|
|
46
|
+
incremental: 0,
|
|
47
|
+
fresh: 0,
|
|
48
|
+
prefillTokens: 0,
|
|
49
|
+
prefillReusedTokens: 0,
|
|
50
|
+
totalPromptTokens: 0,
|
|
51
|
+
totalCacheTokensUsed: 0,
|
|
52
|
+
};
|
|
53
|
+
static EMPTY_HANDLE = {
|
|
54
|
+
ref: '',
|
|
55
|
+
includes: { instructions: false, dataElementCount: 0, tools: false },
|
|
56
|
+
};
|
|
57
|
+
constructor(options) {
|
|
58
|
+
if (options?.cacheDir) {
|
|
59
|
+
// Python runtime may have a different cwd. Resolve relative paths once
|
|
60
|
+
// so refs and cache-index paths remain stable across process calls.
|
|
61
|
+
this.cacheDir = resolvePath(options.cacheDir);
|
|
62
|
+
this.managedDir = false;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.cacheDir = '';
|
|
66
|
+
this.managedDir = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Select backend-local storage before the controller is bound. */
|
|
70
|
+
setModelKind(modelKind) {
|
|
71
|
+
if (this.bound) {
|
|
72
|
+
throw new Error('PyTorchCacheController model kind must be set before bind');
|
|
73
|
+
}
|
|
74
|
+
this.modelKind = modelKind === 'vlm' ? 'vlm' : 'lm';
|
|
75
|
+
}
|
|
76
|
+
async bind(process, formatterOptions, messageProcessor) {
|
|
77
|
+
if (this.bound) {
|
|
78
|
+
throw new Error('PyTorchCacheController is already bound to a process');
|
|
79
|
+
}
|
|
80
|
+
this.process = process;
|
|
81
|
+
this.formatterOptions = formatterOptions;
|
|
82
|
+
this.messageProcessor = messageProcessor;
|
|
83
|
+
if (!this.cacheDir) {
|
|
84
|
+
this.cacheDir = join(tmpdir(), `pytorch-prompt-cache-${randomBytes(6).toString('hex')}`);
|
|
85
|
+
}
|
|
86
|
+
if (this.managedDir) {
|
|
87
|
+
this.cleanupHandler = () => {
|
|
88
|
+
try {
|
|
89
|
+
rmSync(this.cacheDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best-effort cleanup during process exit
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
globalThis.process.on('exit', this.cleanupHandler);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await this.loadIndex();
|
|
99
|
+
}
|
|
100
|
+
this.bound = true;
|
|
101
|
+
}
|
|
102
|
+
async ensureCacheDir() {
|
|
103
|
+
if (this.cacheDirReady)
|
|
104
|
+
return;
|
|
105
|
+
await mkdir(this.cacheDir, { recursive: true, mode: 0o700 });
|
|
106
|
+
this.cacheDirReady = true;
|
|
107
|
+
}
|
|
108
|
+
get indexPath() {
|
|
109
|
+
return join(this.cacheDir, 'cache-index.json');
|
|
110
|
+
}
|
|
111
|
+
readMetaTokenCount(cachePath) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = readFileSync(cachePath + '.meta.json', 'utf-8');
|
|
114
|
+
const meta = JSON.parse(raw);
|
|
115
|
+
return typeof meta.token_count === 'number' && Number.isFinite(meta.token_count)
|
|
116
|
+
? Math.max(0, meta.token_count)
|
|
117
|
+
: 0;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
readPrefixMeta(cachePath) {
|
|
124
|
+
try {
|
|
125
|
+
const raw = readFileSync(cachePath + '.meta.json', 'utf-8');
|
|
126
|
+
const meta = JSON.parse(raw);
|
|
127
|
+
if (!Array.isArray(meta.prefix_offsets) || !Array.isArray(meta.prefix_hashes)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
tokenCount: typeof meta.token_count === 'number' ? Math.max(0, meta.token_count) : 0,
|
|
132
|
+
prefixOffsets: meta.prefix_offsets,
|
|
133
|
+
prefixHashes: meta.prefix_hashes,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async loadIndex() {
|
|
141
|
+
try {
|
|
142
|
+
if (!existsSync(this.indexPath))
|
|
143
|
+
return;
|
|
144
|
+
const release = await lockFile(this.indexPath, { realpath: false });
|
|
145
|
+
try {
|
|
146
|
+
const raw = await readFile(this.indexPath, 'utf-8');
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
if (parsed && parsed.version === 1 && Array.isArray(parsed.entries)) {
|
|
149
|
+
this.cacheIndex = parsed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await release();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// A corrupt index or lock failure should not disable inference.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async saveIndex() {
|
|
161
|
+
if (this.managedDir)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
await this.ensureCacheDir();
|
|
165
|
+
// proper-lockfile locks an existing file. Seed it before acquiring the
|
|
166
|
+
// lock on the first write; subsequent writes replace its contents while
|
|
167
|
+
// holding the lock.
|
|
168
|
+
if (!existsSync(this.indexPath)) {
|
|
169
|
+
await writeFile(this.indexPath, JSON.stringify(this.cacheIndex, null, 2));
|
|
170
|
+
}
|
|
171
|
+
const release = await lockFile(this.indexPath, { realpath: false });
|
|
172
|
+
try {
|
|
173
|
+
await writeFile(this.indexPath, JSON.stringify(this.cacheIndex, null, 2));
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await release();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Cache persistence is best-effort; query execution must continue.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
computeFormatterOptionsHash() {
|
|
184
|
+
if (Object.keys(this.formatterOptions).length === 0)
|
|
185
|
+
return '';
|
|
186
|
+
return createHash('sha256').update(JSON.stringify(this.formatterOptions)).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
computeToolsHash(tools) {
|
|
189
|
+
if (!tools || tools.length === 0)
|
|
190
|
+
return '';
|
|
191
|
+
const sorted = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
192
|
+
return createHash('sha256').update(JSON.stringify(sorted)).digest('hex');
|
|
193
|
+
}
|
|
194
|
+
computeElementHashes(params) {
|
|
195
|
+
const hashes = [];
|
|
196
|
+
for (const element of params.instructions ?? []) {
|
|
197
|
+
hashes.push(`i:${createHash('sha256').update(JSON.stringify(element)).digest('hex')}`);
|
|
198
|
+
}
|
|
199
|
+
for (const element of params.data ?? []) {
|
|
200
|
+
hashes.push(`d:${createHash('sha256').update(JSON.stringify(element)).digest('hex')}`);
|
|
201
|
+
}
|
|
202
|
+
return hashes;
|
|
203
|
+
}
|
|
204
|
+
computeCacheKey(params) {
|
|
205
|
+
const payload = { model: params.model };
|
|
206
|
+
if (params.instructions && params.instructions.length > 0) {
|
|
207
|
+
payload.instructions = params.instructions;
|
|
208
|
+
}
|
|
209
|
+
if (params.data && params.data.length > 0) {
|
|
210
|
+
payload.data = params.data;
|
|
211
|
+
}
|
|
212
|
+
if (Object.keys(this.formatterOptions).length > 0) {
|
|
213
|
+
payload.formatterOptions = this.formatterOptions;
|
|
214
|
+
}
|
|
215
|
+
if (params.tools && params.tools.length > 0) {
|
|
216
|
+
payload.tools = [...params.tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
217
|
+
}
|
|
218
|
+
if (params.reasoningEffort) {
|
|
219
|
+
payload.reasoningEffort = params.reasoningEffort;
|
|
220
|
+
}
|
|
221
|
+
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
222
|
+
}
|
|
223
|
+
generateCachePath(cacheKey) {
|
|
224
|
+
return join(this.cacheDir, `${cacheKey}${CACHE_FILE_EXTENSION}`);
|
|
225
|
+
}
|
|
226
|
+
generateFreshCachePath(cacheKey) {
|
|
227
|
+
const deterministicPath = this.generateCachePath(cacheKey);
|
|
228
|
+
const releasedPath = this.cacheIndex.entries.some((entry) => entry.key === cacheKey
|
|
229
|
+
&& entry.backend === PYTORCH_BACKEND
|
|
230
|
+
&& entry.hint === 'release'
|
|
231
|
+
&& this.getEntryCachePath(entry) === deterministicPath);
|
|
232
|
+
if (!releasedPath)
|
|
233
|
+
return deterministicPath;
|
|
234
|
+
return join(this.cacheDir, `${cacheKey}-${randomBytes(6).toString('hex')}${CACHE_FILE_EXTENSION}`);
|
|
235
|
+
}
|
|
236
|
+
getIndexedCachePath(cacheKey) {
|
|
237
|
+
const indexedPath = this.cacheIndex.entries.find((entry) => entry.key === cacheKey
|
|
238
|
+
&& entry.backend === PYTORCH_BACKEND
|
|
239
|
+
&& entry.hint !== 'release')?.path;
|
|
240
|
+
if (!indexedPath)
|
|
241
|
+
return undefined;
|
|
242
|
+
if (indexedPath.startsWith('memory://'))
|
|
243
|
+
return indexedPath;
|
|
244
|
+
return isAbsolute(indexedPath) ? indexedPath : join(this.cacheDir, indexedPath);
|
|
245
|
+
}
|
|
246
|
+
getEntryCachePath(entry) {
|
|
247
|
+
if (entry.path) {
|
|
248
|
+
if (entry.path.startsWith('memory://'))
|
|
249
|
+
return entry.path;
|
|
250
|
+
return isAbsolute(entry.path) ? entry.path : join(this.cacheDir, entry.path);
|
|
251
|
+
}
|
|
252
|
+
return this.generateCachePath(entry.key);
|
|
253
|
+
}
|
|
254
|
+
toIndexCachePath(cachePath) {
|
|
255
|
+
if (cachePath.startsWith('memory://'))
|
|
256
|
+
return cachePath;
|
|
257
|
+
const absoluteCachePath = isAbsolute(cachePath) ? cachePath : resolvePath(cachePath);
|
|
258
|
+
const relativePath = relative(this.cacheDir, absoluteCachePath);
|
|
259
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`)) {
|
|
260
|
+
return cachePath;
|
|
261
|
+
}
|
|
262
|
+
return relativePath;
|
|
263
|
+
}
|
|
264
|
+
updateLastCache(handle, elementHashes, params) {
|
|
265
|
+
this.lastHandle = handle;
|
|
266
|
+
this.lastElementHashes = elementHashes;
|
|
267
|
+
this.lastHandleModel = params.model;
|
|
268
|
+
this.lastHandleFormatterOptionsHash = this.computeFormatterOptionsHash();
|
|
269
|
+
this.lastHandleToolsHash = this.computeToolsHash(params.tools);
|
|
270
|
+
this.lastHandleReasoningEffort = params.reasoningEffort ?? '';
|
|
271
|
+
}
|
|
272
|
+
clearLastCache() {
|
|
273
|
+
this.lastHandle = undefined;
|
|
274
|
+
this.lastElementHashes = undefined;
|
|
275
|
+
this.lastHandleModel = undefined;
|
|
276
|
+
this.lastHandleFormatterOptionsHash = undefined;
|
|
277
|
+
this.lastHandleToolsHash = undefined;
|
|
278
|
+
this.lastHandleReasoningEffort = undefined;
|
|
279
|
+
}
|
|
280
|
+
computeTokenPrefixHash(tokens, length) {
|
|
281
|
+
const buffer = Buffer.alloc(length * 4);
|
|
282
|
+
for (let i = 0; i < length; i++) {
|
|
283
|
+
buffer.writeInt32LE(tokens[i], i * 4);
|
|
284
|
+
}
|
|
285
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
286
|
+
}
|
|
287
|
+
async computePrefixInfo(params, fullTokens, tools) {
|
|
288
|
+
const instructions = params.instructions ?? [];
|
|
289
|
+
const data = params.data ?? [];
|
|
290
|
+
const boundaryIndices = new Set();
|
|
291
|
+
if (instructions.length > 0 && data.length > 0) {
|
|
292
|
+
boundaryIndices.add(instructions.length - 1);
|
|
293
|
+
}
|
|
294
|
+
// Instructions are already section boundaries. Data may extend the
|
|
295
|
+
// prefix only while it remains immutable and contiguous.
|
|
296
|
+
let lastImmutableIndex = -1;
|
|
297
|
+
for (let i = 0; i < data.length; i++) {
|
|
298
|
+
if (data[i].cacheHint === 'immutable') {
|
|
299
|
+
lastImmutableIndex = instructions.length + i;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (lastImmutableIndex >= 0) {
|
|
306
|
+
boundaryIndices.add(lastImmutableIndex);
|
|
307
|
+
}
|
|
308
|
+
const offsets = [];
|
|
309
|
+
const hashes = [];
|
|
310
|
+
const addPrefix = (offset) => {
|
|
311
|
+
if (offset <= 0 || offsets.includes(offset))
|
|
312
|
+
return;
|
|
313
|
+
offsets.push(offset);
|
|
314
|
+
hashes.push(this.computeTokenPrefixHash(fullTokens, offset));
|
|
315
|
+
};
|
|
316
|
+
for (const boundaryIndex of [...boundaryIndices].sort((a, b) => a - b)) {
|
|
317
|
+
const partialInstructions = boundaryIndex < instructions.length
|
|
318
|
+
? instructions.slice(0, boundaryIndex + 1)
|
|
319
|
+
: instructions;
|
|
320
|
+
const partialData = boundaryIndex >= instructions.length
|
|
321
|
+
? data.slice(0, boundaryIndex - instructions.length + 1)
|
|
322
|
+
: [];
|
|
323
|
+
const partialPrompt = {
|
|
324
|
+
instructions: partialInstructions,
|
|
325
|
+
data: partialData,
|
|
326
|
+
output: [],
|
|
327
|
+
};
|
|
328
|
+
const chatMessages = formatPromptAsMessages(partialPrompt, this.formatterOptions);
|
|
329
|
+
let inferenceMessages = convertMessages(chatMessages, false);
|
|
330
|
+
if (this.messageProcessor) {
|
|
331
|
+
inferenceMessages = this.messageProcessor(inferenceMessages);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const result = await this.process.tokenize(inferenceMessages, tools, params.reasoningEffort);
|
|
335
|
+
if (result.error || !result.token_ids)
|
|
336
|
+
continue;
|
|
337
|
+
let commonLength = 0;
|
|
338
|
+
const maxLength = Math.min(result.token_ids.length, fullTokens.length);
|
|
339
|
+
for (let i = 0; i < maxLength; i++) {
|
|
340
|
+
if (result.token_ids[i] !== fullTokens[i])
|
|
341
|
+
break;
|
|
342
|
+
commonLength = i + 1;
|
|
343
|
+
}
|
|
344
|
+
addPrefix(commonLength);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// A tokenization failure only disables incremental matching.
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// The full sequence is needed to validate exact/superset candidates.
|
|
351
|
+
addPrefix(fullTokens.length);
|
|
352
|
+
return { offsets, hashes };
|
|
353
|
+
}
|
|
354
|
+
async findBestBase(params, fullTokens) {
|
|
355
|
+
// PyTorch VLM is not supported by the current backend. Keep this guard so
|
|
356
|
+
// a direct controller call cannot accidentally create an image cache.
|
|
357
|
+
if (this.modelKind === 'vlm')
|
|
358
|
+
return undefined;
|
|
359
|
+
const newHashes = this.computeElementHashes(params);
|
|
360
|
+
if (newHashes.length === 0)
|
|
361
|
+
return undefined;
|
|
362
|
+
const formatterOptionsHash = this.computeFormatterOptionsHash();
|
|
363
|
+
const toolsHash = this.computeToolsHash(params.tools);
|
|
364
|
+
const candidates = [];
|
|
365
|
+
const staleKeys = [];
|
|
366
|
+
for (const entry of this.cacheIndex.entries) {
|
|
367
|
+
if (entry.backend !== PYTORCH_BACKEND)
|
|
368
|
+
continue;
|
|
369
|
+
if (entry.hint === 'release')
|
|
370
|
+
continue;
|
|
371
|
+
if (entry.model !== params.model
|
|
372
|
+
|| entry.formatterOptionsHash !== formatterOptionsHash
|
|
373
|
+
|| (entry.toolsHash ?? '') !== toolsHash
|
|
374
|
+
|| (entry.reasoningEffort ?? '') !== (params.reasoningEffort ?? '')) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const path = this.getEntryCachePath(entry);
|
|
378
|
+
if (existsSync(path) && this.readMetaTokenCount(path) > 0) {
|
|
379
|
+
candidates.push({ path, elementHashes: entry.elementHashes, label: entry.key.slice(0, 8) });
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
staleKeys.push(entry.key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (this.lastHandle?.ref
|
|
386
|
+
&& this.lastElementHashes
|
|
387
|
+
&& existsSync(this.lastHandle.ref)
|
|
388
|
+
&& this.readMetaTokenCount(this.lastHandle.ref) > 0) {
|
|
389
|
+
const lastCompatible = this.lastHandleModel === params.model
|
|
390
|
+
&& this.lastHandleFormatterOptionsHash === formatterOptionsHash
|
|
391
|
+
&& (this.lastHandleToolsHash ?? '') === toolsHash
|
|
392
|
+
&& (this.lastHandleReasoningEffort ?? '') === (params.reasoningEffort ?? '');
|
|
393
|
+
if (lastCompatible && !candidates.some((candidate) => candidate.path === this.lastHandle.ref)) {
|
|
394
|
+
candidates.push({
|
|
395
|
+
path: this.lastHandle.ref,
|
|
396
|
+
elementHashes: this.lastElementHashes,
|
|
397
|
+
label: 'lastHandle',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (staleKeys.length > 0) {
|
|
402
|
+
this.cacheIndex.entries = this.cacheIndex.entries.filter((entry) => !(staleKeys.includes(entry.key) && entry.backend === PYTORCH_BACKEND));
|
|
403
|
+
this.saveIndex().catch(() => { });
|
|
404
|
+
}
|
|
405
|
+
if (candidates.length === 0)
|
|
406
|
+
return undefined;
|
|
407
|
+
const matchedCandidates = [];
|
|
408
|
+
for (const candidate of candidates) {
|
|
409
|
+
const maxLength = Math.min(candidate.elementHashes.length, newHashes.length);
|
|
410
|
+
let matchLength = 0;
|
|
411
|
+
for (let i = 0; i < maxLength; i++) {
|
|
412
|
+
if (candidate.elementHashes[i] !== newHashes[i])
|
|
413
|
+
break;
|
|
414
|
+
matchLength++;
|
|
415
|
+
}
|
|
416
|
+
if (matchLength > 0) {
|
|
417
|
+
matchedCandidates.push({ candidate, elementMatchLength: matchLength });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (matchedCandidates.length === 0)
|
|
421
|
+
return undefined;
|
|
422
|
+
let bestMatchOffset = 0;
|
|
423
|
+
let bestInfo;
|
|
424
|
+
for (const { candidate, elementMatchLength } of matchedCandidates) {
|
|
425
|
+
const meta = this.readPrefixMeta(candidate.path);
|
|
426
|
+
if (elementMatchLength === candidate.elementHashes.length && elementMatchLength >= newHashes.length) {
|
|
427
|
+
const tokenCount = meta?.tokenCount ?? this.readMetaTokenCount(candidate.path);
|
|
428
|
+
if (tokenCount > bestMatchOffset) {
|
|
429
|
+
bestMatchOffset = tokenCount;
|
|
430
|
+
bestInfo = {
|
|
431
|
+
path: candidate.path,
|
|
432
|
+
coversAll: true,
|
|
433
|
+
sourceElementHashes: candidate.elementHashes,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!meta || meta.prefixOffsets.length === 0) {
|
|
439
|
+
logger.debug(`findBestBase: skip ${candidate.label} (no prefix meta)`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
let matchOffset = 0;
|
|
443
|
+
for (let i = 0; i < meta.prefixHashes.length; i++) {
|
|
444
|
+
const offset = meta.prefixOffsets[i];
|
|
445
|
+
if (typeof offset !== 'number' || offset <= 0 || offset > fullTokens.length)
|
|
446
|
+
break;
|
|
447
|
+
if (this.computeTokenPrefixHash(fullTokens, offset) !== meta.prefixHashes[i])
|
|
448
|
+
break;
|
|
449
|
+
matchOffset = offset;
|
|
450
|
+
}
|
|
451
|
+
if (matchOffset > 0 && matchOffset > bestMatchOffset) {
|
|
452
|
+
bestMatchOffset = matchOffset;
|
|
453
|
+
bestInfo = {
|
|
454
|
+
path: candidate.path,
|
|
455
|
+
trimTokens: matchOffset,
|
|
456
|
+
coversAll: elementMatchLength >= newHashes.length,
|
|
457
|
+
sourceElementHashes: candidate.elementHashes,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (bestInfo) {
|
|
462
|
+
logger.verbose(`findBestBase: match at ${bestMatchOffset} tokens`, bestInfo.trimTokens != null ? `(trim to ${bestInfo.trimTokens} tokens)` : '', bestInfo.coversAll ? '(covers all)' : '');
|
|
463
|
+
}
|
|
464
|
+
return bestInfo;
|
|
465
|
+
}
|
|
466
|
+
addToIndex(params, cacheKey, cachePath) {
|
|
467
|
+
const existing = this.cacheIndex.entries.find((entry) => entry.key === cacheKey
|
|
468
|
+
&& entry.backend === PYTORCH_BACKEND
|
|
469
|
+
&& entry.hint !== 'release');
|
|
470
|
+
if (existing) {
|
|
471
|
+
existing.backend = PYTORCH_BACKEND;
|
|
472
|
+
existing.path = this.toIndexCachePath(cachePath);
|
|
473
|
+
existing.hint = undefined;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
this.cacheIndex.entries.push({
|
|
477
|
+
key: cacheKey,
|
|
478
|
+
model: params.model,
|
|
479
|
+
formatterOptionsHash: this.computeFormatterOptionsHash(),
|
|
480
|
+
elementHashes: this.computeElementHashes(params),
|
|
481
|
+
toolsHash: this.computeToolsHash(params.tools),
|
|
482
|
+
reasoningEffort: params.reasoningEffort,
|
|
483
|
+
createdAt: new Date().toISOString(),
|
|
484
|
+
backend: PYTORCH_BACKEND,
|
|
485
|
+
path: this.toIndexCachePath(cachePath),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
recordQuery() {
|
|
489
|
+
this.stats.totalQueries++;
|
|
490
|
+
}
|
|
491
|
+
recordPromptTokens(newPromptTokens, cacheTokensUsed) {
|
|
492
|
+
this.stats.totalPromptTokens += newPromptTokens + cacheTokensUsed;
|
|
493
|
+
this.stats.totalCacheTokensUsed += cacheTokensUsed;
|
|
494
|
+
}
|
|
495
|
+
readCacheTokenCount(cachePath) {
|
|
496
|
+
return this.cacheTokenCounts.get(cachePath) ?? this.readMetaTokenCount(cachePath);
|
|
497
|
+
}
|
|
498
|
+
getStats() {
|
|
499
|
+
const stats = this.stats;
|
|
500
|
+
return {
|
|
501
|
+
totalQueries: stats.totalQueries,
|
|
502
|
+
memoryHit: stats.memoryHit,
|
|
503
|
+
diskHit: stats.diskHit,
|
|
504
|
+
incremental: stats.incremental,
|
|
505
|
+
fresh: stats.fresh,
|
|
506
|
+
totalPromptTokens: stats.totalPromptTokens,
|
|
507
|
+
prefillReusedTokens: stats.prefillReusedTokens,
|
|
508
|
+
cacheGrowthTokens: stats.prefillTokens - stats.prefillReusedTokens,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async prepare(params) {
|
|
512
|
+
if (!this.bound) {
|
|
513
|
+
throw new Error('PyTorchCacheController is not bound to a process');
|
|
514
|
+
}
|
|
515
|
+
const hasContent = (params.instructions?.length ?? 0) > 0 || (params.data?.length ?? 0) > 0;
|
|
516
|
+
if (!hasContent) {
|
|
517
|
+
throw new Error('Cannot prepare cache with no cacheable content');
|
|
518
|
+
}
|
|
519
|
+
if (this.modelKind === 'vlm') {
|
|
520
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
521
|
+
}
|
|
522
|
+
const cacheKey = this.computeCacheKey(params);
|
|
523
|
+
const existing = this.cacheByHash.get(cacheKey);
|
|
524
|
+
if (existing) {
|
|
525
|
+
this.stats.memoryHit++;
|
|
526
|
+
logger.verbose('cache hit', cacheKey.slice(0, 12));
|
|
527
|
+
return existing;
|
|
528
|
+
}
|
|
529
|
+
const inflight = this.inflightRequests.get(cacheKey);
|
|
530
|
+
if (inflight)
|
|
531
|
+
return inflight;
|
|
532
|
+
const prepareStart = performance.now();
|
|
533
|
+
const promise = this.createCache(params, cacheKey);
|
|
534
|
+
this.inflightRequests.set(cacheKey, promise);
|
|
535
|
+
try {
|
|
536
|
+
const handle = await promise;
|
|
537
|
+
logger.verbose(`prepare total ${(performance.now() - prepareStart).toFixed(0)}ms`, cacheKey.slice(0, 12));
|
|
538
|
+
return handle;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
this.inflightRequests.delete(cacheKey);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
isUnsupportedIncrementalError(error) {
|
|
545
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
546
|
+
return /does not support (incremental prefill|cache prefix metadata)/i.test(message);
|
|
547
|
+
}
|
|
548
|
+
async createCache(params, cacheKey) {
|
|
549
|
+
try {
|
|
550
|
+
await this.ensureCacheDir();
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
logger.verbose('cache dir creation failed, skipping cache:', error instanceof Error ? error.message : String(error));
|
|
554
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
555
|
+
}
|
|
556
|
+
const indexedPath = this.getIndexedCachePath(cacheKey);
|
|
557
|
+
const cachePath = indexedPath
|
|
558
|
+
&& existsSync(indexedPath)
|
|
559
|
+
&& existsSync(indexedPath + '.meta.json')
|
|
560
|
+
&& this.readMetaTokenCount(indexedPath) > 0
|
|
561
|
+
? indexedPath
|
|
562
|
+
: this.generateFreshCachePath(cacheKey);
|
|
563
|
+
let effectiveCachePath = cachePath;
|
|
564
|
+
const elementHashes = this.computeElementHashes(params);
|
|
565
|
+
let supersededRef;
|
|
566
|
+
if (existsSync(cachePath)
|
|
567
|
+
&& existsSync(cachePath + '.meta.json')
|
|
568
|
+
&& this.readMetaTokenCount(cachePath) > 0) {
|
|
569
|
+
this.stats.diskHit++;
|
|
570
|
+
this.cacheTokenCounts.set(cachePath, this.readMetaTokenCount(cachePath));
|
|
571
|
+
logger.verbose('reusing existing cache file', cacheKey.slice(0, 12));
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const prefillPrompt = {
|
|
575
|
+
instructions: params.instructions ?? [],
|
|
576
|
+
data: params.data ?? [],
|
|
577
|
+
output: [],
|
|
578
|
+
};
|
|
579
|
+
const chatMessages = formatPromptAsMessages(prefillPrompt, this.formatterOptions);
|
|
580
|
+
let inferenceMessages = convertMessages(chatMessages, false);
|
|
581
|
+
if (this.messageProcessor) {
|
|
582
|
+
inferenceMessages = this.messageProcessor(inferenceMessages);
|
|
583
|
+
}
|
|
584
|
+
const hasTools = (params.tools?.length ?? 0) > 0;
|
|
585
|
+
const tools = hasTools ? convertToolDefinitions(params.tools) : undefined;
|
|
586
|
+
let fullTokens;
|
|
587
|
+
try {
|
|
588
|
+
const tokenResult = await this.process.tokenize(inferenceMessages, tools, params.reasoningEffort);
|
|
589
|
+
if (!tokenResult.error && tokenResult.token_ids) {
|
|
590
|
+
fullTokens = tokenResult.token_ids;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Cache creation can continue without incremental prefix matching.
|
|
595
|
+
}
|
|
596
|
+
const base = fullTokens ? await this.findBestBase(params, fullTokens) : undefined;
|
|
597
|
+
if (base?.coversAll) {
|
|
598
|
+
this.stats.diskHit++;
|
|
599
|
+
const handle = {
|
|
600
|
+
ref: base.path,
|
|
601
|
+
trimTokens: base.trimTokens,
|
|
602
|
+
includes: {
|
|
603
|
+
instructions: (params.instructions?.length ?? 0) > 0,
|
|
604
|
+
dataElementCount: params.data?.length ?? 0,
|
|
605
|
+
tools: hasTools,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
this.cacheByHash.set(cacheKey, handle);
|
|
609
|
+
this.updateLastCache(handle, base.sourceElementHashes, params);
|
|
610
|
+
return handle;
|
|
611
|
+
}
|
|
612
|
+
if (params.readOnly) {
|
|
613
|
+
logger.verbose('read-only cache miss', cacheKey.slice(0, 12));
|
|
614
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
615
|
+
}
|
|
616
|
+
let prefixOffsets;
|
|
617
|
+
let prefixHashes;
|
|
618
|
+
if (fullTokens) {
|
|
619
|
+
const prefixInfo = await this.computePrefixInfo(params, fullTokens, tools);
|
|
620
|
+
if (prefixInfo.offsets.length > 0) {
|
|
621
|
+
prefixOffsets = prefixInfo.offsets;
|
|
622
|
+
prefixHashes = prefixInfo.hashes;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const prefillStart = performance.now();
|
|
626
|
+
let prefillResult;
|
|
627
|
+
let usedBase = base;
|
|
628
|
+
try {
|
|
629
|
+
prefillResult = await this.process.cachePrefill(cachePath, inferenceMessages, base?.path, base?.trimTokens, prefixOffsets, prefixHashes, tools, params.reasoningEffort, params.images, params.maxImageSize);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
// The CUDA template intentionally rejects the CPU template's
|
|
633
|
+
// incremental/prefix metadata. Retry a plain prefill so one
|
|
634
|
+
// controller works with either PyTorch runtime variant.
|
|
635
|
+
if (!this.isUnsupportedIncrementalError(error) || (!usedBase && !prefixOffsets)) {
|
|
636
|
+
logger.verbose('prefill failed, skipping cache:', error instanceof Error ? error.message : String(error));
|
|
637
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
prefillResult = await this.process.cachePrefill(cachePath, inferenceMessages, undefined, undefined, undefined, undefined, tools, params.reasoningEffort, params.images, params.maxImageSize);
|
|
641
|
+
usedBase = undefined;
|
|
642
|
+
}
|
|
643
|
+
catch (retryError) {
|
|
644
|
+
logger.verbose('prefill failed, skipping cache:', retryError instanceof Error ? retryError.message : String(retryError));
|
|
645
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (typeof prefillResult?.cache_path === 'string' && prefillResult.cache_path.length > 0) {
|
|
649
|
+
effectiveCachePath = prefillResult.cache_path;
|
|
650
|
+
}
|
|
651
|
+
const returnedTokenCount = typeof prefillResult?.token_count === 'number'
|
|
652
|
+
? Math.max(0, prefillResult.token_count)
|
|
653
|
+
: undefined;
|
|
654
|
+
const tokenCount = returnedTokenCount ?? this.readMetaTokenCount(effectiveCachePath);
|
|
655
|
+
if (tokenCount > 0) {
|
|
656
|
+
this.cacheTokenCounts.set(effectiveCachePath, tokenCount);
|
|
657
|
+
}
|
|
658
|
+
const reusedTokens = usedBase
|
|
659
|
+
? usedBase.trimTokens ?? this.readMetaTokenCount(usedBase.path)
|
|
660
|
+
: 0;
|
|
661
|
+
this.stats.prefillTokens += tokenCount;
|
|
662
|
+
this.stats.prefillReusedTokens += reusedTokens;
|
|
663
|
+
if (usedBase) {
|
|
664
|
+
this.stats.incremental++;
|
|
665
|
+
supersededRef = usedBase.path;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
this.stats.fresh++;
|
|
669
|
+
}
|
|
670
|
+
logger.verbose(`prefill ${(performance.now() - prefillStart).toFixed(0)}ms`, usedBase ? '(incremental)' : '(fresh)');
|
|
671
|
+
if (this.closed) {
|
|
672
|
+
await unlink(effectiveCachePath).catch(() => { });
|
|
673
|
+
await unlink(effectiveCachePath + '.meta.json').catch(() => { });
|
|
674
|
+
return PyTorchCacheController.EMPTY_HANDLE;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const handle = {
|
|
678
|
+
ref: effectiveCachePath,
|
|
679
|
+
includes: {
|
|
680
|
+
instructions: (params.instructions?.length ?? 0) > 0,
|
|
681
|
+
dataElementCount: params.data?.length ?? 0,
|
|
682
|
+
tools: (params.tools?.length ?? 0) > 0,
|
|
683
|
+
},
|
|
684
|
+
supersedes: supersededRef,
|
|
685
|
+
};
|
|
686
|
+
this.cacheByHash.set(cacheKey, handle);
|
|
687
|
+
this.updateLastCache(handle, elementHashes, params);
|
|
688
|
+
this.addToIndex(params, cacheKey, effectiveCachePath);
|
|
689
|
+
if (supersededRef) {
|
|
690
|
+
this.release(supersededRef);
|
|
691
|
+
}
|
|
692
|
+
await this.saveIndex();
|
|
693
|
+
return handle;
|
|
694
|
+
}
|
|
695
|
+
release(ref) {
|
|
696
|
+
logger.debug('release', ref);
|
|
697
|
+
const entry = this.cacheIndex.entries.find((candidate) => candidate.backend === PYTORCH_BACKEND && this.getEntryCachePath(candidate) === ref);
|
|
698
|
+
if (entry)
|
|
699
|
+
entry.hint = 'release';
|
|
700
|
+
for (const [key, handle] of this.cacheByHash) {
|
|
701
|
+
if (handle.ref === ref)
|
|
702
|
+
this.cacheByHash.delete(key);
|
|
703
|
+
}
|
|
704
|
+
if (this.lastHandle?.ref === ref)
|
|
705
|
+
this.clearLastCache();
|
|
706
|
+
this.saveIndex().catch(() => { });
|
|
707
|
+
}
|
|
708
|
+
async close() {
|
|
709
|
+
this.closed = true;
|
|
710
|
+
const timeout = new Promise((resolve) => {
|
|
711
|
+
const timer = setTimeout(resolve, 30_000);
|
|
712
|
+
timer.unref();
|
|
713
|
+
});
|
|
714
|
+
await Promise.race([
|
|
715
|
+
Promise.allSettled([...this.inflightRequests.values()]),
|
|
716
|
+
timeout,
|
|
717
|
+
]);
|
|
718
|
+
this.inflightRequests.clear();
|
|
719
|
+
this.cacheByHash.clear();
|
|
720
|
+
this.cacheTokenCounts.clear();
|
|
721
|
+
this.clearLastCache();
|
|
722
|
+
if (this.managedDir && this.cacheDir) {
|
|
723
|
+
await rm(this.cacheDir, { recursive: true, force: true }).catch(() => { });
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
const released = this.cacheIndex.entries.filter((entry) => entry.backend === PYTORCH_BACKEND && entry.hint === 'release');
|
|
727
|
+
await Promise.allSettled(released.flatMap((entry) => {
|
|
728
|
+
const path = this.getEntryCachePath(entry);
|
|
729
|
+
if (path.startsWith('memory://'))
|
|
730
|
+
return [];
|
|
731
|
+
return [unlink(path), unlink(path + '.meta.json')];
|
|
732
|
+
}));
|
|
733
|
+
this.cacheIndex.entries = this.cacheIndex.entries.filter((entry) => !(entry.backend === PYTORCH_BACKEND && entry.hint === 'release'));
|
|
734
|
+
await this.saveIndex();
|
|
735
|
+
}
|
|
736
|
+
this.cacheDirReady = false;
|
|
737
|
+
if (this.cleanupHandler) {
|
|
738
|
+
globalThis.process.removeListener('exit', this.cleanupHandler);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
//# sourceMappingURL=pytorch-cache-controller.js.map
|