Files
2026-07-13 13:17:40 +08:00

462 lines
13 KiB
Python

# coding: utf-8
import io
import json
import logging
import os
import socket
import time
import cupy
import numpy as np
import torch
import ray
import ray.cloudpickle as pickle
import ray.cluster_utils
from ray._private.ray_microbenchmark_helpers import timeit
from ray.air._internal import torch_utils
from ray.dag import DAGContext, InputNode
from ray.util.collective.collective_group import nccl_util
logger = logging.getLogger(__name__)
SHAPE = None
DTYPE = torch.float16
NUM_ITERS = 10
@ray.remote
class TorchIpcWorker:
def __init__(self):
self.device = torch_utils.get_devices()[0]
def send(self, shape, dtype, value: int):
t = torch.ones(shape, dtype=dtype, device=self.device) * value
if self.device.type == "cuda":
# NOTE(swang): This is needed because the IPC can get sent before
# the value has been written to memory. But somehow the read value
# is still the wrong one?
torch.cuda.synchronize()
h = cupy.cuda.runtime.ipcGetMemHandle(t.data_ptr())
return h
def recv(self, device_ptr, num_bytes, shape, dtype):
h = cupy.cuda.runtime.ipcOpenMemHandle(device_ptr)
m = cupy.cuda.UnownedMemory(h, num_bytes, None)
m_ptr = cupy.cuda.MemoryPointer(m, 0)
tensor = torch.tensor(cupy.ndarray(shape, dtype, m_ptr), device=self.device)
assert tensor.device == self.device
return (tensor[0].item(), tensor.shape, tensor.dtype)
@ray.remote
class TorchTensorWorker:
def __init__(self):
self.device = torch_utils.get_devices()[0]
def send(self, shape, dtype, _):
t = torch.ones(shape, dtype=dtype, device=self.device) * 1
return t
def recv(self, tensor):
# This benchmark tests the overhead of sending a tensor between
# actors. To minimize the overhead of shared memory transfer,
# we return only a byte string.
assert tensor.device == self.device
return b"x"
@ray.remote(num_gpus=1)
class NcclWorker:
def __init__(self, rank):
self.rank = rank
def get_node_id(self):
return ray.get_runtime_context().get_node_id()
def init(self, world_size):
from ray.air._internal import torch_utils
self.device = torch_utils.get_devices()[0]
self.world_size = world_size
torch.distributed.init_process_group(
backend="nccl",
world_size=world_size,
rank=self.rank,
)
def _send(self, buf, num_el, rank):
torch.distributed.send(buf, rank)
def _recv(self, buf, num_el, rank):
torch.distributed.recv(buf, rank)
def do_send_recv(self, shape, dtype):
other_rank = (self.rank + 1) % self.world_size
def _run():
if self.rank == 0:
i = np.random.randint(100)
input_buffer = torch.ones(shape, dtype=dtype, device=self.device) * i
self._send(input_buffer, input_buffer.numel(), other_rank)
else:
input_buffer = torch.empty(shape, dtype=dtype, device=self.device)
self._recv(input_buffer, input_buffer.numel(), other_rank)
torch.cuda.synchronize()
return timeit("exec_nccl_gpu", _run)
def exec_ray_dag(
label,
sender,
receiver,
use_nccl=False,
use_cgraph=True,
static_shape=False,
direct_return=False,
):
# Test torch.Tensor sent between actors.
with InputNode() as inp:
dag = sender.send.bind(SHAPE, DTYPE, inp)
if use_cgraph:
dag = dag.with_tensor_transport(
transport="nccl" if use_nccl else "auto",
_static_shape=static_shape,
_direct_return=direct_return,
)
dag = receiver.recv.bind(dag)
if use_cgraph:
dag = dag.experimental_compile()
def _run():
ref = dag.execute(b"x")
result = ray.get(ref)
assert result == b"x"
else:
def _run():
result = ray.get(dag.execute(b"x"))
assert result == b"x"
results = timeit(label, _run)
if use_cgraph:
dag.teardown()
# Workaround for Ray bug in reusing GPUs too quickly.
# See https://github.com/ray-project/ray/issues/44821.
ray.kill(sender)
ray.kill(receiver)
time.sleep(1)
return results
def exec_ray_dag_ipc(label, sender, receiver, use_nccl=False):
# Test torch.Tensor sent between actors.
with InputNode() as inp:
dag = sender.send.bind(SHAPE, DTYPE, inp)
dag = receiver.recv.bind(
dag,
# torch.float16 has item size of 2 bytes.
SHAPE[0] * 2,
SHAPE,
nccl_util.TORCH_NUMPY_DTYPE_MAP[DTYPE],
)
compiled_dag = dag.experimental_compile(_buffer_size_bytes=int(SHAPE[0] * 3))
# Flag that each run can set if it sees incorrect results.
ok = [True]
def _run():
i = np.random.randint(100)
ref = compiled_dag.execute(i)
result = ray.get(ref)
if result != (i, SHAPE, DTYPE):
ok[0] = False
results = timeit(label, _run)
if not ok[0]:
logger.warning("IPC DAG returned incorrect result")
compiled_dag.teardown()
return results
def _exec_torch_cpu_cpu():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE) * i
t2 = t.to(copy=True)
assert (t2[0].item(), t2.shape, t2.dtype) == (i, SHAPE, DTYPE)
def _exec_torch_gpu():
i = np.random.randint(100)
device_from = torch.device("cuda:1")
device_to = torch.device("cuda:0")
t = torch.ones(SHAPE, dtype=DTYPE, device=device_from) * i
t2 = t.to(device_to)
torch.cuda.synchronize(device_to)
assert (t2[0].item(), t2.shape, t2.dtype) == (i, SHAPE, DTYPE)
def exec_nccl_gpu(sender_hint, receiver_hint):
workers = [
NcclWorker.options(**sender_hint).remote(0),
NcclWorker.options(**receiver_hint).remote(1),
]
# node_id = ray.get(workers[0].get_node_id.remote())
# head_node = [node for node in ray.nodes() if node["NodeID"] == node_id]
# assert len(head_node) == 1
# head_node = head_node[0]
# rank_0_addr = f"{head_node['NodeManagerAddress']}:8888"
ray.get([worker.init.remote(2) for worker in workers])
tasks = [worker.do_send_recv.remote(SHAPE, DTYPE) for worker in workers]
done_refs, _ = ray.wait(tasks, num_returns=1)
results = ray.get(done_refs[0])
# Workaround for Ray bug in reusing GPUs too quickly.
# See https://github.com/ray-project/ray/issues/44821.
for worker in workers:
ray.kill(worker)
time.sleep(1)
return results
def _exec_torch_gpu_cpu_gpu():
i = np.random.randint(100)
device_from = torch.device("cuda:0")
device_to = torch.device("cuda:1")
t = torch.ones(SHAPE, dtype=DTYPE, device=device_from) * i
t = t.to("cpu")
t2 = t.to(device_to)
torch.cuda.synchronize(device_to)
assert (t2[0].item(), t2.shape, t2.dtype) == (i, SHAPE, DTYPE)
def _exec_pickle_cpu():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE) * i
byte_stream = io.BytesIO()
pickle.dump(t, byte_stream)
byte_stream.seek(0)
pickle.load(byte_stream)
def _exec_pickle_gpu():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE, device="cuda") * i
byte_stream = io.BytesIO()
pickle.dump(t, byte_stream)
byte_stream.seek(0)
pickle.load(byte_stream)
def _exec_ray_put_cpu():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE) * i
ray.get(ray.put(t))
def _exec_ray_put_np_zero_copy():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE) * i
torch.as_tensor(ray.get(ray.put(t.numpy())))
def _exec_ray_put_gpu():
i = np.random.randint(100)
t = torch.ones(SHAPE, dtype=DTYPE, device="cuda") * i
ray.get(ray.put(t))
def exec_ray_dag_cpu(sender_hint, receiver_hint):
sender = TorchTensorWorker.options(**sender_hint).remote()
receiver = TorchTensorWorker.options(**receiver_hint).remote()
return exec_ray_dag("exec_ray_dag_cpu", sender, receiver)
def exec_ray_core_cpu(sender_hint, receiver_hint):
time.sleep(1)
sender = TorchTensorWorker.options(**sender_hint).remote()
receiver = TorchTensorWorker.options(**receiver_hint).remote()
return exec_ray_dag("exec_ray_core_cpu", sender, receiver, use_cgraph=False)
def exec_ray_dag_gpu_ipc_gpu():
time.sleep(1)
sender = TorchIpcWorker.options(num_gpus=1).remote()
receiver = TorchIpcWorker.options(num_gpus=1).remote()
return exec_ray_dag_ipc("exec_ray_dag_gpu_ipc_gpu", sender, receiver)
def exec_ray_dag_gpu_cpu_gpu(sender_hint, receiver_hint):
time.sleep(1)
sender = TorchTensorWorker.options(num_gpus=1, **sender_hint).remote()
receiver = TorchTensorWorker.options(num_gpus=1, **receiver_hint).remote()
return exec_ray_dag("exec_ray_dag_gpu_cpu_gpu", sender, receiver)
def exec_ray_dag_gpu_nccl(
sender_hint,
receiver_hint,
static_shape: bool = False,
direct_return: bool = False,
):
time.sleep(1)
sender = TorchTensorWorker.options(num_gpus=1, **sender_hint).remote()
receiver = TorchTensorWorker.options(num_gpus=1, **receiver_hint).remote()
return exec_ray_dag(
"exec_ray_dag_gpu_nccl"
+ ("_static_shape" if static_shape else "")
+ ("_direct_return" if direct_return else ""),
sender,
receiver,
use_nccl=True,
static_shape=static_shape,
direct_return=direct_return,
)
def exec_ray_core_gpu(sender_hint, receiver_hint):
time.sleep(1)
sender = TorchTensorWorker.options(num_gpus=1, **sender_hint).remote()
receiver = TorchTensorWorker.options(num_gpus=1, **receiver_hint).remote()
return exec_ray_dag("exec_ray_core_gpu", sender, receiver, use_cgraph=False)
def main(distributed):
results = []
ray.init(
runtime_env={
"env_vars": {
"CUDA_VISIBLE_DEVICES": "0,1",
# Needed for torch distributed.
"MASTER_ADDR": socket.gethostbyname(socket.gethostname()),
"MASTER_PORT": "8888",
}
}
)
# NCCL takes a while to warm up on multi node so increase the default
# timeout.
ctx = DAGContext.get_current()
ctx.get_timeout = 120
sender_hint, receiver_hint = {}, {}
if distributed:
local_node_id = ray.get_runtime_context().get_node_id()
node_ids = [node["NodeID"] for node in ray.nodes()]
remote_node_ids = [node_id for node_id in node_ids if node_id != local_node_id]
assert remote_node_ids
remote_node_id = remote_node_ids[0]
# Pin sender on local node and receiver on the other node for consistent
# results.
sender_hint = {"label_selector": {ray._raylet.RAY_NODE_ID_KEY: local_node_id}}
receiver_hint = {
"label_selector": {ray._raylet.RAY_NODE_ID_KEY: remote_node_id}
}
if not distributed:
results += timeit("exec_torch_cpu_cpu", _exec_torch_cpu_cpu)
results += timeit("exec_torch_gpu", _exec_torch_gpu)
results += timeit("exec_torch_gpu_cpu_gpu", _exec_torch_gpu_cpu_gpu)
results += exec_nccl_gpu(sender_hint, receiver_hint)
if not distributed:
results += timeit("exec_ray_put_cpu", _exec_ray_put_cpu)
results += timeit("exec_ray_put_np_zero_copy", _exec_ray_put_np_zero_copy)
results += timeit("exec_ray_put_gpu", _exec_ray_put_gpu)
results += exec_ray_core_cpu(sender_hint, receiver_hint)
results += exec_ray_dag_cpu(sender_hint, receiver_hint)
results += exec_ray_core_gpu(sender_hint, receiver_hint)
results += exec_ray_dag_gpu_cpu_gpu(sender_hint, receiver_hint)
results += exec_ray_dag_gpu_nccl(
sender_hint, receiver_hint, static_shape=True, direct_return=True
)
results += exec_ray_dag_gpu_nccl(
sender_hint, receiver_hint, static_shape=False, direct_return=True
)
results += exec_ray_dag_gpu_nccl(
sender_hint, receiver_hint, static_shape=True, direct_return=False
)
results += exec_ray_dag_gpu_nccl(
sender_hint, receiver_hint, static_shape=False, direct_return=False
)
return results
def to_dict_key(key: str):
for r in [" ", ":", "-"]:
key = key.replace(r, "_")
for r in ["(", ")"]:
key = key.replace(r, "")
return key
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--tensor-size-bytes",
type=int,
# 100KB
default=100_000,
)
parser.add_argument(
"--distributed",
action="store_true",
help="Whether this is running on more than one node",
)
args = parser.parse_args()
# Divide by 2 because we're using torch.float16.
SHAPE = (args.tensor_size_bytes // 2,)
results = main(args.distributed)
result_dict = {
f"{to_dict_key(v[0])}": (v[1], v[2]) for v in results if v is not None
}
perf_metrics = [
{
"perf_metric_name": to_dict_key(v[0]),
"perf_metric_value": v[1],
"perf_metric_type": "THROUGHPUT",
}
for v in results
if v is not None
]
result_dict["perf_metrics"] = perf_metrics
test_output_json = os.environ.get(
"TEST_OUTPUT_JSON", "/tmp/microbenchmark_gpu.json"
)
with open(test_output_json, "wt") as f:
json.dump(result_dict, f)