Repository navigation
Conversation
Every `lance.dataset()` opened without a session builds its own `Arc<Manifest>` holding the table's whole fragment list, plus its own index and metadata cache budgets and its own object-store client. Daft opens one per scan task, per writer flush and per maintenance instance, so a many-fragment table pays that again for every concurrently open dataset. Pass one process-level `Session` to every open instead. The manifest is a metadata-cache entry keyed by (uri, version, e_tag), so every open of the snapshot the driver pinned hands back a clone of the same `Arc<Manifest>`. `read_blobs` additionally stops closing over the driver's `LanceDataset`: `__reduce__` ships the whole re-encoded manifest per task, which both inflates the payload and bypasses the session's manifest cache. It now carries the uri plus the pinned open kwargs and reopens once per worker process. `DAFT_LANCE_SHARE_SESSION=0` restores lance's per-open sessions. It is an escape hatch, and the only way to get pre-sharing behavior inside a Ray worker, which is what the A/B benchmark needs: a driver-side monkeypatch never reaches the worker. Measured in a Ray worker on a KubeRay cluster, `read_lance` -> `write_parquet` (a write rather than an aggregation on purpose: a task holds its parquet buffers at the same time as its manifest, so the two compete for one memory limit): - 4000 fragments x 10k rows, concurrency 24, whole table: worker ru_maxrss grew 206.5 MB unshared against 157.4 MB shared, so 49.1 MB less resident. Daft's own sampled peak agrees (487.8 vs 442.1 MB). - 100k fragments x 10k rows, concurrency 8, reading 1% in groups of ten: 1052.2 MB against 573.3 MB, so 479.0 MB less resident -- and the pipeline finished in 61.9s instead of 609.6s. At that scale the cost of not sharing is mostly time: each unshared task re-parsed the same 8 MB manifest, about 5.5s of the 6.1s it spent. - Repeated `nearest` queries on a 200k-row IVF_PQ index run 1.7-2.0x faster, because the index is loaded once per process instead of once per query. The maintenance path is the exception and is covered so the exception stays documented: Daft runs each `daft.cls` instance in a process of its own, so a maintenance worker opens the table exactly once however high the concurrency, and nothing multiplies by it. An indexed table does not change that -- a task's open brings in the index's metadata entry only, and the index data is read once on the driver, when the compaction commit remaps it onto the new fragment ids. Tests assert the mechanism rather than the megabytes: `Session.size_bytes()` is exact, needs no large table, and is zero for any open that did not go through the process session. `benchmarks/lance_session_memory.py` (`make bench`) is the counterpart that puts numbers on it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes Made
Every
lance.dataset()opened without a session builds its ownArc<Manifest>holding the table's whole fragment list, plus its own index and metadata cache budgets and its own object-store client. Daft opens one per scan task, one per writer flush and one per maintenance instance, so a many-fragment table pays that again for every concurrently open dataset.This passes one process-level
lance.Sessionto every open instead (daft_lance.namespace.get_shared_session). The manifest is a metadata-cache entry keyed by(uri, version, e_tag), so every open of the snapshot the driver pinned hands back a clone of the sameArc<Manifest>.utils.construct_lance_dataset_handle,DatasetOpenContext._openandLanceDataSink(including thewrite_fragmentscall, whose Rust writer loads the dataset on every flush) now open through that session.read_blobsadditionally stops closing over the driver'sLanceDataset.LanceDataset.__reduce__ships the whole re-encoded manifest per task, which both inflates the payload and bypasses the session's manifest cache; it now carries the uri plus the pinned open kwargs and reopens once per worker process.DAFT_LANCE_SHARE_SESSION=0restores lance's per-open sessions. It is an escape hatch, and the only way to get pre-sharing behaviour inside a Ray worker, which the A/B benchmark needs: a driver-side monkeypatch never reaches the worker.What it saves, per path
The three paths differ and only one of them multiplies by concurrency, so all three were measured rather than assumed.
read_blobs, writesscantask_max_parallelconcurrent opens in one processnearest, point lookups)compact_files,update_columns,merge_columns)The maintenance path is the exception, and it is covered so the exception stays documented. Daft runs each
daft.clsinstance in a process of its own (measured on both runners;use_process=Falsedoes not change it under Ray), so a maintenance worker opens the table exactly once however high the concurrency. An indexed table does not change that either: a task's open brings in the index's metadata entry only, and the index data is read once on the driver, when the compaction commit remaps every entry onto the new fragment ids. If Daft ever fuses those instances into one process,test_maintenance_instances_each_get_their_own_processfails and says so.Measurements
In a Ray worker on a KubeRay cluster,
read_lance→write_parquet. A write rather than an aggregation on purpose: a task holds its parquet writer's buffers at the same time as its manifest, so the two compete for one memory limit, which is what a real backfill does. An aggregation keeps almost nothing resident and flatters the worker.ru_maxrssgrowthAt 100k fragments the cost of not sharing is mostly time: each unshared task re-parsed the same 8 MB manifest, about 5.5s of the 6.1s it spent. Repeated
nearestqueries on a 200k-row IVF_PQ index run 1.7–2.0× faster for the same reason.Two knobs keep the large table affordable without weakening the measurement, because a manifest is the whole fragment list and an open costs the same whatever its task then reads: reading a prefix of the table (
--read-fraction) prunes whole fragments, andread_lance(fragment_group_size=N)puts several fragments in one task. The benchmark refuses to report a number when the resulting task count is too low for the opens to overlap.Related Issues
None.
Checklist
README.md(new## Environment Variablessection, plus the per-path table above).tests/io/lance/test_shared_session.pyandtests/io/lance/test_ray_session_memory.py.Tests assert the mechanism rather than the megabytes:
Session.size_bytes()is exact, needs no large table, and is zero for any open that did not go through the process session — which is exactly what the pre-sharing behaviour looks like.benchmarks/lance_session_memory.py(make bench) is the counterpart that puts numbers on it.make testalso now setsRAY_ENABLE_UV_RUN_RUNTIME_ENV=0. Ray walks the driver's process tree, seesuv run, and replicates that environment to every worker by packaging the working directory and rebuilding the venv per worker; underDAFT_RUNNER=raythat made worker startup fail once the suite had created enough actors (277ActorUnschedulableError). Workers inherit the interpreter from the raylet regardless.Verified on this branch rebased onto
main:DAFT_RUNNER=native make test533 passed,DAFT_RUNNER=ray make test532 passed,make lintandmake check-formatclean.