Skip to content

Standalone datapipe and ShardTensor fixes ahead of domain-parallel reading - #1979

Open
coreyjadams wants to merge 2 commits into
mainfrom
dp-01-standalone-fixes
Open

coreyjadams wants to merge 2 commits into
mainfrom
dp-01-standalone-fixes

Conversation

@coreyjadams

@coreyjadams coreyjadams commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

PhysicsNeMo Pull Request

This PR brings standalone fixes and reorganizations that are necessary to start work for the domain-parallel data loading and unified recipe domain parallel support.

Datapipes:

  • MeshReader / DomainMeshReader gain close() (fixes MeshDataset.close()
    raising AttributeError); the extra-boundary loader is split out.
  • Zarr readers: extract-method refactor shared with the tensorstore reader,
    a shared Reader._window_indices, and a deterministic subsampling window
    key (configured list order instead of set iteration order).
  • record_consumer_stream unwraps ShardTensor / DTensor leaves.

ShardTensor:

  • scatter_tensor returns a shard that owns its storage (dynamo could not
    fakeify the nonzero-offset ranks' views).
  • Redistribute planner key is immutable and the spec hash is invalidated on
    mutation (planner cache no longer grows per step).
  • Shard-shape gather is device agnostic; three small torch.compile fixes
    (_conversion_scope, plain-tensor promotion under no_grad, __new__).

Description

Checklist

Dependencies

Review Process

All PRs are reviewed by the PhysicsNeMo team before merging.

Depending on which files are changed, GitHub may automatically assign a maintainer for review.

We are also testing AI-based code review tools (e.g., Greptile), which may add automated comments with a confidence score.
This score reflects the AI’s assessment of merge readiness and is not a qualitative judgment of your work, nor is
it an indication that the PR will be accepted / rejected.

AI-generated feedback should be reviewed critically for usefulness.
You are not required to respond to every AI comment, but they are intended to help both authors and reviewers.
Please react to Greptile comments with 👍 or 👎 to provide feedback on their accuracy.


Stack created with GitHub Stacks CLIGive Feedback 💬

Coverage note

Codecov patch coverage on this PR is currently an artifact: the PR Coverage job runs pytest under --testmon, whose own tracer displaces the outer coverage run, so nothing executed inside tests is recorded (see #1988 for the fix and reproduction). The tests added here will only show up in the patch number once #1988 lands and this branch re-runs CI. The multigpu_static additions run under the ci:multi-gpu label but only count toward coverage in the nightly.

@github-actions

Copy link
Copy Markdown
Contributor

CODEOWNERS review map

Current for commit be1c1eb8631e. An approval covers every file listed for that owner; one owner is sufficient for shared files.

@coreyjadams — 8 file(s)
  • physicsnemo/datapipes/protocols.py
  • physicsnemo/datapipes/readers/base.py
  • physicsnemo/datapipes/readers/mesh.py
  • physicsnemo/datapipes/readers/tensorstore_zarr.py
  • physicsnemo/datapipes/readers/zarr.py
  • physicsnemo/domain_parallel/_shard_redistribute.py
  • physicsnemo/domain_parallel/_shard_tensor_spec.py
  • physicsnemo/domain_parallel/shard_tensor.py
@negin513 — 8 file(s)
  • physicsnemo/datapipes/protocols.py
  • physicsnemo/datapipes/readers/base.py
  • physicsnemo/datapipes/readers/mesh.py
  • physicsnemo/datapipes/readers/tensorstore_zarr.py
  • physicsnemo/datapipes/readers/zarr.py
  • physicsnemo/domain_parallel/_shard_redistribute.py
  • physicsnemo/domain_parallel/_shard_tensor_spec.py
  • physicsnemo/domain_parallel/shard_tensor.py

No CODEOWNER

  • CHANGELOG.md

Comment /codeowners-info to refresh.

@coreyjadams
coreyjadams added this pull request to stack #1984 September 10, 2026 19:07
@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Retrigger

The PR is not yet safe to merge because shard-shape inference fails for CPU/Gloo meshes on CUDA-capable hosts.

Findings

  1. P1 Gloo Collectives Use CUDA

Summary

  • Adds close() support to mesh readers and separates extra-boundary loading.
  • Centralizes coordinated-subsampling window generation for both Zarr backends.
  • Uses immutable DTensor planner keys and invalidates cached ShardTensorSpec hashes after shard-shape mutation.
  • Makes scattered shards own their storage and adjusts ShardTensor construction and conversion for torch.compile.
  • The new shard-shape collective device selection does not follow CPU/Gloo meshes on CUDA-capable hosts.

Reviews (1) · Last reviewed commit: "Standalone datapipe and ShardTensor fixe..."

# Collectives run on the process group's backend device: CUDA when
# available, otherwise CPU (gloo) -- so CPU-only single-process use of
# ``sharding_shapes="infer"`` works too.
device = "cuda" if torch.cuda.is_available() else "cpu"

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.

P1 Gloo Collectives Use CUDA

On a CUDA-capable host using a CPU DeviceMesh backed by Gloo, this selects CUDA for the shape tensors without checking local_group. Calling ShardTensor.from_local(..., sharding_shapes="infer") then passes CUDA tensors to a CPU process group, causing shard-shape inference to fail. Select the collective device from the mesh or process-group backend instead of host-wide CUDA availability.

@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.06977% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.35%. Comparing base (26eaf48) to head (f69588c).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
physicsnemo/domain_parallel/_shard_tensor_spec.py 22.22% 6 Missing and 1 partial ⚠️
physicsnemo/domain_parallel/shard_tensor.py 40.00% 5 Missing and 1 partial ⚠️
physicsnemo/domain_parallel/_shard_redistribute.py 33.33% 4 Missing ⚠️
physicsnemo/datapipes/protocols.py 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1979      +/-   ##
==========================================
- Coverage   72.64%   72.35%   -0.30%     
==========================================
  Files         928      928              
  Lines       69963    70005      +42     
  Branches    10542    10549       +7     
==========================================
- Hits        50828    50650     -178     
- Misses      15632    15868     +236     
+ Partials     3503     3487      -16     
Files with missing lines Coverage Δ
physicsnemo/datapipes/readers/base.py 92.85% <100.00%> (+9.75%) ⬆️
physicsnemo/datapipes/readers/mesh.py 87.27% <100.00%> (+6.25%) ⬆️
physicsnemo/datapipes/readers/tensorstore_zarr.py 88.13% <100.00%> (+4.67%) ⬆️
physicsnemo/datapipes/readers/zarr.py 91.33% <100.00%> (+8.25%) ⬆️
physicsnemo/datapipes/protocols.py 95.00% <88.88%> (-0.56%) ⬇️
physicsnemo/domain_parallel/_shard_redistribute.py 44.93% <33.33%> (-35.25%) ⬇️
physicsnemo/domain_parallel/shard_tensor.py 61.59% <40.00%> (-21.69%) ⬇️
physicsnemo/domain_parallel/_shard_tensor_spec.py 45.00% <22.22%> (-33.36%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@copy-pr-bot

copy-pr-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

coreyjadams added a commit that referenced this pull request Sep 14, 2026
`coverage run -m pytest --testmon` measured nothing executed inside tests:
pytest-testmon installs its own coverage.py tracer per test to track
dependencies, which replaces the outer tracer. The merged PR report was
therefore the nightly baseline (main's line numbers) plus import-time
lines, so any file whose lines shifted appeared to lose coverage
(e.g. PR #1979: mesh.py lines hit by 28 passing tests reported missing).

Reproduced locally on test_mesh_readers.py: plain run 60%, --testmon 15%,
--testmon --testmon-nocollect 60%.

Pass --testmon-nocollect so testmon only selects tests. The Coverage job
restores but never publishes the testmon DB, so nothing is lost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
coreyjadams added a commit that referenced this pull request Sep 14, 2026
`coverage run -m pytest --testmon` measured nothing executed inside tests:
pytest-testmon installs its own coverage.py tracer per test to track
dependencies, which replaces the outer tracer. The merged PR report was
therefore the nightly baseline (main's line numbers) plus import-time
lines, so any file whose lines shifted appeared to lose coverage
(e.g. PR #1979: mesh.py lines hit by 28 passing tests reported missing).

Reproduced locally on test_mesh_readers.py: plain run 60%, --testmon 15%,
--testmon --testmon-nocollect 60%.

Pass --testmon-nocollect so testmon only selects tests. The Coverage job
restores but never publishes the testmon DB, so nothing is lost.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
coreyjadams and others added 2 commits September 14, 2026 15:47
…ading

- MeshReader/DomainMeshReader.close(); extra-boundary loader split
- Zarr readers: extract-method refactor, shared Reader._window_indices,
  deterministic subsampling key order
- record_consumer_stream unwraps ShardTensor/DTensor leaves
- ShardTensor: scatter_tensor owns its storage, immutable redistribute
  planner key, spec hash invalidation, gloo shape gather, compile fixes
Datapipes (CPU-runnable):
- Reader._window_indices: disabled, list-order key fallback, no target
  present, seeded reproducibility; include_index_in_metadata toggle.
- ZarrReader._array_rows in both modes; absent leading target key falls
  back to the next configured key; no-target loads full arrays.
- TensorStoreZarrReader: same fallbacks plus _finalize_sample merging
  arrays, attributes and defaults.
- Mesh readers: close() no-ops, MeshDataset.close propagates,
  include_index_in_metadata, metadata built before pin_memory,
  _load_extra_boundary_meshes split, multiple-match warning, zarr extra
  boundary via from_zarr.
- record_consumer_stream: to_local() unwrap (CPU and CUDA), non-callable
  to_local ignored.

Domain parallel (multigpu_static):
- _plan_key strips sharding shapes; torch's redistribute planner cache
  stops growing across populated/unpopulated specs.
- ShardTensorSpec.__setattr__ drops the cached hash on _sharding_shapes,
  including the lazy sharding_shapes() population path.
- _gather_shard_shapes_for_dim with torch.Size and tensor inputs.
- scatter_tensor local shard owns its storage (no view, zero offset).
- _convert_args_to_dtensor creates no autograd state below autograd.
- _conversion_scope keeps depth attribute at 0; ShardTensor.__new__ is a
  non-recursive dynamo skip and constructs inside compiled code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coreyjadams
coreyjadams force-pushed the dp-01-standalone-fixes branch from 4250344 to f69588c Compare September 14, 2026 20:47
return None
return spawn_generator(self._seed_base, self._epoch, index)

def _window_indices(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This function becomes a generic way to slice into an array, regardless of the type (numpy, torch, zarr, etc) that we can reuse across datareaders.

Comment on lines +364 to +366
metadata = self._get_sample_metadata(index)
if self.include_index_in_metadata:
metadata["index"] = index

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The metadata is moved up here because in PR 4 of the stack, we have an early-exit here for loading domain parallel data, and this lets us not load the meta data in two places.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

(meaning, it's a reorder with no behavior change)

Comment on lines +661 to +663

if self.pin_memory:
dm = dm.pin_memory()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Moving it here just makes MeshReader and DomainMeshReader read meta data and then pin memory in the same order.

interior=dm.interior,
boundaries={
**dict(dm.boundaries),
**self._load_extra_boundary_meshes(index),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This gets split into two paths deliberately. When we make it domain parallel in PR 4, we have to have a "proto mesh" type object that isn't yet a full mesh, but instead is a mesh-like object that's sharded. This splitring lets us load all the extras, without constructing (and then throwing away) a full mesh later

Comment on lines +260 to +263
def _open_stores(
self, index: int
) -> tuple[dict[str, Any], dict[str, torch.Tensor]]:
"""Open the sample's array stores (async, metadata-only) + attributes."""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this breaks up _load sample into three pieces:

  • open stores is attripes, and async opens for arrays (metadata only)
  • _load_sample issues the reads itself.
  • finalize samples waits on every read issued, or adds missing default values

In the domain parallel component, well make _load_sample_domain_parallel and reuse other components.

# Per-sample generator: reproducible regardless of read order/thread.
generator = self._index_generator(index)
def _open_sample(self, index: int) -> tuple[Any, set[str]]:
"""Open the sample's group and validate required-field availability."""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same refactor here as tensorstore_zarr to split _load_sample into three pieces (open, read, finalize), but there is an extra move here to push the cyclic index indicis into Reader._window_indices.

Comment on lines -294 to -295
subsample_indices = _cyclic_block_indices(
array_shape, n_points, generator=generator

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Here, _cyclic_block_indices is removed, we use the base call now instead.

Comment on lines +92 to +101
def _record(t: torch.Tensor) -> None:
# Distributed wrapper subclasses (ShardTensor/DTensor) must record
# their local tensor: record_stream on the wrapper re-enters its
# dispatch machinery, which has no handling for Stream arguments.
to_local = getattr(t, "to_local", None)
if callable(to_local):
t = to_local()
if t.is_cuda:
t.record_stream(stream)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is to ensure we record the stream on the local tensor, if using a sharded tensor, rather than the tensor itself. We don't have record stream machinery in ShardTensor .. that would be a viable path too, I suppose.

Comment on lines +357 to +367
def _plan_key(spec: DTensorSpec) -> DTensorSpec:
r"""Immutable, canonical key for torch's cached redistribute planner.

A plain ``DTensorSpec`` hashes and compares on mesh, placements and
``tensor_meta`` only -- it carries no lazily-populated sharding shapes, so
it can sit in a ``functools.cache`` without going stale.
"""
meta = spec.tensor_meta
if meta is not None:
meta = TensorMeta(tuple(meta.shape), tuple(meta.stride), meta.dtype)
return DTensorSpec(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This changes is all about redistribute caching on specs: the shard shapes was getting added to the spec after it was in the cache table, invalidating the cache, and the cache was growing without limit. We don't need that to route the operations, only to do the operations themselves. SO we actually drop to DTensorSpec for the caching key itself which is totally static.

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

Labels

ci:multi-gpu Run this PR on multiGPU ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant