Skip to content

[data] Add write_delta release benchmark - #64925

Open
abhishekverma-ray wants to merge 2 commits into
ray-project:masterfrom
abhishekverma-ray:stack3/pr2-write-delta-benchmark
Open

[data] Add write_delta release benchmark#64925
abhishekverma-ray wants to merge 2 commits into
ray-project:masterfrom
abhishekverma-ray:stack3/pr2-write-delta-benchmark

Conversation

@abhishekverma-ray

Copy link
Copy Markdown
Contributor

Summary

Adds a write_delta counterpart to the existing write_parquet release benchmark,
reusing the same real dataset and harness rather than inventing new synthetic-data
infrastructure:

  • read_and_consume_benchmark.py: adds a --write-delta consume option, mirroring
    the existing --write (write_parquet) option exactly — same read path, same
    Benchmark/BenchmarkMetric reporting, just a different write call.
  • release_data_tests.yaml: registers a write_delta test entry alongside
    write_parquet, reading the same TPCH SF1000 lineitem source
    (s3://ray-benchmark-data/tpch/parquet/sf1000/lineitem), so the two are a direct,
    apples-to-apples Parquet-vs-Delta write comparison on identical input data.
  • byod_install_deltalake.sh: new post-build script installing deltalake==1.5.0
    (matching the version pinned in python/requirements_compiled*.txt), needed
    because — unlike parquet/image/tfrecords — deltalake isn't in the base
    release-test image. Mirrors byod_install_pyiceberg.sh's exact shape.

Test plan

  • python -m py_compile on the modified script — passes.
  • ruff check / black --check (repo-pinned versions) on the modified script —
    clean.
  • shellcheck (via pre-commit) on the new byod script — clean.
  • YAML: loaded release_data_tests.yaml with yaml.safe_load and confirmed the new
    write_delta entry parses correctly and appears exactly once.
  • End-to-end local smoke test (no S3/cluster access): wrote a small local Parquet
    source, invoked the modified script's main() directly with a constructed
    args.Namespace (--format parquet --write-delta equivalent) against a local
    write_delta destination, confirmed the Benchmark harness times and reports the
    run, and confirmed the resulting Delta table reads back correctly via
    ray.data.read_delta(...).

AI assistance disclosure

This PR was developed with AI assistance (Claude). All changes were reviewed and are
defended by the submitting human.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces support for writing Ray Datasets to Delta Lake tables by implementing a new DeltaDatasink and exposing it via Dataset.write_delta. It includes comprehensive unit tests, benchmark integration, and release test configurations. The review feedback highlights a critical path-resolution bug when writing to cloud storage (such as S3) due to mismatched leading slashes, and recommends updating the type hint for ray_remote_args to be PEP 484 compliant.

Comment thread python/ray/data/_internal/datasource/delta_datasink.py Outdated
Comment thread python/ray/data/dataset.py Outdated
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 3 times, most recently from d420387 to 3c8305a Compare July 22, 2026 06:48
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
abhishekverma-ray added a commit to abhishekverma-ray/ray-dev-abhishek that referenced this pull request Jul 22, 2026
Ports a small, independently-authored prototype write_delta implementation
for a side-by-side comparison against the standalone write_delta design
on stack2/pr1-write-delta-core. Dataset.write_delta and the DeltaDatasink
class are kept as close to the shared original as possible -- deliberate
fixes are called out below; everything else is unmodified -- so the diff
between the two stacks reflects genuine design differences, not
incidental ones.

Supports SaveMode.APPEND and SaveMode.OVERWRITE (a prototype scope, by
design): workers write Parquet files via pyarrow.dataset.write_dataset
and report AddAction metadata; the driver commits all files in one Delta
transaction via the deltalake library, creating the table if it doesn't
exist. Also supports partition_by (Hive-style), storage_options, and
optional name/description table metadata.

Bug fix 1 (found while building a write_delta release benchmark on top of
this branch): a Dataset sourced from a file-listing-based read (e.g.
read_parquet) failed every write with "Schema error: Invalid data type
for Delta Lake: Null". Root cause: on_write_start's schema argument can
be Ray Data's internal file-listing metadata schema (e.g. a null-typed
__file_chunk_metadata column) rather than the dataset's actual row
schema, and _resolve_schema unconditionally preferred that captured
schema over the real schema workers observed while writing. Fixed by
having write() return the schema of the Arrow table(s) it actually wrote,
and having _resolve_schema prefer that over the driver-side capture,
which now serves only its originally-intended purpose: a fallback for
a genuinely empty dataset with no worker-observed schema at all.

