mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
perf(python): use the native lance reader for local 2-phase blocks
f5e23ab0 routed 2-phase block reads through the backend-agnostic query
builder so they would also work against a RemoteTable, which `to_lance()`
does not support. That is the right default, but it costs a measurable
amount on the local path: the generic builder re-plans each positional
range read, and the filtered path has to select `_rowoffset` and sort by
it afterwards because `take_offsets` makes no ordering guarantee.
Resolve the table once per dataset instance instead. A local table yields
a pylance handle and reads go straight to `scanner(offset=, limit=)` or
`take()`, both of which already return rows in the requested order; a
RemoteTable raises NotImplementedError, is cached as None, and keeps the
generic path unchanged.
The cached handle is Rust-backed and not picklable, so __getstate__ drops
the key entirely rather than setting it to None -- a worker that inherited
a None would read it as "confirmed remote" and never take the fast path
against its own reconnected table.
Local-NVMe 2-phase read throughput 78.7 -> 100.6 MB/s, which with the
preceding commit restores the pre-regression 2.3x speedup over 1-phase
(now 2.0x, within trial-to-trial noise of the September baseline).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e907ff8c0e
commit
d268300a4a
@@ -1056,14 +1056,41 @@ class StreamingDataset(IterableDataset):
|
||||
pinned.checkout(self._pinned_version)
|
||||
return pinned
|
||||
|
||||
def _pinned_lance_dataset(self, pinned_table):
|
||||
"""Native pylance handle for ``pinned_table``, if it's a local table.
|
||||
|
||||
``to_lance()`` only works against a local table (it raises
|
||||
``NotImplementedError`` on ``RemoteTable``); cached per dataset
|
||||
instance (keyed by nothing -- there's only ever one pin per
|
||||
instance) so the conversion runs at most once rather than once per
|
||||
block read. ``None`` means ``pinned_table`` is remote and callers
|
||||
must use the backend-agnostic query builder instead.
|
||||
"""
|
||||
if not hasattr(self, "_pinned_lance_ds_cache"):
|
||||
try:
|
||||
self._pinned_lance_ds_cache = pinned_table.to_lance()
|
||||
except NotImplementedError:
|
||||
self._pinned_lance_ds_cache = None
|
||||
return self._pinned_lance_ds_cache
|
||||
|
||||
def _read_block(
|
||||
self, pinned_table, block_id: int, columns: Optional[list[str]]
|
||||
) -> pa.Table:
|
||||
"""Read one 2-phase block's live rows, in natural (ascending) order."""
|
||||
lance_ds = self._pinned_lance_dataset(pinned_table)
|
||||
if self._filter is None:
|
||||
# offset/limit is a positional, sequential read, so its output
|
||||
# order is exactly storage order -- no restoration needed.
|
||||
row_start, row_end = self._block_ranges[block_id]
|
||||
if lance_ds is not None:
|
||||
# Local table: pylance's native scanner is a direct,
|
||||
# purpose-built positional range read -- substantially
|
||||
# faster than routing the same offset/limit request through
|
||||
# the generic, backend-agnostic query builder below, which
|
||||
# exists so this also works against a remote (Cloud) table.
|
||||
return lance_ds.scanner(
|
||||
offset=row_start, limit=row_end - row_start, columns=columns
|
||||
).to_table()
|
||||
q = pinned_table.search().offset(row_start).limit(row_end - row_start)
|
||||
if columns is not None:
|
||||
q = q.select(columns)
|
||||
@@ -1071,12 +1098,18 @@ class StreamingDataset(IterableDataset):
|
||||
|
||||
# Filtered: this block's exact live-row dataset offsets were
|
||||
# already computed once, up front (from the "_rowoffset" system
|
||||
# column -- see __init__), so take_offsets fetches just the live
|
||||
# rows themselves, skipping any dead-row gaps entirely. Unlike
|
||||
# offset/limit, take_offsets makes no ordering guarantee for an
|
||||
# column -- see __init__), so a sparse take fetches just the live
|
||||
# rows themselves, skipping any dead-row gaps entirely.
|
||||
offsets = self._block_live_offsets[block_id]
|
||||
if lance_ds is not None:
|
||||
# Local table: pylance's native take() preserves the requested
|
||||
# offset order already, unlike the generic take_offsets() path
|
||||
# below, so no extra sort-and-rebuild pass is needed here.
|
||||
return lance_ds.take(offsets.tolist(), columns=columns)
|
||||
|
||||
# Remote table: take_offsets makes no ordering guarantee for an
|
||||
# arbitrary index list, so "_rowoffset" is always also selected
|
||||
# and used to restore the request's ascending order locally.
|
||||
offsets = self._block_live_offsets[block_id]
|
||||
select_cols = (columns if columns is not None else self._all_columns) + [
|
||||
"_rowoffset"
|
||||
]
|
||||
@@ -1914,6 +1947,11 @@ class StreamingDataset(IterableDataset):
|
||||
):
|
||||
state[key] = None
|
||||
state["_consumer_iterator_lock"] = None
|
||||
# Non-picklable Rust-backed handle (see _pinned_lance_dataset);
|
||||
# dropped rather than set to None so the worker's first _read_block
|
||||
# call recomputes it fresh against its own reconnected table instead
|
||||
# of permanently misreading an absent cache as "confirmed remote".
|
||||
state.pop("_pinned_lance_ds_cache", None)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
||||
Reference in New Issue
Block a user