Back to Openviking

Preliminary cuVS index results

benchmark/cuvs/PRELIMINARY_RESULTS.md

0.4.825.5 KB
Original Source

Preliminary cuVS index results

These results are an engineering checkpoint for the OpenViking cuVS integration. They are not a final performance claim. The measurements use one GPU, one public ANN dataset, and deterministic synthetic vectors for exact high-dimensional scaling. Every reported row aggregates five independent processes as median +/- median absolute deviation (MAD).

Public-dataset setup

  • Harness revision: 5dbc6a570404c9d1e2c8b584f6447919d0fee62a
  • Dataset: ann-benchmarks glove-100-angular
  • Source SHA-256: 544af1d5e84e112cd4749571dcfd8ca109818a572f850af75a3a09e093a953c4
  • Base vectors: 1,183,514 x 100D float32
  • Queries: 10,000; K=10; cosine ranking via normalized inner product
  • Ground truth: exact neighbors supplied by ann-benchmarks
  • GPU: NVIDIA H20
  • Software: cuVS 26.06, CuPy 14.1.1, CUDA runtime 12.9

The native backend is the existing OpenViking CPU flat-index call path. In this document, cuVS GPU exact specifically means cuVS brute-force on the GPU. It does not mean CAGRA. CAGRA is the approximate GPU index and uses graph_degree=64 and intermediate_graph_degree=128 for the high-recall results. Each public-dataset process times all 10,000 queries after ten warm-up batches.

The index-only harness uses float32 for both native and cuVS exact search. The collection and service sections intentionally preserve normal application behavior instead: native keeps its default per-vector-scale int8 index and cuVS keeps a float32 GPU shadow. Opting into cuVS does not rewrite the native index. Those later sections are therefore not equal-dtype or equal-memory comparisons and report native Recall@K against cuVS brute-force.

GPU memory footprint

The current cuVS shadow stores float32 vectors. The brute-force payload is therefore N * dimension * 4 bytes. CAGRA also retains a uint32 neighbor graph of approximately N * graph_degree * 4 bytes and may require an intermediate graph during build. A cached filter bitset adds approximately N / 8 bytes.

The harness records cudaMemGetInfo immediately before and after index build. The following retained-build deltas are medians from five clean processes:

DatasetBackendGPU memory delta
100K x 768D syntheticcuVS brute-force294 MiB
1M x 768D syntheticcuVS brute-force2.9 GiB
100K x 1024D syntheticcuVS brute-force392 MiB
1M x 1024D syntheticcuVS brute-force3.9 GiB
1,183,514 x 100D GloVecuVS brute-force452 MiB
1,183,514 x 100D GloVecuVS CAGRA872 MiB

These are not peak-VRAM measurements. Allocator state, CAGRA build parameters, query batch size, and concurrent GPU use can add transient memory. The deltas also exclude the approximately 327 MiB CUDA runtime/context baseline observed before build in these processes. The opt-in memory-aware auto mode initializes that runtime first, then estimates the float32 payload, CAGRA retained and intermediate graphs, and filter cache; multiplies the result by a 2.0 default safety factor; and preserves 1 GiB of free memory. Insufficient budget keeps the native path for that query and allows a later retry.

Batch size 1

This is the closest index-level approximation of the current OpenViking single-query integration. It includes Python dispatch, host-to-device query copy, GPU execution, and result copy back to host. It excludes embedding, HTTP, record lookup, reranking, and LLM work.

BackendRecall@10warm p50 (ms/query)warm p95 (ms/query)warm QPS
OpenViking native exact1.0000 +/- 0.000040.209 +/- 0.07140.798 +/- 0.04324.7 +/- 0.1
cuVS GPU brute-force exact1.0000 +/- 0.00000.796 +/- 0.0050.815 +/- 0.0051,254.5 +/- 7.1
cuVS CAGRA ANN, itopk_size=5120.9633 +/- 0.00031.730 +/- 0.0161.995 +/- 0.008562.8 +/- 1.7
cuVS CAGRA ANN, itopk_size=20480.9944 +/- 0.00021.797 +/- 0.0192.036 +/- 0.006549.0 +/- 0.9

In this low-batch regime, cuVS GPU brute-force exact delivers a 50.5x median warm-p50 speedup and 50.7x higher median QPS than the current native CPU exact call path. CAGRA is slower than GPU brute-force exact even before requiring 0.99 recall, so CAGRA should not be selected for this dataset and query shape solely because it is approximate.

