Interleaved Lance Reader#2232
Conversation
Greptile SummaryThis PR adds
Confidence Score: 5/5Safe to merge — the new Lance interleaved reader correctly delegates to the existing read path, and null sample_id values are now rejected at the split boundary rather than silently merged. The core read-and-split logic is straightforward, reusing the well-tested LanceReaderStage.read_task path and the refactored split_table_by_group utility. The null-validation order change is intentional and consistent with the new documented contract. The only genuine finding is a dead-code metadata fallback that can never fire. No data-correctness or runtime-crash risks were identified. nemo_curator/core/utils.py deserves a second look for callers of split_table_by_group with no batch limits that may now surface null-sample_id errors they previously swallowed silently. Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant ILR as InterleavedLanceReader
participant LPS as LancePartitioningStage
participant ILRS as InterleavedLanceReaderStage
participant LRS as LanceReaderStage.read_task
participant STG as split_table_by_group
Caller->>ILR: decompose()
ILR-->>Caller: [LancePartitioningStage, InterleavedLanceReaderStage]
Caller->>LPS: process(EmptyTask)
LPS->>LPS: open dataset, pin version
LPS-->>Caller: list[LanceReadTask] (fragment_ids + version)
Caller->>ILRS: process(LanceReadTask)
ILRS->>LRS: read_task(task, read_kwargs, fields)
LRS->>LRS: _dataset_and_scanner_kwargs()
LRS->>LRS: scanner.to_table() + blob materialization
LRS-->>ILRS: ReaderOutput(table, metadata)
ILRS->>ILRS: align_interleaved_table(table)
ILRS->>STG: split_table_by_group(table, sample_id, max_batch_bytes, max_batch_rows)
STG->>STG: validate no null sample_ids
STG->>STG: compute group boundaries
STG-->>ILRS: list[pa.Table] (splits)
ILRS->>ILRS: build InterleavedBatch per split
ILRS->>ILRS: batch.validate() per split
ILRS-->>Caller: "InterleavedBatch | list[InterleavedBatch]"
Reviews (6): Last reviewed commit: "Refactor Interleaved Lance Reader: Repla..." | Re-trigger Greptile |
|
/ok-to-test |
VibhuJawa
left a comment
There was a problem hiding this comment.
codex: Consolidation opportunities with the existing Lance and interleaved reader infrastructure.
| return flushed_rows | ||
|
|
||
|
|
||
| def _tables_from_record_batches( # noqa: C901 |
There was a problem hiding this comment.
codex: This overlaps with split_table_by_group_max_bytes, which is already used by the Parquet and WebDataset interleaved readers. Can we consolidate these into one shared interleaved batching utility that accepts tables or record batches, preserves row order, and supports byte, row, and output limits?
There was a problem hiding this comment.
makes sense, but it appears to me that existing utility sorts by group column; and this Lance path preserves row order and supports streaming carry-over, row limits, and output limits.
I think it needs a careful shared utility refactor with tests across Parquet/WebDataset/Lance. @VibhuJawa do you think this is necessary to merge ?
There was a problem hiding this comment.
Actually, I am still confused about the value of the streaming path. _tables_from_record_batches() collects every output chunk in a list, and process() returns them only after scanning finishes. Therefore, it does not provide bounded-memory streaming unless max_output_rows stops the scan early.
What concrete workload requires this path instead of reading the task normally and splitting the resulting table afterward?
For example:
output = self.read_task(task, dict(self.read_kwargs or {}), self.fields)
table = align_interleaved_table(output.data)
splits = split_table_by_group(
table,
"sample_id",
max_batch_bytes=self.max_batch_bytes,
max_batch_rows=self.max_batch_rows,
)Like, what are we trying to achieve by adding this code ?
There was a problem hiding this comment.
The original motivation for this path was to avoid read_task() calling scanner.to_table() and constructing one complete fragment-size Arrow table before any document-aware splitting occurs.
But based on my experiments with mint1t, I think this might not be necessary, so I'll refactor to use your recommendation 👍
VibhuJawa
left a comment
There was a problem hiding this comment.
codex: Optional dependency import boundary.
VibhuJawa
left a comment
There was a problem hiding this comment.
Mostly looks good, added a comment on compatibility shim.
| group_column: str, | ||
| max_batch_bytes: int | None, | ||
| ) -> list[pa.Table]: | ||
| """Backward-compatible wrapper for byte-limited interleaved batching.""" |
There was a problem hiding this comment.
This function split_table_by_group looks good, dont think we should add the compatibility shim , so can get rid of split_table_by_group_max_bytes(
|
|
||
| def test_interleaved_lance_reader_validates_required_fields() -> None: | ||
| with pytest.raises(ValueError, match="omit required columns"): | ||
| InterleavedLanceReader(path="example.lance", fields=["sample_id"]).decompose() |
There was a problem hiding this comment.
codex: Can we test the owning stage directly and verify that the error identifies every missing field? Please rename this to `test_interleaved_lance_reader_stage_reports_missing_required_fields.
Something like:
def test_interleaved_lance_reader_stage_reports_missing_required_fields() -> None:
fields = ["sample_id"]
missing = sorted(InterleavedBatch.REQUIRED_COLUMNS - set(fields))
with pytest.raises(ValueError) as exc_info:
InterleavedLanceReaderStage(fields=fields)
assert str(exc_info.value) == (
f"InterleavedLanceReaderStage fields are missing required columns: {missing}"
)
| ["doc-c", "doc-c"], | ||
| ] | ||
| batches = result if isinstance(result, list) else [result] | ||
| assert all(batch._stage_perf is not task._stage_perf for batch in batches) |
There was a problem hiding this comment.
codex: Since task._stage_perf is empty, this would still pass if the reader discarded its contents and created unrelated empty lists. Can we seed the task with a performance entry and assert that every output preserves it while owning a distinct list?
| dataset_path = tmp_path / "version.lance" | ||
| rows = [_row("doc-a", 0, "text", "a0"), _row("doc-a", 1, "image")] | ||
| _write_interleaved_dataset(dataset_path, rows) | ||
| version = lance.dataset(str(dataset_path)).version |
There was a problem hiding this comment.
codex: The requested version is also the current dataset version, so this test passes even if read_kwargs["version"] is ignored.
Can we write distinguishable old and new dataset versions, request the old version through InterleavedLanceReader, and assert both task.version == old_version and that only the old rows are returned? This would also match the version-history coverage in test_lance_reader_uses_task_version_over_read_kwargs.
- Introduced `InterleavedLanceReader` and `InterleavedLanceReaderStage` for handling Lance-backed interleaved datasets. - Updated `__init__.py` files to include new classes in the public API. - Added tests for the new reader functionality, ensuring validation of required fields and correct processing of interleaved batches. Signed-off-by: [Ao Tang] <aot@nvidia.com> Signed-off-by: Ao Tang <aot@nvidia.com>
…a Updates Signed-off-by: Ao Tang <aot@nvidia.com>
Signed-off-by: Ao Tang <aot@nvidia.com>
- Introduced lazy loading for `InterleavedLanceReader` and `InterleavedLanceReaderStage` to optimize imports. - Updated schema alignment logic with a new `align_interleaved_table` function for better handling of interleaved Arrow tables. - Improved error handling in `__getattr__` for dynamic attribute access in module imports. - Refactored metadata handling in `LanceReaderStage` to streamline dataset reading and metadata extraction. Signed-off-by: Ao Tang <aot@nvidia.com>
…improve error handling Signed-off-by: Ao Tang <aot@nvidia.com>
…ytes with split_table_by_group for consistency Signed-off-by: Ao Tang <aot@nvidia.com>
|
/ok-to-test |
VibhuJawa
left a comment
There was a problem hiding this comment.
LGTM, thanks for the work on this
Summary
Adds an
InterleavedLanceReaderfor row-wise interleaved multimodal Lance datasets.The reader builds on Curator's existing
LancePartitioningStageandLanceReaderStage, then splits each resulting Arrow table into validatedInterleavedBatchoutputs while preserving row order andsample_iddocument boundaries.
Changes
nemo_curator.stages.interleaved.lance.InterleavedLanceReaderInterleavedLanceReaderStageLanceReaderStage.read_task()for:max_batch_bytesmax_batch_rowsfragment_idsfragments_per_partitionto the same
sample_idInterleavedBatchfields and rejects nullsample_idvalues
Tests
Added unit coverage for:
sample_idmax_output_rowsbehavior without emitting partial samples