vectrojs 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +828 -4
- package/bin/vectro.mjs +82 -0
- package/dist/vectro.wasm +0 -0
- package/mcp/server.mjs +288 -0
- package/package.json +84 -6
- package/src/arrow.mjs +905 -0
- package/src/bitpack.mjs +135 -0
- package/src/browser.mjs +129 -0
- package/src/bytes.mjs +60 -0
- package/src/csv-kernel.mjs +205 -0
- package/src/csv.mjs +270 -0
- package/src/dataset.mjs +1763 -0
- package/src/dates.mjs +139 -0
- package/src/emitter.mjs +109 -0
- package/src/engine.mjs +881 -0
- package/src/files-browser.mjs +104 -0
- package/src/files-node.mjs +59 -0
- package/src/frames.mjs +191 -0
- package/src/fuse.mjs +512 -0
- package/src/interp.mjs +305 -0
- package/src/join.mjs +534 -0
- package/src/kernels.mjs +312 -0
- package/src/ndjson.mjs +136 -0
- package/src/node.mjs +120 -0
- package/src/pack.mjs +114 -0
- package/src/parallel-sort.mjs +99 -0
- package/src/partition-kernels.mjs +388 -0
- package/src/partition.mjs +395 -0
- package/src/plan.mjs +551 -0
- package/src/portable.mjs +21 -0
- package/src/rank-kernels.mjs +81 -0
- package/src/rows.mjs +91 -0
- package/src/serve/catalog.mjs +148 -0
- package/src/serve/flight.mjs +270 -0
- package/src/serve/formats.mjs +87 -0
- package/src/serve/http.mjs +182 -0
- package/src/serve/index.mjs +9 -0
- package/src/serve/odata.mjs +291 -0
- package/src/serve/pg.mjs +588 -0
- package/src/serve/pgcatalog.mjs +220 -0
- package/src/serve/pgtypes.mjs +199 -0
- package/src/sketch.mjs +75 -0
- package/src/sort-kernels.mjs +117 -0
- package/src/sort.mjs +102 -0
- package/src/split-worker-core.mjs +252 -0
- package/src/split-worker-node.mjs +8 -0
- package/src/split-worker.mjs +7 -0
- package/src/split.mjs +741 -0
- package/src/sql.mjs +1558 -0
- package/src/sqltext.mjs +132 -0
- package/src/static-kernels.mjs +164 -0
- package/src/stream-worker.mjs +553 -0
- package/src/table-kernels.mjs +323 -0
- package/src/table.mjs +2247 -0
- package/src/textmap.mjs +159 -0
- package/src/vectors.mjs +138 -0
- package/src/wasm.mjs +254 -0
- package/src/window.mjs +160 -0
- package/src/worker-browser.mjs +21 -0
- package/src/worker-loop.mjs +106 -0
- package/src/worker.mjs +22 -0
- package/src/zones.mjs +75 -0
package/src/textmap.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Functions of whole values, text ones above all (SQL's UPPER, SUBSTR, ||,
|
|
2
|
+
// CAST(x AS VARCHAR), text CASE...): a JavaScript function of the inputs'
|
|
3
|
+
// values, run once per distinct combination of them rather than per row (a
|
|
4
|
+
// category column's labels are few), its answers spread back to the rows.
|
|
5
|
+
// Text answers become a category column whose labels live in `state`
|
|
6
|
+
// ({ dict, codes }), which only grows: chunk after chunk of a dataset, the
|
|
7
|
+
// same text keeps the same code.
|
|
8
|
+
import { decodeOne } from './pack.mjs';
|
|
9
|
+
import { decodeString } from './csv-kernel.mjs';
|
|
10
|
+
|
|
11
|
+
// Each row's value of a column as a small whole number (its id), and the
|
|
12
|
+
// value of each id: text as strings ('' for missing), numbers and dates as
|
|
13
|
+
// numbers (NaN for missing).
|
|
14
|
+
function idsOf(engine, c, n, f32) {
|
|
15
|
+
const d = engine.live(c.data), ids = new Int32Array(n);
|
|
16
|
+
if (c.dict) {
|
|
17
|
+
const K = c.dict.length;
|
|
18
|
+
for (let i = 0; i < n; i++) { const v = c.enc ? decodeOne(c.enc, d[i], f32) : d[i]; ids[i] = v === v ? v : K; }
|
|
19
|
+
return { ids, size: K + 1, value: (id) => (id < K ? c.dict[id] : '') };
|
|
20
|
+
}
|
|
21
|
+
if (c.strings) {
|
|
22
|
+
const bytes = engine.live(c.strings), seen = new Map(), vals = [];
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const v = d[i], s = v === v ? decodeString(bytes, v) ?? '' : '';
|
|
25
|
+
let id = seen.get(s);
|
|
26
|
+
if (id === undefined) { id = vals.length; vals.push(s); seen.set(s, id); }
|
|
27
|
+
ids[i] = id;
|
|
28
|
+
}
|
|
29
|
+
return { ids, size: Math.max(1, vals.length), value: (id) => vals[id] };
|
|
30
|
+
}
|
|
31
|
+
const x = c.enc ? Float64Array.from({ length: n }, (_, i) => decodeOne(c.enc, d[i], f32)) : d;
|
|
32
|
+
let lo = Infinity, hi = -Infinity, whole = true;
|
|
33
|
+
for (let i = 0; i < n; i++) { const v = x[i]; if (v !== v) continue; if (v < lo) lo = v; if (v > hi) hi = v; if (whole && v !== Math.floor(v)) whole = false; }
|
|
34
|
+
if (lo === Infinity) { ids.fill(0); return { ids, size: 1, value: () => NaN }; }
|
|
35
|
+
if (whole && hi - lo < 2 ** 22) { // small whole numbers: the id is the value (from the least); missing last
|
|
36
|
+
const miss = hi - lo + 1;
|
|
37
|
+
for (let i = 0; i < n; i++) { const v = x[i]; ids[i] = v === v ? v - lo : miss; }
|
|
38
|
+
return { ids, size: miss + 1, value: (id) => (id === miss ? NaN : (id + lo === 0 ? 0 : id + lo)) };
|
|
39
|
+
}
|
|
40
|
+
const seen = new Map(), vals = [];
|
|
41
|
+
for (let i = 0; i < n; i++) {
|
|
42
|
+
const v = x[i], k = v === v ? (v === 0 ? 0 : v) : NaN; // (-0 and 0 are one value; NaN keys a Map fine)
|
|
43
|
+
let id = seen.get(k);
|
|
44
|
+
if (id === undefined) { id = vals.length; vals.push(k); seen.set(k, id); }
|
|
45
|
+
ids[i] = id;
|
|
46
|
+
}
|
|
47
|
+
return { ids, size: Math.max(1, vals.length), value: (id) => vals[id] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A column read as ids without a pass of its own, when it can be: id = value -
|
|
51
|
+
// lo (category codes, or whole numbers in a small range), missing -> miss.
|
|
52
|
+
// range: [min, max, values not whole] of a number column (from the engine), or none
|
|
53
|
+
function directOf(engine, c, n, f32, range) {
|
|
54
|
+
if (c.strings || c.enc) return null;
|
|
55
|
+
const d = engine.live(c.data).subarray(0, n);
|
|
56
|
+
if (c.dict) { const K = c.dict.length; return { d, lo: 0, size: K + 1, miss: K, value: (id) => (id < K ? c.dict[id] : '') }; }
|
|
57
|
+
let [lo, hi, frac] = range ?? [Infinity, -Infinity, 0];
|
|
58
|
+
if (!range) for (let i = 0; i < n; i++) { const v = d[i]; if (v !== v) continue; if (v < lo) lo = v; if (v > hi) hi = v; if (v !== Math.floor(v)) frac++; }
|
|
59
|
+
if (!(lo <= hi) || frac || hi - lo >= 2 ** 22) return null;
|
|
60
|
+
const miss = hi - lo + 1;
|
|
61
|
+
if (c.numbered) return { d, lo, size: miss + 1, miss, value: (id) => (id === miss ? NaN : c.numbered(id + lo)) }; // (group numbers of the values)
|
|
62
|
+
return { d, lo, size: miss + 1, miss, value: (id) => (id === miss ? NaN : id + lo === 0 ? 0 : id + lo) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// dst[i] = fn(values of row i): codes (text, labels added to state) or numbers.
|
|
66
|
+
export function textmapRows(engine, cols, n, f32, fn, text, state, dst, ranges = []) {
|
|
67
|
+
const codeOf = (r) => {
|
|
68
|
+
const s = r === null || r === undefined || (typeof r === 'number' && r !== r) ? '' : String(r);
|
|
69
|
+
let code = state.codes.get(s);
|
|
70
|
+
if (code === undefined) { code = state.dict.length; state.dict.push(s); state.codes.set(s, code); }
|
|
71
|
+
return code;
|
|
72
|
+
};
|
|
73
|
+
const answer = text ? codeOf : (r) => (typeof r === 'number' ? r : r === null || r === undefined || r === '' ? NaN : Number(r));
|
|
74
|
+
if (!cols.length) { dst.fill(answer(fn()), 0, n); return; }
|
|
75
|
+
const direct = cols.map((c, k) => directOf(engine, c, n, f32, ranges[k]));
|
|
76
|
+
if (cols.length === 1) { // one input: each value once, then a lookup per row
|
|
77
|
+
let x = direct[0];
|
|
78
|
+
if (!x) { const r = idsOf(engine, cols[0], n, f32); x = { d: r.ids, lo: 0, size: r.size, miss: -1, value: r.value }; } // (ids: never missing)
|
|
79
|
+
const seen = new Uint8Array(x.size + 1), res = new Float64Array(x.size + 1);
|
|
80
|
+
markIds(x.d, x.lo, x.miss, seen, n);
|
|
81
|
+
for (let id = 0; id < seen.length; id++) if (seen[id]) res[id] = answer(fn(x.value(id)));
|
|
82
|
+
spreadIds(x.d, x.lo, x.miss, res, dst, n);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const product = direct.every(Boolean) ? direct.reduce((a, x) => a * x.size, 1) : Infinity;
|
|
86
|
+
if (product <= 2 ** 22) { // every input direct and few combinations: the combination as one number, read as it goes
|
|
87
|
+
const slot = new Int32Array(product).fill(-1), res = [];
|
|
88
|
+
const strides = direct.map((_, k) => direct.slice(k + 1).reduce((a, x) => a * x.size, 1));
|
|
89
|
+
const [a, b] = direct, sa = strides[0];
|
|
90
|
+
if (direct.length === 2) {
|
|
91
|
+
markPairs(a.d, a.lo, a.miss, b.d, b.lo, b.miss, sa, slot, n); // (slot: each pair's first row)
|
|
92
|
+
const byKey = new Float64Array(product), id = (x, v) => (v === v ? v - x.lo : x.miss);
|
|
93
|
+
for (let key = 0; key < product; key++) { const i = slot[key]; if (i >= 0) byKey[key] = answer(fn(a.value(id(a, a.d[i])), b.value(id(b, b.d[i])))); }
|
|
94
|
+
spreadPairs(a.d, a.lo, a.miss, b.d, b.lo, b.miss, sa, byKey, dst, n);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (let i = 0; i < n; i++) {
|
|
98
|
+
let key;
|
|
99
|
+
if (direct.length === 2) { const u = a.d[i], w = b.d[i]; key = ((u === u ? u - a.lo : a.miss) * sa + (w === w ? w - b.lo : b.miss)) | 0; }
|
|
100
|
+
else { key = 0; for (let k = 0; k < direct.length; k++) { const v = direct[k].d[i]; key += (v === v ? v - direct[k].lo : direct[k].miss) * strides[k]; } key |= 0; }
|
|
101
|
+
let j = slot[key];
|
|
102
|
+
if (j < 0) {
|
|
103
|
+
j = slot[key] = res.length;
|
|
104
|
+
res.push(answer(fn(...direct.map((x) => { const v = x.d[i]; return x.value(v === v ? v - x.lo : x.miss); }))));
|
|
105
|
+
}
|
|
106
|
+
dst[i] = res[j];
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
dst.set(rowsByCombination(engine, cols, n, f32, fn, answer));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// (hot loops as small functions with no calls inside: a callback there,
|
|
114
|
+
// a new one each time, would make V8 throw the compiled loop away)
|
|
115
|
+
function markIds(d, lo, miss, seen, n) { for (let i = 0; i < n; i++) { const v = d[i]; seen[v === v ? (v - lo) | 0 : miss] = 1; } }
|
|
116
|
+
function spreadIds(d, lo, miss, res, dst, n) { for (let i = 0; i < n; i++) { const v = d[i]; dst[i] = res[v === v ? (v - lo) | 0 : miss]; } }
|
|
117
|
+
function markPairs(ad, alo, amiss, bd, blo, bmiss, sa, first, n) {
|
|
118
|
+
for (let i = 0; i < n; i++) {
|
|
119
|
+
const u = ad[i], w = bd[i], x = u === u ? (u - alo) | 0 : amiss, y = w === w ? (w - blo) | 0 : bmiss, key = x * sa + y;
|
|
120
|
+
if (first[key] < 0) first[key] = i;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function spreadPairs(ad, alo, amiss, bd, blo, bmiss, sa, byKey, dst, n) {
|
|
124
|
+
for (let i = 0; i < n; i++) { const u = ad[i], w = bd[i], x = u === u ? (u - alo) | 0 : amiss, y = w === w ? (w - blo) | 0 : bmiss; dst[i] = byKey[x * sa + y]; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// several inputs, any kind: one number per row for its combination (mixed
|
|
128
|
+
// radix, renumbered when it would grow too big), each distinct one once
|
|
129
|
+
function rowsByCombination(engine, cols, n, f32, fn, answer) {
|
|
130
|
+
const out = new Float64Array(n);
|
|
131
|
+
const ins = cols.map((c) => idsOf(engine, c, n, f32));
|
|
132
|
+
// several: one number per row for its combination (mixed radix, renumbered
|
|
133
|
+
// when it would grow too big), then each distinct combination once
|
|
134
|
+
let comb = Float64Array.from(ins[0].ids), size = ins[0].size;
|
|
135
|
+
const renumber = () => {
|
|
136
|
+
const m = new Map();
|
|
137
|
+
for (let i = 0; i < n; i++) { let id = m.get(comb[i]); if (id === undefined) { id = m.size; m.set(comb[i], id); } comb[i] = id; }
|
|
138
|
+
size = Math.max(1, m.size);
|
|
139
|
+
};
|
|
140
|
+
for (let k = 1; k < ins.length; k++) {
|
|
141
|
+
if (size * ins[k].size > 2 ** 52) renumber();
|
|
142
|
+
const { ids, size: s } = ins[k];
|
|
143
|
+
for (let i = 0; i < n; i++) comb[i] = comb[i] * s + ids[i];
|
|
144
|
+
size *= s;
|
|
145
|
+
}
|
|
146
|
+
const dense = size <= 2 ** 24 ? new Int32Array(size).fill(-1) : null, map = dense ? null : new Map();
|
|
147
|
+
const res = [];
|
|
148
|
+
for (let i = 0; i < n; i++) {
|
|
149
|
+
const key = comb[i];
|
|
150
|
+
let j = dense ? dense[key] : map.get(key);
|
|
151
|
+
if (j === undefined || j < 0) {
|
|
152
|
+
j = res.length;
|
|
153
|
+
res.push(answer(fn(...ins.map((x) => x.value(x.ids[i])))));
|
|
154
|
+
if (dense) dense[key] = j; else map.set(key, j);
|
|
155
|
+
}
|
|
156
|
+
out[i] = res[j];
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
package/src/vectors.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Vector search: nearest embeddings to a query, exact (no approximate index),
|
|
2
|
+
// on every core.
|
|
3
|
+
//
|
|
4
|
+
// const index = await VectorIndex.create(engine, {
|
|
5
|
+
// vectors, // n embeddings: number[][] or one flat Float32Array (n * dim)
|
|
6
|
+
// dim: 384,
|
|
7
|
+
// metric: 'cosine', // 'cosine' (default) | 'dot' | 'l2'
|
|
8
|
+
// columns: { id, title, year }, // anything to return or filter on
|
|
9
|
+
// });
|
|
10
|
+
// await index.searchAsync(query, { k: 10, filter: 'year >= 2020' });
|
|
11
|
+
// // -> [{ id, title, year, score }, ...] best first
|
|
12
|
+
//
|
|
13
|
+
// Embeddings are stored as columns of a float32 table (v0 … v{dim-1}), so the
|
|
14
|
+
// score is an ordinary fused expression, q0*v0 + q1*v1 + …, computed 24
|
|
15
|
+
// dimensions per pass with the query as scalars, then top(k). That makes
|
|
16
|
+
// filters, split tables (Web Workers on any page) and edge runtimes work with
|
|
17
|
+
// no extra code. cosine: vectors and query are normalized, score = dot
|
|
18
|
+
// (higher is closer). l2: score = squared distance (lower is closer).
|
|
19
|
+
const DIMS_PER_PASS = 24; // a pass reads 24 vector columns + the running sum
|
|
20
|
+
const METRICS = new Set(['cosine', 'dot', 'l2']);
|
|
21
|
+
|
|
22
|
+
export class VectorIndex {
|
|
23
|
+
constructor(table, { dim, metric, names }) {
|
|
24
|
+
this.table = table;
|
|
25
|
+
this.dim = dim;
|
|
26
|
+
this.metric = metric;
|
|
27
|
+
this.names = names; // the non-vector columns returned with each hit
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// engine: a normal Engine (Node, browser, portable) or a SplitEngine.
|
|
31
|
+
static async create(engine, { vectors, dim, metric = 'cosine', columns = {} }) {
|
|
32
|
+
if (!METRICS.has(metric)) throw new Error(`vectro: metric must be 'cosine', 'dot' or 'l2', got "${metric}"`);
|
|
33
|
+
if (!(dim >= 1)) throw new Error('vectro: VectorIndex needs dim, the length of each embedding');
|
|
34
|
+
const flat = flatten(vectors, dim);
|
|
35
|
+
const n = flat.length / dim;
|
|
36
|
+
for (const [name, values] of Object.entries(columns)) {
|
|
37
|
+
if (/^v\d+$/.test(name) || name.startsWith('__np_')) throw new Error(`vectro: column name "${name}" is reserved for vectors`);
|
|
38
|
+
if (values.length !== n) throw new RangeError(`vectro: column "${name}" has ${values.length} values, expected ${n} (one per vector)`);
|
|
39
|
+
}
|
|
40
|
+
const cols = { ...columns };
|
|
41
|
+
const vcols = Array.from({ length: dim }, () => new Float32Array(n));
|
|
42
|
+
const norm2 = new Float32Array(n);
|
|
43
|
+
for (let i = 0; i < n; i++) {
|
|
44
|
+
let s = 0;
|
|
45
|
+
for (let j = 0; j < dim; j++) s += flat[i * dim + j] ** 2;
|
|
46
|
+
const scale = metric === 'cosine' && s > 0 ? 1 / Math.sqrt(s) : 1;
|
|
47
|
+
for (let j = 0; j < dim; j++) vcols[j][i] = flat[i * dim + j] * scale;
|
|
48
|
+
norm2[i] = metric === 'cosine' ? 1 : s;
|
|
49
|
+
}
|
|
50
|
+
vcols.forEach((c, j) => { cols[`v${j}`] = c; });
|
|
51
|
+
if (metric === 'l2') cols.__np_norm2 = norm2;
|
|
52
|
+
const table = typeof engine.tableAsync === 'function'
|
|
53
|
+
? await engine.tableAsync(cols, { type: Float32Array })
|
|
54
|
+
: engine.table(cols, { type: Float32Array });
|
|
55
|
+
return new VectorIndex(table, { dim, metric, names: Object.keys(columns) });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get length() { return this.table.length; }
|
|
59
|
+
|
|
60
|
+
// The k closest vectors to `query` (among rows matching `filter`), best first.
|
|
61
|
+
async searchAsync(query, { k = 10, filter = null, params = {} } = {}) {
|
|
62
|
+
const q = Array.from(query);
|
|
63
|
+
if (q.length !== this.dim) throw new RangeError(`vectro: query has ${q.length} values, the index has dim ${this.dim}`);
|
|
64
|
+
const view = filter ? this.table.filter(filter, params) : this.table;
|
|
65
|
+
const opts = { k, metric: this.metric, dim: this.dim, names: this.names };
|
|
66
|
+
// split tables: every worker searches its own rows in one round trip
|
|
67
|
+
if (typeof view.vectorSearchAsync === 'function') return view.vectorSearchAsync(q, opts);
|
|
68
|
+
return searchTable(view, q, opts);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async dispose() { await this.table.dispose(); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function flatten(vectors, dim) {
|
|
75
|
+
if (ArrayBuffer.isView(vectors)) {
|
|
76
|
+
if (vectors.length % dim) throw new RangeError(`vectro: ${vectors.length} values is not a whole number of ${dim}-dimension vectors`);
|
|
77
|
+
return vectors;
|
|
78
|
+
}
|
|
79
|
+
const flat = new Float32Array(vectors.length * dim);
|
|
80
|
+
vectors.forEach((v, i) => {
|
|
81
|
+
if (v.length !== dim) throw new RangeError(`vectro: vector ${i} has ${v.length} values, expected ${dim}`);
|
|
82
|
+
flat.set(v, i * dim);
|
|
83
|
+
});
|
|
84
|
+
return flat;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Search one table (or filtered view): running dot product, DIMS_PER_PASS
|
|
88
|
+
// dimensions per pass, then top k over a narrow view (not the vectors).
|
|
89
|
+
export async function searchTable(view, query, { k, metric, dim, names }) {
|
|
90
|
+
const q = Float64Array.from(query);
|
|
91
|
+
if (metric === 'cosine') {
|
|
92
|
+
const len = Math.hypot(...q);
|
|
93
|
+
if (len > 0) for (let j = 0; j < q.length; j++) q[j] /= len;
|
|
94
|
+
}
|
|
95
|
+
const made = [];
|
|
96
|
+
try {
|
|
97
|
+
let prev = null;
|
|
98
|
+
for (let j0 = 0, pass = 0; j0 < dim; j0 += DIMS_PER_PASS, pass++) {
|
|
99
|
+
const terms = [], scalars = {};
|
|
100
|
+
for (let j = j0; j < Math.min(dim, j0 + DIMS_PER_PASS); j++) {
|
|
101
|
+
terms.push(`__np_q${j - j0} * v${j}`);
|
|
102
|
+
scalars[`__np_q${j - j0}`] = q[j];
|
|
103
|
+
}
|
|
104
|
+
const name = `__np_dot${pass}`;
|
|
105
|
+
view = await view.deriveAsync(name, prev ? `${prev} + ${terms.join(' + ')}` : terms.join(' + '), scalars);
|
|
106
|
+
made.push(view);
|
|
107
|
+
prev = name;
|
|
108
|
+
}
|
|
109
|
+
let key, scoreCol = prev;
|
|
110
|
+
if (metric === 'l2') {
|
|
111
|
+
view = await view.deriveAsync('__np_score', `__np_norm2 - 2 * ${prev} + __np_qq`, { __np_qq: q.reduce((a, x) => a + x * x, 0) });
|
|
112
|
+
made.push(view);
|
|
113
|
+
key = scoreCol = '__np_score';
|
|
114
|
+
} else {
|
|
115
|
+
key = `-${prev}`;
|
|
116
|
+
}
|
|
117
|
+
const best = await view.select(...names, scoreCol).topAsync(k, key);
|
|
118
|
+
try {
|
|
119
|
+
return best.toRows().map((r) => {
|
|
120
|
+
const hit = {};
|
|
121
|
+
for (const name of names) hit[name] = r[name];
|
|
122
|
+
hit.score = metric === 'l2' ? Math.max(0, r[scoreCol]) : r[scoreCol];
|
|
123
|
+
return hit;
|
|
124
|
+
});
|
|
125
|
+
} finally {
|
|
126
|
+
best.dispose();
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
for (const t of made.reverse()) await t.dispose();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Merge per-worker hits (each best first) into the overall best k.
|
|
134
|
+
export function mergeHits(lists, k, metric) {
|
|
135
|
+
const all = lists.flat();
|
|
136
|
+
const better = metric === 'l2' ? (a, b) => a.score - b.score : (a, b) => b.score - a.score;
|
|
137
|
+
return all.sort(better).slice(0, k);
|
|
138
|
+
}
|
package/src/wasm.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Generates the SIMD kernels as a WebAssembly binary at startup, so the
|
|
2
|
+
// package needs no compiler toolchain. Only the handful of instructions the
|
|
3
|
+
// kernels use are encoded here.
|
|
4
|
+
|
|
5
|
+
export const I32 = 0x7f, F32 = 0x7d, F64 = 0x7c, V128 = 0x7b;
|
|
6
|
+
|
|
7
|
+
function uleb(n) {
|
|
8
|
+
const out = [];
|
|
9
|
+
do {
|
|
10
|
+
let b = n & 0x7f;
|
|
11
|
+
n >>>= 7;
|
|
12
|
+
if (n) b |= 0x80;
|
|
13
|
+
out.push(b);
|
|
14
|
+
} while (n);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sleb(n) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (;;) {
|
|
21
|
+
const b = n & 0x7f;
|
|
22
|
+
n >>= 7;
|
|
23
|
+
if ((n === 0 && !(b & 0x40)) || (n === -1 && b & 0x40)) { out.push(b); return out; }
|
|
24
|
+
out.push(b | 0x80);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bytesOf = (Type, v) => [...new Uint8Array(new Type([v]).buffer)];
|
|
29
|
+
const name = (s) => { const b = [...new TextEncoder().encode(s)]; return [...uleb(b.length), ...b]; };
|
|
30
|
+
const vec = (items) => [...uleb(items.length), ...items.flat()];
|
|
31
|
+
const section = (id, content) => [id, ...uleb(content.length), ...content];
|
|
32
|
+
|
|
33
|
+
// ---- instructions -----------------------------------------------------------
|
|
34
|
+
export const get = (i) => [0x20, ...uleb(i)];
|
|
35
|
+
export const set = (i) => [0x21, ...uleb(i)];
|
|
36
|
+
export const i32c = (v) => [0x41, ...sleb(v)];
|
|
37
|
+
export const block = [0x02, 0x40], loop = [0x03, 0x40], end = [0x0b];
|
|
38
|
+
export const br = (l) => [0x0c, ...uleb(l)];
|
|
39
|
+
export const brIf = (l) => [0x0d, ...uleb(l)];
|
|
40
|
+
export const i32add = [0x6a], i32mul = [0x6c], i32and = [0x71], i32geu = [0x4f], i32wrapI64 = [0xa7];
|
|
41
|
+
export const simd = (code, ...imm) => [0xfd, ...uleb(code), ...imm];
|
|
42
|
+
export const memarg = (align, offset) => [...uleb(align), ...uleb(offset)];
|
|
43
|
+
export const vload = (off) => simd(0x00, ...memarg(0, off));
|
|
44
|
+
export const vstore = (off) => simd(0x0b, ...memarg(0, off));
|
|
45
|
+
|
|
46
|
+
// Per element type: scalar opcodes and their SIMD (128-bit) equivalents.
|
|
47
|
+
export const TY = {
|
|
48
|
+
f64: {
|
|
49
|
+
val: F64, bytes: 8, lanes: 2, cnst: (v) => [0x44, ...bytesOf(Float64Array, v)],
|
|
50
|
+
load: [0x2b, ...memarg(3, 0)], store: [0x39, ...memarg(3, 0)],
|
|
51
|
+
add: [0xa0], mul: [0xa2], min: [0xa4], max: [0xa5], gt: [0x64], toF64: [],
|
|
52
|
+
vsplat: simd(0x14), vlane: (l) => simd(0x21, l),
|
|
53
|
+
vadd: simd(0xf0), vmul: simd(0xf2), vmin: simd(0xf4), vmax: simd(0xf5), vgt: simd(0x4a),
|
|
54
|
+
// comparison masks are 64-bit integer lanes
|
|
55
|
+
mlanes: 2, msub: simd(0xd1), madd: simd(0xce), mlane: (l) => [...simd(0x1d, l), ...i32wrapI64],
|
|
56
|
+
},
|
|
57
|
+
f32: {
|
|
58
|
+
val: F32, bytes: 4, lanes: 4, cnst: (v) => [0x43, ...bytesOf(Float32Array, v)],
|
|
59
|
+
load: [0x2a, ...memarg(2, 0)], store: [0x38, ...memarg(2, 0)],
|
|
60
|
+
add: [0x92], mul: [0x94], min: [0x96], max: [0x97], gt: [0x5e], toF64: [0xbb],
|
|
61
|
+
vsplat: simd(0x13), vlane: (l) => simd(0x1f, l),
|
|
62
|
+
vadd: simd(0xe4), vmul: simd(0xe6), vmin: simd(0xe8), vmax: simd(0xe9), vgt: simd(0x44),
|
|
63
|
+
mlanes: 4, msub: simd(0xb1), madd: simd(0xae), mlane: (l) => simd(0x1b, l),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export class Fn {
|
|
68
|
+
constructor(exportName, params, results) {
|
|
69
|
+
this.exportName = exportName;
|
|
70
|
+
this.params = params;
|
|
71
|
+
this.results = results;
|
|
72
|
+
this.locals = [];
|
|
73
|
+
this.code = [];
|
|
74
|
+
}
|
|
75
|
+
local(type) { this.locals.push(type); return this.params.length + this.locals.length - 1; }
|
|
76
|
+
emit(...parts) { for (const p of parts) this.code.push(...p); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Loop skeleton shared by every kernel. `ptrs` advance together; the main loop
|
|
80
|
+
// handles U vectors (U*16 bytes) per iteration, then a scalar loop does the
|
|
81
|
+
// leftover elements.
|
|
82
|
+
// strides: bytes per row of each pointer (default: the element type's; packed
|
|
83
|
+
// columns use fewer, see pack.mjs).
|
|
84
|
+
export function loops(f, ty, { ptrs, n, U, body, between = () => {}, tail, strides = null }) {
|
|
85
|
+
const vend = f.local(I32), stop = f.local(I32);
|
|
86
|
+
const perIter = ty.lanes * U;
|
|
87
|
+
const stride = (i) => strides?.[i] ?? ty.bytes;
|
|
88
|
+
f.emit(get(n), i32c(~(perIter - 1)), i32and, i32c(stride(0)), i32mul, get(ptrs[0]), i32add, set(vend));
|
|
89
|
+
f.emit(get(n), i32c(stride(0)), i32mul, get(ptrs[0]), i32add, set(stop));
|
|
90
|
+
f.emit(block, loop, get(ptrs[0]), get(vend), i32geu, brIf(1));
|
|
91
|
+
body();
|
|
92
|
+
ptrs.forEach((p, i) => f.emit(get(p), i32c(stride(i) * perIter), i32add, set(p)));
|
|
93
|
+
f.emit(br(0), end, end);
|
|
94
|
+
between();
|
|
95
|
+
f.emit(block, loop, get(ptrs[0]), get(stop), i32geu, brIf(1));
|
|
96
|
+
tail();
|
|
97
|
+
ptrs.forEach((p, i) => f.emit(get(p), i32c(stride(i)), i32add, set(p)));
|
|
98
|
+
f.emit(br(0), end, end);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// sum / min / max with 4 independent vector accumulators, so the CPU can run
|
|
102
|
+
// several additions at once instead of waiting on each one.
|
|
103
|
+
function reduction(t, kind) {
|
|
104
|
+
const ty = TY[t];
|
|
105
|
+
const f = new Fn(`${kind}_${t}`, [I32, I32], [F64]);
|
|
106
|
+
const [p, n] = [0, 1];
|
|
107
|
+
const U = 4;
|
|
108
|
+
const acc = Array.from({ length: U }, () => f.local(V128));
|
|
109
|
+
const s = f.local(ty.val);
|
|
110
|
+
const identity = kind === 'sum' ? 0 : kind === 'min' ? Infinity : -Infinity;
|
|
111
|
+
const vop = ty[{ sum: 'vadd', min: 'vmin', max: 'vmax' }[kind]];
|
|
112
|
+
const sop = ty[{ sum: 'add', min: 'min', max: 'max' }[kind]];
|
|
113
|
+
for (const a of acc) f.emit(ty.cnst(identity), ty.vsplat, set(a));
|
|
114
|
+
loops(f, ty, {
|
|
115
|
+
ptrs: [p], n, U,
|
|
116
|
+
body: () => acc.forEach((a, k) => f.emit(get(a), get(p), vload(16 * k), vop, set(a))),
|
|
117
|
+
between: () => {
|
|
118
|
+
f.emit(get(acc[0]), get(acc[1]), vop, get(acc[2]), get(acc[3]), vop, vop, set(acc[0]));
|
|
119
|
+
f.emit(get(acc[0]), ty.vlane(0));
|
|
120
|
+
for (let l = 1; l < ty.lanes; l++) f.emit(get(acc[0]), ty.vlane(l), sop);
|
|
121
|
+
f.emit(set(s));
|
|
122
|
+
},
|
|
123
|
+
tail: () => f.emit(get(s), get(p), ty.load, sop, set(s)),
|
|
124
|
+
});
|
|
125
|
+
f.emit(get(s), ty.toF64);
|
|
126
|
+
return f;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function dot(t) {
|
|
130
|
+
const ty = TY[t];
|
|
131
|
+
const f = new Fn(`dot_${t}`, [I32, I32, I32], [F64]);
|
|
132
|
+
const [pa, pb, n] = [0, 1, 2];
|
|
133
|
+
const U = 4;
|
|
134
|
+
const acc = Array.from({ length: U }, () => f.local(V128));
|
|
135
|
+
const s = f.local(ty.val);
|
|
136
|
+
for (const a of acc) f.emit(ty.cnst(0), ty.vsplat, set(a));
|
|
137
|
+
loops(f, ty, {
|
|
138
|
+
ptrs: [pa, pb], n, U,
|
|
139
|
+
body: () => acc.forEach((a, k) => f.emit(get(a), get(pa), vload(16 * k), get(pb), vload(16 * k), ty.vmul, ty.vadd, set(a))),
|
|
140
|
+
between: () => {
|
|
141
|
+
f.emit(get(acc[0]), get(acc[1]), ty.vadd, get(acc[2]), get(acc[3]), ty.vadd, ty.vadd, set(acc[0]));
|
|
142
|
+
f.emit(get(acc[0]), ty.vlane(0));
|
|
143
|
+
for (let l = 1; l < ty.lanes; l++) f.emit(get(acc[0]), ty.vlane(l), ty.add);
|
|
144
|
+
f.emit(set(s));
|
|
145
|
+
},
|
|
146
|
+
tail: () => f.emit(get(s), get(pa), ty.load, get(pb), ty.load, ty.mul, ty.add, set(s)),
|
|
147
|
+
});
|
|
148
|
+
f.emit(get(s), ty.toF64);
|
|
149
|
+
return f;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Count elements > x. A SIMD compare gives all-ones (-1) per matching lane,
|
|
153
|
+
// so subtracting the mask adds 1 to each lane's counter.
|
|
154
|
+
function countGreater(t) {
|
|
155
|
+
const ty = TY[t];
|
|
156
|
+
const f = new Fn(`countgt_${t}`, [I32, I32, ty.val], [I32]);
|
|
157
|
+
const [p, n, x] = [0, 1, 2];
|
|
158
|
+
const U = 4;
|
|
159
|
+
const vx = f.local(V128), c = f.local(I32);
|
|
160
|
+
const acc = Array.from({ length: U }, () => f.local(V128));
|
|
161
|
+
f.emit(get(x), ty.vsplat, set(vx));
|
|
162
|
+
for (const a of acc) f.emit(i32c(0), simd(0x11), set(a)); // i32x4.splat(0) = all-zero vector
|
|
163
|
+
loops(f, ty, {
|
|
164
|
+
ptrs: [p], n, U,
|
|
165
|
+
body: () => acc.forEach((a, k) => f.emit(get(a), get(p), vload(16 * k), get(vx), ty.vgt, ty.msub, set(a))),
|
|
166
|
+
between: () => {
|
|
167
|
+
f.emit(get(acc[0]), get(acc[1]), ty.madd, get(acc[2]), get(acc[3]), ty.madd, ty.madd, set(acc[0]));
|
|
168
|
+
f.emit(get(acc[0]), ty.mlane(0));
|
|
169
|
+
for (let l = 1; l < ty.mlanes; l++) f.emit(get(acc[0]), ty.mlane(l), i32add);
|
|
170
|
+
f.emit(set(c));
|
|
171
|
+
},
|
|
172
|
+
tail: () => f.emit(get(c), get(p), ty.load, get(x), ty.gt, i32add, set(c)),
|
|
173
|
+
});
|
|
174
|
+
f.emit(get(c));
|
|
175
|
+
return f;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// out = a + b
|
|
179
|
+
function add(t) {
|
|
180
|
+
const ty = TY[t];
|
|
181
|
+
const f = new Fn(`add_${t}`, [I32, I32, I32, I32], []);
|
|
182
|
+
const [pa, pb, po, n] = [0, 1, 2, 3];
|
|
183
|
+
const U = 2;
|
|
184
|
+
loops(f, ty, {
|
|
185
|
+
ptrs: [pa, pb, po], n, U,
|
|
186
|
+
body: () => { for (let k = 0; k < U; k++) f.emit(get(po), get(pa), vload(16 * k), get(pb), vload(16 * k), ty.vadd, vstore(16 * k)); },
|
|
187
|
+
tail: () => f.emit(get(po), get(pa), ty.load, get(pb), ty.load, ty.add, ty.store),
|
|
188
|
+
});
|
|
189
|
+
return f;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// out = a * k + c
|
|
193
|
+
function scale(t) {
|
|
194
|
+
const ty = TY[t];
|
|
195
|
+
const f = new Fn(`scale_${t}`, [I32, I32, I32, ty.val, ty.val], []);
|
|
196
|
+
const [pa, po, n, k, c] = [0, 1, 2, 3, 4];
|
|
197
|
+
const U = 2;
|
|
198
|
+
const vk = f.local(V128), vc = f.local(V128);
|
|
199
|
+
f.emit(get(k), ty.vsplat, set(vk), get(c), ty.vsplat, set(vc));
|
|
200
|
+
loops(f, ty, {
|
|
201
|
+
ptrs: [pa, po], n, U,
|
|
202
|
+
body: () => { for (let j = 0; j < U; j++) f.emit(get(po), get(pa), vload(16 * j), get(vk), ty.vmul, get(vc), ty.vadd, vstore(16 * j)); },
|
|
203
|
+
tail: () => f.emit(get(po), get(pa), ty.load, get(k), ty.mul, get(c), ty.add, ty.store),
|
|
204
|
+
});
|
|
205
|
+
return f;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// float32 sums are left to JS: a 4-lane float32 accumulator loses too much
|
|
209
|
+
// precision over millions of values (it would need pairwise summation).
|
|
210
|
+
export function kernelFns() {
|
|
211
|
+
return [
|
|
212
|
+
reduction('f64', 'sum'), reduction('f64', 'min'), reduction('f64', 'max'),
|
|
213
|
+
reduction('f32', 'min'), reduction('f32', 'max'),
|
|
214
|
+
dot('f64'), countGreater('f64'), countGreater('f32'),
|
|
215
|
+
add('f64'), add('f32'), scale('f64'), scale('f32'),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function buildKernels(shared = true) {
|
|
220
|
+
return buildModule(kernelFns(), shared);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Assemble functions into a module that imports env.memory: shared memory
|
|
224
|
+
// when threads use it, plain memory for single-thread and edge builds.
|
|
225
|
+
export function buildModule(fns, shared = true) {
|
|
226
|
+
const types = [];
|
|
227
|
+
const typeIndex = fns.map((f) => {
|
|
228
|
+
const sig = [0x60, ...vec(f.params.map((x) => [x])), ...vec(f.results.map((x) => [x]))];
|
|
229
|
+
let i = types.findIndex((t) => t.join() === sig.join());
|
|
230
|
+
if (i < 0) { types.push(sig); i = types.length - 1; }
|
|
231
|
+
return i;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const importMemory = [...name('env'), ...name('memory'), 0x02, shared ? 0x03 : 0x01, ...uleb(1), ...uleb(65536)]; // max 4GB
|
|
235
|
+
const bodies = fns.map((f) => {
|
|
236
|
+
const groups = [];
|
|
237
|
+
for (const t of f.locals) {
|
|
238
|
+
const last = groups[groups.length - 1];
|
|
239
|
+
if (last && last[1] === t) last[0]++;
|
|
240
|
+
else groups.push([1, t]);
|
|
241
|
+
}
|
|
242
|
+
const body = [...vec(groups.map(([count, t]) => [...uleb(count), t])), ...f.code, ...end];
|
|
243
|
+
return [...uleb(body.length), ...body];
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return new Uint8Array([
|
|
247
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
248
|
+
...section(1, vec(types)),
|
|
249
|
+
...section(2, vec([importMemory])),
|
|
250
|
+
...section(3, vec(typeIndex.map((i) => uleb(i)))),
|
|
251
|
+
...section(7, vec(fns.map((f, i) => [...name(f.exportName), 0x00, ...uleb(i)]))),
|
|
252
|
+
...section(10, vec(bodies)),
|
|
253
|
+
]);
|
|
254
|
+
}
|