blob: f3d6a6a5eb05be49ca8c779875c44a60484ec6c1 [file]
# 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()