Skip to content

Commit e50fbc8

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: 92ae2d9 Pull-Request: #1652
1 parent 0cbd9eb commit e50fbc8

10 files changed

Lines changed: 174 additions & 137 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: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@
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

2222
import numpy as np
2323
import torch
2424
from torch import Tensor
2525

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

3128

@@ -194,8 +191,9 @@ def sharded(
194191

195192

196193
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).
194+
"""Return the slice for ``chunk_idx`` when splitting into chunks.
195+
196+
Uses ``torch.chunk`` semantics (last chunk may be smaller).
199197
"""
200198
chunk_size = (total_size + num_chunks - 1) // num_chunks
201199
start = chunk_idx * chunk_size
@@ -313,9 +311,9 @@ def _deduplicate_src_specs(
313311
if not has_replica:
314312
return src_specs
315313

316-
seen: dict[tuple[slice, ...], _ShardSpec] = {}
314+
seen: dict[tuple[tuple[int, int], ...], _ShardSpec] = {}
317315
for spec in src_specs:
318-
key = spec.slices
316+
key = tuple((s.start, s.stop) for s in spec.slices)
319317
if key not in seen or spec.rank < seen[key].rank:
320318
seen[key] = spec
321319
return list(seen.values())
@@ -356,9 +354,7 @@ def _compute_transfer_plan(
356354
)
357355

358356
# Deduplicate replicated src specs
359-
src_specs_dedup = _deduplicate_src_specs(
360-
src_specs, src_placements, src_mesh_shape
361-
)
357+
src_specs_dedup = _deduplicate_src_specs(src_specs, src_placements, src_mesh_shape)
362358

363359
plan = _TransferPlan(global_shape=global_shape)
364360

@@ -422,9 +418,7 @@ def execute_transfer_plan(
422418

423419
if dst_buffer is not None:
424420
for transfer in recvs:
425-
chunk_shape = tuple(
426-
s.stop - s.start for s in transfer.global_slices
427-
)
421+
chunk_shape = tuple(s.stop - s.start for s in transfer.global_slices)
428422
buf = torch.empty(
429423
chunk_shape, dtype=dst_buffer.dtype, device=dst_buffer.device
430424
)
@@ -496,9 +490,7 @@ def __init__(self, endpoint):
496490
self._endpoint = endpoint
497491

498492
def _tensor_to_numpy(self, t: Tensor) -> np.ndarray:
499-
return np.frombuffer(
500-
t.contiguous().view(torch.uint8).numpy(), dtype=np.uint8
501-
)
493+
return np.frombuffer(t.contiguous().view(torch.uint8).numpy(), dtype=np.uint8)
502494

503495
def send_tensor(self, tensor: Tensor, dst: int, *, tag: int = 0) -> None:
504496
import asyncio
@@ -627,9 +619,9 @@ class ParameterPlan:
627619

628620

629621
class ModelTransferPlan:
630-
"""Precomputed plan for transferring an entire model's parameters
631-
between two differently-sharded meshes.
622+
"""Precomputed plan for transferring an entire model's parameters.
632623
624+
Transfers between two differently-sharded meshes.
633625
Designed for LLM post-training: compute once at setup, execute
634626
every training iteration with near-zero overhead.
635627
@@ -647,7 +639,9 @@ class ModelTransferPlan:
647639
)
648640
"""
649641

650-
def __init__(self, param_plans: list[ParameterPlan], batches: list[list[ParameterPlan]]):
642+
def __init__(
643+
self, param_plans: list[ParameterPlan], batches: list[list[ParameterPlan]]
644+
):
651645
self._param_plans = param_plans
652646
self._batches = batches
653647

@@ -784,7 +778,6 @@ def execute(
784778
if src_tensor is not None and pp.transform is not None:
785779
src_tensor = pp.transform(src_tensor)
786780

787-
sends = pp.plan.sends_for_rank(rank)
788781
recvs = pp.plan.recvs_for_rank(rank)
789782

790783
# Allocate dst buffer if this rank receives data
@@ -810,9 +803,7 @@ def execute(
810803
return result
811804

812805
@staticmethod
813-
def _rank_coords_for(
814-
rank: int, desc: ShardingDescriptor
815-
) -> tuple[int, ...]:
806+
def _rank_coords_for(rank: int, desc: ShardingDescriptor) -> tuple[int, ...]:
816807
"""Find the mesh coordinates for *rank* in the descriptor's mesh."""
817808
if desc.rank_map is not None:
818809
for coords, r in desc.rank_map.items():
@@ -867,7 +858,9 @@ def summary(self) -> str:
867858
if n_optimal:
868859
lines.append(f" Strategy C (optimal P2P): {n_optimal} params")
869860
if n_materialize:
870-
lines.append(f" Strategy A (materialize): {n_materialize} params (have transforms)")
861+
lines.append(
862+
f" Strategy A (materialize): {n_materialize} params (have transforms)"
863+
)
871864
if n_direct:
872865
lines.append(f" Direct copy: {n_direct} params (same sharding)")
873866
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
@@ -9462,6 +9462,7 @@ def _recv(
94629462
)
94639463

94649464
return _tag
9465+
94659466
def dtensor_send(
94669467
self,
94679468
dst,
@@ -9620,7 +9621,7 @@ def _dtensor_send_materialize(self, dst, *, backend) -> None:
96209621

96219622
dst_int = dst if isinstance(dst, int) else 0
96229623
backend.send_object(metadata, dst_int)
9623-
for key, tensor in tensors:
9624+
for _key, tensor in tensors:
96249625
backend.send_tensor(tensor.contiguous(), dst_int)
96259626

96269627
def _dtensor_recv_materialize(self, src, *, backend) -> None:
@@ -9691,7 +9692,7 @@ def _dtensor_send_redistribute(self, dst, *, backend) -> None:
96919692

96929693
dst_int = dst if isinstance(dst, int) else 0
96939694
backend.send_object(metadata, dst_int)
9694-
for key, tensor in tensors:
9695+
for _key, tensor in tensors:
96959696
backend.send_tensor(tensor.contiguous(), dst_int)
96969697

96979698
def _dtensor_recv_redistribute(self, src, *, backend) -> None:
@@ -9734,13 +9735,12 @@ def _dtensor_send_optimal(self, dst, *, backend, dst_mesh, dst_placements) -> No
97349735
Computes which slices of each local shard need to go to which dst
97359736
rank, then issues targeted P2P sends for just those slices.
97369737
"""
9737-
from torch import distributed as dist
9738-
97399738
from tensordict._dtensor import (
97409739
_compute_transfer_plan,
97419740
_mesh_all_ranks,
97429741
_mesh_to_rank_map,
97439742
)
9743+
from torch import distributed as dist
97449744

97459745
my_rank = dist.get_rank()
97469746

@@ -9802,13 +9802,12 @@ def _dtensor_recv_optimal(self, src, *, backend, src_mesh, src_placements) -> No
98029802
Computes which slices this rank needs and from which src ranks,
98039803
then issues targeted P2P recvs and assembles the local shard.
98049804
"""
9805-
from torch import distributed as dist
9806-
98079805
from tensordict._dtensor import (
98089806
_compute_transfer_plan,
98099807
_mesh_all_ranks,
98109808
_mesh_to_rank_map,
98119809
)
9810+
from torch import distributed as dist
98129811

98139812
my_rank = dist.get_rank()
98149813

@@ -9867,9 +9866,7 @@ def _dtensor_recv_optimal(self, src, *, backend, src_mesh, src_placements) -> No
98679866
local_tensor[transfer.dst_slices] = buf
98689867
tag += 1
98699868

9870-
self._set_str(
9871-
key, local_tensor, inplace=True, validated=True
9872-
)
9869+
self._set_str(key, local_tensor, inplace=True, validated=True)
98739870
else:
98749871
# Non-DTensor: recv from the first src rank
98759872
first_src = _mesh_all_ranks(src_mesh)[0]

tensordict/dtensor_adapters/megatron.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from __future__ import annotations
1919

20-
from typing import Callable, Sequence
20+
from typing import Callable
2121

2222
import torch
2323

@@ -47,7 +47,9 @@ def __init__(
4747
self._tp_size = dist.get_world_size(tp_group)
4848
self._tp_group = tp_group
4949
self._tp_ranks = list(range(dist.get_world_size(tp_group)))
50-
self._tp_rank_map = {(i,): dist.get_global_rank(tp_group, i) for i in self._tp_ranks}
50+
self._tp_rank_map = {
51+
(i,): dist.get_global_rank(tp_group, i) for i in self._tp_ranks
52+
}
5153

5254
self._ep_group = ep_group
5355
self._ep_size = dist.get_world_size(ep_group) if ep_group is not None else 1

tensordict/dtensor_adapters/vllm.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,5 @@ def describe(self, name: str, param) -> ShardingDescriptor:
8080
def describe_model(self, model) -> dict[str, ShardingDescriptor]:
8181
"""Describe all parameters in a vLLM model."""
8282
return {
83-
name: self.describe(name, param)
84-
for name, param in model.named_parameters()
83+
name: self.describe(name, param) for name, param in model.named_parameters()
8584
}

0 commit comments

Comments
 (0)