Files
apache--tvm/tests/python/disco/test_nvshmem.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

391 lines
13 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Basic tests for a Disco nvshmem support"""
# pylint: disable=missing-docstring
import multiprocessing
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import numpy as np
import pytest
from tvm_ffi import Shape
import tvm
import tvm.testing
from tvm.runtime import disco as di
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T
from tvm.testing import env
if di is None:
pytest.skip("disco runtime is not available", allow_module_level=True)
pytestmark = [
pytest.mark.gpu,
pytest.mark.skipif(not env.has_nvshmem(), reason="need nvshmem"),
]
_SOCKET_SESSION_TESTER = None
def _get_free_port():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("", 0))
port = sock.getsockname()[1]
sock.close()
return port
class SocketSessionTester:
"""Run a disco SocketSession with one local node and remote nodes.
Each remote node is a `tvm.exec.disco_remote_socket_session` subprocess
launched with the current Python interpreter.
"""
def __init__(self, num_workers, num_nodes=2, num_groups=1):
# Initialize the attributes used by __del__ first, so that teardown is
# safe even when __init__ raises below.
self.sess = None
self.remote_nodes = []
assert num_workers % num_nodes == 0
num_workers_per_node = num_workers // num_nodes
server_host = "localhost"
server_port = _get_free_port()
server_exc = []
def start_server():
try:
self.sess = di.SocketSession(
num_nodes, num_workers_per_node, num_groups, server_host, server_port
)
except Exception as exc: # pylint: disable=broad-except
server_exc.append(exc)
thread = threading.Thread(target=start_server)
thread.start()
cmd = "tvm.exec.disco_remote_socket_session"
for _i in range(num_nodes - 1):
self.remote_nodes.append(
subprocess.Popen(
[
sys.executable,
"-m",
cmd,
server_host,
str(server_port),
str(num_workers_per_node),
],
stdout=sys.stdout,
stderr=sys.stderr,
)
)
thread.join()
if server_exc:
raise server_exc[0]
# Bound at class creation: module globals may already be cleared when
# __del__ runs during interpreter shutdown.
_TIMEOUT_EXPIRED = subprocess.TimeoutExpired
def __del__(self):
try:
# Shut down the session first so remote nodes can exit gracefully.
if self.sess is not None:
self.sess.shutdown()
finally:
for node in self.remote_nodes:
try:
node.wait(timeout=10)
except self._TIMEOUT_EXPIRED:
node.kill()
node.wait()
def create_socket_session(num_workers):
"""Create a socket session backed by one local and one remote node.
The tester is kept alive in a module-level global so that the session
survives until the next call (or interpreter exit) replaces it.
"""
global _SOCKET_SESSION_TESTER
# Rebind (not `del`) so the global stays defined if the constructor raises.
_SOCKET_SESSION_TESTER = None
_SOCKET_SESSION_TESTER = SocketSessionTester(num_workers)
assert _SOCKET_SESSION_TESTER.sess is not None
return _SOCKET_SESSION_TESTER.sess
_all_session_kinds = [di.ProcessSession, create_socket_session]
_all_num_workers = [2, 4]
_SUBPROCESS_TIMEOUT_SEC = 600
def _run_in_fresh_process(target, *args):
"""Run a test body in a freshly spawned process.
After the first call to `nvshmem_init`, a subsequent call to `nvshmem_init`
or `nvshmem_init_thread` in the same program results in undefined behavior,
and worker-0 of a Disco session lives in the calling process. So each test
body must run in its own process. The 'spawn' start method avoids
inheriting CUDA state from this process.
"""
def run_and_check():
proc = multiprocessing.get_context("spawn").Process(target=target, args=args)
proc.start()
proc.join(timeout=_SUBPROCESS_TIMEOUT_SEC)
if proc.is_alive():
proc.kill()
proc.join()
pytest.fail(
f"{target.__name__}{args} timed out after {_SUBPROCESS_TIMEOUT_SEC} seconds"
)
assert proc.exitcode == 0, f"{target.__name__}{args} failed with exit code {proc.exitcode}"
tvm.testing.run_with_gpu_lock(run_and_check)
def _require_cuda_devices(num_workers):
# Each nvshmem worker binds its own CUDA device (cudaSetDevice(worker_id)).
if not all(tvm.cuda(i).exist for i in range(num_workers)):
pytest.skip(f"Requires {num_workers} CUDA devices")
def _init_finalize(session_kind, num_workers):
sess = session_kind(num_workers=num_workers)
f_init_nvshmem_uid = tvm.get_global_func("runtime.disco.nvshmem.init_nvshmem_uid")
uid = f_init_nvshmem_uid()
init_dfunc = sess.get_global_func("runtime.disco.nvshmem.init_nvshmem")
init_dfunc(uid, num_workers, 0)
sess.sync_worker_0()
finalize_dfunc = sess.get_global_func("runtime.disco.nvshmem.finalize_nvshmem")
finalize_dfunc()
sess.sync_worker_0()
def _empty(session_kind, num_workers):
device = tvm.cuda()
sess = session_kind(num_workers=num_workers)
f_init_nvshmem_uid = tvm.get_global_func("runtime.disco.nvshmem.init_nvshmem_uid")
uid = f_init_nvshmem_uid()
init_dfunc = sess.get_global_func("runtime.disco.nvshmem.init_nvshmem")
init_dfunc(uid, num_workers, 0)
sess.sync_worker_0()
empty_dfunc = sess.get_global_func("runtime.disco.nvshmem.empty")
_a = empty_dfunc(Shape((32, 64)), "float32", device)
_b = empty_dfunc(Shape((64, 32)), "float32", device)
sess.sync_worker_0()
finalize_dfunc = sess.get_global_func("runtime.disco.nvshmem.finalize_nvshmem")
finalize_dfunc()
sess.sync_worker_0()
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("num_workers", _all_num_workers)
def test_nvshmem_init_finalize(session_kind, num_workers: int):
_require_cuda_devices(num_workers)
_run_in_fresh_process(_init_finalize, session_kind, num_workers)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("num_workers", _all_num_workers)
def test_nvshmem_empty(session_kind, num_workers: int):
_require_cuda_devices(num_workers)
_run_in_fresh_process(_empty, session_kind, num_workers)
def _compile():
num_workers = 2
sess = di.ProcessSession(num_workers=num_workers)
f_init_nvshmem_uid = tvm.get_global_func("runtime.disco.nvshmem.init_nvshmem_uid")
uid = f_init_nvshmem_uid()
init_dfunc = sess.get_global_func("runtime.disco.nvshmem.init_nvshmem")
init_dfunc(uid, num_workers, 0)
sess.sync_worker_0()
@T.prim_func(s_tir=True)
def main(A: T.Buffer((8, 16), "float32"), B: T.Buffer((16, 8), "float32")):
for i in T.thread_binding(T.int64(8), thread="threadIdx.y"):
for j in T.thread_binding(T.int64(16), thread="threadIdx.x"):
with T.sblock("T_transpose"):
v0 = T.axis.spatial(T.int64(8), i)
v1 = T.axis.spatial(T.int64(16), j)
T.reads(A[v0, v1])
T.writes(B[v1, v0])
B[v1, v0] = A[v0, v1]
tmpdir = tempfile.mkdtemp()
try:
path = tmpdir + "/test.so"
A_np = np.arange(8 * 16).astype("float32").reshape([8, 16])
B_np = np.zeros((16, 8), dtype="float32")
A_array = sess.empty(A_np.shape, "float32")
B_array = sess.empty(B_np.shape, "float32")
A_array.debug_copy_from(0, A_np)
target = tvm.target.Target("cuda")
tvm.compile(main, target=target).export_library(path)
mod = sess.load_vm_module(path)
mod["main"](A_array, B_array)
B_res = B_array.debug_get_from_remote(0).numpy()
np.testing.assert_equal(B_res, A_np.T)
# sync all workers to make sure the temporary files are cleaned up after all workers
# finish the execution
sess._sync_all()
finalize_dfunc = sess.get_global_func("runtime.disco.nvshmem.finalize_nvshmem")
finalize_dfunc()
sess.sync_worker_0()
finally:
sess.shutdown()
shutil.rmtree(tmpdir, ignore_errors=True)
def test_nvshmem_compile():
_require_cuda_devices(2)
_run_in_fresh_process(_compile)
NVSHMEM_QUERY_KERNEL_SOURCE = """
#include <nvshmem.h>
extern "C" __global__ void nvshmem_query_kernel(int* my_pe_out, int* n_pes_out) {
my_pe_out[0] = nvshmem_my_pe();
n_pes_out[0] = nvshmem_n_pes();
}
"""
def _kernel_compile(compile_mode):
"""Compile and run a kernel that calls NVSHMEM functions.
Runs in a fresh process, so setting the env var is safe.
"""
os.environ["TVM_CUDA_COMPILE_MODE"] = compile_mode
num_workers = 2
sess = di.ProcessSession(num_workers=num_workers)
f_init_nvshmem_uid = tvm.get_global_func("runtime.disco.nvshmem.init_nvshmem_uid")
uid = f_init_nvshmem_uid()
init_dfunc = sess.get_global_func("runtime.disco.nvshmem.init_nvshmem")
init_dfunc(uid, num_workers, 0)
sess.sync_worker_0()
try:
@I.ir_module(s_tir=True)
class NvshmemQueryModule:
@T.prim_func(s_tir=True)
def query_pe(
my_pe_out: T.Buffer((1,), "int32"),
n_pes_out: T.Buffer((1,), "int32"),
):
with T.sblock("root"):
T.reads()
T.writes(my_pe_out[0:1], n_pes_out[0:1])
T.call_kernel(
NVSHMEM_QUERY_KERNEL_SOURCE,
((1,), (1,)), # grid=(1,), block=(1,)
my_pe_out.data,
n_pes_out.data,
kernel_name="nvshmem_query_kernel",
)
@R.function
def main() -> R.Tuple(R.Tensor((1,), "int32"), R.Tensor((1,), "int32")):
cls = NvshmemQueryModule
with R.dataflow():
my_pe = R.call_tir(
cls.query_pe,
(),
out_ty=[
R.Tensor((1,), "int32"),
R.Tensor((1,), "int32"),
],
)
R.output(my_pe)
return my_pe
tmpdir = tempfile.mkdtemp()
try:
path = tmpdir + "/test_nvshmem_kernel.so"
target = tvm.target.Target("cuda")
tvm.compile(NvshmemQueryModule, target=target).export_library(path)
mod = sess.load_vm_module(path)
result = mod["main"]()
# Verify results from each worker
for worker_id in range(num_workers):
my_pe_result, n_pes_result = result.debug_get_from_remote(worker_id)
my_pe_val = my_pe_result.numpy()[0]
n_pes_val = n_pes_result.numpy()[0]
assert my_pe_val == worker_id, (
f"Worker {worker_id} reported my_pe={my_pe_val}, expected {worker_id}"
)
assert n_pes_val == num_workers, (
f"Worker {worker_id} reported n_pes={n_pes_val}, expected {num_workers}"
)
# Sync all workers before cleanup
sess._sync_all()
finalize_dfunc = sess.get_global_func("runtime.disco.nvshmem.finalize_nvshmem")
finalize_dfunc()
sess.sync_worker_0()
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
finally:
sess.shutdown()
def test_nvshmem_kernel_compile_nvcc():
"""Test NVSHMEM kernel compilation with nvcc."""
_require_cuda_devices(2)
_run_in_fresh_process(_kernel_compile, "nvcc")
def test_nvshmem_kernel_compile_nvrtc():
"""Test NVSHMEM kernel compilation with nvrtc."""
_require_cuda_devices(2)
try:
from cuda.bindings import nvrtc # noqa: F401
except ImportError:
pytest.skip("cuda-python not available, skipping nvrtc test")
_run_in_fresh_process(_kernel_compile, "nvrtc")
if __name__ == "__main__":
tvm.testing.main()