diff --git a/.gitignore b/.gitignore index b336acd..545b248 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ htmlcov/ # Local config / secrets .env *.local + +# Benchmark artifacts +benchmark_results.txt +benchmarks/benchmark_results.txt +benchmarks/*.swp diff --git a/benchmarks/benchmark_quantization.py b/benchmarks/benchmark_quantization.py new file mode 100644 index 0000000..f3340ff --- /dev/null +++ b/benchmarks/benchmark_quantization.py @@ -0,0 +1,197 @@ +"""Benchmark ScalarQuantizer vs ProductQuantizer.""" + +import time + +import numpy as np + +from dynavec.quantization import ProductQuantizer, ScalarQuantizer + + +def generate_dataset(n_vectors=10_000, dim=32, seed=0): + """Generate a reproducible clustered float32 dataset.""" + rng = np.random.default_rng(seed) + + centers = rng.normal(size=(8, dim)).astype(np.float32) + assign = rng.integers(0, 8, size=n_vectors) + + x = centers[assign] + 0.05 * rng.normal( + size=(n_vectors, dim) + ).astype(np.float32) + + return x.astype(np.float32) + + +def benchmark_encode(quantizer, vectors, iterations=5): + """Measure average encode time.""" + # Warm-up + quantizer.encode(vectors) + + times = [] + + for _ in range(iterations): + start = time.perf_counter() + quantizer.encode(vectors) + times.append(time.perf_counter() - start) + + return np.mean(times) + + +def benchmark_decode(quantizer, codes, iterations=5): + """Measure average decode time.""" + # Warm-up + quantizer.decode(codes) + + times = [] + + for _ in range(iterations): + start = time.perf_counter() + quantizer.decode(codes) + times.append(time.perf_counter() - start) + + return np.mean(times) + + +def benchmark(): + dimensions = 32 + dataset_sizes = [1_000, 10_000] + + print("=" * 80) + print("Dynavec Quantization Benchmark") + print("ScalarQuantizer INT8 vs ProductQuantizer") + print("=" * 80) + + print() + + for n_vectors in dataset_sizes: + print(f"Dataset: {n_vectors:,} vectors × {dimensions} dimensions") + print("-" * 80) + + vectors = generate_dataset( + n_vectors=n_vectors, + dim=dimensions, + ) + + raw_bytes_per_vector = dimensions * 4 + + # --------------------------------------------------------- + # Scalar Quantizer + # --------------------------------------------------------- + scalar = ScalarQuantizer() + + start = time.perf_counter() + scalar.fit(vectors) + scalar_fit_time = time.perf_counter() - start + + scalar_codes = scalar.encode(vectors) + + scalar_encode_time = benchmark_encode( + scalar, + vectors, + ) + + scalar_decode_time = benchmark_decode( + scalar, + scalar_codes, + ) + + scalar_reconstruction_error = scalar.reconstruction_error( + vectors + ) + + scalar_bytes_per_vector = scalar.code_size_bytes + scalar_total_bytes = scalar_codes.nbytes + scalar_compression = ( + raw_bytes_per_vector / scalar_bytes_per_vector + ) + + # --------------------------------------------------------- + # Product Quantizer + # --------------------------------------------------------- + pq = ProductQuantizer( + m=8, + nbits=8, + iters=25, + seed=0, + ) + + start = time.perf_counter() + pq.fit(vectors) + pq_fit_time = time.perf_counter() - start + + pq_codes = pq.encode(vectors) + + pq_encode_time = benchmark_encode( + pq, + vectors, + ) + + pq_decode_time = benchmark_decode( + pq, + pq_codes, + ) + + pq_reconstruction_error = pq.reconstruction_error( + vectors + ) + + pq_bytes_per_vector = pq.code_size_bytes + pq_total_bytes = pq_codes.nbytes + pq_compression = ( + raw_bytes_per_vector / pq_bytes_per_vector + ) + + # --------------------------------------------------------- + # Results + # --------------------------------------------------------- + print() + print("ScalarQuantizer (INT8)") + print(f" Fit time: {scalar_fit_time:.4f} sec") + print(f" Encode time: {scalar_encode_time:.4f} sec") + print(f" Decode time: {scalar_decode_time:.4f} sec") + print( + f" Reconstruction error: " + f"{scalar_reconstruction_error:.8f}" + ) + print(f" Bytes/vector: {scalar_bytes_per_vector}") + print(f" Total encoded size: {scalar_total_bytes / 1024:.2f} KB") + print(f" Compression: {scalar_compression:.2f}x") + + print() + print("ProductQuantizer (PQ)") + print(f" Fit time: {pq_fit_time:.4f} sec") + print(f" Encode time: {pq_encode_time:.4f} sec") + print(f" Decode time: {pq_decode_time:.4f} sec") + print( + f" Reconstruction error: " + f"{pq_reconstruction_error:.8f}" + ) + print(f" Bytes/vector: {pq_bytes_per_vector}") + print(f" Total encoded size: {pq_total_bytes / 1024:.2f} KB") + print(f" Compression: {pq_compression:.2f}x") + + print() + print("Comparison") + print( + f" Scalar/PQ compression: " + f"{scalar_compression / pq_compression:.2f}x" + ) + + if scalar_encode_time > 0: + print( + f" PQ encode / Scalar encode: " + f"{pq_encode_time / scalar_encode_time:.2f}x" + ) + + if scalar_decode_time > 0: + print( + f" PQ decode / Scalar decode: " + f"{pq_decode_time / scalar_decode_time:.2f}x" + ) + + print() + print("=" * 80) + print() + + +if __name__ == "__main__": + benchmark() diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css index 3f1a43f..9c599b8 100644 --- a/dashboard/app/globals.css +++ b/dashboard/app/globals.css @@ -3,14 +3,77 @@ @tailwind components; @tailwind utilities; -:root { color-scheme: light; } +/* --- Light: the landing-site palette (styles.css) --- */ +:root { + color-scheme: light; + + --bg: #fbfaf8; /* warm paper */ + --surface: #ffffff; + --ink: #14110f; /* warm near-black */ + --muted: #6f6862; + --faint: #a99f97; + --line: #ece6df; + --accent: #e8623b; /* coral */ + --accent-ink: #b8472a; /* darker coral for text/hover on light */ + --accent-soft: #fdeee8; /* light coral wash */ + --ok: #2f7d5b; + --err: #b8472a; + + --track: #f0ece6; /* meter/track fill */ + --thead: #faf6f1; /* table header wash */ + --shadow: rgba(20, 17, 15, .07); + --scroll-thumb: #e0d9d1; + --tip-bg: #ffffff; /* recharts tooltip */ + --tip-line: #ece6df; + + /* op badges */ + --op-search-bg: #eef3ff; --op-search-fg: #3b5bdb; --op-search-line: #dbe3ff; + --op-graph-bg: #f3eeff; --op-graph-fg: #7048e8; --op-graph-line: #e5dbff; + --op-upsert-bg: #eafaf1; --op-upsert-fg: #2f7d5b; --op-upsert-line: #d3f0e0; +} + +/* --- Dark: the site's warm inverted tones (--inv-bg #14110f / --inv-fg #fbfaf8) --- */ +.dark { + color-scheme: dark; + + --bg: #14110f; /* warm near-black paper */ + --surface: #1d1916; /* lifted warm panel */ + --ink: #f4efe8; /* warm off-white */ + --muted: #a79d94; + --faint: #766c64; + --line: #2d2723; + --accent: #f0714a; /* brighter coral for dark backgrounds */ + --accent-ink: #f59a7c; /* lighter coral for text/hover on dark */ + --accent-soft: #2a1712; /* dark coral wash */ + --ok: #57b98d; + --err: #ef8a63; + + --track: #2d2723; + --thead: #1a1613; + --shadow: rgba(0, 0, 0, .4); + --scroll-thumb: #3a332e; + --tip-bg: #1d1916; + --tip-line: #2d2723; + + --op-search-bg: #1a2233; --op-search-fg: #9db6ff; --op-search-line: #2b3a5c; + --op-graph-bg: #221a33; --op-graph-fg: #b9a4f5; --op-graph-line: #3a2e5c; + --op-upsert-bg: #142720; --op-upsert-fg: #57b98d; --op-upsert-line: #24463a; +} + html, body { padding: 0; margin: 0; } body { - background: theme('colors.bg'); - color: theme('colors.ink'); + background: var(--bg); + color: var(--ink); font-family: theme('fontFamily.sans'); - border-top: 3px solid theme('colors.accent'); + border-top: 3px solid var(--accent); -webkit-font-smoothing: antialiased; + transition: background-color .18s ease, color .18s ease; } ::-webkit-scrollbar { width: 10px; height: 10px; } -::-webkit-scrollbar-thumb { background: #e0d9d1; border-radius: 6px; } +::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 6px; } + +/* Trace op badges — themed via CSS variables so they flip with .dark */ +.op-badge { background: var(--op-search-bg); color: var(--op-search-fg); border-color: var(--op-search-line); } +.op-search { background: var(--op-search-bg); color: var(--op-search-fg); border-color: var(--op-search-line); } +.op-graph_search { background: var(--op-graph-bg); color: var(--op-graph-fg); border-color: var(--op-graph-line); } +.op-upsert { background: var(--op-upsert-bg); color: var(--op-upsert-fg); border-color: var(--op-upsert-line); } diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx index 33be36c..735422c 100644 --- a/dashboard/app/layout.tsx +++ b/dashboard/app/layout.tsx @@ -6,9 +6,17 @@ export const metadata: Metadata = { description: "Real-time retrieval observability for dynavec — latency, cache, traces.", }; +// Set the theme class before first paint so there is no light→dark flash. +// Precedence: ?theme= override (deep-linkable, used for parity screenshots) > +// saved choice > OS preference; defaults to light (matching the site). +const themeInit = `(function(){try{var q=new URLSearchParams(location.search).get('theme');var s=localStorage.getItem('dynavec-theme');var d=q?q==='dark':(s?s==='dark':matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);if(q){try{localStorage.setItem('dynavec-theme',d?'dark':'light');}catch(e){}}}catch(e){}})();`; + export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + + +