Skip to main content

winnex-madhava

Deterministic vector search with mathematical guarantees.

Every document excluded from the results carries a proof that it could not be in the top-K — by the Cauchy-Schwarz inequality. Zero bound violations by construction.

PyPI version PyPI - Downloads PyPI - Python Versions CI License: BSL 1.1 C++ Benchmark


winnex-madhava is a real, pip-installable Python package with a native C++20 core. It answers a question no approximate index (HNSW, IVF, PQ) can answer:

"Prove that your search did not miss a relevant document."

The proof is per-document and mathematical: a Cauchy-Schwarz upper bound on the inner product, which converts into a lower bound on L2². If the bound says a vector cannot be in the top-K, that vector is not in the top-K. No heuristics, no random graphs, no "we think it's fine."

Verified against the official BIGANN-100M L2 ground truth — see Benchmarks.

Table of contents


Installation

pip install winnex-madhava

Requirements: Python ≥ 3.8 and NumPy. The C++ core ships pre-built in the wheel (manylinux x86-64); a C++20 compiler + CMake ≥ 3.20 are needed only when building from source.

Python version support. Pre-built manylinux wheels ship for CPython 3.10, 3.11 and 3.12 (cibuildwheel). Python ≥ 3.10 required.

Installing straight from this repo works too:

pip install git+https://github.com/winnex-ai/winnex-madhava.git

How to know your install is working. After installing, run:

python -c "import winnex_madhava; print(winnex_madhava.__version__)"

You should see 1.3.0 or newer. If you see No module named, you are on the unsupported source-build path (see the warning above).

Quick start

import numpy as np
import winnex_madhava

# 1. Build an engine over your corpus (uint8, shape (n, dim)).
corpus = np.random.randint(0, 256, size=(100_000, 128), dtype=np.uint8)

engine = winnex_madhava.build_engine(corpus, dim=128, k=10)
print(f"indexed {engine.num_vectors()} vectors in {engine.build_seconds():.2f}s")

# 2. Search.
query = corpus[0].astype(np.float32)   # (128,) float32
result = engine.search(query)

print(result.indices)                  # top-K dataset ids
print(result.latency_ms)               # milliseconds
print(result.bound_violations)         # always 0 — the guarantee

That's it. Same query + same data → same result, every time. Deterministic.

When should you use this?

winnex-madhava is for the cases where "fast but unprovable" is a liability. The trade-off is simple: you pay more latency per query than an approximate index, but you get a mathematical proof per document and a much faster build.

Use case Why winnex-madhava
Regulated retrieval (legal discovery, medical records, financial compliance, government audits) Every excluded document carries a proof it could not be in the top-K. Defensible in court.
Continuous ingestion / dynamic RAG (corpus changes frequently) Build is ~10–1000× faster than HNSW — no painful rebuilds. Rebuild the whole index on every ingestion.
Batch processing Scan everything with bounds; throughput over latency.
RAM/CPU-constrained environments Int8-quantized projections use ~4× less memory than float32 (18.6 GB for 100M×128D).
RAG that must not silently drop a relevant document Deterministic recall ceiling reachable; 0 bound violations.
Auditability / compliance (EU AI Act, LGPD, HIPAA) Deterministic (same input → same output), per-document audit trail.

When should you NOT use this?

Be honest — winnex-madhava is not the right tool for:

  • Lowest-latency serving (sub-ms QPS). HNSW/IVF are faster per query — but they are approximate (no guarantee). The exact scan is speed=True on GPU (~2.4 ms at 1M, single-query) or the bound engine on CPU (~11.7 ms). If you need millions of queries/sec and can tolerate approximation, use an approximate index.
  • default mode with arbitrary float32 corpora. The default engine input contract is uint8 (0–255). If you pass raw float embeddings to default mode, they get truncated to uint8 and recall collapses. For float32 embeddings, use speed=True (the exact GPU scan), which accepts float32 directly.
  • Tiny / low-dimensional corpora (d < ~8). The projection overhead dominates; a plain search_exact scan is faster and simpler.
  • GPU inference for other models. The speed-mode GPU path (OpenCL) is dedicated to vector search; it is not a general inference backend.

Parameter guide

build_engine is parametrizable to reflect the full Winnex stack. All parameters have sensible defaults — start with the defaults and tune only what you need.

engine = winnex_madhava.build_engine(
    corpus,                          # (n, dim) uint8 (default) OR float32 (speed)
    dim=128,                         # vector dimensionality (default: corpus.shape[1])
    metric="cosine",                 # "cosine" (normalized embeddings) or "l2" (raw uint8)
    quant="int8",                    # "int8" (fast, memory-light) or "none" (float32 exact)
    stage1_dim=64,                   # Stage-1 QR projection (wide bound B1)
    stage2_dim=128,                  # Stage-2 QR projection (tight bound B2); 0 disables cascade
    k=10,                            # number of results
    k1_fraction=0.05,                # Stage-1 keep fraction (5% of N)
    k2_fraction=0.01,                # Stage-2 keep fraction (1% of N)
    modulation=True,                 # error-backprop ranking (prune by B2, rank by B1+α(B2−B1))
    postfilter=True,                 # exact metric re-score on survivors
    normalize_input=True,            # L2-normalize vectors (used when metric="cosine")
    seed=42,                         # PRNG seed for the MGS projections (deterministic)
)

Choosing metric

