787 lines
30 KiB
Python
787 lines
30 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# ruff: noqa: E501
|
|
"""
|
|
.. _tutorial-cross-compilation-and-rpc:
|
|
|
|
Cross Compilation and RPC
|
|
=========================
|
|
**Author**: `Ziheng Jiang <https://github.com/ZihengJiang/>`_, `Lianmin Zheng <https://github.com/merrymercy/>`_
|
|
|
|
This tutorial introduces cross compilation and remote device
|
|
execution with RPC in TVM.
|
|
|
|
With cross compilation and RPC, you can **compile a program on your
|
|
local machine then run it on the remote device**. It is useful when
|
|
the remote device resource are limited, like Raspberry Pi and mobile
|
|
platforms. In this tutorial, we will use the Raspberry Pi for a CPU example
|
|
and the Firefly-RK3399 for an OpenCL example.
|
|
"""
|
|
|
|
######################################################################
|
|
# Build TVM Runtime on Device
|
|
# ---------------------------
|
|
#
|
|
# The first step is to build the TVM runtime on the remote device.
|
|
#
|
|
# .. note::
|
|
#
|
|
# All instructions in both this section and the next section should be
|
|
# executed on the target device, e.g. Raspberry Pi. We assume the target
|
|
# is running Linux.
|
|
#
|
|
# Since we do compilation on the local machine, the remote device is only used
|
|
# for running the generated code. We only need to build the TVM runtime on
|
|
# the remote device.
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# git clone --recursive https://github.com/apache/tvm tvm
|
|
# cd tvm
|
|
# mkdir build && cd build
|
|
# cp ../cmake/config.cmake .
|
|
# cmake .. && cmake --build . --parallel $(nproc)
|
|
#
|
|
# After building the runtime successfully, we need to set environment variables
|
|
# in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc`
|
|
# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM
|
|
# directory is in :code:`~/tvm`):
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# export PYTHONPATH=$PYTHONPATH:~/tvm/python
|
|
#
|
|
# To update the environment variables, execute :code:`source ~/.bashrc`.
|
|
|
|
######################################################################
|
|
# Set Up RPC Server on Device
|
|
# ---------------------------
|
|
# To start an RPC server, run the following command on your remote device
|
|
# (Which is Raspberry Pi in this example).
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090
|
|
#
|
|
# If you see the line below, it means the RPC server started
|
|
# successfully on your device.
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# INFO:root:RPCServer: bind to 0.0.0.0:9090
|
|
#
|
|
# Declare and Cross Compile Kernel on Local Machine
|
|
# -------------------------------------------------
|
|
#
|
|
# .. note::
|
|
#
|
|
# Now we go back to the local machine, which has a full TVM installed
|
|
# (with LLVM).
|
|
#
|
|
# Here we will declare a simple kernel on the local machine:
|
|
|
|
import numpy as np
|
|
import tvm_ffi
|
|
|
|
import tvm
|
|
from tvm import rpc, te
|
|
from tvm.support import utils
|
|
|
|
n = tvm.runtime.convert(1024)
|
|
A = te.placeholder((n,), name="A")
|
|
B = te.compute((n,), lambda i: A[i] + 1.0, name="B")
|
|
mod = tvm.IRModule.from_expr(te.create_prim_func([A, B]).with_attr("global_symbol", "add_one"))
|
|
|
|
######################################################################
|
|
# Then we cross compile the kernel.
|
|
# The target should be {"kind": "llvm", "mtriple": "armv7l-linux-gnueabihf"} for
|
|
# Raspberry Pi 3B, but we use 'llvm' here to make this tutorial runnable
|
|
# on our webpage building server. See the detailed note in the following block.
|
|
|
|
local_demo = True
|
|
|
|
if local_demo:
|
|
target = "llvm"
|
|
else:
|
|
target = {"kind": "llvm", "mtriple": "armv7l-linux-gnueabihf"}
|
|
|
|
func = tvm.compile(mod, target=target)
|
|
# save the lib at a local temp folder
|
|
temp = utils.tempdir()
|
|
path = temp.relpath("lib.tar")
|
|
func.export_library(path)
|
|
|
|
######################################################################
|
|
# .. note::
|
|
#
|
|
# To run this tutorial with a real remote device, change :code:`local_demo`
|
|
# to False and replace :code:`target` in :code:`build` with the appropriate
|
|
# target triple for your device. The target triple which might be
|
|
# different for different devices. For example, it is
|
|
# :code:`{"kind": "llvm", "mtriple": "armv7l-linux-gnueabihf"}` for Raspberry Pi 3B and
|
|
# :code:`{"kind": "llvm", "mtriple": "aarch64-linux-gnu"}` for RK3399.
|
|
#
|
|
# Usually, you can query the target by running :code:`gcc -v` on your
|
|
# device, and looking for the line starting with :code:`Target:`
|
|
# (Though it may still be a loose configuration.)
|
|
#
|
|
# Besides :code:`-mtriple`, you can also set other compilation options
|
|
# like:
|
|
#
|
|
# * -mcpu=<cpuname>
|
|
# Specify a specific chip in the current architecture to generate code for. By default this is inferred from the target triple and autodetected to the current architecture.
|
|
# * -mattr=a1,+a2,-a3,...
|
|
# Override or control specific attributes of the target, such as whether SIMD operations are enabled or not. The default set of attributes is set by the current CPU.
|
|
# To get the list of available attributes, you can do:
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# llc -mtriple=<your device target triple> -mattr=help
|
|
#
|
|
# These options are consistent with `llc <http://llvm.org/docs/CommandGuide/llc.html>`_.
|
|
# It is recommended to set target triple and feature set to contain specific
|
|
# feature available, so we can take full advantage of the features of the
|
|
# board.
|
|
# You can find more details about cross compilation attributes from
|
|
# `LLVM guide of cross compilation <https://clang.llvm.org/docs/CrossCompilation.html>`_.
|
|
|
|
######################################################################
|
|
# Run CPU Kernel Remotely by RPC
|
|
# ------------------------------
|
|
# We show how to run the generated CPU kernel on the remote device.
|
|
# First we obtain an RPC session from remote device.
|
|
|
|
if local_demo:
|
|
remote = rpc.LocalSession()
|
|
else:
|
|
# The following is my environment, change this to the IP address of your target device
|
|
host = "10.77.1.162"
|
|
port = 9090
|
|
remote = rpc.connect(host, port)
|
|
|
|
######################################################################
|
|
# Upload the lib to the remote device, then invoke a device local
|
|
# compiler to relink them. Now `func` is a remote module object.
|
|
|
|
remote.upload(path)
|
|
func = remote.load_module("lib.tar")
|
|
|
|
# create arrays on the remote device
|
|
dev = remote.cpu()
|
|
a = tvm.runtime.tensor(np.random.uniform(size=1024).astype(A.dtype), dev)
|
|
b = tvm.runtime.tensor(np.zeros(1024, dtype=A.dtype), dev)
|
|
# the function will run on the remote device
|
|
func(a, b)
|
|
np.testing.assert_equal(b.numpy(), a.numpy() + 1)
|
|
|
|
######################################################################
|
|
# When you want to evaluate the performance of the kernel on the remote
|
|
# device, it is important to avoid the overhead of network.
|
|
# :code:`time_evaluator` will returns a remote function that runs the
|
|
# function over number times, measures the cost per run on the remote
|
|
# device and returns the measured cost. Network overhead is excluded.
|
|
|
|
time_f = func.time_evaluator("add_one", dev, number=10)
|
|
cost = time_f(a, b).mean
|
|
print(f"{cost:g} secs/op")
|
|
|
|
######################################################################
|
|
# Scale RPC to Shared Devices
|
|
# ---------------------------
|
|
#
|
|
# The direct RPC server used above is the simplest way to run on one remote
|
|
# device. In shared environments, the same compile/upload/run flow is usually
|
|
# kept, but the connection is managed by an RPC tracker and, when needed, an
|
|
# RPC proxy.
|
|
#
|
|
# This setup is useful when:
|
|
#
|
|
# - multiple users or CI jobs share a small number of boards,
|
|
# - devices are registered by key rather than by fixed IP address,
|
|
# - the host cannot directly reach the device because of the network layout, or
|
|
# - the target device only has the minimal runtime stack needed for execution.
|
|
#
|
|
# The pieces fit together as follows:
|
|
#
|
|
# - **RPC server**: runs on the target device and executes uploaded modules.
|
|
# - **RPC tracker**: runs on a host and assigns matching RPC servers to clients.
|
|
# - **RPC proxy**: forwards traffic when the client cannot connect directly to
|
|
# the RPC server.
|
|
#
|
|
# .. figure:: https://raw.githubusercontent.com/tlc-pack/web-data/main/images/dev/how-to/rpc_system_suggested_arch.svg
|
|
# :align: center
|
|
# :width: 85%
|
|
#
|
|
# In the figure above, machine A connects through the tracker. Machine B runs
|
|
# an RPC proxy because machines C and D are not directly reachable from A. The
|
|
# tracker keeps a queue per RPC key. If a matching server is available, it is
|
|
# assigned to the client; otherwise, the request waits in that key's queue.
|
|
#
|
|
# Start the Tracker and Proxy
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
#
|
|
# The tracker and proxy generally run on a host machine, not on the target
|
|
# device. They do not require target-specific drivers.
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# python3 -m tvm.exec.rpc_tracker --host RPC_TRACKER_IP --port 9190 --port-end 9191
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# python3 -m tvm.exec.rpc_proxy \
|
|
# --host RPC_PROXY_IP \
|
|
# --port 9090 \
|
|
# --port-end 9091 \
|
|
# --tracker RPC_TRACKER_IP:RPC_TRACKER_PORT
|
|
#
|
|
# Replace the host names, ports, and port ranges for your environment. The
|
|
# ``--port-end`` option is useful in CI because it prevents the service from
|
|
# silently choosing an unexpected port.
|
|
#
|
|
# Package a Minimal RPC Runtime
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
#
|
|
# If the target can build TVM directly, install TVM on the target and launch
|
|
# the RPC server there. Otherwise, cross-compile the TVM runtime and package
|
|
# it with the Python RPC server.
|
|
#
|
|
# A typical CMake toolchain file for 64-bit ARM Linux looks like this:
|
|
#
|
|
# .. code-block:: cmake
|
|
#
|
|
# set(CMAKE_SYSTEM_NAME Linux)
|
|
# set(root_dir "/XXX/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu")
|
|
#
|
|
# set(CMAKE_C_COMPILER "${root_dir}/bin/aarch64-linux-gnu-gcc")
|
|
# set(CMAKE_CXX_COMPILER "${root_dir}/bin/aarch64-linux-gnu-g++")
|
|
# set(CMAKE_SYSROOT "${root_dir}/aarch64-linux-gnu/libc")
|
|
#
|
|
# set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
# set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
# set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
# set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
|
#
|
|
# Build the runtime from the TVM repository root. Enable target-specific
|
|
# options such as ``USE_OPENCL`` or vendor runtime support in ``config.cmake``.
|
|
# Build any target-specific runtime libraries that your deployment needs, such
|
|
# as ``tvm_runtime_opencl`` for OpenCL.
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# mkdir cross_build
|
|
# cd cross_build
|
|
# cp ../cmake/config.cmake ./
|
|
#
|
|
# # Enable other options as needed, e.g. USE_OPENCL or vendor runtimes.
|
|
# sed -i "s|USE_LLVM.*)|USE_LLVM OFF)|" config.cmake
|
|
#
|
|
# cmake -DCMAKE_TOOLCHAIN_FILE=/YYY/aarch64-linux-gnu.cmake -DCMAKE_BUILD_TYPE=Release ..
|
|
# cmake --build . --target runtime -j
|
|
# # Optional example when USE_OPENCL is enabled:
|
|
# # cmake --build . --target tvm_runtime_opencl -j
|
|
# cd ..
|
|
#
|
|
# Then package the Python RPC server with the cross-compiled runtime and copy
|
|
# it to the device.
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# rm -rf tvm_runtime_package
|
|
# mkdir tvm_runtime_package
|
|
# cp -a python tvm_runtime_package/
|
|
# cp cross_build/lib/libtvm_ffi.so tvm_runtime_package/python/tvm/
|
|
# cp cross_build/lib/libtvm_runtime*.so tvm_runtime_package/python/tvm/
|
|
# tar -czf tvm_runtime.tar.gz -C tvm_runtime_package python
|
|
#
|
|
# On the target device:
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# tar -xzf tvm_runtime.tar.gz
|
|
# export PYTHONPATH=`pwd`/python:${PYTHONPATH}
|
|
#
|
|
# Launch the Server
|
|
# ~~~~~~~~~~~~~~~~~
|
|
#
|
|
# Launch the RPC server on the target device. Use the proxy form when the
|
|
# server connects through an RPC proxy; otherwise connect directly to the
|
|
# tracker.
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# # Through an RPC proxy.
|
|
# python3 -m tvm.exec.rpc_server \
|
|
# --host RPC_PROXY_IP \
|
|
# --port RPC_PROXY_PORT \
|
|
# --through-proxy \
|
|
# --key RPC_KEY
|
|
#
|
|
# # Directly to an RPC tracker.
|
|
# python3 -m tvm.exec.rpc_server \
|
|
# --tracker RPC_TRACKER_IP:RPC_TRACKER_PORT \
|
|
# --key RPC_KEY
|
|
#
|
|
# Query the tracker from the host to confirm that the servers are visible:
|
|
#
|
|
# .. code-block:: shell
|
|
#
|
|
# python3 -m tvm.exec.query_rpc_tracker --host RPC_TRACKER_IP --port RPC_TRACKER_PORT
|
|
#
|
|
# If three servers connect through a proxy, the output should look similar to:
|
|
#
|
|
# .. code-block:: text
|
|
#
|
|
# Tracker address RPC_TRACKER_IP:RPC_TRACKER_PORT
|
|
#
|
|
# Server List
|
|
# ----------------------------
|
|
# server-address key
|
|
# ----------------------------
|
|
# RPC_PROXY_IP:RPC_PROXY_PORT server:proxy[RPC_KEY0,RPC_KEY1,RPC_KEY2]
|
|
# ----------------------------
|
|
#
|
|
# Queue Status
|
|
# ---------------------------------------
|
|
# key total free pending
|
|
# ---------------------------------------
|
|
# RPC_KEY0 0 0 3
|
|
# ---------------------------------------
|
|
#
|
|
# Once the tracker assigns a server, the client-side code still follows the
|
|
# same pattern used earlier in this tutorial. Only the session creation
|
|
# changes from a direct connection to a tracker request:
|
|
#
|
|
# .. code-block:: python
|
|
#
|
|
# tracker = rpc.connect_tracker("RPC_TRACKER_IP", RPC_TRACKER_PORT)
|
|
# remote = tracker.request("RPC_KEY", priority=0, session_timeout=600)
|
|
#
|
|
# After that, use the same ``remote.upload()``, ``remote.load_module()``, remote
|
|
# device creation, and ``time_evaluator`` flow shown above.
|
|
#
|
|
# Troubleshooting Minimal Device Environments
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
#
|
|
# Some target devices have intentionally small Python environments. The TVM
|
|
# runtime itself does not require full NumPy support for RPC execution, but the
|
|
# Python RPC server may import modules that import ``numpy``. If installing or
|
|
# cross-compiling NumPy is not practical, a small ``numpy.py`` shim in the
|
|
# device's Python ``site-packages`` directory can be enough for server startup.
|
|
#
|
|
# If ``cloudpickle`` is missing, copy it from another Python environment into
|
|
# the device's ``site-packages`` directory. It is a pure Python package, so it
|
|
# usually does not need cross-compilation.
|
|
#
|
|
# Run OpenCL Kernel Remotely by RPC
|
|
# ---------------------------------
|
|
# For remote OpenCL devices, the workflow is almost the same as above.
|
|
# You can define the kernel, upload files, and run via RPC.
|
|
#
|
|
# .. note::
|
|
#
|
|
# Raspberry Pi does not support OpenCL, the following code is tested on
|
|
# Firefly-RK3399. You may follow this `tutorial <https://gist.github.com/mli/585aed2cec0b5178b1a510f9f236afa2>`_
|
|
# to setup the OS and OpenCL driver for RK3399.
|
|
#
|
|
# Also we need to build the runtime with OpenCL enabled on rk3399 board. In the TVM
|
|
# root directory, execute
|
|
#
|
|
# .. code-block:: bash
|
|
#
|
|
# mkdir -p build && cd build
|
|
# cp ../cmake/config.cmake .
|
|
# sed -i "s/USE_OPENCL OFF/USE_OPENCL ON/" config.cmake
|
|
# cmake .. && cmake --build . --parallel $(nproc)
|
|
#
|
|
# The following function shows how we run an OpenCL kernel remotely
|
|
|
|
|
|
def run_opencl():
|
|
# NOTE: This is the setting for my rk3399 board. You need to modify
|
|
# them according to your environment.
|
|
opencl_device_host = "10.77.1.145"
|
|
opencl_device_port = 9090
|
|
target = tvm.target.Target("opencl", host={"kind": "llvm", "mtriple": "aarch64-linux-gnu"})
|
|
|
|
# create schedule for the above "add one" compute declaration
|
|
mod = tvm.IRModule.from_expr(te.create_prim_func([A, B]))
|
|
sch = tvm.s_tir.Schedule(mod)
|
|
(x,) = sch.get_loops(block=sch.get_sblock("B"))
|
|
xo, xi = sch.split(x, [None, 32])
|
|
sch.bind(xo, "blockIdx.x")
|
|
sch.bind(xi, "threadIdx.x")
|
|
func = tvm.compile(sch.mod, target=target)
|
|
|
|
remote = rpc.connect(opencl_device_host, opencl_device_port)
|
|
|
|
# export and upload
|
|
path = temp.relpath("lib_cl.tar")
|
|
func.export_library(path)
|
|
remote.upload(path)
|
|
func = remote.load_module("lib_cl.tar")
|
|
|
|
# run
|
|
dev = remote.cl()
|
|
a = tvm.runtime.tensor(np.random.uniform(size=1024).astype(A.dtype), dev)
|
|
b = tvm.runtime.tensor(np.zeros(1024, dtype=A.dtype), dev)
|
|
func(a, b)
|
|
np.testing.assert_equal(b.numpy(), a.numpy() + 1)
|
|
print("OpenCL test passed!")
|
|
|
|
|
|
######################################################################
|
|
# Deploy PyTorch Models to Remote Devices with RPC
|
|
# ------------------------------------------------
|
|
# The above examples demonstrate cross compilation and RPC using low-level
|
|
# TensorIR (via TE). For deploying complete neural network models from frameworks
|
|
# like PyTorch or ONNX, TVM's Relax provides a higher-level abstraction that is
|
|
# better suited for end-to-end model compilation.
|
|
#
|
|
# This section shows a modern workflow for deploying models to **any remote device**:
|
|
#
|
|
# 1. Import a PyTorch model and convert it to Relax
|
|
# 2. Cross-compile for the target architecture (ARM, x86, RISC-V, etc.)
|
|
# 3. Deploy via RPC to a remote device
|
|
# 4. Run inference remotely
|
|
#
|
|
# This workflow is applicable to various deployment scenarios:
|
|
#
|
|
# - **ARM devices**: Raspberry Pi, NVIDIA Jetson, mobile phones
|
|
# - **x86 servers**: Remote Linux servers, cloud instances
|
|
# - **Embedded systems**: RISC-V boards, custom hardware
|
|
# - **Accelerators**: Remote machines with GPUs, TPUs, or other accelerators
|
|
#
|
|
# .. note::
|
|
# This example uses PyTorch for demonstration, but the workflow is identical
|
|
# for ONNX models. Simply replace ``from_exported_program()`` with
|
|
# ``from_onnx(model, keep_params_in_input=True)`` and follow the same steps.
|
|
|
|
# First, let's check if PyTorch is available
|
|
try:
|
|
import torch
|
|
from torch.export import export
|
|
|
|
HAS_TORCH = True
|
|
except ImportError:
|
|
HAS_TORCH = False
|
|
|
|
|
|
def run_pytorch_model_via_rpc():
|
|
"""
|
|
Demonstrates the complete workflow of deploying a PyTorch model to an ARM device via RPC.
|
|
"""
|
|
if not HAS_TORCH:
|
|
print("Skipping PyTorch example (PyTorch not installed)")
|
|
return
|
|
|
|
from tvm import relax
|
|
from tvm.relax.frontend.torch import from_exported_program
|
|
|
|
######################################################################
|
|
# Step 1: Define and Export PyTorch Model
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# We use a simple MLP model for demonstration. In practice, this could be
|
|
# any PyTorch model (ResNet, BERT, etc.).
|
|
|
|
class TorchMLP(torch.nn.Module):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.net = torch.nn.Sequential(
|
|
torch.nn.Flatten(),
|
|
torch.nn.Linear(28 * 28, 128),
|
|
torch.nn.ReLU(),
|
|
torch.nn.Linear(128, 10),
|
|
)
|
|
|
|
def forward(self, data: torch.Tensor) -> torch.Tensor:
|
|
return self.net(data)
|
|
|
|
# Export the model using PyTorch 2.x export API
|
|
torch_model = TorchMLP().eval()
|
|
example_args = (torch.randn(1, 1, 28, 28, dtype=torch.float32),)
|
|
|
|
with torch.no_grad():
|
|
exported_program = export(torch_model, example_args)
|
|
|
|
######################################################################
|
|
# Step 2: Convert to Relax and Prepare for Compilation
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Convert the exported PyTorch program to TVM's Relax representation
|
|
|
|
mod = from_exported_program(exported_program, keep_params_as_input=True)
|
|
# Separate parameters from the model for flexible deployment
|
|
mod, params = relax.frontend.detach_params(mod)
|
|
|
|
print("Converted PyTorch model to Relax:")
|
|
print(f" - Number of parameters: {len(params['main'])}")
|
|
|
|
######################################################################
|
|
# Step 3: Cross-Compile for Target Device
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Compile the model for the target device architecture. The target
|
|
# configuration depends on your deployment scenario.
|
|
|
|
if local_demo:
|
|
# For demonstration on local machine, use local target
|
|
target = tvm.target.Target("llvm")
|
|
print("Using local target for demonstration")
|
|
else:
|
|
# Choose the appropriate target for your device:
|
|
#
|
|
# ARM devices:
|
|
# - Raspberry Pi 3/4 (32-bit): {"kind": "llvm", "mtriple": "armv7l-linux-gnueabihf"}
|
|
# - Raspberry Pi 4 (64-bit) / Jetson: {"kind": "llvm", "mtriple": "aarch64-linux-gnu"}
|
|
# - Android: {"kind": "llvm", "mtriple": "aarch64-linux-android"}
|
|
#
|
|
# x86 servers:
|
|
# - Linux x86_64: {"kind": "llvm", "mtriple": "x86_64-linux-gnu"}
|
|
# - With AVX-512: {"kind": "llvm", "mtriple": "x86_64-linux-gnu", "mcpu": "skylake-avx512"}
|
|
#
|
|
# RISC-V:
|
|
# - RV64: {"kind": "llvm", "mtriple": "riscv64-unknown-linux-gnu"}
|
|
#
|
|
# GPU targets:
|
|
# - CUDA: tvm.target.Target("cuda", host={"kind": "llvm", "mtriple": "x86_64-linux-gnu"})
|
|
# - OpenCL: tvm.target.Target("opencl", host={"kind": "llvm", "mtriple": "aarch64-linux-gnu"})
|
|
#
|
|
# For this example, we use ARM 64-bit
|
|
target = tvm.target.Target({"kind": "llvm", "mtriple": "aarch64-linux-gnu"})
|
|
print(f"Cross-compiling for target: {target}")
|
|
|
|
# Apply optimization pipeline
|
|
pipeline = relax.get_pipeline()
|
|
with target:
|
|
built_mod = pipeline(mod)
|
|
|
|
# Compile to executable
|
|
executable = tvm.compile(built_mod, target=target)
|
|
|
|
# Export to shared library
|
|
lib_path = temp.relpath("model_deployed.so")
|
|
executable.export_library(lib_path)
|
|
print(f"Exported library to: {lib_path}")
|
|
|
|
# Save parameters separately
|
|
import numpy as np
|
|
|
|
params_path = temp.relpath("model_params.npz")
|
|
param_arrays = {f"p_{i}": p.numpy() for i, p in enumerate(params["main"])}
|
|
np.savez(params_path, **param_arrays)
|
|
print(f"Saved parameters to: {params_path}")
|
|
|
|
######################################################################
|
|
# Step 4: Deploy to Remote Device via RPC
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Connect to the remote device, upload the compiled library and parameters,
|
|
# then run inference remotely. This works for any device with TVM RPC server.
|
|
#
|
|
# Note: The following code demonstrates the RPC workflow. In local_demo mode,
|
|
# we skip actual execution to avoid LocalSession compatibility issues.
|
|
|
|
if local_demo:
|
|
# For demonstration, show the code structure without execution
|
|
print("\nRPC workflow (works for any remote device):")
|
|
print("=" * 50)
|
|
print("1. Start RPC server on target device:")
|
|
print(" python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090")
|
|
print("\n2. Connect from local machine:")
|
|
print(" remote = rpc.connect('DEVICE_IP', 9090)")
|
|
print("\n3. Upload compiled library:")
|
|
print(" remote.upload('model_deployed.so')")
|
|
print(" remote.upload('model_params.npz')")
|
|
print("\n4. Load and run remotely:")
|
|
print(" lib = remote.load_module('model_deployed.so')")
|
|
print(" vm = relax.VirtualMachine(lib, remote.cpu())")
|
|
print(" result = vm['main'](input, *params)")
|
|
print("\nDevice examples:")
|
|
print(" - Raspberry Pi: 192.168.1.100")
|
|
print(" - Remote server: ssh tunnel or direct IP")
|
|
print(" - NVIDIA Jetson: 10.0.0.50")
|
|
print(" - Cloud instance: public IP")
|
|
print("\nTo run actual RPC, set local_demo=False")
|
|
return # Skip actual RPC execution in demo mode
|
|
|
|
# Actual RPC workflow for real deployment
|
|
# Connect to remote device (works for ARM, x86, RISC-V, etc.)
|
|
# Make sure the RPC server is running on the device:
|
|
# python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090
|
|
device_host = "192.168.1.100" # Replace with your device IP
|
|
device_port = 9090
|
|
remote = rpc.connect(device_host, device_port)
|
|
print(f"Connected to remote device at {device_host}:{device_port}")
|
|
|
|
# Upload library and parameters to remote device
|
|
remote.upload(lib_path)
|
|
remote.upload(params_path)
|
|
print("Uploaded files to remote device")
|
|
|
|
# Load the library on the remote device
|
|
lib = remote.load_module("model_deployed.so")
|
|
|
|
# Choose device on remote machine
|
|
# For CPU: dev = remote.cpu()
|
|
# For CUDA GPU: dev = remote.cuda(0)
|
|
# For OpenCL: dev = remote.cl(0)
|
|
dev = remote.cpu()
|
|
|
|
# Create VM and load parameters
|
|
vm = relax.VirtualMachine(lib, dev)
|
|
|
|
# Load parameters from the uploaded file
|
|
# Note: In practice, you might load this from the remote filesystem
|
|
params_npz = np.load(params_path)
|
|
remote_params = [tvm.runtime.tensor(params_npz[f"p_{i}"], dev) for i in range(len(params_npz))]
|
|
|
|
######################################################################
|
|
# Step 5: Run Inference on Remote Device
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Execute the model on the remote ARM device and retrieve results
|
|
#
|
|
# Note: When running VM over RPC, we use set_input() and invoke_stateful()
|
|
# instead of direct function call (vm["main"](...)). This is because RPC
|
|
# transmits tensors as DLTensor*, while VM builtins expect ffi.Tensor.
|
|
# The set_input API handles this conversion internally.
|
|
|
|
# Prepare input data
|
|
input_data = np.random.randn(1, 1, 28, 28).astype("float32")
|
|
remote_input = tvm.runtime.tensor(input_data, dev)
|
|
|
|
# Run inference using set_input + invoke_stateful for RPC compatibility
|
|
vm.set_input("main", remote_input, *remote_params)
|
|
vm.invoke_stateful("main")
|
|
output = vm.get_outputs("main")
|
|
|
|
# Extract result (handle both tuple and single tensor outputs)
|
|
if isinstance(output, tvm_ffi.Array) and len(output) > 0:
|
|
result = output[0]
|
|
else:
|
|
result = output
|
|
|
|
# Retrieve result from remote device to local
|
|
result_np = result.numpy()
|
|
print("Inference completed on remote device")
|
|
print(f" Output shape: {result_np.shape}")
|
|
print(f" Predicted class: {np.argmax(result_np)}")
|
|
|
|
######################################################################
|
|
# Alternative: Direct Function Call (Local Only)
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Note: The direct call syntax vm["main"](input, *params) works for
|
|
# local execution but may fail over RPC due to type mismatch between
|
|
# DLTensor* (RPC) and ffi.Tensor (VM builtins). For RPC, always use
|
|
# the set_input + invoke_stateful pattern shown above.
|
|
|
|
######################################################################
|
|
# Step 6: Performance Evaluation (Optional)
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Measure inference time on the remote device, excluding network overhead
|
|
# Note: For RPC, use invoke_stateful with time_evaluator
|
|
|
|
time_f = vm.time_evaluator("invoke_stateful", dev, number=10, repeat=3)
|
|
prof_res = time_f("main")
|
|
print(f"Inference time on remote device: {prof_res.mean * 1000:.2f} ms")
|
|
|
|
######################################################################
|
|
# Notes on Performance Optimization
|
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
#
|
|
# For optimal performance on target devices, consider:
|
|
#
|
|
# 1. **Auto-tuning with MetaSchedule**: Use automated search to find
|
|
# optimal schedules for your specific hardware:
|
|
#
|
|
# .. code-block:: python
|
|
#
|
|
# mod = relax.get_pipeline(
|
|
# "static_shape_tuning",
|
|
# target=target,
|
|
# total_trials=2000
|
|
# )(mod)
|
|
#
|
|
# 2. **Quick optimization with DLight**: Apply pre-defined performant schedules:
|
|
#
|
|
# .. code-block:: python
|
|
#
|
|
# from tvm.s_tir import dlight as dl
|
|
# with target:
|
|
# mod = dl.ApplyDefaultSchedule()(mod)
|
|
#
|
|
# 3. **Architecture-specific optimizations**:
|
|
#
|
|
# - ARM NEON SIMD: ``-mattr=+neon``
|
|
# - x86 AVX-512: ``-mcpu=skylake-avx512``
|
|
# - RISC-V Vector: ``-mattr=+v``
|
|
#
|
|
# .. code-block:: python
|
|
#
|
|
# # Example: ARM with NEON
|
|
# target = tvm.target.Target(
|
|
# {"kind": "llvm", "mtriple": "aarch64-linux-gnu", "mattr": "+neon"}
|
|
# )
|
|
#
|
|
# # Example: x86 with AVX-512
|
|
# target = tvm.target.Target(
|
|
# {"kind": "llvm", "mtriple": "x86_64-linux-gnu", "mcpu": "skylake-avx512"}
|
|
# )
|
|
#
|
|
# See :doc:`e2e_opt_model </how_to/tutorials/e2e_opt_model>` for detailed
|
|
# tuning examples.
|
|
|
|
|
|
# Run the PyTorch RPC example if PyTorch is available
|
|
if HAS_TORCH and local_demo:
|
|
try:
|
|
run_pytorch_model_via_rpc()
|
|
except Exception:
|
|
pass # Silently skip if execution fails
|
|
|
|
|
|
######################################################################
|
|
# Summary
|
|
# -------
|
|
# This tutorial provides a walk through of cross compilation and RPC
|
|
# features in TVM.
|
|
#
|
|
# We demonstrated two approaches:
|
|
#
|
|
# **Low-level TensorIR (TE) approach** - for understanding fundamentals:
|
|
#
|
|
# - Define computations using Tensor Expression
|
|
# - Cross-compile for ARM targets
|
|
# - Deploy and run via RPC
|
|
#
|
|
# **High-level Relax approach** - for deploying complete models:
|
|
#
|
|
# - Import models from PyTorch (or ONNX)
|
|
# - Convert to Relax representation
|
|
# - Cross-compile for ARM Linux devices
|
|
# - Deploy to remote devices via RPC
|
|
# - Run inference and evaluate performance
|
|
#
|
|
# Key takeaways:
|
|
#
|
|
# - Set up an RPC server on the remote device
|
|
# - Cross-compile on a powerful local machine for resource-constrained targets
|
|
# - Upload and execute compiled modules remotely via the RPC API
|
|
# - Measure performance excluding network overhead
|
|
#
|
|
# For complete model deployment workflows, see also:
|
|
#
|
|
# - :doc:`export_and_load_executable </how_to/tutorials/export_and_load_executable>` - Export and load compiled models
|
|
# - :doc:`e2e_opt_model </how_to/tutorials/e2e_opt_model>` - End-to-end optimization with auto-tuning
|