|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import argparse |
| 4 | +import os |
| 5 | +import time |
| 6 | + |
| 7 | +import torch |
| 8 | +import torch.distributed as dist |
| 9 | + |
| 10 | +import cutlass.cute as cute |
| 11 | +from cutlass.cute.runtime import from_dlpack |
| 12 | + |
| 13 | + |
| 14 | +def _symm_mem(): |
| 15 | + import torch.distributed._symmetric_memory as symm_mem |
| 16 | + |
| 17 | + return symm_mem |
| 18 | + |
| 19 | + |
| 20 | +@cute.kernel |
| 21 | +def _all_reduce_simple_kernel( |
| 22 | + inputs: list[cute.Tensor], |
| 23 | + output: cute.Tensor, |
| 24 | + thr_layout: cute.Layout, |
| 25 | + val_layout: cute.Layout, |
| 26 | +): |
| 27 | + tidx, _, _ = cute.arch.thread_idx() |
| 28 | + bidx, _, _ = cute.arch.block_idx() |
| 29 | + |
| 30 | + blk_coord = ((None, None), bidx) |
| 31 | + local_tile_out = output[blk_coord] |
| 32 | + local_tile_list = [tensor[blk_coord] for tensor in inputs] |
| 33 | + |
| 34 | + assert all(tensor.element_type == inputs[0].element_type for tensor in inputs) |
| 35 | + |
| 36 | + copy_atom_load = cute.make_copy_atom( |
| 37 | + cute.nvgpu.CopyUniversalOp(), |
| 38 | + inputs[0].element_type, |
| 39 | + ) |
| 40 | + copy_atom_store = cute.make_copy_atom( |
| 41 | + cute.nvgpu.CopyUniversalOp(), |
| 42 | + inputs[0].element_type, |
| 43 | + ) |
| 44 | + tiled_copy = cute.make_tiled_copy_tv(copy_atom_load, thr_layout, val_layout) |
| 45 | + thr_copy = tiled_copy.get_slice(tidx) |
| 46 | + |
| 47 | + thr_tensor_list = [thr_copy.partition_S(tensor) for tensor in local_tile_list] |
| 48 | + thr_out = thr_copy.partition_D(local_tile_out) |
| 49 | + frg_tensor_list = [cute.make_fragment_like(tensor) for tensor in thr_tensor_list] |
| 50 | + frg_acc = cute.make_fragment_like(thr_out) |
| 51 | + frg_acc.fill(0.0) |
| 52 | + |
| 53 | + for thr_tensor, frg_tensor in zip(thr_tensor_list, frg_tensor_list): |
| 54 | + cute.copy(copy_atom_load, thr_tensor, frg_tensor) |
| 55 | + frg_acc.store(frg_tensor.load() + frg_acc.load()) |
| 56 | + |
| 57 | + cute.copy(copy_atom_store, frg_acc, thr_out) |
| 58 | + |
| 59 | + |
| 60 | +@cute.jit |
| 61 | +def _all_reduce_simple( |
| 62 | + inputs: list[cute.Tensor], |
| 63 | + output: cute.Tensor, |
| 64 | +): |
| 65 | + thr_layout = cute.make_ordered_layout((4, 32), order=(1, 0)) |
| 66 | + val_layout = cute.make_ordered_layout((4, 4), order=(1, 0)) |
| 67 | + tiler_mn, tv_layout = cute.make_layout_tv(thr_layout, val_layout) |
| 68 | + |
| 69 | + divided_inputs = [cute.zipped_divide(tensor, tiler_mn) for tensor in inputs] |
| 70 | + divided_output = cute.zipped_divide(output, tiler_mn) |
| 71 | + _all_reduce_simple_kernel( |
| 72 | + divided_inputs, |
| 73 | + divided_output, |
| 74 | + thr_layout, |
| 75 | + val_layout, |
| 76 | + ).launch( |
| 77 | + grid=[cute.size(divided_output, mode=[1]), 1, 1], |
| 78 | + block=[cute.size(tv_layout, mode=[0]), 1, 1], |
| 79 | + ) |
| 80 | + |
| 81 | + |
| 82 | +def symmetric_memory_peer_tensors( |
| 83 | + local_tensor: torch.Tensor, |
| 84 | + group: dist.ProcessGroup | str | None = None, |
| 85 | +) -> tuple[object, list[torch.Tensor]]: |
| 86 | + """Rendezvous a PyTorch symmetric-memory tensor and return all peer views.""" |
| 87 | + if group is None: |
| 88 | + group = dist.group.WORLD |
| 89 | + handle = _symm_mem().rendezvous(local_tensor, group=group) |
| 90 | + return handle, [ |
| 91 | + handle.get_buffer(peer_rank, local_tensor.shape, local_tensor.dtype) |
| 92 | + for peer_rank in range(handle.world_size) |
| 93 | + ] |
| 94 | + |
| 95 | + |
| 96 | +def compile_symmetric_memory_all_reduce( |
| 97 | + peer_tensors: list[torch.Tensor], |
| 98 | + output: torch.Tensor, |
| 99 | +): |
| 100 | + """Compile the simple CuTeDSL peer-load all-reduce for the given tensor layouts.""" |
| 101 | + if output.dtype != torch.float32 or any( |
| 102 | + tensor.dtype != torch.float32 for tensor in peer_tensors |
| 103 | + ): |
| 104 | + raise TypeError("symmetric_memory_all_reduce currently expects float32 tensors") |
| 105 | + return cute.compile( |
| 106 | + _all_reduce_simple, |
| 107 | + [from_dlpack(tensor) for tensor in peer_tensors], |
| 108 | + from_dlpack(output), |
| 109 | + ) |
| 110 | + |
| 111 | + |
| 112 | +def symmetric_memory_all_reduce( |
| 113 | + peer_tensors: list[torch.Tensor], |
| 114 | + output: torch.Tensor, |
| 115 | + compiled=None, |
| 116 | +): |
| 117 | + """Run the simple CuTeDSL all-reduce over PyTorch symmetric-memory peer tensors.""" |
| 118 | + if compiled is None: |
| 119 | + compiled = compile_symmetric_memory_all_reduce(peer_tensors, output) |
| 120 | + compiled( |
| 121 | + [from_dlpack(tensor) for tensor in peer_tensors], |
| 122 | + from_dlpack(output), |
| 123 | + ) |
| 124 | + return output |
| 125 | + |
| 126 | + |
| 127 | +def init_torchrun_process_group() -> None: |
| 128 | + """Initialize CUDA and torch.distributed for `torchrun` launched examples.""" |
| 129 | + os.environ.setdefault("TORCH_SYMM_MEM_DISABLE_MULTICAST", "1") |
| 130 | + local_rank = int(os.environ["LOCAL_RANK"]) |
| 131 | + torch.cuda.set_device(local_rank) |
| 132 | + dist.init_process_group(backend="cpu:gloo,cuda:nccl") |
| 133 | + |
| 134 | + |
| 135 | +def run_symmetric_memory_all_reduce_example( |
| 136 | + m: int, |
| 137 | + n: int, |
| 138 | + *, |
| 139 | + warmup_iterations: int = 2, |
| 140 | + iterations: int = 10, |
| 141 | + skip_ref_check: bool = False, |
| 142 | + benchmark: bool = False, |
| 143 | +) -> torch.Tensor: |
| 144 | + """Run a torchrun-friendly CuTeDSL all-reduce example using PyTorch symmetric memory.""" |
| 145 | + rank = dist.get_rank() |
| 146 | + world_size = dist.get_world_size() |
| 147 | + device = torch.device("cuda", torch.cuda.current_device()) |
| 148 | + if rank == 0: |
| 149 | + print("\nRunning CuTeDSL symmetric-memory all-reduce with:") |
| 150 | + print(f"Tensor dimensions: [{m}, {n}]") |
| 151 | + print(f"GPU count: {world_size}") |
| 152 | + |
| 153 | + local_tensor = _symm_mem().empty((m, n), dtype=torch.float32, device=device) |
| 154 | + local_tensor.random_(0, 100) |
| 155 | + _, peer_tensors = symmetric_memory_peer_tensors(local_tensor) |
| 156 | + output = torch.zeros((m, n), device=device) |
| 157 | + |
| 158 | + if rank == 0: |
| 159 | + print("Compiling kernel with cute.compile ...") |
| 160 | + start_time = time.time() |
| 161 | + compiled = compile_symmetric_memory_all_reduce(peer_tensors, output) |
| 162 | + if rank == 0: |
| 163 | + print(f"Compilation time: {time.time() - start_time:.4f} seconds") |
| 164 | + |
| 165 | + if not skip_ref_check: |
| 166 | + dist.barrier(device_ids=[device.index]) |
| 167 | + symmetric_memory_all_reduce(peer_tensors, output, compiled) |
| 168 | + dist.barrier(device_ids=[device.index]) |
| 169 | + torch.testing.assert_close(sum(tensor.cpu() for tensor in peer_tensors), output.cpu()) |
| 170 | + if rank == 0: |
| 171 | + print("Results verified successfully!") |
| 172 | + |
| 173 | + if not benchmark: |
| 174 | + return output |
| 175 | + |
| 176 | + for _ in range(warmup_iterations): |
| 177 | + symmetric_memory_all_reduce(peer_tensors, output, compiled) |
| 178 | + torch.cuda.synchronize() |
| 179 | + |
| 180 | + start = torch.cuda.Event(enable_timing=True) |
| 181 | + end = torch.cuda.Event(enable_timing=True) |
| 182 | + start.record() |
| 183 | + for _ in range(iterations): |
| 184 | + symmetric_memory_all_reduce(peer_tensors, output, compiled) |
| 185 | + end.record() |
| 186 | + end.synchronize() |
| 187 | + avg_time_us = start.elapsed_time(end) * 1000 / iterations |
| 188 | + |
| 189 | + if rank == 0: |
| 190 | + bytes_moved = (world_size + 1) * output.numel() * output.element_size() |
| 191 | + print(f"Kernel execution time: {avg_time_us / 1e3:.4f} ms") |
| 192 | + print(f"Achieved memory throughput: {bytes_moved / (avg_time_us / 1e6) / 1e9:.2f} GB/s") |
| 193 | + print(f"First few elements of result:\n{output[:3, :3]}") |
| 194 | + |
| 195 | + return output |
| 196 | + |
| 197 | + |
| 198 | +def main() -> None: |
| 199 | + parser = argparse.ArgumentParser( |
| 200 | + description="simple CuTeDSL all-reduce using PyTorch symmetric memory" |
| 201 | + ) |
| 202 | + parser.add_argument("--M", default=1024, type=int) |
| 203 | + parser.add_argument("--N", default=1024, type=int) |
| 204 | + parser.add_argument("--warmup_iterations", default=2, type=int) |
| 205 | + parser.add_argument("--iterations", default=10, type=int) |
| 206 | + parser.add_argument("--skip_ref_check", action="store_true") |
| 207 | + parser.add_argument("--benchmark", action="store_true") |
| 208 | + args = parser.parse_args() |
| 209 | + |
| 210 | + init_torchrun_process_group() |
| 211 | + try: |
| 212 | + run_symmetric_memory_all_reduce_example( |
| 213 | + args.M, |
| 214 | + args.N, |
| 215 | + warmup_iterations=args.warmup_iterations, |
| 216 | + iterations=args.iterations, |
| 217 | + skip_ref_check=args.skip_ref_check, |
| 218 | + benchmark=args.benchmark, |
| 219 | + ) |
| 220 | + finally: |
| 221 | + dist.destroy_process_group() |
| 222 | + |
| 223 | + |
| 224 | +if __name__ == "__main__": |
| 225 | + main() |
0 commit comments