metric Input contract Use when
"cosine" (default) uint8 representing normalized embeddings (unit L2 norm) Your vectors are embeddings (SBERT, etc.). This matches the Winnex stack.
"l2" raw uint8 values (BIGANN-style, non-normalized) Your data is raw uint8 and you want exact L2 semantics.

Choosing quant

quant Memory Fidelity
"int8" (default) ~4× less memory (projections stored as int8) Bound stays exact (quantization margin added); recall preserved.
"none" float32 projections Exact float32 — maximum fidelity, more memory.

Choosing stage1_dim / stage2_dim

The two-stage cascade is the Winnex architecture: a wide bound B1 (Stage-1, cheap) prunes to k1, then a tight bound B2 (Stage-2, more expensive) prunes to k2. Set stage2_dim=0 for a single-stage engine (BIGANN-L2 baseline). Pruning always uses the tightest available bound — modulation is used only for ranking, never for pruning (the stack's FIX(1) invariant).

Choosing modulation

When True, survivors are ranked by B1 + α·(B2−B1) with α = sigmoid((e1−e2)/mean(e1)) — the error-backpropagation refinement. This improves ranking quality without ever sacrificing the 0-violation guarantee. Set False to rank purely by the bound.

Choosing postfilter

When True, the exact metric is re-computed on the surviving top-k2, so the final result is the true top-K of the surviving set. This closes the gap between bound ranking and exact ranking. Leave it on unless you need speed.

Choosing speed / speed_n_anchors / speed_nprobe

Parameter Default Effect
speed False True → native speed mode (C++ QKᵀ matmul + fused topk; OpenCL GPU default, CUDA opt-in, OpenMP/AVX2 on CPU)
speed_n_anchors 0 K PiPrime anchors for O(K) navigation. >=2 → sublinear (route query to the nprobe most-similar anchor cells); 0 → brute-force exact scan
speed_nprobe 4 Anchor cells probed per query. Higher = better recall, more latency
speed_opencl_lib "" Explicit OpenCL loader/driver .so for the GPU backend. Empty = WINNEX_OPENCL_LIB env var, else the platform ICD loader. No hardcoded vendor fallback — you choose the loader.

Choosing the OpenCL loader (transparent, no hardcoding). The GPU backend loads the OpenCL library at runtime via dlopen — it never hardcodes a vendor .so. The loader is resolved per-engine in this order:

  1. speed_opencl_lib="..." — pin an exact loader/driver (e.g. "libOpenCL.so.1" for the generic ICD loader, "libnvidia-opencl.so.1" for the NVIDIA driver, "libmali.so.1" for ARM, or a full path).
  2. $WINNEX_OPENCL_LIB — same override, via environment.
  3. The standard platform ICD loader (libOpenCL.so.1).

Every attempt is logged ([Winnex Madhava] OpenCL loader resolved: ... / failed to load: ...), and gpu_reason() reports the exact loader tried — a CPU fallback is always explainable. Use require_gpu=True to raise instead of falling back:

# Pin the ICD loader explicitly (e.g. when it is not on the default dlopen path)
eng = winnex_madhava.build_engine(
    corpus, k=10, speed=True, metric="cosine",
    speed_opencl_lib="/usr/lib/x86_64-linux-gnu/libOpenCL.so.1",
    require_gpu=True,     # raise if this loader has no GPU device
)
print(eng.backend_name(), eng.gpu_reason())   # "gpu" or the reason

Why configurable? A machine can have the NVIDIA driver + ICD vendor file (/etc/OpenCL/vendors/nvidia.icd) but lack the generic loader (libOpenCL.so.1) on the dlopen path. Without this option the GPU would be silently invisible. With speed_opencl_lib you point at the installed loader and the SpeedEngine enables the GPU (OpenCL QKᵀ + device topk).

import winnex_madhava, numpy as np

# Speed mode — brute-force exact scan (default)
eng_bf = winnex_madhava.build_engine(
    corpus_u8, k=10, speed=True, metric="l2",
)

# Speed mode — O(K) anchor navigation (sublinear, intelligent)
eng_an = winnex_madhava.build_engine(
    corpus_u8, k=10, speed=True, metric="l2",
    speed_n_anchors=16,   # K PiPrime anchors (SVD + Gram-Schmidt)
    speed_nprobe=8,       # cells probed (trade recall vs cost)
)

How it works. K orthonormal anchors (SVD power-iteration + Gram-Schmidt, inspired by the PiPrime navigation) partition the corpus into Voronoi cells. A query is routed to the nprobe most-similar cells via q @ anchors.T (O(K·d), tiny), then the QKᵀ scan runs only over the members of those cells — sublinear, not a full N·d scan.

Honest usage.

  • speed_n_anchors=0 (default) is the brute-force exact scan — correct everywhere, O(N·d). Use it when you need guaranteed exact top-K.
  • speed_n_anchors>=2 is sublinear — it evaluates only the relevant cells, at a recall cost that depends on nprobe. On structured data, nprobe=8 reaches 100% of the exact-scan ceiling; nprobe=4 may drop recall. Tune on your data.
  • The CPU speed mode is an exact scan (O(N·d)) — HNSW is faster on raw CPU latency. The value is exactness + build speed + determinism.

Speed GPU — how the fused kernel works (v1.7.2)

The GPU path (OpenCL) runs the QKᵀ matmul as a single fused kernel (qkt_fused_topk) that also computes the per-row top-k — no intermediate scores[N] matrix is materialized. Two properties matter for latency:

  • Parallelism (M work-groups per query). The kernel splits the corpus scan into M contiguous chunks, each handled by a separate work-group. This keeps all GPU compute units active even for a single query — the reason single-query latency dropped from 47.8ms → 2.41ms (20×) at 1M. M is auto-tuned from the GPU size (64 by default), so no parameter to set.
  • Coalesced memory access. Adjacent work-items read adjacent vectors, so each 32-byte cache line fetched from global memory is used by an entire warp. This is what makes the scan memory-bound at ~448 GB/s instead of ~2%.
Query mode GPU (OpenCL) 1M CPU 1M Notes
single-query 2.41 ms 9.56 ms GPU 4× faster
batch (100 q) 1.55 ms/q ~9 ms/q GPU 6× faster

Latency guidance. Use speed=True with metric="l2" (or "cosine") for an exact scan on GPU — the fastest correct path per query. For throughput (batch), search_batch amortizes the kernel launch; at 1M it sustains ~600-640 QPS.

Streaming — 100M vectors without loading the corpus into RAM

winnex-madhava searches 100M vectors (12.8 GB) without ever loading the raw corpus into RAM. The corpus is memory-mapped (np.memmap), the C++ core builds the int8-quantized projections in streaming blocks, and only those compressed projections (~19 GB at 100M) live in RAM.

How it works

base.u8bin (12.8 GB, 100M×128D)
    │
    ├── mmap — NEVER loaded into RAM
    │
    ├── Build in blocks of 500K:
    │     mmap → uint8→float32 → project (stage1+stage2) → int8 quantize
    │     → keep pr1_i8 (6.4 GB) + pr2_i8 (12.8 GB) + e1/e2 (1.6 GB) in RAM
    │
    └── Search (O(N) over int8 in RAM):
          Stage 1: bound over pr1_i8 → k1
          Stage 2: tighter bound over pr2_i8 → k2 = min(k2_fraction·N, k2_max)
          Stage 3: exact metric over k2 survivors (mmap only those) → top-K

The key knob is k2_max (default 2000): it caps the Stage-2 survivors, so the exact Stage-3 scoring is bounded at large scale. This is the bigann_stream V3 optimization — the bound in Stage 2 already isolates the best candidates in the first 2000, so the cap costs no recall.

import numpy as np
import winnex_madhava

# Stream a 100M corpus without loading it into RAM.
base = np.memmap("base.u8bin", dtype=np.uint8, mode="r", shape=(100_000_000, 128))

engine = winnex_madhava.build_engine(
    base,
    dim=128,
    metric="cosine",      # V3-style (or "l2")
    k1_fraction=0.05,
    k2_fraction=0.01,
    k2_max=2000,          # the 100M streaming knob
    postfilter=True,
)
res = engine.search(query_f32)
print(res.indices, res.bound_violations)  # 0 violations — the guarantee

Verified at 100M (Kaggle, notebook winnex-madhava-stream-100m)

Scale Build (s) Lat (ms) R@10 NDCG RSS (GB) Vio
100K 1.3 7.5 0.750 0.658 0.4 0
1M 5.9 66 0.843 0.667 0.8 0
10M 34.0 698 0.501 0.544 3.8 0
100M 342.6 7592 0.780 0.813 31.4 0

100M indexed in 342.6 s (~5.7 min, 4 CPUs) via mmap — the raw 12.8 GB corpus is never loaded into RAM. 0 bound violations at every scale.

Note. k2_max caps the Stage-2 survivors. At 100M this limits the exact post-filter to 2000 vectors instead of 1M, making the search tractable. Verified: R@10 is identical with k2_max=2000 vs no cap.

API

winnex_madhava.build_engine(corpus, **kwargs) -> MadhavaL2 | MadhavaSpeed

Build an engine over a (n, dim) array. With the default (uint8) corpus the native C++ MadhavaL2 is returned. With speed=True a MadhavaSpeed is returned — the native QKᵀ matmul engine with fused topk (OpenCL GPU default, CUDA opt-in at build, OpenMP/AVX2 on CPU), optionally with O(K) anchor navigation via speed_n_anchors/speed_nprobe. See Parameter guide.

engine.search(query: np.ndarray) -> SearchResult

Returns indices, latency_ms, k1, k2, k3, bound_pairs, bound_violations, modulation_gain, and the honest pruning breakdown pruned_by_bound / pruned_by_prefilter / exact_evals.

engine.search_exact(query: np.ndarray) -> SearchResult

Exhaustive scan over all N vectors — the recall ceiling of your corpus. Use it to measure how close an approximate index gets to the physical limit.

engine.search_audited(query, k=10, max_audit_records=500) -> dict

The same top-K plus a per-document mathematical certificate — the winnex-audit-cpp / GovAuditRecord format consumed by the tracer-gov and tracer-med compliance flows. Returns:

{
  "indices": [...], "latency_ms": ..., "bound_violations": 0,
  "audit_candidates": int, "audit_excluded": int,
  "audit": [  # per-document proofs
    {
      "doc_id": int, "true_cosine": float, "projected_cosine": float,
      "residual_norm": float, "upper_bound": float, "threshold": float,
      "excluded": bool, "stage": "stage1"|"stage2"|"survived"|"in_topk",
    }, ...
  ],
}

Each excluded=true record is a document the Cauchy-Schwarz bound proves cannot be in the exact top-K (UB < threshold for cosine; L2²-lower-bound > threshold for L2). The math is the motor's own (ub_raw, residuals1, exact_score) — no reimplementation. The certificate examines the max_audit_records documents nearest the top-K boundary plus the top-K themselves, so per-query cost stays bounded (tracer-gov default = 500).

engine.audit_json(query, k=10, max_audit_records=500) -> str

The audited result as a JSON string (the audit_json of winnex-audit-cpp) — ready to attach to a certificate / QR / WORM evidence record.

engine.search_with_commitment(query, k=10, max_sample=50) -> dict

The production audit-trail path (1.9.2+). Returns a compact AuditCommitment instead of the full O(N) certificate — the ~400–500 byte record you store in a WORM:

{
  "indices": [...], "bound_pairs": ..., "bound_violations": 0,
  "latency_ms": ...,
  "total_excluded_count": 15508,     # docs the bound PROVED outside top-K
  "global_threshold": 0.5996,        # exact score of the K-th result
  "sampled_records": [               # deterministic boundary sample (<= max_sample)
    {"doc_id": 1042, "upper_bound": 0.5995, "excluded": true}, ...
  ],
}

total_excluded_count is the raw mathematical fact; sampled_records is a deterministic boundary-biased sample (up to max_sample) of excluded docs for spot-check audit. Memory is O(max_sample), NOT O(N) — the motor never materializes the excluded list, and max_sample is honored exactly (verified: max_sample=2 → 2 records for 15,540 exclusions). This replaces the measured ~2 MB/query search_audited payload that broke WORM-backed compliance flows at scale.

Hybrid security model. The commitment carries no internal hash and no key. The compliance layer (tracer-gov / tracer-med core.commitment) hashes the raw fields and signs them with an Ed25519 private key held outside the C++ binary, then stores the ~500-byte signed record in the WORM — defeating the "lying engine" attack (a compromised binary cannot forge past records; it never holds the signing key).

Conceptual Foundation — winnex-audit-cpp

The per-document certificate format (AuditRecord / GovAuditRecord) — the doc_id, true_cosine, projected_cosine, residual_norm, upper_bound, threshold, excluded, stage contract this motor emits byte-for-byte — originates from the winnex-audit-cpp repository. That repository is the source of the specification (and the pre-patent mathematical foundation), not a runtime dependency: the certificate is produced natively inside this motor's C++ search loop (the Witness Architecture), so no separate audit layer is required. Audit attempts performed outside the motor diverge at high dimension (measured: 462/973 false "excluded" on arXiv d=1536 in the intermediate 1.9.0) — the Witness hook captures the proof at the exact moment the decision is made, using the exact global threshold of that instant.

winnex_madhava.benchmark_vs_groundtruth(engine, queries, gt_ids, *, query_alignment=1, k=None) -> dict

Evaluate against ground-truth id lists. Returns recall_at_k, ndcg_at_k, latency_ms, and per-query detail.

Metrics

  • winnex_madhava.recall_at_k(result, gt_set, k) — robust recall@K: |result[:K] ∩ gt| / min(K, |gt|). Normalizes by min(K, |gt|) so a perfect scan scores exactly 1.0 even when the ground truth has fewer than K relevant ids in the subset.
  • winnex_madhava.ndcg_at_k(result, gt_set, k) — NDCG@K with the same min(K, |gt|) normalization.
  • winnex_madhava.read_bigann_groundtruth(path, n_queries)

The mathematics

For any query q and candidate vector v, the Cauchy-Schwarz inequality bounds the raw inner product:

⟨v, q⟩  ≤  ⟨Pv, Pq⟩  +  ‖v − PᵀPv‖ · ‖q − PᵀPq‖

where P is a QR-orthogonalized (Modified Gram-Schmidt) random projection. Because

‖v − q‖²  =  ‖v‖² + ‖q‖² − 2·⟨v, q⟩

the bound on ⟨v, q⟩ becomes a lower bound on L2²:

‖v − q‖²  ≥  ‖v‖² + ‖q‖² − 2·UB(⟨v, q⟩)

Stage 1 computes this lower bound for every vector and keeps the top-k1 by smallest L2². Any vector pruned here is mathematically proven not to be in the exact top-K. Bound violations = 0 by construction.

Stage 2 (optional) applies a tighter bound B2 on the k1 survivors. Post-filter computes the exact metric on the surviving top-k2, so the result is the true top-K of the surviving set. Because Stage 1/2 never prune a real neighbor, the post-filter recovers everything a perfect scan would find.

The residual ‖v − PᵀPv‖ is computed on the real float32 projection, not the int8-quantized one — this is what the inequality requires, and it is what makes the bound exact rather than approximate.

UB Width mode (basis="pca_corpus")

The bound's tightness is governed by the residual width e(v) = ‖v − PᵀPv‖. A random projection (the historical default) leaves e(v) ≈ √(1 − s/d), which at high dimension (d = 1536) is so wide that the bound cannot prune anything — the scan degenerates to exhaustive search.

UB Width aligns the projection to the principal directions of the corpus (basis="pca_corpus"), so the residual shrinks to the manifold residual √(1 − ν(s)), where ν(s) is the variance captured by the top-s principal axes. Because the basis remains orthonormal, the Cauchy-Schwarz bound stays valid in the original space: 0 bound violations by construction, at full recall. The score is always evaluated exactly in the original space — the projection only tightens the bound, it never replaces the metric.

import winnex_madhava, numpy as np

# UB Width mode — the PCA basis is computed inside the C++ engine
engine = winnex_madhava.build_engine(
    embeddings_f32,          # float32 embeddings (unit-norm)
    metric="cosine", basis="pca_corpus",
    stage1_dim=192, k=10,
)
res = engine.search(query_f32)
print(res.bound_violations)   # 0 — the proof

Agnostic guarantee (the product, not a benchmark). The engine is dataset-agnostic by construction: the Cauchy-Schwarz bound ⟨v,q⟩ ≤ ⟨Pv,Pq⟩ + e(v)e(q) holds for any corpus, in any dimension. Verified on unstructured (random) unit-norm vectors — the worst case, where there is no manifold to exploit:

d basis bound violations (out of 3000)
128 random 0
128 pca_corpus 0
1536 random 0
1536 pca_corpus 0

The 0-violation guarantee is a property of the algorithm, not of any dataset. For a corpus with a low-dimensional manifold, the PCA-aligned basis tightens e(v) and restores pruning at high dimension; for an isotropic corpus it falls back to the (still exact, still 0-violation) scan.

Honest pruning breakdown (what the motor really prunes). SearchResult reports pruned_by_bound (vectors the Cauchy-Schwarz bound PROVED outside top-K) separately from pruned_by_prefilter (vectors cut by the fixed k1_fraction Stage-1 keep, without a per-vector certificate). This exposes the truth: a wide bound prunes nothing by proof; the fixed cutoff is the Stage-1 mechanism that guarantees recall via the exact post-filter.

Public benchmark — 3 real Kaggle datasets, package installed from PyPI: Kaggle

Dataset dim mode recall@10 bound viol. pruned_by_bound prefilter e(v)
GloVe 100 random 1.000 0 79.3% 15.7%
BIGANN-100M 128 random 1.000 0 100.0% ~0 0.0005
arXiv OpenAI 1536 random 0.995 0 0.0% 95.0% 0.967
arXiv OpenAI 1536 pca_corpus 1.000 0 80.5% 14.5% 0.793

Reading (honest). At d = 1536 the random basis has e(v) ≈ 0.97 — the bound is ~1.9 wide and cannot prove any vector is outside top-K (pruned_by_bound = 0.0%); the "95%" is the fixed k1_fraction cutoff, not the bound. The PCA-aligned basis tightens e(v) to 0.79 and the bound proves 80.5% of the corpus is outside the top-10, at full recall (1.000). The kernel installs winnex-madhava from PyPI, reads the raw public datasets, and measures only what the motor returns (no numpy ground-truth, no re-ordering).

PCA build time (1.9.2 → 1.9.6 — honest benchmark on the Kaggle runtime)

The pca_corpus basis build at d=1536 was suspected to be the high-dim bottleneck (~20-25 s measured locally with a high OMP_NUM_THREADS). The public benchmarks winnex-madhava-1-9-5-honest and winnex-madhava-1-9-6-honest (install the package from PyPI, measure build time on the Kaggle runtime) showed the real picture:

Dataset dim basis build 1.9.2 build 1.9.5 build 1.9.6 recall@10 bound viol.
GloVe (20k) 100 pca_corpus 0.2 s 0.2 s 0.2 s 1.000 0
BIGANN (20k) 128 pca_corpus 0.5 s 11.4 s ⚠️ 0.3 s 1.000 0
arXiv OpenAI (20k) 1536 pca_corpus 4.0 s 4.1 s 4.4 s 1.000 0
arXiv OpenAI (20k) 1536 random 1.9 s 1.9 s 2.0 s 0.996 0

Honest reading. The ~20-25 s "G1 bottleneck" was an artifact of a high OMP_NUM_THREADS environment, not the engine — on the Kaggle runtime the 1.9.2 pca build was already 0.2-4.0 s. The 1.9.5 matrix-free experiment (C·v = Aᵀ(A·v)/sample) regressed low/mid dim: BIGANN d=128 went 0.5 s → 11.4 s, because O(2·sample·D·s·iters) ≫ O(D²·sample) when sample=10k > D. 1.9.6 reverts to the direct covariance (BIGANN back to 0.3 s) and keeps the safe wins — the contiguous subsample read and the power-iteration cap exposed as the caller-owned pca_iterations knob (1.9.7). The pca_sample reduction (10k → 3k) shows a bounded trade-off: build 4.4 s → 4.2 s, bound coverage 61.2% → 57.3%, recall unchanged at 1.0.

Validity unchanged. The basis is still an orthonormal set in the ORIGINAL D-dimensional space, so UB(v,q)=⟨Pv,Pq⟩+e(v)e(q) remains sound. Verified: recall@10 = 1.000 and 0 bound violations across d = 64/128/384/1536 × basis random/pca_corpus; deterministic basis across runs; dominant subspace aligned (cos = 1.0) to the true eigendecomposition. The AuditCommitment validates 100/100 (sample-bounded, count-match, deterministic, genuine) on all datasets.

Benchmarks

The benchmark reference — a corrected, honest note

⚠️ GT-validity correction (2026-08-08). Earlier benchmarks reported R@10 numbers (e.g. 0.52 at 10M, 0.836 at 100M) measured against the official BIGANN GT file shipped in the shurangwu/bigann-100m Kaggle dataset. A rigorous audit proved that this GT is not usable with that base: the dataset's base.u8bin has a vector order that differs from the canonical BIGANN base, so the GT ids point to the wrong vectors. Verified: the GT top-1 id is never the true neighbor (0/500 hits; L2² of GT ids ≈ random). Recalls measured against that GT were not meaningful. They are retained only as historical records and should not be cited.

The valid reference is the exact-scan local ceiling on the same subset — recall of each method vs the true nearest neighbors (see the Real benchmark above). Against that reference, winnex-madhava recovers 99.6% of the exact top-10 with 0 bound violations (bound engine) and 100% (exact GPU scan), while HNSW/IVF/IVF-PQ lose recall (47.8-97.6%).

Build vs Latency — the honest trade-off

The build advantage is independent of the GT and is a real, measured property of the engine:

Task winnex-madhava HNSW
Index 1M (build) 2.0 s 159 s (78× slower)
Index 10M (build) ~2.4 s ~30 min+
Index 100M (build, streaming) ~342 s ~6+ hours

winnex-madhava scans all vectors with a mathematical bound (higher latency per query), but the build is ultra-fast — no graph to construct. This makes it ideal for continuous ingestion / dynamic RAG, where HNSW's expensive rebuilds are a liability.

Kaggle benchmark (reproducible)

Run it yourself with one click — the notebook installs winnex-madhava from PyPI, indexes real data, and reports the exact-scan ceiling vs the Madhava result, plus a side-by-side comparison with FAISS HNSW/IVF/IVF-PQ using the same robust recall function:

Kaggle

Kaggle

GloVe honest benchmark. Installs winnex-madhava from PyPI, indexes real GloVe embeddings (dataset anmolkumar/glove-embeddings), and reports Recall@10 vs the exact-scan on the same data — the honest ceiling. 0 bound violations by construction. Result: Recall@10 = 0.448, NDCG@10 = 0.577, 0 bound violations (100K vectors).

Real benchmark — pip-installed wheel, validated reference, GPU (v1.7.2)

Installs winnex-madhava from PyPI via pip and benchmarks it against a mathematically valid reference: the exact-scan ceiling on the same subset (recall of each method vs the true nearest neighbors). The comparison includes FAISS HNSW / IVF / IVF-PQ baselines on the same data.

Kaggle

⚠️ GT validity discovery (documented in the notebook). The shurangwu/bigann-100m dataset provides base.u8bin whose vector order differs from the canonical BIGANN base. The unif_groundtruth_10k.bin ids refer to the canonical order and therefore point to the wrong vectors in this base. Verified: the official GT top-1 id is never the true neighbor (0/500 hits in the exact top-10; L2² of GT ids ≈ random). Recalls measured against this GT with this base are not meaningful. The notebook detects this at runtime (gt_validated=false, gt_recall_scan_exact=0.0) and uses the exact-scan local ceiling instead — a reference that is valid independent of the GT.

Results (BIGANN-100M, subset 1M, 100 queries, Kaggle GPU P100, recall vs exact-scan ceiling on the same subset):

Method R@10 Lat (ms) QPS Build (s) Efficiency Bound vio.
Exact-scan ceiling (local) 1.0000 3.0
Madhava bound (int8 5%/1%) 0.9960 45.8 22 2.0 100% 0
Madhava speed GPU (OpenCL) 1.0000 6.14 163 1.5 100%
Madhava speed GPU batch 1.0000 3.09 323 100%
HNSW(ef=128) 0.9760 0.56 1800 159 98%
HNSW(ef=64) 0.9330 0.34 2928 159 94%
IVF(nlist=4000,np=50) 0.9250 0.75 1335 61 93%
IVF(nlist=4000,np=10) 0.6840 0.29 3472 61 69%
IVF-PQ(nlist=4000,np=10) 0.4780 0.23 4282 10 48%

Read the honest insight:

  • The exact-scan ceiling is the true physical limit — a perfect exhaustive scan scores 1.0 against itself. Any method's recall is measured against this valid reference, not the invalid GT.
  • The Madhava bound engine recovers 99.6% of the exact top-10 with 0 bound violations and a ~77× faster build than HNSW (2.0s vs 159s).
  • The speed GPU is exact (R@10 = 1.0) — it returns the true top-10, at 6.1 ms single-query and 3.1 ms/query batch.
  • Approximate baselines lose recall: HNSW(ef=128) 97.6%, IVF(np=50) 92.5%, IVF-PQ 47.8% — they are faster (sub-ms) but not provably complete.
  • bound_violations == 0 is the per-document Cauchy-Schwarz guarantee.

Speed benchmark (BIGANN-100M, v1.6.0) — historical

Historical note. This v1.6.0 benchmark reports efficiency vs the subset's exact-scan ceiling (not absolute recall vs the official GT, which the corrected audit shows is unusable with the dataset's reordered base — see The benchmark reference). The relative efficiency claims (exact scan = 100%) remain valid; absolute recall values should not be cited. See the Real benchmark for current, valid numbers.

