Skip to content

Commit e9e8a68

Browse files
Compiler option "--target-ethos-u-dump_npu_functions_coverage" has been replaced by more generic "--dump-offloads" with the same meaning
1 parent 3680b3c commit e9e8a68

File tree

12 files changed

+542
-11
lines changed

12 files changed

+542
-11
lines changed

python/tvm/driver/tvmc/compiler.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
Provides support to compile networks both AOT and JIT.
1919
"""
2020
import logging
21+
import os
2122
import os.path
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 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,15 @@ def compile_model(
292313

293314
config = parse_configs(pass_context_configs)
294315

316+
# remember initial relay
317+
initial_relay_astext = (
318+
mod.astext(
319+
show_meta_data=False,
320+
).split("\n")
321+
if dump_offloads != ""
322+
else ""
323+
)
324+
295325
tvm_target, extra_targets = target_from_cli(target, additional_target_options)
296326
tvm_target, target_host = Target.canon_target_and_host(tvm_target, target_host)
297327

@@ -316,6 +346,10 @@ def compile_model(
316346
for partition_function, opts in zip(partition_functions, partition_opts):
317347
mod = partition_function(mod, params, mod_name=mod_name, **opts)
318348

349+
if initial_relay_astext != "":
350+
# dump which operations are offloaded to which backend
351+
dump_operation_offloads(mod, initial_relay_astext, dump_offloads)
352+
319353
if tuning_records and os.path.exists(tuning_records):
320354
logger.debug("tuning records file provided: %s", tuning_records)
321355

@@ -459,3 +493,65 @@ def save_dumps(module_name: str, dumps: Dict[str, str], dump_root: str = "."):
459493
dump_name = module_name + "." + dump_format
460494
with open(Path(dump_root, dump_name), "w") as f:
461495
f.write(dumps[dump_format])
496+
497+
498+
def dump_operation_offloads(mod: tvm.ir.IRModule, initial_relay_astext: list, dump_path: str):
499+
"""This helper function forms a line-by-line output of the initial Relay lines,
500+
indicating which operations are ported to which backend,
501+
indicating the composite that includes those operations e.g
502+
'device1 <- device2.qnn_conv2d'
503+
'device1 <- %0 = qnn.conv2d(%tfl.quantize, %v_param_1, ...'
504+
'device1 <- %1 = nn.bias_add(%0, %v_param_2, axis=3);'
505+
'device1 <- %2 = qnn.requantize(%1, meta[relay.Constant]...'
506+
'device2 <- device2.reshape'
507+
'device2 <- %3 = reshape(%206, newshape=[1, 1001]);'
508+
509+
Parameters
510+
----------
511+
mod : tvm.ir.IRModule
512+
The IRModule that gets generated from a relay frontend.
513+
initial_relay_astext : list
514+
List of input model IR strings.
515+
dump_path: str
516+
Value of the "dump_offloads" compiler atribute.
517+
Could be dash ("-") or file path or empty string for
518+
printing to console, file or doing nothing respectively.
519+
"""
520+
print_to_console = dump_path == "-"
521+
save_to_file = all([dump_path != "-", dump_path != ""])
522+
523+
if print_to_console or save_to_file:
524+
525+
operations_distribution = analyze_operations_distribution(mod)
526+
output = []
527+
prev_op_name = ""
528+
for s in initial_relay_astext:
529+
result = re.search(r"(output_name: )(.*)(:0:0)(.*)\*/", s)
530+
if result:
531+
op = result.group(2)
532+
hardw_id = ""
533+
if op in operations_distribution:
534+
hardw_id = operations_distribution[op][0]
535+
result = re.search(r"(.*)(_PART_)", op)
536+
op_name = result.group(1) if result else op
537+
if prev_op_name != op_name:
538+
# assume that operations of one composite go one after another in the relay
539+
prev_op_name = op_name
540+
# add the name of a composite
541+
output.append(
542+
f"{hardw_id:10} <- {(operations_distribution[op][1])}"
543+
)
544+
if "span" in s:
545+
sub = re.sub(r" /\* span(.*)\*/", "", s).lstrip()
546+
if hardw_id == "":
547+
hardw_id = "generic"
548+
output.append(f"{hardw_id:10} <- {sub}")
549+
else:
550+
output.append(f"{hardw_id:10} <- {sub}")
551+
if print_to_console:
552+
print("\n" + "\n".join(output))
553+
if save_to_file:
554+
file_path = os.path.abspath(dump_path)
555+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
556+
with open(file_path, "w") as f:
557+
f.write("\n".join(output))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 maintai the dictionary unic_op_ids where
26+
the tuple (compiler, compiler operation name) corresponds
27+
to unique input module layer name
28+
29+
Attributes
30+
----------
31+
unic_op_ids : Dict[str, Tuple(str, str)]
32+
Mapping from unique input module layer name to the tuple of compiler and operation name.
33+
func_name : str
34+
Function name.
35+
compiler_name : str
36+
A name of the compiler (e.g. 'ethos-u' or 'cmsis-nn').
37+
"""
38+
39+
def __init__(self):
40+
self.unic_op_ids = {}
41+
self.func_name = ""
42+
self.compiler_name = ""
43+
super().__init__()
44+
45+
def extract(self, call: relay.Call):
46+
self.compiler_name = call.attrs["Compiler"]
47+
self.visit(call)
48+
49+
def get_src_op_from_span(self, span):
50+
span_elements = span.split(",")
51+
for s in span_elements:
52+
if "output_name" in s:
53+
src_op = s.split("output_name: ")[1]
54+
return src_op
55+
return ""
56+
57+
def visit_call(self, call: relay.Call):
58+
if isinstance(call.op, tvm.ir.Op):
59+
if call.span:
60+
src = self.get_src_op_from_span(call.span.source_name.name)
61+
self.unic_op_ids[src] = [self.compiler_name, self.func_name]
62+
if isinstance(call.op, relay.Function):
63+
self.func_name = call.op.attrs["Composite"]
64+
super().visit_call(call)
65+
66+
67+
def analyze_operations_distribution(mod):
68+
"""Traverses the graph to get "output_name" field from the op's span. The
69+
result is maintained in the dictionary unic_op_ids where
70+
the tuple (compiler, compiler operation name) corresponds to unique input module layer name
71+
72+
Parameters
73+
----------
74+
tvm.ir.IRModule
75+
The IRModule that gets generated from a relay frontend.
76+
77+
Returns
78+
-------
79+
unic_op_ids : Dict[str, Tuple(str, str)]
80+
Mapping from unique input module layer name to the tuple of compiler and operation name.
81+
"""
82+
analyze = AnalyzeOperationsDistribution()
83+
for _, func in mod.functions.items():
84+
if "Compiler" in func.attrs:
85+
analyze.extract(func)
86+
return analyze.unic_op_ids