Addresses review feedback from cursor[bot] and gemini-code-assist[bot]
on both PR ray-project#64923 (this branch) and PR ray-project#64925 (the release-benchmark
branch stacked on top, whose cumulative diff includes this file), across
four review rounds:
- Worker tasks now honor storage_options when building their Parquet
  write filesystem (previously only the driver's DeltaTable/
  create_table_with_add_actions calls received it, so a cloud write with
  explicit credentials could succeed on the driver's commit while every
  worker's file write silently used different, ambient credentials, or
  failed outright).
- An empty-dataset APPEND against an already-existing table is now a
  pure no-op -- no schema resolution, no empty commit added to the Delta
  log. APPEND against a not-yet-existing table with no resolvable schema
  still raises (a brand-new table can't be created without knowing its
  shape).
- An empty-dataset OVERWRITE against an already-existing table now falls
  back to that table's own current schema instead of raising: truncating
  an existing table is well-defined even when the incoming write has no
  rows (and therefore no worker-observed schema) to resolve one from.
- Partition value/key parsing: a NULL partition value (written by
  PyArrow's Hive writer as the literal directory name
  "__HIVE_DEFAULT_PARTITION__") now round-trips back as Python None
  instead of that literal string; partition column names are now
  URL-unquoted the same way values already were.
- write() now unifies the schema across every non-empty block a single
  task writes, instead of keeping only the last block's schema (a task
  can receive multiple blocks with promotable but non-identical
  schemas).
- The worker-side Azure filesystem helper now reads the SAS token from
  AZURE_STORAGE_SAS_TOKEN, matching ray.data.catalog's vended-credential
  key, instead of a different name that would never match.
- The worker-side GCS filesystem helper no longer leaves
  GOOGLE_APPLICATION_CREDENTIALS mutated after use -- the env var is a
  process-wide value that could otherwise leak a task's credential path
  to other tasks reusing the same warm Ray worker.
- The written-file-to-table-root relative path computation now strips
  leading slashes from both sides before computing the relative path:
  some cloud filesystems can return a table root without a leading slash
  from FileSystem.from_uri while written-file paths from that same
  filesystem still carry one (verified against a real S3-compatible
  server), and that mismatch previously made the relative path walk
  upward instead of resolving correctly, corrupting the Delta log.
- The worker-side S3 filesystem helper now also accepts AWS_DEFAULT_REGION
  (a common boto-style key) as a fallback when AWS_REGION isn't set,
  matching what the driver's commit path (which forwards the full
  storage_options dict to deltalake) can already honor.
- Dataset.write_delta's ray_remote_args parameter is now typed
  Optional[Dict[str, Any]], matching its actual default of None.

Adds a new test suite (test_write_delta.py) covering what this
implementation actually supports: APPEND/OVERWRITE round-trips,
unsupported-mode rejection, single- and multi-column partitioning,
multi-block writes (including cross-block schema unification within a
single task), table name/description metadata, the empty-dataset error
path (and the empty-APPEND-against-existing-table and
empty-OVERWRITE-against-existing-table no-op/truncate paths),
storage_options passthrough to both the commit calls and the worker
filesystem (S3, Azure, and GCS, including the GCS env-var restore
behavior and the AWS_DEFAULT_REGION fallback), null/URL-encoded
partition value and key handling, a simulated cross-filesystem
leading-slash mismatch in the written-file relative path computation,
and a regression test for the read_parquet-sourced schema bug above.

One pyproject.toml per-file-ignore added (F401) to keep the ported file
lint-clean without editing it for that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Abhishek Verma <abhishek.verma@anyscale.com>
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch from 3c8305a to 6ef6d60 Compare July 22, 2026 07:07
@ray-gardener ray-gardener Bot added data Ray Data-related issues release-test release test labels Jul 22, 2026
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch from 6ef6d60 to de0eaf8 Compare July 22, 2026 08:28
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
abhishekverma-ray added a commit to abhishekverma-ray/ray-dev-abhishek that referenced this pull request Jul 22, 2026
Ports a small, independently-authored prototype write_delta implementation
for a side-by-side comparison against the standalone write_delta design
on stack2/pr1-write-delta-core. Dataset.write_delta and the DeltaDatasink
class are kept as close to the shared original as possible -- deliberate
fixes are called out below; everything else is unmodified -- so the diff
between the two stacks reflects genuine design differences, not
incidental ones.

