sliftutils 1.7.13 → 1.7.14
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/.claude/settings.local.json +2 -1
- package/index.d.ts +64 -38
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/misc/https/hostServer.d.ts +7 -0
- package/misc/https/hostServer.ts +89 -20
- package/misc/openrouter.ts +17 -14
- package/package.json +2 -2
- package/storage/ArchivesDisk.d.ts +1 -1
- package/storage/ArchivesDisk.ts +2 -1
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
- package/storage/IArchives.d.ts +5 -1
- package/storage/IArchives.ts +5 -1
- package/storage/backblaze.d.ts +1 -1
- package/storage/backblaze.ts +9 -6
- package/storage/remoteStorage/ArchivesRemote.d.ts +1 -1
- package/storage/remoteStorage/ArchivesRemote.ts +2 -1
- package/storage/remoteStorage/ArchivesUrl.d.ts +1 -1
- package/storage/remoteStorage/ArchivesUrl.ts +13 -9
- package/storage/remoteStorage/blobStore.d.ts +6 -2
- package/storage/remoteStorage/blobStore.ts +46 -6
- package/storage/remoteStorage/createArchives.d.ts +10 -11
- package/storage/remoteStorage/createArchives.ts +74 -26
- package/storage/remoteStorage/deployTakeover.d.ts +24 -0
- package/storage/remoteStorage/deployTakeover.ts +373 -0
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +16 -11
- package/storage/remoteStorage/dist/blobStore.ts.cache +39 -4
- package/storage/remoteStorage/dist/createArchives.ts.cache +91 -9
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +371 -0
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +9 -3
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +3 -3
- package/storage/remoteStorage/dist/storageController.ts.cache +11 -12
- package/storage/remoteStorage/dist/storageServerState.ts.cache +120 -19
- package/storage/remoteStorage/remoteConfig.d.ts +1 -0
- package/storage/remoteStorage/remoteConfig.ts +6 -0
- package/storage/remoteStorage/storageServer.ts +14 -1
- package/storage/remoteStorage/storageServerState.d.ts +3 -0
- package/storage/remoteStorage/storageServerState.ts +55 -8
- package/yarn.lock +4 -4
- package/storage/dist/faceFramesData.ts.cache +0 -55
- package/storage/embeddingBench.d.ts +0 -1
- package/storage/embeddingBench.ts +0 -152
- package/storage/faceFramesData.d.ts +0 -1
- package/storage/faceFramesData.ts +0 -49
- package/storage/faceFramesServer.d.ts +0 -1
- package/storage/faceFramesServer.ts +0 -56
- package/storage/proxydatabase/ivfDbCheck.d.ts +0 -1
- package/storage/proxydatabase/ivfDbCheck.ts +0 -85
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
WRITE_PAST_WINDOW_GRACE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
|
|
12
12
|
} from "../IArchives";
|
|
13
13
|
import { ROUTING_FILE, parseRoutingData, parseHostedUrl, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
|
|
14
|
+
import { applyDeployRemap, getFlushDeadline, onTakeoverEvent } from "./deployTakeover";
|
|
14
15
|
import { createApiArchives } from "./createArchives";
|
|
15
16
|
import type { IStorage } from "../IStorage";
|
|
16
17
|
import type { AccessRequest, TrustRecord } from "./storageController";
|
|
@@ -157,6 +158,13 @@ function getBucketFolder(account: string, bucketName: string): string {
|
|
|
157
158
|
return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
// Ports we are listening on beyond the main config port - a deploy-takeover successor serves on a
|
|
162
|
+
// temporary alternate port, and must recognize alternate-port source URLs as itself
|
|
163
|
+
const extraListenPorts = new Set<number>();
|
|
164
|
+
export function addExtraListenPort(port: number): void {
|
|
165
|
+
extraListenPorts.add(port);
|
|
166
|
+
}
|
|
167
|
+
|
|
160
168
|
function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
|
|
161
169
|
let { domain, port } = getStorageServerConfig();
|
|
162
170
|
let indexes: number[] = [];
|
|
@@ -164,7 +172,7 @@ function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: str
|
|
|
164
172
|
let source = routing.sources[i];
|
|
165
173
|
if (typeof source === "string" || source.type !== "remote") continue;
|
|
166
174
|
let parsed = parseHostedUrl(source.url);
|
|
167
|
-
if (parsed.address === domain && parsed.port === port && parsed.account === account && parsed.bucketName === bucketName) {
|
|
175
|
+
if (parsed.address === domain && (parsed.port === port || extraListenPorts.has(parsed.port)) && parsed.account === account && parsed.bucketName === bucketName) {
|
|
168
176
|
indexes.push(i);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
@@ -223,12 +231,17 @@ function scheduleWindowBoundaryRebuild(loaded: LoadedBucket): void {
|
|
|
223
231
|
|
|
224
232
|
function buildBucket(account: string, bucketName: string, routing: RemoteConfig): LoadedBucket {
|
|
225
233
|
let folder = getBucketFolder(account, bucketName);
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
// The deploy-takeover remap is an INTERPRETATION overlay: everything behavioral (self entries,
|
|
235
|
+
// windows, downstream sources) uses the remapped view, while loaded.routing/routingJSON stay
|
|
236
|
+
// the STORED config - so version guards, change detection, and synchronization never see (or
|
|
237
|
+
// persist) the remap
|
|
238
|
+
let effective = applyDeployRemap(routing);
|
|
239
|
+
let selfIndexes = findSelfIndexes(effective, account, bucketName);
|
|
240
|
+
let selfEntries = selfIndexes.map(i => effective.sources[i] as HostedConfig);
|
|
228
241
|
let self = selectEntryAt(selfEntries, Date.now());
|
|
229
242
|
let selfIndex = -1;
|
|
230
243
|
if (self) {
|
|
231
|
-
selfIndex =
|
|
244
|
+
selfIndex = effective.sources.indexOf(self);
|
|
232
245
|
}
|
|
233
246
|
let store: IBucketStore;
|
|
234
247
|
if (self?.rawDisk) {
|
|
@@ -251,8 +264,8 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
|
|
|
251
264
|
if (selfIndex === -1) {
|
|
252
265
|
console.log(`This server is not in the routing config for bucket ${account}/${bucketName}; keeping its data on disk but no longer synchronizing it`);
|
|
253
266
|
} else {
|
|
254
|
-
for (let i = selfIndex + 1; i <
|
|
255
|
-
let source =
|
|
267
|
+
for (let i = selfIndex + 1; i < effective.sources.length; i++) {
|
|
268
|
+
let source = effective.sources[i];
|
|
256
269
|
if (typeof source === "string" || ownIndexes.has(i)) continue;
|
|
257
270
|
// Disjoint-shard sources never talk to each other; a partial overlap syncs only
|
|
258
271
|
// the intersection (scans ignore the rest, writes only send matching keys)
|
|
@@ -271,6 +284,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
|
|
|
271
284
|
void scheduleRoutingReload(account, bucketName);
|
|
272
285
|
},
|
|
273
286
|
readerDiskLimit: self?.readerDiskLimit,
|
|
287
|
+
getFlushDeadline,
|
|
274
288
|
});
|
|
275
289
|
}
|
|
276
290
|
let loaded: LoadedBucket = { account, bucketName, routing, routingJSON: JSON.stringify(routing), selfEntries, self, store };
|
|
@@ -421,7 +435,9 @@ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
|
|
|
421
435
|
let progress = bucket.store.getSyncProgress?.();
|
|
422
436
|
return {
|
|
423
437
|
supportsChangesAfter: !!bucket.store.getChangesAfter,
|
|
424
|
-
|
|
438
|
+
// The remapped (interpretation) view: getConfig is how clients learn of an in-progress
|
|
439
|
+
// deploy takeover, since the remap is never written into the routing file itself
|
|
440
|
+
remoteConfig: applyDeployRemap(bucket.routing),
|
|
425
441
|
index: progress?.index,
|
|
426
442
|
indexSources: progress?.sources,
|
|
427
443
|
readerDiskLimit: progress?.readerDiskLimit,
|
|
@@ -429,6 +445,36 @@ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
|
|
|
429
445
|
};
|
|
430
446
|
}
|
|
431
447
|
|
|
448
|
+
export async function rebuildAllLoadedBuckets(): Promise<void> {
|
|
449
|
+
for (let key of [...buckets.keys()]) {
|
|
450
|
+
let slash = key.indexOf("/");
|
|
451
|
+
await scheduleRoutingReload(key.slice(0, slash), key.slice(slash + 1), { force: true });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function rescanAllLoadedBucketDisks(): Promise<void> {
|
|
456
|
+
for (let loadedPromise of [...buckets.values()]) {
|
|
457
|
+
let loaded = await loadedPromise.catch(() => undefined);
|
|
458
|
+
if (!loaded) continue;
|
|
459
|
+
let store = loaded.store;
|
|
460
|
+
if (!(store instanceof BlobStore)) continue;
|
|
461
|
+
try {
|
|
462
|
+
await store.rescanBase();
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error(`Deploy switchover disk rescan failed for bucket ${loaded.account}/${loaded.bucketName}: ${(e as Error).stack ?? e}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
onTakeoverEvent(event => {
|
|
470
|
+
if (event === "remapChanged") {
|
|
471
|
+
void rebuildAllLoadedBuckets();
|
|
472
|
+
}
|
|
473
|
+
if (event === "diskScan") {
|
|
474
|
+
void rescanAllLoadedBucketDisks();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
432
478
|
export type ServerBucketInfo = {
|
|
433
479
|
bucketName: string;
|
|
434
480
|
// Loaded in memory (created or accessed since startup), so its synchronization is running
|
|
@@ -512,8 +558,9 @@ class ArchivesLocalBucket implements IArchives {
|
|
|
512
558
|
if (!bucket) return undefined;
|
|
513
559
|
return await bucket.store.get2(fileName, config);
|
|
514
560
|
}
|
|
515
|
-
public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<
|
|
561
|
+
public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
|
|
516
562
|
await writeBucketFile(this.account, this.bucketName, fileName, data, config);
|
|
563
|
+
return fileName;
|
|
517
564
|
}
|
|
518
565
|
public async del(fileName: string): Promise<void> {
|
|
519
566
|
await deleteBucketFile(this.account, this.bucketName, fileName);
|
package/yarn.lock
CHANGED
|
@@ -1944,10 +1944,10 @@ slash@^3.0.0:
|
|
|
1944
1944
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
|
1945
1945
|
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
|
1946
1946
|
|
|
1947
|
-
socket-function@^1.2.
|
|
1948
|
-
version "1.2.
|
|
1949
|
-
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.
|
|
1950
|
-
integrity sha512-
|
|
1947
|
+
socket-function@^1.2.20:
|
|
1948
|
+
version "1.2.20"
|
|
1949
|
+
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.20.tgz#8cd141cd84504a320583e09749ed91cee293db45"
|
|
1950
|
+
integrity sha512-aLHs7S9MKb6WY5pjtNG+sfISVIWIv7toeX7Zt5K6f1R/sVFLb46IBElZ0Hf81LHenVYwDNsKN0uAX/fkBBIqlw==
|
|
1951
1951
|
dependencies:
|
|
1952
1952
|
"@types/pako" "^2.0.3"
|
|
1953
1953
|
"@types/ws" "^8.5.3"
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
6
|
-
//exports.ensureFaceData = void 0;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const http_1 = __importDefault(require("http"));
|
|
11
|
-
// Resolves a face-embedding test file to a local path that holds at least the requested number of bytes. On
|
|
12
|
-
// the machine that owns the data it just uses the original file. Anywhere else it downloads the missing prefix
|
|
13
|
-
// from the faceFramesServer over a range request and caches it in the temp dir, so re-runs only fetch what
|
|
14
|
-
// they haven't already. Point it at a different server with FACEFRAMES_URL.
|
|
15
|
-
const SOURCE_DIR = "/root/claude-work/face-embeddings";
|
|
16
|
-
const SERVER_BASE = process.env.FACEFRAMES_URL || "http://65.109.93.113:8799";
|
|
17
|
-
function downloadInto(fileName, start, endExclusive, cachePath) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const url = `${SERVER_BASE}/${fileName}`;
|
|
20
|
-
const options = { headers: { Range: `bytes=${start}-${endExclusive - 1}` } };
|
|
21
|
-
const request = http_1.default.get(url, options, response => {
|
|
22
|
-
if (response.statusCode !== 206 && response.statusCode !== 200) {
|
|
23
|
-
response.resume();
|
|
24
|
-
reject(new Error(`${url} returned ${response.statusCode}`));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const out = fs_1.default.createWriteStream(cachePath, start ? { flags: "r+", start } : { flags: "w" });
|
|
28
|
-
response.pipe(out);
|
|
29
|
-
out.on("error", reject);
|
|
30
|
-
response.on("error", reject);
|
|
31
|
-
out.on("finish", resolve);
|
|
32
|
-
});
|
|
33
|
-
request.on("error", reject);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
async function ensureFaceData(fileName, byteLength) {
|
|
37
|
-
const local = path_1.default.join(SOURCE_DIR, fileName);
|
|
38
|
-
if (fs_1.default.existsSync(local) && fs_1.default.statSync(local).size >= byteLength) {
|
|
39
|
-
return local;
|
|
40
|
-
}
|
|
41
|
-
const cachePath = path_1.default.join(os_1.default.tmpdir(), fileName);
|
|
42
|
-
let cachedBytes = 0;
|
|
43
|
-
if (fs_1.default.existsSync(cachePath)) {
|
|
44
|
-
cachedBytes = fs_1.default.statSync(cachePath).size;
|
|
45
|
-
}
|
|
46
|
-
if (cachedBytes < byteLength) {
|
|
47
|
-
const megabytes = ((byteLength - cachedBytes) / 1e6).toFixed(0);
|
|
48
|
-
console.log(`downloading ${megabytes}MB of ${fileName} from ${SERVER_BASE} -> ${cachePath}`);
|
|
49
|
-
await downloadInto(fileName, cachedBytes, byteLength, cachePath);
|
|
50
|
-
}
|
|
51
|
-
return cachePath;
|
|
52
|
-
}
|
|
53
|
-
exports.ensureFaceData = ensureFaceData;
|
|
54
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjZUZyYW1lc0RhdGEuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJmYWNlRnJhbWVzRGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSw0Q0FBb0I7QUFDcEIsNENBQW9CO0FBQ3BCLGdEQUF3QjtBQUN4QixnREFBd0I7QUFFeEIsNEdBQTRHO0FBQzVHLCtHQUErRztBQUMvRywyR0FBMkc7QUFDM0csNEVBQTRFO0FBQzVFLE1BQU0sVUFBVSxHQUFHLG1DQUFtQyxDQUFDO0FBQ3ZELE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxJQUFJLDJCQUEyQixDQUFDO0FBRTlFLFNBQVMsWUFBWSxDQUFDLFFBQWdCLEVBQUUsS0FBYSxFQUFFLFlBQW9CLEVBQUUsU0FBaUI7SUFDMUYsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNuQyxNQUFNLEdBQUcsR0FBRyxHQUFHLFdBQVcsSUFBSSxRQUFRLEVBQUUsQ0FBQztRQUN6QyxNQUFNLE9BQU8sR0FBRyxFQUFFLE9BQU8sRUFBRSxFQUFFLEtBQUssRUFBRSxTQUFTLEtBQUssSUFBSSxZQUFZLEdBQUcsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDO1FBQzdFLE1BQU0sT0FBTyxHQUFHLGNBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLE9BQU8sRUFBRSxRQUFRLENBQUMsRUFBRTtZQUM5QyxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssR0FBRyxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssR0FBRyxFQUFFLENBQUM7Z0JBQzdELFFBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDbEIsTUFBTSxDQUFDLElBQUksS0FBSyxDQUFDLEdBQUcsR0FBRyxhQUFhLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUFDLENBQUM7Z0JBQzVELE9BQU87WUFDWCxDQUFDO1lBQ0QsTUFBTSxHQUFHLEdBQUcsWUFBRSxDQUFDLGlCQUFpQixDQUFDLFNBQVMsRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztZQUM3RixRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ25CLEdBQUcsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3hCLFFBQVEsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQzdCLEdBQUcsQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQzlCLENBQUMsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDaEMsQ0FBQyxDQUFDLENBQUM7QUFDUCxDQUFDO0FBRU0sS0FBSyxVQUFVLGNBQWMsQ0FBQyxRQUFnQixFQUFFLFVBQWtCO0lBQ3JFLE1BQU0sS0FBSyxHQUFHLGNBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBQzlDLElBQUksWUFBRSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsSUFBSSxZQUFFLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsQ0FBQztRQUNoRSxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0lBQ0QsTUFBTSxTQUFTLEdBQUcsY0FBSSxDQUFDLElBQUksQ0FBQyxZQUFFLENBQUMsTUFBTSxFQUFFLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDbkQsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLElBQUksWUFBRSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO1FBQzNCLFdBQVcsR0FBRyxZQUFFLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxDQUFDLElBQUksQ0FBQztJQUM5QyxDQUFDO0lBQ0QsSUFBSSxXQUFXLEdBQUcsVUFBVSxFQUFFLENBQUM7UUFDM0IsTUFBTSxTQUFTLEdBQUcsQ0FBQyxDQUFDLFVBQVUsR0FBRyxXQUFXLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDaEUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxlQUFlLFNBQVMsU0FBUyxRQUFRLFNBQVMsV0FBVyxPQUFPLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFDN0YsTUFBTSxZQUFZLENBQUMsUUFBUSxFQUFFLFdBQVcsRUFBRSxVQUFVLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDckUsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ3JCLENBQUM7QUFoQkQsd0NBZ0JDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZzIGZyb20gXCJmc1wiO1xuaW1wb3J0IG9zIGZyb20gXCJvc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIjtcbmltcG9ydCBodHRwIGZyb20gXCJodHRwXCI7XG5cbi8vIFJlc29sdmVzIGEgZmFjZS1lbWJlZGRpbmcgdGVzdCBmaWxlIHRvIGEgbG9jYWwgcGF0aCB0aGF0IGhvbGRzIGF0IGxlYXN0IHRoZSByZXF1ZXN0ZWQgbnVtYmVyIG9mIGJ5dGVzLiBPblxuLy8gdGhlIG1hY2hpbmUgdGhhdCBvd25zIHRoZSBkYXRhIGl0IGp1c3QgdXNlcyB0aGUgb3JpZ2luYWwgZmlsZS4gQW55d2hlcmUgZWxzZSBpdCBkb3dubG9hZHMgdGhlIG1pc3NpbmcgcHJlZml4XG4vLyBmcm9tIHRoZSBmYWNlRnJhbWVzU2VydmVyIG92ZXIgYSByYW5nZSByZXF1ZXN0IGFuZCBjYWNoZXMgaXQgaW4gdGhlIHRlbXAgZGlyLCBzbyByZS1ydW5zIG9ubHkgZmV0Y2ggd2hhdFxuLy8gdGhleSBoYXZlbid0IGFscmVhZHkuIFBvaW50IGl0IGF0IGEgZGlmZmVyZW50IHNlcnZlciB3aXRoIEZBQ0VGUkFNRVNfVVJMLlxuY29uc3QgU09VUkNFX0RJUiA9IFwiL3Jvb3QvY2xhdWRlLXdvcmsvZmFjZS1lbWJlZGRpbmdzXCI7XG5jb25zdCBTRVJWRVJfQkFTRSA9IHByb2Nlc3MuZW52LkZBQ0VGUkFNRVNfVVJMIHx8IFwiaHR0cDovLzY1LjEwOS45My4xMTM6ODc5OVwiO1xuXG5mdW5jdGlvbiBkb3dubG9hZEludG8oZmlsZU5hbWU6IHN0cmluZywgc3RhcnQ6IG51bWJlciwgZW5kRXhjbHVzaXZlOiBudW1iZXIsIGNhY2hlUGF0aDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgY29uc3QgdXJsID0gYCR7U0VSVkVSX0JBU0V9LyR7ZmlsZU5hbWV9YDtcbiAgICAgICAgY29uc3Qgb3B0aW9ucyA9IHsgaGVhZGVyczogeyBSYW5nZTogYGJ5dGVzPSR7c3RhcnR9LSR7ZW5kRXhjbHVzaXZlIC0gMX1gIH0gfTtcbiAgICAgICAgY29uc3QgcmVxdWVzdCA9IGh0dHAuZ2V0KHVybCwgb3B0aW9ucywgcmVzcG9uc2UgPT4ge1xuICAgICAgICAgICAgaWYgKHJlc3BvbnNlLnN0YXR1c0NvZGUgIT09IDIwNiAmJiByZXNwb25zZS5zdGF0dXNDb2RlICE9PSAyMDApIHtcbiAgICAgICAgICAgICAgICByZXNwb25zZS5yZXN1bWUoKTtcbiAgICAgICAgICAgICAgICByZWplY3QobmV3IEVycm9yKGAke3VybH0gcmV0dXJuZWQgJHtyZXNwb25zZS5zdGF0dXNDb2RlfWApKTtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBjb25zdCBvdXQgPSBmcy5jcmVhdGVXcml0ZVN0cmVhbShjYWNoZVBhdGgsIHN0YXJ0ID8geyBmbGFnczogXCJyK1wiLCBzdGFydCB9IDogeyBmbGFnczogXCJ3XCIgfSk7XG4gICAgICAgICAgICByZXNwb25zZS5waXBlKG91dCk7XG4gICAgICAgICAgICBvdXQub24oXCJlcnJvclwiLCByZWplY3QpO1xuICAgICAgICAgICAgcmVzcG9uc2Uub24oXCJlcnJvclwiLCByZWplY3QpO1xuICAgICAgICAgICAgb3V0Lm9uKFwiZmluaXNoXCIsIHJlc29sdmUpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmVxdWVzdC5vbihcImVycm9yXCIsIHJlamVjdCk7XG4gICAgfSk7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBlbnN1cmVGYWNlRGF0YShmaWxlTmFtZTogc3RyaW5nLCBieXRlTGVuZ3RoOiBudW1iZXIpOiBQcm9taXNlPHN0cmluZz4ge1xuICAgIGNvbnN0IGxvY2FsID0gcGF0aC5qb2luKFNPVVJDRV9ESVIsIGZpbGVOYW1lKTtcbiAgICBpZiAoZnMuZXhpc3RzU3luYyhsb2NhbCkgJiYgZnMuc3RhdFN5bmMobG9jYWwpLnNpemUgPj0gYnl0ZUxlbmd0aCkge1xuICAgICAgICByZXR1cm4gbG9jYWw7XG4gICAgfVxuICAgIGNvbnN0IGNhY2hlUGF0aCA9IHBhdGguam9pbihvcy50bXBkaXIoKSwgZmlsZU5hbWUpO1xuICAgIGxldCBjYWNoZWRCeXRlcyA9IDA7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMoY2FjaGVQYXRoKSkge1xuICAgICAgICBjYWNoZWRCeXRlcyA9IGZzLnN0YXRTeW5jKGNhY2hlUGF0aCkuc2l6ZTtcbiAgICB9XG4gICAgaWYgKGNhY2hlZEJ5dGVzIDwgYnl0ZUxlbmd0aCkge1xuICAgICAgICBjb25zdCBtZWdhYnl0ZXMgPSAoKGJ5dGVMZW5ndGggLSBjYWNoZWRCeXRlcykgLyAxZTYpLnRvRml4ZWQoMCk7XG4gICAgICAgIGNvbnNvbGUubG9nKGBkb3dubG9hZGluZyAke21lZ2FieXRlc31NQiBvZiAke2ZpbGVOYW1lfSBmcm9tICR7U0VSVkVSX0JBU0V9IC0+ICR7Y2FjaGVQYXRofWApO1xuICAgICAgICBhd2FpdCBkb3dubG9hZEludG8oZmlsZU5hbWUsIGNhY2hlZEJ5dGVzLCBieXRlTGVuZ3RoLCBjYWNoZVBhdGgpO1xuICAgIH1cbiAgICByZXR1cm4gY2FjaGVQYXRoO1xufVxuIl19
|
|
55
|
-
/* _JS_SOURCE_HASH = "623e4d719ca42b7e83579fd05b7d111f3158958917d536b1ac30e1e3373ad1ce"; */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { ensureFaceData } from "./faceFramesData";
|
|
3
|
-
import { encodeEmbedding, averageEmbeddings, embeddingToFloat32, releaseFloat32, getCloseness, StoredEmbedding, EmbeddingFormat } from "./embeddingFormats";
|
|
4
|
-
|
|
5
|
-
// Runs a generic k-means parameterized by an arbitrary getCloseness, so we can race the closeness
|
|
6
|
-
// implementations (all exported from embeddingFormats) on a real clustering workload — plus a variant that
|
|
7
|
-
// decodes every embedding to float32 once and uses plain float dots. Push + run with: typenode storage/embeddingBench.ts
|
|
8
|
-
|
|
9
|
-
const DIM = 512;
|
|
10
|
-
const FILE_NAME = "faceEntry2.f32";
|
|
11
|
-
const MODEL = "buffalo_l";
|
|
12
|
-
const FORMAT: EmbeddingFormat = "q8g8_2048";
|
|
13
|
-
const CONFIG = { format: FORMAT, model: MODEL };
|
|
14
|
-
|
|
15
|
-
async function loadEmbeddings(count: number): Promise<StoredEmbedding[]> {
|
|
16
|
-
const byteLength = count * DIM * 4;
|
|
17
|
-
const filePath = await ensureFaceData(FILE_NAME, byteLength);
|
|
18
|
-
const buffer = Buffer.alloc(byteLength);
|
|
19
|
-
const handle = fs.openSync(filePath, "r");
|
|
20
|
-
fs.readSync(handle, buffer, 0, byteLength, 0);
|
|
21
|
-
fs.closeSync(handle);
|
|
22
|
-
const floats = new Float32Array(buffer.buffer, buffer.byteOffset, count * DIM);
|
|
23
|
-
const out: StoredEmbedding[] = [];
|
|
24
|
-
for (let index = 0; index < count; index++) {
|
|
25
|
-
out.push(encodeEmbedding({ input: floats.subarray(index * DIM, (index + 1) * DIM), format: FORMAT, model: MODEL }));
|
|
26
|
-
}
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Plain k-means: assign each member to the nearest centroid via `closeness`, recompute centroids as the
|
|
31
|
-
// (re-encoded) mean. The closeness function is the only thing that varies.
|
|
32
|
-
function kmeans(members: StoredEmbedding[], clusterCount: number, iterations: number, closeness: (a: StoredEmbedding, b: StoredEmbedding) => number): number {
|
|
33
|
-
let centroids: StoredEmbedding[] = [];
|
|
34
|
-
const seedStride = members.length / clusterCount;
|
|
35
|
-
for (let index = 0; index < clusterCount; index++) {
|
|
36
|
-
centroids.push(members[Math.floor(index * seedStride)]);
|
|
37
|
-
}
|
|
38
|
-
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
39
|
-
const groups: StoredEmbedding[][] = [];
|
|
40
|
-
for (let index = 0; index < centroids.length; index++) {
|
|
41
|
-
groups.push([]);
|
|
42
|
-
}
|
|
43
|
-
for (const member of members) {
|
|
44
|
-
let bestIndex = 0;
|
|
45
|
-
let bestCloseness = -Infinity;
|
|
46
|
-
for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
|
|
47
|
-
const value = closeness(member, centroids[centroidIndex]);
|
|
48
|
-
if (value > bestCloseness) {
|
|
49
|
-
bestCloseness = value;
|
|
50
|
-
bestIndex = centroidIndex;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
groups[bestIndex].push(member);
|
|
54
|
-
}
|
|
55
|
-
const next: StoredEmbedding[] = [];
|
|
56
|
-
for (const group of groups) {
|
|
57
|
-
if (group.length) {
|
|
58
|
-
next.push(averageEmbeddings(group, CONFIG));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
centroids = next;
|
|
62
|
-
}
|
|
63
|
-
return centroids.length;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// The same k-means but every embedding is decoded to a float32 array ONCE up front; centroids stay float
|
|
67
|
-
// arrays and assignment is a plain float dot. No per-comparison decode, no centroid re-encode.
|
|
68
|
-
function kmeansFloat32Once(members: StoredEmbedding[], clusterCount: number, iterations: number): number {
|
|
69
|
-
const memberFloats: Float32Array[] = [];
|
|
70
|
-
for (const member of members) {
|
|
71
|
-
memberFloats.push(embeddingToFloat32(member, true));
|
|
72
|
-
}
|
|
73
|
-
let centroids: Float32Array[] = [];
|
|
74
|
-
const seedStride = members.length / clusterCount;
|
|
75
|
-
for (let index = 0; index < clusterCount; index++) {
|
|
76
|
-
centroids.push(memberFloats[Math.floor(index * seedStride)]);
|
|
77
|
-
}
|
|
78
|
-
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
79
|
-
const groups: number[][] = [];
|
|
80
|
-
for (let index = 0; index < centroids.length; index++) {
|
|
81
|
-
groups.push([]);
|
|
82
|
-
}
|
|
83
|
-
for (let memberIndex = 0; memberIndex < memberFloats.length; memberIndex++) {
|
|
84
|
-
const memberFloat = memberFloats[memberIndex];
|
|
85
|
-
let bestIndex = 0;
|
|
86
|
-
let bestDot = -Infinity;
|
|
87
|
-
for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
|
|
88
|
-
const centroidFloat = centroids[centroidIndex];
|
|
89
|
-
const length = Math.min(memberFloat.length, centroidFloat.length);
|
|
90
|
-
let dot = 0;
|
|
91
|
-
for (let dim = 0; dim < length; dim++) {
|
|
92
|
-
dot += memberFloat[dim] * centroidFloat[dim];
|
|
93
|
-
}
|
|
94
|
-
if (dot > bestDot) {
|
|
95
|
-
bestDot = dot;
|
|
96
|
-
bestIndex = centroidIndex;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
groups[bestIndex].push(memberIndex);
|
|
100
|
-
}
|
|
101
|
-
const next: Float32Array[] = [];
|
|
102
|
-
for (const group of groups) {
|
|
103
|
-
if (!group.length) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const length = memberFloats[group[0]].length;
|
|
107
|
-
const sum = new Float32Array(length);
|
|
108
|
-
for (const memberIndex of group) {
|
|
109
|
-
const memberFloat = memberFloats[memberIndex];
|
|
110
|
-
for (let dim = 0; dim < length; dim++) {
|
|
111
|
-
sum[dim] += memberFloat[dim];
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
let norm = 0;
|
|
115
|
-
for (let dim = 0; dim < length; dim++) {
|
|
116
|
-
norm += sum[dim] * sum[dim];
|
|
117
|
-
}
|
|
118
|
-
const magnitude = Math.sqrt(norm) || 1;
|
|
119
|
-
for (let dim = 0; dim < length; dim++) {
|
|
120
|
-
sum[dim] /= magnitude;
|
|
121
|
-
}
|
|
122
|
-
next.push(sum);
|
|
123
|
-
}
|
|
124
|
-
centroids = next;
|
|
125
|
-
}
|
|
126
|
-
for (const memberFloat of memberFloats) {
|
|
127
|
-
releaseFloat32(memberFloat);
|
|
128
|
-
}
|
|
129
|
-
return centroids.length;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function time(name: string, run: () => number): number {
|
|
133
|
-
const start = Date.now();
|
|
134
|
-
const clusters = run();
|
|
135
|
-
const seconds = (Date.now() - start) / 1000;
|
|
136
|
-
console.log(` ${name.padEnd(10)} ${seconds.toFixed(3)}s (${clusters} clusters)`);
|
|
137
|
-
return seconds;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function main() {
|
|
141
|
-
const members = await loadEmbeddings(5000);
|
|
142
|
-
const clusterCount = 40;
|
|
143
|
-
const iterations = 4;
|
|
144
|
-
console.log(`k-means: ${members.length} members, ${clusterCount} clusters, ${iterations} iterations\n`);
|
|
145
|
-
|
|
146
|
-
kmeans(members.slice(0, 500), 8, 1, getCloseness); // warm up the JIT
|
|
147
|
-
|
|
148
|
-
time("getCloseness", () => kmeans(members, clusterCount, iterations, getCloseness));
|
|
149
|
-
time("f32-once", () => kmeansFloat32Once(members, clusterCount, iterations));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
main();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function ensureFaceData(fileName: string, byteLength: number): Promise<string>;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import http from "http";
|
|
5
|
-
|
|
6
|
-
// Resolves a face-embedding test file to a local path that holds at least the requested number of bytes. On
|
|
7
|
-
// the machine that owns the data it just uses the original file. Anywhere else it downloads the missing prefix
|
|
8
|
-
// from the faceFramesServer over a range request and caches it in the temp dir, so re-runs only fetch what
|
|
9
|
-
// they haven't already. Point it at a different server with FACEFRAMES_URL.
|
|
10
|
-
const SOURCE_DIR = "/root/claude-work/face-embeddings";
|
|
11
|
-
const SERVER_BASE = process.env.FACEFRAMES_URL || "http://65.109.93.113:8799";
|
|
12
|
-
|
|
13
|
-
function downloadInto(fileName: string, start: number, endExclusive: number, cachePath: string): Promise<void> {
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
const url = `${SERVER_BASE}/${fileName}`;
|
|
16
|
-
const options = { headers: { Range: `bytes=${start}-${endExclusive - 1}` } };
|
|
17
|
-
const request = http.get(url, options, response => {
|
|
18
|
-
if (response.statusCode !== 206 && response.statusCode !== 200) {
|
|
19
|
-
response.resume();
|
|
20
|
-
reject(new Error(`${url} returned ${response.statusCode}`));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const out = fs.createWriteStream(cachePath, start ? { flags: "r+", start } : { flags: "w" });
|
|
24
|
-
response.pipe(out);
|
|
25
|
-
out.on("error", reject);
|
|
26
|
-
response.on("error", reject);
|
|
27
|
-
out.on("finish", resolve);
|
|
28
|
-
});
|
|
29
|
-
request.on("error", reject);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function ensureFaceData(fileName: string, byteLength: number): Promise<string> {
|
|
34
|
-
const local = path.join(SOURCE_DIR, fileName);
|
|
35
|
-
if (fs.existsSync(local) && fs.statSync(local).size >= byteLength) {
|
|
36
|
-
return local;
|
|
37
|
-
}
|
|
38
|
-
const cachePath = path.join(os.tmpdir(), fileName);
|
|
39
|
-
let cachedBytes = 0;
|
|
40
|
-
if (fs.existsSync(cachePath)) {
|
|
41
|
-
cachedBytes = fs.statSync(cachePath).size;
|
|
42
|
-
}
|
|
43
|
-
if (cachedBytes < byteLength) {
|
|
44
|
-
const megabytes = ((byteLength - cachedBytes) / 1e6).toFixed(0);
|
|
45
|
-
console.log(`downloading ${megabytes}MB of ${fileName} from ${SERVER_BASE} -> ${cachePath}`);
|
|
46
|
-
await downloadInto(fileName, cachedBytes, byteLength, cachePath);
|
|
47
|
-
}
|
|
48
|
-
return cachePath;
|
|
49
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import http from "http";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
// Throwaway HTTP server that serves the face-embedding test files so the benchmarks can be run from another
|
|
6
|
-
// machine (which downloads + caches a prefix via faceFramesData). Supports range requests, so a client only
|
|
7
|
-
// pulls the bytes it needs out of the 7.7GB full file. Meant to be left running in the background; it dies on
|
|
8
|
-
// reboot and that is fine. Run with: typenode storage/faceFramesServer.ts
|
|
9
|
-
const DATA_DIR = "/root/claude-work/face-embeddings";
|
|
10
|
-
const PORT = Number(process.env.PORT) || 8799;
|
|
11
|
-
|
|
12
|
-
function resolveFile(requestPath: string): string | undefined {
|
|
13
|
-
const name = path.basename(decodeURIComponent(requestPath));
|
|
14
|
-
if (!name.endsWith(".f32")) {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const full = path.join(DATA_DIR, name);
|
|
18
|
-
if (!fs.existsSync(full)) {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
return full;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const server = http.createServer((request, response) => {
|
|
25
|
-
const filePath = resolveFile(request.url || "");
|
|
26
|
-
if (!filePath) {
|
|
27
|
-
response.writeHead(404);
|
|
28
|
-
response.end("not found");
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const total = fs.statSync(filePath).size;
|
|
32
|
-
const rangeHeader = request.headers.range;
|
|
33
|
-
const rangeMatch = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
|
|
34
|
-
if (rangeMatch) {
|
|
35
|
-
const start = Number(rangeMatch[1]);
|
|
36
|
-
const end = rangeMatch[2] ? Number(rangeMatch[2]) : total - 1;
|
|
37
|
-
response.writeHead(206, {
|
|
38
|
-
"Content-Type": "application/octet-stream",
|
|
39
|
-
"Accept-Ranges": "bytes",
|
|
40
|
-
"Content-Range": `bytes ${start}-${end}/${total}`,
|
|
41
|
-
"Content-Length": end - start + 1,
|
|
42
|
-
});
|
|
43
|
-
fs.createReadStream(filePath, { start, end }).pipe(response);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
response.writeHead(200, {
|
|
47
|
-
"Content-Type": "application/octet-stream",
|
|
48
|
-
"Accept-Ranges": "bytes",
|
|
49
|
-
"Content-Length": total,
|
|
50
|
-
});
|
|
51
|
-
fs.createReadStream(filePath).pipe(response);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
server.listen(PORT, () => {
|
|
55
|
-
console.log(`serving ${DATA_DIR}/*.f32 on port ${PORT}`);
|
|
56
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { InMemoryDatabase } from "./inMemoryDatabase";
|
|
3
|
-
import { IvfEmbeddingRoot, IvfConfig, EmbeddingInput, insertEmbeddings, searchEmbeddings } from "./ivfEmbeddingDatabase";
|
|
4
|
-
import { encodeEmbedding, EmbeddingFormat } from "../embeddingFormats";
|
|
5
|
-
import { ensureFaceData } from "../faceFramesData";
|
|
6
|
-
|
|
7
|
-
// Exercises the tiered IVF embedding database through the in-memory Database wrapper, which counts every
|
|
8
|
-
// read/write/delete call and the bytes through each, across dataset sizes and load patterns, with timing.
|
|
9
|
-
|
|
10
|
-
const DIM = 512;
|
|
11
|
-
const FILE_NAME = "faceFrames2_full.f32";
|
|
12
|
-
const MODEL = "buffalo_l";
|
|
13
|
-
const FORMAT: EmbeddingFormat = "q8g8_2048";
|
|
14
|
-
|
|
15
|
-
async function loadFloats(count: number): Promise<Float32Array> {
|
|
16
|
-
const byteLength = count * DIM * 4;
|
|
17
|
-
const filePath = await ensureFaceData(FILE_NAME, byteLength);
|
|
18
|
-
const buffer = Buffer.alloc(byteLength);
|
|
19
|
-
const handle = fs.openSync(filePath, "r");
|
|
20
|
-
fs.readSync(handle, buffer, 0, byteLength, 0);
|
|
21
|
-
fs.closeSync(handle);
|
|
22
|
-
return new Float32Array(buffer.buffer, buffer.byteOffset, count * DIM);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function buildInputs(count: number): Promise<EmbeddingInput[]> {
|
|
26
|
-
const floats = await loadFloats(count);
|
|
27
|
-
const inputs: EmbeddingInput[] = [];
|
|
28
|
-
for (let index = 0; index < count; index++) {
|
|
29
|
-
const slice = floats.subarray(index * DIM, (index + 1) * DIM);
|
|
30
|
-
inputs.push({ ref: "e" + index, embedding: encodeEmbedding({ input: slice, format: FORMAT, model: MODEL }) });
|
|
31
|
-
}
|
|
32
|
-
return inputs;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function freshDatabase(config: IvfConfig): InMemoryDatabase<IvfEmbeddingRoot> {
|
|
36
|
-
const root: IvfEmbeddingRoot = { config, count: 0, flat: {}, byRef: {}, steps: {}, centroids: {}, cells: {} };
|
|
37
|
-
return new InMemoryDatabase<IvfEmbeddingRoot>(root);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function pad(value: string | number, width: number): string {
|
|
41
|
-
return String(value).padStart(width);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function main() {
|
|
45
|
-
const config: IvfConfig = { model: MODEL, format: FORMAT, cellTargetSize: 128 };
|
|
46
|
-
const runs: { size: number; batch: number }[] = [
|
|
47
|
-
{ size: 10000, batch: 1 },
|
|
48
|
-
{ size: 10000, batch: 100 },
|
|
49
|
-
{ size: 100000, batch: 1000 },
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
let maxSize = 0;
|
|
53
|
-
for (const run of runs) {
|
|
54
|
-
if (run.size > maxSize) {
|
|
55
|
-
maxSize = run.size;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const loadStart = Date.now();
|
|
59
|
-
const allInputs = await buildInputs(maxSize);
|
|
60
|
-
console.log(`loaded + encoded ${maxSize} embeddings in ${((Date.now() - loadStart) / 1000).toFixed(1)}s\n`);
|
|
61
|
-
|
|
62
|
-
console.log(`${pad("added", 7)} ${pad("batch", 5)} | ${pad("secs", 7)} ${pad("reads", 8)} ${pad("writes", 8)} ${pad("deletes", 8)} ${pad("readMB", 9)} ${pad("writeMB", 8)} | recall`);
|
|
63
|
-
for (const run of runs) {
|
|
64
|
-
const database = freshDatabase(config);
|
|
65
|
-
const start = Date.now();
|
|
66
|
-
for (let offset = 0; offset < run.size; offset += run.batch) {
|
|
67
|
-
insertEmbeddings(database, allInputs.slice(offset, offset + run.batch));
|
|
68
|
-
}
|
|
69
|
-
const seconds = (Date.now() - start) / 1000;
|
|
70
|
-
|
|
71
|
-
let found = 0;
|
|
72
|
-
const sampleCount = 100;
|
|
73
|
-
for (let index = 0; index < sampleCount; index++) {
|
|
74
|
-
const query = allInputs[Math.floor(index * run.size / sampleCount)];
|
|
75
|
-
const hits = searchEmbeddings(database, query.embedding, { probeBudget: 512, resultCount: 1 });
|
|
76
|
-
if (hits && hits.length && hits[0].ref === query.ref) {
|
|
77
|
-
found++;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const recall = (found / sampleCount).toFixed(2);
|
|
81
|
-
console.log(`${pad(run.size, 7)} ${pad(run.batch, 5)} | ${pad(seconds.toFixed(1), 7)} ${pad(database.readCalls, 8)} ${pad(database.writeCalls, 8)} ${pad(database.deleteCalls, 8)} ${pad((database.bytesRead / 1e6).toFixed(0), 9)} ${pad((database.bytesWritten / 1e6).toFixed(0), 8)} | ${recall}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
void main();
|