Skip to content

Commit f94ee59

Browse files
committed
[DTensor] Add Strategy C (optimal P2P transfer using transfer plan)
Implement _dtensor_send_optimal and _dtensor_recv_optimal: - Sender: computes transfer plan from src/dst meshes and placements, extracts only the needed slices from local shards, and sends via P2P - Receiver: computes same plan, receives slices into the right positions of the local buffer, wraps as DTensor via from_local() - Both torch.distributed and UCXX transports supported Update "auto" strategy resolution to pick "optimal" when dst_mesh/src_mesh and dst_placements/src_placements are provided, falling back to "materialize" otherwise. Add _mesh_to_rank_map and _mesh_all_ranks helpers to _dtensor.py. Made-with: Cursor ghstack-source-id: faa4852 Pull-Request: #1642
1 parent 9db6031 commit f94ee59

2 files changed

Lines changed: 169 additions & 9 deletions

File tree

tensordict/_dtensor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,22 @@ def _get_transport_backend(
431431
f"Unknown transport {transport!r}. "
432432
"Expected 'torch_distributed', 'ucxx', or 'auto'."
433433
)
434+
435+
436+
# ---------------------------------------------------------------------------
437+
# DeviceMesh helpers
438+
# ---------------------------------------------------------------------------
439+
440+
441+
def _mesh_to_rank_map(mesh) -> dict[tuple[int, ...], int]:
442+
"""Convert a DeviceMesh to a {coords: global_rank} dict."""
443+
mesh_tensor = mesh.mesh
444+
result = {}
445+
for idx in itertools.product(*(range(s) for s in mesh_tensor.shape)):
446+
result[idx] = int(mesh_tensor[idx].item())
447+
return result
448+
449+
450+
def _mesh_all_ranks(mesh) -> list[int]:
451+
"""Return all global ranks in a DeviceMesh (flat, sorted)."""
452+
return sorted(mesh.mesh.flatten().tolist())

tensordict/base.py

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9506,7 +9506,11 @@ def dtensor_send(
95069506

95079507
resolved = strategy
95089508
if resolved == "auto":
9509-
resolved = "materialize"
9509+
resolved = (
9510+
"optimal"
9511+
if dst_mesh is not None and dst_placements is not None
9512+
else "materialize"
9513+
)
95109514

95119515
if resolved == "materialize":
95129516
self._dtensor_send_materialize(dst, backend=backend)
@@ -9563,7 +9567,11 @@ def dtensor_recv(
95639567

95649568
resolved = strategy
95659569
if resolved == "auto":
9566-
resolved = "materialize"
9570+
resolved = (
9571+
"optimal"
9572+
if src_mesh is not None and src_placements is not None
9573+
else "materialize"
9574+
)
95679575

95689576
if resolved == "materialize":
95699577
self._dtensor_recv_materialize(src, backend=backend)
@@ -9689,7 +9697,6 @@ def _dtensor_recv_redistribute(self, src, *, backend) -> None:
96899697
dtype = getattr(torch, meta["dtype"].replace("torch.", ""))
96909698
if meta["is_dtensor"]:
96919699
local_shape = torch.Size(meta["local_shape"])
9692-
global_shape = torch.Size(meta["global_shape"])
96939700
buf = torch.empty(local_shape, dtype=dtype)
96949701
backend.recv_tensor(buf, src_int)
96959702
# Store as plain tensor with metadata attached.
@@ -9705,17 +9712,151 @@ def _dtensor_recv_redistribute(self, src, *, backend) -> None:
97059712
self._set_str(key, buf, inplace=False, validated=True)
97069713

97079714
def _dtensor_send_optimal(self, dst, *, backend, dst_mesh, dst_placements) -> None:
9708-
raise NotImplementedError(
9709-
"Strategy 'optimal' is not yet implemented. "
9710-
"Use strategy='materialize' for now."
9715+
"""Send using the optimal P2P transfer plan.
9716+
9717+
Computes which slices of each local shard need to go to which dst
9718+
rank, then issues targeted P2P sends for just those slices.
9719+
"""
9720+
from torch import distributed as dist
9721+
9722+
from tensordict._dtensor import (
9723+
_compute_transfer_plan,
9724+
_mesh_all_ranks,
9725+
_mesh_to_rank_map,
97119726
)
97129727

9728+
my_rank = dist.get_rank()
9729+
9730+
# Normalise dst_placements to per-key dict
9731+
_dst_plc = dst_placements
9732+
if _dst_plc is not None and not isinstance(_dst_plc, dict):
9733+
_dst_plc = {key: _dst_plc for key in self.sorted_keys}
9734+
9735+
tag = 0
9736+
for key in self.sorted_keys:
9737+
value = self._get_str(key, NO_DEFAULT)
9738+
if _is_tensor_collection(type(value)):
9739+
raise NotImplementedError(
9740+
"Nested TensorDicts in dtensor_send are not yet supported."
9741+
)
9742+
9743+
if hasattr(value, "placements"):
9744+
src_mesh = value.device_mesh
9745+
src_placements_t = tuple(value.placements)
9746+
global_shape = value.shape
9747+
local_tensor = value.to_local()
9748+
9749+
key_dst_plc = _dst_plc[key] if _dst_plc is not None else None
9750+
if key_dst_plc is None:
9751+
raise ValueError(
9752+
f"dst_placements is required for optimal strategy, "
9753+
f"missing for key {key!r}."
9754+
)
9755+
9756+
src_mesh_shape = tuple(src_mesh.mesh.shape)
9757+
dst_mesh_shape = tuple(dst_mesh.mesh.shape)
9758+
9759+
src_rank_map = _mesh_to_rank_map(src_mesh)
9760+
dst_rank_map = _mesh_to_rank_map(dst_mesh)
9761+
9762+
plan = _compute_transfer_plan(
9763+
global_shape=global_shape,
9764+
src_mesh_shape=src_mesh_shape,
9765+
src_placements=src_placements_t,
9766+
dst_mesh_shape=dst_mesh_shape,
9767+
dst_placements=key_dst_plc,
9768+
src_rank_map=src_rank_map,
9769+
dst_rank_map=dst_rank_map,
9770+
)
9771+
9772+
for transfer in plan.sends_for_rank(my_rank):
9773+
chunk = local_tensor[transfer.src_slices].contiguous()
9774+
backend.send_tensor(chunk, transfer.dst_rank, tag=tag)
9775+
tag += 1
9776+
else:
9777+
# Non-DTensor: send to all dst ranks
9778+
for dst_rank in _mesh_all_ranks(dst_mesh):
9779+
backend.send_tensor(value.contiguous(), dst_rank, tag=tag)
9780+
tag += 1
9781+
97139782
def _dtensor_recv_optimal(self, src, *, backend, src_mesh, src_placements) -> None:
9714-
raise NotImplementedError(
9715-
"Strategy 'optimal' is not yet implemented. "
9716-
"Use strategy='materialize' for now."
9783+
"""Receive using the optimal P2P transfer plan.
9784+
9785+
Computes which slices this rank needs and from which src ranks,
9786+
then issues targeted P2P recvs and assembles the local shard.
9787+
"""
9788+
from torch import distributed as dist
9789+
9790+
from tensordict._dtensor import (
9791+
_compute_transfer_plan,
9792+
_mesh_all_ranks,
9793+
_mesh_to_rank_map,
97179794
)
97189795

9796+
my_rank = dist.get_rank()
9797+
9798+
# Normalise src_placements to per-key dict
9799+
_src_plc = src_placements
9800+
if _src_plc is not None and not isinstance(_src_plc, dict):
9801+
_src_plc = {key: _src_plc for key in self.sorted_keys}
9802+
9803+
tag = 0
9804+
for key in self.sorted_keys:
9805+
value = self._get_str(key, NO_DEFAULT)
9806+
if _is_tensor_collection(type(value)):
9807+
raise NotImplementedError(
9808+
"Nested TensorDicts in dtensor_recv are not yet supported."
9809+
)
9810+
9811+
if hasattr(value, "placements"):
9812+
dst_mesh = value.device_mesh
9813+
dst_placements_t = tuple(value.placements)
9814+
global_shape = value.shape
9815+
local_tensor = value.to_local()
9816+
9817+
key_src_plc = _src_plc[key] if _src_plc is not None else None
9818+
if key_src_plc is None:
9819+
raise ValueError(
9820+
f"src_placements is required for optimal strategy, "
9821+
f"missing for key {key!r}."
9822+
)
9823+
9824+
src_mesh_shape = tuple(src_mesh.mesh.shape)
9825+
dst_mesh_shape = tuple(dst_mesh.mesh.shape)
9826+
9827+
src_rank_map = _mesh_to_rank_map(src_mesh)
9828+
dst_rank_map = _mesh_to_rank_map(dst_mesh)
9829+
9830+
plan = _compute_transfer_plan(
9831+
global_shape=global_shape,
9832+
src_mesh_shape=src_mesh_shape,
9833+
src_placements=key_src_plc,
9834+
dst_mesh_shape=dst_mesh_shape,
9835+
dst_placements=dst_placements_t,
9836+
src_rank_map=src_rank_map,
9837+
dst_rank_map=dst_rank_map,
9838+
)
9839+
9840+
for transfer in plan.recvs_for_rank(my_rank):
9841+
chunk_shape = tuple(
9842+
s.stop - s.start for s in transfer.global_slices
9843+
)
9844+
buf = torch.empty(chunk_shape, dtype=local_tensor.dtype)
9845+
backend.recv_tensor(buf, transfer.src_rank, tag=tag)
9846+
local_tensor[transfer.dst_slices] = buf
9847+
tag += 1
9848+
9849+
self._set_str(
9850+
key, local_tensor, inplace=True, validated=True
9851+
)
9852+
else:
9853+
# Non-DTensor: recv from the first src rank
9854+
first_src = _mesh_all_ranks(src_mesh)[0]
9855+
buf = torch.empty_like(value)
9856+
backend.recv_tensor(buf, first_src, tag=tag)
9857+
self._set_str(key, buf, inplace=False, validated=True)
9858+
tag += 1
9859+
97199860
def init_remote(
97209861
self,
97219862
dst: int | None = None,

0 commit comments

Comments
 (0)