Supports SaveMode.APPEND and SaveMode.OVERWRITE (a prototype scope, by
design): workers write Parquet files via pyarrow.dataset.write_dataset
and report AddAction metadata; the driver commits all files in one Delta
transaction via the deltalake library, creating the table if it doesn't
exist. Also supports partition_by (Hive-style), storage_options, and
optional name/description table metadata.

Bug fix 1 (found while building a write_delta release benchmark on top of
this branch): a Dataset sourced from a file-listing-based read (e.g.
read_parquet) failed every write with "Schema error: Invalid data type
for Delta Lake: Null". Root cause: on_write_start's schema argument can
be Ray Data's internal file-listing metadata schema (e.g. a null-typed
__file_chunk_metadata column) rather than the dataset's actual row
schema, and _resolve_schema unconditionally preferred that captured
schema over the real schema workers observed while writing. Fixed by
having write() return the schema of the Arrow table(s) it actually wrote,
and having _resolve_schema prefer that over the driver-side capture,
which now serves only its originally-intended purpose: a fallback for
a genuinely empty dataset with no worker-observed schema at all.

Bug fix 2 (found from an actual CI run of the release benchmark, build
101454): a large distributed write (TPCH SF1000, ~1000 concurrent write
tasks against a brand-new table root) failed with "AWS Error SLOW_DOWN
during PutObject operation" while writing Parquet files, before any
commit was attempted. Root cause: pyarrow.dataset.write_dataset defaults
to create_dir=True, which for an S3-backed filesystem issues a PutObject
to a single marker key representing the table root directory (verified
empirically -- S3 has no real directories, so this is purely a
convention). With ~1000 tasks all racing to create that identical key
against a cold prefix, the concurrent-PUT burst triggers AWS throttling
for no functional benefit (object writes don't need a pre-existing
directory marker). Fixed by passing create_dir=False specifically when
the resolved filesystem is a cloud object store (S3FileSystem,
GcsFileSystem, AzureFileSystem); local (and other real) filesystems
still get create_dir=True since they genuinely need the directory to
exist (verified separately that create_dir=False raises
FileNotFoundError against a local path that doesn't exist yet).

Addresses review feedback from cursor[bot] and gemini-code-assist[bot]
on both PR ray-project#64923 (this branch) and PR ray-project#64925 (the release-benchmark
branch stacked on top, whose cumulative diff includes this file), across
four review rounds:
- Worker tasks now honor storage_options when building their Parquet
  write filesystem (previously only the driver's DeltaTable/
  create_table_with_add_actions calls received it, so a cloud write with
  explicit credentials could succeed on the driver's commit while every
  worker's file write silently used different, ambient credentials, or
  failed outright).
- An empty-dataset APPEND against an already-existing table is now a
  pure no-op -- no schema resolution, no empty commit added to the Delta
  log. APPEND against a not-yet-existing table with no resolvable schema
  still raises (a brand-new table can't be created without knowing its
  shape).
- An empty-dataset OVERWRITE against an already-existing table now falls
  back to that table's own current schema instead of raising: truncating
  an existing table is well-defined even when the incoming write has no
  rows (and therefore no worker-observed schema) to resolve one from.
- Partition value/key parsing: a NULL partition value (written by
  PyArrow's Hive writer as the literal directory name
  "__HIVE_DEFAULT_PARTITION__") now round-trips back as Python None
  instead of that literal string; partition column names are now
  URL-unquoted the same way values already were.
- write() now unifies the schema across every non-empty block a single
  task writes, instead of keeping only the last block's schema (a task
  can receive multiple blocks with promotable but non-identical
  schemas).
- The worker-side Azure filesystem helper now reads the SAS token from
  AZURE_STORAGE_SAS_TOKEN, matching ray.data.catalog's vended-credential
  key, instead of a different name that would never match.
