Skip to content

Commit 44de505

Browse files
[TVMC][microNPU] tvmc option for printing which operators are offloaded to Ethos-U
1 parent 6c5be6f commit 44de505

File tree

10 files changed

+428
-26
lines changed

10 files changed

+428
-26
lines changed

python/tvm/driver/tvmc/compiler.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
"""
2020
import logging
2121
import os.path
22+
from copy import deepcopy
2223
from typing import Any, Optional, Dict, List, Union, Callable, Sequence
2324
from pathlib import Path
25+
import re
2426

2527
import tvm
2628
from tvm import autotvm, auto_scheduler
@@ -30,6 +32,7 @@
3032
from tvm.ir.memory_pools import WorkspaceMemoryPools
3133
from tvm.target import Target
3234
from tvm.relay.backend import Executor, Runtime
35+
from tvm.relay.analysis.operations_distribution import analyze_operations_distribution
3336

3437
from . import composite_target, frontends, TVMCException
3538
from .model import TVMCModel, TVMCPackage
@@ -73,6 +76,16 @@ def add_compile_parser(subparsers, _, json_params):
7376
default="",
7477
help="comma separated list of formats to export the input model, e.g. 'asm,ll,relay'.",
7578
)
79+
parser.add_argument(
80+
"--dump-offloads",
81+
default="",
82+
help="output a mapping of which operations of the initial Relay "
83+
"will be transferred to which backend, indicating the composite "
84+
"that includes those operations, "
85+
"e.g. '--dump-offloads -' to dump to the console, "
86+
"e.g. '--dump-offloads <path_to_file>' to dump to the file. "
87+
"If not presented, no output is done. ",
88+
)
7689
parser.add_argument(
7790
"--model-format",
7891
choices=frontends.get_frontend_names(),
@@ -175,6 +188,8 @@ def drive_compile(args):
175188

176189
dump_code = [x.strip() for x in args.dump_code.split(",")] if args.dump_code else None
177190

191+
dump_offloads = args.dump_offloads if args.dump_offloads else ""
192+
178193
additional_targets = reconstruct_target_args(args)
179194
workspace_pools_target, extra_targets = target_from_cli(args.target, additional_targets)
180195

@@ -190,6 +205,7 @@ def drive_compile(args):
190205
cross_options=args.cross_compiler_options,
191206
output_format=args.output_format,
192207
dump_code=dump_code,
208+
dump_offloads=dump_offloads,
193209
target_host=None,
194210
desired_layout=args.desired_layout,
195211
disabled_pass=args.disabled_pass,
@@ -216,6 +232,7 @@ def compile_model(
216232
cross_options: Optional[str] = None,
217233
output_format: str = "so",
218234
dump_code: Optional[List[str]] = None,
235+
dump_offloads: str = "",
219236
target_host: Optional[str] = None,
220237
desired_layout: Optional[str] = None,
221238
disabled_pass: Optional[str] = None,
@@ -257,6 +274,10 @@ def compile_model(
257274
dump_code : list, optional
258275
Dump the generated code for the specified source types, on
259276
the requested target.
277+
dump_offloads : str
278+
Dump the information about the partition of input model's layers by external codegen.
279+
Can be '' to not dump at all, '-' to dump to the console
280+
or '<path_to_file>' to dump to the specified file.
260281
target_host : str, optional
261282
The target of the host machine if host-side code
262283
needs to be generated.
@@ -292,6 +313,11 @@ def compile_model(
292313

293314
config = parse_configs(pass_context_configs)
294315

316+
initial_relay = None
317+
if dump_offloads != "":
318+
# remember initial relay
319+
initial_relay = deepcopy(tvmc_model.mod)
320+
295321
tvm_target, extra_targets = target_from_cli(target, additional_target_options)
296322
tvm_target, target_host = Target.canon_target_and_host(tvm_target, target_host)
297323

@@ -316,6 +342,10 @@ def compile_model(
316342
for partition_function, opts in zip(partition_functions, partition_opts):
317343
mod = partition_function(mod, params, mod_name=mod_name, **opts)
318344

345+
if initial_relay:
346+
# dump which operations are offloaded to which backend
347+
dump_operation_offloads(mod, initial_relay, dump_offloads)
348+
319349
if tuning_records and os.path.exists(tuning_records):
320350
logger.debug("tuning records file provided: %s", tuning_records)
321351

@@ -459,3 +489,79 @@ def save_dumps(module_name: str, dumps: Dict[str, str], dump_root: str = "."):
459489
dump_name = module_name + "." + dump_format
460490
with open(Path(dump_root, dump_name), "w") as f:
461491
f.write(dumps[dump_format])
492+
493+
494+
def dump_operation_offloads(mod: tvm.ir.IRModule, initial_mod: tvm.ir.IRModule, dump_path: str):
495+
"""This helper function forms a line-by-line output of the initial Relay lines,
496+
indicating which operations are ported to which target,
497+
and indicating the composite that includes those operations;
498+
the 'generic' target refers to operations uploaded to the host, e.g
499+
'target1 <- target2.qnn_conv2d'
500+
'target1 <- %0 = qnn.conv2d(%tfl.quantize, %v_param_1, ...'
501+
'target1 <- %1 = nn.bias_add(%0, %v_param_2, axis=3);'
502+
'target1 <- %2 = qnn.requantize(%1, meta[relay.Constant]...'
503+
'target2 <- target2.reshape'
504+
'target2 <- %3 = reshape(%2, newshape=[1, 1001]);'
505+
'generic <- %4 = nn.pad(%3, -128f, pad_width=[[0, 0], [1, 1]...'
506+
507+
Parameters
508+
----------
509+
mod : tvm.ir.IRModule
510+
The IRModule that gets generated from a relay frontend.
511+
initial_relay_astext : list
512+
List of input model IR strings.
513+
dump_path: str
514+
Value of the "dump_offloads" compiler atribute.
515+
Could be dash ("-") or file path or empty string for
516+
printing to console, file or doing nothing respectively.
517+
"""
518+
print_to_console = dump_path == "-"
519+
save_to_file = all([dump_path != "-", dump_path != ""])
520+
521+
if print_to_console or save_to_file:
522+
523+
operations_distribution = analyze_operations_distribution(mod)
524+
525+
def annotate_f(x):
526+
ret = ""
527+
if isinstance(x, relay.Call):
528+
assert (
529+
x.span.source_name.name in operations_distribution
530+
), f"Initial relay op {x.span.source_name.name} is not present \
531+
in relay after partition"
532+
compiler_name, op_name, func_id = operations_distribution[x.span.source_name.name]
533+
ret = f", func_id: {func_id}, compiler_name: {compiler_name}, op_name: {op_name}"
534+
return ret
535+
536+
initial_relay_astext = initial_mod.astext(show_meta_data=False, annotate=annotate_f).split(
537+
"\n"
538+
)
539+
540+
output = []
541+
prev_func_id = -1
542+
for s in initial_relay_astext:
543+
result = re.search(
544+
r"(func_id: )(.*)(, compiler_name: )(.*)(, op_name: )((.*)(?=;)|(.*))", s
545+
)
546+
if result:
547+
func_id = result.group(2)
548+
target_name = result.group(4)
549+
op_name = result.group(6)
550+
if prev_func_id != func_id:
551+
# assume that operations of one composite go one after another in the relay
552+
prev_func_id = func_id
553+
# add the name of a composite
554+
if op_name != "generic":
555+
output.append(f"{target_name:10} <- {op_name}")
556+
sub = re.sub(r", compiler_name: (.*)", "", s).lstrip()
557+
if target_name == "generic":
558+
output.append(f"{target_name:10} <- {sub}")
559+
else:
560+
output.append(f"{target_name:10} <- {sub}")
561+
if print_to_console:
562+
print("\n" + "\n".join(output))
563+
if save_to_file:
564+
file_path = os.path.abspath(dump_path)
565+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
566+
with open(file_path, "w") as f:
567+
f.write("\n".join(output))
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
"""Utilities that enable analyze Relay and get mappings for unique
18+
input module layer name to the tuple of compiler and operation name"""
19+
import tvm
20+
from tvm import relay
21+
from tvm.relay.expr_functor import ExprVisitor
22+
23+
24+
class AnalyzeOperationsDistribution(ExprVisitor):
25+
"""A visitor pass that maintains the dictionary unique_op_ids where
26+
the tuple (compiler, compiler operation name, ) corresponds
27+
to unique input module layer name
28+
29+
Attributes
30+
----------
31+
unique_op_ids : Dict[str, str, int]
32+
Mapping from unique input module layer name to the tuple of compiler,
33+
operation name and function id.
34+
func_name : str
35+
Function id in partitioned module.
36+
func_id : int
37+
Function identifier in partitioned module.
38+
compiler_name : str
39+
A name of the compiler (e.g. 'ethos-u' or 'cmsis-nn').
40+
"""
41+
42+
def __init__(self):
43+
self.unique_op_ids = {}
44+
self.func_name = ""
45+
self.func_id = 0
46+
self.compiler_name = ""
47+
super().__init__()
48+
49+
def extract(self, call: relay.Call):
50+
self.compiler_name = "generic"
51+
self.func_name = "generic"
52+
if "Compiler" in call.attrs:
53+
self.compiler_name = call.attrs["Compiler"]
54+
self.visit(call)
55+
56+
def visit_call(self, call: relay.Call):
57+
if isinstance(call.op, tvm.ir.Op):
58+
if call.span:
59+
src = call.span.source_name.name
60+
self.unique_op_ids[src] = [self.compiler_name, self.func_name, self.func_id]
61+
if isinstance(call.op, relay.Function):
62+
self.func_name = call.op.attrs["Composite"]
63+
self.func_id += 1
64+
super().visit_call(call)
65+
66+
67+
def analyze_operations_distribution(mod):
68+
"""Traverses the partitioned graph to get source name field from the op's span.
69+
The result is maintained in the dictionary unique_op_ids where the tuple
70+
(compiler name, compiler composite name, function identifier in the partitioned graph)
71+
corresponds to unique input module layer name
72+
73+
Parameters
74+
----------
75+
tvm.ir.IRModule
76+
The IRModule that gets generated from a relay frontend.
77+
78+
Returns
79+
-------
80+
unique_op_ids : Dict[str, str, int]
81+
Mapping from unique input module layer name to the tuple of compiler,
82+
operation name and function id.
83+
"""
84+
analyze = AnalyzeOperationsDistribution()
85+
for _, func in mod.functions.items():
86+
analyze.extract(func)
87+
return analyze.unique_op_ids

python/tvm/relay/frontend/common.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,20 @@ def __init__(self, span):
10671067
self._span = tvm.relay.Span(tvm.relay.SourceName(span.decode("utf-8")), 0, 0, 0, 0)
10681068
else:
10691069
assert False, f"unsupported span type: {type(span)}"
1070+
self.suffix_str = "_PART_"
1071+
self.counter = 0
1072+
self.distance_from_leaf = -1
1073+
1074+
def _create_span(self):
1075+
"""Adds suffix_str + counter value to _span.source_name.name,
1076+
to create a unique source_name for the Relay layer
1077+
"""
1078+
if self.distance_from_leaf == 0:
1079+
return tvm.relay.Span(tvm.relay.SourceName(self._span), 0, 0, 0, 0)
1080+
self.distance_from_leaf -= 1
1081+
span_str = "{}{}{}".format(self._span.source_name.name, self.suffix_str, str(self.counter))
1082+
self.counter += 1
1083+
return tvm.relay.Span(tvm.relay.SourceName(span_str), 0, 0, 0, 0)
10701084

10711085
def visit(self, expr):
10721086
if hasattr(expr, "span") and expr.span:
@@ -1093,7 +1107,7 @@ def visit_call(self, call):
10931107
# ExprMutator will return directly if subject belongs to Op type
10941108
new_op = self.visit(call.op)
10951109
return _expr.CallWithFields(
1096-
call, new_op, new_args, call.attrs, call.type_args, None, self._span
1110+
call, new_op, new_args, call.attrs, call.type_args, None, self._create_span()
10971111
)
10981112

10991113
def visit_var(self, var):

src/relay/transforms/annotate_target.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class AnnotateTargetRewriter : public ExprRewriter {
258258
Array<Expr> compiler_begins = std::get<1>(target_n_args);
259259
Call new_call = Call(post_call->op, compiler_begins, post_call->attrs);
260260
new_call->checked_type_ = pre->checked_type_;
261+
new_call->span = pre->span;
261262

262263
// Update the target map.
263264
op_expr_to_target_[new_call] = target;

tests/python/contrib/test_ethosu/infra.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def get_tflite_model(model_url):
285285
return tflite_model_buf
286286

287287

288-
def get_tflite_graph(tf_func, shapes, ranges=None):
288+
def get_tflite_graph(tf_func, shapes, ranges=None, partitioned=True):
289289
tensor_specs = [tf.TensorSpec(shape, dtype=tf.float32) for shape in shapes]
290290
if not ranges:
291291
ranges = [(0, 1) for _ in shapes]
@@ -314,8 +314,9 @@ def representative_dataset():
314314
tflite_model = tflite.Model.Model.GetRootAsModel(tflite_graph, 0)
315315

316316
relay_module, params = relay.frontend.from_tflite(tflite_model)
317-
mod = partition_for_ethosu(relay_module, params)
318-
return mod, tflite_graph
317+
if partitioned:
318+
relay_module = partition_for_ethosu(relay_module, params)
319+
return relay_module, tflite_graph
319320

320321

321322
def compare_ethosu_with_reference(

0 commit comments

Comments
 (0)