The winnex-madhava speed mode — native C++ (OpenCL GPU default, OpenMP/AVX2 on CPU), with O(K) PiPrime anchor navigation (sublinear, not brute force) — compared against HNSW / IVF / IVF-PQ on the BIGANN-100M dataset (subset 1M, 30 queries, official L2 ground truth):

Kaggle

Method R@10 NDCG Lat (ms) QPS Effic.
HNSW(ef=64) 0.0067 0.0120 0.46 2185 67%
HNSW(ef=128) 0.0100 0.0151 0.69 1454 100%
IVF-PQ(m=16) 0.0067 0.0147 1.20 835 67%
IVF(nprobe=10) 0.0067 0.0120 2.16 463 67%
IVF(nprobe=50) 0.0100 0.0151 9.44 106 100%
Madhava speed (brute) 0.0100 0.0151 24.1 41 100%
Madhava speed O(K) a=16 np=8 0.0100 0.0151 35.2 28 100%
Madhava speed O(K) a=32 np=4 0.0067 0.0120 29.2 34 67%

Read the honest insight:

  • The official BIGANN GT is for the full 100M. On a 1M subset only ~1% of true neighbors exist, so all methods (including exact scan) cap at R@10 ≈ 0.01. The "efficiency" column is relative to the subset's exact-scan ceiling — the speed mode reaches 100% (it is exact).
  • HNSW is faster in raw latency on CPU (0.69 ms vs 24 ms) — expected: the speed mode does an exact scan (O(N·d)); HNSW is sublinear. On GPU, the QKᵀ matmul closes much of the gap — the fused kernel (v1.7.2) runs an exact scan of 1M in 2.41 ms (see the real benchmark).
  • O(K) anchors: with nprobe=8, the anchor navigation reaches 100% of the ceiling (the anchors capture the true neighbors) while evaluating only the relevant cells. With nprobe=4, recall drops to 67% — the nprobe trade-off is real and documented.
  • Build: speed brute = 0.6 s vs HNSW = 332 s (~556× faster).
  • The speed mode's value is exactness + build speed + determinism, not beating HNSW on raw CPU latency. On GPU it competes directly on latency.