- The worker-side GCS filesystem helper no longer leaves
  GOOGLE_APPLICATION_CREDENTIALS mutated after use -- the env var is a
  process-wide value that could otherwise leak a task's credential path
  to other tasks reusing the same warm Ray worker.
- The written-file-to-table-root relative path computation now strips
  leading slashes from both sides before computing the relative path:
  some cloud filesystems can return a table root without a leading slash
  from FileSystem.from_uri while written-file paths from that same
  filesystem still carry one (verified against a real S3-compatible
  server), and that mismatch previously made the relative path walk
  upward instead of resolving correctly, corrupting the Delta log.
- The worker-side S3 filesystem helper now also accepts AWS_DEFAULT_REGION
  (a common boto-style key) as a fallback when AWS_REGION isn't set,
  matching what the driver's commit path (which forwards the full
  storage_options dict to deltalake) can already honor.
- Dataset.write_delta's ray_remote_args parameter is now typed
  Optional[Dict[str, Any]], matching its actual default of None.

Adds a new test suite (test_write_delta.py) covering what this
implementation actually supports: APPEND/OVERWRITE round-trips,
unsupported-mode rejection, single- and multi-column partitioning,
multi-block writes (including cross-block schema unification within a
single task), table name/description metadata, the empty-dataset error
path (and the empty-APPEND-against-existing-table and
empty-OVERWRITE-against-existing-table no-op/truncate paths),
storage_options passthrough to both the commit calls and the worker
filesystem (S3, Azure, and GCS, including the GCS env-var restore
behavior and the AWS_DEFAULT_REGION fallback), null/URL-encoded
partition value and key handling, a simulated cross-filesystem
leading-slash mismatch in the written-file relative path computation, a
regression test for the read_parquet-sourced schema bug above, and a
regression test that create_dir is only skipped for cloud object-store
filesystems.

One pyproject.toml per-file-ignore added (F401) to keep the ported file
lint-clean without editing it for that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Abhishek Verma <abhishek.verma@anyscale.com>
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch from de0eaf8 to 152518f Compare July 22, 2026 09:23
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 2 times, most recently from 4016849 to e74c587 Compare July 24, 2026 05:04
filesystem=filesystem,
format="parquet",
partitioning=partitioning,
basename_template=f"part-{task_idx:05d}-{write_uuid}-{{i}}.parquet",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ignores job-level write uuid

Medium Severity

Each _write_parquet call generates a new uuid.uuid4() for the basename instead of using the job-level write_uuid Ray Data injects into TaskContext.kwargs. Retried write tasks therefore land under new object keys, leaving orphaned Parquet files on storage and preventing existing_data_behavior="overwrite_or_ignore" from reusing the same paths on retry.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e74c587. Configure here.

@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 3 times, most recently from d201482 to ca5ed49 Compare July 25, 2026 15:00
Comment thread python/ray/data/_internal/datasource/delta_datasink.py
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 2 times, most recently from 3a0fa63 to 15da910 Compare July 25, 2026 15:38

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 15da910. Configure here.

Comment thread python/ray/data/_internal/datasource/delta_datasink.py Outdated
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 3 times, most recently from 12b1451 to 0bf3b90 Compare July 25, 2026 16:15
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch 2 times, most recently from 7c4fb1c to 986ee6c Compare July 27, 2026 07:31
abhishekverma-ray added a commit to abhishekverma-ray/ray-dev-abhishek that referenced this pull request Jul 27, 2026
Ports a small, independently-authored prototype write_delta implementation
for a side-by-side comparison against the standalone write_delta design
on stack2/pr1-write-delta-core. Dataset.write_delta and the DeltaDatasink
class are kept as close to the shared original as possible -- deliberate
fixes are called out below; everything else is unmodified -- so the diff
between the two stacks reflects genuine design differences, not
incidental ones.

Supports SaveMode.APPEND and SaveMode.OVERWRITE (a prototype scope, by
design): workers write Parquet files via pyarrow.dataset.write_dataset
and report AddAction metadata; the driver commits all files in one Delta
transaction via the deltalake library, creating the table if it doesn't
exist. Also supports partition_by (Hive-style), storage_options, and
optional name/description table metadata.

Bug fix 1 (found while building a write_delta release benchmark on top of
this branch): a Dataset sourced from a file-listing-based read (e.g.
read_parquet) failed every write with "Schema error: Invalid data type
for Delta Lake: Null". Root cause: on_write_start's schema argument can
be Ray Data's internal file-listing metadata schema (e.g. a null-typed
__file_chunk_metadata column) rather than the dataset's actual row
schema, and _resolve_schema unconditionally preferred that captured
schema over the real schema workers observed while writing. Fixed by
having write() return the schema of the Arrow table(s) it actually wrote,
and having _resolve_schema prefer that over the driver-side capture,
which now serves only its originally-intended purpose: a fallback for
a genuinely empty dataset with no worker-observed schema at all.

Bug fix 2 (found from an actual CI run of the release benchmark, build
101454): a large distributed write (TPCH SF1000, ~1000 concurrent write
tasks against a brand-new table root) failed with "AWS Error SLOW_DOWN
during PutObject operation" while writing Parquet files, before any
commit was attempted. Root cause: pyarrow.dataset.write_dataset defaults
to create_dir=True, which for an S3-backed filesystem issues a PutObject
to a single marker key representing the table root directory (verified
empirically -- S3 has no real directories, so this is purely a
convention). With ~1000 tasks all racing to create that identical key
against a cold prefix, the concurrent-PUT burst triggers AWS throttling
for no functional benefit (object writes don't need a pre-existing
directory marker). Fixed by passing create_dir=False specifically when
the resolved filesystem is a cloud object store (S3FileSystem,
GcsFileSystem, AzureFileSystem); local (and other real) filesystems
still get create_dir=True since they genuinely need the directory to
exist (verified separately that create_dir=False raises
FileNotFoundError against a local path that doesn't exist yet).

Addresses review feedback from cursor[bot] and gemini-code-assist[bot]
on both PR ray-project#64923 (this branch) and PR ray-project#64925 (the release-benchmark
branch stacked on top, whose cumulative diff includes this file), across
four review rounds:
- Worker tasks now honor storage_options when building their Parquet
  write filesystem (previously only the driver's DeltaTable/
  create_table_with_add_actions calls received it, so a cloud write with
  explicit credentials could succeed on the driver's commit while every
  worker's file write silently used different, ambient credentials, or
  failed outright).
- An empty-dataset APPEND against an already-existing table is now a
  pure no-op -- no schema resolution, no empty commit added to the Delta
  log. APPEND against a not-yet-existing table with no resolvable schema
  still raises (a brand-new table can't be created without knowing its
  shape).
- An empty-dataset OVERWRITE against an already-existing table now falls
  back to that table's own current schema instead of raising: truncating
  an existing table is well-defined even when the incoming write has no
  rows (and therefore no worker-observed schema) to resolve one from.
- Partition value/key parsing: a NULL partition value (written by
  PyArrow's Hive writer as the literal directory name
  "__HIVE_DEFAULT_PARTITION__") now round-trips back as Python None
  instead of that literal string; partition column names are now
  URL-unquoted the same way values already were.
- write() now unifies the schema across every non-empty block a single
  task writes, instead of keeping only the last block's schema (a task
  can receive multiple blocks with promotable but non-identical
  schemas).
- The worker-side Azure filesystem helper now reads the SAS token from
  AZURE_STORAGE_SAS_TOKEN, matching ray.data.catalog's vended-credential
  key, instead of a different name that would never match.
- The worker-side GCS filesystem helper no longer leaves
  GOOGLE_APPLICATION_CREDENTIALS mutated after use -- the env var is a
  process-wide value that could otherwise leak a task's credential path
  to other tasks reusing the same warm Ray worker.
- The written-file-to-table-root relative path computation now strips
  leading slashes from both sides before computing the relative path:
  some cloud filesystems can return a table root without a leading slash
  from FileSystem.from_uri while written-file paths from that same
  filesystem still carry one (verified against a real S3-compatible
  server), and that mismatch previously made the relative path walk
  upward instead of resolving correctly, corrupting the Delta log.
- The worker-side S3 filesystem helper now also accepts AWS_DEFAULT_REGION
  (a common boto-style key) as a fallback when AWS_REGION isn't set,
  matching what the driver's commit path (which forwards the full
  storage_options dict to deltalake) can already honor.
- Dataset.write_delta's ray_remote_args parameter is now typed
  Optional[Dict[str, Any]], matching its actual default of None.

Adds a new test suite (test_write_delta.py) covering what this
implementation actually supports: APPEND/OVERWRITE round-trips,
unsupported-mode rejection, single- and multi-column partitioning,
multi-block writes (including cross-block schema unification within a
single task), table name/description metadata, the empty-dataset error
path (and the empty-APPEND-against-existing-table and
empty-OVERWRITE-against-existing-table no-op/truncate paths),
storage_options passthrough to both the commit calls and the worker
filesystem (S3, Azure, and GCS, including the GCS env-var restore
behavior and the AWS_DEFAULT_REGION fallback), null/URL-encoded
partition value and key handling, a simulated cross-filesystem
leading-slash mismatch in the written-file relative path computation, a
regression test for the read_parquet-sourced schema bug above, and a
regression test that create_dir is only skipped for cloud object-store
filesystems.

One pyproject.toml per-file-ignore added (F401) to keep the ported file
lint-clean without editing it for that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Abhishek Verma <abhishek.verma@anyscale.com>
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch from 986ee6c to 3506500 Compare July 27, 2026 08:47
abhishekverma-ray and others added 2 commits July 27, 2026 22:46
Ports a small, independently-authored prototype write_delta implementation
for a side-by-side comparison against the standalone write_delta design
on stack2/pr1-write-delta-core. Dataset.write_delta and the DeltaDatasink
class are kept as close to the shared original as possible -- deliberate
fixes are called out below; everything else is unmodified -- so the diff
between the two stacks reflects genuine design differences, not
incidental ones.

Supports SaveMode.APPEND and SaveMode.OVERWRITE (a prototype scope, by
design): workers write Parquet files via pyarrow.dataset.write_dataset
and report AddAction metadata; the driver commits all files in one Delta
transaction via the deltalake library, creating the table if it doesn't
exist. Also supports partition_by (Hive-style), storage_options, and
optional name/description table metadata.

Bug fix 1 (found while building a write_delta release benchmark on top of
this branch): a Dataset sourced from a file-listing-based read (e.g.
read_parquet) failed every write with "Schema error: Invalid data type
for Delta Lake: Null". Root cause: on_write_start's schema argument can
be Ray Data's internal file-listing metadata schema (e.g. a null-typed
__file_chunk_metadata column) rather than the dataset's actual row
schema, and _resolve_schema unconditionally preferred that captured
schema over the real schema workers observed while writing. Fixed by
having write() return the schema of the Arrow table(s) it actually wrote,
and having _resolve_schema prefer that over the driver-side capture,
which now serves only its originally-intended purpose: a fallback for
a genuinely empty dataset with no worker-observed schema at all.

Bug fix 2 (found from an actual CI run of the release benchmark, build
101454): a large distributed write (TPCH SF1000, ~1000 concurrent write
tasks against a brand-new table root) failed with "AWS Error SLOW_DOWN
during PutObject operation" while writing Parquet files, before any
commit was attempted. Root cause: pyarrow.dataset.write_dataset defaults
to create_dir=True, which for an S3-backed filesystem issues a PutObject
to a single marker key representing the table root directory (verified
empirically -- S3 has no real directories, so this is purely a
convention). With ~1000 tasks all racing to create that identical key
against a cold prefix, the concurrent-PUT burst triggers AWS throttling
for no functional benefit (object writes don't need a pre-existing
directory marker). Fixed by passing create_dir=False specifically when
the resolved filesystem is a cloud object store (S3FileSystem,
GcsFileSystem, AzureFileSystem); local (and other real) filesystems
still get create_dir=True since they genuinely need the directory to
exist (verified separately that create_dir=False raises
FileNotFoundError against a local path that doesn't exist yet).

Addresses review feedback from cursor[bot] and gemini-code-assist[bot]
on both PR ray-project#64923 (this branch) and PR ray-project#64925 (the release-benchmark
branch stacked on top, whose cumulative diff includes this file), across
four review rounds:
- Worker tasks now honor storage_options when building their Parquet
  write filesystem (previously only the driver's DeltaTable/
  create_table_with_add_actions calls received it, so a cloud write with
  explicit credentials could succeed on the driver's commit while every
  worker's file write silently used different, ambient credentials, or
  failed outright).