Before warm-up, the first batch=1 search was 85.7 +/- 9.6 ms for native exact and 104.0 +/- 2.9 ms for cuVS GPU brute-force exact. The observed ranges were 76.2--122.9 ms and 101.1--157.3 ms, respectively. Cold-start latency therefore remains a separate integration concern; the warm comparison above must not be read as startup latency.

Batch size 128

This measures vector-index throughput capacity. It is not current OpenViking server throughput because the integration currently submits one query at a time.

BackendRecall@10QPSRelative to GPU exact
cuVS GPU brute-force exact1.0000 +/- 0.000035,495 +/- 341.00x
cuVS CAGRA ANN, itopk_size=5120.9628 +/- 0.000443,595 +/- 2801.23x
cuVS CAGRA ANN, itopk_size=20480.9943 +/- 0.000221,711 +/- 790.61x

CAGRA shows a throughput benefit only at the lower recall point in this initial run. At approximately 0.99 recall, GPU exact remains faster.

High-dimensional exact scaling

To approximate common embedding dimensions without making unsupported ANN quality claims, this matrix uses deterministic normalized Gaussian vectors and compares only the two exact paths. The batch=1 crossover runs time 1,000 queries per process through 10K vectors and 200 queries per process at 100K and 1M vectors. All rows have Recall@10=1.0.

The speedup column is native warm p50 divided by cuVS GPU brute-force warm p50. Values above 1.0 mean GPU exact is faster.

DimVectorsNative p50 (ms)GPU exact p50 (ms)Speedup
7681000.031 +/- 0.0000.243 +/- 0.0030.13x
7681K0.122 +/- 0.0010.248 +/- 0.0010.49x
7682K0.219 +/- 0.0010.249 +/- 0.0000.88x
7685K0.487 +/- 0.0130.245 +/- 0.0021.99x
76810K0.915 +/- 0.0140.248 +/- 0.0013.69x
768100K21.991 +/- 0.1770.341 +/- 0.00364.5x
7681M229.728 +/- 0.5801.435 +/- 0.002160.1x
10241000.039 +/- 0.0000.244 +/- 0.0020.16x
10241K0.159 +/- 0.0050.247 +/- 0.0010.64x
10242K0.285 +/- 0.0020.250 +/- 0.0021.14x
10245K0.633 +/- 0.0000.253 +/- 0.0042.50x
102410K1.234 +/- 0.0480.252 +/- 0.0014.90x
1024100K28.443 +/- 0.2620.376 +/- 0.00175.7x
10241M306.730 +/- 0.8381.708 +/- 0.002179.6x

On this integration path, native CPU exact remains faster through 2K vectors at 768D, while GPU exact is faster by 5K. At 1024D, the observed crossover is between 1K and 2K vectors. These are hardware- and implementation-specific boundaries, not universal algorithm thresholds.

This is a warm crossover. The first cuVS search remained approximately 99--110 ms across the synthetic shapes, while native first-search latency was below 1 ms at 2K vectors and below. Short-lived or rarely queried collections therefore need a separate residency/cold-start policy even when their warm vector count is above the crossover.

For a stable GPU capacity measurement, batch=128 runs use 50 search repetitions, or 10,000 timed queries per process:

DimVectorscuVS GPU exact QPS
768100K67,650 +/- 160
7681M12,479 +/- 2
1024100K57,385 +/- 10
10241M9,880 +/- 0.3

These capacity numbers do not represent current OpenViking server throughput; the integration still submits one query at a time.

Build scope

BackendBuild time (s)Scope
OpenViking native exact10.537 +/- 0.099Python DeltaRecord creation plus native upsert
cuVS GPU brute-force exact0.219 +/- 0.001host-to-device matrix copy plus index wrapper
cuVS CAGRA high-recall graph4.719 +/- 0.085matrix copy plus graph construction

These build times do not isolate equivalent kernels. In particular, the native path includes the current row-oriented OpenViking ingestion interface while the cuVS paths accept the full matrix. They should be treated as integration costs, not as a pure CPU-versus-GPU algorithm comparison.

The same caveat applies to high-dimensional exact build time and GPU-memory delta:

