feat: add Lance-native MemWAL HNSW for shard writer#6795
Conversation
|
ACTION NEEDED The PR title and description are used as the merge commit message. Please update your PR title and description to match the specification. For details on the error please inspect the "PR Title Check" action. |
Thanks for doing this, super informative! I think we probably want to test both this and also keep the old small schema of just a id + 1024 dim vector case, just to make sure we have knowledge of the range of throughput depending on the per-row size. (technically the same comment for the FTS PR you have, we should test both this schema and a small schema with just id + 1.5kb text). |
|
Addressed this in ff768f1: the native shard-writer benchmark now has |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
Follow-up for the small-schema part of @jackye1995's comment: I ran the native Rust shard-writer benchmark with
Artifacts:
This is not a full max-throughput sweep, but it verifies the benchmark covers both requested schema shapes and gives an initial small-schema range to compare against the FineWeb-shaped results. |
|
I ran the native Rust MemWAL shard-writer benchmark for both schema shapes based on suggestion from @jackye1995. FineWeb-shaped baseline:
Small vector-only schema:
Takeaways:
|
Previously flush_with_indexes wrote N+1 manifest files: one for the data fragment via Dataset::write, then one per index via apply_commit. Now all index metadata is collected without committing and the v1 manifest is overwritten in-place with write_manifest_file_to_path to produce a single-version dataset containing both data and all indexes.
jackye1995
left a comment
There was a problem hiding this comment.
thanks for the work! I added one missing part about flushing memtable as a single commit also here.
## Summary This PR adds a Lance-native in-memory HNSW implementation for MemWAL and wires it into the async `ShardWriter` path. The benchmark shape follows @jackye1995's suggestion to use a FineWeb-like baseline: a text payload around 1.5 KiB plus a 1024-dim `f32` vector column, or about 5760 bytes per row. I used hnswlib's construction hot path as a reference while adapting the implementation to Lance MemWAL's requirements: multi-reader/single-writer access, reader-visible publication during writes, and reuse of the vector data already held by the MemTable instead of copying vectors into a separate HNSW-owned buffer. Main changes: - add `rust/lance/src/dataset/mem_wal/hnsw/` with a MemWAL-oriented HNSW graph and Arrow-backed vector storage - update `mem_wal/index/hnsw.rs` to use the new graph for active MemTable vector search - add WAL queue stats so benchmarks can distinguish WAL flush lag from final close/drain work - add native Rust benchmarks under `rust/lance/benches/`, including side-by-side HNSW/hnswlib comparison scaffolding and the shard-writer WAL backpressure benchmark - add `--schema-shape fineweb|vector_only` to the native shard-writer benchmark so we can run both the FineWeb-shaped case and the older small `id + 1024-dim vector` case requested in review ## Benchmark Summary So Far Baseline before this work was roughly: - safe durable async+index throughput: ~3.66 MB/s (`batch=50`, 512 KiB WAL, no backpressure) - previous peak with manageable backpressure: ~6.17 MB/s (`batch=100`, 2 MiB WAL, ~24s drain) - bottleneck identified as active in-memory HNSW insert throughput, not S3 bandwidth Best current WAL-queue results on `c7i.16xlarge`, S3 bucket `jack-devland-build`, FineWeb-shaped rows, `--skip-close`, and explicit WAL queue stats: | batch | WAL | target rows/s | actual rows/s | MB/s | final WAL queue | max WAL queue | thread setting | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | | 10 | 16 MiB | 8000 | 8000 | 46.08 | 0.310% | 1.526% | rayon=48, tokio=16 | | 100 | 8 MiB | 6000 | 6000 | 34.56 | 0.130% | 1.070% | rayon=64, tokio=16 | | 1000 | 8 MiB | 8000 | 8000 | 46.08 | 0.200% | 1.500% | rayon=64, tokio=8 | Interpretation: for these paced single-shard runs, WAL flush can keep up at 34-46 MB/s without accumulating material WAL backlog. This is about 7.5x over the previous 6.17 MB/s peak and about 12.6x over the earlier 3.66 MB/s no-backpressure point on the same FineWeb-shaped workload. Thread ablation suggests high Tokio worker counts are not required for this path; the important CPU knob is the Rayon pool used by async index construction. Moderate Tokio settings (`4-16`) were enough in the tested single-writer workload. Small-schema follow-up for @jackye1995's comment, using `--schema-shape vector_only` (`id + 1024-dim f32 vector`, default row estimate 4160 bytes) on the 12xlarge EC2 runner, S3 bucket `jack-devland-build`, `--skip-close`, and the same WAL-queue stats: | batch | WAL | target rows/s | actual rows/s | MB/s | p99 ms | final WAL queue | max WAL queue | thread setting | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | | 10 | 16 MiB | 8000 | 7999.855 | 33.279 | 0.020 | 0.481% | 1.998% | rayon=64, tokio=64 | | 10 | 16 MiB | 11000 | 10999.782 | 45.759 | 0.019 | 0.481% | 2.495% | rayon=64, tokio=64 | | 100 | 8 MiB | 6000 | 5999.954 | 24.960 | 0.042 | 0.340% | 1.400% | rayon=64, tokio=32 | | 100 | 8 MiB | 9000 | 8999.850 | 37.439 | 0.045 | 0.340% | 2.100% | rayon=64, tokio=32 | | 1000 | 8 MiB | 8000 | 7999.913 | 33.280 | 0.084 | 0.100% | 1.800% | rayon=64, tokio=8 | | 1000 | 8 MiB | 11000 | 10999.909 | 45.760 | 0.154 | 4.600% | 4.600% | rayon=64, tokio=8 | This is not a full max-throughput sweep for the small schema, but it confirms the benchmark can run the requested older shape and that it reaches the same ~45.8 MB/s region at 11k rows/s, with the `batch=1000` high-rate case starting to show more WAL queue backlog than the smaller batches. ## Analysis Artifacts Saved local analysis: - `/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/wal-queue-all-runs-20260515/RESULTS.md` - `/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/thread-ablation-20260515/RESULTS.md` - `/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/vector-only-pr-20260515/` S3 result artifacts: - `s3://jack-devland-build/memwal-walqueue-panel-20260514T171240Z/_bench_results/` - `s3://jack-devland-build/memwal-walqueue-supplement-20260514T182826Z/_bench_results/` - `s3://jack-devland-build/memwal-walqueue-higher-20260515T001817Z/_bench_results/` - `s3://jack-devland-build/memwal-thread-ablation-20260515T060229Z/_bench_results/` - `s3://jack-devland-build/memwal-vector-only-pr-20260515T090611Z/_bench_results/` ## Validation - `cargo fmt --all --check` - `cargo fmt --manifest-path python/Cargo.toml --all --check` - `cargo check -p lance --bench mem_wal_shard_writer_backpressure` - `cargo check --manifest-path python/Cargo.toml --lib` - `cargo clippy -p lance --tests -- -D warnings` - `cargo test -p lance dataset::mem_wal::index::hnsw::tests::test_index_concurrent_insert_and_search -- --exact` cc @hamersaw --------- Co-authored-by: Jack Ye <[email protected]>
Summary
This PR adds a Lance-native in-memory HNSW implementation for MemWAL and wires it into the async
ShardWriterpath.The benchmark shape follows @jackye1995's suggestion to use a FineWeb-like baseline: a text payload around 1.5 KiB plus a 1024-dim
f32vector column, or about 5760 bytes per row. I used hnswlib's construction hot path as a reference while adapting the implementation to Lance MemWAL's requirements: multi-reader/single-writer access, reader-visible publication during writes, and reuse of the vector data already held by the MemTable instead of copying vectors into a separate HNSW-owned buffer.Main changes:
rust/lance/src/dataset/mem_wal/hnsw/with a MemWAL-oriented HNSW graph and Arrow-backed vector storagemem_wal/index/hnsw.rsto use the new graph for active MemTable vector searchrust/lance/benches/, including side-by-side HNSW/hnswlib comparison scaffolding and the shard-writer WAL backpressure benchmark--schema-shape fineweb|vector_onlyto the native shard-writer benchmark so we can run both the FineWeb-shaped case and the older smallid + 1024-dim vectorcase requested in reviewBenchmark Summary So Far
Baseline before this work was roughly:
batch=50, 512 KiB WAL, no backpressure)batch=100, 2 MiB WAL, ~24s drain)Best current WAL-queue results on
c7i.16xlarge, S3 bucketjack-devland-build, FineWeb-shaped rows,--skip-close, and explicit WAL queue stats:Interpretation: for these paced single-shard runs, WAL flush can keep up at 34-46 MB/s without accumulating material WAL backlog. This is about 7.5x over the previous 6.17 MB/s peak and about 12.6x over the earlier 3.66 MB/s no-backpressure point on the same FineWeb-shaped workload.
Thread ablation suggests high Tokio worker counts are not required for this path; the important CPU knob is the Rayon pool used by async index construction. Moderate Tokio settings (
4-16) were enough in the tested single-writer workload.Small-schema follow-up for @jackye1995's comment, using
--schema-shape vector_only(id + 1024-dim f32 vector, default row estimate 4160 bytes) on the 12xlarge EC2 runner, S3 bucketjack-devland-build,--skip-close, and the same WAL-queue stats:This is not a full max-throughput sweep for the small schema, but it confirms the benchmark can run the requested older shape and that it reaches the same ~45.8 MB/s region at 11k rows/s, with the
batch=1000high-rate case starting to show more WAL queue backlog than the smaller batches.Analysis Artifacts
Saved local analysis:
/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/wal-queue-all-runs-20260515/RESULTS.md/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/thread-ablation-20260515/RESULTS.md/Users/jackye/ai/analysis/lance/jack-mem-wal-hnsw/vector-only-pr-20260515/S3 result artifacts:
s3://jack-devland-build/memwal-walqueue-panel-20260514T171240Z/_bench_results/s3://jack-devland-build/memwal-walqueue-supplement-20260514T182826Z/_bench_results/s3://jack-devland-build/memwal-walqueue-higher-20260515T001817Z/_bench_results/s3://jack-devland-build/memwal-thread-ablation-20260515T060229Z/_bench_results/s3://jack-devland-build/memwal-vector-only-pr-20260515T090611Z/_bench_results/Validation
cargo fmt --all --checkcargo fmt --manifest-path python/Cargo.toml --all --checkcargo check -p lance --bench mem_wal_shard_writer_backpressurecargo check --manifest-path python/Cargo.toml --libcargo clippy -p lance --tests -- -D warningscargo test -p lance dataset::mem_wal::index::hnsw::tests::test_index_concurrent_insert_and_search -- --exactcc @hamersaw