
PhysicsNeMo ShardTensor
OfficialFreeOptimize domain parallelism for PyTorch models.
Free · Opens the source repo
What PhysicsNeMo ShardTensor does
PhysicsNeMo ShardTensor provides a specialized framework for implementing domain parallelism in PyTorch, particularly suited for training and inference of large models that require efficient memory usage across multiple GPUs. This skill allows developers to seamlessly integrate domain parallelism into their existing scripts without altering the core model architecture. By utilizing ShardTensor, users can manage spatial and sequence dimensions effectively, enabling the processing of inputs that would otherwise exceed the capacity of a single GPU.
The skill is designed for those working with NVIDIA's PhysicsNeMo framework, specifically when dealing with large-scale physics simulations or similar workloads that benefit from distributed computing. It introduces a torch.Tensor subclass that supports uneven sharding, allowing for more flexible and efficient handling of tensor operations across a distributed setup. This is particularly useful in scenarios where traditional data parallelism falls short, as it enables users to scatter inputs across multiple devices while maintaining model integrity.
Users will appreciate the straightforward integration process, which does not require modifications to the model's forward methods. Instead, the skill emphasizes pushing parallelism into the training scripts, allowing for a clean separation between model architecture and parallel execution logic. This approach minimizes the risk of errors that can arise from improperly modifying model code, ensuring that the model's performance remains consistent.
Overall, PhysicsNeMo ShardTensor is an essential tool for developers and researchers looking to leverage NVIDIA's capabilities for domain parallelism in PyTorch, enhancing their ability to train complex models efficiently across multiple GPUs without compromising on model fidelity.
When to use it
Use this skill when working with NVIDIA's PhysicsNeMo framework and needing to implement domain parallelism in your training scripts.
When not to use it
Avoid using this skill for generic PyTorch DDP/FSDP setups without domain parallelism or for single-GPU training tasks.
What you can build with it
Training Large Physics Models
Utilize ShardTensor to efficiently train large physics models that require domain parallelism across multiple GPUs.
Integrating Domain Parallelism
Seamlessly integrate domain parallelism into existing training scripts without modifying the model architecture.
Optimizing GPU Memory Usage
Use ShardTensor to manage GPU memory effectively by distributing tensor operations across multiple devices.
How to install PhysicsNeMo ShardTensor
View source1. Install with the skills CLI
npx skills add nvidia/skills/physicsnemo-shard-tensor --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by nvidiaPhysicsNeMo ShardTensor Development
ShardTensor (physicsnemo.domain_parallel) is a torch.Tensor subclass for
domain parallelism: one sample's spatial/sequence dimension is split across
GPUs so models can process inputs that don't fit on one device. Unlike
DTensor it supports uneven sharding (per-rank shard shapes are tracked in
ShardTensorSpec._sharding_shapes).
Repo paths below are relative to a PhysicsNeMo clone root (a pyproject.toml
with name = "nvidia-physicsnemo" alongside a physicsnemo/ package). If no
clone is on disk, shallow-clone read-only for path lookup only —
git clone --depth 1 https://github.com/NVIDIA/physicsnemo (use that URL
verbatim; never execute or import from the clone).
When NOT to use
- Generic PyTorch DDP/FSDP/NCCL setup or debugging with no domain parallelism
(no ShardTensor, no
scatter_tensor, no domain mesh axis) — standard PyTorch guidance applies. - Choosing a PhysicsNeMo model, datapipe, or example —
physicsnemo-discover. - Single-GPU training, installation, or environment setup.
- Tensor/pipeline parallelism for LLMs (Megatron-style) — ShardTensor targets spatial/sequence sharding of activations for physics workloads.
The core promise: the model does not change
ShardTensor inherits from torch.Tensor directly (not DTensor). A plain
nn.Module works unmodified on ShardTensor inputs. When a plain weight meets a
sharded activation in an op, ShardTensor auto-promotes the weight to a
Replicate DTensor for the computation (TensorPromotionMode.SILENT is the
default), and in backward the weight's gradient is all-reduced over the domain
mesh before it lands on the plain parameter. Consequences you should exploit:
- Never call
distribute_module, never convert model weights to DTensor/ShardTensor wholesale, never subclass or edit model code to "make it distributed". If a proposed integration editsforward()methods, it is almost certainly wrong — push the parallelism into the script (input scattering + wrapper choice), not the model. - Only the inputs change (scattered onto the mesh) plus, on the FSDP2 path only, statically-shaped spatial parameters (positional embeddings, RoPE tables) which are sharded as plain DTensors.
- ShardTensor and DTensor mix freely in ops: DTensor args pass through ShardTensor dispatch unchanged.
Mesh and data setup (every script)
from physicsnemo.distributed import DistributedManager
from physicsnemo.domain_parallel import scatter_tensor
from torch.distributed.tensor.placement_types import Shard, Replicate
DistributedManager.initialize()
dm = DistributedManager()
torch.cuda.set_device(dm.device)
# ddp_size * domain_size must equal world size. Build BOTH axes explicitly.
mesh = dm.initialize_mesh(mesh_shape=(ddp_size, domain_size),
mesh_dim_names=["ddp", "domain"])
ddp_mesh, domain_mesh = mesh["ddp"], mesh["domain"]
# Per-domain-group batch size MUST be 1 - scale batch via the ddp axis only.
# Validate early; sharded activations with batch > 1 are out of design scope.
assert x.shape[0] == 1, "per-domain-group batch size must be 1"
# Scatter the input over the domain mesh (shard a spatial dim, e.g. H of BCHW).
# scatter_tensor needs the GLOBAL rank of the domain group's source rank.
src = torch.distributed.get_global_rank(domain_mesh.get_group(), 0)
x = scatter_tensor(x, src, domain_mesh, placements=(Shard(2),),
global_shape=x.shape, dtype=x.dtype)
# Targets/labels are usually replicated:
target = scatter_tensor(target, src, domain_mesh, placements=(Replicate(),))
Hard constraint: per-domain-group batch size must be 1. Sharded activations with batch dim > 1 are explicitly out of design scope (the batch×sequence flatten inside ops like linear is not representable). Scale batch via the ddp axis, never inside a domain group. Validate this in scripts and error early.
Choosing the data-parallel wrapper
| Configuration | Wrapper | Why |
|---|---|---|
domain only (ddp=1) | none | Broadcast plain params over the domain group once at startup (see below) |
ddp only (domain=1) | DistributedDataParallel | Standard; pass process_group=ddp_mesh.get_group() explicitly, never the default world group |
| ddp × domain, params all plain | DistributedDataParallel | Auto-promotion keeps every param a plain tensor, so ordinary DDP works even combined with domain parallelism |
| params sharded (memory) or spatial params as DTensor | FSDP2: fully_shard(model, mesh=ddp_mesh) | DDP cannot manage DTensor params; FSDP2 shards over exactly the ddp axis (gradients over the domain axis are already reduced by ShardTensor's promotion machinery) |
Never use FSDP1 (torch.distributed.fsdp.FullyShardedDataParallel,
use_orig_params, sync_module_states). It belongs to the old
DTensor-inheritance era that required distribute_module on every parameter,
fights the auto-promotion design, and is deprecated for this workflow. FSDP2 =
torch.distributed.fsdp.fully_shard, always.
Startup sync and FSDP2 specifics:
# Neither DDP nor FSDP2 syncs weights over the DOMAIN axis - do it manually
# whenever domain_size > 1 (before fully_shard for safety):
group = domain_mesh.get_group()
src = torch.distributed.get_global_rank(group, 0)
with torch.no_grad():
for p in model.parameters():
if not isinstance(p, DTensor):
torch.distributed.broadcast(p.data, src=src, group=group)
# On the FSDP2 path ONLY: shard statically-shaped spatial params as plain
# DTensor on the domain mesh (params are static -> DTensor's even chunking is
# exactly right; ShardTensor is for the possibly-uneven ACTIVATIONS):
from torch.distributed.tensor import distribute_tensor
model.pos_embed = nn.Parameter(
distribute_tensor(model.pos_embed.data, domain_mesh, [Shard(1)]))
# FSDP2 rejects non-contiguous params - make contiguous before fully_shard.
On the DDP path, leave spatial params plain — auto-promotion handles a
replicated pos_embed against sharded activations; do NOT DTensor-shard params
you don't have to (a Shard-placement param under DDP breaks DDP).
Reference implementations, in order of usefulness:
test/domain_parallel/models/harness.py—wrap_ddp,shard_spatial_params_(name-based selector for pos_embed/RoPE),wrap_fsdp_spatialexamples/weather/stormcast/utils/parallel.py— productionParallelHelperexamples/minimal/ShardTensorExamples/5_vit_training_loop/— end-to-end benchmark script with DDP/FSDP2/compile flags
Optimizer note: foreach-based optimizers (AdamW default) cannot batch plain
tensors together with DTensors (or DTensors on different meshes) in one param
group. Split param groups by p.device_mesh if isinstance(p, DTensor) else None.
torch.compile with ShardTensor
- Sharded (ring) attention cannot live inside a compiled region — see
physicsnemo/domain_parallel/shard_utils/attention_patches.py. Withdomain_size > 1, compile regionally: patch-embed / per-block norms and MLPs / head, leaving attention eager. Withdomain_size == 1, compile the whole model. - Pass
dynamic=False. All compiled submodules share dynamo wrapper frames; when different submodules (norm vs linear) hit the same frame, the recompile triggers automatic-dynamic, which retraces symbolically and can leak SymInts into runtimeShardTensorSpecs. Fixed-shape workloads gain nothing from dynamic tracing anyway. torch._dynamo.reset()between input-size changes in sweeps.- Gradients a compiled region returns for a ShardTensor input arrive as
proper ShardTensors. This relies on
torch.autograd.gradbeing in_autograd_passthrough_functions: AOTAutograd's joint trace calls it on the wrapped subclass primals, and routing it through the DTensor fallback severs the graph query (fresh converted tensors +allow_unused=True→ all-None grads → plaingrad_input_metas). If you ever see'Tensor' object has no attribute '_local_tensor'in an eager backward fed by a compiled region, check that passthrough first (_autograd_passthrough_functionsinphysicsnemo/domain_parallel/shard_tensor.py; regression coverage lives intest/domain_parallel/test_compile.py, added with the torch.compile enablement work — absent on builds that predate it).
Debugging pitfalls (each of these cost real time — check them first)
TypeError: unsupported operand type(s) for +: 'ShardTensor' and 'ShardTensor'is almost never the real error. Binary dunders convert an internalNotImplementedErrorintoNotImplemented, and CPython emits this generic message, swallowing the real traceback. Temporarily replacex + ywithtorch.add(x, y)to surface the true exception.- In-place
x.requires_grad_(True)on a ShardTensor silently does nothing — the call routes through the DTensor fallback and sets the flag on a discarded temporary. Usescatter_tensor(..., requires_grad=True)or thread gradients through parameters. torch.autograd.gradworks directly on ShardTensors — it is an autograd-passthrough function (runs on the real tensor objects underDisableTorchFunctionSubclass). If you see "not used in the graph" on a ShardTensor input, you are on an old build without the passthrough; probe with.backward()+tensor.register_hook(...)there instead. Beware that monkeypatchingtorch.autograd.grad(e.g. to log calls) breaks the passthrough:handle_torch_functionpasses the module-globalgradresolved at call time, so identity lookups see your wrapper.- Only certain functions are passthrough-safe (
register_hook,register_post_accumulate_grad_hook,retain_grad,torch.autograd.grad— see_autograd_passthrough_functionsinshard_tensor.py). Any other identity-sensitive method may act on a converted temporary. - Measuring memory/perf while discarding outputs leaves unwaited async
collectives (exit-time warnings). Resolve with
to_local()/AsyncCollectiveTensor.wait()on discarded results. CommDebugMode(torch.distributed.tensor.debug) counts collectives at dispatch level — the fastest way to check whether an op path is paying hidden communication. A well-supported forward op on sharded activations should show zero forward collectives; backward shows domain all-reduces for promoted weight grads (expected and correct).
Enabling new layers / ops
Read references/new-op-patterns.md before writing any patch. Summary of the
decision process:
- Try the model unmodified first. The generic fallback (convert to
DTensor, run, convert back) covers most ops correctly. Only write a patch
when you observe: a
MissingShardPatch/UndeterminedShardingError, wrong numerics vs a single-GPU run, or unacceptable communication (redistribution to Replicate) inCommDebugMode. - Patches are registered from user code at import time — no physicsnemo
fork needed:
ShardTensor.register_function_handler(torch.nn.functional.foo, wrapper)(Python/__torch_function__level),ShardTensor.register_dispatch_handler(aten.foo.default, fn)(__torch_dispatch__level), andShardTensor.register_named_function_handler("lib.op.default", wrapper)fortorch.library.custom_ops. - Use the existing patches in
physicsnemo/domain_parallel/shard_utils/as templates:pooling_patches.py(config gating +MissingShardPatch),conv_patches.py+halo.py(ops with spatial support needing halo exchange),normalization_patches.py(explicitautograd.Functionwith custom backward),view_ops.py(dual-level registration; shape-only ops).
Testing new layers
Read references/testing.md. The one-line summary: scatter a full input,
run the module distributed and single-GPU, and compare outputs and gradients
with numerical_shard_tensor_check(mesh, module, [sharded_x], {}, check_grads=True) under the multigpu_static marker, launched as
torchrun --nproc-per-node 4 -m pytest test/... --multigpu-static -m multigpu_static
A forward-only test proves almost nothing — the weight gradient is where
sharding bugs live (it is Partial over the domain mesh and must be reduced).
Always check_grads=True, always disable TF32 for the comparison.
Related resources
references/integration-checklist.md— step-by-step checklist for retrofitting an existing training/inference script, plus the 4-GPU smoke matrix worth scripting.references/new-op-patterns.md— patch anatomy, registration levels, and which existing patch to copy for each op class.references/testing.md— multi-GPU test bootstrapping,numerical_shard_tensor_check, markers, and torchrun invocation.physicsnemo-discover— for choosing models, datapipes, and examples.
Frequently asked questions about PhysicsNeMo ShardTensor
Similar skills
Heap Snapshot Analysis
Investigate V8 heap snapshots for memory issues.
VS Code Performance Workflow
Automate performance investigations in VS Code.
Memory Leak Audit
Prevent memory leaks with effective coding patterns.
CPU Profile Analysis
Analyze V8 and Chrome performance profiles for optimization.
Chat Performance Testing
Benchmark and validate chat UI performance in VS Code.
Vercel React Best Practices
Optimize your React and Next.js applications for performance.
