Files

363 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);