Skip to content

Commit c8b343f

Browse files
jeffkbkimfacebook-github-bot
authored andcommitted
Fix transfer_tensors_to_cpu crash on mixed CPU/CUDA dicts
Summary: `transfer_tensors_to_cpu` selects the source device from the first CUDA tensor, but the copy loop guarded only on `isinstance(v, torch.Tensor)` and ran the device-to-host path — including `record_stream`, which is CUDA-only — on every tensor. For an input dict that mixes CPU and CUDA tensors this raised `NotImplementedError: aten::record_stream ... 'CPU' backend`. Guard the copy loop so only CUDA tensors are transferred; CPU tensors and non-tensor values pass through unchanged, matching the existing source-device selection guard. Differential Revision: D113422068
1 parent 91fa127 commit c8b343f

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

torchrec/metrics/deferrable_metrics.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ def transfer_tensors_to_cpu(
7777
7878
Returns the CPU tensor dict and a CUDA event recorded on the dedicated
7979
stream that tracks completion. For CPU-only inputs, returns the dict
80-
as-is with None event. Non-tensor values are preserved unchanged.
81-
All tensors are assumed to be on the same device.
80+
as-is with None event. Only CUDA tensors are transferred; CPU tensors and
81+
non-tensor values are passed through unchanged. CUDA tensors are assumed to
82+
share a single device.
8283
"""
8384
source_device: torch.device | None = None
8485
for v in tensors.values():
@@ -99,7 +100,7 @@ def transfer_tensors_to_cpu(
99100
with torch.cuda.stream(dtoh_stream):
100101
cpu_tensors: dict[str, Any] = {}
101102
for k, v in tensors.items():
102-
if isinstance(v, torch.Tensor):
103+
if isinstance(v, torch.Tensor) and v.device.type == "cuda":
103104
dst = torch.empty(
104105
v.shape, dtype=v.dtype, device="cpu", pin_memory=True
105106
)

torchrec/metrics/tests/test_deferrable_metrics.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,31 @@ def test_cuda_transfer_uses_dedicated_stream(self) -> None:
396396
dtoh_stream.synchronize()
397397
self.assertTrue(event.query())
398398

399+
@unittest.skipIf(not torch.cuda.is_available(), "CUDA required")
400+
def test_mixed_cpu_and_cuda_tensors(self) -> None:
401+
"""Regression: a dict mixing CPU and CUDA tensors must not crash.
402+
Previously the copy loop guarded only on isinstance(v, Tensor) and
403+
called v.record_stream (CUDA-only) on the CPU tensors too, raising
404+
NotImplementedError. CPU tensors must pass through untouched while the
405+
CUDA tensor is transferred to CPU."""
406+
cpu_tensor = torch.tensor([1.0, 2.0])
407+
tensors: dict[str, Any] = {
408+
"cuda_val": torch.tensor([0.5], device="cuda"),
409+
"cpu_val": cpu_tensor,
410+
"name": "task1",
411+
}
412+
cpu_tensors, event = transfer_tensors_to_cpu(tensors)
413+
self.assertIsNotNone(event)
414+
# CUDA tensor transferred to CPU.
415+
self.assertEqual(cpu_tensors["cuda_val"].device.type, "cpu")
416+
# CPU tensor passed through as the same object (not copied/pinned).
417+
self.assertIs(cpu_tensors["cpu_val"], cpu_tensor)
418+
# Non-tensor preserved.
419+
self.assertEqual(cpu_tensors["name"], "task1")
420+
if event is not None:
421+
event.synchronize()
422+
torch.testing.assert_close(cpu_tensors["cuda_val"], torch.tensor([0.5]))
423+
399424
@unittest.skipIf(not torch.cuda.is_available(), "CUDA required")
400425
def test_cuda_transfer_destination_is_pinned(self) -> None:
401426
"""The CPU destination must be pinned so cudaMemcpyAsync is truly

0 commit comments

Comments
 (0)