sliftutils 1.4.81 → 1.4.82
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/index.d.ts +61 -13
- package/package.json +1 -1
- package/spec.txt +18 -0
- package/storage/dist/embeddingFormats.ts.cache +340 -0
- package/storage/dist/faceFramesData.ts.cache +55 -0
- package/storage/embeddingBench.d.ts +1 -0
- package/storage/embeddingBench.ts +152 -0
- package/storage/embeddingFormats.d.ts +4 -1
- package/storage/embeddingFormats.ts +173 -111
- package/storage/faceFramesData.d.ts +1 -0
- package/storage/faceFramesData.ts +49 -0
- package/storage/faceFramesServer.d.ts +1 -0
- package/storage/faceFramesServer.ts +56 -0
- package/storage/proxydatabase/dist/Database.ts.cache +13 -0
- package/storage/proxydatabase/dist/inMemoryDatabase.ts.cache +105 -0
- package/storage/proxydatabase/dist/ivfDbCheck.ts.cache +81 -0
- package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +334 -0
- package/storage/proxydatabase/dist/transactionSet.ts.cache +108 -0
- package/storage/proxydatabase/inMemoryDatabase.d.ts +13 -0
- package/storage/proxydatabase/inMemoryDatabase.ts +108 -0
- package/storage/proxydatabase/ivfDbCheck.d.ts +1 -0
- package/storage/proxydatabase/ivfDbCheck.ts +85 -0
- package/storage/proxydatabase/ivfEmbeddingDatabase.d.ts +11 -7
- package/storage/proxydatabase/ivfEmbeddingDatabase.ts +289 -144
- package/storage/proxydatabase/transactionSet.d.ts +9 -5
- package/storage/proxydatabase/transactionSet.ts +43 -22
|
@@ -0,0 +1,152 @@
|
|
|
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();
|
|
@@ -14,6 +14,10 @@ export type StoredEmbedding = {
|
|
|
14
14
|
data: Uint8Array;
|
|
15
15
|
scales: Uint8Array;
|
|
16
16
|
};
|
|
17
|
+
export declare function embeddingLength(input: Float32Array | StoredEmbedding): number;
|
|
18
|
+
export declare function releaseFloat32(buffer: Float32Array): void;
|
|
19
|
+
export declare function embeddingToFloat32(input: Float32Array | StoredEmbedding, usePool?: boolean): Float32Array;
|
|
20
|
+
export declare const getCloseness: (a: Float32Array | StoredEmbedding, b: Float32Array | StoredEmbedding) => number;
|
|
17
21
|
export declare function encodeEmbedding(config: {
|
|
18
22
|
input: Float32Array | StoredEmbedding;
|
|
19
23
|
format: EmbeddingFormat;
|
|
@@ -26,4 +30,3 @@ export declare function averageEmbeddings(embeddings: StoredEmbedding[], config:
|
|
|
26
30
|
model: string;
|
|
27
31
|
}): StoredEmbedding;
|
|
28
32
|
export declare function hashEmbedding(stored: StoredEmbedding): string;
|
|
29
|
-
export declare const getCloseness: (embedding1: Float32Array | StoredEmbedding, embedding2: Float32Array | StoredEmbedding) => number;
|
|
@@ -64,25 +64,25 @@ const f32Scratch = new Float32Array(1);
|
|
|
64
64
|
const i32Scratch = new Int32Array(f32Scratch.buffer);
|
|
65
65
|
function floatToHalf(value: number): number {
|
|
66
66
|
f32Scratch[0] = value;
|
|
67
|
-
let
|
|
68
|
-
let bits = (
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
if (
|
|
72
|
-
if (
|
|
67
|
+
let bitsIn = i32Scratch[0];
|
|
68
|
+
let bits = (bitsIn >> 16) & 0x8000;
|
|
69
|
+
let mantissa = (bitsIn >> 12) & 0x07ff;
|
|
70
|
+
let exponent = (bitsIn >> 23) & 0xff;
|
|
71
|
+
if (exponent < 103) return bits;
|
|
72
|
+
if (exponent > 142) {
|
|
73
73
|
bits |= 0x7c00;
|
|
74
|
-
if (
|
|
75
|
-
bits |= (
|
|
74
|
+
if (exponent !== 255) {
|
|
75
|
+
bits |= (bitsIn & 0x007fffff);
|
|
76
76
|
}
|
|
77
77
|
return bits;
|
|
78
78
|
}
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
bits |= (
|
|
79
|
+
if (exponent < 113) {
|
|
80
|
+
mantissa |= 0x0800;
|
|
81
|
+
bits |= (mantissa >> (114 - exponent)) + ((mantissa >> (113 - exponent)) & 1);
|
|
82
82
|
return bits;
|
|
83
83
|
}
|
|
84
|
-
bits |= ((
|
|
85
|
-
bits +=
|
|
84
|
+
bits |= ((exponent - 112) << 10) | (mantissa >> 1);
|
|
85
|
+
bits += mantissa & 1;
|
|
86
86
|
return bits;
|
|
87
87
|
}
|
|
88
88
|
function halfToFloat(half: number): number {
|
|
@@ -110,8 +110,8 @@ function encodeFloat16(values: Float32Array): Uint8Array {
|
|
|
110
110
|
}
|
|
111
111
|
let out = new Uint8Array(values.length * 2);
|
|
112
112
|
let view = new DataView(out.buffer);
|
|
113
|
-
for (let
|
|
114
|
-
view.setUint16(
|
|
113
|
+
for (let index = 0; index < values.length; index++) {
|
|
114
|
+
view.setUint16(index * 2, floatToHalf(values[index]), true);
|
|
115
115
|
}
|
|
116
116
|
return out;
|
|
117
117
|
}
|
|
@@ -126,58 +126,121 @@ function asFloat16Indexable(bytes: Uint8Array): { readonly length: number;[index
|
|
|
126
126
|
}
|
|
127
127
|
let halves = new Uint16Array(bytes.buffer, bytes.byteOffset, count);
|
|
128
128
|
let out = new Float32Array(count);
|
|
129
|
-
for (let
|
|
130
|
-
out[
|
|
129
|
+
for (let index = 0; index < count; index++) {
|
|
130
|
+
out[index] = halfToFloat(halves[index]);
|
|
131
131
|
}
|
|
132
132
|
return out;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function
|
|
136
|
-
let sum = 0;
|
|
137
|
-
for (let i = 0; i < vector.length; i++) {
|
|
138
|
-
sum += vector[i] * vector[i];
|
|
139
|
-
}
|
|
140
|
-
let magnitude = Math.sqrt(sum);
|
|
141
|
-
if (!magnitude) return vector;
|
|
142
|
-
for (let i = 0; i < vector.length; i++) {
|
|
143
|
-
vector[i] /= magnitude;
|
|
144
|
-
}
|
|
145
|
-
return vector;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function embeddingLength(input: Float32Array | StoredEmbedding): number {
|
|
135
|
+
export function embeddingLength(input: Float32Array | StoredEmbedding): number {
|
|
149
136
|
if (input instanceof Float32Array) return input.length;
|
|
150
137
|
if (input.kind === "float32") return input.values.length;
|
|
151
138
|
return input.data.length;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
141
|
+
// A pool of reusable Float32 buffers keyed by length. Most embeddings share a size, so internal hot paths
|
|
142
|
+
// borrow a buffer (embeddingToFloat32 with usePool) and return it (releaseFloat32) to avoid per-op
|
|
143
|
+
// allocations and GC thrashing. A borrowed buffer holds stale data; whoever borrows it overwrites it fully.
|
|
144
|
+
//
|
|
145
|
+
// Each free buffer records when it was last released; an hourly sweep drops any that have sat idle for over
|
|
146
|
+
// an hour, so the runtime can free buffers a burst of work allocated but isn't actually reusing anymore.
|
|
147
|
+
const POOL_MAX_IDLE_MS = 60 * 60 * 1000;
|
|
148
|
+
type PooledFloat32 = { buffer: Float32Array; releasedAt: number };
|
|
149
|
+
const float32Pool = new Map<number, PooledFloat32[]>();
|
|
150
|
+
function acquireFloat32(length: number): Float32Array {
|
|
151
|
+
let free = float32Pool.get(length);
|
|
152
|
+
if (free && free.length) {
|
|
153
|
+
return free.pop()!.buffer;
|
|
154
|
+
}
|
|
155
|
+
return new Float32Array(length);
|
|
156
|
+
}
|
|
157
|
+
export function releaseFloat32(buffer: Float32Array): void {
|
|
158
|
+
let free = float32Pool.get(buffer.length);
|
|
159
|
+
if (!free) {
|
|
160
|
+
free = [];
|
|
161
|
+
float32Pool.set(buffer.length, free);
|
|
162
|
+
}
|
|
163
|
+
free.push({ buffer, releasedAt: Date.now() });
|
|
164
|
+
}
|
|
165
|
+
function sweepFloat32Pool(): void {
|
|
166
|
+
let cutoff = Date.now() - POOL_MAX_IDLE_MS;
|
|
167
|
+
for (let free of float32Pool.values()) {
|
|
168
|
+
let writeIndex = 0;
|
|
169
|
+
for (let readIndex = 0; readIndex < free.length; readIndex++) {
|
|
170
|
+
if (free[readIndex].releasedAt > cutoff) {
|
|
171
|
+
free[writeIndex++] = free[readIndex];
|
|
172
|
+
}
|
|
161
173
|
}
|
|
174
|
+
free.length = writeIndex;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const poolSweepTimer = setInterval(sweepFloat32Pool, POOL_MAX_IDLE_MS) as unknown as { unref?: () => void };
|
|
178
|
+
if (typeof poolSweepTimer.unref === "function") {
|
|
179
|
+
poolSweepTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Decode to a Float32Array (full length). Stored values are already unit-normalized (encodeEmbedding does
|
|
183
|
+
// that), so this only de-quantizes. By default it allocates a fresh array (a float input is returned as-is,
|
|
184
|
+
// no copy). With usePool it borrows a pooled buffer the caller must releaseFloat32 — used by hot internal
|
|
185
|
+
// paths so repeated decodes don't allocate.
|
|
186
|
+
export function embeddingToFloat32(input: Float32Array | StoredEmbedding, usePool = false): Float32Array {
|
|
187
|
+
if (input instanceof Float32Array) {
|
|
188
|
+
if (!usePool) return input;
|
|
189
|
+
let out = acquireFloat32(input.length);
|
|
190
|
+
out.set(input);
|
|
162
191
|
return out;
|
|
163
192
|
}
|
|
164
193
|
if (input.kind === "float32") {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
194
|
+
if (!usePool) return input.values;
|
|
195
|
+
let out = acquireFloat32(input.values.length);
|
|
196
|
+
out.set(input.values);
|
|
169
197
|
return out;
|
|
170
198
|
}
|
|
171
199
|
let int8 = asInt8(input.data);
|
|
172
200
|
let scales = asFloat16Indexable(input.scales);
|
|
173
201
|
let groupSize = input.groupSize;
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
let length = int8.length;
|
|
203
|
+
let out = usePool ? acquireFloat32(length) : new Float32Array(length);
|
|
204
|
+
let group = 0;
|
|
205
|
+
for (let start = 0; start < length; start += groupSize) {
|
|
206
|
+
let scale = scales[group++];
|
|
207
|
+
let end = Math.min(start + groupSize, length);
|
|
208
|
+
for (let index = start; index < end; index++) {
|
|
209
|
+
out[index] = int8[index] * scale;
|
|
210
|
+
}
|
|
176
211
|
}
|
|
177
212
|
return out;
|
|
178
213
|
}
|
|
179
214
|
|
|
180
|
-
|
|
215
|
+
function closenessFromDot(dot: number): number {
|
|
216
|
+
// Inputs are unit vectors, so euclidean^2 = 2 - 2*dot; clamp away tiny negatives from float error.
|
|
217
|
+
return 1 - Math.sqrt(Math.max(0, 2 - 2 * dot));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function dotFloat32(a: Float32Array, b: Float32Array): number {
|
|
221
|
+
let length = Math.min(a.length, b.length);
|
|
222
|
+
let dot = 0;
|
|
223
|
+
for (let index = 0; index < length; index++) {
|
|
224
|
+
dot += a[index] * b[index];
|
|
225
|
+
}
|
|
226
|
+
return dot;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Closeness = 1 - euclidean distance of two unit vectors. Always decode both to (pooled) float32 and dot:
|
|
230
|
+
// simple, and once the decodes are pooled it's the fastest per-call path. Batch callers (k-means) decode
|
|
231
|
+
// every vector once themselves instead of going through here.
|
|
232
|
+
export const getCloseness = measureWrap(function getCloseness(a: Float32Array | StoredEmbedding, b: Float32Array | StoredEmbedding): number {
|
|
233
|
+
let va = embeddingToFloat32(a, true);
|
|
234
|
+
let vb = embeddingToFloat32(b, true);
|
|
235
|
+
let result = closenessFromDot(dotFloat32(va, vb));
|
|
236
|
+
releaseFloat32(va);
|
|
237
|
+
releaseFloat32(vb);
|
|
238
|
+
return result;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Encode a vector (or re-encode an embedding) into the requested format. This is THE place we normalize:
|
|
242
|
+
// the (truncated) vector is made unit length here, so anything that's been encoded is reliably normalized
|
|
243
|
+
// and the comparison/decoding paths never have to re-normalize.
|
|
181
244
|
export function encodeEmbedding(config: {
|
|
182
245
|
input: Float32Array | StoredEmbedding;
|
|
183
246
|
format: EmbeddingFormat;
|
|
@@ -185,46 +248,57 @@ export function encodeEmbedding(config: {
|
|
|
185
248
|
}): StoredEmbedding {
|
|
186
249
|
let { input, format, model } = config;
|
|
187
250
|
let formatConfig = FORMAT_CONFIGS[format];
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
let decoded = embeddingToFloat32(input, true);
|
|
252
|
+
let length = decoded.length;
|
|
253
|
+
if (formatConfig.truncation !== undefined && formatConfig.truncation < length) {
|
|
254
|
+
length = formatConfig.truncation;
|
|
255
|
+
}
|
|
256
|
+
let norm = 0;
|
|
257
|
+
for (let index = 0; index < length; index++) {
|
|
258
|
+
norm += decoded[index] * decoded[index];
|
|
259
|
+
}
|
|
260
|
+
let magnitude = Math.sqrt(norm) || 1;
|
|
261
|
+
for (let index = 0; index < length; index++) {
|
|
262
|
+
decoded[index] /= magnitude;
|
|
191
263
|
}
|
|
192
|
-
// Truncate to a fresh array, then renormalize so the shortened vector is unit length again.
|
|
193
|
-
let vector = normalizeInPlace(decodeToLength(input, length));
|
|
194
264
|
|
|
265
|
+
let result: StoredEmbedding;
|
|
195
266
|
if (!formatConfig.quant) {
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
267
|
+
result = { kind: "float32", model, values: new Float32Array(decoded.subarray(0, length)) };
|
|
268
|
+
} else {
|
|
269
|
+
let { type, groupSize } = formatConfig.quant;
|
|
270
|
+
let groupCount = Math.ceil(length / groupSize);
|
|
271
|
+
let int8 = new Int8Array(length);
|
|
272
|
+
let scales = new Float32Array(groupCount);
|
|
273
|
+
for (let group = 0; group < groupCount; group++) {
|
|
274
|
+
let start = group * groupSize;
|
|
275
|
+
let end = Math.min(start + groupSize, length);
|
|
276
|
+
let maxAbs = 0;
|
|
277
|
+
for (let index = start; index < end; index++) {
|
|
278
|
+
let absValue = Math.abs(decoded[index]);
|
|
279
|
+
if (absValue > maxAbs) maxAbs = absValue;
|
|
280
|
+
}
|
|
281
|
+
let scale = maxAbs / INT8_MAX;
|
|
282
|
+
scales[group] = scale;
|
|
283
|
+
if (!scale) continue;
|
|
284
|
+
for (let index = start; index < end; index++) {
|
|
285
|
+
let quantized = Math.round(decoded[index] / scale);
|
|
286
|
+
if (quantized > INT8_MAX) quantized = INT8_MAX;
|
|
287
|
+
if (quantized < -INT8_MAX) quantized = -INT8_MAX;
|
|
288
|
+
int8[index] = quantized;
|
|
289
|
+
}
|
|
218
290
|
}
|
|
291
|
+
result = {
|
|
292
|
+
kind: "quant",
|
|
293
|
+
model,
|
|
294
|
+
type,
|
|
295
|
+
groupSize,
|
|
296
|
+
data: new Uint8Array(int8.buffer, int8.byteOffset, int8.byteLength),
|
|
297
|
+
scales: encodeFloat16(scales),
|
|
298
|
+
};
|
|
219
299
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
model,
|
|
223
|
-
type,
|
|
224
|
-
groupSize,
|
|
225
|
-
data: new Uint8Array(int8.buffer, int8.byteOffset, int8.byteLength),
|
|
226
|
-
scales: encodeFloat16(scales),
|
|
227
|
-
};
|
|
300
|
+
releaseFloat32(decoded);
|
|
301
|
+
return result;
|
|
228
302
|
}
|
|
229
303
|
|
|
230
304
|
type SerializedHeader =
|
|
@@ -273,52 +347,40 @@ export function deserializeStoredEmbedding(base64: string): StoredEmbedding {
|
|
|
273
347
|
};
|
|
274
348
|
}
|
|
275
349
|
|
|
276
|
-
// Mean of several embeddings (
|
|
277
|
-
//
|
|
350
|
+
// Mean of several embeddings (encodeEmbedding normalizes the result to a unit centroid). Borrows a pooled
|
|
351
|
+
// accumulator.
|
|
278
352
|
export function averageEmbeddings(embeddings: StoredEmbedding[], config: { format: EmbeddingFormat; model: string }): StoredEmbedding {
|
|
279
353
|
let length = Infinity;
|
|
280
354
|
for (let embedding of embeddings) {
|
|
281
355
|
length = Math.min(length, embeddingLength(embedding));
|
|
282
356
|
}
|
|
283
|
-
let sum =
|
|
357
|
+
let sum = acquireFloat32(length);
|
|
358
|
+
sum.fill(0);
|
|
284
359
|
for (let embedding of embeddings) {
|
|
285
|
-
let
|
|
360
|
+
let vector = embeddingToFloat32(embedding, true);
|
|
286
361
|
for (let dimension = 0; dimension < length; dimension++) {
|
|
287
|
-
sum[dimension] +=
|
|
362
|
+
sum[dimension] += vector[dimension];
|
|
288
363
|
}
|
|
364
|
+
releaseFloat32(vector);
|
|
289
365
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return encodeEmbedding({ input: sum, format: config.format, model: config.model });
|
|
366
|
+
let result = encodeEmbedding({ input: sum.subarray(0, length), format: config.format, model: config.model });
|
|
367
|
+
releaseFloat32(sum);
|
|
368
|
+
return result;
|
|
294
369
|
}
|
|
295
370
|
|
|
296
|
-
// A stable 16-byte (base64) content hash of an embedding, for use as a cell id derived from its
|
|
371
|
+
// A stable 16-byte (base64) content hash of an embedding's values, for use as a cell id derived from its
|
|
372
|
+
// centroid. Iterates via the accessor (the value bits are hashed, so any format hashes consistently).
|
|
297
373
|
export function hashEmbedding(stored: StoredEmbedding): string {
|
|
298
|
-
let
|
|
374
|
+
let vector = embeddingToFloat32(stored, true);
|
|
375
|
+
let length = vector.length;
|
|
299
376
|
let lanes = new Uint32Array([2166136261, 2654435761, 40503, 3266489917]);
|
|
300
|
-
for (let
|
|
301
|
-
|
|
377
|
+
for (let index = 0; index < length; index++) {
|
|
378
|
+
f32Scratch[0] = vector[index];
|
|
379
|
+
let bits = i32Scratch[0];
|
|
302
380
|
for (let lane = 0; lane < lanes.length; lane++) {
|
|
303
|
-
lanes[lane] = Math.imul(lanes[lane] ^ (
|
|
381
|
+
lanes[lane] = Math.imul(lanes[lane] ^ (bits + lane * 131 + index), 16777619);
|
|
304
382
|
}
|
|
305
383
|
}
|
|
384
|
+
releaseFloat32(vector);
|
|
306
385
|
return Buffer.from(lanes.buffer).toString("base64");
|
|
307
386
|
}
|
|
308
|
-
|
|
309
|
-
// Compare two embeddings in any format / truncation. Both are decoded, truncated to their common
|
|
310
|
-
// length, and unit-normalized, then scored as 1 minus their euclidean distance.
|
|
311
|
-
export const getCloseness = measureWrap(function getCloseness(
|
|
312
|
-
embedding1: Float32Array | StoredEmbedding,
|
|
313
|
-
embedding2: Float32Array | StoredEmbedding,
|
|
314
|
-
): number {
|
|
315
|
-
let length = Math.min(embeddingLength(embedding1), embeddingLength(embedding2));
|
|
316
|
-
let vector1 = normalizeInPlace(decodeToLength(embedding1, length));
|
|
317
|
-
let vector2 = normalizeInPlace(decodeToLength(embedding2, length));
|
|
318
|
-
let sum = 0;
|
|
319
|
-
for (let i = 0; i < length; i++) {
|
|
320
|
-
let diff = vector1[i] - vector2[i];
|
|
321
|
-
sum += diff * diff;
|
|
322
|
-
}
|
|
323
|
-
return 1 - Math.sqrt(sum);
|
|
324
|
-
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureFaceData(fileName: string, byteLength: number): Promise<string>;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
|
+
//exports.namespaceDatabase = void 0;
|
|
4
|
+
function namespaceDatabase(database, into) {
|
|
5
|
+
return {
|
|
6
|
+
readData: (deref) => database.readData(root => deref(into(root))),
|
|
7
|
+
writeData: (deref, newValue) => database.writeData(root => deref(into(root)), newValue),
|
|
8
|
+
deleteData: (deref) => database.deleteData(root => deref(into(root))),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
exports.namespaceDatabase = namespaceDatabase;
|
|
12
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRGF0YWJhc2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJEYXRhYmFzZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFPQSxTQUFnQixpQkFBaUIsQ0FDN0IsUUFBd0IsRUFDeEIsSUFBeUI7SUFFekIsT0FBTztRQUNILFFBQVEsRUFBRSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUNqRSxTQUFTLEVBQUUsQ0FBQyxLQUFLLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFLFFBQVEsQ0FBQztRQUN2RixVQUFVLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7S0FDeEUsQ0FBQztBQUNOLENBQUM7QUFURCw4Q0FTQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIHVuZGVmaW5lZCBmcm9tIGEgcmVhZCBtZWFucyBcIm5vdCBzeW5jZWQgeWV0XCIsIE5PVCBcImFic2VudFwiIOKAlCBzdG9yYWdlIGluaXRpYWxpemVzIG9uIGl0cyBvd24uIFNvIHJlYWQgZXZlcnl0aGluZyBhIHN0YWdlIG5lZWRzIGluIG9uZSByZWFkRGF0YSBhbmQgYmFpbCBpZiB0aGUgd2hvbGUgcmVzdWx0IGlzIHVuZGVmaW5lZDsgdGhlIGRhdGFiYXNlIG5ldmVyIHJldHVybnMgYW4gaW5kaXZpZHVhbCB1bnN5bmNlZCB2YWx1ZSBpbnNpZGUgYW4gb3RoZXJ3aXNlLXN1Y2Nlc3NmdWwgcmVhZC5cbmV4cG9ydCBpbnRlcmZhY2UgRGF0YWJhc2U8Um9vdD4ge1xuICAgIHJlYWREYXRhOiA8VmFsdWU+KGRlcmVmOiAocm9vdDogUm9vdCkgPT4gVmFsdWUpID0+IFZhbHVlIHwgdW5kZWZpbmVkO1xuICAgIHdyaXRlRGF0YTogPFZhbHVlPihkZXJlZjogKHJvb3Q6IFJvb3QpID0+IFZhbHVlLCBuZXdWYWx1ZTogVmFsdWUpID0+IHZvaWQ7XG4gICAgZGVsZXRlRGF0YTogKGRlcmVmOiAocm9vdDogUm9vdCkgPT4gdW5rbm93bikgPT4gdm9pZDtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIG5hbWVzcGFjZURhdGFiYXNlPFJvb3QsIFN1Yj4oXG4gICAgZGF0YWJhc2U6IERhdGFiYXNlPFJvb3Q+LFxuICAgIGludG86IChyb290OiBSb290KSA9PiBTdWIsXG4pOiBEYXRhYmFzZTxTdWI+IHtcbiAgICByZXR1cm4ge1xuICAgICAgICByZWFkRGF0YTogKGRlcmVmKSA9PiBkYXRhYmFzZS5yZWFkRGF0YShyb290ID0+IGRlcmVmKGludG8ocm9vdCkpKSxcbiAgICAgICAgd3JpdGVEYXRhOiAoZGVyZWYsIG5ld1ZhbHVlKSA9PiBkYXRhYmFzZS53cml0ZURhdGEocm9vdCA9PiBkZXJlZihpbnRvKHJvb3QpKSwgbmV3VmFsdWUpLFxuICAgICAgICBkZWxldGVEYXRhOiAoZGVyZWYpID0+IGRhdGFiYXNlLmRlbGV0ZURhdGEocm9vdCA9PiBkZXJlZihpbnRvKHJvb3QpKSksXG4gICAgfTtcbn1cbiJdfQ==
|
|
13
|
+
/* _JS_SOURCE_HASH = "93a620d26b6a59665b5f7dab689d9739693a4990e1c4781abcddc6cda24bda11"; */
|