test(python): add data overlay benchmark suite#7544
Conversation
1156dd2 to
687507f
Compare
687507f to
92734fa
Compare
d1e3be6 to
d1fa12b
Compare
92734fa to
9a09276
Compare
91b3987 to
ecfb29c
Compare
6d52845 to
968d024
Compare
|
Important This PR touches the Lance format specification. Substantive changes to the format specification — the If this is a meaningful format change:
|
40a4a5c to
c3837ce
Compare
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: QUIET Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Benchmark resultsRebased onto latest Setup: 1M rows, local disk (NVMe), Manifest growth (
|
| overlays | 1% contiguous | 1% stride | 10% stride |
|---|---|---|---|
| 1 | 16.4 KB/ea | 40.2 KB/ea | 253 KB/ea |
| 4 | 10.3 KB/ea | 25.2 KB/ea | 158 KB/ea |
| 16 | 8.8 KB/ea | 21.4 KB/ea | 134 KB/ea |
| 64 | 8.4 KB/ea | 20.5 KB/ea | 128 KB/ea |
(64 layers @ 10% stride → ~8.2 MB manifest total.)
Write cost (test_overlay_write.py)
Updating 1% of rows via an overlay vs. the existing rewrite-based mechanisms. narrow = id + val; wide = id + val + 64-d float32 payload:
| approach | narrow: read / persisted | wide: read / persisted |
|---|---|---|
| update | 6.2 MB / 100 KB | 8.7 MB / 2.66 MB |
| merge_insert | 2.4 MB / 101 KB | 2.4 MB / 2.66 MB |
| merge_insert (indexed) | 12.7 MB / 100 KB | 15.3 MB / 2.66 MB |
| overlay | 143 B / 105 KB | 143 B / 105 KB |
| full_rewrite | 6.2 MB / 6.17 MB | 262 MB / 262 MB |
The rewrite-based approaches delete-and-reinsert whole rows, so their cost scales with row width. An overlay only ever touches the changed column: read bytes stay ~0 and persisted bytes stay constant (~105 KB, dominated by the coverage-bitmap + manifest overhead) regardless of how wide the row is — the advantage over update/merge_insert grows directly with row width, and vs. full_rewrite it's already ~60-2500x smaller at this width.
Read scaling (test_overlay_read.py)
1M-row int32 scan/take, strided coverage, v2.0 format:
| overlays | take (mean) | scan (mean) |
|---|---|---|
| 0 | ~0.25 ms | ~1.7 ms |
| 4 | ~0.7 ms | ~7-8 ms |
| 16 | ~1.2 ms | ~40 ms |
- take stays roughly flat (rank pushdown means only the requested rows are touched).
- scan degrades close to linearly with overlay-layer count (~24x at 16 layers) — this is the existing
route_overlayscost profile (offset-major bitmap probing), not something this PR changes. It's the strongest argument for overlay compaction. - Wide embeddings (3072-d): scan is ~600-870 ms regardless of overlay count, since base I/O volume dominates over overlay-routing cost at that width.
cc for the demo this week: the standout numbers are the overlay write cost (143 B read, constant persisted bytes independent of row width) and the scan-cost-vs-layer-count curve as the motivating case for compaction.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Adds a Python benchmark suite for data overlay files in python/ci_benchmarks/, covering three areas: - manifest size growth vs. number of overlays and coverage, - bytes written updating 1% of a column via an overlay vs. update / merge_insert / full rewrite, across narrow and wide rows, - take and scan time + cold read IO as overlay layers, coverage fraction, fragmentation, and data type vary. Shared dataset/overlay construction and measurement helpers live in ci_benchmarks/overlays.py and build fixtures through the public lance.LanceOperation.DataOverlay binding. The write benchmark verifies each approach actually performs the update and splits persisted bytes into data vs. metadata, since the rewrite mechanisms are delete-and- reinsert (whole rows, all columns) while an overlay writes only the changed column. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overlay read benchmark's take/scan scaling sweep only covered a 4-byte int32 value column, where the base read is trivial and per-layer merge cost dominates. ML overlays typically target wide vector columns, so add test_overlay_read_wide: the same overlays x coverage-pattern grid on a 3072-d float32 embedding (12 KiB/row). Thread an embedding_dim through the overlay helpers so the value width is configurable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… API `DataOverlayFile` takes a single `offsets` param, not `shared_offsets`, and the release-build opt-in env var is `LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES` (not `LANCE_ENABLE_DATA_OVERLAY_FILES`). Both diverged from the benchmark helper after PR #7540 landed the real Python bindings; commit()'s returned handle skipped flag validation so this only surfaced on a fresh dataset open.
Self-review pass on the overlay benchmark suite:
- Remove the unused `base_path` parameter of `commit_overlay_layers` (the
body derives its data dir from the dataset URI) and the dead `dir_size`
helper; both had no effect and misled the reader.
- Rename `value_type` -> `_value_type` to match the module's private helpers.
- Document the `{u64::MAX - version}.manifest` naming in `manifest_size`.
- Guard the read and manifest benchmark fixtures: assert a covered cell reads
back the overlaid value and that overlays grow the manifest, so a
silently-broken overlay can't let the benchmark pass while measuring plain
base reads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c3837ce to
8204329
Compare
| def _gen_values( | ||
| dtype: str, | ||
| n: int, | ||
| rng: np.random.Generator, | ||
| embedding_dim: int = EMBEDDING_DIM, | ||
| ) -> pa.Array: | ||
| if dtype == "int32": | ||
| return pa.array(rng.integers(0, 1 << 30, size=n, dtype=np.int32)) | ||
| if dtype == "embedding": | ||
| flat = rng.random(n * embedding_dim, dtype=np.float32) | ||
| return pa.FixedSizeListArray.from_arrays(pa.array(flat), embedding_dim) | ||
| raise ValueError(f"unknown overlay benchmark dtype {dtype!r}") |
There was a problem hiding this comment.
question: other tests use from lance._datagen import rand_batches, can we use that here?
There was a problem hiding this comment.
I looked at it, but rand_batches is byte-budgeted (num_batches/batch_size_bytes) with no exact row-count control, and it randomizes every column. These fixtures need an exact num_rows with rows_per_file == num_rows (single fragment, so the take indices and per-fragment coverage offsets are well-defined), so I kept the small custom generator. It also avoids the datagen-feature build requirement (is_datagen_supported()). Happy to switch if you'd prefer the dependency — let me know.
There was a problem hiding this comment.
Done — switched to it (ed64118, 1be2bec). rand_batches only sized batches by bytes, but the underlying lance-datagen builder already has into_reader_rows, so I exposed a rows_per_batch option on the binding (mutually exclusive with batch_size_bytes) and make_base_dataset now uses it for an exact row count. Overlay values keep their own generator so each layer stays distinct from the base (the read-visibility guard relies on that). Built with --features datagen and verified: exact row counts (single + multi-fragment), the int32/embedding schemas, overlay-visible-on-read, and the 1M-row manifest case all pass.
…t read benchmark - README: remove the per-benchmark descriptions; keep it to general suite advice. - Manifest/read benchmarks: report via record_property only, no printing. - Drop test_overlay_read_dtype: its int32 and embedding read costs are already covered by test_overlay_read_scaling (int32) and test_overlay_read_wide (embedding) across the full overlay-layer sweep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The write-cost comparison (overlay vs. update / merge_insert / full rewrite) is a useful one-off measurement but not something we need to keep re-running in CI for regressions. Remove it, along with the helpers it exclusively used (file_sizes, written_breakdown, proc_io_counters, and the make_base_dataset payload column). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The datagen binding could only size generated batches by bytes. The underlying lance-datagen builder already supports an exact row count (into_reader_rows), so expose it: rand_batches gains a rows_per_batch option, mutually exclusive with batch_size_bytes. This lets callers request an exact number of rows per batch rather than approximating via a byte budget. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled base-dataset generator in make_base_dataset with lance._datagen.rand_batches (row-count mode), matching the other ci_benchmarks suites. One batch per fragment; num_rows must be a multiple of rows_per_file. Overlay values keep their own generator so each layer stays distinct from the base (the read-visibility guard depends on that). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Python benchmark suite for data overlay files in
python/ci_benchmarks/, stacked on #7540 (which exposes theDataOverlaycommit binding these benchmarks use to build fixtures).The benchmarks generate their own base datasets and overlays into a temp dir, so no pre-generation step is needed. Three areas:
test_overlay_manifest.py— manifest growth vs. number of overlays and coverage size/pattern.test_overlay_write.py— bytes written updating 1% of a column via an overlay vs.update/merge_insert/ full rewrite.test_overlay_read.py— take and scan wall time (pytest-benchmark) plus cold read IO (io_stats_incremental) as overlay layers, coverage fraction, fragmentation (contiguous vs. strided), and data type (int32 vs. fixed-size-list embedding) vary, across v2.0 and v2.1.Shared dataset/overlay construction and measurement helpers (
/proc/self/io, on-disk size, manifest size) live inci_benchmarks/overlays.py.Notes:
update-via-overlay path does not exist yet, so the write benchmark's overlay arm is hand-rolled and writes fresh values without reading the base column first; the headline metric is bytes written, latency secondary.🤖 Generated with Claude Code