Skip to content

Commit 854f2e9

Browse files
authored
[Unity][Hexagon] Enable Relax VM for Hexagon (#14415)
This is attempt to port PR (tlc-pack/relax#167) (submitted by @psrivas2 and @YuchenJin) from tlc-pack/relax to enable Hexagon tests with Relax VM.
1 parent 23146d6 commit 854f2e9

File tree

4 files changed

+273
-3
lines changed

4 files changed

+273
-3
lines changed

python/tvm/contrib/hexagon/session.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
from typing import Union
2424

2525
import tvm
26+
from tvm import relax
2627
from tvm import rpc as _rpc
28+
from tvm.contrib import utils
2729
import tvm.contrib.hexagon as hexagon
2830
from tvm.relay.backend.executor_factory import (
2931
ExecutorFactoryModule,
@@ -283,13 +285,13 @@ def get_graph_debug_executor(
283285
graph_json, graph_debug_mod, self.device, dump_root=str(dump_root)
284286
)
285287

286-
def get_executor_from_factory(self, module: ExecutorFactoryModule):
288+
def get_executor_from_factory(self, module: Union[ExecutorFactoryModule, relax.Executable]):
287289
"""Create a local GraphModule which consumes a remote libmod.
288290
289291
Parameters
290292
----------
291293
292-
module : ExecutorFactoryModule
294+
module : Union[ExecutorFactoryModule, relax.Executable]
293295
294296
The module to upload to the remote
295297
session and load.
@@ -298,6 +300,8 @@ def get_executor_from_factory(self, module: ExecutorFactoryModule):
298300
return self._aot_executor_from_factory(module)
299301
if isinstance(module, GraphExecutorFactoryModule):
300302
return self._graph_executor_from_factory(module)
303+
if isinstance(module, relax.Executable):
304+
return self._relax_vm_executable_executor(module)
301305

302306
raise TypeError(f"Unsupported executor type: {type(module)}")
303307

@@ -349,6 +353,35 @@ def _graph_executor_from_factory(
349353
"""
350354
return self.get_graph_executor(module.get_graph_json(), module.get_lib())
351355

356+
def _relax_vm_executable_executor(self, vm_exec: relax.Executable):
357+
"""Create a local TVM module which consumes a remote vm executable.
358+
359+
Paramters
360+
---------
361+
362+
vm_exec : relax.Executable
363+
The Relax VM Executable to upload to the remote and load. This will typically be the
364+
output of `relax.build`.
365+
366+
Returns
367+
-------
368+
TVMModule :
369+
TVM module object
370+
"""
371+
assert self._rpc is not None, "Hexagon session must be started using __enter__ prior to use"
372+
373+
temp_dir = utils.tempdir()
374+
path_exec = temp_dir.relpath("exec.so")
375+
376+
vm_exec.mod.export_library(
377+
path_exec,
378+
fcompile=hexagon.create_aot_shared,
379+
hexagon_arch="v68",
380+
)
381+
382+
path = self.upload(path_exec, "exec.so")
383+
return self._rpc.get_function("tvm.hexagon.load_module")(str(path))
384+
352385
def _aot_executor_from_factory(
353386
self,
354387
module: Union[str, pathlib.Path, AOTExecutorFactoryModule],

python/tvm/runtime/module.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ def export_library(self, file_name, fcompile=None, addons=None, workspace_dir=No
498498
object_format = "cu"
499499
has_c_module = True
500500
else:
501-
assert module.type_key == "llvm" or module.type_key == "static_library"
501+
assert module.is_dso_exportable
502502
global_object_format = object_format = "o"
503503

504504
path_obj = os.path.join(workspace_dir, f"lib{index}.{object_format}")

src/runtime/hexagon/hexagon_module.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class HexagonModuleNode : public runtime::ModuleNode {
6464
const char* type_key() const final { return "hexagon"; }
6565
void SaveToFile(const std::string& file_name, const std::string& format) override;
6666
void SaveToBinary(dmlc::Stream* stream) override;
67+
bool IsDSOExportable() const final { return true; }
6768

6869
protected:
6970
std::string data_;
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
"""Relax hexagon test."""
18+
19+
import numpy as np
20+
import pytest
21+
import tvm.testing
22+
from tvm import relay, relax, runtime
23+
from tvm.relax.testing import relay_translator
24+
from tvm.contrib.hexagon.session import Session
25+
from tvm.relay import testing
26+
27+
28+
class TestConv2d:
29+
"""Test conv2d op"""
30+
31+
n_batch = tvm.testing.parameter(1, relay.Any())
32+
33+
@tvm.testing.requires_hexagon
34+
def test_conv2d(self, hexagon_session: Session, n_batch):
35+
"""Test Relax conv2d op and compare with Relay"""
36+
dtype = "float32"
37+
data = relay.var("data", relay.TensorType((n_batch, 64, 64, 3), dtype))
38+
weight = relay.var("weight", relay.TensorType((5, 5, 3, 8), dtype))
39+
y = relay.nn.conv2d(
40+
data,
41+
weight,
42+
padding=(2, 2),
43+
kernel_size=(5, 5),
44+
data_layout="NHWC",
45+
kernel_layout="HWIO",
46+
out_dtype="float32",
47+
)
48+
f = relay.Function([data, weight], y)
49+
relay_mod = tvm.IRModule.from_expr(f)
50+
51+
target_hexagon = tvm.target.hexagon("v68")
52+
target = tvm.target.Target(target_hexagon, host=target_hexagon)
53+
relax_mod = relay_translator.from_relay(relay_mod["main"], target)
54+
55+
exe = relax.build(relax_mod, target)
56+
dev = hexagon_session.device
57+
vm_mod = hexagon_session.get_executor_from_factory(exe)
58+
vm_rt = relax.VirtualMachine(vm_mod, dev)
59+
60+
data_np = np.random.rand(1, 64, 64, 3).astype(np.float32)
61+
weight_np = np.random.rand(5, 5, 3, 8).astype(np.float32)
62+
63+
# Run on hexagon and get result
64+
data = tvm.nd.array(data_np, dev)
65+
weight = tvm.nd.array(weight_np, dev)
66+
vm_rt.set_input("main", data, weight)
67+
vm_rt.invoke_stateful("main")
68+
hexagon_res = vm_rt.get_outputs("main")
69+
70+
# Compile and run on Relay for comparison.
71+
dev = tvm.cpu()
72+
data = tvm.nd.array(data_np, dev)
73+
weight = tvm.nd.array(weight_np, dev)
74+
75+
target = tvm.target.Target("llvm", host="llvm")
76+
vm_exec = relay.vm.compile(relay_mod, target=target)
77+
vm_factory = runtime.vm.VirtualMachine(vm_exec, tvm.cpu())
78+
relay_res = vm_factory.invoke("main", data, weight)
79+
tvm.testing.assert_allclose(hexagon_res.numpy(), relay_res.numpy(), rtol=1e-3)
80+
81+
82+
class TestMLP:
83+
"""Test MLP"""
84+
85+
n_batch = tvm.testing.parameter(1, relay.Any())
86+
87+
@tvm.testing.requires_hexagon
88+
def test_mlp(self, hexagon_session: Session, n_batch):
89+
"""Test Relax MLP and compare with Relay"""
90+
relay_mod, params = testing.mlp.get_workload(batch_size=n_batch, dtype="float32")
91+
92+
target_hexagon = tvm.target.hexagon("v68")
93+
target = tvm.target.Target(target_hexagon, host=target_hexagon)
94+
relax_mod = relay_translator.from_relay(relay_mod["main"], target, params)
95+
96+
exe = relax.build(relax_mod, target)
97+
hexagon_device = hexagon_session.device
98+
99+
vm_mod = hexagon_session.get_executor_from_factory(exe)
100+
vm_rt = relax.VirtualMachine(vm_mod, hexagon_device)
101+
102+
shape = (1, 1, 28, 28)
103+
data_np = np.random.rand(*shape).astype("float32")
104+
data = tvm.nd.array(data_np, hexagon_device)
105+
vm_rt.set_input("main", data)
106+
vm_rt.invoke_stateful("main")
107+
hexagon_res = vm_rt.get_outputs("main")
108+
109+
# Compile and run on Relay for comparison.
110+
cpu_dev = tvm.cpu()
111+
data = tvm.nd.array(data_np, cpu_dev)
112+
113+
target = tvm.target.Target("llvm", host="llvm")
114+
vm_exec = relay.vm.compile(relay_mod, target=target)
115+
vm_factory = runtime.vm.VirtualMachine(vm_exec, cpu_dev)
116+
relay_res = vm_factory.invoke("main", data, **params)
117+
tvm.testing.assert_allclose(hexagon_res.numpy(), relay_res.numpy(), rtol=1e-3)
118+
119+
120+
def get_onnx_mobilenet():
121+
"""Download and import mobilenet model with ONNX"""
122+
import onnx # pylint: disable=import-outside-toplevel
123+
124+
# pylint: disable=line-too-long
125+
model_url = "https://github.com/onnx/models/raw/main/vision/classification/mobilenet/model/mobilenetv2-7.onnx"
126+
model_path = tvm.contrib.download.download_testdata(
127+
model_url, "mobilenetv2-7.onnx", module="onnx"
128+
)
129+
return onnx.load(model_path)
130+
131+
132+
@pytest.mark.skip("takes too long (~20min)")
133+
@tvm.testing.requires_hexagon
134+
def test_mobilenet_onnx(hexagon_session: Session):
135+
"""Test MobileNetV2 ONNX model"""
136+
onnx_model = get_onnx_mobilenet()
137+
data_np = np.random.rand(1, 3, 224, 224).astype("float32")
138+
shape_dict = {"input": data_np.shape}
139+
relay_mod, _ = relay.frontend.from_onnx(onnx_model, shape_dict, freeze_params=True)
140+
141+
target_hexagon = tvm.target.hexagon("v68")
142+
target = tvm.target.Target(target_hexagon, host=target_hexagon)
143+
relax_mod = relay_translator.from_relay(relay_mod["main"], target_hexagon)
144+
145+
# Compile and run on Hexagon.
146+
exe = relax.build(relax_mod, target)
147+
dev = hexagon_session.device
148+
149+
vm_mod = hexagon_session.get_executor_from_factory(exe)
150+
vm_rt = relax.VirtualMachine(vm_mod, dev)
151+
data = tvm.nd.array(data_np, dev)
152+
vm_rt.set_input("main", data)
153+
vm_rt.invoke_stateful("main")
154+
hexagon_res = vm_rt.get_outputs("main")
155+
156+
# Compile and run on LLVM for comparison.
157+
relax_mod = relay_translator.from_relay(relay_mod["main"], "llvm")
158+
exe = relax.build(relax_mod, "llvm")
159+
dev = tvm.cpu()
160+
vm_rt = relax.VirtualMachine(exe, dev)
161+
data = tvm.nd.array(data_np, dev)
162+
llvm_res = vm_rt["main"](data)
163+
tvm.testing.assert_allclose(hexagon_res.numpy(), llvm_res.numpy(), rtol=1e-3)
164+
165+
166+
@pytest.mark.skip("takes too long (~20min)")
167+
@tvm.testing.requires_hexagon
168+
def test_mobilenet(hexagon_session: Session):
169+
"""Test MobileNet workload"""
170+
relay_mod, params = testing.mobilenet.get_workload(batch_size=1, dtype="float32")
171+
data_np = np.random.rand(1, 3, 224, 224).astype("float32")
172+
173+
target_hexagon = tvm.target.hexagon("v68")
174+
target = tvm.target.Target(target_hexagon, host=target_hexagon)
175+
176+
# translate the relay mobilenet and bind params
177+
relax_mod = relay_translator.from_relay(relay_mod["main"], target, params)
178+
179+
# Compile and run on Hexagon.
180+
exe = relax.build(relax_mod, target)
181+
dev = hexagon_session.device
182+
183+
vm_mod = hexagon_session.get_executor_from_factory(exe)
184+
vm_rt = relax.VirtualMachine(vm_mod, dev)
185+
data = tvm.nd.array(data_np, dev)
186+
vm_rt.set_input("main", data)
187+
vm_rt.invoke_stateful("main")
188+
hexagon_res = vm_rt.get_outputs("main")
189+
190+
# Compile and run on LLVM for comparison.
191+
relax_mod = relay_translator.from_relay(relay_mod["main"], "llvm", params)
192+
exe = relax.build(relax_mod, "llvm")
193+
dev = tvm.cpu()
194+
vm_rt = relax.VirtualMachine(exe, dev)
195+
data = tvm.nd.array(data_np, dev)
196+
llvm_res = vm_rt["main"](data)
197+
tvm.testing.assert_allclose(hexagon_res.numpy(), llvm_res.numpy(), rtol=1e-3)
198+
199+
200+
@pytest.mark.skip("takes too long (~20min)")
201+
@tvm.testing.requires_hexagon
202+
def test_mobilenet_dyn(hexagon_session: Session):
203+
"""Test MobileNet workload with dynamic batch size"""
204+
relay_mod, params = testing.mobilenet.get_workload(batch_size=relay.Any(), dtype="float32")
205+
data_np = np.random.rand(1, 3, 224, 224).astype("float32")
206+
207+
target_hexagon = tvm.target.hexagon("v68")
208+
target = tvm.target.Target(target_hexagon, host=target_hexagon)
209+
210+
# translate the relay mobilenet and bind params
211+
relax_mod = relay_translator.from_relay(relay_mod["main"], target, params)
212+
213+
# Compile and run on Hexagon.
214+
exe = relax.build(relax_mod, target)
215+
dev = hexagon_session.device
216+
217+
vm_mod = hexagon_session.get_executor_from_factory(exe)
218+
vm_rt = relax.VirtualMachine(vm_mod, dev)
219+
data = tvm.nd.array(data_np, dev)
220+
vm_rt.set_input("main", data)
221+
vm_rt.invoke_stateful("main")
222+
hexagon_res = vm_rt.get_outputs("main")
223+
224+
# Compile and run on Relay for comparison.
225+
dev = tvm.cpu()
226+
data = tvm.nd.array(data_np, dev)
227+
228+
target = tvm.target.Target("llvm", host="llvm")
229+
vm_exec = relay.vm.compile(relay_mod, target=target)
230+
vm_factory = runtime.vm.VirtualMachine(vm_exec, tvm.cpu())
231+
relay_res = vm_factory.invoke("main", data, **params)
232+
tvm.testing.assert_allclose(hexagon_res.numpy(), relay_res.numpy(), rtol=1e-3)
233+
234+
235+
if __name__ == "__main__":
236+
tvm.testing.main()

0 commit comments

Comments
 (0)