- An empty-dataset APPEND against an already-existing table is now a
  pure no-op -- no schema resolution, no empty commit added to the Delta
  log. APPEND against a not-yet-existing table with no resolvable schema
  still raises (a brand-new table can't be created without knowing its
  shape).
- An empty-dataset OVERWRITE against an already-existing table now falls
  back to that table's own current schema instead of raising: truncating
  an existing table is well-defined even when the incoming write has no
  rows (and therefore no worker-observed schema) to resolve one from.
- Partition value/key parsing: a NULL partition value (written by
  PyArrow's Hive writer as the literal directory name
  "__HIVE_DEFAULT_PARTITION__") now round-trips back as Python None
  instead of that literal string; partition column names are now
  URL-unquoted the same way values already were.
- write() now unifies the schema across every non-empty block a single
  task writes, instead of keeping only the last block's schema (a task
  can receive multiple blocks with promotable but non-identical
  schemas).
- The worker-side Azure filesystem helper now reads the SAS token from
  AZURE_STORAGE_SAS_TOKEN, matching ray.data.catalog's vended-credential
  key, instead of a different name that would never match.
- The worker-side GCS filesystem helper no longer leaves
  GOOGLE_APPLICATION_CREDENTIALS mutated after use -- the env var is a
  process-wide value that could otherwise leak a task's credential path
  to other tasks reusing the same warm Ray worker.
- The written-file-to-table-root relative path computation now strips
  leading slashes from both sides before computing the relative path:
  some cloud filesystems can return a table root without a leading slash
  from FileSystem.from_uri while written-file paths from that same
  filesystem still carry one (verified against a real S3-compatible
  server), and that mismatch previously made the relative path walk
  upward instead of resolving correctly, corrupting the Delta log.
- The worker-side S3 filesystem helper now also accepts AWS_DEFAULT_REGION
  (a common boto-style key) as a fallback when AWS_REGION isn't set,
  matching what the driver's commit path (which forwards the full
  storage_options dict to deltalake) can already honor.
- Dataset.write_delta's ray_remote_args parameter is now typed
  Optional[Dict[str, Any]], matching its actual default of None.

Adds a new test suite (test_write_delta.py) covering what this
implementation actually supports: APPEND/OVERWRITE round-trips,
unsupported-mode rejection, single- and multi-column partitioning,
multi-block writes (including cross-block schema unification within a
single task), table name/description metadata, the empty-dataset error
path (and the empty-APPEND-against-existing-table and
empty-OVERWRITE-against-existing-table no-op/truncate paths),
storage_options passthrough to both the commit calls and the worker
filesystem (S3, Azure, and GCS, including the GCS env-var restore
behavior and the AWS_DEFAULT_REGION fallback), null/URL-encoded
partition value and key handling, a simulated cross-filesystem
leading-slash mismatch in the written-file relative path computation, a
regression test for the read_parquet-sourced schema bug above, and a
regression test that create_dir is only skipped for cloud object-store
filesystems.

One pyproject.toml per-file-ignore added (F401) to keep the ported file
lint-clean without editing it for that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Abhishek Verma <abhishek.verma@anyscale.com>
Adds a write_delta counterpart to the existing write_parquet release
benchmark, reusing the same real dataset and harness rather than
inventing new synthetic-data infrastructure:

- read_and_consume_benchmark.py: add a --write-delta consume option,
  mirroring the existing --write (write_parquet) option exactly --
  same read path, same Benchmark/BenchmarkMetric reporting, just a
  different write call.
- release_data_tests.yaml: register a write_delta test entry alongside
  write_parquet, reading the same TPCH SF1000 lineitem source
  (s3://ray-benchmark-data/tpch/parquet/sf1000/lineitem), so the two
  are a direct, apples-to-apples Parquet-vs-Delta write comparison on
  identical input data.
- byod_install_deltalake.sh: new post-build script installing
  deltalake==1.5.0 (matching the version pinned in
  python/requirements_compiled*.txt), needed because -- unlike
  parquet/image/tfrecords -- deltalake isn't in the base release-test
  image. Mirrors byod_install_pyiceberg.sh's exact shape.

Bug fix (found from an actual CI run of this test, build 101449): the
write itself succeeded (6B rows, 815GiB written), but the subsequent
commit step failed with "OSError: Generic S3 error ... Received
redirect without LOCATION, this normally indicates an incorrectly
configured region". Root cause: the ray-data-write-benchmark bucket is
in us-west-2 (confirmed via its S3 HEAD response's x-amz-bucket-region
header), but deltalake's Rust S3 client defaults to us-east-1 and,
unlike PyArrow's S3FileSystem (which write_parquet uses and which
resolves a bucket's region automatically), doesn't follow a region
redirect -- it needs the region supplied explicitly via
storage_options. Fixed by passing storage_options={"AWS_REGION":
"us-west-2"} to the write_delta call, matching the us-west-2 region
convention already used elsewhere in this same directory
(training_ingest_benchmark.py's S3_IMAGE_AWS_REGION,
multi_node_train_benchmark.py's AWS_REGION env var).

Second bug fix (found from a subsequent CI run, build 101454): a large
distributed write (~1000 concurrent tasks against a brand-new table
root) failed with "AWS Error SLOW_DOWN during PutObject operation"
while writing Parquet files. Root cause (fixed on stack3/pr1-write-delta-core,
which this branch is stacked on): pyarrow.dataset.write_dataset's default
create_dir=True made every task independently PUT the same directory
marker key for the table root, and ~1000 tasks racing on that one key
triggered AWS throttling. Fixed there by skipping create_dir for cloud
object-store filesystems (S3/GCS/Azure), which have no real
directories.

Adds three more write_delta benchmark variants on top of the original
(all reading exclusively from our own internal TPCH test-data bucket,
s3://ray-benchmark-data/tpch/parquet/sf{N}/lineitem -- no external or
public dataset is used anywhere in this change), inspired by (but not
copying any data/code/infra from) delta-io/delta's own benchmark suite,
which tests data loading at both a small "smoke" scale and a large
scale:
- write_delta_smoke: SF10 (small, already used elsewhere in this file)
  on a single-node fixed_size_1_cpu_compute.yaml cluster (also already
  used elsewhere in this file as a smoke tier), for fast, cheap nightly
  feedback on the basic write_delta path without waiting for the
  expensive SF1000 run.
- write_delta_overwrite: SF100, exercising SaveMode.OVERWRITE against
  an already-existing table (not just a brand-new one, which the
  original entry already covers via APPEND). Populates the table once
  first (a separately-reported "setup_populate" Benchmark.run_fn step)
  so the measured "main" step genuinely replaces existing data of
  realistic size, rather than trivially creating an empty table --
  SF100 (not SF1000) keeps this variant's now-doubled read/write cost
  proportionate to the existing SF1000 test rather than doubling it.
- write_delta_partitioned: SF1000, Hive-partitioned by column08 (this
  S3 dataset stores lineitem columns positionally, not by TPCH name;
  column08 is l_returnflag per tpch/common.py's TABLE_COLUMNS mapping
  -- a 3-value, low-cardinality column, safe at this scale), exercising
  the partitioned-write code path at full scale, which the original
  entry doesn't touch at all.

read_and_consume_benchmark.py gains two new --write-delta modifier
flags (--write-delta-mode, --write-delta-partition-by; validated to
require --write-delta) to drive these variants without duplicating the
consume_fn logic. No changes needed to benchmark.py: Benchmark.run_fn
already supports multiple named calls per script run (its own docstring
documents exactly this pattern), which is what the overwrite variant's
setup_populate/main split relies on.

Verified locally end-to-end (local Parquet source, local write_delta
destination, no S3/cluster access): the original single-write path by
invoking the modified script's main() directly with a constructed
args.Namespace; the three new variants via a dedicated smoke test
confirming (a) the CLI validation guard rejects the new modifier flags
without --write-delta, (b) SaveMode.OVERWRITE against an
already-populated table results in the original row count, not double
(i.e. genuinely overwritten, not appended), and (c)
--write-delta-partition-by produces the expected Hive-style
column08=<value> directories and reads back correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Abhishek Verma <abhishek.verma@anyscale.com>
@abhishekverma-ray
abhishekverma-ray force-pushed the stack3/pr2-write-delta-benchmark branch from 3506500 to 49c22f9 Compare July 27, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data Ray Data-related issues release-test release test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant