Skip to content

feat(interleaved): add InterleavedParquetReader and InterleavedWebdatasetWriter - #1657

Merged
VibhuJawa merged 25 commits into
NVIDIA-NeMo:mainfrom
VibhuJawa:feat/interleaved-parquet-reader-wds-writer
Apr 3, 2026
Merged

feat(interleaved): add InterleavedParquetReader and InterleavedWebdatasetWriter#1657
VibhuJawa merged 25 commits into
NVIDIA-NeMo:mainfrom
VibhuJawa:feat/interleaved-parquet-reader-wds-writer

Conversation

@VibhuJawa

@VibhuJawa VibhuJawa commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

feat(interleaved): add InterleavedParquetReader and InterleavedWebdatasetWriter

Overview

Completes the interleaved IO round-trip by adding the two missing directions:

Parquet  ──read──►  InterleavedBatch  ──write──►  WDS tar

With the existing WDS reader and Parquet writer on main, all four IO combinations are now supported:

WDS tar  ⇄  InterleavedBatch  ⇄  Parquet

New classes

Class File Description
InterleavedParquetReaderStage io/readers/parquet.py Reads Parquet → InterleavedBatch
InterleavedParquetReader io/reader.py Composite stage (FilePartitioning + ReaderStage)
InterleavedWebdatasetWriterStage io/writers/webdataset.py Writes InterleavedBatch → MINT-1T-style tar shards

Key design decisions

InterleavedParquetReaderStage

  • fields= tuple selects passthrough columns — consistent with WDS reader; reserved columns always read; missing columns null-filled in a single pass
  • pq.read_schema() per file for push-down column projection (avoids reading unnecessary data from disk/S3)
  • max_batch_bytes splitting via split_table_by_group_max_bytes; each split's source_files lists only its contributing parquet files
  • _source_files_for_split() moved to BaseInterleavedReader so both WDS and Parquet readers share per-split lineage tracking

InterleavedWebdatasetWriterStage

  • Overrides _write_dataframe(); base class handles deterministic naming, materialization, schema alignment, and process() orchestration
  • urllib.parse.quote(sample_id, safe="") key escaping — injective, roundtrip-safe via sample_id_field="sample_id" on read
  • df.groupby("sample_id", sort=False) — O(n) single pass instead of O(n×m) per-sample filter
  • Positions may have gaps; output arrays sized max_pos + 1 with None at gaps (WDS reader skips None)
  • Supported modalities: metadata, text, image — any other raises ValueError at write time

Benchmarks

All four IO paths benchmarked end-to-end on 80 local NVMe shards (6,818 samples, MINT-1T PDF data) with an aspect-ratio filter applied. Full results and reproduction instructions: benchmark gist.

Path Wall-clock Samples/sec Input Output
WDS → Parquet 76.8 s 88.8 9.9 GB (80 tars) 3.84 GB (57,713 rows)
WDS → WDS 75.4 s 90.4 9.9 GB (80 tars) 4.06 GB (6,818 samples)
PQ → Parquet 15.7 s 435.0 6.0 GB (80 pq) 3.83 GB (57,713 rows)
PQ → WDS 18.5 s 368.4 6.0 GB (80 pq) 4.06 GB (6,818 samples)

Key takeaway: Parquet-sourced paths are ~5× faster than WDS-sourced paths. The bottleneck is entirely in the WDS reader (396 s cumulative stage time across 80 shards vs 14 s for PQ) due to sequential tar scanning and network image fetches. Filter and writer costs are near-identical across formats.

Stage-level breakdown (cumulative across 80 tasks, 16 workers)

Stage WDS→PQ WDS→WDS PQ→PQ PQ→WDS
Reader 396.7 s 395.3 s 13.4 s 14.1 s
Aspect ratio filter 35.1 s 34.2 s 40.5 s 40.6 s
Writer 12.0 s 45.4 s 13.1 s 48.1 s

Environment: Python 3.10.12 · Ray 2.54.0 · PyArrow 19.0.1 · 16-core CPU · NVMe · materialize_on_read=True

Tests (15 new)

Parquet reader (test_multimodal_reader.py)

  • test_parquet_reader_roundtrip — write then read back; sample_id, modality, text_content match
  • test_parquet_reader_missing_columns_filled_with_null — 3-column file; all reserved cols null-filled
  • test_parquet_reader_fields_subsetfields= selects extra cols; reserved always present
  • test_parquet_reader_fields_null_fill_missingfields=("nonexistent",) → null column, no error
  • test_parquet_reader_max_batch_bytes_splits — 2 files → 2 splits; each lists only its contributing file in source_files
  • test_parquet_reader_empty_file — 0-row parquet → empty batch, correct schema, no crash
  • test_parquet_reader_composite_decomposedecompose()[FilePartitioningStage, InterleavedParquetReaderStage]

WDS writer (test_multimodal_writer.py)

  • test_escape_key_encodes_special_charsa/b:ca%2Fb%3Ac
  • test_ext_from_content_type_known / _fallback — MIME → extension mapping
  • test_wds_writer_roundtrip — write then read back via InterleavedWebdatasetReaderStage
  • test_wds_writer_text_only_sample — no image rows; JSON has all-None images list
  • test_wds_writer_unsupported_modality_raisesmodality="video"ValueError
  • test_wds_writer_key_escaping — special-char sample_id; roundtrip recovers original id
  • test_wds_writer_passthrough_columns_in_json — extra url col in metadata row → appears in JSON payload
  • test_wds_writer_null_binary_skips_member — null binary_content → no image tar member written
  • test_wds_writer_deterministic_filename — same source_files + task_id → same output filename

VibhuJawa and others added 12 commits March 24, 2026 06:38
…e-class fixes, and bug fixes

Splits out the foundation layer from PR NVIDIA-NeMo#1570 as a standalone PR:

Schema utilities (utils/schema.py — new):
- reconcile_schema(): canonical types for reserved columns, passthrough columns preserved
- align_table(): pad, reorder, cast tables to a target schema; safe=True for passthrough
  columns to surface overflow errors instead of silently corrupting data (B5 fix)

Reader base (readers/base.py):
- Add schema= and schema_overrides= parameters for strict schema alignment
- _resolve_schema() helper: schema > schema_overrides > INTERLEAVED_SCHEMA priority
- Warn (not silently ignore) when both schema= and schema_overrides= are provided (C5)
- Fix return type annotation: -> pa.Schema | None (C4)
- Preserve nullable=False from INTERLEAVED_SCHEMA when applying schema_overrides (C6)

Writer base (writers/base.py):
- Add schema= and schema_overrides= for output alignment (same API as reader)
- Add on_materialize_error= parameter: "error"|"warn"|"drop_row"|"drop_sample"
- _materialize_dataframe(): apply error policy after fetch; guard binary_content column
- Make _write_dataframe() non-abstract (default pass body) to allow WDS-style overrides (D3)

Materialization (utils/materialization.py):
- _build_global_range_index(): group paths by filesystem object so mixed-backend batches
  work correctly (B3 fix)

WebDataset reader (readers/webdataset.py):
- Rename tiff result variable to tiff_frame to remove variable shadowing (D5 fix)

Parquet reader (readers/parquet.py):
- Guard pq.read_schema() call behind if self.fields is not None to avoid N+1 schema
  reads in the common case (C1 fix)

Tests:
- test_base_writer.py (new): 9 tests covering on_materialize_error modes, schema
  alignment, and write_data orchestration
- test_materialization.py: 4 new tests for multi-backend path grouping (B3)
- test_multimodal_core.py: 4 new align_table tests for safe-cast behaviour (B5)
- test_multimodal_reader.py: schema_overrides and nullable preservation tests (C5/C6)
- conftest.py: additional helpers for new test patterns

Note: InterleavedParquetReader, InterleavedParquetReaderStage, and
InterleavedWebdatasetWriterStage are intentionally excluded from this branch;
they will be added in the follow-up PR on feat/interleaved-io-readers-writers.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…ad code, pre-compute write_kwargs

Move resolve_schema to utils/schema.py:
- Removes cross-module readers->writers private import (_resolve_schema was
  defined in readers/base.py and imported by writers/base.py)
- Renamed _resolve_schema to resolve_schema (public shared utility)
- Returns None when both args are None instead of unreachable ValueError

writers/base.py:
- Pre-compute _effective_write_kwargs in __post_init__ (filters storage_options,
  forces index=False) so write_data() no longer copies the dict on every task
- Remove noisy section-header comments

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…oundation

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>

# Conflicts:
#	.github/workflows/config/.secrets.baseline
This override was added to work around ray[llm]'s unconditional nixl dep on ARM
but is not part of the interleaved IO foundation work.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
Reverts the removal — nixl-cu12 override is needed to work around
ray[llm]'s unconditional nixl dep on ARM.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
source_id_field was only used as a passthrough exclusion hint — the same
effect is achievable via the fields= parameter. Having both source_id_field
and sample_id_field was confusing with overlapping names. Removing
source_id_field simplifies the API with no loss of functionality.

Update all tests, benchmarking scripts, tutorials, and README to remove
all references to source_id_field.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
- align_table: remove redundant reconcile_schema(target) call; the
  safe/unsafe cast logic already handles large<->small conversions for
  reserved columns, and re-reconciling a user-provided schema would
  silently override intentional type overrides
- _scatter_range_blobs: restore specific tuple type hints
  (list[tuple[str,int,int]]) lost when range key shape changed from
  (offset,size) to (fs_path,offset,size)
- reconcile_schema: add comment explaining why dictionary-encoded
  passthrough columns are unwrapped to their value type

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…der metadata

- Fix interleaved_data_quickstart.ipynb:
  - max_aspect_ratio 1.0 -> 2.0 (was dropping 180 rows instead of 22)
  - write filtered_batch instead of unfiltered batch in Step 5
- Fix InterleavedWebdatasetReaderStage.process(): populate source_files
  in output metadata from task.data so the writer can use a deterministic
  hash-based filename instead of a UUID

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Signed-off-by: Vibhu Jawa <vibhujawa@gmail.com>
The interleaved_data_quickstart.ipynb outputs contain base64-encoded PNG
images and hex sha256 hashes flagged by detect-secrets. These are all
false positives from inline cell output, not real credentials.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
When max_batch_bytes splits a single tar into N batches, each batch
previously got the same source_files=[tar_path], making splits
indistinguishable by source_files alone.

Append '::split_{idx:05d}' to each path when there are multiple splits,
so each batch's source_files uniquely identifies both the source tar and
its position within the split sequence. Single-split output is unchanged.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…t reader

When max_batch_bytes causes the reader to emit multiple InterleavedBatch
splits, each split's source_files metadata now lists only the tar files
that actually contributed rows to that split, instead of all source tars.

Also optimises the hot path: sample_id is recorded once per member
(from the first/metadata row) rather than iterating every row, and
unique() deduplicates the sample_id column in Arrow before conversion.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
@VibhuJawa

Copy link
Copy Markdown
Contributor Author

/claude review

Comment thread tests/stages/interleaved/test_materialization.py Outdated
Comment thread nemo_curator/stages/interleaved/README.md
- Add test_schema_utils.py: 8 tests for resolve_schema, reconcile_schema, align_table
- Add 5 tests in test_multimodal_core.py: aspect ratio edge cases, iter_materialized_bytes paths
- Add inputs/outputs contract test in test_base_writer.py
- Remove dead metadata-propagation code from utils/schema.py

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…oundation

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…ebdatasetWriterStage

Completes the interleaved IO round-trip by adding the two missing directions:

- InterleavedParquetReaderStage: reads Parquet → InterleavedBatch with
  fields= tuple for passthrough column selection, null-fill for missing
  columns, max_batch_bytes splitting, and per-split source_files tracking.
- InterleavedParquetReader: composite stage wrapping FilePartitioningStage
  + InterleavedParquetReaderStage.
- InterleavedWebdatasetWriterStage: writes InterleavedBatch → MINT-1T-style
  tar shards, reconstructing texts/images lists from row-based data with
  percent-encoded tar member keys for safe roundtrip via sample_id_field.
- _source_files_for_split() moved to BaseInterleavedReader so both
  WDS and Parquet readers share the per-split source tracking logic.

Tests: 15 new tests covering roundtrip, missing columns, fields null-fill,
max_batch_bytes splits, key escaping, passthrough columns, and all
on_materialize_error modes.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…etReader

Aligns naming with the Interleaved* prefix convention used by all other
stages in this module. InterleavedParquetReader was already correctly named;
this makes the WebDataset composite reader consistent.

Also updates README.md architecture diagram to show both composite readers
(InterleavedWebdatasetReader and InterleavedParquetReader) side-by-side,
reflecting the full round-trip pipeline.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…chmark

- Add collect_parquet_input_metrics and collect_wds_input_metrics to utils.py
- Add shared _accumulate_modality_counts helper to eliminate duplicated loop
- Collect input metrics before pipeline.run() in multimodal_mint1t_benchmark.py
- Add run_mint1t_benchmark_all.sh runner for all 4 paths to /raid/vjawa/benchamrk_ouput_dir

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
…uet-reader-wds-writer

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
Add _count_wds_samples helper that counts .json members across tars
(one per sample). Used by collect_wds_input_metrics and
collect_wds_output_metrics so WDS paths report sample counts and
throughput alongside parquet paths.

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
Comment thread nemo_curator/stages/interleaved/io/readers/parquet.py Outdated
Comment thread nemo_curator/stages/interleaved/io/writers/webdataset.py Outdated
Comment thread benchmarking/scripts/utils.py Outdated
- Replace 4 narrow input/output collect functions with 2 format-scoped
  functions (collect_interleaved_parquet_metrics, collect_interleaved_wds_metrics)
  using neutral keys; caller adds input_/output_ prefix via dict comprehension
- Add _resolve_paths helper to unify single-file vs directory resolution
- Track num_samples (distinct sample_ids), num_metadata, num_texts, num_images,
  materialize_error_count in parquet metrics; text/image counts in WDS metrics
- Add validate_wds_ordering(tar_path) single-file spot-checker with bunching
  detection, position collision, and missing image validation
- Fix _check_wds_sample return to tuple[int, bool] | None (None replaces -1)
- Add _validate_output helper with clean bool | None semantics
- Add wds_valid and throughput_samples_per_sec to benchmark metrics output

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>
@VibhuJawa VibhuJawa mentioned this pull request Apr 3, 2026
3 tasks
… iterrows with itertuples

- InterleavedParquetReaderStage and InterleavedWebdatasetReaderStage now cache
  resolve_storage_options() in __post_init__ instead of calling it on every
  process() invocation; read_kwargs is immutable after construction so the
  result never changes across tasks
- Replace iterrows() with itertuples(index=False) in WDS writer _write_sample;
  all accessed columns (position, modality, text_content, content_type,
  binary_content) are RESERVED_COLUMNS guaranteed present after _align_output();
  ~7%% process-time improvement measured on single-shard benchmark

Signed-off-by: Vibhu Jawa <vjawa@nvidia.com>

@abhinavg4 abhinavg4 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants