# 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()