English · 한국어
Performance and how vecscope works¶
Performance¶
Exact k-NN is quadratic in the number of rows: doubling the rows roughly quadruples the time,
and a full audit (--sample 0) of 100k rows takes minutes on a laptop.
The k-NN pass works in row blocks sized to keep its distance matrix near 1 GiB whatever the row count. The other passes need a few float32 copies of the matrix, so the peak grows with rows x dims at roughly four times the matrix size.
Measured on an 8-vCPU AMD EPYC 7R13 with 8 BLAS threads (NumPy only, single process, clustered synthetic corpus, k=10). Memory is the peak the audit itself allocates; the input matrix comes on top. A three-run median in separate processes agreed with these single runs within 2%, and the OS peak memory within 0.04 GiB.
| rows | dim | seconds | peak memory (input excluded) |
|---|---|---|---|
| 10,000 | 384 | 1.3 | 0.40 GiB |
| 10,000 | 768 | 2.0 | 0.41 GiB |
| 10,000 | 1,536 | 3.6 | 0.44 GiB |
| 50,000 | 384 | 17 | 1.63 GiB |
| 50,000 | 768 | 23 | 1.70 GiB |
| 50,000 | 1,536 | 36 | 1.84 GiB |
| 100,000 | 384 | 65 | 1.71 GiB |
| 100,000 | 768 | 86 | 1.85 GiB |
| 100,000 | 1,536 | 130 | 3.45 GiB |
| 200,000 | 768 | 338 | 3.46 GiB |
| 200,000, default 100k sample | 768 | 85 | 2.14 GiB |
- Time grows close to n² (3.5 to 3.9x per doubling from 50k up).
- Memory is flat from 50k rows at 384 and 768 dims because the k-NN block shrinks with n, and steps up at 1,536 dims and at 200k rows.
- The default 100k-row sample keeps any corpus near the 100k rows.
--queriesadds one more pass over the corpus per query batch, andmetric_mismatchadds a second k-NN pass only when vector norms vary.
To re-measure on your machine at the sizes you care about:
python examples/benchmark_scale.py --rows 50000 100000 --dim 768 # the audit table above
python examples/benchmark_sweep.py # the index sweep
Both take --corpus (clustered, isotropic, anisotropic, duplicates, lowrank, zipf) and
record the machine in their JSON.
Sampled audits¶
Blocking bounds memory, not work: the exact pass still costs about n² × dim multiply-adds, so a million rows at 768 dims is roughly 100 times the 100k-row figure.
For corpora that size, audit the default sample (or several with --sample-repeats) and
reserve --sample 0 for the findings that need every row. A sampled audit is a statement
about the sample. Which findings need every row:
| finding | what a sample gives | needs every row |
|---|---|---|
| anisotropy, effective_rank, rogue_dimensions, norms, metric_mismatch, distance_concentration | a distribution estimate: within 2.3% of the full value on 200k x 384 with 100k, 50k and 20k samples, and the verdict holds | no |
| hubness, antihubs | a lower bound: k-occurrence concentrates with density, so the same corpus scores less hubness on a smaller sample (skewness 2.6 warn on all 200k rows, 1.15 on a quarter, 0.16 on a tenth). Verdicts other than fail are marked inconclusive |
to measure the corpus's own hubness |
| collapse | the duplicate share seen inside the sample: a pair counts only when both rows were drawn, so the sampled share is 0.96 to 1.03 x (sample fraction x full share); the verdict uses the estimated full share, at least the exact copies counted over every row | to count or list every near-duplicate |
| outliers | an estimate that over-counts on small samples (3.9x at a tenth); verdicts other than ok are marked inconclusive |
to find every outlier |
| connectivity | the islands of the sample's own k-NN graph, which miss most islands of the corpus (37% of rows in islands scored 0.08% on a quarter sample); verdicts other than fail are marked inconclusive |
yes, to judge connectivity at all |
| ann_recall | not available on a sample: ids must line up with the ANN file | yes (--sample 0) |
| query_gap, query_shift, query_hubness, ood_queries | reference distances from the sample; every query is scored. query_gap is a ratio of two distances that both grow on a smaller sample; compare query_shift only against a baseline with the same sample size | no |
A sampled audit therefore:
- sets
evidence_scopetosampled(fullwhen every row was audited) and lists inneeds_full_auditthe findings whose verdict cannot stand for the corpus: the inconclusive ones and, with--sample-repeats, the unstable ones. Anything consuming the JSON can gate on these two keys without reading the per-finding caveats - lists
inconclusive_findings(the verdicts the table marks inconclusive) - flags them
INCONCLUSIVE (sampled)in the text output and names them in the gate line - takes the collapse verdict from
details.estimated_full_share(see below) - repeats each caveat in the finding's explanation next to the number
estimated_full_share is the sampled share divided by the sample fraction, or the share of
rows with an exact copy counted over every row (details.full_corpus_exact_copy_share) when
that is larger. Counting exact copies over the whole corpus is cheap (one digest per row), so
rare copies the sample missed still count. Near-duplicates that are not exact copies are only
seen in the sample.
The 200k-row figures come from the
verification;
tests/test_regression_injection.py checks the same properties at 6k rows on every run.
Every number is exact for the rows it saw; vecscope never substitutes an approximate neighbor search for the exact one. The only approximation is the choice of rows.
How vecscope works¶
vecscope computes exact k-NN once over the matrix and derives every diagnostic from it:
- the k-occurrence distribution gives hubness and anti-hubs
- nearest-neighbor cosines give collapse, outliers and distance concentration
- the k-NN graph gives connectivity
- random pairs and the covariance spectrum give anisotropy, effective rank and rogue dimensions
- vector norms give the norm spread
- a second inner-product pass (skipped when norms are uniform) gives metric mismatch
- the ANN neighbor file, if present, is compared against the exact neighbors for recall
Query embeddings, if present, get their own exact search against the corpus for the query-side findings.
Everything is deterministic: fixed seeds, exact neighbors, no learned components, no network. Two runs on the same file give the same JSON.
Exact k-NN is quadratic in the number of rows, so inputs above 100,000 rows are audited on a
seeded random sample unless you pass --sample 0.