sliftutils 1.4.81 → 1.4.83

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.
@@ -0,0 +1,108 @@
1
+ import { Database } from "./Database";
2
+
3
+ // Rough byte size of a value, for the read/written-bytes metrics.
4
+ function sizeOf(value: unknown): number {
5
+ if (value === undefined || value === null) {
6
+ return 0;
7
+ }
8
+ if (value instanceof Uint8Array) {
9
+ return value.byteLength;
10
+ }
11
+ if (typeof value === "string") {
12
+ return value.length;
13
+ }
14
+ if (typeof value === "number") {
15
+ return 8;
16
+ }
17
+ if (typeof value === "boolean") {
18
+ return 1;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ let total = 0;
22
+ for (const entry of value) {
23
+ total += sizeOf(entry);
24
+ }
25
+ return total;
26
+ }
27
+ if (typeof value === "object") {
28
+ let total = 0;
29
+ const record = value as Record<string, unknown>;
30
+ for (const key of Object.keys(record)) {
31
+ total += sizeOf(record[key]);
32
+ }
33
+ return total;
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ // Record the property path a dereference lambda walks, so a write/delete can be applied at it.
39
+ function capturePath(deref: (root: unknown) => unknown): string[] {
40
+ const path: string[] = [];
41
+ const handler: ProxyHandler<object> = {
42
+ get(_target, key) {
43
+ if (typeof key === "string") {
44
+ path.push(key);
45
+ }
46
+ return new Proxy({}, handler);
47
+ },
48
+ };
49
+ deref(new Proxy({}, handler));
50
+ return path;
51
+ }
52
+
53
+ function setPath(root: Record<string, unknown>, path: string[], value: unknown): void {
54
+ let node = root;
55
+ for (let index = 0; index < path.length - 1; index++) {
56
+ const key = path[index];
57
+ const next = node[key];
58
+ if (!next || typeof next !== "object") {
59
+ node[key] = {};
60
+ }
61
+ node = node[key] as Record<string, unknown>;
62
+ }
63
+ node[path[path.length - 1]] = value;
64
+ }
65
+
66
+ function deletePath(root: Record<string, unknown>, path: string[]): void {
67
+ let node = root;
68
+ for (let index = 0; index < path.length - 1; index++) {
69
+ const next = node[path[index]];
70
+ if (!next || typeof next !== "object") {
71
+ return;
72
+ }
73
+ node = next as Record<string, unknown>;
74
+ }
75
+ delete node[path[path.length - 1]];
76
+ }
77
+
78
+ // In-memory Database for tests/dev: holds the whole root as a plain object and counts every call plus the
79
+ // bytes flowing through. Synchronous and always "synced", so reads only return undefined for a genuinely
80
+ // missing scalar (a missing transaction set reads as empty inside the set helpers).
81
+ export class InMemoryDatabase<Root> implements Database<Root> {
82
+ public readCalls = 0;
83
+ public writeCalls = 0;
84
+ public deleteCalls = 0;
85
+ public bytesRead = 0;
86
+ public bytesWritten = 0;
87
+ private root: Record<string, unknown>;
88
+
89
+ constructor(initial: Root) {
90
+ this.root = initial as Record<string, unknown>;
91
+ }
92
+
93
+ readData<Value>(deref: (root: Root) => Value): Value | undefined {
94
+ this.readCalls++;
95
+ const result = deref(this.root as Root);
96
+ this.bytesRead += sizeOf(result);
97
+ return result;
98
+ }
99
+ writeData<Value>(deref: (root: Root) => Value, newValue: Value): void {
100
+ this.writeCalls++;
101
+ this.bytesWritten += sizeOf(newValue);
102
+ setPath(this.root, capturePath(deref as (root: unknown) => unknown), newValue);
103
+ }
104
+ deleteData(deref: (root: Root) => unknown): void {
105
+ this.deleteCalls++;
106
+ deletePath(this.root, capturePath(deref as (root: unknown) => unknown));
107
+ }
108
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
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();
@@ -4,16 +4,22 @@ import { StoredEmbedding, EmbeddingFormat } from "../embeddingFormats";
4
4
  export type IvfConfig = {
5
5
  model: string;
6
6
  format: EmbeddingFormat;
7
- cellTarget: number;
8
- splitAt: number;
7
+ cellTargetSize: number;
9
8
  };
10
9
  export type IvfEmbeddingRoot = {
11
10
  config: IvfConfig;
12
- centroids: TransactionSetStore;
11
+ count: number;
12
+ flat: TransactionSetStore<StoredEmbedding>;
13
+ byRef: {
14
+ [ref: string]: Uint8Array;
15
+ };
16
+ steps: {
17
+ [step: string]: boolean;
18
+ };
19
+ centroids: TransactionSetStore<StoredEmbedding>;
13
20
  cells: {
14
- [cellId: string]: TransactionSetStore;
21
+ [cellId: string]: TransactionSetStore<StoredEmbedding>;
15
22
  };
16
- refIndex: TransactionSetStore;
17
23
  };
18
24
  export type EmbeddingInput = {
19
25
  ref: string;
@@ -23,9 +29,11 @@ export type SearchHit = {
23
29
  ref: string;
24
30
  closeness: number;
25
31
  };
32
+ export declare function rebuildStructure(database: Database<IvfEmbeddingRoot>): void;
26
33
  export declare function searchEmbeddings(database: Database<IvfEmbeddingRoot>, query: StoredEmbedding, options: {
27
34
  probeBudget: number;
28
35
  resultCount: number;
29
36
  }): SearchHit[] | undefined;
30
- export declare function insertEmbeddings(database: Database<IvfEmbeddingRoot>, items: EmbeddingInput[]): true | undefined;
31
- export declare function removeEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): true | undefined;
37
+ export declare function lookupEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): Map<string, StoredEmbedding> | undefined;
38
+ export declare function insertEmbeddings(database: Database<IvfEmbeddingRoot>, items: EmbeddingInput[]): undefined;
39
+ export declare function removeEmbeddings(database: Database<IvfEmbeddingRoot>, refs: string[]): void;