DimVectorsNative build (s)GPU exact build (s)GPU delta (GiB)
768100K4.488 +/- 0.0290.203 +/- 0.0000.29
7681M44.294 +/- 0.1461.597 +/- 0.0062.87
1024100K6.165 +/- 0.0300.210 +/- 0.0010.38
10241M60.990 +/- 0.2811.697 +/- 0.0113.82

Collection adapter, filter, and lifecycle

This matrix moves one level above the index microbenchmark. It calls CollectionAdapter.query() and therefore includes OpenViking filter handling, label-to-record lookup, result normalization, persistence, and lazy index rebuild. The initial uncached-filter run uses revision 84f79c5f52b553561299d42730949b612f3fe29c; the prepared-filter-cache follow-up uses revision 087f2a280dc06031665a0bbdb1ef26a9fa2735da. Each result aggregates five independent processes on the same H20/software setup as the exact-scaling runs.

  • Dataset: 100,000 deterministic normalized Gaussian vectors, 768D float32
  • Queries: 50 per scenario; K=10; three warm-up queries
  • Filters: 10%, 1%, and 0.1% target selectivity, with both uniformly distributed and contiguous matching records
  • Mutations: upsert 1, 100, 1,000, and 10,000 records; delete 100 records; close and reopen the persistent collection

This comparison deliberately uses the normal collection defaults. The native flat index uses its default int8 quantization, while cuVS brute-force retains float32 vectors. Consequently, the native path is not the float exact baseline from the index microbenchmark. Its Recall@10 against cuVS GPU brute-force was 0.982 without a filter and 0.978--0.994 with filters. This is both a quality and memory semantic that the integration must make explicit.

Initial uncached-filter result

ScenarioNative p50 (ms)cuVS p50 (ms)Native Recall@10cuVS Recall@10Relative p50
Unfiltered9.413 +/- 0.2380.970 +/- 0.0060.9821.000cuVS 9.7x faster
Uniform 10%1.871 +/- 0.041106.306 +/- 2.2040.9881.000cuVS 56.8x slower
Uniform 1%0.509 +/- 0.003105.816 +/- 2.0220.9861.000cuVS 207.8x slower
Uniform 0.1%0.319 +/- 0.002105.292 +/- 2.7210.9941.000cuVS 330.2x slower
Clustered 10%1.374 +/- 0.002106.706 +/- 1.2970.9781.000cuVS 77.7x slower
Clustered 1%0.468 +/- 0.001104.634 +/- 1.3080.9821.000cuVS 223.3x slower
Clustered 0.1%0.321 +/- 0.004104.219 +/- 1.7480.9941.000cuVS 324.9x slower

Unfiltered collection lookup preserves a material GPU benefit: median QPS was 967.3 +/- 2.5 for cuVS and 106.4 +/- 3.4 for native, a 9.1x throughput ratio. That is smaller than the index-only 100K x 768D result because collection lookup adds fixed host-side work and the native collection path uses int8 quantization.

The initial filtered result identified an integration blocker, not an inherent cuVS limitation. That revision evaluated the predicate against every Python record and rebuilt the GPU prefilter mask on every query. All six cuVS filter scenarios remained near 104--106 ms regardless of selectivity or record distribution.

Prepared-filter-cache follow-up

The follow-up adds a bounded LRU of prepared GPU bitsets and invalidates it on upsert or delete. The first query for each new filter still performs the host predicate scan; subsequent searches with the same filter reuse the device bitset.

ScenarioFirst cuVS query (ms)Cached cuVS p50 (ms)Native p50 (ms)Cached relative p50
Uniform 10%141.022 +/- 1.2481.077 +/- 0.0141.832 +/- 0.066cuVS 1.70x faster
Uniform 1%133.535 +/- 0.7720.989 +/- 0.0110.520 +/- 0.002cuVS 1.90x slower
Uniform 0.1%123.198 +/- 0.6990.948 +/- 0.0020.330 +/- 0.003cuVS 2.88x slower
Clustered 10%121.070 +/- 0.6371.090 +/- 0.0031.389 +/- 0.011cuVS 1.27x faster
Clustered 1%119.758 +/- 1.7630.987 +/- 0.0040.470 +/- 0.003cuVS 2.10x slower
Clustered 0.1%119.441 +/- 1.1750.960 +/- 0.0060.325 +/- 0.002cuVS 2.95x slower

Repeated-filter cuVS p50 improves by 97.9--111.0x over the uncached revision. The 10% scenarios now favor cuVS, while the native scalar index remains faster for 1% and 0.1% selectivity. Unfiltered p50 on the follow-up was 0.965 +/- 0.019 ms for cuVS and 8.643 +/- 0.151 ms for native, an approximately 9.0x ratio.

The cache addresses repeated filters but not first-use or high-cardinality filters: a new predicate still costs approximately 119--141 ms at 100K records. Reusing OpenViking's scalar-index candidate labels is still worth evaluating for those cases. The cache's configured size is 16 prepared filters, and data mutation clears it before rebuilding the dense index.

Native bitmap bridge and auto-routing follow-up

The next follow-up removes that Python predicate scan. Each GPU rebuild now registers its label order with the native engine once. A new filter is evaluated by OpenViking's existing scalar/path index and projected through the registered native-offset layout into a cuVS-row bitset. Repeated filters retain the device bitset; mutations invalidate both the layout and filter cache.

Auto mode also caches a routing decision. The measured defaults send scalar filters with at most 2,000 candidates to native vector recall, keep wider scalar filters on cuVS, and use a lower 200-candidate threshold for URI filters. The lower URI threshold avoids repeatedly paying native Trie/subtree-bitmap work for medium-sized path scopes. Both thresholds are configurable and apply only to auto mode; explicit backend=cuvs continues to use the GPU dense path.

The following values aggregate five independent processes over the same 100K x 768D, 50-query collection workload. The URI rows add unique hierarchical paths whose prefixes select 10%, 1%, and 0.1% of records.

ScenarioFirst query (ms)Auto warm p50 (ms)Route
Unfiltered1,902.930 +/- 6.0450.972 +/- 0.015cuVS; first query includes lazy GPU build
Uniform scalar 10%2.984 +/- 0.0341.119 +/- 0.029cuVS
Uniform scalar 1%2.096 +/- 0.0190.643 +/- 0.018native
Uniform scalar 0.1%1.486 +/- 0.0140.404 +/- 0.009native
Clustered scalar 10%1.853 +/- 0.0121.133 +/- 0.018cuVS
Clustered scalar 1%1.994 +/- 0.0610.578 +/- 0.006native
Clustered scalar 0.1%1.384 +/- 0.0200.399 +/- 0.002native
URI path 10%12.860 +/- 0.5691.126 +/- 0.024cuVS
URI path 1%3.087 +/- 0.0311.020 +/- 0.027cuVS
URI path 0.1%1.635 +/- 0.0140.455 +/- 0.010native

For the six scalar rows, first-use latency is now 1.38--2.98 ms instead of the previous 119--141 ms, a 47--86x reduction. Cached 10% filters retain the GPU advantage, while 1% and 0.1% scalar filters recover the native candidate-pruning advantage. URI first-use cost depends on subtree width: the 10% prefix still spends about 12.9 ms traversing and unioning the native path bitmaps, but that work is avoided after its cuVS bitset is cached. The 1% URI case remains on GPU because repeatedly rebuilding its native path bitmap was slower than cached cuVS search; only the 0.1% URI case crosses the more conservative path threshold.

These auto-mode rows can execute over either the native int8 representation or the cuVS float32 representation. They are latency-routing results, not an equal-dtype comparison; applications requiring one fixed numerical representation should select an explicit backend or disable the native-routing thresholds.

Dirty-index selective-first follow-up

Review of the auto-routing path found that the first implementation decided route_native only after _rebuild_if_needed(). Results remained correct, but a selective filtered query could still pay the full GPU rebuild before falling back to native. Revision d2a74c1af026da0a482955ed2498c03f5f44654c moves native filter resolution and the candidate-count decision before GPU memory admission and rebuild.

A targeted follow-up runs a 0.1% scalar filter as the first query while the 100K x 768D auto index is dirty. Five independent clean processes produced:

MetricMedian +/- MAD
Selective-first query latency11.670 +/- 0.156 ms
Selective-first GPU-memory delta0 +/- 0 bytes
Subsequent unfiltered first query1,889.281 +/- 11.155 ms
Unfiltered query after a one-record mutation1,632.840 +/- 81.424 ms

All five selective-first runs recorded exactly zero GPU-memory delta. The following unfiltered query still paid the lazy build, demonstrating that the native route neither allocated the GPU dataset nor accidentally cleared dirty state. The 11.7 ms selective-first value is higher than a new filter on an already-built index because it includes one-time registration of the current 100K-label native-to-cuVS layout. It nevertheless avoids roughly 1.9 seconds of unnecessary GPU construction for this query shape.

Ingestion, mutation, and restart

OperationNative mediancuVS median
Ingest 100K records29.849 s38.545 s
First unfiltered query after ingest23.108 ms1,811.843 ms
Upsert 1: next query12.167 ms1,410.397 ms
Upsert 100: next query12.835 ms1,402.790 ms
Upsert 1K: next query13.915 ms1,438.549 ms
Upsert 10K: next query14.578 ms1,535.870 ms
Delete 100: next query13.574 ms1,405.846 ms
First query after close/reopen5.338 s15.315 s
Warm query after reopen10.551 ms1.118 ms

Every cuVS mutation marks the index dirty, and the next query synchronously rebuilds the full 100K-vector GPU index. The rebuild cost is therefore almost independent of whether one or 10,000 records were updated. The write API itself remains comparatively cheap; the cost is shifted onto the next reader. A production integration should rebuild in the background, coalesce mutations, and atomically swap the completed index while the prior snapshot continues to serve queries.

Persistent collection loading is lazy, so the reopen time appears in the first query rather than adapter construction. The cuVS first query includes store recovery, rebuilding Python-side records, and building the GPU index. After that work, the reopened cuVS collection returns to approximately 1.1 ms.

Median host RSS increase during ingestion was 0.93 GiB for native and 2.71 GiB for cuVS; cuVS additionally used a 0.29 GiB GPU-memory delta after search. The cuVS host overhead is dominated by the current per-vector Python tuple mirror. These RSS deltas are directional rather than an allocator-isolated memory benchmark because both backends run sequentially in each process.

Async service-facade concurrency

This level calls VikingVectorIndexBackend.query() through OpenViking's normal asyncio.to_thread boundary. It includes tenant-filter injection and async request scheduling, but deliberately uses precomputed query vectors: HTTP, authentication, embedding, reranking, and LLM work are still excluded.

  • Harness revision: e7af4e0c26c82eb58dbb47c07dcaa54b402fbaab
  • Dataset: 100,000 normalized Gaussian vectors, 768D; K=10
  • Concurrency: 1, 4, 16, 32, and 64; default thread-pool limit 32
  • Cached scenarios: 200 requests per concurrency level
  • New-filter scenario: 32 requests per concurrency level, each with a distinct 100-record range in addition to the automatic tenant filter
  • Post-mutation scenario: one full-record upsert, followed by a simultaneous read burst whose size equals the concurrency level

Results aggregate five independent clean processes on H20. Values written with +/- include median absolute deviation; the compact paired-latency tables show process medians where dispersion is omitted. The native collection continues to use its default int8 vector quantization, while cuVS brute-force retains float32 vectors.

Cached tenant filter

Every service-facade request is automatically scoped by account_id; this scenario warms and reuses that prepared filter.

ConcurrencyNative p50 (ms)Native QPScuVS p50 (ms)cuVS p99 (ms)cuVS QPScuVS/native QPS
114.136 +/- 0.06671.3 +/- 2.31.492 +/- 0.0112.175 +/- 0.030614.8 +/- 8.28.63x
416.311 +/- 0.177238.0 +/- 1.76.094 +/- 0.0279.912 +/- 0.176632.6 +/- 5.12.66x
1616.504 +/- 0.240907.8 +/- 25.124.820 +/- 0.08429.885 +/- 1.351636.0 +/- 3.90.70x
3230.050 +/- 1.618982.7 +/- 21.847.998 +/- 0.58554.204 +/- 0.330651.8 +/- 6.80.66x
6454.251 +/- 1.0591,056.8 +/- 4.692.641 +/- 0.53199.232 +/- 1.133677.2 +/- 7.00.64x

cuVS keeps a strong low-concurrency advantage, but its throughput remains near 615--677 QPS as concurrency increases. Native CPU search scales across the thread pool and crosses cuVS between concurrency 4 and 16. cuVS p50 rises from 1.5 ms to 92.6 ms at concurrency 64 because requests queue behind the current index-wide lock; this integration does not yet use concurrent CUDA streams or micro-batching.