python/tvm/relay/expr.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,13 @@ class TupleGetItem(ExprWithOp):
316316
317317
index: int
318318
The index.
319+
320+
span: Optional[tvm.relay.Span]
321+
Span that points to original source code
319322
"""
320323

321-
def __init__(self, tuple_value, index):
322-
self.__init_handle_by_constructor__(_ffi_api.TupleGetItem, tuple_value, index)
324+
def __init__(self, tuple_value, index, span=None):
325+
self.__init_handle_by_constructor__(_ffi_api.TupleGetItem, tuple_value, index, span)
323326

324327

325328
@tvm._ffi.register_object("relay.RefCreate")

python/tvm/relay/frontend/common.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .. import op as _op
3131
from .. import ty as _ty
3232
from .. import analysis
33+
from ..expr_functor import ExprMutator
3334

3435

3536
class DuplicateFilter:
@@ -997,3 +998,57 @@ def try_resolve_var_to_const(x, graph_params):
997998
return _op.const(value, dtype)
998999

9991000
return x
1001+
1002+
1003+
# Span filling to Relay for TFLite taken from
1004+
# commit https://github.com/apache/tvm/pull/9723
1005+
def set_span(sym, node_name):
1006+
"""Set up the span of relay expression(s) while converting OP"""
1007+
1008+
class SpanFiller(ExprMutator):
1009+
"""SpanFiller"""
1010+
1011+
def __init__(self, node_name, suffix_str="_PART_"):
1012+
ExprMutator.__init__(self)
1013+
self.node_name = node_name
1014+
self.suffix_str = suffix_str
1015+
self.counter = 0
1016+
self.distance_from_leaf = -1
1017+
1018+
def _create_span(self):
1019+
if self.distance_from_leaf == 0:
1020+
return tvm.relay.Span(tvm.relay.SourceName(self.node_name), 0, 0, 0, 0)
1021+
self.distance_from_leaf -= 1
1022+
span_str = "{}{}{}".format(self.node_name, self.suffix_str, str(self.counter))
1023+
self.counter += 1
1024+
return tvm.relay.Span(tvm.relay.SourceName(span_str), 0, 0, 0, 0)
1025+
1026+
def visit_call(self, call):
1027+
if call.span is None:
1028+
self.distance_from_leaf += 1
1029+
new_args = [self.visit(arg) for arg in call.args]
1030+
return _expr.Call(
1031+
call.op, new_args, call.attrs, call.type_args, self._create_span()
1032+
)
1033+
return call
1034+
1035+
def visit_tuple(self, tup):
1036+
if tup.span is None:
1037+
self.distance_from_leaf += 1
1038+
return _expr.Tuple([self.visit(field) for field in tup.fields], self._create_span())
1039+
return tup
1040+
1041+
def visit_tuple_getitem(self, op):
1042+
if op.span is None:
1043+
self.distance_from_leaf += 1
1044+
return _expr.TupleGetItem(self.visit(op.tuple_value), op.index, self._create_span())
1045+
return op
1046+
1047+
def fill(self, sym):
1048+
if isinstance(sym, _expr.TupleWrapper):
1049+
return _expr.TupleWrapper(self.visit(sym.tuple_value), sym.size)
1050+
if isinstance(sym, _expr.RelayExpr):
1051+
return self.visit(sym)
1052+
return sym
1053+
1054+
return SpanFiller(node_name).fill(sym)

python/tvm/relay/frontend/tflite.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .common import ExprTable
3535
from .common import infer_shape as _infer_shape
3636
from .common import lstm_cell, to_int_list, shape_of, try_infer_value
37+
from .common import set_span
3738
from .tflite_flexbuffer import FlexBufferDecoder
3839

3940
__all__ = ["from_tflite"]
@@ -275,14 +276,21 @@ def convert_op_to_relay(self):
275276
if ret is None:
276277
continue
277278

279+
# Span filling to Relay for TFLite taken from
280+
# commit https://github.com/apache/tvm/pull/9723
278281
if len(output_tensors) == 1:
279282
tensor_idx = output_tensors[0].tensor_idx
280-
self.exp_tab.set_expr(get_tensor_name(self.subgraph, tensor_idx), ret)
283+
curr_output = get_tensor_name(self.subgraph, tensor_idx)
284+
ret = set_span(ret, "location: {}, output_name: {}".format(op_idx, curr_output))
285+
self.exp_tab.set_expr(curr_output, ret)
281286
else:
282-
for idx, output_tensor in enumerate(output_tensors):
283-
self.exp_tab.set_expr(
284-
get_tensor_name(self.subgraph, output_tensor.tensor_idx), ret[idx]
285-
)
287+
out_names = []
288+
for output_tensor in output_tensors:
289+
out_names.append(get_tensor_name(self.subgraph, output_tensor.tensor_idx))
290+
curr_output = ", ".join(out_names)
291+
ret = set_span(ret, "location: {}, output_name: {}".format(op_idx, curr_output))
292+
for idx, out_name in enumerate(out_names):
293+
self.exp_tab.set_expr(out_name, ret[idx])
286294

287295
def get_op_code_str(self, op):
288296
"""Get TFLite ops string representation"""

src/printer/relay_text_printer.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,11 @@ Doc RelayTextPrinter::VisitExpr_(const TupleNode* op) {
375375

376376
Doc RelayTextPrinter::VisitExpr_(const TupleGetItemNode* op) {
377377
Doc doc;
378-
return doc << Print(op->tuple) << "." << op->index;
378+
doc << Print(op->tuple) << "." << op->index;
379+
if (op->span.defined()) {
380+
doc << " /* " << PrintSpan(op->span) << " */";
381+
}
382+
return doc;
379383
}
380384

381385
Doc RelayTextPrinter::VisitExpr_(const IfNode* op) {

src/relay/ir/expr.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,8 @@ TupleGetItem WithFields(TupleGetItem tuple_get_item, Optional<Expr> opt_tuple,
410410

411411
TVM_REGISTER_NODE_TYPE(TupleGetItemNode);
412412

413-
TVM_REGISTER_GLOBAL("relay.ir.TupleGetItem").set_body_typed([](Expr tuple, int index) {
414-
return TupleGetItem(tuple, index);
413+
TVM_REGISTER_GLOBAL("relay.ir.TupleGetItem").set_body_typed([](Expr tuple, int index, Span span) {
414+
return TupleGetItem(tuple, index, span);
415415
});
416416

417417
TVM_STATIC_IR_FUNCTOR(ReprPrinter, vtable)

0 commit comments

Comments
 (0)