Skip to content

Commit d4fbb31

Browse files
committed
[DTensor] Fix CI: Python 3.10 compat, lint, and pyi stubs
- Fix _deduplicate_src_specs to use hashable (start, stop) tuples instead of slice objects as dict keys (slice is unhashable on Python < 3.12) - Add dtensor_send/dtensor_recv stubs to tensorclass.pyi - Fix lint: remove unused imports (TensorDictPipe TYPE_CHECKING, Sequence in megatron, _TransferPlan/ParameterPlan in tests), unused variable (sends in ModelTransferPlan.execute), loop variable naming (key -> _key in base.py) - Fix docstring formatting (D205/D415 in _chunk_slice, ModelTransferPlan) - Add examples/*.py to T201 (print) lint ignore in setup.cfg - Remove unused variable in example file - Auto-format with ufmt/black Made-with: Cursor ghstack-source-id: 4fd0dc9 Pull-Request: #1652 Made-with: Cursor
1 parent ba7f65c commit d4fbb31

10 files changed

Lines changed: 182 additions & 140 deletions

File tree

examples/dtensor_transfer_distributed_test.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222

2323
import torch
2424
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
2825

2926
from tensordict import TensorDict
27+
from torch.distributed.device_mesh import DeviceMesh
28+
from torch.distributed.tensor import distribute_tensor, Shard
3029

3130

3231
def log(msg: str):
@@ -54,9 +53,7 @@ def test_strategy_a_materialize():
5453
mesh = DeviceMesh("cuda", torch.arange(world_size))
5554

5655
torch.manual_seed(42)
57-
full_a = torch.arange(
58-
world_size * 10, dtype=torch.float32, device="cuda"
59-
)
56+
full_a = torch.arange(world_size * 10, dtype=torch.float32, device="cuda")
6057
full_b = torch.randn(4, world_size * 8, dtype=torch.float32, device="cuda")
6158

6259
dt_a = distribute_tensor(full_a, mesh, [Shard(0)])
@@ -120,9 +117,7 @@ def test_strategy_b_redistribute():
120117

121118
mesh = DeviceMesh("cuda", torch.arange(world_size))
122119

123-
full_tensor = torch.arange(
124-
world_size * 12, dtype=torch.float32, device="cuda"
125-
)
120+
full_tensor = torch.arange(world_size * 12, dtype=torch.float32, device="cuda")
126121
dt = distribute_tensor(full_tensor, mesh, [Shard(0)])
127122
td_src = TensorDict(weight=dt)
128123

@@ -151,9 +146,9 @@ def test_strategy_b_redistribute():
151146

152147
expected_local = list(full_tensor.chunk(world_size))[0]
153148
received = td_recv["weight"]
154-
assert torch.allclose(received, expected_local), (
155-
f"weight mismatch: got {received}, expected {expected_local}"
156-
)
149+
assert torch.allclose(
150+
received, expected_local
151+
), f"weight mismatch: got {received}, expected {expected_local}"
157152
log(" Verification PASSED!")
158153

159154
dist.barrier()
@@ -332,8 +327,10 @@ def main():
332327

333328
torch.cuda.set_device(rank % torch.cuda.device_count())
334329

335-
log(f"Initialized: world_size={world_size}, "
336-
f"device=cuda:{rank % torch.cuda.device_count()}")
330+
log(
331+
f"Initialized: world_size={world_size}, "
332+
f"device=cuda:{rank % torch.cuda.device_count()}"
333+
)
337334

338335
test_plain_tensor()
339336
test_strategy_a_materialize()

examples/dtensor_transfer_plan_test.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212

1313
import torch
1414

15-
from tensordict._dtensor import (
16-
_compute_all_local_slices,
17-
_compute_transfer_plan,
18-
)
15+
from tensordict._dtensor import _compute_all_local_slices, _compute_transfer_plan
1916
from torch.distributed.tensor.placement_types import Replicate, Shard
2017

2118

@@ -52,9 +49,9 @@ def test_shard4_to_shard2():
5249

5350
expected = list(full.chunk(2))
5451
for i in range(2):
55-
assert torch.equal(dst_shards[i], expected[i]), (
56-
f"rank {i}: expected {expected[i]}, got {dst_shards[i]}"
57-
)
52+
assert torch.equal(
53+
dst_shards[i], expected[i]
54+
), f"rank {i}: expected {expected[i]}, got {dst_shards[i]}"
5855
print(" PASSED\n")
5956

6057

@@ -78,8 +75,7 @@ def test_2d_to_1d():
7875
print(f" Number of transfers: {len(plan.transfers)}")
7976
for t in plan.transfers:
8077
print(
81-
f" rank {t.src_rank} -> rank {t.dst_rank}: "
82-
f"global {t.global_slices}"
78+
f" rank {t.src_rank} -> rank {t.dst_rank}: " f"global {t.global_slices}"
8379
)
8480

8581
top = full[:5]
@@ -134,8 +130,7 @@ def test_dp_tp_to_tp_only():
134130
print("\n Transfers:")
135131
for t in plan.transfers:
136132
print(
137-
f" rank {t.src_rank} -> rank {t.dst_rank}: "
138-
f"global {t.global_slices}"
133+
f" rank {t.src_rank} -> rank {t.dst_rank}: " f"global {t.global_slices}"
139134
)
140135

141136
src_shards = {s.rank: full[s.slices].clone() for s in src_specs}
@@ -148,9 +143,9 @@ def test_dp_tp_to_tp_only():
148143
for s in dst_specs:
149144
expected = full[s.slices]
150145
actual = dst_buffers[s.rank]
151-
assert torch.equal(actual, expected), (
152-
f"rank {s.rank}: mismatch!\n expected: {expected}\n got: {actual}"
153-
)
146+
assert torch.equal(
147+
actual, expected
148+
), f"rank {s.rank}: mismatch!\n expected: {expected}\n got: {actual}"
154149
print(" PASSED\n")
155150

156151

@@ -160,8 +155,6 @@ def test_replicate_to_shard():
160155
print("Test: Replicate on 4 ranks -> Shard(0) on 2 ranks")
161156
print("=" * 60)
162157

163-
full = torch.arange(100, dtype=torch.float32)
164-
165158
plan = _compute_transfer_plan(
166159
global_shape=[100],
167160
src_mesh_shape=[4],
@@ -173,12 +166,11 @@ def test_replicate_to_shard():
173166
print(f" Number of transfers: {len(plan.transfers)}")
174167
for t in plan.transfers:
175168
print(
176-
f" rank {t.src_rank} -> rank {t.dst_rank}: "
177-
f"global {t.global_slices}"
169+
f" rank {t.src_rank} -> rank {t.dst_rank}: " f"global {t.global_slices}"
178170
)
179-
assert all(t.src_rank == 0 for t in plan.transfers), (
180-
"Expected deduplication to use only rank 0 as source"
181-
)
171+
assert all(
172+
t.src_rank == 0 for t in plan.transfers
173+
), "Expected deduplication to use only rank 0 as source"
182174
print(" PASSED\n")
183175

184176

examples/minimal_p2p_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Minimal NCCL P2P test to verify send/recv works."""
33

44
import json
5+
56
import torch
67
import torch.distributed as dist
78

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ per-file-ignores =
1818
packaging/*/**.py: T201
1919
.github/scripts/*.py: T201
2020
benchmarks/*.py: T201
21+
examples/*.py: T201
2122
exclude = venv
2223
extend-select = B901, C401, C408, C409
2324

tensordict/_dtensor.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,11 @@
1717
import json
1818
import struct
1919
from dataclasses import dataclass, field
20-
from typing import Any, Callable, Protocol, runtime_checkable, Sequence, TYPE_CHECKING
20+
from typing import Any, Callable, Protocol, runtime_checkable, Sequence
2121

22-
import numpy as np
2322
import torch
2423
from torch import Tensor
2524

26-
if TYPE_CHECKING:
27-
from tensordict._ucxx import TensorDictPipe
28-
2925
_has_ucxx = importlib.util.find_spec("ucxx") is not None
3026

3127

@@ -194,8 +190,9 @@ def sharded(
194190

195191

196192
def _chunk_slice(total_size: int, num_chunks: int, chunk_idx: int) -> slice:
197-
"""Return the slice for ``chunk_idx`` when splitting *total_size* into
198-
*num_chunks* using ``torch.chunk`` semantics (last chunk may be smaller).
193+
"""Return the slice for ``chunk_idx`` when splitting into chunks.
194+
195+
Uses ``torch.chunk`` semantics (last chunk may be smaller).
199196
"""
200197
chunk_size = (total_size + num_chunks - 1) // num_chunks
201198
start = chunk_idx * chunk_size
@@ -313,9 +310,9 @@ def _deduplicate_src_specs(
313310
if not has_replica:
314311
return src_specs
315312

316-
seen: dict[tuple[slice, ...], _ShardSpec] = {}
313+
seen: dict[tuple[tuple[int, int], ...], _ShardSpec] = {}
317314
for spec in src_specs:
318-
key = spec.slices
315+
key = tuple((s.start, s.stop) for s in spec.slices)
319316
if key not in seen or spec.rank < seen[key].rank:
320317
seen[key] = spec
321318
return list(seen.values())
@@ -356,9 +353,7 @@ def _compute_transfer_plan(
356353
)
357354

358355
# Deduplicate replicated src specs
359-
src_specs_dedup = _deduplicate_src_specs(
360-
src_specs, src_placements, src_mesh_shape
361-
)
356+
src_specs_dedup = _deduplicate_src_specs(src_specs, src_placements, src_mesh_shape)
362357

363358
plan = _TransferPlan(global_shape=global_shape)
364359

@@ -422,9 +417,7 @@ def execute_transfer_plan(
422417

423418
if dst_buffer is not None:
424419
for transfer in recvs:
425-
chunk_shape = tuple(
426-
s.stop - s.start for s in transfer.global_slices
427-
)
420+
chunk_shape = tuple(s.stop - s.start for s in transfer.global_slices)
428421
buf = torch.empty(
429422
chunk_shape, dtype=dst_buffer.dtype, device=dst_buffer.device
430423
)
@@ -486,7 +479,7 @@ def recv_object(self, src: int) -> Any:
486479
length = int(length_t.item())
487480
data_t = torch.empty(length, dtype=torch.uint8, device="cuda")
488481
dist.recv(data_t, src=src, group=self.group)
489-
return json.loads(bytes(data_t.cpu().numpy()))
482+
return json.loads(bytes(data_t.cpu().tolist()))
490483

491484

492485
class _UCXXBackend:
@@ -510,10 +503,10 @@ class _UCXXBackend:
510503
def __init__(self, endpoint):
511504
self._endpoint = endpoint
512505

513-
def _tensor_to_numpy(self, t: Tensor) -> np.ndarray:
514-
return np.frombuffer(
515-
t.contiguous().view(torch.uint8).numpy(), dtype=np.uint8
516-
)
506+
def _tensor_to_numpy(self, t: Tensor):
507+
import numpy as np
508+
509+
return np.frombuffer(t.contiguous().view(torch.uint8).numpy(), dtype=np.uint8)
517510

518511
def send_tensor(self, tensor: Tensor, dst: int, *, tag: int = 0) -> None:
519512
import asyncio
@@ -550,12 +543,16 @@ def recv_object(self, src: int) -> Any:
550543
return asyncio.run(self._arecv_object())
551544

552545
async def _asend_object(self, obj: Any) -> None:
546+
import numpy as np
547+
553548
data = json.dumps(obj).encode("utf-8")
554549
length = struct.pack("<Q", len(data))
555550
await self._endpoint.send(np.frombuffer(length, dtype=np.uint8))
556551
await self._endpoint.send(np.frombuffer(data, dtype=np.uint8).copy())
557552

558553
async def _arecv_object(self) -> Any:
554+
import numpy as np
555+
559556
len_buf = np.empty(8, dtype=np.uint8)
560557
await self._endpoint.recv(len_buf)
561558
length = struct.unpack("<Q", len_buf.tobytes())[0]
@@ -642,9 +639,9 @@ class ParameterPlan:
642639

643640

644641
class ModelTransferPlan:
645-
"""Precomputed plan for transferring an entire model's parameters
646-
between two differently-sharded meshes.
642+
"""Precomputed plan for transferring an entire model's parameters.
647643
644+
Transfers between two differently-sharded meshes.
648645
Designed for LLM post-training: compute once at setup, execute
649646
every training iteration with near-zero overhead.
650647
@@ -662,7 +659,9 @@ class ModelTransferPlan:
662659
)
663660
"""
664661

665-
def __init__(self, param_plans: list[ParameterPlan], batches: list[list[ParameterPlan]]):
662+
def __init__(
663+
self, param_plans: list[ParameterPlan], batches: list[list[ParameterPlan]]
664+
):
666665
self._param_plans = param_plans
667666
self._batches = batches
668667

@@ -803,7 +802,6 @@ def execute(
803802
if src_tensor is not None and pp.transform is not None:
804803
src_tensor = pp.transform(src_tensor)
805804

806-
sends = pp.plan.sends_for_rank(rank)
807805
recvs = pp.plan.recvs_for_rank(rank)
808806

809807
# Allocate dst buffer if this rank receives data
@@ -832,9 +830,7 @@ def execute(
832830
return result
833831

834832
@staticmethod
835-
def _rank_coords_for(
836-
rank: int, desc: ShardingDescriptor
837-
) -> tuple[int, ...]:
833+
def _rank_coords_for(rank: int, desc: ShardingDescriptor) -> tuple[int, ...]:
838834
"""Find the mesh coordinates for *rank* in the descriptor's mesh."""
839835
if desc.rank_map is not None:
840836
for coords, r in desc.rank_map.items():
@@ -889,7 +885,9 @@ def summary(self) -> str:
889885
if n_optimal:
890886
lines.append(f" Strategy C (optimal P2P): {n_optimal} params")
891887
if n_materialize:
892-
lines.append(f" Strategy A (materialize): {n_materialize} params (have transforms)")
888+
lines.append(
889+
f" Strategy A (materialize): {n_materialize} params (have transforms)"
890+
)
893891
if n_direct:
894892
lines.append(f" Direct copy: {n_direct} params (same sharding)")
895893
lines.append(f" Total transfer: {self.total_bytes / 1024**2:.1f} MB (float32)")

tensordict/base.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9595,6 +9595,7 @@ def _recv(
95959595
)
95969596

95979597
return _tag
9598+
95989599
def dtensor_send(
95999600
self,
96009601
dst,
@@ -9762,7 +9763,7 @@ def _dtensor_send_materialize(self, dst, *, backend) -> None:
97629763

97639764
dst_int = dst if isinstance(dst, int) else 0
97649765
backend.send_object(metadata, dst_int)
9765-
for key, tensor in tensors:
9766+
for _key, tensor in tensors:
97669767
backend.send_tensor(tensor.contiguous(), dst_int)
97679768

97689769
def _dtensor_recv_materialize(self, src, *, backend) -> None:
@@ -9833,7 +9834,7 @@ def _dtensor_send_redistribute(self, dst, *, backend) -> None:
98339834

98349835
dst_int = dst if isinstance(dst, int) else 0
98359836
backend.send_object(metadata, dst_int)
9836-
for key, tensor in tensors:
9837+
for _key, tensor in tensors:
98379838
backend.send_tensor(tensor.contiguous(), dst_int)
98389839

98399840
def _dtensor_recv_redistribute(self, src, *, backend) -> None:
@@ -9878,13 +9879,12 @@ def _dtensor_send_optimal(self, dst, *, backend, dst_mesh, dst_placements) -> No
98789879
Computes which slices of each local shard need to go to which dst
98799880
rank, then issues targeted P2P sends for just those slices.
98809881
"""
9881-
from torch import distributed as dist
9882-
98839882
from tensordict._dtensor import (
98849883
_compute_transfer_plan,
98859884
_mesh_all_ranks,
98869885
_mesh_to_rank_map,
98879886
)
9887+
from torch import distributed as dist
98889888

98899889
my_rank = dist.get_rank()
98909890

@@ -9946,13 +9946,12 @@ def _dtensor_recv_optimal(self, src, *, backend, src_mesh, src_placements) -> No
99469946
Computes which slices this rank needs and from which src ranks,
99479947
then issues targeted P2P recvs and assembles the local shard.
99489948
"""
9949-
from torch import distributed as dist
9950-
99519949
from tensordict._dtensor import (
99529950
_compute_transfer_plan,
99539951
_mesh_all_ranks,
99549952
_mesh_to_rank_map,
99559953
)
9954+
from torch import distributed as dist
99569955

99579956
my_rank = dist.get_rank()
99589957

@@ -10011,9 +10010,7 @@ def _dtensor_recv_optimal(self, src, *, backend, src_mesh, src_placements) -> No
1001110010
local_tensor[transfer.dst_slices] = buf
1001210011
tag += 1
1001310012

10014-
self._set_str(
10015-
key, local_tensor, inplace=True, validated=True
10016-
)
10013+
self._set_str(key, local_tensor, inplace=True, validated=True)
1001710014
else:
1001810015
# Non-DTensor: recv from the first src rank
1001910016
first_src = _mesh_all_ranks(src_mesh)[0]

0 commit comments

Comments
 (0)