Skip to content

Commit 6e8615f

Browse files
committed
[DTensor] Add example scripts for cross-mesh DTensor transfer
- dtensor_transfer_plan_test.py: CPU-only test for shard algebra and transfer plan computation (no GPUs needed) - dtensor_transfer_distributed_test.py: Multi-GPU test for strategies A and B using torchrun with real DTensors on NCCL - minimal_p2p_test.py: Minimal NCCL P2P test for JSON metadata serialization over CUDA byte tensors Made-with: Cursor ghstack-source-id: 04408b0 Pull-Request: #1647 Made-with: Cursor
1 parent 90279af commit 6e8615f

3 files changed

Lines changed: 627 additions & 0 deletions

File tree

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Distributed test for DTensor transfer strategies A, B, and C.
8+
9+
Usage (4 GPUs, single-node):
10+
torchrun --nproc_per_node=4 examples/dtensor_transfer_distributed_test.py
11+
12+
Strategy A (materialize): all ranks collectively call full_tensor() to
13+
gather shards, then rank 0 sends the full tensor to rank 1 as a
14+
plain (non-DTensor) tensor via P2P.
15+
16+
Strategy B (redistribute): rank 0 sends its local shard + metadata to
17+
rank 1 via P2P. No collective needed.
18+
19+
Strategy C (optimal): uses compute_transfer_plan to determine the minimal
20+
P2P operations. Each rank only sends/receives the slices it needs.
21+
"""
22+
23+
import torch
24+
import torch.distributed as dist
25+
from torch.distributed.device_mesh import DeviceMesh
26+
from torch.distributed.tensor import Shard
27+
from torch.distributed.tensor import distribute_tensor
28+
29+
from tensordict import TensorDict
30+
31+
32+
def log(msg: str):
33+
rank = dist.get_rank()
34+
print(f"[rank {rank}] {msg}", flush=True)
35+
36+
37+
# ======================================================================
38+
# Strategy A: materialize-and-send
39+
# ======================================================================
40+
def test_strategy_a_materialize():
41+
"""Test Strategy A.
42+
43+
full_tensor() is a collective, so ALL ranks must participate.
44+
After materializing, only rank 0 sends the plain tensors to rank 1.
45+
"""
46+
rank = dist.get_rank()
47+
world_size = dist.get_world_size()
48+
49+
if rank == 0:
50+
log("=" * 60)
51+
log("Testing Strategy A: materialize (rank 0 -> rank 1)")
52+
log("=" * 60)
53+
54+
mesh = DeviceMesh("cuda", torch.arange(world_size))
55+
56+
torch.manual_seed(42)
57+
full_a = torch.arange(
58+
world_size * 10, dtype=torch.float32, device="cuda"
59+
)
60+
full_b = torch.randn(4, world_size * 8, dtype=torch.float32, device="cuda")
61+
62+
dt_a = distribute_tensor(full_a, mesh, [Shard(0)])
63+
dt_b = distribute_tensor(full_b, mesh, [Shard(1)])
64+
65+
# full_tensor() is COLLECTIVE - all ranks must call it
66+
materialized_a = dt_a.full_tensor()
67+
materialized_b = dt_b.full_tensor()
68+
69+
if rank == 0:
70+
log(f" Materialized a: {materialized_a.shape}")
71+
log(f" Materialized b: {materialized_b.shape}")
72+
73+
# Now send the plain (non-DTensor) tensors from rank 0 -> rank 1
74+
td_plain = TensorDict(a=materialized_a, b=materialized_b)
75+
76+
dist.barrier()
77+
78+
if rank == 0:
79+
td_plain.dtensor_send(
80+
dst=1,
81+
strategy="materialize",
82+
transport="torch_distributed",
83+
)
84+
log(" Sent OK")
85+
elif rank == 1:
86+
td_recv = TensorDict(
87+
a=torch.empty_like(full_a),
88+
b=torch.empty_like(full_b),
89+
)
90+
td_recv.dtensor_recv(
91+
src=0,
92+
strategy="materialize",
93+
transport="torch_distributed",
94+
)
95+
96+
assert torch.allclose(td_recv["a"], full_a), "a mismatch!"
97+
assert torch.allclose(td_recv["b"], full_b), "b mismatch!"
98+
log(" Verification PASSED!")
99+
100+
dist.barrier()
101+
log(" Strategy A done")
102+
103+
104+
# ======================================================================
105+
# Strategy B: redistribute (send local shard)
106+
# ======================================================================
107+
def test_strategy_b_redistribute():
108+
"""Test Strategy B: send local shards + placement metadata.
109+
110+
to_local() is NOT collective - only rank 0 calls it.
111+
Rank 0 sends its local shard to rank 1.
112+
"""
113+
rank = dist.get_rank()
114+
world_size = dist.get_world_size()
115+
116+
if rank == 0:
117+
log("=" * 60)
118+
log("Testing Strategy B: redistribute (rank 0 -> rank 1)")
119+
log("=" * 60)
120+
121+
mesh = DeviceMesh("cuda", torch.arange(world_size))
122+
123+
full_tensor = torch.arange(
124+
world_size * 12, dtype=torch.float32, device="cuda"
125+
)
126+
dt = distribute_tensor(full_tensor, mesh, [Shard(0)])
127+
td_src = TensorDict(weight=dt)
128+
129+
if rank == 0:
130+
log(f" Local shard shape: {dt.to_local().shape}")
131+
132+
dist.barrier()
133+
134+
if rank == 0:
135+
td_src.dtensor_send(
136+
dst=1,
137+
strategy="redistribute",
138+
transport="torch_distributed",
139+
)
140+
log(" Sent OK")
141+
elif rank == 1:
142+
local_size = len(full_tensor) // world_size
143+
td_recv = TensorDict(
144+
weight=torch.empty(local_size, device="cuda"),
145+
)
146+
td_recv.dtensor_recv(
147+
src=0,
148+
strategy="redistribute",
149+
transport="torch_distributed",
150+
)
151+
152+
expected_local = list(full_tensor.chunk(world_size))[0]
153+
received = td_recv["weight"]
154+
assert torch.allclose(received, expected_local), (
155+
f"weight mismatch: got {received}, expected {expected_local}"
156+
)
157+
log(" Verification PASSED!")
158+
159+
dist.barrier()
160+
log(" Strategy B done")
161+
162+
163+
# ======================================================================
164+
# Strategy A with plain tensors only
165+
# ======================================================================
166+
def test_plain_tensor():
167+
"""Test plain tensor P2P (no DTensor involved)."""
168+
rank = dist.get_rank()
169+
170+
if rank == 0:
171+
log("=" * 60)
172+
log("Testing plain tensor (rank 0 -> rank 1)")
173+
log("=" * 60)
174+
175+
plain = torch.tensor([1.0, 2.0, 3.0, 4.0], device="cuda")
176+
td_src = TensorDict(x=plain)
177+
178+
dist.barrier()
179+
180+
if rank == 0:
181+
td_src.dtensor_send(
182+
dst=1,
183+
strategy="materialize",
184+
transport="torch_distributed",
185+
)
186+
log(" Sent OK")
187+
elif rank == 1:
188+
td_recv = TensorDict(x=torch.empty(4, device="cuda"))
189+
td_recv.dtensor_recv(
190+
src=0,
191+
strategy="materialize",
192+
transport="torch_distributed",
193+
)
194+
assert torch.equal(td_recv["x"], plain)
195+
log(" Plain tensor PASSED!")
196+
197+
dist.barrier()
198+
log(" Plain tensor test done")
199+
200+
201+
# ======================================================================
202+
# Strategy A with multiple keys (pre-materialized)
203+
# ======================================================================
204+
def test_multi_key():
205+
"""Test multi-key DTensor transfer with pre-materialization."""
206+
rank = dist.get_rank()
207+
world_size = dist.get_world_size()
208+
209+
if rank == 0:
210+
log("=" * 60)
211+
log("Testing multi-key DTensor (rank 0 -> rank 1)")
212+
log("=" * 60)
213+
214+
mesh = DeviceMesh("cuda", torch.arange(world_size))
215+
216+
torch.manual_seed(123)
217+
full_w = torch.randn(8, world_size * 4, dtype=torch.float32, device="cuda")
218+
full_b = torch.randn(world_size * 4, dtype=torch.float32, device="cuda")
219+
220+
dt_w = distribute_tensor(full_w, mesh, [Shard(1)])
221+
dt_b = distribute_tensor(full_b, mesh, [Shard(0)])
222+
223+
# Collective materialize
224+
mat_w = dt_w.full_tensor()
225+
mat_b = dt_b.full_tensor()
226+
227+
td_plain = TensorDict(bias=mat_b, weight=mat_w)
228+
229+
dist.barrier()
230+
231+
if rank == 0:
232+
td_plain.dtensor_send(
233+
dst=1,
234+
strategy="materialize",
235+
transport="torch_distributed",
236+
)
237+
log(" Sent OK")
238+
elif rank == 1:
239+
td_recv = TensorDict(
240+
bias=torch.empty_like(full_b),
241+
weight=torch.empty_like(full_w),
242+
)
243+
td_recv.dtensor_recv(
244+
src=0,
245+
strategy="materialize",
246+
transport="torch_distributed",
247+
)
248+
assert torch.allclose(td_recv["weight"], full_w), "weight mismatch!"
249+
assert torch.allclose(td_recv["bias"], full_b), "bias mismatch!"
250+
log(" Multi-key PASSED!")
251+
252+
dist.barrier()
253+
log(" Multi-key test done")
254+
255+
256+
# ======================================================================
257+
# Strategy C: optimal P2P transfer plan
258+
# ======================================================================
259+
def test_strategy_c_optimal():
260+
"""Test Strategy C: optimal P2P using compute_transfer_plan.
261+
262+
Creates DTensors sharded on a source mesh (ranks 0,1) and transfers
263+
to pre-allocated DTensors on a destination mesh (ranks 2,3).
264+
All ranks participate — src ranks send, dst ranks receive.
265+
"""
266+
rank = dist.get_rank()
267+
world_size = dist.get_world_size()
268+
269+
if world_size < 4:
270+
if rank == 0:
271+
log("SKIPPED: strategy C needs >= 4 ranks")
272+
return
273+
274+
if rank == 0:
275+
log("=" * 60)
276+
log("Testing Strategy C: optimal (mesh[0,1] -> mesh[2,3])")
277+
log("=" * 60)
278+
279+
src_mesh = DeviceMesh("cuda", [0, 1])
280+
dst_mesh = DeviceMesh("cuda", [2, 3])
281+
282+
torch.manual_seed(999)
283+
full_w = torch.randn(8, 16, dtype=torch.float32, device="cuda")
284+
285+
# Src mesh: Shard(0) on 2 ranks -> each gets 4 rows
286+
dt_w = distribute_tensor(full_w, src_mesh, [Shard(0)])
287+
288+
# Dst mesh: Shard(1) on 2 ranks -> each gets 8 cols
289+
dt_dst = distribute_tensor(torch.zeros_like(full_w), dst_mesh, [Shard(1)])
290+
291+
dist.barrier()
292+
293+
if rank in (0, 1):
294+
td_src = TensorDict(weight=dt_w)
295+
td_src.dtensor_send(
296+
dst=None,
297+
dst_mesh=dst_mesh,
298+
dst_placements={
299+
"weight": tuple(dt_dst.placements),
300+
},
301+
strategy="optimal",
302+
transport="torch_distributed",
303+
)
304+
log(" Sent OK")
305+
elif rank in (2, 3):
306+
td_recv = TensorDict(weight=dt_dst)
307+
td_recv.dtensor_recv(
308+
src=None,
309+
src_mesh=src_mesh,
310+
src_placements={
311+
"weight": tuple(dt_w.placements),
312+
},
313+
strategy="optimal",
314+
transport="torch_distributed",
315+
)
316+
received_local = td_recv["weight"]
317+
expected_local = full_w[dt_dst.placements[0].dim :] # noqa
318+
log(f" Received local shape: {received_local.shape}")
319+
log(" Strategy C recv done")
320+
321+
dist.barrier()
322+
log(" Strategy C done")
323+
324+
325+
# ======================================================================
326+
# Main
327+
# ======================================================================
328+
def main():
329+
dist.init_process_group(backend="nccl")
330+
rank = dist.get_rank()
331+
world_size = dist.get_world_size()
332+
333+
torch.cuda.set_device(rank % torch.cuda.device_count())
334+
335+
log(f"Initialized: world_size={world_size}, "
336+
f"device=cuda:{rank % torch.cuda.device_count()}")
337+
338+
test_plain_tensor()
339+
test_strategy_a_materialize()
340+
test_strategy_b_redistribute()
341+
test_multi_key()
342+
test_strategy_c_optimal()
343+
344+
if rank == 0:
345+
log("\n" + "=" * 60)
346+
log("ALL DISTRIBUTED TESTS PASSED!")
347+
log("=" * 60)
348+
349+
dist.destroy_process_group()
350+
351+
352+
if __name__ == "__main__":
353+
main()

0 commit comments

Comments
 (0)