| # 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. |
| """iOS RPC Server tests.""" |
| # pylint: disable=invalid-name, no-value-for-parameter, missing-function-docstring, import-error |
| import multiprocessing |
| import pytest |
| import numpy as np |
| |
| import tvm.testing |
| import tvm.relay.testing |
| from tvm import te |
| from tvm import rpc |
| from tvm import relay, auto_scheduler |
| from tvm.contrib import utils, xcode, graph_executor |
| from tvm.autotvm.measure import request_remote |
| from tvm.auto_scheduler.measure_record import load_records |
| from tvm.auto_scheduler.measure import MeasureErrorNo |
| from tvm.auto_scheduler.utils import call_func_with_timeout |
| from tvm.contrib.popen_pool import PopenWorker, StatusKind |
| from tvm.rpc import tracker, proxy, server_ios_launcher |
| |
| |
| HOST_URL = "0.0.0.0" |
| HOST_PORT = 9190 |
| DEVICE_KEY = "ios_mobile_device" |
| |
| |
| TEMPORARY_DIRECTORY = utils.tempdir() |
| ARCH = "x86_64" |
| SDK = "iphonesimulator" |
| DSO_NAME = "lib.dylib" |
| DTYPE = "float32" |
| |
| |
| np.random.seed(0) |
| |
| |
| ios_rpc_bundle_description_required = pytest.mark.skipif( |
| not server_ios_launcher.ServerIOSLauncher.is_compatible_environment(), |
| reason="To run this test, you need to set environment variables required in ServerIOSLauncher.", |
| ) |
| |
| |
| @pytest.fixture(scope="session", autouse=True) |
| def setup_and_teardown_actions(): |
| """Setup and teardown actions for pytest.""" |
| |
| # No setup actions |
| yield |
| # Teardown actions: |
| server_ios_launcher.ServerIOSLauncher.shutdown_booted_devices() |
| |
| |
| def setup_rpc_standalone_configuration(f): |
| """ |
| Host -- RPC server |
| """ |
| |
| def wrapper(): |
| with server_ios_launcher.ServerIOSContextManager( |
| mode=server_ios_launcher.RPCServerMode.standalone.value, |
| host=HOST_URL, |
| port=HOST_PORT, |
| key=DEVICE_KEY, |
| ) as ios_server: |
| f(host=ios_server.host, port=ios_server.port) |
| |
| return wrapper |
| |
| |
| def setup_rpc_proxy_configuration(f): |
| """ |
| Host -- Proxy -- RPC server |
| """ |
| |
| def wrapper(): |
| proxy_server = proxy.Proxy(host=HOST_URL, port=HOST_PORT) |
| with server_ios_launcher.ServerIOSContextManager( |
| mode=server_ios_launcher.RPCServerMode.proxy.value, |
| host=proxy_server.host, |
| port=proxy_server.port, |
| key=DEVICE_KEY, |
| ): |
| f(host=proxy_server.host, port=proxy_server.port) |
| proxy_server.terminate() |
| |
| return wrapper |
| |
| |
| def setup_rpc_tracker_configuration(f): |
| """ |
| tracker |
| / \ |
| Host -- RPC server |
| """ |
| |
| def wrapper(): |
| tracker_server = tracker.Tracker(host=HOST_URL, port=HOST_PORT, silent=True) |
| with server_ios_launcher.ServerIOSContextManager( |
| mode=server_ios_launcher.RPCServerMode.tracker.value, |
| host=tracker_server.host, |
| port=tracker_server.port, |
| key=DEVICE_KEY, |
| ): |
| f(host=tracker_server.host, port=tracker_server.port) |
| tracker_server.terminate() |
| |
| return wrapper |
| |
| |
| def setup_rpc_tracker_via_proxy_configuration(f): |
| """ |
| tracker |
| / \ |
| Host -- Proxy -- RPC server |
| """ |
| |
| def wrapper(): |
| tracker_server = tracker.Tracker(host=HOST_URL, port=HOST_PORT, silent=True) |
| proxy_server_tracker = proxy.Proxy( |
| host=HOST_URL, port=8888, tracker_addr=(tracker_server.host, tracker_server.port) |
| ) |
| with server_ios_launcher.ServerIOSContextManager( |
| mode=server_ios_launcher.RPCServerMode.proxy.value, |
| host=proxy_server_tracker.host, |
| port=proxy_server_tracker.port, |
| key=DEVICE_KEY, |
| ): |
| f(host=tracker_server.host, port=tracker_server.port) |
| proxy_server_tracker.terminate() |
| tracker_server.terminate() |
| |
| return wrapper |
| |
| |
| def wrapper_for_call_function_with_timeout(timeout, func, args=(), kwargs=None): |
| """Wrapper for call_func_with_timeout.""" |
| |
| def wrapper(*_args, **_kwargs): |
| """ |
| This wrapper is needed because the cloudpicle |
| cannot serialize objects that contain pointers (RPCSession) |
| """ |
| func(*_args, **_kwargs) |
| return StatusKind.COMPLETE |
| |
| worker = PopenWorker() |
| ret = call_func_with_timeout(worker, timeout=timeout, func=wrapper, args=args, kwargs=kwargs) |
| if isinstance(ret, Exception): |
| raise ret |
| return ret |
| |
| |
| def try_create_remote_session(session_factory, args=(), kwargs=None): |
| """Deadlock-safe RPC Session creation.""" |
| |
| try: |
| successful_attempt = True |
| results = [] |
| for _ in range(2): |
| ret = wrapper_for_call_function_with_timeout( |
| timeout=10, func=session_factory, args=args, kwargs=kwargs |
| ) |
| results.append(ret) |
| if not np.all(np.array(results) == StatusKind.COMPLETE): |
| raise ValueError("One or more sessions ended incorrectly.") |
| except Exception as e: # pylint: disable=broad-except |
| successful_attempt = False |
| print(e) |
| return successful_attempt |
| |
| |
| def ios_create_dylib(output, objects, **kwargs): # pylint: disable=unused-argument |
| xcode.create_dylib(output, objects, arch=ARCH, sdk=SDK) |
| |
| |
| ios_create_dylib.output_format = "dylib" |
| |
| |
| def export_lib(lib): |
| """Export lib to temporary directory.""" |
| |
| path_dso = TEMPORARY_DIRECTORY.relpath(DSO_NAME) |
| lib.export_library(path_dso, fcompile=ios_create_dylib) |
| return path_dso |
| |
| |
| def get_add_relay_module(a_numpy, b_numpy): |
| """Get simple relay module that add two tensors.""" |
| |
| a = relay.var("a", shape=a_numpy.shape, dtype=DTYPE) |
| b = relay.var("b", shape=b_numpy.shape, dtype=DTYPE) |
| params = {} |
| out = tvm.IRModule.from_expr(relay.add(a, b)) |
| return out, params |
| |
| |
| def get_add_module(target): |
| """Get simple module that add two tensors.""" |
| |
| n = te.var("n") |
| A = te.placeholder((n,), name="A") |
| B = te.placeholder((n,), name="B") |
| C = te.compute(A.shape, lambda i: A[i] + B[i], name="C") |
| s = te.create_schedule(C.op) |
| return tvm.build(s, [A, B, C], target=target, target_host=target, name="simple_add") |
| |
| |
| @pytest.mark.dependency() |
| @ios_rpc_bundle_description_required |
| @setup_rpc_standalone_configuration |
| def test_rpc_standalone(host, port): |
| status_ok = try_create_remote_session(session_factory=rpc.connect, args=(host, port)) |
| assert status_ok |
| |
| |
| @pytest.mark.dependency() |
| @ios_rpc_bundle_description_required |
| @setup_rpc_proxy_configuration |
| def test_rpc_proxy(host, port): |
| status_ok = try_create_remote_session( |
| session_factory=rpc.connect, args=(host, port, DEVICE_KEY) |
| ) |
| assert status_ok |
| |
| |
| @pytest.mark.dependency() |
| @ios_rpc_bundle_description_required |
| @setup_rpc_tracker_configuration |
| def test_rpc_tracker(host, port): |
| status_ok = try_create_remote_session( |
| session_factory=request_remote, args=(DEVICE_KEY, host, port) |
| ) |
| assert status_ok |
| |
| |
| @pytest.mark.dependency() |
| @ios_rpc_bundle_description_required |
| @setup_rpc_tracker_via_proxy_configuration |
| def test_rpc_tracker_via_proxy(host, port): |
| status_ok = try_create_remote_session( |
| session_factory=request_remote, args=(DEVICE_KEY, host, port) |
| ) |
| assert status_ok |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_standalone"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_standalone_configuration |
| def test_can_call_remote_function_with_rpc_standalone(host, port): |
| remote_session = rpc.connect(host, port) |
| f = remote_session.get_function("runtime.GetFFIString") |
| assert f("hello") == "hello" |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_proxy"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_proxy_configuration |
| def test_can_call_remote_function_with_rpc_proxy(host, port): |
| remote_session = rpc.connect(host, port, key=DEVICE_KEY) |
| f = remote_session.get_function("runtime.GetFFIString") |
| assert f("hello") == "hello" |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_tracker"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_tracker_configuration |
| def test_can_call_remote_function_with_rpc_tracker(host, port): |
| remote_session = request_remote(DEVICE_KEY, host, port) |
| f = remote_session.get_function("runtime.GetFFIString") |
| assert f("hello") == "hello" |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_tracker_via_proxy"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_tracker_via_proxy_configuration |
| def test_can_call_remote_function_with_rpc_tracker_via_proxy(host, port): |
| remote_session = request_remote(DEVICE_KEY, host, port) |
| f = remote_session.get_function("runtime.GetFFIString") |
| assert f("hello") == "hello" |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_standalone"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_standalone_configuration |
| def test_basic_functionality_of_rpc_session(host, port): |
| remote_session = rpc.connect(host, port) |
| device = remote_session.cpu(0) |
| |
| target = tvm.target.Target(target=f"llvm -mtriple={ARCH}-apple-darwin") |
| lib = get_add_module(target) |
| path_dso = export_lib(lib) |
| |
| # Check correct upload |
| remote_session.upload(path_dso) |
| |
| # Check correct download |
| downloaded_lib = remote_session.download(DSO_NAME) |
| with open(path_dso, "rb") as source_lib_file: |
| assert downloaded_lib == bytearray( |
| source_lib_file.read() |
| ), "The downloaded module does not match the loaded module" |
| |
| # Check correct remote computing |
| lib = remote_session.load_module(DSO_NAME) |
| n = 100 |
| a = tvm.nd.array(np.random.uniform(size=n).astype(DTYPE), device) |
| b = tvm.nd.array(np.random.uniform(size=n).astype(DTYPE), device) |
| c = tvm.nd.array(np.zeros(n, dtype=DTYPE), device) |
| lib(a, b, c) |
| tvm.testing.assert_allclose(c.numpy(), a.numpy() + b.numpy()) |
| |
| # Check correct remove |
| remote_session.remove(DSO_NAME) |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_standalone"]) |
| @pytest.mark.xfail(reason="Not implemented functionality") |
| @ios_rpc_bundle_description_required |
| @setup_rpc_standalone_configuration |
| def test_cleanup_workspace_after_session_end(host, port): |
| # Arrange |
| remote_session = rpc.connect(host, port) |
| target = tvm.target.Target(target=f"llvm -mtriple={ARCH}-apple-darwin") |
| lib = get_add_module(target) |
| path_dso = export_lib(lib) |
| remote_session.upload(path_dso) |
| |
| # Act |
| del remote_session |
| remote_session = rpc.connect(host, port) |
| try: |
| remote_session.download(DSO_NAME) |
| status_ok = False |
| except Exception as _: # pylint: disable=broad-except |
| status_ok = True |
| |
| # Assert |
| assert status_ok, "Workspace not cleared after RPC Session termination." |
| |
| |
| @pytest.mark.dependency(depends=["test_rpc_standalone"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_standalone_configuration |
| def test_graph_executor_remote_run(host, port): |
| remote_session = rpc.connect(host, port) |
| target = tvm.target.Target(target=f"llvm -mtriple={ARCH}-apple-darwin") |
| device = remote_session.cpu(0) |
| |
| size = 100 |
| a = np.random.uniform(size=size).astype(DTYPE) |
| b = np.random.uniform(size=size).astype(DTYPE) |
| mod, params = get_add_relay_module(a, b) |
| with tvm.transform.PassContext(opt_level=3): |
| lib = relay.build(mod, target=target, target_host=target, params=params) |
| |
| path_dso = export_lib(lib) |
| remote_session.upload(path_dso) |
| lib = remote_session.load_module(DSO_NAME) |
| |
| gen_module = graph_executor.GraphModule(lib["default"](device)) |
| |
| # Check set input |
| gen_module.set_input("a", tvm.nd.array(a)) |
| gen_module.set_input("b", tvm.nd.array(b)) |
| tvm.testing.assert_allclose(gen_module.get_input(0).numpy(), a) |
| tvm.testing.assert_allclose(gen_module.get_input(1).numpy(), b) |
| |
| # Check run |
| gen_module.run() |
| out = gen_module.get_output(0) |
| tvm.testing.assert_allclose(out.numpy(), a + b) |
| |
| |
| @pytest.mark.xfail( |
| strict=False, reason="flaky test (see https://github.com/apache/tvm/issues/9824)" |
| ) |
| @pytest.mark.dependency(depends=["test_rpc_tracker"]) |
| @ios_rpc_bundle_description_required |
| @setup_rpc_tracker_configuration |
| def test_check_auto_schedule_tuning(host, port): # pylint: disable=too-many-locals |
| log_file = TEMPORARY_DIRECTORY.relpath("ios_tuning_stat.log") |
| target = tvm.target.Target(target=f"llvm -mtriple={ARCH}-apple-darwin") |
| mod, params = relay.testing.mlp.get_workload(batch_size=4, image_shape=(1, 4, 4)) |
| |
| try: |
| status_ok = True |
| measure_runner = auto_scheduler.RPCRunner( |
| DEVICE_KEY, |
| host, |
| port, |
| min_repeat_ms=1, |
| timeout=10, |
| n_parallel=multiprocessing.cpu_count(), |
| ) |
| builder = auto_scheduler.LocalBuilder(timeout=10, build_func=ios_create_dylib) |
| tune_option = auto_scheduler.TuningOptions( |
| builder=builder, |
| num_measure_trials=2, |
| num_measures_per_round=1, |
| runner=measure_runner, |
| measure_callbacks=[auto_scheduler.RecordToFile(log_file)], |
| verbose=0, |
| ) |
| |
| tasks, task_weights = auto_scheduler.extract_tasks(mod["main"], params, target) |
| tasks, task_weights = tasks[:2], task_weights[:2] |
| tuner = auto_scheduler.TaskScheduler(tasks, task_weights) |
| tuner.tune(tune_option, search_policy="sketch.random") |
| |
| # Check tuning log |
| tuning_statistic = list(load_records(log_file)) |
| for _, measure_result in tuning_statistic: |
| if measure_result.error_no != MeasureErrorNo.NO_ERROR: |
| raise ValueError( |
| f"Error for MeasureResult. Error code: {measure_result.error_no}," |
| f" for details see MeasureErrorNO." |
| ) |
| |
| except Exception as e: # pylint: disable=broad-except |
| status_ok = False |
| print(e) |
| |
| assert status_ok, "Tuning failed, see logs." |
| |
| |
| if __name__ == "__main__": |
| tvm.testing.main() |