Limitations (read this first)

We are explicit about what winnex-madhava does not do. Most "surprising" behavior below is by design — the engine is optimized for a specific input domain, and using it outside that domain silently degrades quality.

Input: default (uint8) vs speed (float32)

default mode treats every corpus vector as uint8 bytes (np.uint8), values 0–255. This is the BIGANN-style quantized format the math assumes.

# ✅ Correct (default mode)
corpus = np.random.randint(0, 256, size=(10_000, 128), dtype=np.uint8)
engine = winnex_madhava.build_engine(corpus, dim=128, k=10)
query  = corpus[0].astype(np.float32)     # float32 *of the uint8 values*

For float32 embeddings (cosine), pass them directly — build_engine routes float32 corpora to the native float32 bound path (no uint8 truncation), or use speed=True for the exact GPU scan:

# ✅ Correct for float32 embeddings
embeddings = np.random.randn(10_000, 128).astype(np.float32)
embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
engine = winnex_madhava.build_engine(embeddings, dim=128, k=10,
                                     metric="cosine")

requires-python >= 3.10 with pre-built wheels for 3.10/3.11/3.12

Wheels ship for CPython 3.10, 3.11 and 3.12 (manylinux). Installing on any of these pulls the pre-built wheel — no compiler needed. Only non-x86-64 or future Python versions fall back to building from source.

The guarantee is per-document bound-correctness, not "great recall"

bound_violations == 0 means: every vector the engine pruned was provably not in the exact top-K. It does not mean the returned top-K is the true top-K. If k1_fraction is too small (e.g. 0.001 on a hard dataset), the survivors may be a weak sample and recall drops — still with 0 violations. The bound is sound, but pruning quality depends on stage1_dim, stage2_dim and k1_fraction. Tune them on your data.

Scope is now machine-readable (1.9.10). Every SearchResult carries recall_guarantee:

  • "exact_global" — the exact post-filter scored all N vectors (k3 == N): the returned top-K is the exact global top-K.
  • "pool_only" — the post-filter scored only the k1/k2 survivors (k3 < N): the returned top-K is the best within the pool; docs cut by the prefilter were discarded without a per-vector proof, so the global top-K is not guaranteed.

search_exact() always reports "exact_global". search_with_commitment() carries the same field, so a compliance layer never signs a pool_only commitment as if it proved the global top-K.

For a global guarantee, use audit_exhaustive=True (1.9.10):

eng = wm.build_engine(X, metric="cosine", k=10,
                      audit_exhaustive=True)   # k1 = k2 = N, no early_exit
r = eng.search(q)
assert r.recall_guarantee == "exact_global"    # r IS the exact global top-K

This forces the post-filter pool to cover the entire corpus (and disables early_exit), so search() == search_exact() by construction. Cost: O(N·d) per query — no recall pruning. Audit/compliance/WORM consumers (tracer-gov / tracer-med) that sign a certificate should use this mode. Default is False (historical behavior, no perf change): the pruning path remains the fast option, and now it honestly reports pool_only when it cannot prove the global top-K.

Lower-dimensional / tiny corpora

The Stage-1 QR projection shines on high-dimensional uint8 data (64–1000D). On tiny corpora or d < ~8 the projection overhead dominates and an exact search_exact scan is both faster and simpler.

Speed mode: CPU exact scan is O(N·d); GPU needs a CUDA build

The CPU speed mode is an exact scan — O(N·d) per query. It is correct everywhere but HNSW beats it on raw CPU latency (sub-linear vs linear). The O(K) anchor navigation (speed_n_anchors>=2) reduces the evaluated set to the relevant cells (sub-linear in data touched), with a recall cost that depends on nprobe.

The GPU path is a fused QKᵀ+topk kernel. The default backend is OpenCL (src/speed_opencl.cpp) — vendor-neutral, JIT-compiled, no nvcc required — with a CUDA opt-in (src/speed_gpu.cu, built via -DMADHAVA_USE_CUDA=ON). The GPU is enabled automatically when an OpenCL loader + device is present, else it falls back to CPU (see require_gpu=True to force a hard error instead). There is no persistence/serialize API yet — rebuild per process.

O(K) anchor recall depends on nprobe

The sublinear speed mode trades recall for speed like any IVF index: with a small nprobe, some true neighbors may fall outside the probed cells (measured: nprobe=4 → 67% of the exact ceiling, nprobe=8 → 100% on structured data). Tune speed_n_anchors/speed_nprobe on your data.

Honest comparison

We are explicit about where winnex-madhava does not win:

Use case Best tool Why
Lowest latency (sub-ms) HNSW HNSW ≈ 0.45 ms vs madhava bound ≈ 2.7 ms at 50K×1536D (approximate vs exact)
Exact scan, low latency winnex-madhava speed GPU Fused QKᵀ+topk: 2.41 ms at 1M single-query — the fastest exact path
Provable completeness winnex-madhava Only engine with 0 bound violations + per-doc proof
Frequent index rebuilds winnex-madhava Build ≈ 1 s (10M) vs HNSW ≈ 1025 s
Regulated / auditable retrieval winnex-madhava Deterministic, per-document audit trail

If you need raw speed, use HNSW — it is excellent. winnex-madhava is for the regions where "fast but unprovable" is a liability: legal discovery, medical records, financial compliance, government audits, and RAG systems that must not silently drop a relevant document.

Build from source

# Wheel + sdist (pip-installable)
python -m build

# C++ library only
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build        # C++ unit tests

# Python tests
python -m pytest tests/python/

License

Business Source License 1.1 (BSL 1.1) — the same license as the rest of the Winnex stack.

What BSL 1.1 means for you

  • Free to use for evaluation and non-production work — study, test, prototype, benchmark. This is the recommended way to start.
  • Not free for commercial / production use (a "Search Service" that exposes the functionality to third parties as a service). That requires a commercial license from Winnex.
  • Change date: the license converts to GPL v2.0 or later on the change date (see the full license text), at which point the standard open-source terms apply.

How to get a commercial license: email pay@winnex.ai. The Winnex team will issue a license agreement for your use case (ISV embedding, database vendor, platform company, or internal production deployment).

Contact

pay@winnex.ai · Winnex Brasil Soluções Empresariais LTDA-ME · Goiânia, Brazil

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

winnex_madhava-1.9.15.tar.gz (265.8 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

winnex_madhava-1.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (865.6 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

winnex_madhava-1.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (861.0 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

winnex_madhava-1.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (859.4 kB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

File details

Details for the file winnex_madhava-1.9.15.tar.gz.

File metadata

  • Download URL: winnex_madhava-1.9.15.tar.gz
  • Upload date:
  • Size: 265.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for winnex_madhava-1.9.15.tar.gz
Algorithm Hash digest
SHA256 9282bea2534fc128d908b6d4686f5cf096c513b48f5ac7a4e24d064d9a6e7a3e
MD5 85ad494f411a1035fa478d68c887c89b
BLAKE2b-256 d9e79474078bdce23fafa5886303195cc8971d3378b8c57c713e44dfceae5368

See more details on using hashes here.

Provenance

The following attestation bundles were made for winnex_madhava-1.9.15.tar.gz:

Publisher: publish.yml on winnex-ai/winnex-madhava

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file winnex_madhava-1.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for winnex_madhava-1.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 9fd5285525e8ec5735303ccafc5078a1a0e1e8ac116416daefe39655f424a2e3
MD5 e369dbf920c0b5fcab08352a74bf9c2e
BLAKE2b-256 0c230033a86e25dd5203a2c45d8ca57e4ee398713d7316d6d7a600756a728a21

See more details on using hashes here.

Provenance

The following attestation bundles were made for winnex_madhava-1.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on winnex-ai/winnex-madhava

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file winnex_madhava-1.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for winnex_madhava-1.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7844234824415d0b64f4b097386b40b74edd468b5d2ba9b092ad05f89a37f733
MD5 ea3b78d2195c473792ce98597dd7448e
BLAKE2b-256 1ecc04ea5bba7aebb814c105523328606ae1324e3a9834bb7583f129c5bc13ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for winnex_madhava-1.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on winnex-ai/winnex-madhava

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file winnex_madhava-1.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for winnex_madhava-1.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 352c17261a718ae796428e89331963d752e76891d2436ba2e1697587e2c69114
MD5 72307f117867645b45aa378e4364f2b1
BLAKE2b-256 fe19aa75bbf78e3afdf9eb0ce2ce4b536fde5343bb44e2efe73c989c7de8dc01

See more details on using hashes here.

Provenance

The following attestation bundles were made for winnex_madhava-1.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on winnex-ai/winnex-madhava

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.9.15 This release

4 files

1.9.14

4 files

1.9.13

4 files

1.9.12

4 files

1.9.11

4 files

1.9.10

4 files

1.9.9

4 files

1.9.8

4 files

1.9.7

4 files

1.9.6

4 files

1.9.5

4 files

1.9.3

2 files

1.9.2

2 files

1.9.1

2 files

1.9.0

2 files

1.8.8

2 files

1.8.7

2 files

1.8.6

2 files

1.8.5

2 files

1.8.4

2 files

1.8.3

2 files

1.8.2

2 files

1.8.1

2 files

1.8.0

2 files

1.7.2

2 files

1.7.1

2 files

1.7.0

2 files

1.6.1

2 files

1.6.0

2 files

1.5.0

2 files

1.4.1

2 files

1.4.0

2 files

1.3.3

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.1

2 files

1.2.0

2 files

1.1.4

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.1

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page