chore: import upstream snapshot with attribution
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
# 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.
"""Test sharded loader"""
# pylint: disable=missing-docstring
import pathlib
import tempfile
import numpy as np
import pytest
import tvm
import tvm.testing
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
@pytest.mark.gpu
@pytest.mark.skipif(tvm.runtime.disco is None, reason="disco runtime is not available")
@pytest.mark.skipif(not env.has_nccl(), reason="need nccl")
@pytest.mark.skipif(not env.has_multi_gpu(), reason="need multiple gpus")
def test_callback():
"""Simulate lazy loading of parameters in a callback
The output of a lazy parameter loading, which would accept a
callback to load the parameters.
"""
@I.ir_module(s_tir=True)
class Module:
@T.prim_func(private=True, s_tir=True)
def slice_A(
A: T.Buffer((4, 4), "int32"),
rank: T.int64,
A_sharded: T.Buffer((2, 4), "int32"),
):
for i, j in T.grid(2, 4):
with T.sblock("slice_A"):
vi, vj = T.axis.remap("SS", [i, j])
A_sharded[vi, vj] = A[rank * 2 + vi, vj]
@T.prim_func(private=True, s_tir=True)
def slice_B(
B: T.Buffer((2, 2), "float32"),
rank: T.int64,
B_sharded: T.Buffer((2, 1), "float32"),
):
for i in range(2):
with T.sblock("slice_B"):
vi = T.axis.spatial(2, i)
B_sharded[vi, 0] = B[vi, rank]
@R.function
def transform_params(
rank_arg: R.Prim("int64"),
fget_item: R.Callable([R.Any, R.Prim("int64")], R.Any),
):
cls = Module
A = fget_item(R.str("A"), R.prim_value(0))
A = R.match_cast(A, R.Tensor([4, 4], "int32"))
A = R.call_tir(
cls.slice_A,
(A, rank_arg),
out_ty=R.Tensor([2, 4], "int32"),
)
B = fget_item(R.str("B"), R.prim_value(1))
B = R.match_cast(B, R.Tensor([2, 2], "float32"))
B = R.call_tir(
cls.slice_B,
(B, rank_arg),
out_ty=R.Tensor([2, 1], "float32"),
)
return (A, B)
pipeline = tvm.ir.transform.Sequential(
[
tvm.relax.transform.LegalizeOps(),
tvm.s_tir.dlight.ApplyDefaultSchedule(tvm.s_tir.dlight.gpu.Fallback()),
],
name="pipeline",
)
with tvm.target.Target("cuda"):
mod = Module
mod = pipeline(mod)
built = tvm.compile(mod, "cuda")
num_shards = 2
with tempfile.TemporaryDirectory() as temp_dir:
temp_dir = pathlib.Path(temp_dir)
# TODO(Lunderberg): Update `disco.Session.load_vm_module` to
# allow a `tvm.runtime.Module` argument. This would avoid the
# need for a temporary file.
shlib_path = temp_dir.joinpath("libtemp.so")
built.export_library(shlib_path)
def run_and_check():
session = tvm.runtime.disco.ProcessSession(num_workers=num_shards)
try:
session.import_python_module("tvm.exec.disco_worker")
session.init_ccl("nccl", *range(num_shards))
worker_device = session.get_global_func("runtime.disco.device")()
worker_id = session.get_global_func("runtime.disco.worker_rank")()
callback_maker = session.get_global_func("tests.disco.test_callback")
fget_item = callback_maker(worker_device)
vm = session.load_vm_module(shlib_path.as_posix())
transform_params = vm["transform_params"]
params = transform_params(worker_id, fget_item)
# Worker 0 is the same PID as the controlling scope, so
# `debug_get_from_remote(0)` returns the Tensor containing
# the output.
params_gpu0 = params.debug_get_from_remote(0)
assert params_gpu0[0].device == tvm.cuda(0)
assert params_gpu0[1].device == tvm.cuda(0)
np.testing.assert_array_equal(
params_gpu0[0].numpy(),
[
[0, 1, 2, 3],
[4, 5, 6, 7],
],
)
np.testing.assert_array_equal(
params_gpu0[1].numpy(),
[[0], [2]],
)
# Worker 1 is a different PID altogether, so
# `debug_get_from_remote(1)` returns a new Tensor within the
# calling scope's PID.
params_gpu1 = params.debug_get_from_remote(1)
assert params_gpu1[0].device == tvm.cpu()
assert params_gpu1[1].device == tvm.cpu()
np.testing.assert_array_equal(
params_gpu1[0].numpy(),
[
[8, 9, 10, 11],
[12, 13, 14, 15],
],
)
np.testing.assert_array_equal(
params_gpu1[1].numpy(),
[[1], [3]],
)
finally:
session.shutdown()
tvm.testing.run_with_gpu_lock(run_and_check)
if __name__ == "__main__":
tvm.testing.main()
+708
View File
@@ -0,0 +1,708 @@
# 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.
# pylint: disable=missing-docstring
"""Tests for NCCL/RCCL"""
import tempfile
import numpy as np
import pytest
import tvm
import tvm.testing
from tvm import get_global_func
from tvm import relax as rx
from tvm.runtime import disco as di
from tvm.runtime.vm import VirtualMachine
from tvm.s_tir import dlight as dl
from tvm.script import relax as R
if di is None:
pytest.skip("disco runtime is not available", allow_module_level=True)
_all_session_kinds = [di.ThreadedSession, di.ProcessSession]
_compiled_ccl = get_global_func("runtime.disco.compiled_ccl", allow_missing=True)
if _compiled_ccl is None:
pytest.skip("Disco CCL is not enabled in this TVM build", allow_module_level=True)
_ccl = [_compiled_ccl()]
def create_device_target(ccl):
if ccl == "nccl":
dev = tvm.cuda(0)
else:
dev = tvm.rocm(0)
target = tvm.target.Target.from_device(dev)
return (dev, target)
def _run_with_ccl_session(session_kind, ccl, devices, func, *, num_groups=1):
def run_and_check():
sess = session_kind(num_workers=len(devices), num_groups=num_groups)
try:
sess.init_ccl(ccl, *devices)
return func(sess)
finally:
sess.shutdown()
return tvm.testing.run_with_gpu_lock(run_and_check)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_init(session_kind, ccl):
devices = [0, 1]
def run_test(_sess):
pass
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_allreduce(session_kind, ccl):
devices = [0, 1]
array_1 = np.arange(12, dtype="float32").reshape(3, 4)
array_2 = np.arange(start=1, stop=-11, step=-1, dtype="float32").reshape(3, 4)
def run_test(sess):
d_array = sess.empty((3, 4), "float32")
d_array.debug_copy_from(0, array_1)
d_array.debug_copy_from(1, array_2)
for op, np_op in [ # pylint: disable=invalid-name
("sum", np.add),
("prod", np.multiply),
("min", np.minimum),
("max", np.maximum),
("avg", lambda a, b: (a + b) * 0.5),
]:
dst_array = sess.empty((3, 4), "float32")
sess.allreduce(d_array, dst_array, op=op)
result = dst_array.debug_get_from_remote(0).numpy()
expected = np_op(array_1, array_2)
np.testing.assert_equal(result, expected)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_group_allreduce(session_kind, ccl):
devices = [0, 1, 2, 3]
array_1 = np.arange(12, dtype="float32").reshape(3, 4)
array_2 = np.arange(start=1, stop=-11, step=-1, dtype="float32").reshape(3, 4)
array_3 = np.arange(30, dtype="float32").reshape(5, 6)
array_4 = np.arange(start=1, stop=-29, step=-1, dtype="float32").reshape(5, 6)
def run_test(sess):
d_array_1 = sess.empty((3, 4), "float32")
d_array_2 = sess.empty((5, 6), "float32")
d_array_1.debug_copy_from(0, array_1)
d_array_1.debug_copy_from(1, array_2)
d_array_2.debug_copy_from(2, array_3)
d_array_2.debug_copy_from(3, array_4)
for op, np_op in [ # pylint: disable=invalid-name
("sum", np.add),
("prod", np.multiply),
("min", np.minimum),
("max", np.maximum),
("avg", lambda a, b: (a + b) * 0.5),
]:
dst_array_1 = sess.empty((3, 4), "float32")
dst_array_2 = sess.empty((5, 6), "float32")
sess.allreduce(d_array_1, dst_array_1, op=op, in_group=True)
sess.allreduce(d_array_2, dst_array_2, op=op, in_group=True)
result_1 = dst_array_1.debug_get_from_remote(0).numpy()
result_2 = dst_array_2.debug_get_from_remote(2).numpy()
expected_1 = np_op(array_1, array_2)
expected_2 = np_op(array_3, array_4)
np.testing.assert_equal(result_1, expected_1)
np.testing.assert_equal(result_2, expected_2)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_allgather(session_kind, ccl):
devices = [0, 1]
array = np.arange(36, dtype="float32")
def run_test(sess):
d_src = sess.empty((3, 3, 2), "float32")
d_dst = sess.empty((3, 4, 3), "float32")
d_src.debug_copy_from(0, array[:18])
d_src.debug_copy_from(1, array[18:])
sess.allgather(d_src, d_dst)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array.reshape(3, 4, 3),
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(1).numpy(),
array.reshape(3, 4, 3),
)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_group_allgather(session_kind, ccl):
devices = [0, 1, 2, 3]
array_1 = np.arange(36, dtype="float32")
array_2 = np.arange(48, dtype="float32")
def run_test(sess):
d_src_1 = sess.empty((3, 3, 2), "float32")
d_dst_1 = sess.empty((3, 4, 3), "float32")
d_src_2 = sess.empty((2, 4, 3), "float32")
d_dst_2 = sess.empty((2, 6, 4), "float32")
d_src_1.debug_copy_from(0, array_1[:18])
d_src_1.debug_copy_from(1, array_1[18:])
d_src_2.debug_copy_from(2, array_2[:24])
d_src_2.debug_copy_from(3, array_2[24:])
sess.allgather(d_src_1, d_dst_1, in_group=True)
sess.allgather(d_src_2, d_dst_2, in_group=True)
np.testing.assert_equal(
d_dst_1.debug_get_from_remote(0).numpy(),
array_1.reshape(3, 4, 3),
)
np.testing.assert_equal(
d_dst_1.debug_get_from_remote(1).numpy(),
array_1.reshape(3, 4, 3),
)
np.testing.assert_equal(
d_dst_2.debug_get_from_remote(2).numpy(),
array_2.reshape(2, 6, 4),
)
np.testing.assert_equal(
d_dst_2.debug_get_from_remote(3).numpy(),
array_2.reshape(2, 6, 4),
)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
@pytest.mark.parametrize("use_explicit_output", [True, False])
def test_broadcast(session_kind, ccl, use_explicit_output):
devices = [0, 1]
array = np.arange(12, dtype="float32").reshape(3, 4)
def run_test(sess):
if use_explicit_output:
src_array = sess.empty((3, 4), "float32", worker0_only=True)
src_array.debug_copy_from(0, array)
dst_array = sess.empty((3, 4), "float32")
sess.broadcast_from_worker0(src_array, dst_array)
else:
dst_array = sess.broadcast(array)
result = dst_array.debug_get_from_remote(1).numpy()
np.testing.assert_equal(result, array)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_group_broadcast(session_kind, ccl):
devices = [0, 1, 2, 3]
array_1 = np.arange(12, dtype="float32").reshape(3, 4)
array_2 = np.multiply(array_1, -1)
def run_test(sess):
src_array = sess.empty((3, 4), "float32", worker0_only=True, in_group=True)
src_array.debug_copy_from(0, array_1)
src_array.debug_copy_from(2, array_2)
dst_array = sess.empty((3, 4), "float32")
sess.broadcast_from_worker0(src_array, dst_array)
result_1 = dst_array.debug_get_from_remote(1).numpy()
np.testing.assert_equal(result_1, array_1)
result_3 = dst_array.debug_get_from_remote(3).numpy()
np.testing.assert_equal(result_3, array_2)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
@pytest.mark.parametrize("use_explicit_output", [True, False])
def test_scatter(session_kind, ccl, use_explicit_output, capfd):
devices = [0, 1]
array = np.arange(36, dtype="float32").reshape(2, 6, 3)
def run_test(sess):
if use_explicit_output:
d_src = sess.empty((2, 6, 3), "float32", worker0_only=True)
d_dst = sess.empty((6, 3), "float32")
d_src.debug_copy_from(0, array)
sess.scatter_from_worker0(d_src, d_dst)
else:
d_dst = sess.scatter(array)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array[0, :, :],
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(1).numpy(),
array[1, :, :],
)
captured = capfd.readouterr()
assert not captured.err, (
"No warning messages should be generated from disco.Session.scatter_from_worker0"
)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_group_scatter(session_kind, ccl, capfd):
devices = [0, 1, 2, 3]
array_1 = np.arange(36, dtype="float32").reshape(2, 6, 3)
array_2 = np.multiply(array_1, -1)
def run_test(sess):
d_src = sess.empty((2, 6, 3), "float32", worker0_only=True, in_group=True)
d_src.debug_copy_from(0, array_1)
d_src.debug_copy_from(2, array_2)
d_dst = sess.empty((6, 3), "float32")
sess.scatter_from_worker0(d_src, d_dst)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array_1[0, :, :],
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(1).numpy(),
array_1[1, :, :],
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(2).numpy(),
array_2[0, :, :],
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(3).numpy(),
array_2[1, :, :],
)
captured = capfd.readouterr()
assert not captured.err, (
"No warning messages should be generated from disco.Session.scatter_from_worker0"
)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_scatter_with_implicit_reshape(session_kind, ccl, capfd):
"""Scatter may perform an implicit reshape
Scattering elements to the workers requires the total number of
elements to be divisible by the number of workers. It does not
necessarily correspond to scattering across the outermost
dimension. Here, the number of workers (2) and the outermost
dimension (3) are not divisible, but the scatter may still be
performed.
This is only allowed when the caller explicitly uses the
`sess.scatter_from_worker0` method, and is not allowed in
`sess.scatter` method. Because the `sess.scatter` method may
perform an allocation on the disco workers, it requires that the
scatter occur across the outermost dimension.
"""
devices = [0, 1]
array = np.arange(36, dtype="float32").reshape(3, 4, 3)
def run_test(sess):
d_src = sess.empty((3, 4, 3), "float32", worker0_only=True)
d_dst = sess.empty((3, 3, 2), "float32")
d_src.debug_copy_from(0, array)
sess.scatter_from_worker0(d_src, d_dst)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array.flat[:18].reshape(3, 3, 2),
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(1).numpy(),
array.flat[18:].reshape(3, 3, 2),
)
captured = capfd.readouterr()
assert not captured.err, (
"No warning messages should be generated from disco.Session.scatter_from_worker0"
)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_gather(session_kind, ccl, capfd):
devices = [0, 1]
array = np.arange(36, dtype="float32")
def run_test(sess):
d_src = sess.empty((3, 3, 2), "float32")
d_dst = sess.empty((3, 4, 3), "float32", worker0_only=True)
d_src.debug_copy_from(0, array[:18])
d_src.debug_copy_from(1, array[18:])
sess.gather_to_worker0(d_src, d_dst)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array.reshape(3, 4, 3),
)
captured = capfd.readouterr()
assert not captured.err, (
"No warning messages should be generated from disco.Session.gather_to_worker0"
)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_group_gather(session_kind, ccl, capfd):
devices = [0, 1, 2, 3]
array_1 = np.arange(36, dtype="float32")
array_2 = np.multiply(array_1, -1)
def run_test(sess):
d_src = sess.empty((3, 3, 2), "float32")
d_dst = sess.empty((3, 4, 3), "float32", worker0_only=True, in_group=True)
d_src.debug_copy_from(0, array_1[:18])
d_src.debug_copy_from(1, array_1[18:])
d_src.debug_copy_from(2, array_2[:18])
d_src.debug_copy_from(3, array_2[18:])
sess.gather_to_worker0(d_src, d_dst)
np.testing.assert_equal(
d_dst.debug_get_from_remote(0).numpy(),
array_1.reshape(3, 4, 3),
)
np.testing.assert_equal(
d_dst.debug_get_from_remote(2).numpy(),
array_2.reshape(3, 4, 3),
)
captured = capfd.readouterr()
assert not captured.err, (
"No warning messages should be generated from disco.Session.gather_to_worker0"
)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_send_to_next_group_receive_from_prev_group(session_kind, ccl):
devices = [0, 1, 2, 3]
array_1 = np.arange(12, dtype="float32").reshape(3, 4)
array_2 = np.arange(start=1, stop=-11, step=-1, dtype="float32").reshape(3, 4)
def run_test(sess):
d_array = sess.empty((3, 4), "float32")
d_array.debug_copy_from(0, array_1)
d_array.debug_copy_from(1, array_2)
sess.get_global_func(
"runtime.disco." + ccl + ".test_send_to_next_group_recv_from_prev_group"
)(d_array)
result_1 = d_array.debug_get_from_remote(2).numpy()
result_2 = d_array.debug_get_from_remote(3).numpy()
np.testing.assert_equal(result_1, array_1)
np.testing.assert_equal(result_2, array_2)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_worker2_send_to_worker0(session_kind, ccl):
devices = [0, 1, 2, 3]
array = np.arange(start=1, stop=-11, step=-1, dtype="float32").reshape(3, 4)
def run_test(sess):
d_array = sess.empty((3, 4), "float32")
d_array.debug_copy_from(2, array)
sess.get_global_func("runtime.disco." + ccl + ".test_worker2_sends_to_worker0")(d_array)
result = d_array.debug_get_from_remote(0).numpy()
np.testing.assert_equal(result, array)
_run_with_ccl_session(session_kind, ccl, devices, run_test, num_groups=2)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_mlp(session_kind, ccl): # pylint: disable=too-many-locals
devices = [0, 1]
# pylint: disable=invalid-name
@tvm.script.ir_module
class MLP: # pylint: disable=too-few-public-methods
@R.function
def main(
x: R.Tensor((128, 128), "float32"),
W1: R.Tensor((128, 128), "float32"),
W2: R.Tensor((128, 128), "float32"),
) -> R.Tensor((128, 128), "float32"):
R.func_attr({"global_symbol": "main"})
with R.dataflow():
lv0: R.Tensor((128, 128), "float32") = R.matmul(x, W1)
lv1: R.Tensor((128, 128), "float32") = R.nn.gelu(lv0)
lv2: R.Tensor((128, 128), "float32") = R.matmul(lv1, W2)
R.output(lv2)
return lv2
@tvm.script.ir_module
class ShardedMLP: # pylint: disable=too-few-public-methods
@R.function
def main(
x: R.Tensor((128, 128), "float32"),
W1: R.Tensor((128, 64), "float32"), # shard along axis 1
W2: R.Tensor((64, 128), "float32"), # shard along axis 0
) -> R.Tensor((128, 128), "float32"):
R.func_attr({"global_symbol": "main"})
with R.dataflow():
broadcast_x: R.Tensor((128, 128), "float32") = R.ccl.broadcast_from_worker0(x)
lv0: R.Tensor((128, 64), "float32") = R.matmul(broadcast_x, W1)
lv1: R.Tensor((128, 64), "float32") = R.nn.gelu(lv0)
lv2: R.Tensor((128, 128), "float32") = R.matmul(lv1, W2)
lv3: R.Tensor((128, 128), "float32") = R.ccl.allreduce(lv2, "sum")
R.output(lv3)
return lv3
# pylint: enable=invalid-name
dev, target = create_device_target(ccl)
def relax_build(mod, target):
with target:
mod = rx.get_pipeline("zero")(mod) # pylint: disable=no-value-for-parameter
mod = dl.ApplyDefaultSchedule( # pylint: disable=not-callable
dl.gpu.Matmul(),
dl.gpu.GEMV(),
dl.gpu.Reduction(),
dl.gpu.GeneralReduction(),
dl.gpu.Fallback(),
)(mod)
return tvm.compile(mod, target=target)
# pylint: disable=invalid-name
X = np.random.randn(128, 128).astype("float32")
W1 = np.random.randn(128, 128).astype("float32")
W2 = np.random.randn(128, 128).astype("float32")
expected_ex = relax_build(MLP, target)
sharded_ex = relax_build(ShardedMLP, target)
with tempfile.TemporaryDirectory() as tmpdir:
path = tmpdir + "/test.so"
sharded_ex.export_library(path)
def run_test(sess):
Y_expected = VirtualMachine(expected_ex, device=dev)["main"](
tvm.runtime.tensor(X, device=dev),
tvm.runtime.tensor(W1, device=dev),
tvm.runtime.tensor(W2, device=dev),
).numpy()
mod = sess.load_vm_module(path)
d_X = sess.empty((128, 128), "float32")
d_W1 = sess.empty((128, 64), "float32")
d_W2 = sess.empty((64, 128), "float32")
d_X.debug_copy_from(0, X)
d_W1.debug_copy_from(0, W1[:, :64])
d_W1.debug_copy_from(1, W1[:, 64:])
d_W2.debug_copy_from(0, W2[:64, :])
d_W2.debug_copy_from(1, W2[64:, :])
d_Y = mod["main"](d_X, d_W1, d_W2)
Y_result = tvm.runtime.empty((128, 128), "float32", device=dev)
sess.copy_from_worker_0(Y_result, d_Y)
sess.sync_worker_0()
Y_result = Y_result.numpy()
tvm.testing.assert_allclose(Y_result, Y_expected, rtol=1e-4, atol=1e-4)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
# pylint: enable=invalid-name
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("ccl", _ccl)
def test_attention(session_kind, ccl): # pylint: disable=too-many-locals,too-many-statements
devices = [0, 1]
# pylint: disable=invalid-name
@tvm.script.ir_module
class Attention: # pylint: disable=too-few-public-methods
@R.function
def main( # pylint: disable=too-many-locals
x: R.Tensor((1, 10, 128), "float32"),
Wq: R.Tensor((128, 512), "float32"),
Wk: R.Tensor((128, 512), "float32"),
Wv: R.Tensor((128, 512), "float32"),
Wo: R.Tensor((512, 128), "float32"),
) -> R.Tensor((128, 128), "float32"):
R.func_attr({"global_symbol": "main"})
with R.dataflow():
# q
lv0: R.Tensor((1, 10, 512), "float32") = R.matmul(x, Wq)
lv1: R.Tensor((1, 10, 8, 64), "float32") = R.reshape(lv0, [1, 10, 8, 64])
lv2: R.Tensor((1, 8, 10, 64), "float32") = R.permute_dims(lv1, [0, 2, 1, 3])
# k
lv3: R.Tensor((1, 10, 512), "float32") = R.matmul(x, Wk)
lv4: R.Tensor((1, 10, 8, 64), "float32") = R.reshape(lv3, [1, 10, 8, 64])
lv5: R.Tensor((1, 8, 10, 64), "float32") = R.permute_dims(lv4, [0, 2, 1, 3])
# v
lv6: R.Tensor((1, 10, 512), "float32") = R.matmul(x, Wv)
lv7: R.Tensor((1, 10, 8, 64), "float32") = R.reshape(lv6, [1, 10, 8, 64])
lv8: R.Tensor((1, 8, 10, 64), "float32") = R.permute_dims(lv7, [0, 2, 1, 3])
# softmax(q @ k / sqrt(dk))
lv9: R.Tensor((1, 8, 64, 10), "float32") = R.permute_dims(lv5, [0, 1, 3, 2])
lv10: R.Tensor((1, 8, 10, 10), "float32") = R.matmul(lv2, lv9)
lv11: R.Tensor((1, 8, 10, 10), "float32") = R.multiply(
lv10, R.const(1 / 8, "float32")
)
lv12: R.Tensor((1, 8, 10, 10), "float32") = R.nn.softmax(lv11, axis=-1)
# attn_weight @ v
lv13: R.Tensor((1, 8, 10, 64), "float32") = R.matmul(lv12, lv8)
lv14: R.Tensor((1, 10, 8, 64), "float32") = R.permute_dims(lv13, [0, 2, 1, 3])
lv15: R.Tensor((1, 10, 512), "float32") = R.reshape(lv14, [1, 10, 512])
# attn_output @ o
lv16: R.Tensor((1, 10, 128), "float32") = R.matmul(lv15, Wo)
R.output(lv16)
return lv16
@tvm.script.ir_module
class ShardedAttention: # pylint: disable=too-few-public-methods
@R.function
def main( # pylint: disable=too-many-locals
x: R.Tensor((1, 10, 128), "float32"),
Wq: R.Tensor((128, 256), "float32"), # shard along axis 1
Wk: R.Tensor((128, 256), "float32"), # shard along axis 1
Wv: R.Tensor((128, 256), "float32"), # shard along axis 1
Wo: R.Tensor((256, 128), "float32"), # shard along axis 0
) -> R.Tensor((128, 128), "float32"):
R.func_attr({"global_symbol": "main"})
with R.dataflow():
broadcast_x: R.Tensor((1, 10, 128), "float32") = R.ccl.broadcast_from_worker0(x)
# q
lv0: R.Tensor((1, 10, 256), "float32") = R.matmul(broadcast_x, Wq)
lv1: R.Tensor((1, 10, 4, 64), "float32") = R.reshape(lv0, [1, 10, 4, 64])
lv2: R.Tensor((1, 4, 10, 64), "float32") = R.permute_dims(lv1, [0, 2, 1, 3])
# k
lv3: R.Tensor((1, 10, 256), "float32") = R.matmul(broadcast_x, Wk)
lv4: R.Tensor((1, 10, 4, 64), "float32") = R.reshape(lv3, [1, 10, 4, 64])
lv5: R.Tensor((1, 4, 10, 64), "float32") = R.permute_dims(lv4, [0, 2, 1, 3])
# v
lv6: R.Tensor((1, 10, 256), "float32") = R.matmul(broadcast_x, Wv)
lv7: R.Tensor((1, 10, 4, 64), "float32") = R.reshape(lv6, [1, 10, 4, 64])
lv8: R.Tensor((1, 4, 10, 64), "float32") = R.permute_dims(lv7, [0, 2, 1, 3])
# softmax(q @ k / sqrt(dk))
lv9: R.Tensor((1, 4, 64, 10), "float32") = R.permute_dims(lv5, [0, 1, 3, 2])
lv10: R.Tensor((1, 4, 10, 10), "float32") = R.matmul(lv2, lv9)
lv11: R.Tensor((1, 4, 10, 10), "float32") = R.multiply(
lv10, R.const(1 / 8, "float32")
)
lv12: R.Tensor((1, 4, 10, 10), "float32") = R.nn.softmax(lv11, axis=-1)
# attn_weight @ v
lv13: R.Tensor((1, 4, 10, 64), "float32") = R.matmul(lv12, lv8)
lv14: R.Tensor((1, 10, 4, 64), "float32") = R.permute_dims(lv13, [0, 2, 1, 3])
lv15: R.Tensor((1, 10, 256), "float32") = R.reshape(lv14, [1, 10, 256])
# attn_output @ o
lv16: R.Tensor((1, 10, 128), "float32") = R.matmul(lv15, Wo)
lv17: R.Tensor((1, 10, 128), "float32") = R.ccl.allreduce(lv16, "sum")
R.output(lv17)
return lv17
# pylint: enable=invalid-name
dev, target = create_device_target(ccl)
def relax_build(mod, target):
with target:
mod = rx.get_pipeline("zero")(mod) # pylint: disable=no-value-for-parameter
mod = dl.ApplyDefaultSchedule( # pylint: disable=not-callable
dl.gpu.Matmul(),
dl.gpu.GEMV(),
dl.gpu.Reduction(),
dl.gpu.GeneralReduction(),
dl.gpu.Fallback(),
)(mod)
return tvm.compile(mod, target=target)
# pylint: disable=invalid-name
X = np.random.randn(1, 10, 128).astype("float32")
Wq = np.random.randn(128, 512).astype("float32")
Wk = np.random.randn(128, 512).astype("float32")
Wv = np.random.randn(128, 512).astype("float32")
Wo = np.random.randn(512, 128).astype("float32")
expected_ex = relax_build(Attention, target)
sharded_ex = relax_build(ShardedAttention, target)
with tempfile.TemporaryDirectory() as tmpdir:
path = tmpdir + "/test.so"
sharded_ex.export_library(path)
def run_test(sess):
Y_expected = VirtualMachine(expected_ex, device=dev)["main"](
tvm.runtime.tensor(X, device=dev),
tvm.runtime.tensor(Wq, device=dev),
tvm.runtime.tensor(Wk, device=dev),
tvm.runtime.tensor(Wv, device=dev),
tvm.runtime.tensor(Wo, device=dev),
).numpy()
mod = sess.load_vm_module(path)
d_X = sess.empty((1, 10, 128), "float32")
d_Wq = sess.empty((128, 256), "float32")
d_Wk = sess.empty((128, 256), "float32")
d_Wv = sess.empty((128, 256), "float32")
d_Wo = sess.empty((256, 128), "float32")
d_X.debug_copy_from(0, X)
d_Wq.debug_copy_from(0, Wq[:, :256])
d_Wq.debug_copy_from(1, Wq[:, 256:])
d_Wk.debug_copy_from(0, Wk[:, :256])
d_Wk.debug_copy_from(1, Wk[:, 256:])
d_Wv.debug_copy_from(0, Wv[:, :256])
d_Wv.debug_copy_from(1, Wv[:, 256:])
d_Wo.debug_copy_from(0, Wo[:256, :])
d_Wo.debug_copy_from(1, Wo[256:, :])
d_Y = mod["main"](d_X, d_Wq, d_Wk, d_Wv, d_Wo)
Y_result = tvm.runtime.empty((1, 10, 128), "float32", device=dev)
sess.copy_from_worker_0(Y_result, d_Y)
sess.sync_worker_0()
Y_result = Y_result.numpy()
tvm.testing.assert_allclose(Y_result, Y_expected, rtol=1e-3, atol=1e-3)
_run_with_ccl_session(session_kind, ccl, devices, run_test)
# pylint: enable=invalid-name
if __name__ == "__main__":
tvm.testing.main()
@@ -0,0 +1,88 @@
# 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.
import enum
from functools import reduce
from itertools import product
import numpy as np
import pytest
from tvm_ffi import Shape
import tvm
import tvm.testing
from tvm.runtime import DataType, disco
if disco is None:
pytest.skip("disco runtime is not available", allow_module_level=True)
class AllReduceStrategyType(enum.IntEnum):
RING = 0
ONESHOT = 1
TWOSHOT = 2
AUTO = 3
_shapes = [(2, 3), (3, 4), (128, 128)]
_strategies = [
AllReduceStrategyType.RING,
AllReduceStrategyType.ONESHOT,
AllReduceStrategyType.TWOSHOT,
AllReduceStrategyType.AUTO,
]
_compiled_ccl = tvm.get_global_func("runtime.disco.compiled_ccl", allow_missing=True)
if _compiled_ccl is None:
pytest.skip("Disco CCL is not enabled in this TVM build", allow_module_level=True)
_ccl = [ccl for ccl in _compiled_ccl() if ccl == "nccl"]
@pytest.mark.parametrize("shape", _shapes)
@pytest.mark.parametrize("ccl", _ccl)
@pytest.mark.parametrize("strategy", _strategies)
def test_allreduce(shape, ccl, strategy):
devices = [0, 1]
sess = disco.ProcessSession(num_workers=len(devices))
sess.init_ccl(ccl, *devices)
num_elements = reduce(lambda x, y: x * y, shape)
dtype = "float32"
falloc_ipc_storage = sess.get_global_func("runtime.disco.cuda_ipc.alloc_storage")
falloc_tensor = sess.get_global_func("vm.builtin.alloc_tensor")
fallreduce = sess.get_global_func("runtime.disco.cuda_ipc.custom_allreduce")
d_storage = sess.call_packed(falloc_ipc_storage, Shape(shape), DataType(dtype))
d_input = sess.call_packed(falloc_tensor, d_storage, 0, Shape(shape), DataType(dtype))
array_1 = np.arange(num_elements, dtype="float32").reshape(*shape)
array_2 = np.arange(start=1, stop=-(num_elements - 1), step=-1, dtype="float32").reshape(*shape)
d_input.debug_copy_from(0, array_1)
d_input.debug_copy_from(1, array_2)
d_output = sess.empty(shape, "float32")
sess.call_packed(fallreduce, d_input, strategy, d_output)
result_1 = d_output.debug_get_from_remote(0).numpy()
result_2 = d_output.debug_get_from_remote(1).numpy()
expected = np.add(array_1, array_2)
np.testing.assert_equal(result_1, expected)
np.testing.assert_equal(result_2, expected)
if __name__ == "__main__":
for shape, strategy in product(_shapes, _strategies):
test_allreduce(shape, "nccl", strategy)
+496
View File
@@ -0,0 +1,496 @@
# 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.
# ruff: noqa: F401, F841
"""Test sharded loader"""
# pylint: disable=missing-docstring
import json
import tempfile
import numpy as np
import pytest
from tvm_ffi import Shape, register_global_func
import tvm
import tvm.testing
from tvm import relax as rx
from tvm.contrib import tvmjs
from tvm.runtime import disco as di
from tvm.s_tir import dlight as dl
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.target import Target
from tvm.testing import env
# `runtime.disco.compiled_ccl` is registered together with the CCL runtime
# functions, so its absence means the disco CCL runtime is not in this build.
_compiled_ccl = tvm.get_global_func("runtime.disco.compiled_ccl", allow_missing=True)
if _compiled_ccl is None or _compiled_ccl() != "nccl":
pytest.skip("Disco NCCL support is not available", allow_module_level=True)
# All tests in this file shard across two GPUs.
pytestmark = [
pytest.mark.skipif(not env.has_multi_gpu(), reason="need multiple gpus"),
]
def _run_with_nccl_session(devices, func):
def run_and_check():
sess = di.ThreadedSession(num_workers=len(devices))
try:
sess.init_ccl("nccl", *devices)
return func(sess)
finally:
sess.shutdown()
return tvm.testing.run_with_gpu_lock(run_and_check)
@register_global_func("tests.disco.shard_dim_0", override=True)
def _shard_dim_0(src, num_shards, tgt):
s_0, s_1 = src.shape
tgt.copyfrom(src.numpy().reshape(num_shards, s_0 // num_shards, s_1))
@register_global_func("tests.disco.shard_dim_1", override=True)
def _shard_dim_1(src, num_shards, tgt):
s_0, s_1 = src.shape
tgt.copyfrom(src.numpy().reshape(s_0, num_shards, s_1 // num_shards).transpose(1, 0, 2))
@register_global_func("tests.disco.shard_qkv_0", override=True)
def _shard_qkv_0(src, num_shards, q_heads, kv_heads, tgt):
total_dim, hidden_size = src.shape
head_dim = total_dim // (q_heads + kv_heads + kv_heads)
q_dim = q_heads * head_dim
kv_dim = kv_heads * head_dim
w_q = src.numpy()[:q_dim, :].reshape(
num_shards,
q_heads // num_shards,
head_dim,
hidden_size,
)
w_k = src.numpy()[q_dim : q_dim + kv_dim, :].reshape(
num_shards,
kv_heads // num_shards,
head_dim,
hidden_size,
)
w_v = src.numpy()[q_dim + kv_dim :, :].reshape(
num_shards,
kv_heads // num_shards,
head_dim,
hidden_size,
)
w_qkv = np.concatenate([w_q, w_k, w_v], axis=1)
tgt.copyfrom(w_qkv)
@register_global_func("tests.disco.shard_qkv_1", override=True)
def _shard_qkv_1(src, tgt):
s, _, _, h = src.shape # pylint: disable=invalid-name
tgt.copyfrom(src.numpy().reshape(s, -1, h))
def _create_loader(sess, path, param_dict, shard_info):
path_tensor_cache = path + "/tensor-cache.json"
tvmjs.dump_tensor_cache(param_dict, path, encode_format="raw")
with open(path_tensor_cache, encoding="utf-8") as i_f:
tensor_cache = i_f.read()
loader_create = sess.get_global_func("runtime.disco.ShardLoader")
loader = loader_create(path_tensor_cache, tensor_cache, json.dumps(shard_info), None)
return loader
def _simulate_presharded_weights(base_path, param_dict, num_shards, shard_info):
"""Create fake weights to simulate those produced MLC-LLM's pre-sharding"""
sharded_params = {}
for key, ndarray in param_dict.items():
assert key in shard_info, f"ShardInfo lacks shard info about param: {key}"
shard_dim = shard_info[key]
sharded_params[key] = [
tvm.runtime.tensor(np_shard)
for np_shard in np.split(ndarray, num_shards, axis=shard_dim)
]
# Re-order so that the parameter order is sorted first by shard,
# then by parameter. This matches the ordering used by MLC-LLM,
# and avoids having *.bin files that must be accessed by more than
# one worker.
sharded_params = {
f"{key}_shard-{i + 1}-of-{num_shards}": shards[i]
for i in range(num_shards)
for key, shards in sharded_params.items()
}
tvmjs.dump_tensor_cache(
sharded_params,
base_path,
encode_format="raw",
)
def test_load_shard():
devices = [0, 1]
num_shards = len(devices)
param_dict = {
"x_0": np.random.uniform(size=[64, 128]).astype("float16"),
"x_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {
"x_0": [
[
"tests.disco.shard_dim_1",
[(num_shards, 64, 64), "float16"],
num_shards,
],
],
"x_1": [
[
"tests.disco.shard_dim_0",
[(num_shards, 16, 128), "float32"],
num_shards,
]
],
}
with tempfile.TemporaryDirectory() as path:
def run_test(sess):
loader = _create_loader(sess, path, param_dict, shard_info)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoad")
d_0 = loader_load(loader, Shape([0]))
d_1 = loader_load(loader, Shape([1]))
np.testing.assert_equal(
param_dict["x_0"][:, 0:64],
d_0.debug_get_from_remote(0).numpy(),
)
np.testing.assert_equal(
param_dict["x_0"][:, 64:128],
d_0.debug_get_from_remote(1).numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][0:16, :],
d_1.debug_get_from_remote(0).numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][16:32, :],
d_1.debug_get_from_remote(1).numpy(),
)
_run_with_nccl_session(devices, run_test)
def _create_presharded_loader(sess, path):
path_tensor_cache = path + "/tensor-cache.json"
with open(path_tensor_cache, encoding="utf-8") as i_f:
tensor_cache = i_f.read()
loader_create = sess.get_global_func("runtime.disco.ShardLoader")
loader = loader_create(path_tensor_cache, tensor_cache, json.dumps({}), None)
return loader
def test_load_presharded():
devices = [0, 1]
param_dict = {
"x_0": np.random.uniform(size=[64, 128]).astype("float16"),
"x_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {
"x_0": 1,
"x_1": 0,
}
with tempfile.TemporaryDirectory() as path:
_simulate_presharded_weights(path, param_dict, len(devices), shard_info)
def run_test(sess):
loader = _create_presharded_loader(sess, path)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoadPresharded")
d_0 = loader_load(loader, Shape([0]))
d_1 = loader_load(loader, Shape([1]))
np.testing.assert_equal(
param_dict["x_0"][:, 0:64],
d_0.debug_get_from_remote(0).numpy(),
)
np.testing.assert_equal(
param_dict["x_0"][:, 64:128],
d_0.debug_get_from_remote(1).numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][0:16, :],
d_1.debug_get_from_remote(0).numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][16:32, :],
d_1.debug_get_from_remote(1).numpy(),
)
_run_with_nccl_session(devices, run_test)
def test_load_shard_in_relax():
devices = [0, 1]
num_shards = len(devices)
param_dict = {
"x_0": np.random.uniform(size=[64, 128]).astype("float16"),
"x_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {
"x_0": [
[
"tests.disco.shard_dim_1",
[(num_shards, 64, 64), "float16"],
num_shards,
],
],
"x_1": [
[
"tests.disco.shard_dim_0",
[(num_shards, 16, 128), "float32"],
num_shards,
]
],
}
# pylint: disable=invalid-name
@I.ir_module
class Module: # pylint: disable=too-few-public-methods
@R.function
def main(
loader: R.Any,
) -> R.Tuple(R.Tensor((64, 64), "float32"), R.Tensor((16, 128), "float32")):
R.func_attr({"global_symbol": "main"})
with R.dataflow():
lv0: R.Tensor((64, 64), "float32") = R.call_pure_packed(
"runtime.disco.ShardLoaderLoad",
loader,
R.shape([0]),
ty_args=R.Tensor((64, 64), "float32"),
)
lv1: R.Tensor((16, 128), "float32") = R.call_pure_packed(
"runtime.disco.ShardLoaderLoad",
loader,
R.shape([1]),
ty_args=R.Tensor((16, 128), "float32"),
)
lv2 = R.tuple(lv0, lv1)
R.output(lv2)
return lv2
# pylint: enable=invalid-name
def relax_build(mod, target):
with target:
mod = rx.get_pipeline("zero")(mod) # pylint: disable=no-value-for-parameter
return tvm.compile(mod, target="cuda")
target = Target(
{
"kind": "cuda",
"max_shared_memory_per_block": 49152,
"max_threads_per_block": 1024,
"thread_warp_size": 32,
"registers_per_block": 65536,
"arch": "sm_80",
}
)
with tempfile.TemporaryDirectory() as tmpdir:
dso_path = tmpdir + "/test.so"
relax_build(Module, target).export_library(dso_path)
def run_test(sess):
mod = sess.load_vm_module(dso_path)
loader = _create_loader(sess, tmpdir, param_dict, shard_info)
result = mod["main"](loader)
np.testing.assert_equal(
param_dict["x_0"][:, 0:64],
result.debug_get_from_remote(0)[0].numpy(),
)
np.testing.assert_equal(
param_dict["x_0"][:, 64:128],
result.debug_get_from_remote(1)[0].numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][0:16, :],
result.debug_get_from_remote(0)[1].numpy(),
)
np.testing.assert_equal(
param_dict["x_1"][16:32, :],
result.debug_get_from_remote(1)[1].numpy(),
)
_run_with_nccl_session(devices, run_test)
def test_load_shard_all():
devices = [0, 1]
num_shards = len(devices)
param_dict = {
"param_0": np.random.uniform(size=[64, 128]).astype("float16"),
"param_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {
"param_0": [
[
"tests.disco.shard_dim_1",
[(num_shards, 64, 64), "float16"],
num_shards,
],
],
"param_1": [
[
"tests.disco.shard_dim_0",
[(2, 16, 128), "float32"],
num_shards,
]
],
}
with tempfile.TemporaryDirectory() as path:
def run_test(sess):
loader = _create_loader(sess, path, param_dict, shard_info)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoadAll")
params = loader_load(loader)
p_0 = params.debug_get_from_remote(0)
p_1 = params.debug_get_from_remote(1)
np.testing.assert_equal(param_dict["param_0"][:, 0:64], p_0[0].numpy())
np.testing.assert_equal(param_dict["param_0"][:, 64:128], p_1[0].numpy())
np.testing.assert_equal(param_dict["param_1"][0:16, :], p_0[1].numpy())
np.testing.assert_equal(param_dict["param_1"][16:32, :], p_1[1].numpy())
_run_with_nccl_session(devices, run_test)
def test_load_all_presharded():
devices = [0, 1]
num_shards = len(devices)
param_dict = {
"param_0": np.random.uniform(size=[64, 128]).astype("float16"),
"param_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {
"param_0": 0,
"param_1": 1,
}
with tempfile.TemporaryDirectory() as path:
_simulate_presharded_weights(path, param_dict, len(devices), shard_info)
def run_test(sess):
loader = _create_presharded_loader(sess, path)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoadAllPresharded")
params = loader_load(loader)
p_0 = params.debug_get_from_remote(0)
p_1 = params.debug_get_from_remote(1)
np.testing.assert_equal(param_dict["param_0"][0:32, :], p_0[0].numpy())
np.testing.assert_equal(param_dict["param_0"][32:64, :], p_1[0].numpy())
np.testing.assert_equal(param_dict["param_1"][:, 0:64], p_0[1].numpy())
np.testing.assert_equal(param_dict["param_1"][:, 64:128], p_1[1].numpy())
_run_with_nccl_session(devices, run_test)
def test_load_shard_broadcast():
devices = [0, 1]
param_dict = {
"param_0": np.random.uniform(size=[64, 128]).astype("float16"),
"param_1": np.random.uniform(size=[32, 128]).astype("float32"),
}
shard_info = {}
with tempfile.TemporaryDirectory() as path:
def run_test(sess):
loader = _create_loader(sess, path, param_dict, shard_info)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoadAll")
params = loader_load(loader)
p_0 = params.debug_get_from_remote(0)
p_1 = params.debug_get_from_remote(1)
np.testing.assert_equal(param_dict["param_0"], p_0[0].numpy())
np.testing.assert_equal(param_dict["param_0"], p_1[0].numpy())
np.testing.assert_equal(param_dict["param_1"], p_0[1].numpy())
np.testing.assert_equal(param_dict["param_1"], p_1[1].numpy())
_run_with_nccl_session(devices, run_test)
def test_load_qkv_proj_shard(): # pylint: disable=too-many-locals
devices = [0, 1]
num_shards = len(devices)
q_heads = 8
kv_heads = 10
head_dim = 10
hidden_size = 20
w_q = np.random.uniform(size=[q_heads * head_dim, hidden_size]).astype("float16")
w_k = np.random.uniform(size=[kv_heads * head_dim, hidden_size]).astype("float16")
w_v = np.random.uniform(size=[kv_heads * head_dim, hidden_size]).astype("float16")
w_qkv = np.concatenate([w_q, w_k, w_v], axis=0)
param_dict = {"w_qkv": w_qkv}
np_qkv = np.concatenate(
[
w_q.reshape((num_shards, q_heads // num_shards, head_dim, hidden_size)),
w_k.reshape((num_shards, kv_heads // num_shards, head_dim, hidden_size)),
w_v.reshape((num_shards, kv_heads // num_shards, head_dim, hidden_size)),
],
axis=1,
).reshape((num_shards, -1, hidden_size))
shard_info = {
"w_qkv": [
[
"tests.disco.shard_qkv_0",
[
(num_shards, (q_heads + kv_heads * 2) // num_shards, head_dim, hidden_size),
"float16",
],
num_shards,
q_heads,
kv_heads,
],
[
"tests.disco.shard_qkv_1",
[
(num_shards, (q_heads + kv_heads * 2) // num_shards * head_dim, hidden_size),
"float16",
],
],
],
}
with tempfile.TemporaryDirectory() as path:
def run_test(sess):
loader = _create_loader(sess, path, param_dict, shard_info)
loader_load = sess.get_global_func("runtime.disco.ShardLoaderLoad")
d_0 = loader_load(loader, Shape([0]))
np.testing.assert_equal(
np_qkv[0],
d_0.debug_get_from_remote(0).numpy(),
)
np.testing.assert_equal(
np_qkv[1],
d_0.debug_get_from_remote(1).numpy(),
)
_run_with_nccl_session(devices, run_test)
if __name__ == "__main__":
tvm.testing.main()
+390
View File
@@ -0,0 +1,390 @@
# 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()
+347
View File
@@ -0,0 +1,347 @@
# 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 session"""
# pylint: disable=missing-docstring
import socket
import subprocess
import sys
import tempfile
import threading
import numpy as np
import pytest
from tvm_ffi import Shape
from tvm_ffi.core import String
import tvm
import tvm.testing
# Imported for the side effect of registering the tests.disco.* worker functions.
from tvm.exec import disco_worker as _ # noqa: F401 # pylint: disable=unused-import
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
if di is None:
pytest.skip("disco runtime is not available", allow_module_level=True)
_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
def _numpy_to_worker_0(sess: di.Session, np_array: np.array, device):
x_array = sess.empty(np_array.shape, "float32", device=device)
host_array = tvm.runtime.tensor(np_array, device=device)
sess.copy_to_worker_0(host_array, x_array)
return x_array
def _numpy_from_worker_0(sess: di.Session, remote_array, shape, dtype):
host_array = tvm.runtime.empty(shape, dtype, device=tvm.cpu())
sess.copy_from_worker_0(host_array, remote_array)
sess.sync_worker_0()
return host_array.numpy()
_all_session_kinds = [di.ThreadedSession, di.ProcessSession, create_socket_session]
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_int(session_kind): # pylint: disable=invalid-name
num_workers = 4
sess = session_kind(num_workers=num_workers)
func: di.DPackedFunc = sess.get_global_func("tests.disco.add_one")
result: di.DRef = func(1)
for i in range(num_workers):
assert result.debug_get_from_remote(i) == 2
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_float(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
func: di.DPackedFunc = sess.get_global_func("tests.disco.add_one_float")
result: di.DRef = func(1.5)
for i in range(num_workers):
assert result.debug_get_from_remote(i) == 2.0
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_tensor(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
device = tvm.cpu(0)
x_np = np.arange(6).astype("float32").reshape([2, 3])
y_np = np.arange(6).astype("float32").reshape([2, 3]) + 1
x_disc = _numpy_to_worker_0(sess, x_np, device=device)
y_disc = sess.get_global_func("tests.disco.add_one_tensor")(x_disc)
y_nd = _numpy_from_worker_0(sess, y_disc, shape=y_np.shape, dtype=y_np.dtype)
np.testing.assert_equal(y_nd, y_np)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_string(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
func: di.DPackedFunc = sess.get_global_func("tests.disco.str")
result: di.DRef = func("hello")
for i in range(num_workers):
assert result.debug_get_from_remote(i) == "hello_suffix"
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_string_obj(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
func: di.DPackedFunc = sess.get_global_func("tests.disco.str_obj")
result: di.DRef = func(String("hello"))
for i in range(num_workers):
value = result.debug_get_from_remote(i)
assert isinstance(value, str)
assert value == "hello_suffix"
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_shape_tuple(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
func: di.DPackedFunc = sess.get_global_func("tests.disco.shape_tuple")
result: di.DRef = func(Shape([1, 2, 3]))
for i in range(num_workers):
value = result.debug_get_from_remote(i)
assert isinstance(value, Shape)
assert list(value) == [1, 2, 3, 4, 5]
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_vm_module(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
# pylint: disable=invalid-name
@I.ir_module(s_tir=True)
class TestMod:
@T.prim_func(s_tir=True)
def transpose(A: T.Buffer((8, 16), "float32"), B: T.Buffer((16, 8), "float32")):
for i, j in T.grid(16, 8):
with T.sblock("transpose"):
vi, vj = T.axis.remap("SS", [i, j])
B[vi, vj] = A[vj, vi]
@R.function
def main(A: R.Tensor((8, 16), dtype="float32")) -> R.Tensor((16, 8), dtype="float32"):
cls = TestMod
with R.dataflow():
B = R.call_tir(cls.transpose, (A,), out_ty=R.Tensor((16, 8), dtype="float32"))
R.output(B)
return B
# pylint: enable=invalid-name
with tempfile.TemporaryDirectory() as tmpdir:
path = tmpdir + "/test.so"
device = tvm.cpu()
x_np = np.arange(8 * 16).astype("float32").reshape([8, 16])
y_np = x_np.transpose()
tvm.compile(TestMod, target="llvm").export_library(path)
mod = sess.load_vm_module(path, device=device)
x_disc = _numpy_to_worker_0(sess, x_np, device=device)
y_disc = mod["main"](x_disc)
y_nd = _numpy_from_worker_0(sess, y_disc, shape=y_np.shape, dtype=y_np.dtype)
np.testing.assert_equal(y_nd, y_np)
# sync all workers to make sure the temporary files are cleaned up after all workers
# finish the execution
for i in range(num_workers):
sess._sync_worker(i)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
def test_vm_multi_func(session_kind):
num_workers = 4
sess = session_kind(num_workers=num_workers)
# pylint: disable=invalid-name
@I.ir_module(s_tir=True)
class TestMod:
@T.prim_func(s_tir=True)
def t1(A: T.Buffer((8, 16), "float32"), B: T.Buffer((16, 8), "float32")):
for i, j in T.grid(16, 8):
with T.sblock("t1"):
vi, vj = T.axis.remap("SS", [i, j])
B[vi, vj] = A[vj, vi]
@T.prim_func(s_tir=True)
def t2(A: T.Buffer((16, 8), "float32"), B: T.Buffer((8, 16), "float32")):
for i, j in T.grid(8, 16):
with T.sblock("t2"):
vi, vj = T.axis.remap("SS", [i, j])
B[vi, vj] = A[vj, vi]
@R.function
def transpose_1(A: R.Tensor((8, 16), dtype="float32")) -> R.Tensor(
(16, 8), dtype="float32"
):
R.func_attr({"global_symbol": "transpose_1"})
cls = TestMod
with R.dataflow():
B = R.call_tir(cls.t1, (A,), out_ty=R.Tensor((16, 8), dtype="float32"))
R.output(B)
return B
@R.function
def transpose_2(A: R.Tensor((16, 8), dtype="float32")) -> R.Tensor(
(8, 16), dtype="float32"
):
R.func_attr({"global_symbol": "transpose_2"})
cls = TestMod
with R.dataflow():
B = R.call_tir(cls.t2, (A,), out_ty=R.Tensor((8, 16), dtype="float32"))
R.output(B)
return B
# pylint: enable=invalid-name
with tempfile.TemporaryDirectory() as tmpdir:
path = tmpdir + "/test.so"
device = tvm.cpu()
x_np = np.arange(8 * 16).astype("float32").reshape([8, 16])
y_np = x_np.transpose()
tvm.compile(TestMod, target="llvm").export_library(path)
mod = sess.load_vm_module(path, device=device)
x_disc = _numpy_to_worker_0(sess, x_np, device=device)
y_disc = mod["transpose_1"](x_disc)
z_disc = mod["transpose_2"](y_disc)
y_nd = _numpy_from_worker_0(sess, y_disc, shape=y_np.shape, dtype=y_np.dtype)
z_nd = _numpy_from_worker_0(sess, z_disc, shape=x_np.shape, dtype=x_np.dtype)
np.testing.assert_equal(y_nd, y_np)
np.testing.assert_equal(z_nd, x_np)
# sync all workers to make sure the temporary files are cleaned up after all workers
# finish the execution
for i in range(num_workers):
sess._sync_worker(i)
@pytest.mark.parametrize("session_kind", _all_session_kinds)
@pytest.mark.parametrize("num_workers", [1, 2, 4])
def test_num_workers(session_kind, num_workers):
if session_kind == create_socket_session and num_workers < 2:
return
sess = session_kind(num_workers=num_workers)
assert sess.num_workers == num_workers
if __name__ == "__main__":
tvm.testing.main()