Problem
LanceDataset._take_rows with a single stable row id costs about two minutes of pure CPU on a large dataset whose row-id sequences contain holes. The cost does not scale with the number of requested row ids. A second call on the same LanceDataset object returns immediately. The work therefore looks like a one-time resolution across the whole dataset, not a per-id lookup.
Measurements
pylance 9.1.0-beta.2, release wheel, Linux x86-64, dataset in the same AWS region.
Dataset A — 15.4B rows, 17,601 fragments. A compaction materialized its deletions, so its row-id sequences hold holes.
| call |
wall |
cpu |
lance.dataset(uri) |
10.9 s |
|
_take_rows(row_ids=[1 id], columns=["a_float_col"]) |
134 s |
|
_take_rows(row_ids=[3 ids], columns=["a_float_col"]) |
137 s |
136 s |
_take_rows(row_ids=[10 ids], columns=["a_float_col"]) |
106 s |
|
| the same call again on the same object |
0.00 s |
|
ds.get_fragments()[0].take(range(10), columns=["a_float_col"]) |
0.23 s |
|
Dataset B — 1.69B rows, 19,034 fragments, contiguous row-id sequences.
| call |
wall |
cpu |
lance.dataset(uri) |
0.6 s |
|
_take_rows(row_ids=[1 id]) |
0.4 s |
0.3 s |
Dataset B holds more fragments than dataset A and resolves in 0.4 s, so the fragment count is not the driver. On dataset A the cpu time equals the wall time, so the call does no IO.
A py-spy native profile of one cold call (152 s under the profiler, stripped wheel, so no symbol names): 75.6% inside the native _take_rows call, 18.0% in realloc, 2.3% in free and munmap.
Repro shape
This is not reduced to a script yet. The shape is:
- Write a dataset with stable row ids and many fragments.
- Delete a fraction of the rows in most fragments.
- Compact, so the deletions become holes in the row-id sequences.
- Open the dataset in a fresh process and call
_take_rows with one row id.
Related
PR #8534 keeps the scan position when a segment is read in order. Its benchmark puts a single select at about 20 µs. That rate does not account for the two minutes measured above, so the cause here may be different. Both touch row-id resolution over segments with holes, so linking the two.
Problem
LanceDataset._take_rowswith a single stable row id costs about two minutes of pure CPU on a large dataset whose row-id sequences contain holes. The cost does not scale with the number of requested row ids. A second call on the sameLanceDatasetobject returns immediately. The work therefore looks like a one-time resolution across the whole dataset, not a per-id lookup.Measurements
pylance 9.1.0-beta.2, release wheel, Linux x86-64, dataset in the same AWS region.
Dataset A — 15.4B rows, 17,601 fragments. A compaction materialized its deletions, so its row-id sequences hold holes.
lance.dataset(uri)_take_rows(row_ids=[1 id], columns=["a_float_col"])_take_rows(row_ids=[3 ids], columns=["a_float_col"])_take_rows(row_ids=[10 ids], columns=["a_float_col"])ds.get_fragments()[0].take(range(10), columns=["a_float_col"])Dataset B — 1.69B rows, 19,034 fragments, contiguous row-id sequences.
lance.dataset(uri)_take_rows(row_ids=[1 id])Dataset B holds more fragments than dataset A and resolves in 0.4 s, so the fragment count is not the driver. On dataset A the cpu time equals the wall time, so the call does no IO.
A py-spy native profile of one cold call (152 s under the profiler, stripped wheel, so no symbol names): 75.6% inside the native
_take_rowscall, 18.0% inrealloc, 2.3% infreeandmunmap.Repro shape
This is not reduced to a script yet. The shape is:
_take_rowswith one row id.Related
PR #8534 keeps the scan position when a segment is read in order. Its benchmark puts a single
selectat about 20 µs. That rate does not account for the two minutes measured above, so the cause here may be different. Both touch row-id resolution over segments with holes, so linking the two.