Cached 10% filter

ConcurrencyNative p50 (ms)Native QPScuVS p50 (ms)cuVS QPScuVS/native QPS
12.452 +/- 0.011385.5 +/- 3.51.564 +/- 0.006582.7 +/- 10.51.51x
42.937 +/- 0.0381,165.6 +/- 14.76.473 +/- 0.091591.6 +/- 5.40.51x
1612.860 +/- 0.2271,151.7 +/- 7.027.400 +/- 0.257576.0 +/- 5.70.50x
6448.681 +/- 0.5021,215.1 +/- 19.6103.497 +/- 0.930604.3 +/- 4.10.50x

The more selective native scalar path crosses cuVS between concurrency 1 and 4. Prepared-mask reuse removes predicate construction from this scenario, so the remaining cuVS plateau is the search serialization and service overhead.

New filter per request

ConcurrencyNative p50 / p99 (ms)Native QPScuVS p50 / p99 (ms)cuVS QPS
10.807 / 2.9391,034.6280.607 / 308.0683.5
43.088 / 5.8481,132.71,104.427 / 1,159.4143.6
1612.009 / 19.6901,104.64,365.132 / 4,429.8933.6
3220.632 / 27.2881,133.04,581.936 / 8,734.4043.6

The native scalar index resolves each distinct 100-record range efficiently. cuVS still scans all host-side records for every LRU miss, and the index lock serializes those scans. At concurrency 32, half the requests complete around 4.6 seconds while the tail approaches 8.7 seconds. The concurrency-64 case is not separately reported because this scenario has only 32 total requests.

Read burst after one mutation

ConcurrencyNative p50 / p99 (ms)cuVS p50 / p99 (ms)cuVS burst wall time (s)
121.467 / 21.4671,827.384 / 1,827.3841.828 +/- 0.170
423.436 / 23.8601,563.884 / 1,565.9541.566 +/- 0.008
1624.621 / 28.0311,701.266 / 1,711.6891.712 +/- 0.118
3236.285 / 40.4171,612.954 / 1,633.4181.635 +/- 0.046
6449.734 / 69.8581,849.853 / 1,894.1711.896 +/- 0.022

The cuVS write call itself takes only approximately 3.0--3.6 ms; the next reader rebuilds the full index while every concurrent reader waits on the same lock. Higher apparent burst QPS is therefore not steady-state capacity: it is the same rebuild delay amortized across more requests released together.

Current conclusion and next checks

  1. Native CPU exact remains preferable for very small collections. On this setup, cuVS GPU brute-force exact crosses over between 2K--5K vectors at 768D and 1K--2K vectors at 1024D.
  2. CAGRA requires recall-matched tuning; an approximate label alone does not imply better performance.
  3. CAGRA can improve batched capacity around Recall@10=0.96, but this benefit requires batching that the current OpenViking integration does not expose.
  4. Collection-level unfiltered lookup retains an approximately 9.0x warm-p50 benefit at 100K x 768D, but native int8 versus cuVS float32 is not an equal-memory or equal-quality comparison.
  5. Caching prepared filter bitsets improves repeated-filter cuVS p50 by 98--111x. Repeated 10% filters now favor cuVS, while native remains 1.9--3.0x faster at 1% and 0.1% selectivity. A new filter still pays a 119--141 ms host scan, so scalar-index candidate reuse remains relevant.
  6. Synchronous lazy rebuild shifts approximately 1.4--1.7 seconds onto the first reader after any mutation at 100K x 768D. Background snapshot rebuild and atomic swap should be evaluated before enabling cuVS for write-active collections.
  7. Independent process results are stable for this hardware and dataset, but cross-node and cross-day variance are not yet measured.
  8. GloVe-100 and normalized Gaussian vectors are engineering datasets, not a representative agent-memory corpus.
  9. At the async service facade, cuVS wins cached tenant search through concurrency 4, but native crosses it by concurrency 16. The current cuVS lock holds throughput near 600--680 QPS and turns concurrency into queueing latency rather than GPU parallelism.
  10. The next engineering step is to evaluate a read-safe immutable snapshot with concurrent searches or micro-batching, plus background rebuild. A later end-to-end matrix should then add HTTP, embedding, reranking, and a real agent-memory workload.