2020"""
2121import logging
2222import os .path
23+ import re
24+ import itertools
25+ from copy import deepcopy
2326from typing import Any , Optional , Dict , List , Union , Callable , Sequence
2427from pathlib import Path
28+ from collections import defaultdict
2529
2630import tvm
2731from tvm import autotvm , auto_scheduler
3135from tvm .ir .memory_pools import WorkspaceMemoryPools
3236from tvm .target import Target
3337from 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
3541from . import composite_target , frontends , TVMCException
3642from .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 ))
0 commit comments