Skip to content

Commit afe244b

Browse files
Added a separate optional pass _SuffixTagger to add suffixes to the span field of Call.
1 parent 9a99fc8 commit afe244b

File tree

10 files changed

+832
-8
lines changed

10 files changed

+832
-8
lines changed

python/tvm/driver/tvmc/compiler.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
"""
2121
import logging
2222
import os.path
23+
import re
24+
import itertools
25+
from copy import deepcopy
2326
from typing import Any, Optional, Dict, List, Union, Callable, Sequence
2427
from pathlib import Path
28+
from collections import defaultdict
2529

2630
import tvm
2731
from tvm import autotvm, auto_scheduler
@@ -31,6 +35,8 @@
3135
from tvm.ir.memory_pools import WorkspaceMemoryPools
3236
from tvm.target import Target
3337
from tvm.relay.backend import Executor, Runtime
38+
from tvm.relay.analysis.operations_distribution import analyze_operations_distribution
39+
from tvm.relay.transform.suffixes import tag_suffixes
3440

3541
from . import composite_target, frontends, TVMCException
3642
from .model import TVMCModel, TVMCPackage
@@ -69,6 +75,16 @@ def add_compile_parser(subparsers, _, json_params):
6975
default="",
7076
help="comma separated list of formats to export the input model, e.g. 'asm,ll,tir,relay'.",
7177
)
78+
parser.add_argument(
79+
"--dump-offloads",
80+
default="",
81+
help="output a mapping of which operations of the initial Relay "
82+
"will be transferred to which backend, indicating the composite "
83+
"that includes those operations, "
84+
"e.g. '--dump-offloads -' to dump to the console, "
85+
"e.g. '--dump-offloads <path_to_file>' to dump to the file. "
86+
"If not presented, no output is done. ",
87+
)
7288
parser.add_argument(
7389
"--model-format",
7490
choices=frontends.get_frontend_names(),
@@ -171,6 +187,8 @@ def drive_compile(args):
171187

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

190+
dump_offloads = args.dump_offloads if args.dump_offloads else ""
191+
174192
additional_targets = reconstruct_target_args(args)
175193
workspace_pools_target, extra_targets = target_from_cli(args.target, additional_targets)
176194
transform_args = parse_graph_transform_args(args)
@@ -187,6 +205,7 @@ def drive_compile(args):
187205
cross_options=args.cross_compiler_options,
188206
output_format=args.output_format,
189207
dump_code=dump_code,
208+
dump_offloads=dump_offloads,
190209
target_host=None,
191210
disabled_pass=args.disabled_pass,
192211
pass_context_configs=args.pass_config,
@@ -213,6 +232,7 @@ def compile_model(
213232
cross_options: Optional[str] = None,
214233
output_format: str = "so",
215234
dump_code: Optional[List[str]] = None,
235+
dump_offloads: str = "",
216236
target_host: Optional[str] = None,
217237
disabled_pass: Optional[str] = None,
218238
pass_context_configs: Optional[List[str]] = None,
@@ -259,6 +279,10 @@ def compile_model(
259279
dump_code : list[str], optional
260280
Dump the generated code for the specified source types, on
261281
the requested target. Choose from: ["asm", "ll", "tir", "relay"].
282+
dump_offloads : str
283+
Dump the information about the partition of input model's layers by external codegen.
284+
Can be '' to not dump at all, '-' to dump to the console
285+
or '<path_to_file>' to dump to the specified file.
262286
target_host : str, optional
263287
The target of the host machine if host-side code
264288
needs to be generated.
@@ -313,6 +337,13 @@ def compile_model(
313337
if "tir" in dump_code:
314338
config, dumps = add_tir_to_dumps(config, dumps)
315339

340+
initial_relay = None
341+
if dump_offloads != "":
342+
# add suffixes to the span field for calls in Relay
343+
mod = tag_suffixes(mod)
344+
# remember initial Relay
345+
initial_relay = deepcopy(mod)
346+
316347
tvm_target, extra_targets = target_from_cli(target, additional_target_options)
317348
tvm_target, target_host = Target.canon_target_and_host(tvm_target, target_host)
318349

@@ -337,6 +368,10 @@ def compile_model(
337368
for partition_function, opts in zip(partition_functions, partition_opts):
338369
mod = partition_function(mod, params, mod_name=mod_name, **opts)
339370

371+
if initial_relay:
372+
# dump which operations are offloaded to which backend
373+
dump_operation_offloads(mod, initial_relay, dump_offloads)
374+
340375
if tuning_records and os.path.exists(tuning_records):
341376
logger.debug("tuning records file provided: %s", tuning_records)
342377

@@ -496,3 +531,141 @@ def save_dumps(module_name: str, dumps: Dict[str, str], dump_root: str = "."):
496531
dump_name = module_name + "." + dump_format
497532
with open(Path(dump_root, dump_name), "w") as f:
498533
f.write(dumps[dump_format])
534+
535+
536+
def dump_operation_offloads(mod: tvm.ir.IRModule, initial_mod: tvm.ir.IRModule, dump_path: str):
537+
"""This helper function forms a line-by-line output of the initial Relay lines,
538+
indicating which operations are ported to which target,
539+
and indicating the composite that includes those operations;
540+
the 'generic' target refers to operations uploaded to the host, e.g
541+
'target1 <- target1.qnn_conv2d'
542+
'target1 <- %0 = qnn.conv2d(%tfl.quantize, %v_param_1, ...'
543+
'target1 <- %1 = nn.bias_add(%0, %v_param_2, axis=3);'
544+
'target1 <- %2 = qnn.requantize(%1, meta[relay.Constant]...'
545+
'target2 <- target2.reshape'
546+
'target2 <- %3 = reshape(%2, newshape=[1, 1001]);'
547+
'generic <- %4 = nn.pad(%3, -128f, pad_width=[[0, 0], [1, 1]...'
548+
549+
Parameters
550+
----------
551+
mod : tvm.ir.IRModule
552+
The partitioned IRModule with external global functions.
553+
initial_mod : tvm.ir.IRModule
554+
The initial IRModule that gets generated from a relay frontend.
555+
dump_path: str
556+
Value of the "dump_offloads" compiler atribute.
557+
Could be dash ("-") or file path or empty string for
558+
printing to console, file or doing nothing respectively.
559+
"""
560+
print_to_console = dump_path == "-"
561+
save_to_file = all([dump_path != "-", dump_path != ""])
562+
563+
if print_to_console or save_to_file:
564+
565+
operations_distribution = analyze_operations_distribution(mod)
566+
567+
def annotate_f(x):
568+
ret = ""
569+
if isinstance(x, relay.Call):
570+
# if there is no x.span.source_name.name in operations_distribution,
571+
# this could mean that the span was not copied during the application of passes
572+
# to the Relay, in which case we can not associate the initial Relay string
573+
# with the resulting Relay call
574+
source_name = x.span.source_name.name
575+
if source_name in operations_distribution:
576+
compiler_name, op_name, func_id = operations_distribution[source_name]
577+
ret = (
578+
f", compiler_name: {compiler_name}, op_name: {op_name}, "
579+
f"func_id: {func_id}"
580+
)
581+
else:
582+
ret = ", compiler_name: unknown, op_name: unknown, func_id: unknown"
583+
return ret
584+
585+
initial_relay_astext = initial_mod.astext(show_meta_data=False, annotate=annotate_f).split(
586+
"\n"
587+
)
588+
589+
# funcs_list is a list of internal composite/function IDs
590+
# generated by analyze_operations_distribution().
591+
# funcs_list helps keep the order of lines from the initial Relay.
592+
funcs_list = []
593+
594+
# target_statistic is a mapping of the target name to the
595+
# number of initial Relay calls offloaded on the target
596+
target_statistic = defaultdict(int)
597+
598+
# funcs_dict is a mapping of the generated analyze_operations_distribution
599+
# internal composite/function IDs to a list, where:
600+
# 1st element is
601+
# (1a): target name - it could be "generic" or "unknown" or
602+
# (1b): specific target name, like "ethos-u" or "cmsis-nn"
603+
# 2nd element is
604+
# (2a): corresponding initial Relay line for the case (1a) or
605+
# (2b): the name of the target composite functon in the other case (1b)
606+
# 3rd element or subsequent ones are presented only for the case (2b)
607+
# and are the initial Relay lines included in the corresponding
608+
# target composite functon
609+
funcs_dict = {}
610+
611+
# Here we group together initial Relay lines from the one composite
612+
counter = itertools.count()
613+
for s in initial_relay_astext:
614+
result = re.search(
615+
r"(compiler_name: )(.*)(, op_name: )(.*)(, func_id: )((.*)(?=;)|(.*))", s
616+
)
617+
if result:
618+
target_name = result.group(2)
619+
op_name = result.group(4)
620+
func_id = result.group(6)
621+
s = re.sub(r", compiler_name: (.*)", "", s).lstrip()
622+
target_statistic[target_name] += 1
623+
624+
# create an identifier for each "unknown" case to keep the lines order
625+
if func_id == "unknown":
626+
func_id = str(next(counter) * -1)
627+
628+
if func_id not in funcs_dict:
629+
funcs_list.append(func_id)
630+
funcs_dict[func_id] = [target_name]
631+
if target_name not in ["unknown", "generic"]:
632+
funcs_dict[func_id].append(op_name)
633+
634+
funcs_dict[func_id].append(s)
635+
636+
# Here we prepare the output for printing.
637+
# The output in most cases keeps the original order of the Relay lines
638+
# but some lines are moved to be in the corresponding composite group
639+
output = []
640+
total = 0
641+
output.append("Total number of operators and distribution by targets")
642+
output.append("Total:")
643+
for target, statistic in target_statistic.items():
644+
total += statistic
645+
output.append(f"{target}: {statistic}")
646+
output[1] += f" {total}"
647+
output[len(target_statistic) + 1] += "\n"
648+
649+
for func_id in funcs_list:
650+
_list = funcs_dict[func_id]
651+
output.append(f"{_list[0]:10} <- {_list[1]}")
652+
if _list[0] == "unknown":
653+
output.append(
654+
"Warning: The above line means that some pass(es) \
655+
in Relay partitioning"
656+
)
657+
output.append("do not copy the span when the call is recreated")
658+
output.append(
659+
"and a line from initial Relay could not be associated \
660+
with the resulting Relay"
661+
)
662+
for el in _list[2:]:
663+
output.append(f"{_list[0]:10} <- {el}")
664+
665+
if print_to_console:
666+
print("\n" + "\n".join(output))
667+
if save_to_file:
668+
file_path = os.path.abspath(dump_path)
669+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
670+
with open(file_path, "w") as f:
671+
f.write("\n".join(output))
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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
18+
the unique identifier of the Relay line to the tuple of
19+
compiler name, composite name and composite/function identifier."""
20+
import tvm
21+
from tvm import relay
22+
from tvm.relay.expr_functor import ExprVisitor
23+
24+
25+
class AnalyzeOperationsDistribution(ExprVisitor):
26+
"""A visitor pass that maintains the dictionary unique_op_ids where
27+
the tuple (compiler name, composite name, composite/function identifier)
28+
corresponds to the unique identifier of the Relay line.
29+
TVMC compiler adds a unique Relay line identifier as a suffix
30+
to the call span field using the tag_suffixes pass
31+
if the --dump-offloads option is specified.
32+
33+
Attributes
34+
----------
35+
unique_op_ids : Dict[str, str, int]
36+
Mapping the unique identifier of the Relay line obtained from
37+
the "span" field of the Call and the tuple of compiler name,
38+
composite name and internal composite/function identifier.
39+
func_name : str
40+
The name of the composite name in the partitioned Relay or
41+
'generic' in case the Call has not been included in any composite.
42+
func_id : int
43+
Internal(inside unique_op_ids) composite/function identifier.
44+
compiler_name : str
45+
A name of the compiler (e.g. 'ethos-u' or 'cmsis-nn') or 'generic'
46+
in case the Call has not been included in any composite.
47+
"""
48+
49+
def __init__(self):
50+
self.unique_op_ids = {}
51+
self.func_name = ""
52+
self.func_id = 1
53+
self.compiler_name = ""
54+
super().__init__()
55+
56+
def extract(self, call: relay.Call):
57+
self.compiler_name = "generic"
58+
self.func_name = "generic"
59+
if "Compiler" in call.attrs:
60+
self.compiler_name = call.attrs["Compiler"]
61+
self.visit(call)
62+
63+
def visit_call(self, call: relay.Call):
64+
if isinstance(call.op, tvm.ir.Op):
65+
if call.span:
66+
src = call.span.source_name.name
67+
self.unique_op_ids[src] = [self.compiler_name, self.func_name, self.func_id]
68+
if self.func_name == "generic":
69+
self.func_id += 1
70+
if isinstance(call.op, relay.Function):
71+
self.func_name = call.op.attrs["Composite"]
72+
self.func_id += 1
73+
super().visit_call(call)
74+
75+
76+
def analyze_operations_distribution(mod):
77+
"""Traverses the partitioned graph to get the unique identifier
78+
of the Relay line from the Call's span field.
79+
The result is maintained in the dictionary unique_op_ids where
80+
the unique indicator obtained from the op's span corresponds to
81+
the tuple (compiler name, composite name, composite/function identifier).
82+
With this information we can annotate the textual representation
83+
of the initial Relay by indicating into which target composite
84+
and function the operators are converted
85+
86+
Parameters
87+
----------
88+
mod : tvm.ir.IRModule
89+
The partitioned Relay graph usually obtained with
90+
partition_for_<target> function
91+
92+
Returns
93+
-------
94+
unique_op_ids : Dict[str, str, int]
95+
Mapping from the unique identifier of the Relay line to the tuple of
96+
compiler name, composite name, internal composite/function
97+
identifier.
98+
"""
99+
analyze = AnalyzeOperationsDistribution()
100+
for _, func in mod.functions.items():
101+
analyze.extract(func)
102+
return analyze.unique_op_ids

0 commit comments

Comments
 (0)