Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ htmlcov/
# Local config / secrets
.env
*.local

# Benchmark artifacts
benchmark_results.txt
benchmarks/benchmark_results.txt
benchmarks/*.swp
197 changes: 197 additions & 0 deletions benchmarks/benchmark_quantization.py
Original file line number Diff line number Diff line change
@@ -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()
73 changes: 68 additions & 5 deletions dashboard/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
10 changes: 9 additions & 1 deletion dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInit }} />
</head>
<body>{children}</body>
</html>
);
Expand Down
8 changes: 4 additions & 4 deletions dashboard/components/LatencyChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import type { Metrics } from "@/lib/types";

const ROWS: [keyof Metrics, string, string][] = [
["p50", "p50", "#2f7d5b"],
["p95", "p95", "#e8623b"],
["p99", "p99", "#b8472a"],
["p50", "p50", "var(--ok)"],
["p95", "p95", "var(--accent)"],
["p99", "p99", "var(--accent-ink)"],
];

export default function LatencyChart({ m }: { m: Metrics }) {
Expand All @@ -23,7 +23,7 @@ export default function LatencyChart({ m }: { m: Metrics }) {
<span className="font-mono" style={{ color }}>{label}</span>
<span className="font-mono">{Number(v).toFixed(1)} ms</span>
</div>
<div className="h-2.5 bg-[#f0ece6] rounded-full">
<div className="h-2.5 bg-track rounded-full">
<div className="h-full rounded-full" style={{ width: `${(v / max) * 100}%`, background: color }} />
</div>
</div>
Expand Down
46 changes: 46 additions & 0 deletions dashboard/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";
import { useEffect, useState } from "react";

// Toggles the `.dark` class on <html> and persists the choice. The initial
// class is set pre-paint by the inline script in app/layout.tsx (no flash);
// this component only syncs its icon to that state and flips it on click.
export default function ThemeToggle() {
const [dark, setDark] = useState(false);

useEffect(() => {
setDark(document.documentElement.classList.contains("dark"));
}, []);

const toggle = () => {
const next = !document.documentElement.classList.contains("dark");
document.documentElement.classList.toggle("dark", next);
try {
localStorage.setItem("dynavec-theme", next ? "dark" : "light");
} catch {
/* storage may be unavailable */
}
setDark(next);
};

return (
<button
onClick={toggle}
aria-label={dark ? "Switch to light theme" : "Switch to dark theme"}
title={dark ? "Light" : "Dark"}
className="grid place-items-center w-8 h-8 border-[1.5px] border-ink rounded-lg bg-surface text-ink hover:text-accent"
>
{dark ? (
// sun
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
<circle cx="12" cy="12" r="4.2" />
<path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M19.1 4.9l-1.8 1.8M6.7 17.3l-1.8 1.8" />
</svg>
) : (
// moon
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 14.5A8 8 0 1 1 9.5 4a6.2 6.2 0 0 0 10.5 10.5z" />
</svg>
)}
</button>
);
}
4 changes: 3 additions & 1 deletion dashboard/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import ThemeToggle from "@/components/ThemeToggle";

const RANGES = [
{ label: "30m", w: 1800 },
Expand Down Expand Up @@ -53,11 +54,12 @@ export default function TopBar({
onClick={onAuto}
className={
"font-mono text-[12px] border-[1.5px] border-ink rounded-lg px-3 py-1.5 " +
(auto ? "bg-ink text-white" : "bg-surface text-ink")
(auto ? "bg-ink text-bg" : "bg-surface text-ink")
}
>
Auto-refresh
</button>
<ThemeToggle />
</header>
);
}
Loading