363 lines
13 KiB
JavaScript
363 lines
13 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// High-resolution timer
|
||
const now = () => {
|
||
const [s, ns] = process.hrtime();
|
||
return s * 1e9 + ns;
|
||
};
|
||
|
||
async function benchmark() {
|
||
console.log('╔══════════════════════════════════════════════════════════════╗');
|
||
console.log('║ MICRO HNSW WASM v2.2 - DEEP BENCHMARK & ANALYSIS ║');
|
||
console.log('╚══════════════════════════════════════════════════════════════╝\n');
|
||
|
||
// Load WASM
|
||
const wasmPath = path.join(__dirname, 'micro_hnsw.wasm');
|
||
const wasmBuffer = fs.readFileSync(wasmPath);
|
||
const wasmModule = await WebAssembly.instantiate(wasmBuffer);
|
||
const wasm = wasmModule.instance.exports;
|
||
const memory = new Float32Array(wasm.memory.buffer);
|
||
|
||
console.log('=== BINARY ANALYSIS ===');
|
||
console.log('Size: ' + wasmBuffer.length + ' bytes (' + (wasmBuffer.length/1024).toFixed(2) + ' KB)');
|
||
console.log('Target: 8192 bytes (8 KB)');
|
||
console.log('Headroom: ' + (8192 - wasmBuffer.length) + ' bytes (' + ((8192 - wasmBuffer.length)/8192*100).toFixed(1) + '%)');
|
||
console.log('Functions exported: ' + Object.keys(wasm).filter(k => typeof wasm[k] === 'function').length);
|
||
console.log('');
|
||
|
||
// ========== HNSW BENCHMARKS ==========
|
||
console.log('=== HNSW BENCHMARKS ===');
|
||
|
||
const DIMS = 16;
|
||
const ITERATIONS = 1000;
|
||
|
||
// Benchmark: Init
|
||
let t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.init(DIMS, 0, 0);
|
||
}
|
||
let initTime = (now() - t0) / ITERATIONS;
|
||
console.log('init(): ' + initTime.toFixed(0) + ' ns/op');
|
||
|
||
// Prepare insert buffer
|
||
wasm.init(DIMS, 0, 0);
|
||
const insertPtr = wasm.get_insert_ptr() / 4;
|
||
|
||
// Benchmark: Single insert (empty index)
|
||
t0 = now();
|
||
for (let iter = 0; iter < 100; iter++) {
|
||
wasm.init(DIMS, 0, 0);
|
||
for (let j = 0; j < DIMS; j++) memory[insertPtr + j] = Math.random();
|
||
wasm.insert();
|
||
}
|
||
let insertFirstTime = (now() - t0) / 100;
|
||
console.log('insert() first: ' + insertFirstTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Insert with connections (fill to 16 vectors)
|
||
wasm.init(DIMS, 0, 0);
|
||
for (let i = 0; i < 16; i++) {
|
||
for (let j = 0; j < DIMS; j++) memory[insertPtr + j] = Math.random();
|
||
wasm.insert();
|
||
}
|
||
|
||
t0 = now();
|
||
for (let iter = 0; iter < 100; iter++) {
|
||
wasm.init(DIMS, 0, 0);
|
||
for (let i = 0; i < 16; i++) {
|
||
for (let j = 0; j < DIMS; j++) memory[insertPtr + j] = Math.random();
|
||
wasm.insert();
|
||
}
|
||
}
|
||
let insert16Time = (now() - t0) / 100;
|
||
console.log('insert() x16: ' + (insert16Time/1000).toFixed(1) + ' µs total (' + (insert16Time/16).toFixed(0) + ' ns avg/vector)');
|
||
|
||
// Fill to 32 vectors for search benchmark
|
||
wasm.init(DIMS, 0, 0);
|
||
for (let i = 0; i < 32; i++) {
|
||
for (let j = 0; j < DIMS; j++) memory[insertPtr + j] = Math.random();
|
||
wasm.insert();
|
||
}
|
||
console.log('Indexed: ' + wasm.count() + ' vectors');
|
||
|
||
// Benchmark: Search k=1
|
||
const queryPtr = wasm.get_query_ptr() / 4;
|
||
for (let j = 0; j < DIMS; j++) memory[queryPtr + j] = Math.random();
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.search(1);
|
||
}
|
||
let search1Time = (now() - t0) / ITERATIONS;
|
||
console.log('search(k=1): ' + search1Time.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Search k=6
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.search(6);
|
||
}
|
||
let search6Time = (now() - t0) / ITERATIONS;
|
||
console.log('search(k=6): ' + search6Time.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Search k=16
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.search(16);
|
||
}
|
||
let search16Time = (now() - t0) / ITERATIONS;
|
||
console.log('search(k=16): ' + search16Time.toFixed(0) + ' ns/op');
|
||
|
||
console.log('');
|
||
|
||
// ========== GNN BENCHMARKS ==========
|
||
console.log('=== GNN BENCHMARKS ===');
|
||
|
||
// Benchmark: Node type operations
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.set_node_type(i % 32, i % 16);
|
||
}
|
||
let setTypeTime = (now() - t0) / ITERATIONS;
|
||
console.log('set_node_type(): ' + setTypeTime.toFixed(0) + ' ns/op');
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.get_node_type(i % 32);
|
||
}
|
||
let getTypeTime = (now() - t0) / ITERATIONS;
|
||
console.log('get_node_type(): ' + getTypeTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Edge weight operations
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.set_edge_weight(i % 32, i % 256);
|
||
}
|
||
let setWeightTime = (now() - t0) / ITERATIONS;
|
||
console.log('set_edge_weight(): ' + setWeightTime.toFixed(0) + ' ns/op');
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.get_edge_weight(i % 32);
|
||
}
|
||
let getWeightTime = (now() - t0) / ITERATIONS;
|
||
console.log('get_edge_weight(): ' + getWeightTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Aggregate neighbors
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.aggregate_neighbors(i % 32);
|
||
}
|
||
let aggregateTime = (now() - t0) / ITERATIONS;
|
||
console.log('aggregate(): ' + aggregateTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: Update vector
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.update_vector(i % 32, 0.01);
|
||
}
|
||
let updateTime = (now() - t0) / ITERATIONS;
|
||
console.log('update_vector(): ' + updateTime.toFixed(0) + ' ns/op');
|
||
|
||
console.log('');
|
||
|
||
// ========== SNN BENCHMARKS ==========
|
||
console.log('=== SNN BENCHMARKS ===');
|
||
|
||
wasm.snn_reset();
|
||
|
||
// Benchmark: snn_inject
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_inject(i % 32, 0.1);
|
||
}
|
||
let injectTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_inject(): ' + injectTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: snn_step
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_step(1.0);
|
||
}
|
||
let stepTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_step(): ' + stepTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: snn_propagate
|
||
// First make some neurons spike
|
||
wasm.snn_reset();
|
||
for (let i = 0; i < 8; i++) wasm.snn_inject(i, 2.0);
|
||
wasm.snn_step(1.0);
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_propagate(0.5);
|
||
}
|
||
let propagateTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_propagate(): ' + propagateTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: snn_stdp
|
||
wasm.snn_reset();
|
||
for (let i = 0; i < 8; i++) wasm.snn_inject(i, 2.0);
|
||
wasm.snn_step(1.0);
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_stdp();
|
||
}
|
||
let stdpTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_stdp(): ' + stdpTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: snn_tick (combined)
|
||
wasm.snn_reset();
|
||
for (let i = 0; i < 8; i++) wasm.snn_inject(i, 0.5);
|
||
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_tick(1.0, 0.5, 1);
|
||
}
|
||
let tickTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_tick(): ' + tickTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: snn_get_spikes
|
||
t0 = now();
|
||
for (let i = 0; i < ITERATIONS; i++) {
|
||
wasm.snn_get_spikes();
|
||
}
|
||
let getSpikesTime = (now() - t0) / ITERATIONS;
|
||
console.log('snn_get_spikes(): ' + getSpikesTime.toFixed(0) + ' ns/op');
|
||
|
||
// Benchmark: hnsw_to_snn
|
||
wasm.snn_reset();
|
||
t0 = now();
|
||
for (let i = 0; i < 100; i++) {
|
||
wasm.hnsw_to_snn(6, 1.0);
|
||
}
|
||
let hnswToSnnTime = (now() - t0) / 100;
|
||
console.log('hnsw_to_snn(): ' + hnswToSnnTime.toFixed(0) + ' ns/op');
|
||
|
||
console.log('');
|
||
|
||
// ========== MEMORY ANALYSIS ==========
|
||
console.log('=== MEMORY LAYOUT ANALYSIS ===');
|
||
|
||
const memoryBytes = wasm.memory.buffer.byteLength;
|
||
console.log('Linear memory: ' + memoryBytes + ' bytes (' + (memoryBytes/1024) + ' KB)');
|
||
console.log('Insert ptr: ' + wasm.get_insert_ptr());
|
||
console.log('Query ptr: ' + wasm.get_query_ptr());
|
||
console.log('Result ptr: ' + wasm.get_result_ptr());
|
||
console.log('Global ptr: ' + wasm.get_global_ptr());
|
||
console.log('Delta ptr: ' + wasm.get_delta_ptr());
|
||
|
||
// Calculate static data size from WASM
|
||
const dataEnd = wasm.__data_end;
|
||
const heapBase = wasm.__heap_base;
|
||
console.log('Data end: ' + dataEnd);
|
||
console.log('Heap base: ' + heapBase);
|
||
console.log('Static data: ' + (heapBase - 0) + ' bytes');
|
||
|
||
console.log('');
|
||
|
||
// ========== THROUGHPUT ANALYSIS ==========
|
||
console.log('=== THROUGHPUT ANALYSIS ===');
|
||
|
||
const searchOpsPerSec = 1e9 / search6Time;
|
||
const insertOpsPerSec = 1e9 / (insert16Time / 16);
|
||
const tickOpsPerSec = 1e9 / tickTime;
|
||
|
||
console.log('Search (k=6): ' + (searchOpsPerSec/1e6).toFixed(2) + ' M ops/sec');
|
||
console.log('Insert: ' + (insertOpsPerSec/1e6).toFixed(2) + ' M ops/sec');
|
||
console.log('SNN tick: ' + (tickOpsPerSec/1e6).toFixed(2) + ' M ops/sec');
|
||
|
||
// ASIC projection (256 cores)
|
||
console.log('\n--- 256-Core ASIC Projection ---');
|
||
console.log('Search: ' + (searchOpsPerSec * 256 / 1e9).toFixed(2) + ' B ops/sec');
|
||
console.log('SNN tick: ' + (tickOpsPerSec * 256 / 1e6).toFixed(0) + ' M neurons/sec');
|
||
console.log('Total vectors: ' + (32 * 256) + ' (32/core × 256 cores)');
|
||
|
||
console.log('');
|
||
|
||
// ========== ACCURACY TEST ==========
|
||
console.log('=== ACCURACY VALIDATION ===');
|
||
|
||
// Test search accuracy with known vectors
|
||
wasm.init(4, 0, 0); // L2 metric, 4 dims
|
||
const testVectors = [
|
||
[1, 0, 0, 0],
|
||
[0, 1, 0, 0],
|
||
[0, 0, 1, 0],
|
||
[0, 0, 0, 1],
|
||
[0.5, 0.5, 0, 0],
|
||
];
|
||
|
||
for (const v of testVectors) {
|
||
for (let j = 0; j < 4; j++) memory[insertPtr + j] = v[j];
|
||
wasm.insert();
|
||
}
|
||
|
||
// Query closest to [1,0,0,0]
|
||
memory[queryPtr] = 0.9;
|
||
memory[queryPtr + 1] = 0.1;
|
||
memory[queryPtr + 2] = 0;
|
||
memory[queryPtr + 3] = 0;
|
||
|
||
const found = wasm.search(3);
|
||
const resultPtr = wasm.get_result_ptr();
|
||
const resultU8 = new Uint8Array(wasm.memory.buffer);
|
||
const resultF32 = new Float32Array(wasm.memory.buffer);
|
||
|
||
console.log('Query: [0.9, 0.1, 0, 0], Expected nearest: idx=0 [1,0,0,0]');
|
||
console.log('Found ' + found + ' neighbors:');
|
||
for (let i = 0; i < found; i++) {
|
||
const idx = resultU8[resultPtr + i * 8];
|
||
const dist = resultF32[(resultPtr + i * 8 + 4) / 4];
|
||
console.log(' #' + (i+1) + ': idx=' + idx + ' dist=' + dist.toFixed(4) + ' vec=[' + testVectors[idx].join(',') + ']');
|
||
}
|
||
|
||
// Verify correct ordering
|
||
const firstIdx = resultU8[resultPtr];
|
||
if (firstIdx === 0) {
|
||
console.log('✓ Accuracy: PASS (nearest neighbor correct)');
|
||
} else {
|
||
console.log('✗ Accuracy: FAIL (expected idx=0, got idx=' + firstIdx + ')');
|
||
}
|
||
|
||
console.log('');
|
||
|
||
// ========== SNN DYNAMICS VALIDATION ==========
|
||
console.log('=== SNN DYNAMICS VALIDATION ===');
|
||
|
||
wasm.init(4, 0, 0);
|
||
for (const v of testVectors) {
|
||
for (let j = 0; j < 4; j++) memory[insertPtr + j] = v[j];
|
||
wasm.insert();
|
||
}
|
||
|
||
wasm.snn_reset();
|
||
|
||
// Test LIF dynamics
|
||
console.log('LIF Neuron Test (τ=20ms, threshold=1.0):');
|
||
wasm.snn_inject(0, 0.8);
|
||
console.log(' t=0: inject 0.8, membrane=' + wasm.snn_get_membrane(0).toFixed(3));
|
||
|
||
wasm.snn_step(5.0);
|
||
console.log(' t=5: decay, membrane=' + wasm.snn_get_membrane(0).toFixed(3) + ' (expected ~0.6)');
|
||
|
||
wasm.snn_inject(0, 0.5);
|
||
console.log(' t=5: inject +0.5, membrane=' + wasm.snn_get_membrane(0).toFixed(3));
|
||
|
||
const spiked = wasm.snn_step(1.0);
|
||
console.log(' t=6: step, spiked=' + spiked + ', membrane=' + wasm.snn_get_membrane(0).toFixed(3));
|
||
|
||
if (spiked > 0) {
|
||
console.log('✓ LIF dynamics: PASS (spike generated above threshold)');
|
||
} else {
|
||
console.log('✗ LIF dynamics: membrane should have spiked');
|
||
}
|
||
|
||
console.log('');
|
||
console.log('═══════════════════════════════════════════════════════════════');
|
||
console.log(' BENCHMARK COMPLETE');
|
||
console.log('═══════════════════════════════════════════════════════════════');
|
||
}
|
||
|
||
benchmark().catch(console.error);
|