#!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright The LanceDB Authors """Benchmark for StreamingDataset throughput. Sweeps read_batch_size from 1 to 16384 to show how amortising the per-request overhead scales. Each row at each chunk size is timed via the real StreamingDataset so the numbers reflect production code. Run with: cd python uv run --extra tests benchmarks/bench_streaming_dataloader.py Optional env vars: BENCH_NUM_ROWS — total rows in the table (default 49152 = 24 × 2048) BENCH_NUM_SPLITS — number of splits (default 24) BENCH_STEPS — round-robin cycles to time per chunk size (default 100) BENCH_ROW_BYTES — approximate bytes per row padded with a binary column (default 4096, mimics a small embedding/image patch) """ import os import time import tempfile import pyarrow as pa import lancedb from lancedb.streaming import StreamingDataset # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- NUM_SPLITS = int(os.environ.get("BENCH_NUM_SPLITS", 24)) # Default: 2048 rows per split so every chunk size up to 16Ki has ≥1 full # chunk (except 16Ki itself which gets a single full-split fetch — still valid). NUM_ROWS = int(os.environ.get("BENCH_NUM_ROWS", NUM_SPLITS * 2048)) STEPS = int(os.environ.get("BENCH_STEPS", 100)) ROW_BYTES = int(os.environ.get("BENCH_ROW_BYTES", 4096)) assert NUM_ROWS % NUM_SPLITS == 0, "NUM_ROWS must be divisible by NUM_SPLITS" CHUNK_SIZES = [1, 4, 16, 64, 256, 1024, 4096, 16384] # --------------------------------------------------------------------------- # Table helpers # --------------------------------------------------------------------------- def make_table(db_path: str) -> lancedb.table.Table: db = lancedb.connect(db_path) payload = b"x" * ROW_BYTES data = pa.table( { "id": pa.array(range(NUM_ROWS), type=pa.int32()), "payload": pa.array([payload] * NUM_ROWS, type=pa.large_binary()), } ) return db.create_table("bench", data, mode="overwrite") # --------------------------------------------------------------------------- # Timing # --------------------------------------------------------------------------- def bench_chunk(table, chunk_size: int, steps: int) -> tuple[int, float]: """Return (rows_drained, elapsed_seconds) for one timed run.""" total_rows = steps * NUM_SPLITS ds = StreamingDataset( table, num_splits=NUM_SPLITS, shuffle_seed=42, read_batch_size=chunk_size ) count = 0 t0 = time.perf_counter() for _ in ds: count += 1 if count >= total_rows: break return count, time.perf_counter() - t0 # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main() -> None: rows_per_split = NUM_ROWS // NUM_SPLITS print("Benchmark config:") print( f" NUM_ROWS={NUM_ROWS} NUM_SPLITS={NUM_SPLITS} " f"rows/split={rows_per_split} STEPS={STEPS} ROW_BYTES={ROW_BYTES}" ) print(f" ~{NUM_ROWS * ROW_BYTES / 1024 / 1024:.1f} MB total table size") print() with tempfile.TemporaryDirectory() as tmp: print("Creating table...", flush=True) table = make_table(tmp) cols = ( f"{'chunk':>6} {'rows':>6} {'elapsed':>8} {'rows/s':>10} {'ms/step':>9}" ) print(f"\n{cols}") print("-" * 52) for chunk in CHUNK_SIZES: # Warm-up pass (one step's worth of rows) warmup_ds = StreamingDataset( table, num_splits=NUM_SPLITS, shuffle_seed=42, read_batch_size=chunk ) warmup_count = 0 for _ in warmup_ds: warmup_count += 1 if warmup_count >= NUM_SPLITS: break drained, elapsed = bench_chunk(table, chunk, STEPS) rows_per_sec = drained / elapsed if elapsed > 0 else float("inf") ms_per_step = elapsed / STEPS * 1000 print( f"{chunk:>6} {drained:>6} {elapsed:>7.3f}s " f"{rows_per_sec:>10.0f} {ms_per_step:>8.1f}ms" ) print() print("Done.") if __name__ == "__main__": main()