From cb8cc3bd18002a72d2bf69faf51af9ea01c7232a Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 01:47:38 -0700 Subject: [PATCH 01/76] autotvm update & fix --- python/tvm/autotvm/__init__.py | 4 +- python/tvm/autotvm/measure/measure.py | 2 +- python/tvm/autotvm/measure/measure_methods.py | 12 ++- python/tvm/autotvm/record.py | 74 ++++++++++++++++--- python/tvm/autotvm/task/__init__.py | 3 + python/tvm/autotvm/tuner/__init__.py | 2 + python/tvm/autotvm/tuner/callback.py | 48 ++++++++++++ python/tvm/autotvm/tuner/ga_tuner.py | 3 + python/tvm/autotvm/tuner/gridsearch_tuner.py | 6 ++ python/tvm/autotvm/tuner/model_based_tuner.py | 2 +- python/tvm/autotvm/tuner/tuner.py | 39 +++++----- .../tvm/autotvm/tuner/xgboost_cost_model.py | 34 ++++----- python/tvm/autotvm/tuner/xgboost_tuner.py | 11 ++- python/tvm/contrib/download.py | 9 ++- python/tvm/target.py | 53 ++++++++----- 15 files changed, 224 insertions(+), 78 deletions(-) diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index 45d053ef96f7..13b730e8ebae 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -21,6 +21,6 @@ # some shortcuts from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo -from .tuner import callback +from .tuner import callback, tune_tasks from .task import template, get_config, create, ConfigSpace, ConfigEntity -from .record import ApplyHistoryBest as apply_history_best +from .record import ApplyHistoryBest as apply_history_best, load_op_param diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 9f1bf611485a..7442136925e4 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -127,7 +127,7 @@ def measure_option(mode, If is not set, will use environment variable "TVM_TRACKER_HOST" and "TVM_TRACKER_PORT" use_ndk: bool, option - Whether export requires ndk + Whether use Android NDK. Set this to true for Android target. custom_measure_batch: callable, optional custom measure function diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index 9ed7f3a69c84..e04b944e1910 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -210,14 +210,12 @@ def _fbuild(inp): func, args = _build_func(inp, build_option, kwargs) tmp_dir = util.tempdir() - if not kwargs.get('use_ndk', False): - file_name = "tmp_func_%0x.tar" % getrandbits(64) - path = tmp_dir.relpath(file_name) - func.export_library(path) - else: - file_name = "tmp_func_%0x.so" % getrandbits(64) - path = tmp_dir.relpath(file_name) + file_name = "tmp_func_%0x.so" % getrandbits(64) + path = tmp_dir.relpath(file_name) + if kwargs.get('use_ndk', False): # for Android NDK func.export_library(path, ndk.create_shared) + else: + func.export_library(path) remote = request_remote(rpc_device_key, rpc_tracker_addr, rpc_priority, rpc_timeout) remote.upload(path) func = remote.load_module(file_name) diff --git a/python/tvm/autotvm/record.py b/python/tvm/autotvm/record.py index d76a18e13425..20d56ede8dca 100644 --- a/python/tvm/autotvm/record.py +++ b/python/tvm/autotvm/record.py @@ -20,6 +20,9 @@ from .task import DispatchContext, ConfigEntity from .measure import MeasureInput, MeasureResult +from ..contrib.util import tempdir +from ..contrib.download import download + AUTOTVM_LOG_VERSION = 0.1 try: # convert unicode to str for python2 @@ -191,7 +194,7 @@ def __init__(self, records, default=None): self.load(records) - def load(self, records): + def load(self, records, verbose=0): """Load records to this dispatch context Parameters @@ -201,6 +204,9 @@ def load(self, records): If is str, then it should be the filename of a records log file. Each row of this file is an encoded record pair. Otherwise, it is an iterator. + verbose: int, optional + If is 0, output nothing + If is 1, output some debug information """ if isinstance(records, str): records = load_from_file(records) @@ -239,7 +245,8 @@ def load(self, records): best_by_model[key] = (inp, res) break - logging.info("Finish loading %d records", counter) + if verbose: + logging.info("Finish loading %d records", counter) def query(self, target, workload): if target is None: @@ -326,7 +333,7 @@ def pick_best(in_file, out_file): ---------- in_file: str The filename of input - out_file: + out_file: str or file The filename of output """ best_context = ApplyHistoryBest(load_from_file(in_file)) @@ -338,32 +345,81 @@ def pick_best(in_file, out_file): for v in best_context.best_by_targetkey.values(): best_set.add(measure_str_key(v[0])) - logging.info("Extract %d best records from the log file", len(best_set)) + logging.info("Extract %d best records from the %s", len(best_set), in_file) + fout = open(out_file, 'w') if isinstance(out_file, str) else out_file - fout = open(out_file, 'w') for inp, res in load_from_file(in_file): if measure_str_key(inp) in best_set: fout.write(encode(inp, res) + "\n") + best_set.remove(measure_str_key(inp)) -def load_op_param(rootpath=os.path.join(os.path.expanduser('~'), ".tvm", "op_params")): +def load_op_param(rootpath=_target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH, verbose=0): """Load pre-tuned parameters of operators. This function will load all "*.log" file under root path and select best configs. Parameters ---------- - rootpath: str + rootpath: str, optional The root path of stored parameters + verbose: int, optional + If is 0, output nothing + If is 1, output some debug information """ best_context = ApplyHistoryBest([]) for dirpath, _, filenames in os.walk(rootpath): for filename in filenames: - if os.path.splitext(filename)[1] == '.log': - best_context.load(os.path.join(dirpath, filename)) + if filename.endswith('.log'): + best_context.load(os.path.join(dirpath, filename), verbose) assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" DispatchContext.current = best_context + +def download_pretuned_op_param(backend): + """Download pre-tuned parameters of operators for a backend + + Parameters + ---------- + backend: str + The compilation target + """ + root_path = _target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH + if not os.path.isdir(root_path): + # make directory + splits = os.path.split(root_path) + for j in range(1, len(splits)+1): + path = os.path.join(*splits[:j]) + if not os.path.isdir(path): + os.mkdir(path) + + print("Download pre-tuned parameters for %s" % backend) + download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/op_param/%s.log" % backend, + os.path.join(root_path, backend + ".log"), True, verbose=0) + +def list_pretuned_op_param(): + """List all available pre-tuned op parameters for targets + + Returns + ------- + ret: List + All available packets + """ + path = tempdir() + filename = path.relpath("info.json") + print("Download meta info for pre-tuned parameters") + download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/op_param/info.json", + filename, True, verbose=0) + print("") + + with open(filename, "r") as fin: + text = "".join(fin.readlines()) + info = json.loads(text) + keys = list(info.keys()) + keys.sort() + + return [(k, info[k]) for k in keys] + """ Usage: This record executable module has three modes. diff --git a/python/tvm/autotvm/task/__init__.py b/python/tvm/autotvm/task/__init__.py index 6d719e00d2a4..3bdfb7ea7399 100644 --- a/python/tvm/autotvm/task/__init__.py +++ b/python/tvm/autotvm/task/__init__.py @@ -10,3 +10,6 @@ from .space import ConfigSpace, ConfigEntity from .code_hash import attach_code_hash, attach_code_hash_to_arg from .dispatcher import DispatchContext, ApplyConfig, dispatcher + +from .nnvm_integration import register_topi_compute, register_topi_schedule,\ + extract_from_graph diff --git a/python/tvm/autotvm/tuner/__init__.py b/python/tvm/autotvm/tuner/__init__.py index af81442e79f4..c3647ac7dfba 100644 --- a/python/tvm/autotvm/tuner/__init__.py +++ b/python/tvm/autotvm/tuner/__init__.py @@ -12,3 +12,5 @@ from .gridsearch_tuner import GridSearchTuner, RandomTuner from .ga_tuner import GATuner from .xgboost_tuner import XGBTuner + +from .graph_tuning import tune_tasks diff --git a/python/tvm/autotvm/tuner/callback.py b/python/tvm/autotvm/tuner/callback.py index f8265ca70fd7..c4eb00ef9361 100644 --- a/python/tvm/autotvm/tuner/callback.py +++ b/python/tvm/autotvm/tuner/callback.py @@ -1,5 +1,7 @@ # pylint: disable=consider-using-enumerate,invalid-name """Namespace of callback utilities of AutoTVM""" +import sys +import time import numpy as np @@ -83,6 +85,7 @@ def _callback(_, inputs, results): red.set(inp, result) return _callback + class Monitor(object): """A monitor to collect statistic during tuning""" def __init__(self): @@ -110,3 +113,48 @@ def trial_scores(self): def trial_timestamps(self): """get wall clock time stamp of all trials""" return np.array(self.timestamps) + + +def progress_bar(total, prefix=''): + """Display progress bar for tuning + + Parameters + ---------- + total: int + The total number of trials + prefix: str + The prefix of output message + """ + + class Context: + """Context to store local variables""" + def __init__(self): + self.best_flops = 0 + self.cur_flops = 0 + self.ct = 0 + self.total = total + + def __del__(self): + sys.stdout.write(' Done.\n') + + ctx = Context() + tic = time.time() + + def _callback(tuner, inputs, results): + ctx.ct += len(inputs) + + flops = 0 + for inp, res in zip(inputs, results): + if res.error_no == 0: + flops = inp.task.flop / np.mean(res.costs) + + ctx.cur_flops = flops + ctx.best_flops = tuner.best_flops + + sys.stdout.write('\r%s Current/Best: %7.2f/%7.2f GFLOPS | Progress: (%d/%d) ' + '| %.2f s' % + (prefix, ctx.cur_flops/1e9, ctx.best_flops/1e9, ctx.ct, ctx.total, + time.time() - tic)) + sys.stdout.flush() # As suggested by Rom Ruben + + return _callback diff --git a/python/tvm/autotvm/tuner/ga_tuner.py b/python/tvm/autotvm/tuner/ga_tuner.py index aed39a4e2d10..916bd4ee68c6 100644 --- a/python/tvm/autotvm/tuner/ga_tuner.py +++ b/python/tvm/autotvm/tuner/ga_tuner.py @@ -117,3 +117,6 @@ def update(self, inputs, results): def has_next(self): return len(self.visited) - (len(self.genes) - self.trial_pt) < len(self.space) + + def load_history(self, data_set): + pass diff --git a/python/tvm/autotvm/tuner/gridsearch_tuner.py b/python/tvm/autotvm/tuner/gridsearch_tuner.py index cb1a0832b506..21a17a132640 100644 --- a/python/tvm/autotvm/tuner/gridsearch_tuner.py +++ b/python/tvm/autotvm/tuner/gridsearch_tuner.py @@ -25,6 +25,9 @@ def next_batch(self, batch_size): def has_next(self): return self.counter < len(self.task.config_space) + def load_history(self, data_set): + pass + def __getstate__(self): return {"counter": self.counter} @@ -56,6 +59,9 @@ def next_batch(self, batch_size): def has_next(self): return len(self.visited) < len(self.task.config_space) + def load_history(self, data_set): + pass + def __getstate__(self): return {"visited": self.counter} diff --git a/python/tvm/autotvm/tuner/model_based_tuner.py b/python/tvm/autotvm/tuner/model_based_tuner.py index 91ade1abdc47..d1c1b16d3181 100644 --- a/python/tvm/autotvm/tuner/model_based_tuner.py +++ b/python/tvm/autotvm/tuner/model_based_tuner.py @@ -242,7 +242,7 @@ def update(self, inputs, results): self.ys.append(flops) else: self.xs.append(index) - self.ys.append(0) + self.ys.append(0.0) # if we have enough new training samples if len(self.xs) >= self.plan_size * (self.train_ct + 1) \ diff --git a/python/tvm/autotvm/tuner/tuner.py b/python/tvm/autotvm/tuner/tuner.py index 95afe2eaa3f5..3e4d5cb83cc6 100644 --- a/python/tvm/autotvm/tuner/tuner.py +++ b/python/tvm/autotvm/tuner/tuner.py @@ -64,7 +64,7 @@ def update(self, inputs, results): """ pass - def tune(self, n_trial, measure_option, early_stop=None, verbose=1, callbacks=()): + def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callbacks=()): """Begin tuning Parameters @@ -74,7 +74,7 @@ def tune(self, n_trial, measure_option, early_stop=None, verbose=1, callbacks=() measure_option: dict The options for how to measure generated code. You should use the return value ot autotvm.measure_option for this argument. - early_stop: int + early_stopping: int Early stop the tuning when not finding better configs in this number of trials verbose: int 0: silent mode, no output @@ -87,7 +87,7 @@ def tune(self, n_trial, measure_option, early_stop=None, verbose=1, callbacks=() """ measure_batch = create_measure_batch(self.task, measure_option) parallel_num = getattr(measure_batch, 'parallel_num', 1) - early_stop = early_stop or 1e9 + early_stopping = early_stopping or 1e9 i = 0 while i < n_trial: @@ -99,20 +99,20 @@ def tune(self, n_trial, measure_option, early_stop=None, verbose=1, callbacks=() inputs = [MeasureInput(self.task.target, self.task, config) for config in configs] results = measure_batch(inputs) - # print info - if verbose >= 1: - for k, (inp, res) in enumerate(zip(inputs, results)): - config = inp.config - if res.error_no == 0: - flops = inp.task.flop / np.mean(res.costs) - else: - flops = 0 - if flops > self.best_flops: - self.best_flops = flops - self.best_config = config - self.best_measure_pair = (inp, res) - self.best_iter = i + k - + # keep best config + for k, (inp, res) in enumerate(zip(inputs, results)): + config = inp.config + if res.error_no == 0: + flops = inp.task.flop / np.mean(res.costs) + else: + flops = 0 + if flops > self.best_flops: + self.best_flops = flops + self.best_config = config + self.best_measure_pair = (inp, res) + self.best_iter = i + k + + if verbose >= 1: logging.info("No: %d\tGFLOPS: %.2f/%.2f\tresult: %s\t%s", i + k + 1, flops / 1e9, self.best_flops / 1e9, res, config) @@ -124,8 +124,9 @@ def tune(self, n_trial, measure_option, early_stop=None, verbose=1, callbacks=() for callback in callbacks: callback(self, inputs, results) - if i > self.best_iter + early_stop: - logging.info("Early stopped. Best iter: %d.", self.best_iter) + if i > self.best_iter + early_stopping: + if verbose >= 1: + logging.info("Early stopped. Best iter: %d.", self.best_iter) break del measure_batch diff --git a/python/tvm/autotvm/tuner/xgboost_cost_model.py b/python/tvm/autotvm/tuner/xgboost_cost_model.py index 335956f071dd..0a44e72223cc 100644 --- a/python/tvm/autotvm/tuner/xgboost_cost_model.py +++ b/python/tvm/autotvm/tuner/xgboost_cost_model.py @@ -139,6 +139,8 @@ def fit(self, xs, ys, plan_size): x_train = self._get_feature(xs) y_train = np.array(ys) + if np.max(y_train) < 1e-6: + return y_train = y_train / np.max(y_train) valid_index = y_train > 1e-6 @@ -162,17 +164,19 @@ def fit(self, xs, ys, plan_size): ], verbose_eval=self.verbose)]) - logging.info("train: %.2f\tobs: %d\terror: %d\tn_cache: %d", - time.time() - tic, len(xs), - len(xs) - np.sum(valid_index), - self.feature_cache.size(self.fea_type)) + if self.verbose: + logging.info("train: %.2f\tobs: %d\terror: %d\tn_cache: %d", + time.time() - tic, len(xs), + len(xs) - np.sum(valid_index), + self.feature_cache.size(self.fea_type)) def fit_log(self, records, plan_size): tic = time.time() self._reset_pool() args = list(records) - logging.info("Load %d entries from history log file", len(args)) + if self.verbose: + logging.info("Load %d entries from history log file", len(args)) if self.fea_type == 'itervar': feature_extract_func = _extract_itervar_feature_log elif self.fea_type == 'knob': @@ -187,6 +191,8 @@ def fit_log(self, records, plan_size): x_train = xs y_train = ys + if np.max(y_train) < 1e-6: + return y_train /= np.max(y_train) index = np.random.permutation(len(x_train)) @@ -205,7 +211,8 @@ def fit_log(self, records, plan_size): ], verbose_eval=self.verbose)]) - logging.info("train: %.2f\tobs: %d", time.time() - tic, len(xs)) + if self.verbose: + logging.info("train: %.2f\tobs: %d", time.time() - tic, len(xs)) def predict(self, xs, output_margin=False): feas = self._get_feature(xs) @@ -282,7 +289,7 @@ def _extract_itervar_feature_log(arg): if res.error_no == 0: y = inp.task.flop / np.mean(res.costs) else: - y = 0 + y = 0.0 return x, y def _extract_knob_feature_index(index): @@ -301,7 +308,7 @@ def _extract_knob_feature_log(arg): inp.task.instantiate(config) y = inp.task.flop / np.mean(res.costs) else: - y = 0 + y = 0.0 return x, y def _extract_curve_feature_index(index): @@ -325,12 +332,11 @@ def _extract_curve_feature_log(arg): if res.error_no == 0: y = inp.task.flop / np.mean(res.costs) else: - y = 0 + y = 0.0 return x, y def custom_callback(stopping_rounds, metric, fevals, evals=(), log_file=None, - save_file="xgb_checkpoint", save_every=None, maximize=False, verbose_eval=True): """callback function for xgboost to support multiple custom evaluation functions""" from xgboost.core import EarlyStopException @@ -400,18 +406,12 @@ def callback(env): continue infos.append("%s: %.6f" % (item[0], item[1])) - if not isinstance(verbose_eval, bool) and i % verbose_eval == 0: + if not isinstance(verbose_eval, bool) and verbose_eval and i % verbose_eval == 0: logging.info("\t".join(infos)) if log_file: with open(log_file, "a") as fout: fout.write("\t".join(infos) + '\n') - ##### save model ##### - if save_every and i % save_every == 0: - filename = save_file + ".%05d.bst" % i - logging.info("save model to %s ...", filename) - bst.save_model(filename) - ##### choose score and do early stopping ##### score = None for item in eval_res: diff --git a/python/tvm/autotvm/tuner/xgboost_tuner.py b/python/tvm/autotvm/tuner/xgboost_tuner.py index eb3978b5c135..a8e12d6164d3 100644 --- a/python/tvm/autotvm/tuner/xgboost_tuner.py +++ b/python/tvm/autotvm/tuner/xgboost_tuner.py @@ -40,16 +40,21 @@ class XGBTuner(ModelBasedTuner): If is not None, the tuner will first select top-(plan_size * diversity_filter_ratio) candidates according to the cost model and then pick batch_size of them according to the diversity metric. + verbose: int + The Verbose Level. + If is 0, output nothing. + Otherwise, output debug information every `verbose` iterations. """ def __init__(self, task, plan_size=32, feature_type='itervar', loss_type='rank', num_threads=None, - optimizer='sa', diversity_filter_ratio=None): + optimizer='sa', diversity_filter_ratio=None, verbose=50): cost_model = XGBoostCostModel(task, feature_type=feature_type, loss_type=loss_type, - num_threads=num_threads) + num_threads=num_threads, + verbose=verbose // 2) if optimizer == 'sa': - optimizer = SimulatedAnnealingOptimizer(task) + optimizer = SimulatedAnnealingOptimizer(task, verbose=verbose) else: assert isinstance(optimizer, ModelOptimizer), "Optimizer must be " \ "a supported name string" \ diff --git a/python/tvm/contrib/download.py b/python/tvm/contrib/download.py index 6e86ca1daabf..e9bc8144a45b 100644 --- a/python/tvm/contrib/download.py +++ b/python/tvm/contrib/download.py @@ -6,7 +6,7 @@ import sys import time -def download(url, path, overwrite=False, size_compare=False): +def download(url, path, overwrite=False, size_compare=False, verbose=1): """Downloads the file from the internet. Set the input options correctly to overwrite or do the size comparison @@ -23,6 +23,9 @@ def download(url, path, overwrite=False, size_compare=False): size_compare : bool, optional Whether to do size compare to check downloaded file. + + verbose: int, optional + Verbose level """ import requests @@ -45,7 +48,9 @@ def download(url, path, overwrite=False, size_compare=False): return print('File {} exists, skip.'.format(path)) return - print('Downloading from url {} to {}'.format(url, path)) + + if verbose >= 1: + print('Downloading from url {} to {}'.format(url, path)) # Stateful start time start_time = time.time() diff --git a/python/tvm/target.py b/python/tvm/target.py index f983065306bf..cc85e56e9e25 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -40,6 +40,8 @@ """ from __future__ import absolute_import +import os + from ._ffi.base import _LIB_NAME from ._ffi.node import NodeBase, register_node from . import _api_internal @@ -51,6 +53,7 @@ if _LIB_NAME != "libtvm_runtime.so": raise err_msg +AUTOTVM_PRETUNED_PARAM_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "op_param") def _merge_opts(opts, new_opts): """Helper function to merge options""" @@ -72,7 +75,7 @@ class Target(NodeBase): Do not use class constructor, you can create target using the following functions - :any:`tvm.target.create` create target from string - - :any:`tvm.target.rasp` create raspberry pi target + - :any:`tvm.target.arm_cpu` create arm_cpu target - :any:`tvm.target.cuda` create CUDA target - :any:`tvm.target.rocm` create ROCM target - :any:`tvm.target.mali` create Mali target @@ -374,22 +377,6 @@ def rocm(options=None): return _api_internal._TargetCreate("rocm", *options) -def rasp(options=None): - """Returns a rasp target. - - Parameters - ---------- - options : str or list of str - Additional options - """ - opts = ["-device=rasp", - "-mtriple=armv7l-none-linux-gnueabihf", - "-mcpu=cortex-a53", - "-mattr=+neon"] - opts = _merge_opts(opts, options) - return _api_internal._TargetCreate("llvm", *opts) - - def mali(options=None): """Returns a ARM Mali GPU target. @@ -428,6 +415,38 @@ def opengl(options=None): return _api_internal._TargetCreate("opengl", *options) +def arm_cpu(model='unknown', options=None): + """Returns a ARM CPU target. + This function will also donwload pre-tuned op parameters + + Parameters + ---------- + model: str + SoC name or phone name of the arm board. + options : str or list of str + Additional options + """ + opt_table = { + "pixel2": ["-model=snapdragon835", "-target=arm64-linux-android"], + "mate10": ["-model=kirin970", "-target=arm64-linux-android"], + "mate10pro": ["-model=kirin970", "-target=arm64-linux-android"], + "p20": ["-model=kirin970", "-target=arm64-linux-android"], + "p20pro": ["-model=kirin970", "-target=arm64-linux-android"], + "rasp3b": ["-model=bcm2837", "-target=armv7l-linux-gnueabihf"], + "rk3399": ["-model=rk3399", "-target=aarch64-linux-gnu"], + "pynq": ["-model=pynq", "-target=armv7a-linux-eabi"], + } + pre_defined_opt = opt_table.get(model, []) + + if not os.path.isfile(os.path.join(AUTOTVM_PRETUNED_PARAM_ROOT_PATH, "arm_cpu.log")): + from .autotvm.record import download_pretuned_op_param + download_pretuned_op_param("arm_cpu") + + opts = ["-device=arm_cpu"] + pre_defined_opt + opts = _merge_opts(opts, options) + return _api_internal._TargetCreate("llvm", *opts) + + def create(target_str): """Get a target given target string. From 2aaf56f6cb66116875130e51c9175313af73c6de Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 01:48:37 -0700 Subject: [PATCH 02/76] arm cpu topi integration --- nnvm/include/nnvm/top/nn.h | 71 +++ nnvm/python/nnvm/compiler/build_module.py | 5 + nnvm/python/nnvm/top/nn.py | 49 +- nnvm/src/top/nn/convolution.cc | 183 ++++++- python/tvm/autotvm/task/nnvm_integration.py | 291 +++++++++++ python/tvm/autotvm/tuner/graph_tuning.py | 106 ++++ python/tvm/exec/download_op_param.py | 36 ++ python/tvm/exec/pick_best.py | 37 ++ topi/python/topi/__init__.py | 2 +- topi/python/topi/arm_cpu/__init__.py | 4 + topi/python/topi/arm_cpu/conv2d.py | 508 +++++++++++++++++++ topi/python/topi/arm_cpu/depthwise_conv2d.py | 91 ++++ topi/python/topi/generic/nn.py | 46 ++ topi/python/topi/nn/conv2d.py | 270 +++------- topi/python/topi/rasp/conv2d.py | 358 ------------- topi/python/topi/rasp/depthwise_conv2d.py | 207 -------- topi/python/topi/util.py | 53 ++ 17 files changed, 1545 insertions(+), 772 deletions(-) create mode 100644 python/tvm/autotvm/task/nnvm_integration.py create mode 100644 python/tvm/autotvm/tuner/graph_tuning.py create mode 100644 python/tvm/exec/download_op_param.py create mode 100644 python/tvm/exec/pick_best.py create mode 100644 topi/python/topi/arm_cpu/__init__.py create mode 100644 topi/python/topi/arm_cpu/conv2d.py create mode 100644 topi/python/topi/arm_cpu/depthwise_conv2d.py delete mode 100644 topi/python/topi/rasp/conv2d.py delete mode 100644 topi/python/topi/rasp/depthwise_conv2d.py diff --git a/nnvm/include/nnvm/top/nn.h b/nnvm/include/nnvm/top/nn.h index 86bdc60a6236..c9baa116e8aa 100644 --- a/nnvm/include/nnvm/top/nn.h +++ b/nnvm/include/nnvm/top/nn.h @@ -172,6 +172,77 @@ struct Conv2DParam : public dmlc::Parameter { static const constexpr int kBias = 2; }; +struct WinogradWeightTransformParam : public dmlc::Parameter { + int tile_size; + + DMLC_DECLARE_PARAMETER(WinogradWeightTransformParam) { + DMLC_DECLARE_FIELD(tile_size) + .describe("Tile size of winograd. E.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3)"); + } + + static const constexpr int kWeight = 0; +}; + +struct WinogradConv2DParam : public dmlc::Parameter { + int channels; + TShape kernel_size; + TShape strides; + TShape padding; + TShape dilation; + int groups; + std::string layout; + std::string kernel_layout; + std::string out_layout; + int out_dtype; + bool use_bias; + int tile_size; + + DMLC_DECLARE_PARAMETER(WinogradConv2DParam) { + DMLC_DECLARE_FIELD(channels) + .describe("The dimensionality of the output space" + "i.e. the number of output channels in the convolution."); + DMLC_DECLARE_FIELD(kernel_size) + .describe("Specifies the dimensions of the convolution window."); + DMLC_DECLARE_FIELD(strides).set_default(TShape({1, 1})) + .describe("Specifies the strides of the convolution."); + DMLC_DECLARE_FIELD(padding).set_default(TShape({0, 0})) + .describe("If padding is non-zero, then the input is implicitly zero-padded" + "on both sides for padding number of points"); + DMLC_DECLARE_FIELD(dilation).set_default(TShape({1, 1})) + .describe("Specifies the dilation rate to use for dilated convolution."); + DMLC_DECLARE_FIELD(groups).set_default(1) + .describe("Controls the connections between inputs and outputs." + "At groups=1, all inputs are convolved to all outputs." + "At groups=2, the operation becomes equivalent to having two convolution" + "layers side by side, each seeing half the input channels, and producing" + "half the output channels, and both subsequently concatenated."); + DMLC_DECLARE_FIELD(layout).set_default("NCHW") + .describe("Dimension ordering of input data. Can be 'NCHW', 'NHWC', etc." + "'N', 'C', 'H', 'W' stands for batch, channel, height, and width" + "dimensions respectively. Convolution is applied on the 'H' and" + "'W' dimensions."); + DMLC_DECLARE_FIELD(out_layout).set_default("__undef__") + .describe("Dimension ordering of output. Can be 'NCHW', 'NHWC', etc." + "'N', 'C', 'H', 'W' stands for batch, channel, height, and width" + "dimensions respectively. Default to be same as input layout."); + DMLC_DECLARE_FIELD(kernel_layout).set_default("OIHW") + .describe("Dimension ordering of weight. Can be 'OIHW', 'OIHW16o16i', etc." + "'O', 'I', 'H', 'W' stands for num_filter, input_channel, height, and width" + "dimensions respectively."); + DMLC_DECLARE_DTYPE_FIELD(out_dtype) + .add_enum("same", -1) + .set_default(-1) + .describe("Output data type, set to explicit type under mixed precision setting"); + DMLC_DECLARE_FIELD(use_bias).set_default(true) + .describe("Whether the layer uses a bias vector."); + DMLC_DECLARE_FIELD(tile_size) + .describe("Tile size of winograd. E.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3)"); + } + // constants + static const constexpr int kData = 0; + static const constexpr int kWeight = 1; + static const constexpr int kBias = 2; +}; struct Conv2DTransposeParam : public dmlc::Parameter { int channels; diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index ed75b10414c7..1de0cef8156a 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -6,6 +6,7 @@ import tvm from tvm.contrib import graph_runtime +from tvm import autotvm from . import graph_attr, graph_util from .. import graph as _graph from .. import symbol as sym @@ -238,6 +239,10 @@ def build(graph, target=None, shape=None, dtype="float32", raise ValueError("Target is not set in env or passed as argument.") target = tvm.target.create(target) + if autotvm.task.DispatchContext.current is None: + # load pre-tuned parameters from default directory + autotvm.load_op_param() + shape = shape if shape else {} if not isinstance(shape, dict): raise TypeError("require shape to be dict") diff --git a/nnvm/python/nnvm/top/nn.py b/nnvm/python/nnvm/top/nn.py index e86d545736bd..f59424203402 100644 --- a/nnvm/python/nnvm/top/nn.py +++ b/nnvm/python/nnvm/top/nn.py @@ -89,7 +89,7 @@ def compute_conv2d(attrs, inputs, _): layout = attrs["layout"] kernel_layout = attrs["kernel_layout"] out_dtype = attrs["out_dtype"] - out_dtype = None if out_dtype == "same" else out_dtype + out_dtype = inputs[0].dtype if out_dtype == "same" else out_dtype assert layout == "NCHW" or layout == "NHWC" (dilation_h, dilation_w) = dilation if dilation_h < 1 or dilation_w < 1: @@ -196,6 +196,53 @@ def schedule_contrib_conv2d_NCHWc(attrs, outs, target): reg.register_pattern("_contrib_conv2d_NCHWc", OpPattern.OUT_ELEMWISE_FUSABLE) + +@reg.register_compute("_contrib_conv2d_winograd_weight_transform") +def compute_contrib_conv2d_winograd_weight_transform(attrs, inputs, _): + return topi.nn.conv2d_winograd_weight_transform(inputs[0], attrs.get_int('tile_size')) + +@reg.register_schedule("_contrib_conv2d_winograd_weight_transform") +def schedule_contrib_conv2d_winograd_weight_transform(attrs, outs, target): + with tvm.target.create(target): + return topi.generic.schedule_conv2d_winograd_weight_transform(outs) + +reg.register_pattern("_contrib_conv2d_winograd_weight_transform", OpPattern.OUT_ELEMWISE_FUSABLE) + + +@reg.register_compute("_contrib_conv2d_winograd_without_weight_transform") +def compute_contrib_conv2d_winograd_without_weight_transform(attrs, inputs, _): + """Compute definition of conv2d NCHWc""" + padding = attrs.get_int_tuple("padding") + strides = attrs.get_int_tuple("strides") + dilation = attrs.get_int_tuple("dilation") + groups = attrs.get_int("groups") + layout = attrs.get_string("layout") + out_dtype = attrs.get_string("out_dtype") + tile_size = attrs.get_int("tile_size") + out_dtype = inputs[0].dtype if out_dtype == "same" else out_dtype + assert dilation == (1, 1), "Do not support dilate now" + assert groups == 1, "Do not supoort arbitrary group number" + + # pylint: disable=assignment-from-no-return + out = topi.nn.conv2d_winograd_without_weight_transform( + inputs[0], inputs[1], strides, padding, layout, out_dtype, + tile_size) + + if attrs.get_bool("use_bias"): + bias = inputs[2] + bias = topi.expand_dims(bias, axis=1, num_newaxis=2) + out = topi.add(out, bias) + return out + +@reg.register_schedule("_contrib_conv2d_winograd_without_weight_transform") +def schedule_contrib_conv2d_winograd_without_weight_transform(attrs, outs, target): + with tvm.target.create(target): + return topi.generic.schedule_conv2d_winograd_without_weight_transform(outs) + +reg.register_pattern("_contrib_conv2d_winograd_without_weight_transform", + OpPattern.OUT_ELEMWISE_FUSABLE) + + # conv2d_transpose @reg.register_compute("conv2d_transpose") def compute_conv2d_transpose(attrs, inputs, _): diff --git a/nnvm/src/top/nn/convolution.cc b/nnvm/src/top/nn/convolution.cc index 6a0dad17a4c4..bcb317d3ccfc 100644 --- a/nnvm/src/top/nn/convolution.cc +++ b/nnvm/src/top/nn/convolution.cc @@ -130,11 +130,112 @@ inline bool Conv2DInferShape(const nnvm::NodeAttrs& attrs, return true; } +inline bool WinogradConv2DInferShape(const nnvm::NodeAttrs& attrs, + std::vector* in_shape, + std::vector* out_shape) { + static const Layout kNCHW("NCHW"); + static const Layout kOIHW("OIHW"); + + const WinogradConv2DParam& param = nnvm::get(attrs.parsed); + + const Layout in_layout(param.layout); + const Layout kernel_layout(param.kernel_layout); + CHECK(in_layout.convertible(kNCHW)) + << "Conv only support input layouts that are convertible from NCHW." + << " But got " << in_layout; + CHECK(kernel_layout.convertible(kOIHW)) + << "Conv only support kernel layouts that are convertible from OIHW." + << " But got "<< kernel_layout; + Layout out_layout(param.out_layout); + if (!out_layout.defined()) out_layout = in_layout; + CHECK(out_layout.convertible(kNCHW)) + << "Conv only support output layouts that are convertible from NCHW." + << " But got " << out_layout; + + if (param.use_bias) { + CHECK_EQ(in_shape->size(), 3U) << "Input:[data, weight, bias]"; + } else { + CHECK_EQ(in_shape->size(), 2U) << "Input:[data, weight]"; + } + CHECK_EQ(out_shape->size(), 1U); + + TShape dshape = in_shape->at(0); + if (dshape.ndim() == 0) return false; + dshape = ConvertLayout(dshape, in_layout, kNCHW); + + CHECK_EQ(dshape.ndim(), 4U) << "Input data should be 4D"; + CHECK_EQ(param.kernel_size.ndim(), 2U); + CHECK_EQ(param.strides.ndim(), 2U) + << "incorrect stride size: " << param.strides; + CHECK_EQ(param.dilation.ndim(), 2U) + << "incorrect dilate size: " << param.dilation; + CHECK_EQ(dshape[1] % param.groups, 0U) + << "input channels must divide group size"; + CHECK_EQ(param.channels % param.groups, 0U) + << "output channels must divide group size"; + + // NOTE: Do not check weight shape here! + // TShape wshape({param.channels / param.groups, + // dshape[1] / param.groups, + // param.kernel_size[0], + // param.kernel_size[1]}); + // wshape = ConvertLayout(wshape, kOIHW, kernel_layout); + // wshape[kernel_layout.indexof('O')] *= param.groups; + // NNVM_ASSIGN_INPUT_SHAPE(attrs, *in_shape, Conv2DParam::kWeight, wshape); + + if (param.use_bias) { + static const Layout default_bias_layout("C"); + TShape bias_shape({param.channels}); + auto oc_block = out_layout.subsizeof('C'); + if (oc_block > 0) { + size_t split_axis = (out_layout.indexof('C') < out_layout.indexof('c')) ? 1 : 0; + bias_shape = ConvertLayout(bias_shape, default_bias_layout, + default_bias_layout.split('C', split_axis, oc_block)); + } + NNVM_ASSIGN_INPUT_SHAPE(attrs, *in_shape, WinogradConv2DParam::kBias, bias_shape); + } + // dilation + dim_t dilated_ksize_y = 1 + (param.kernel_size[0] - 1) * param.dilation[0]; + dim_t dilated_ksize_x = 1 + (param.kernel_size[1] - 1) * param.dilation[1]; + TShape oshape({dshape[0], param.channels, 0, 0}); + if (dshape[2] != 0) { + oshape[2] = (dshape[2] + param.padding[0] * 2 - dilated_ksize_y) / param.strides[0] + 1; + } + if (dshape[3] != 0) { + oshape[3] = (dshape[3] + param.padding[1] * 2 - dilated_ksize_x) / param.strides[1] + 1; + } + NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_shape, 0, ConvertLayout(oshape, kNCHW, out_layout)); + // Perform incomplete shape inference. Fill in the missing values in data shape. + // 1) We can always fill in the batch_size. + // 2) We can back-calculate the input height/width if the corresponding stride is 1. + oshape = ConvertLayout((*out_shape)[0], out_layout, kNCHW); + dshape[0] = oshape[0]; + if (oshape[2] && param.strides[0] == 1) { + dshape[2] = oshape[2] + dilated_ksize_y - 1 - 2 * param.padding[0]; + } + if (oshape[3] && param.strides[1] == 1) { + dshape[3] = oshape[3] + dilated_ksize_x - 1 - 2 * param.padding[1]; + } + NNVM_ASSIGN_INPUT_SHAPE(attrs, *in_shape, WinogradConv2DParam::kData, + ConvertLayout(dshape, kNCHW, in_layout)); + // Check whether the kernel sizes are valid + if (dshape[2] != 0) { + CHECK_LE(dilated_ksize_y, dshape[2] + 2 * param.padding[0]) + << "kernel size exceed input"; + } + if (dshape[3] != 0) { + CHECK_LE(dilated_ksize_x, dshape[3] + 2 * param.padding[1]) + << "kernel size exceed input"; + } + return true; +} + +template inline bool Conv2DInferType(const nnvm::NodeAttrs& attrs, std::vector* in_type, std::vector* out_type) { - const Conv2DParam& param = nnvm::get(attrs.parsed); + const PARAM& param = nnvm::get(attrs.parsed); if (param.use_bias) { CHECK_EQ(in_type->size(), 3U) << "Input:[data, weight, bias]"; } else { @@ -154,11 +255,12 @@ inline bool Conv2DInferType(const nnvm::NodeAttrs& attrs, } +template inline bool Conv2DCorrectLayout(const NodeAttrs& attrs, std::vector *ilayouts, const std::vector *last_ilayouts, std::vector *olayouts) { - const Conv2DParam& param = nnvm::get(attrs.parsed); + const PARAM& param = nnvm::get(attrs.parsed); const Layout in_layout(param.layout); Layout out_layout(param.out_layout); @@ -213,8 +315,8 @@ a bias vector is created and added to the outputs. .set_attr("FGetAttrDict", ParamGetAttrDict) .set_attr("FListInputNames", UseBiasListInputNames) .set_attr("FInferShape", Conv2DInferShape) -.set_attr("FInferType", Conv2DInferType) -.set_attr("FCorrectLayout", Conv2DCorrectLayout) +.set_attr("FInferType", Conv2DInferType) +.set_attr("FCorrectLayout", Conv2DCorrectLayout) .set_num_outputs(1) .set_num_inputs(UseBiasNumInputs) .set_support_level(2) @@ -238,12 +340,81 @@ NNVM_REGISTER_OP(_contrib_conv2d_NCHWc) .set_attr("FGetAttrDict", ParamGetAttrDict) .set_attr("FListInputNames", UseBiasListInputNames) .set_attr("FInferShape", Conv2DInferShape) -.set_attr("FInferType", Conv2DInferType) -.set_attr("FCorrectLayout", Conv2DCorrectLayout) +.set_attr("FInferType", Conv2DInferType) +.set_attr("FCorrectLayout", Conv2DCorrectLayout) .set_num_outputs(1) .set_num_inputs(UseBiasNumInputs) .set_support_level(2); + +NNVM_REGISTER_OP(_contrib_conv2d_winograd_weight_transform) +.describe(R"code(Weight transformation of winograd fast convolution algorithm. +Separate this into another nnvm symbol in order to enable Precompute Pass to compute the +weight transformation in advance. + +- **weight**: (channels, in_channels, kernel_size[0], kernel_size[1]) +)code" NNVM_ADD_FILELINE) +.add_argument("weight", "4D Tensor", "Weight tensor.") +.add_arguments(WinogradWeightTransformParam::__FIELDS__()) +.set_attr_parser(ParamParser) +.set_attr("FGetAttrDict", ParamGetAttrDict) +.set_attr("FInferShape", [](const nnvm::NodeAttrs& attrs, + std::vector *in_shape, + std::vector *out_shape) { + const auto& param = nnvm::get(attrs.parsed); + const TShape &wshape = (*in_shape)[0]; + + CHECK_EQ(wshape.ndim(), 4) << "Weight should be a 4 dimensional tensor"; + + TShape oshape({param.tile_size + wshape[2] - 1, + param.tile_size + wshape[3] - 1, + wshape[0], + wshape[1]}); + NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_shape, 0, oshape); + return true; + }) +.set_attr("FCorrectLayot",[](const NodeAttrs& attrs, + std::vector *ilayouts, + const std::vector *last_ilayouts, + std::vector *olayouts) { + Layout layout("OIHW"); + NNVM_ASSIGN_LAYOUT(*ilayouts, 0, layout); + NNVM_ASSIGN_LAYOUT(*olayouts, 0, layout); + return true; +}) +.set_attr("FInferType", ElemwiseType<1, 1>) +.set_num_outputs(1) +.set_num_inputs(1) +.set_support_level(5); + +DMLC_REGISTER_PARAMETER(WinogradWeightTransformParam); + +NNVM_REGISTER_OP(_contrib_conv2d_winograd_without_weight_transform) +.describe(R"code(Compute conv2d with winograd algorithm. + +- **data**: Input is 4D array of shape (batch_size, in_channels, height, width) +- **weight**: Any shape + We do not check shape for this input tensor. + +- **bias**: (channels,) +- **out**: Output is 4D array of shape (batch_size, channels, out_height, out_width) +)code" NNVM_ADD_FILELINE) +.add_argument("data", "4D Tensor", "Input data.") +.add_argument("weight", "Tensor", "Transformed weight tensor.") +.add_argument("bias", "1D Tensor", "Bias parameter.") +.add_arguments(WinogradConv2DParam::__FIELDS__()) +.set_attr_parser(ParamParser) +.set_attr("FGetAttrDict", ParamGetAttrDict) +.set_attr("FListInputNames", UseBiasListInputNames) +.set_attr("FInferShape", WinogradConv2DInferShape) +.set_attr("FInferType", Conv2DInferType) +.set_attr("FCorrectLayout", Conv2DCorrectLayout) +.set_num_outputs(1) +.set_num_inputs(UseBiasNumInputs) +.set_support_level(5); + +DMLC_REGISTER_PARAMETER(WinogradConv2DParam); + NNVM_REGISTER_OP(_conv2d_grad) .describe(R"code(2D convolution grad. diff --git a/python/tvm/autotvm/task/nnvm_integration.py b/python/tvm/autotvm/task/nnvm_integration.py new file mode 100644 index 000000000000..160163ffb60f --- /dev/null +++ b/python/tvm/autotvm/task/nnvm_integration.py @@ -0,0 +1,291 @@ +# pylint: disable=unused-variable,invalid-name +""" +Decorator and utilities for the integration with TOPI and NNVM +""" +import warnings + +from ... import _api_internal, tensor, placeholder, target as _target + +from ..util import get_const_tuple, get_func_name +from .task import args_to_workload, dispatcher, create, register + +# Decorators for registering templates to topi +# a table that records all registered dispatcher for all targets +_REGISTED_DISPATHCER = { +} + +def register_topi_compute(topi_compute, target_keys, template_keys): + """Register a tunable template for a topi compute function + + Parameters + ---------- + topi_compute: callable + The overloaded topi compute call + target_keys: str or list of str + The compilation target + template_keys: str or list of str + The template key + + Returns + ------- + decorator: callable + A decorator + """ + fname = get_func_name(topi_compute) + + def _decorator(func=None): + """If call this function without argument, then we will reuse the function body + of original function""" + targets = [target_keys] if isinstance(target_keys, str) else target_keys + for target_key in targets: + if target_key not in _REGISTED_DISPATHCER: + _REGISTED_DISPATHCER[target_key] = {} + if topi_compute not in _REGISTED_DISPATHCER: + @topi_compute.register(target_key) + @dispatcher + def config_dispatcher(*args, **kwargs): + """override topi call as a config dispatcher""" + assert not kwargs, "Do not support kwargs in template function call" + return (fname, ) + args_to_workload(args) + _REGISTED_DISPATHCER[target_key][topi_compute] = config_dispatcher + + config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_compute] + + @config_dispatcher.register(template_keys) + def template_call(cfg, *args, **kwargs): + """call the topi func and attach workload to compute node""" + assert not kwargs, "Do not support kwargs in template function call" + if func is None: + node = topi_compute.fdefault(*args, **kwargs) + else: + node = func(cfg, *args, **kwargs) + + # attach workload to return op + op = node.op + attrs = {} + for k, v in node.op.attrs.items(): + attrs[k] = v + attrs['workload'] = (fname, ) + args_to_workload(args) + if isinstance(op, tensor.ComputeOp): + op = _api_internal._ComputeOp( + op.name, op.tag, attrs, op.axis, op.body) + elif isinstance(op, tensor.ExternOp): + op = _api_internal._ExternOp( + op.name, op.tag, attrs, + op.inputs, op.input_placeholders, + op.output_placeholders, op.body) + else: + raise RuntimeError("Unsupported op type: " + type(op)) + + if isinstance(node, tensor.Tensor): + return op.output(0) + return [op.output(i) for i in range(len(node))] + + return _decorator + +def register_topi_schedule(topi_schedule, target_keys, template_keys): + """Register a tunable template for a topi schedule function + + Parameters + ---------- + topi_schedule: callable + The overloaded topi schedule call + target_keys: str or list of str + The compilation target + template_keys: str or list of str + The template key + + Returns + ------- + decorator: callable + A decorator + """ + def _decorator(func): + targets = [target_keys] if isinstance(target_keys, str) else target_keys + for target_key in targets: + if target_key not in _REGISTED_DISPATHCER: + _REGISTED_DISPATHCER[target_key] = {} + if topi_schedule not in _REGISTED_DISPATHCER[target_key]: + @topi_schedule.register(target_key) + @dispatcher + def config_dispatcher(outs): + """override topi call as a workload dispatcher""" + def traverse(tensors): + """traverse all ops to find attached workload""" + for t in tensors: + op = t.op + if 'workload' in op.attrs: + return op.attrs['workload'] + wkl = traverse(op.input_tensors) + if wkl: + return wkl + return None + + outs = [outs] if isinstance(outs, tensor.Tensor) else outs + workload = traverse(outs) + + if workload is None: + raise RuntimeError("Cannot find workload in attribute of this schedule") + + return args_to_workload(workload) + + _REGISTED_DISPATHCER[target_key][topi_schedule] = config_dispatcher + + config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_schedule] + + @config_dispatcher.register(template_keys) + def template_call(cfg, outs): + """call the schedule func""" + return func(cfg, outs) + + return _decorator + + +def serialize_args(args): + ret = [] + for t in args: + if isinstance(t, tensor.Tensor): + ret.append(('TENSOR', get_const_tuple(t.shape), t.dtype)) + else: + ret.append(t) + return tuple(ret) + + +def deserialize_args(args): + ret = [] + for t in args: + if isinstance(t, tuple) and t[0] == 'TENSOR': + ret.append(placeholder(shape=t[1], dtype=t[2])) + else: + ret.append(t) + return ret + + +# Task extractor for nnvm graph +class TaskExtractEnv: + """Global environment for extracting tuning tasks from nnvm graph""" + current = None + + def __init__(self): + import topi + import nnvm + + self.symbol2topi = { + nnvm.sym.conv2d: [topi.nn.conv2d, topi.nn.depthwise_conv2d_nchw] + } + + self.topi_to_task = { + topi.nn.conv2d: "topi_nn_conv2d", + topi.nn.depthwise_conv2d_nchw: "topi_nn_depthwise_conv2d_nchw", + } + + self._register_dummy() + self._register_topi_task() + self.task_collection = [] + + def _register_dummy(self): + """Register dummy function to track the topi function call""" + for func in self.topi_to_task: + def _local_scope(local_func): + """build a scope to holds the function""" + @local_func.register("dummy", ) + def _dummy_func(*args, **kwargs): + assert not kwargs, "Do not support extracting tuning tasks when" \ + "kwargs is used in TOPI function call." \ + "Please modify it to use only positional args." + + if (self.topi_to_task[local_func], serialize_args(args)) \ + not in self.task_collection: + self.task_collection.append((self.topi_to_task[local_func], + serialize_args(args))) + with _target.create("llvm"): + return local_func(*args) + + _local_scope(func) + + def _register_topi_task(self): + """register tuning wrapper for topi function""" + import topi + + # Tuning wrapper for topi functions + @register("topi_nn_conv2d") + def _topi_nn_conv2d(*args, **kwargs): + assert not kwargs, "Do not support kwargs in template function call" + args = deserialize_args(args) + A, W = args[:2] + layout = args[-2] + assert layout == 'NCHW', "only support NCHW currently" + C = topi.nn.conv2d(*args, **kwargs) + s = topi.generic.schedule_conv2d_nchw([C]) + return s, [A, W, C] + + @register("topi_nn_depthwise_conv2d_nchw") + def _topi_nn_depthwise_conv2d_nchw(*args, **kwargs): + assert not kwargs, "Do not support kwargs in template function call" + args = deserialize_args(args) + A, W = args[:2] + C = topi.nn.depthwise_conv2d_nchw(*args, **kwargs) + s = topi.generic.schedule_depthwise_conv2d_nchw([C]) + return s, [A, W, C] + + def reset(self): + """Reset task collections""" + self.task_collection = [] + + def get_tasks(self): + """Get collected tasks""" + return self.task_collection + + @staticmethod + def get(): + """Get the single instance of TaskExtractEnv""" + if not TaskExtractEnv.current: + TaskExtractEnv.current = TaskExtractEnv() + return TaskExtractEnv.current + +def extract_from_graph(graph, shape, dtype, target, symbols, target_host=None): + """ Extract tuning tasks from a nnvm graph + + Parameters + ---------- + graph : Graph + The graph to tune + shape : dict of str to tuple, optional + The input shape to the graph + dtype : str or dict of str to str + The input types to the graph + target: tvm.target.Target + The compilation target + symbols : Array of nnvm.symbol + Array of nnvm symbols + target_host: tvm.target.Target + The host compilation target + + Returns + ------- + task: Array of autotvm.task.Task + collected tasks + """ + import nnvm.compiler + + env = TaskExtractEnv.get() + + topi_funcs = [] + for sym_name in symbols: + if sym_name in env.symbol2topi: + topi_funcs.extend(env.symbol2topi[sym_name]) + else: + warnings.warn("Symbol %s is not tunable, ignored" % sym_name) + + # run compiler to collect all TOPI calls during compilation + env.reset() + dummy_target = _target.create("llvm -device=dummy") + nnvm.compiler.build(graph, target=dummy_target, shape=shape, dtype=dtype) + + tasks = [] + for task_name, args in env.get_tasks(): + tasks.append(create(task_name, args, + target=target, target_host=target_host, + template_key='vanilla')) + + return tasks diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py new file mode 100644 index 000000000000..9135d5ed1ba1 --- /dev/null +++ b/python/tvm/autotvm/tuner/graph_tuning.py @@ -0,0 +1,106 @@ +"""Utilities for tuning a whole graph (a set of tasks)""" + +import os +from . import callback, XGBTuner, GATuner, RandomTuner, GridSearchTuner +from .. import measure, record, task + +def tune_tasks(tasks, + rpc_device_key, + + tuner='ga', + n_trial=500, + early_stopping=200, + log_filename='tuning.log', + + mea_number=5, + mea_parallel_num=1, + mea_timeout=20, + mea_use_ndk=False, + + use_transfer_learning=True): + """ + Tune a set of tasks + + Parameters + ---------- + tasks: Array of Task + A list of tasks to tune + rpc_device_key: str + The key of devices in rpc tracker + tuner: str + The type of tuner. + If is 'xgb', use :any:`XGBTuner`. + If is 'ga', use :any:`GATuner`. + If is 'random', use :any:`RandomTuner`. + If is 'gridsearch', use :any:`GridSearchTuner`. + n_trial: int + The maximum number of trials for a workload + early_stopping: int + The early stopping metric. The tuner will stop when it cannot find better + config after `early_stopping` trials + log_filename: str + The filename of output log file to store best configs + mea_number: int + The number of runs for taking average for one measurement. + mea_parallel_num: int + The parallel number in measurement. Set this to the number of devices you have. + mea_timeout: int + The timeout of a measurement. + mea_use_ndk: bool + Whether use Android NDK. The this to true if your target is android system + use_transfer_learning: bool + Whether reuse history tuning log to accelerate tuning + """ + + for i in range(len(tasks)): # pylint:disable=consider-using-enumerate + try: # try winograd template + tsk = task.create(tasks[i].name, tasks[i].args, + tasks[i].target, tasks[i].target_host, + 'winograd') + tasks.append(tsk) + except Exception: # pylint:disable=broad-except + pass + + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(tasks): + prefix = "[Task %2d/%2d] " %(i+1, len(tasks)) + + measure_option = measure.measure_option(mode='rpc', + repeat=3, + number=mea_number, + rpc_device_key=rpc_device_key, + parallel_num=mea_parallel_num, + timeout=mea_timeout, + use_ndk=mea_use_ndk) + + # create tuner + if tuner == 'xgb' or tuner == 'xgb-rank': + tuner_obj = XGBTuner(tsk, loss_type='rank', verbose=0) + elif tuner == 'ga': + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == 'random': + tuner_obj = RandomTuner(tsk) + elif tuner == 'gridsearch': + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(record.load_from_file(tmp_log_file)) + + # do tuning + tuner_obj.tune(n_trial=min(n_trial, len(tsk.config_space)), + early_stopping=early_stopping, + measure_option=measure_option, + verbose=0, + callbacks=[ + callback.progress_bar(n_trial, prefix=prefix), + callback.log_to_file(tmp_log_file)]) + + # pick best records to a cache file + record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) diff --git a/python/tvm/exec/download_op_param.py b/python/tvm/exec/download_op_param.py new file mode 100644 index 000000000000..4e00c929b731 --- /dev/null +++ b/python/tvm/exec/download_op_param.py @@ -0,0 +1,36 @@ +# pylint: disable=invalid-name +"""Download pre-tuned parameters of ops""" + +import argparse +import logging + +from ..autotvm.record import list_pretuned_op_param, download_pretuned_op_param + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--target", type=str, nargs='+', + help="Target to download. Use 'all' to download for all targets") + parser.add_argument("-l", action='store_true', help="List available packages") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + if args.l: + info = list_pretuned_op_param() + print("\n%-20s %-20s" % ("Target", "Size")) + print("-" * 41) + for target, info in info: + print("%-20s %-20s" % (target, "%.2f MB" % (info['size']/1000000))) + + if args.target: + info = list_pretuned_op_param() + all_targets = [x[0] for x in info] + if 'all' in args.target: + targets = all_targets + else: + targets = args.target + + for t in targets: + if t not in all_targets: + print("Warning : cannot find tuned parameters of " + t + ". (ignored)") + download_pretuned_op_param(t) diff --git a/python/tvm/exec/pick_best.py b/python/tvm/exec/pick_best.py new file mode 100644 index 000000000000..ea09c2ead003 --- /dev/null +++ b/python/tvm/exec/pick_best.py @@ -0,0 +1,37 @@ +# pylint: disable=invalid-name +"""Pick best log entries from a large file and store them to a small file""" + +import argparse +import os +import logging +import warnings + +from .. import autotvm + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--i", type=str, help="The input file") + parser.add_argument("--o", type=str, help="The output file") + + args = parser.parse_args() + logging.basicConfig(level=logging.INFO) + + if os.path.isfile(args.i): + args.o = args.o or args.i + ".best.log" + autotvm.record.pick_best(args.i, args.o) + elif os.path.isdir(args.i): + args.o = args.o or "best.log" + tmp_filename = args.o + ".tmp" + + with open(tmp_filename, 'w') as tmp_fout: + for filename in os.listdir(args.i): + if filename.endswith(".log"): + try: + autotvm.record.pick_best(filename, tmp_fout) + except Exception: # pylint: disable=broad-except + warnings.warn("Ignore invalid file %s", filename) + logging.info("Run final filter...") + autotvm.record.pick_best(tmp_filename, args.o) + os.remove(tmp_filename) + else: + raise ValueError("Invalid input file: " + args.i) diff --git a/topi/python/topi/__init__.py b/topi/python/topi/__init__.py index d58b37b04518..349f805cc7f2 100644 --- a/topi/python/topi/__init__.py +++ b/topi/python/topi/__init__.py @@ -24,7 +24,7 @@ from . import nn from . import x86 from . import cuda -from . import rasp +from . import arm_cpu from . import mali from . import intel_graphics from . import opengl diff --git a/topi/python/topi/arm_cpu/__init__.py b/topi/python/topi/arm_cpu/__init__.py new file mode 100644 index 000000000000..c42917f8e0e8 --- /dev/null +++ b/topi/python/topi/arm_cpu/__init__.py @@ -0,0 +1,4 @@ +"""Schedule for ARM CPU""" + +from . import conv2d +from . import depthwise_conv2d diff --git a/topi/python/topi/arm_cpu/conv2d.py b/topi/python/topi/arm_cpu/conv2d.py new file mode 100644 index 000000000000..329700a56cb6 --- /dev/null +++ b/topi/python/topi/arm_cpu/conv2d.py @@ -0,0 +1,508 @@ +# pylint: disable=invalid-name,unused-variable,no-else-return +"""Conv2D schedule for ARM CPU""" +from __future__ import absolute_import as _abs + +import numpy as np + +import tvm +from tvm import autotvm + +from ..generic import schedule_conv2d_nchw, schedule_conv2d_winograd_without_weight_transform +from ..util import traverse_inline, get_const_tuple, const_matrix +from ..nn import pad, conv2d, conv2d_alter_layout, conv2d_winograd_without_weight_transform +from ..nn.util import get_const_int, get_pad_tuple + +def _conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype): + """convert argument to workload""" + if len(kernel.shape) == 4: + raw_kernel = kernel + else: # the input kernel is transformed by alter_op_layout + shape = get_const_tuple(kernel.shape) + raw_kernel = tvm.placeholder((shape[0] * shape[4], shape[1], shape[2], shape[3]), + dtype=kernel.dtype) + return ('conv2d', ) + autotvm.task.args_to_workload( + [data, raw_kernel, strides, padding, layout, out_dtype]) + +@conv2d.register('arm_cpu') +@autotvm.task.dispatcher +def config_dispatcher(data, kernel, strides, padding, layout, out_dtype): + """TOPI compute callback. Mark this function as a dispatcher, so + this template can assign config according to workload""" + return _conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype) + +@config_dispatcher.register(['vanilla']) +def decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype): + """spatial packing template""" + return _decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype, num_tile=2) + +@autotvm.task.register_topi_schedule(schedule_conv2d_nchw, 'arm_cpu', ['vanilla', 'winograd']) +def schedule_conv2d_nchw_(cfg, outs): + """TOPI schedule callback""" + s = tvm.create_schedule([x.op for x in outs]) + + def _callback(op): + # schedule conv2d + if 'spatial_conv_output' in op.tag: + output = op.output(0) + conv = op.input_tensors[0] + + data_vec = conv.op.input_tensors[0] + data_pad = data_vec.op.input_tensors[0] + s[data_pad].compute_inline() + + kernel_vec = conv.op.input_tensors[1] + if kernel_vec.op.name == 'kernel_vec': + kernel = kernel_vec.op.input_tensors[0] + else: + kernel = kernel_vec + if isinstance(kernel.op, tvm.tensor.ComputeOp) and "dilate" in kernel.op.tag: + s[kernel].compute_inline() + + _schedule_spatial_pack(cfg, s, data_vec, kernel_vec, conv, output, outs[0]) + + if 'winograd_conv_output' in op.tag: + output = op.output(0) + _schedule_winograd(cfg, s, output, outs[0]) + + traverse_inline(s, outs[0].op, _callback) + return s + + +def _decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype, num_tile): + assert layout == "NCHW", "Only support NCHW" + out_dtype = out_dtype or data.dtype + + _, CI, IH, IW = get_const_tuple(data.shape) + if len(kernel.shape) == 4: + pre_packed = False + CO, _, KH, KW = get_const_tuple(kernel.shape) + else: # kernel tensor is pre packed + pre_packed = True + CO, _, KH, KW, VC = get_const_tuple(kernel.shape) + CO = CO * VC + + pad_top, pad_left, pad_down, pad_right = get_pad_tuple(padding, (KH, KW)) + HSTR, WSTR = strides if isinstance(strides, (tuple, list)) else (strides, strides) + + N = 1 + OH = (IH + pad_top + pad_down - KH) // HSTR + 1 + OW = (IW + pad_left + pad_right - KW) // WSTR + 1 + data_pad = pad(data, [0, 0, pad_top, pad_left], [0, 0, pad_down, pad_right]) + + # ==================== define configuration space ==================== + n, co, oh, ow = cfg.axis(N), cfg.axis(CO), cfg.axis(OH), cfg.axis(OW) + ci, kh, kw = cfg.reduce_axis(CI), cfg.reduce_axis(KH), cfg.reduce_axis(KW) + + if num_tile == 2: # for cpu + co, vc = cfg.define_split('tile_co', co, num_outputs=2) + oh, vh = cfg.define_split('tile_oh', oh, num_outputs=2) + ow, vw = cfg.define_split('tile_ow', ow, num_outputs=2) + elif num_tile == 3: # for gpu + co, _, vc = cfg.define_split('tile_co', co, num_outputs=3) + oh, _, vh = cfg.define_split('tile_oh', oh, num_outputs=3) + ow, _, vw = cfg.define_split('tile_ow', ow, num_outputs=3) + else: + raise RuntimeError("Invalid num_tile") + + cfg.define_reorder("reorder_0", + [n, co, oh, ow, ci, kh, kw, vh, vw, vc], + policy='candidate', candidate=[ + [n, co, oh, ow, ci, kh, kw, vh, vw, vc], + [n, co, oh, ow, ci, kh, kw, vc, vh, vw]]) + + cfg.define_annotate("ann_reduce", [kh, kw], policy='try_unroll') + cfg.define_annotate("ann_spatial", [vh, vw, vc], policy='try_unroll_vec') + # ==================================================================== + + VC = cfg["tile_co"].size[-1] + VH = cfg["tile_oh"].size[-1] + VW = cfg["tile_ow"].size[-1] + + dvshape = (N, OH // VH, OW // VW, CI, VH*HSTR + KH-1, VW*WSTR + KW-1) + kvshape = (CO // VC, CI, KH, KW, VC) + ovshape = (N, CO // VC, OH // VH, OW // VW, VH, VW, VC) + oshape = (N, CO, OH, OW) + + data_vec = tvm.compute(dvshape, lambda n, h, w, ci, vh, vw: + data_pad[n][ci][h*VH*HSTR+vh][w*VW*WSTR+vw], + name='data_vec') + + if pre_packed: + kernel_vec = kernel + else: + kernel_vec = tvm.compute(kvshape, lambda co, ci, kh, kw, vc: + kernel[co*VC+vc][ci][kh][kw], + name='kernel_vec') + + ci = tvm.reduce_axis((0, CI), name='ci') + kh = tvm.reduce_axis((0, KH), name='kh') + kw = tvm.reduce_axis((0, KW), name='kw') + + conv = tvm.compute(ovshape, lambda n, co, h, w, vh, vw, vc: \ + tvm.sum(data_vec[n, h, w, ci, vh*HSTR+kh, vw*WSTR+kw].astype(out_dtype) * + kernel_vec[co, ci, kh, kw, vc].astype(out_dtype), + axis=[ci, kh, kw]), name='conv') + + output = tvm.compute(oshape, lambda n, co, h, w: + conv[n][co//VC][h//VH][w//VW][h%VH][w%VW][co%VC], + name='output_unpack', tag='spatial_conv_output', + attrs={'workload': _conv_arg_to_workload(data, kernel, strides, padding, + layout, out_dtype)}) + return output + +def _schedule_spatial_pack(cfg, s, data_vec, kernel_vec, + conv, output, last): + """schedule implementation""" + n, co, oh, ow, vh, vw, vc = s[conv].op.axis + ci, kh, kw = s[conv].op.reduce_axis + + # schedule conv + cfg["reorder_0"].apply(s, conv, [n, co, oh, ow, ci, kh, kw, vh, vw, vc]) + cfg["ann_reduce"].apply(s, conv, [kh, kw], + axis_lens=[get_const_int(kh.dom.extent), + get_const_int(kw.dom.extent)], + max_unroll=16, + cfg=cfg) + cfg["ann_spatial"].apply(s, conv, [vh, vw, vc], + axis_lens=[cfg['tile_oh'].size[-1], + cfg['tile_ow'].size[-1], + cfg['tile_co'].size[-1]], + max_unroll=16, + cfg=cfg) + + # schedule fusion + n, co, h, w = s[last].op.axis + co, vc = cfg['tile_co'].apply(s, last, co) + oh, vh = cfg['tile_oh'].apply(s, last, h) + ow, vw = cfg['tile_ow'].apply(s, last, w) + s[last].reorder(n, co, oh, ow, vh, vw, vc) + if last != output: + s[output].compute_inline() + cfg["ann_spatial"].apply(s, last, [vh, vw, vc], + axis_lens=[cfg['tile_oh'].size[-1], + cfg['tile_ow'].size[-1], + cfg['tile_co'].size[-1]], + max_unroll=16, + cfg=cfg) + s[conv].compute_at(s[last], ow) + + # mark parallel + s[last].parallel(co) + + _, h, _, _, _, _ = s[data_vec].op.axis + s[data_vec].parallel(h) + + if kernel_vec.op.name == 'kernel_vec': + co, _, _, _, _ = s[kernel_vec].op.axis + s[kernel_vec].pragma(co, 'debug_skip_region') + # kernel packing will be pre-computed during compliation, so we skip this part + # to make tuning records correct + + return s + + +@config_dispatcher.register('winograd') +def decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype): + tile_size = 4 + return _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_size) + +def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_size): + N, CI, IH, IW = get_const_tuple(data.shape) + if len(kernel.shape) == 4: + pre_packed = False + CO, _, KH, KW = get_const_tuple(kernel.shape) + else: + pre_packed = True + H_CAT, W_CAT, CO, CI, VC = get_const_tuple(kernel.shape) + CO *= VC + KH, KW = H_CAT - tile_size + 1, W_CAT - tile_size + 1 + HSTR, WSTR = strides if isinstance(strides, (tuple, list)) else (strides, strides) + HPAD, WPAD, _, _ = get_pad_tuple(padding, kernel) + + assert layout == 'NCHW' + assert KH == 3 and KW == 3 and HPAD == 1 and WPAD == 1 and HSTR == 1 and WSTR == 1 + data_pad = pad(data, (0, 0, HPAD, WPAD), name="data_pad") + + if tile_size == 4: + G_data = np.array([ + [1 / 4.0, 0, 0], + [-1 / 6.0, -1 / 6.0, -1 / 6.0], + [-1 / 6.0, 1 / 6.0, -1 / 6.0], + [1 / 24.0, 1 / 12.0, 1 / 6.0], + [1 / 24.0, -1 / 12.0, 1 / 6.0], + [0, 0, 1]], dtype=np.float32) + + B_data = np.array([ + [4, 0, 0, 0, 0, 0], + [0, -4, 4, -2, 2, 4], + [-5, -4, -4, -1, -1, 0], + [0, 1, -1, 2, -2, -5], + [1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1]], out_dtype) + + A_data = np.array([ + [1, 0, 0, 0], + [1, 1, 1, 1], + [1, -1, 1, -1], + [1, 2, 4, 8], + [1, -2, 4, -8], + [0, 0, 0, 1]], out_dtype) + elif tile_size == 2: + B_data = np.array([ + [1, 0, 0, 0], + [0, 1, -1, 1], + [-1, 1, 1, 0], + [0, 0, 0, -1]], out_dtype) + + G_data = np.array([ + [1, 0, 0], + [1.0/2, 1.0/2, 1.0/2], + [1.0/2, -1.0/2, 1.0/2], + [0, 0, 1], ], out_dtype) + + A_data = np.array([ + [1, 0], + [1, 1], + [1, -1], + [0, -1], + ], out_dtype) + else: + raise ValueError("Unsupported tile size for winograd: " + str(tile_size)) + + + + m = A_data.shape[1] + r = 3 + alpha = m + r - 1 + K = CO + C = CI + + H = (IH + 2 * HPAD - 3) // HSTR + 1 + W = (IW + 2 * WPAD - 3) // WSTR + 1 + nH, nW = (H + m-1) // m, (W + m-1) // m + P = N * nH * nW + + cfg.define_split('tile_p', cfg.axis(P), num_outputs=2, filter=lambda x: x.size[-1] <= 16) + cfg.define_split('tile_k', cfg.axis(K), num_outputs=2, filter=lambda x: x.size[-1] <= 16) + VP = cfg['tile_p'].size[-1] + VK = cfg['tile_k'].size[-1] + + # pack input tile + input_tile = tvm.compute((C, P // VP, alpha, alpha, VP), + lambda c, b, eps, nu, bb: + data_pad[(b*VP+bb) // (nH*nW)][c][(b*VP+bb) // nW % nH * m + eps] + [(b*VP+bb) % nW * m + nu], + name='d') + + # transform kernel + if pre_packed: + U = kernel + else: + G = const_matrix(G_data, 'G') + r_kh = tvm.reduce_axis((0, KH), 'r_kh') + r_kw = tvm.reduce_axis((0, KW), 'r_kw') + U = tvm.compute((alpha, alpha, K // VK, C, VK), lambda eps, nu, k, c, kk: + tvm.sum(kernel[k * VK + kk][c][r_kh][r_kw] * G[eps][r_kh] * G[nu][r_kw], + axis=[r_kh, r_kw]), name='U') + + # transform image + B = const_matrix(B_data, 'B') + r_eps = tvm.reduce_axis((0, alpha), 'r_eps') + r_nu = tvm.reduce_axis((0, alpha), 'r_nu') + V = tvm.compute((alpha, alpha, P // VP, C, VP), lambda eps, nu, b, c, bb: + tvm.sum(input_tile[c][b][r_eps][r_nu][bb] * B[r_eps][eps] * B[r_nu][nu], + axis=[r_eps, r_nu]), name='V') + + # batch gemm + c = tvm.reduce_axis((0, C), name='c') + M = tvm.compute((alpha, alpha, K, P), lambda eps, nu, k, b: + tvm.sum(U[eps][nu][k // VK][c][k % VK] * + V[eps][nu][b // VP][c][b % VP], axis=c), name='M') + + # inverse transform + A = const_matrix(A_data, 'A') + r_eps = tvm.reduce_axis((0, alpha), 'r_eps') + r_nu = tvm.reduce_axis((0, alpha), 'r_nu') + Y = tvm.compute((K, P, m, m), lambda k, b, vh, vw: + tvm.sum(M[r_eps][r_nu][k][b] * A[r_eps][vh] * A[r_nu][vw], + axis=[r_eps, r_nu]), name='Y') + + # unpack output + output = tvm.compute((N, K, H, W), lambda n, k, h, w: + Y[k][n * nH * nW + (h//m) * nW + w//m][h % m][w % m], + name='output', tag='winograd_conv_output', + attrs={'workload': _winograd_conv_arg_to_workload( + data, kernel, strides, padding, layout, out_dtype, tile_size)}) + + cfg.add_flop(2 * N * K * H * W * KH * KW * C) + return output + +def _schedule_winograd(cfg, s, output, last): + Y = output.op.input_tensors[0] + M, A = Y.op.input_tensors + U, V = M.op.input_tensors + d, B = V.op.input_tensors + data_pad = d.op.input_tensors[0] + data = data_pad.op.input_tensors[0] + + # padding + s[data_pad].compute_inline() + + # pack input tiles + s[d].compute_inline() + + # transform kernel + if isinstance(U.op, tvm.tensor.ComputeOp): + kernel, G = U.op.input_tensors + s[G].compute_inline() + eps, nu, k, c, kk, = s[U].op.axis + r_kh, r_kw = s[U].op.reduce_axis + s[U].reorder(k, c, kk, eps, nu, r_kh, r_kw) + s[U].unroll(eps) + s[U].unroll(nu) + s[U].unroll(r_kh) + s[U].unroll(r_kw) + s[U].pragma(k, 'debug_skip_region') + # kernel transformation will be pre-computed during compliation, so we skip this part + # to make tuning records correct + + # transform image + DD = s.cache_read(d, 'global', [V]) + s[B].compute_inline() + eps, nu, b, c, bb = s[V].op.axis + r_eps, r_nu = s[V].op.reduce_axis + s[V].reorder(b, c, bb, eps, nu) + s[V].unroll(eps) + s[V].unroll(nu) + s[V].unroll(r_eps) + s[V].unroll(r_nu) + s[V].parallel(b) + s[DD].compute_at(s[V], bb) + + # batch gemm + eps, nu, k, b = s[M].op.axis + c = s[M].op.reduce_axis[0] + cfg.define_split('tile_c', c, num_outputs=2, filter=lambda x: x.size[-1] <= 16) + co, ci = cfg['tile_c'].apply(s, M, c) + xo, xi = cfg['tile_p'].apply(s, M, b) + s[M].reorder(eps, nu, xo, co, k, ci, xi) + cfg.define_annotate('ann_reduce', [ci], policy='try_unroll') + cfg.define_annotate('ann_spatial', [k, xi], policy='try_unroll_vec') + cfg['ann_reduce'].apply(s, M, [ci], + axis_lens=[cfg['tile_c'].size[-1]], + max_unroll=16, + cfg=cfg) + cfg['ann_spatial'].apply(s, M, [k, xi]) + + # inverse transform + s[A].compute_inline() + k, b, vh, vw = s[Y].op.axis + r_eps, r_nu = s[Y].op.reduce_axis + s[Y].unroll(vh) + s[Y].unroll(vw) + s[Y].unroll(r_eps) + s[Y].unroll(r_nu) + + # output + n, co, h, w = s[last].op.axis + co, coi = cfg['tile_k'].apply(s, last, co) + s[M].compute_at(s[last], co) + s[last].parallel(co) + + MM = s.cache_read(M, 'global', [Y]) + m = get_const_int(A.shape[1]) + ho, wo, hi, wi = s[last].tile(h, w, m, m) + s[Y].compute_at(s[last], wo) + s[MM].compute_at(s[last], wo) + + if output != last: + s[output].compute_inline() + +def _winograd_conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype, tile_size): + """convert argument to workload""" + K = 3 + shape = get_const_tuple(kernel.shape) + alpha = tile_size + K - 1 + if len(kernel.shape) == 4: + assert shape[2:] == (K, K) + CO, CI = shape[:2] + else: + assert shape[:2] == (alpha, alpha) + CO, CI, VCO = shape[2:] + CO *= VCO + + raw_kernel = tvm.placeholder((CO, CI, K, K), dtype=kernel.dtype) + return ('conv2d', ) + autotvm.task.args_to_workload( + [data, raw_kernel, strides, padding, layout, out_dtype]) + + +@conv2d_winograd_without_weight_transform.register(['arm_cpu']) +@autotvm.task.dispatcher +def winograd_ww_config_dispatcher_(data, kernel, strides, padding, layout, out_dtype, tile_size): + return _winograd_conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype, + tile_size) + + +@winograd_ww_config_dispatcher_.register(['winograd']) +def decl_winograd_ww(cfg, data, kernel, strides, padding, layout, out_dtype, tile_size): + return _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, + tile_size) + + +@autotvm.task.register_topi_schedule(schedule_conv2d_winograd_without_weight_transform, + 'arm_cpu', ['winograd']) +def schedule_conv2d_winograd_without_weight_transform_(cfg, outs): + """TOPI schedule callback""" + s = tvm.create_schedule([x.op for x in outs]) + + def _callback(op): + if 'winograd_conv_output' in op.tag: + output = op.output(0) + _schedule_winograd(cfg, s, output, outs[0]) + + traverse_inline(s, outs[0].op, _callback) + return s + + +@conv2d_alter_layout.register(["arm_cpu", "mali"]) +def _alter_conv2d_layout(attrs, inputs, tinfos): + """Alter op layout for pre-computing kernel transformation""" + import nnvm.symbol as sym + copy_inputs = [s for s in inputs] + + new_attrs = {k: attrs[k] for k in attrs.keys()} + + assert attrs.get_int_tuple("dilation") == (1, 1), "Does not support dilation " \ + "when alter_op_layout is enabled" + strides = attrs.get_int_tuple("strides") + padding = attrs.get_int_tuple("padding") + groups = attrs.get_int('groups') + layout = attrs["layout"] + out_dtype = attrs["out_dtype"] + out_dtype = tinfos[0].dtype if out_dtype == "same" else out_dtype + + if groups == 1: + # query config of this workload + workload = _conv_arg_to_workload(tinfos[0], tinfos[1], strides, padding, layout, out_dtype) + cfg = autotvm.task.DispatchContext.current.query(tvm.target.current_target(), workload) + + if cfg.template_key == 'vanilla': + new_attrs['kernel_layout'] = 'OIHW%do' % (cfg['tile_co'].size[-1]) + return sym.conv2d(*copy_inputs, **new_attrs) + else: + tile_size = 4 + + weight = sym.contrib.conv2d_winograd_weight_transform(copy_inputs[1], + tile_size=tile_size) + CO, CI, KH, KW = get_const_tuple(tinfos[1].shape) + VC = cfg['tile_k'].size[-1] + weight = sym.reshape(weight, + shape=(KH + tile_size - 1, KW + tile_size - 1, CO // VC, VC, CI)) + weight = sym.transpose(weight, axes=[0, 1, 2, 4, 3]) + + copy_inputs[1] = weight + new_attrs['tile_size'] = tile_size + return sym.contrib.conv2d_winograd_without_weight_transform(*copy_inputs, **new_attrs) + else: + # do nothing for depthwise convolution + return sym.conv2d(*copy_inputs, **new_attrs) diff --git a/topi/python/topi/arm_cpu/depthwise_conv2d.py b/topi/python/topi/arm_cpu/depthwise_conv2d.py new file mode 100644 index 000000000000..55ca24269a5f --- /dev/null +++ b/topi/python/topi/arm_cpu/depthwise_conv2d.py @@ -0,0 +1,91 @@ +# pylint: disable=invalid-name,unused-variable +"""Depthwise convolution schedule for ARM CPU""" + +import tvm +from tvm import autotvm + +from ..generic import schedule_depthwise_conv2d_nchw +from ..nn import depthwise_conv2d_nchw +from ..util import traverse_inline + +autotvm.task.register_topi_compute(depthwise_conv2d_nchw, 'arm_cpu', 'vanilla')() + +@autotvm.task.register_topi_schedule(schedule_depthwise_conv2d_nchw, 'arm_cpu', 'vanilla') +def schedule_depthwise_conv2d_nchw_(cfg, outs): + """Schedule depthwise conv2d""" + outs = [outs] if isinstance(outs, tvm.tensor.Tensor) else outs + s = tvm.create_schedule([x.op for x in outs]) + + def _schedule(cfg, s, data, data_pad, kernel, output): + A, B, C = data, kernel, output + s[data_pad].compute_inline() + + # define tile + n, c, h, w = s[output].op.axis + cfg.define_split('tile_c', c, num_outputs=2) + cfg.define_split('tile_h', h, num_outputs=2) + cfg.define_split('tile_w', w, num_outputs=2) + + # park data to vector form [n, c, h, w] -> [n, C, h, w, VC] + A0 = s.cache_read(data_pad, "global", C) + _, c, h, w = s[A0].op.axis + c, vc = cfg['tile_c'].apply(s, A0, c) + s[A0].reorder(c, h, w, vc) + A1 = s.cache_write(A0, 'global') + s[A0].compute_inline() + + # park kernel to vector form [co, ci, kh, kw] -> [CO, ci, kh, kw, VC] + B0 = s.cache_read(B, "global", C) + c, m, h, w = s[B0].op.axis + c, vc, = cfg['tile_c'].apply(s, B0, c) + s[B0].reorder(c, m, h, w, vc) + B1 = s.cache_write(B0, 'global') + s[B0].compute_inline() + + _, c, h, w = s[C].op.axis + c, vc, = cfg['tile_c'].apply(s, C, c) + s[C].reorder(c, h, w, vc) + + # depthwise conv + C0 = s.cache_write(C, 'global') + _, c, h, w, vc = s[C0].op.axis + dh, dw = s[C0].op.reduce_axis + oh, ih = cfg['tile_h'].apply(s, C0, h) + ow, iw = cfg['tile_w'].apply(s, C0, w) + s[C0].reorder(c, oh, ow, dh, dw, ih, iw, vc) + s[A1].compute_at(s[C0], oh) + + # try unroll and vectorization + cfg.define_annotate('ann', [ih, iw, vc], policy='try_unroll_vec') + cfg['ann'].apply(s, C0, [ih, iw, vc], + axis_lens=[cfg['tile_h'].size[-1], + cfg['tile_w'].size[-1], + cfg['tile_c'].size[-1]], + max_unroll=16, + cfg=cfg) + + # mark parallel + n, c, h, w = s[C].op.axis + s[C].parallel(c) + + n, c, h, w, vc = s[C0].op.axis + s[C0].parallel(c) + + c, m, h, w, vc = s[B1].op.axis + s[B1].parallel(c) + + return s + + def _callback(op): + if op.tag == 'depthwise_conv2d_nchw': + output = op.output(0) + kernel = op.input_tensors[1] + data = op.input_tensors[0] + data_pad = None + if isinstance(data.op, tvm.tensor.ComputeOp) and "pad" in data.op.tag: + data_pad = data + data = data_pad.op.input_tensors[0] + _schedule(cfg, s, data, data_pad, kernel, output) + + traverse_inline(s, outs[0].op, _callback) + return s diff --git a/topi/python/topi/generic/nn.py b/topi/python/topi/generic/nn.py index fe76b9715d59..3f73bf3a35e3 100644 --- a/topi/python/topi/generic/nn.py +++ b/topi/python/topi/generic/nn.py @@ -91,6 +91,52 @@ def schedule_conv2d_NCHWc(num_filter, kernel_size, strides, return _default_schedule(outs, False) +@tvm.target.generic_func +def schedule_conv2d_winograd_weight_transform(outs): + """Schedule for conv2d_nhwc + + Parameters + ---------- + outs: Array of Tensor + The computation graph description of conv2d_nchw + in the format of an array of tensors. + + Returns + ------- + sch: Schedule + The computation schedule for the op. + """ + s = tvm.create_schedule([x.op for x in outs]) + output = outs[0] + _, G = s[output].op.input_tensors + s[G].compute_inline() + eps, nu, co, ci = s[output].op.axis + r_kh, r_kw = s[output].op.reduce_axis + s[output].reorder(co, ci, r_kh, r_kw, eps, nu) + for axis in [r_kh, r_kw, eps, nu]: + s[output].unroll(axis) + s[output].parallel(co) + return s + + +@tvm.target.generic_func +def schedule_conv2d_winograd_without_weight_transform(outs): + """Schedule for conv2d_nhwc + + Parameters + ---------- + outs: Array of Tensor + The computation graph description of conv2d_nchw + in the format of an array of tensors. + + Returns + ------- + sch: Schedule + The computation schedule for the op. + """ + return _default_schedule(outs, False) + + @tvm.target.generic_func def schedule_conv2d_transpose_nchw(outs): """Schedule for conv2d_transpose_nchw diff --git a/topi/python/topi/nn/conv2d.py b/topi/python/topi/nn/conv2d.py index 43912368cf05..897ebfcaa110 100644 --- a/topi/python/topi/nn/conv2d.py +++ b/topi/python/topi/nn/conv2d.py @@ -3,78 +3,18 @@ """Conv2D operators""" from __future__ import absolute_import as _abs from collections import namedtuple +import numpy as np import tvm + from .pad import pad from .util import get_pad_tuple -from ..util import simplify +from ..util import simplify, const_matrix, get_const_tuple # workload description of conv2d Workload = namedtuple('Workload', ['in_dtype', 'out_dtype', 'height', 'width', 'in_filter', 'out_filter', 'hkernel', 'wkernel', 'hpad', 'wpad', 'hstride', 'wstride']) -# schedule description of spatial -SpatialPack = namedtuple('SpatialPack', - ['vh', 'vw', 'vc', 'ba', 'bc', 'unroll']) - -# schedule description of im2col -Im2ColPack = namedtuple('Im2ColPack', - ['vp', 'vq', 'ba', 'bc', 'unroll']) - -_WORKLOADS = [ - # workloads of resnet18 on imagenet - Workload('float32', 'float32', 224, 224, 3, 64, 7, 7, 3, 3, 2, 2), - Workload('float32', 'float32', 56, 56, 64, 64, 3, 3, 1, 1, 1, 1), - Workload('float32', 'float32', 56, 56, 64, 64, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 56, 56, 64, 128, 3, 3, 1, 1, 2, 2), - Workload('float32', 'float32', 56, 56, 64, 128, 1, 1, 0, 0, 2, 2), - Workload('float32', 'float32', 28, 28, 128, 128, 3, 3, 1, 1, 1, 1), - Workload('float32', 'float32', 28, 28, 128, 256, 3, 3, 1, 1, 2, 2), - Workload('float32', 'float32', 28, 28, 128, 256, 1, 1, 0, 0, 2, 2), - Workload('float32', 'float32', 14, 14, 256, 256, 3, 3, 1, 1, 1, 1), - Workload('float32', 'float32', 14, 14, 256, 512, 3, 3, 1, 1, 2, 2), - Workload('float32', 'float32', 14, 14, 256, 512, 1, 1, 0, 0, 2, 2), - Workload('float32', 'float32', 7, 7, 512, 512, 3, 3, 1, 1, 1, 1), - # workloads of mobile net on imagenet - Workload('float32', 'float32', 224, 224, 3, 32, 3, 3, 1, 1, 2, 2), - Workload('float32', 'float32', 112, 112, 32, 64, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 56, 56, 64, 128, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 56, 56, 128, 128, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 28, 28, 128, 256, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 28, 28, 256, 256, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 14, 14, 256, 512, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 14, 14, 512, 512, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 7, 7, 512, 1024, 1, 1, 0, 0, 1, 1), - Workload('float32', 'float32', 7, 7, 1024, 1024, 1, 1, 0, 0, 1, 1), - # workloads of resnet18 on imagenet (int16->int32 version) - Workload('int16', 'int32', 224, 224, 3, 64, 7, 7, 3, 3, 2, 2), - Workload('int16', 'int32', 56, 56, 64, 64, 3, 3, 1, 1, 1, 1), - Workload('int16', 'int32', 56, 56, 64, 64, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 56, 56, 64, 128, 3, 3, 1, 1, 2, 2), - Workload('int16', 'int32', 56, 56, 64, 128, 1, 1, 0, 0, 2, 2), - Workload('int16', 'int32', 28, 28, 128, 128, 3, 3, 1, 1, 1, 1), - Workload('int16', 'int32', 28, 28, 128, 256, 3, 3, 1, 1, 2, 2), - Workload('int16', 'int32', 28, 28, 128, 256, 1, 1, 0, 0, 2, 2), - Workload('int16', 'int32', 14, 14, 256, 256, 3, 3, 1, 1, 1, 1), - Workload('int16', 'int32', 14, 14, 256, 512, 3, 3, 1, 1, 2, 2), - Workload('int16', 'int32', 14, 14, 256, 512, 1, 1, 0, 0, 2, 2), - Workload('int16', 'int32', 7, 7, 512, 512, 3, 3, 1, 1, 1, 1), - # workloads of mobile net on imagenet (int16->int32 version) - Workload('int16', 'int32', 224, 224, 3, 32, 3, 3, 1, 1, 2, 2), - Workload('int16', 'int32', 112, 112, 32, 64, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 56, 56, 64, 128, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 56, 56, 128, 128, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 28, 28, 128, 256, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 28, 28, 256, 256, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 14, 14, 256, 512, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 14, 14, 512, 512, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 7, 7, 512, 1024, 1, 1, 0, 0, 1, 1), - Workload('int16', 'int32', 7, 7, 1024, 1024, 1, 1, 0, 0, 1, 1), -] - -# platform specific schedule -_CONV_SCHEDULE = {} - @tvm.target.generic_func def conv2d(input, filter, strides, padding, layout='NCHW', out_dtype=None): """Conv2D operator. @@ -178,137 +118,6 @@ def _get_schedule_NCHWc(wkl, layout, out_layout): return wkl -def _spatial_pack(data, kernel, stride, padding, out_dtype=None): - """ Compute convolution with pack on spatial axes. """ - if out_dtype is None: - out_dtype = data.dtype - assert data.shape[0].value == 1, "spatial pack convolution only support batch size=1" - wkl = _get_workload(data, kernel, stride, padding, out_dtype) - sch = _get_schedule(wkl) - - H, W = wkl.height, wkl.width - CI, CO = wkl.in_filter, wkl.out_filter - KH, KW = wkl.hkernel, wkl.wkernel - HPAD, WPAD = wkl.hpad, wkl.wpad - HSTR, WSTR = wkl.hstride, wkl.wstride - HCAT, WCAT = KH-1, KW-1 - - VH = sch.vh - VW = sch.vw - VC = sch.vc - UNROLL = sch.unroll - - TH = H + 2*HPAD - TW = W + 2*WPAD - OH = (H + 2*HPAD - KH) // HSTR + 1 - OW = (W + 2*WPAD - KW) // WSTR + 1 - - dshape = (1, CI, H, W) - dpshape = (1, CI, TH, TW) - dvshape = (1, TH//(VH*HSTR), TW//(VW*WSTR), CI, VH*HSTR+HCAT, VW*WSTR+WCAT) - - kshape = (CO, CI, KH, KW) - kvshape = (CO/VC, CI, KH, KW, VC) - - ovshape = (1, CO // VC, OH // VH, OW // VW, VH, VW, VC) - oshape = (1, CO, OH, OW) - - DOPAD = (HPAD != 0 and WPAD != 0) - if DOPAD: - data_pad = pad(data, (0, 0, HPAD, WPAD), name="data_pad") - else: - data_pad = data - - data_vec = tvm.compute(dvshape, lambda n, h, w, ci, vh, vw: \ - data_pad[n][ci][h*VH*HSTR+vh][w*VW*WSTR+vw], name='data_vec') - - kernel_vec = tvm.compute(kvshape, lambda co, ci, dh, dw, vc: \ - kernel[co*VC+vc][ci][dh][dw], name='kernel_vec') - - ci = tvm.reduce_axis((0, CI), name='ci') - dh = tvm.reduce_axis((0, KH), name='dh') - dw = tvm.reduce_axis((0, KW), name='dw') - - conv = tvm.compute(ovshape, lambda n, co, h, w, vh, vw, vc: \ - tvm.sum(data_vec[n, h, w, ci, vh*HSTR+dh, vw*WSTR+dw].astype(out_dtype) * - kernel_vec[co, ci, dh, dw, vc].astype(out_dtype), - axis=[ci, dh, dw]), name='conv') - - output = tvm.compute(oshape, lambda n, co, h, w: - conv[n][co//VC][h/VH][w//VW][h%VH][w%VW][co%VC], - name='output_unpack', tag='spatial_conv_output') - - return output - - -def _im2col_pack(data, kernel, stride, padding, out_dtype=None): - """ Compute convolution with im2col pack layout. """ - if out_dtype is None: - out_dtype = data.dtype - assert data.shape[0].value == 1, "im2col pack convolution only support batch size=1" - wkl = _get_workload(data, kernel, stride, padding, out_dtype) - sch = _get_schedule(wkl) - - N = 1 - H, W = wkl.height, wkl.width - CI = wkl.in_filter - CO = wkl.out_filter - KH, KW = wkl.hkernel, wkl.wkernel - HPAD, WPAD = wkl.hpad, wkl.hpad - HSTR, WSTR = wkl.hstride, wkl.wstride - - OH = (H + 2*HPAD - KH) // HSTR + 1 - OW = (W + 2*WPAD - KW) // WSTR + 1 - - P = sch.vp - Q = sch.vq - UNROLL = sch.unroll - - dshape = (N, CI, H, W) - dpshape = (N, CI, H+2*HPAD, W+2*WPAD) - dcshape = (N, OH, OW, CI, KH, KW) - dvshape = (N, OH * OW // P, CI, KH, KW, P) - - kshape = (CO, CI, KH, KW) - kvshape = (CO // Q, CI, KH, KW, Q) - - ovshape = (N, CO // Q, OH * OW // P, P, Q) - oshape = (N, CO, OH, OW) - - ############### declaration - - DO_PAD = (wkl.hpad != 0 and wkl.wpad != 0) - if DO_PAD: - data_pad = pad(data, (0, 0, HPAD, WPAD), name="data_pad") - else: - data_pad = data - - data_col = tvm.compute(dcshape, lambda n, oh, ow, ci, hk, wk: \ - data_pad[n][ci][oh*HSTR+hk][ow*WSTR+wk], name='data_col') - - data_vec = tvm.compute(dvshape, lambda n, im, ci, hk, wk, vim: \ - data_col[n][(im*P+vim)//OW][(im*P+vim)%OW][ci][hk][wk], name='data_vec') - - - kernel_vec = tvm.compute(kvshape, lambda co, ci, dh, dw, vc: \ - kernel[co*Q+vc][ci][dh][dw], name='kernel_vec') - - ci = tvm.reduce_axis((0, CI), name='ci') - hk = tvm.reduce_axis((0, KH), name='hk') - wk = tvm.reduce_axis((0, KW), name='wk') - - conv = tvm.compute(ovshape, lambda n, co, im, vim, vco: \ - tvm.sum(data_vec[n][im][ci][hk][wk][vim].astype(out_dtype) * - kernel_vec[co][ci][hk][wk][vco].astype(out_dtype), - axis=[ci, hk, wk]), name='conv') - - output = tvm.compute(oshape, lambda n, co, h, w: \ - conv[n][co//Q][(h*OW+w)//P][(h*OW+w)%P][co%Q], - name='output_vec', tag='im2col_conv_output') - - return output - - def conv2d_nchw(Input, Filter, stride, padding, out_dtype=None): """Convolution operator in NCHW layout. @@ -465,6 +274,7 @@ def conv2d_nhwc(Input, Filter, stride, padding, out_dtype='float32'): name="Conv2dOutput", tag="conv2d_nhwc") return Output + @tvm.target.generic_func def conv2d_NCHWc(data, kernel, num_filter, kernel_size, stride, padding, layout, out_layout, out_dtype='float32'): @@ -510,8 +320,70 @@ def conv2d_NCHWc(data, kernel, num_filter, kernel_size, stride, # default declaration raise ValueError("missing register for topi.nn.conv2d_NCHWc") -# map from schedule type to declaration function -_SCH_TO_DECL_FUNC = { - SpatialPack: _spatial_pack, - Im2ColPack: _im2col_pack, -} + +def conv2d_winograd_weight_transform(kernel, tile_size): + """Weight transformation for winograd + + Parameters + ---------- + kernel: Tensor + The raw kernel tensor + tile_size: int + Tile size of winograd transform. e.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3) + """ + K = 3 + + shape = get_const_tuple(kernel.shape) + assert shape[2:] == (K, K), "Only support 3x3 kernel" + + r = tile_size + K - 1 + shape = (r, r) + shape[:2] + + if tile_size == 2: + G_data = np.array([ + [1, 0, 0], + [1.0/2, 1.0/2, 1.0/2], + [1.0/2, -1.0/2, 1.0/2], + [0, 0, 1], + ], dtype=kernel.dtype) + elif tile_size == 4: + G_data = np.array([ + [1 / 4.0, 0, 0], + [-1 / 6.0, -1 / 6.0, -1 / 6.0], + [-1 / 6.0, 1 / 6.0, -1 / 6.0], + [1 / 24.0, 1 / 12.0, 1 / 6.0], + [1 / 24.0, -1 / 12.0, 1 / 6.0], + [0, 0, 1] + ], dtype=kernel.dtype) + else: + raise ValueError("Unsupoorted tile size:" + tile_size) + + G = const_matrix(G_data, 'G') + r_kh = tvm.reduce_axis((0, K), name='r_kh') + r_kw = tvm.reduce_axis((0, K), name='r_kw') + return tvm.compute(shape, lambda eps, nu, co, ci: + tvm.sum(kernel[co][ci][r_kh][r_kw] * + G[eps][r_kh] * G[nu][r_kw], + axis=[r_kh, r_kw]), name='transform_weight') + + +@tvm.target.generic_func +def conv2d_winograd_without_weight_transform(input, filter, strides, padding, + layout, out_dtype, tile_size): + """Compute convolution in winograd algorithm. The filter is supposed to be transformed + in advance. + + Parameters + ---------- + input : tvm.Tensor + 4-D with shape [batch, in_height, in_width, in_channel] + filter : tvm.Tensor + 4-D with shape [filter_height, filter_width, in_channel, num_filter] + strides : int or a list/tuple of two ints + Stride size, or [stride_height, stride_width] + padding : int or str + Padding size, or ['VALID', 'SAME'] + tile_size: int + Tile size of winograd transform. e.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3) + """ + raise ValueError("missing register for topi.nn.conv2d_winograd_without_weight_transform") diff --git a/topi/python/topi/rasp/conv2d.py b/topi/python/topi/rasp/conv2d.py deleted file mode 100644 index b958d00f2913..000000000000 --- a/topi/python/topi/rasp/conv2d.py +++ /dev/null @@ -1,358 +0,0 @@ -# pylint: disable=invalid-name,unused-variable,invalid-name -"""Conv2D schedule on raspberry pi""" -from __future__ import absolute_import as _abs -import tvm -from .. import tag -from ..nn.conv2d import conv2d as _conv2d, _get_schedule -from ..nn.conv2d import SpatialPack, Im2ColPack -from ..nn.conv2d import _WORKLOADS, _SCH_TO_DECL_FUNC -from ..nn.conv2d import _get_workload -from ..nn.util import infer_pad, infer_stride -from .. import generic - -_SCHEDULES = [ - # float32 imagenet - SpatialPack(1, 8, 4, 1, 4, True), - SpatialPack(1, 7, 4, 2, 4, True), - SpatialPack(1, 4, 8, 4, 1, True), - SpatialPack(1, 4, 4, 1, 16, False), - SpatialPack(1, 4, 8, 4, 8, False), - SpatialPack(1, 7, 4, 3, 8, True), - SpatialPack(1, 2, 8, 1, 8, True), - SpatialPack(2, 1, 16, 1, 4, True), - SpatialPack(1, 7, 4, 1, 1, True), - Im2ColPack(7, 4, 1, 16, True), - Im2ColPack(7, 4, 1, 8, False), - Im2ColPack(7, 4, 1, 16, False), - - # float32 mobilenet - SpatialPack(2, 2, 4, 28, 1, True), - SpatialPack(1, 4, 8, 14, 1, False), - SpatialPack(1, 2, 16, 8, 1, True), - SpatialPack(1, 4, 8, 8, 8, True), - SpatialPack(2, 2, 8, 1, 1, False), - SpatialPack(1, 4, 8, 4, 8, False), - SpatialPack(2, 2, 8, 1, 4, False), - SpatialPack(2, 2, 8, 1, 8, False), - Im2ColPack(7, 4, 1, 16, False), - Im2ColPack(7, 4, 1, 4, True), - - # int8 imagenet - SpatialPack(2, 2, 4, 19, 8, False), - SpatialPack(2, 2, 8, 1, 4, True), - SpatialPack(2, 2, 8, 7, 4, False), - SpatialPack(2, 4, 4, 7, 16, False), - SpatialPack(1, 7, 4, 14, 4, True), - SpatialPack(2, 2, 8, 5, 1, False), - SpatialPack(1, 2, 16, 3, 8, True), - SpatialPack(1, 7, 4, 1, 16, True), - SpatialPack(2, 2, 8, 2, 16, True), - SpatialPack(1, 1, 8, 4, 4, True), - SpatialPack(1, 1, 4, 1, 8, False), - SpatialPack(1, 1, 8, 1, 16, True), - - # int8 mobilenet - SpatialPack(2, 2, 8, 8, 1, True), - SpatialPack(1, 7, 4, 16, 4, True), - SpatialPack(1, 4, 8, 1, 1, True), - SpatialPack(1, 4, 8, 1, 1, True), - SpatialPack(1, 4, 8, 4, 8, True), - SpatialPack(1, 4, 8, 7, 1, True), - SpatialPack(1, 2, 8, 2, 32, True), - SpatialPack(1, 2, 16, 2, 16, True), - SpatialPack(1, 1, 32, 1, 16, False), - SpatialPack(1, 1, 16, 1, 32, True), -] - -@_get_schedule.register("rasp") -def _get_schedule_conv2d(wkl): - if wkl not in _WORKLOADS: - raise ValueError("no schedule for such workload: {}".format(wkl)) - idx = _WORKLOADS.index(wkl) - sch = _SCHEDULES[idx] - return sch - - -@_conv2d.register("rasp") -def _declaration_conv2d(data, kernel, stride, padding, layout, out_dtype): - if out_dtype is None: - out_dtype = data.dtype - assert layout == 'NCHW', "only support NCHW convolution on rasp" - assert data.shape[0].value == 1, "only support batch size=1 convolution on rasp" - wkl = _get_workload(data, kernel, stride, padding, out_dtype) - sch = _get_schedule(wkl) - return _SCH_TO_DECL_FUNC[type(sch)](data, kernel, stride, padding, out_dtype) - - -def _schedule_spatial_conv2d(s, data, data_pad, data_vec, - kernel, kernel_vec, - conv_out, output, last): - # no stride and padding info here - padding = infer_pad(data, data_pad) - if data_pad is None: - stride = infer_stride(data, kernel, output) - else: - stride = infer_stride(data_pad, kernel, output) - wkl = _get_workload(data, kernel, stride, padding, output.dtype) - sch = _get_schedule(wkl) - - H, W = wkl.height, wkl.width - CI, CO = wkl.in_filter, wkl.out_filter - HK, WK = wkl.hkernel, wkl.wkernel - HPAD, WPAD = wkl.hpad, wkl.wpad - HSTR, WSTR = wkl.hstride, wkl.wstride - - HCAT, WCAT = HK-1, WK-1 - DOPAD = (HPAD != 0 and WPAD != 0) - - VH = sch.vh - VW = sch.vw - VC = sch.vc - UNROLL = sch.unroll - - A, B, C = data, kernel, last - A0, A1 = data_pad, data_vec - B0 = kernel_vec - C0, C1 = conv_out, output - - CC = s.cache_write(C0, "global") - - _, co, oh, ow, vh, vw, vc = s[C0].op.axis - if UNROLL: - s[C0].unroll(vw) - s[C0].vectorize(vc) - - s[CC].compute_at(s[C0], ow) - _, co, oh, ow, vh, vw, vc = s[CC].op.axis - ci, dh, dw = s[CC].op.reduce_axis - s[CC].reorder(ci, dh, vh, dw, vw, vc) - - if UNROLL: - s[CC].unroll(vw) - s[CC].vectorize(vc) - - ##### Schedule A - if DOPAD: - s[A0].compute_inline() - - _, h, _, _, _, _ = s[A1].op.axis - if sch.ba == 1: - oaxis = h - paxis = h - else: - oh, ih = s[A1].split(h, sch.ba) - oaxis = oh - paxis = ih - - s[A1].parallel(paxis) - s[A1].pragma(oaxis, "parallel_launch_point") - s[A1].pragma(paxis, "parallel_stride_pattern") - s[A1].pragma(oaxis, "parallel_barrier_when_finish") - - - ##### Schedule B - co, _, _, _, _ = s[B0].op.axis - if sch.bc == 1: - oaxis = co - paxis = co - else: - oco, ico = s[B0].split(co, sch.bc) - oaxis = oco - paxis = ico - - s[B0].parallel(paxis) - s[B0].pragma(oaxis, "parallel_launch_point") - s[B0].pragma(paxis, "parallel_stride_pattern") - s[B0].pragma(oaxis, "parallel_barrier_when_finish") - - - ##### Schedule C - n, co, h, w = s[C].op.axis - co, vc = s[C].split(co, VC) - oh, ow, vh, vw = s[C].tile(h, w, VH, VW) - s[C].reorder(n, co, oh, ow, vh, vw, vc) - if C != C1: - s[C1].compute_inline() - s[C0].compute_at(s[C], ow) - - if sch.bc == 1: - oaxis = co - paxis = co - else: - oco, ico = s[C].split(co, sch.bc) - oaxis = oco - paxis = ico - - s[C].parallel(paxis) - s[C].pragma(oaxis, "parallel_launch_point") - s[C].pragma(paxis, "parallel_stride_pattern") - s[C].pragma(oaxis, "parallel_barrier_when_finish") - - return s - -def _schedule_im2col_conv2d(s, data, data_pad, data_col, data_vec, - kernel, kernel_vec, - conv_out, output, last): - # no stride and padding info here - padding = infer_pad(data, data_pad) - if data_pad is None: - stride = infer_stride(data, kernel, output) - else: - stride = infer_stride(data_pad, kernel, output) - wkl = _get_workload(data, kernel, stride, padding, output.dtype) - sch = _get_schedule(wkl) - - H, W = wkl.height, wkl.width - CI = wkl.in_filter - CO = wkl.out_filter - HK, WK = wkl.hkernel, wkl.wkernel - HPAD, WPAD = wkl.hpad, wkl.wpad - HSTR, WSTR = wkl.hstride, wkl.wstride - - HCAT, WCAT = HK-1, WK-1 - DOPAD = (HPAD != 0 and WPAD != 0) - - P = sch.vp - Q = sch.vq - UNROLL = sch.unroll - - A, B, C = data, kernel, last - A0, A1, A2 = data_pad, data_col, data_vec - B0 = kernel_vec - C0, C1 = conv_out, output - - CC = s.cache_write(C0, "global") - AA = s.cache_read(A2, "global", [CC]) - BB = s.cache_read(B0, "global", [CC]) - - - ##### Schedule CC - _, co, im, vim, vco = s[C0].op.axis - s[C0].unroll(vim) - s[C0].vectorize(vco) - - s[CC].compute_at(s[C0], im) - _, co, im, vim, vco = s[CC].op.axis - ci, hk, wk = s[CC].op.reduce_axis - s[CC].reorder(ci, hk, wk, vim, vco) - s[CC].unroll(vim) - s[CC].vectorize(vco) - # s[CC].unroll(ccr) - - ### Schedule C - _, co, h, w = s[C].op.axis - im = s[C].fuse(h, w) - im, vim = s[C].split(im, P) - co, vco = s[C].split(co, Q) - s[C].reorder(co, im, vim, vco) - - if sch.bc == 1: - oaxis = co - paxis = co - else: - oco, ico = s[C].split(co, sch.bc) - oaxis = oco - paxis = ico - - s[C].parallel(paxis) - s[C].pragma(oaxis, "parallel_launch_point") - s[C].pragma(paxis, "parallel_stride_pattern") - s[C].pragma(oaxis, "parallel_barrier_when_finish") - if C1 != C: - s[C1].compute_inline() - - s[C0].compute_at(s[C], paxis) - - ##### Schedule A - if DOPAD: - s[A0].compute_inline() - s[A1].compute_inline() - s[AA].compute_at(s[CC], wk) - s[AA].unroll(AA.op.axis[4]) - - _, im, _, _, _, _ = s[A2].op.axis - if sch.ba == 1: - oaxis = im - paxis = im - else: - oim, iim = s[A2].split(im, sch.ba) - oaxis = oim - paxis = iim - - s[A2].parallel(paxis) - s[A2].pragma(oaxis, "parallel_launch_point") - s[A2].pragma(paxis, "parallel_stride_pattern") - s[A2].pragma(oaxis, "parallel_barrier_when_finish") - - - ##### Schedule B - s[BB].compute_at(s[CC], wk) - s[BB].vectorize(BB.op.axis[4]) - - co, _, _, _, _ = s[B0].op.axis - if sch.bc == 1: - oaxis = co - paxis = co - else: - oco, ico = s[B0].split(co, sch.bc) - oaxis = oco - paxis = ico - - s[B0].parallel(paxis) - s[B0].pragma(oaxis, "parallel_launch_point") - s[B0].pragma(paxis, "parallel_stride_pattern") - s[B0].pragma(oaxis, "parallel_barrier_when_finish") - - return s - -@generic.schedule_conv2d_nchw.register(["rasp"]) -def schedule_conv2d_nchw(outs): - """Create schedule for tensors""" - s = tvm.create_schedule([x.op for x in outs]) - - def traverse(op): - """Traverse operators from computation graph""" - # inline all one-to-one-mapping operators except the last stage (output) - if tag.is_broadcast(op.tag): - if op not in s.outputs: - s[op].compute_inline() - for tensor in op.input_tensors: - if tensor.op.input_tensors: - traverse(tensor.op) - - if 'spatial_conv_output' in op.tag: - output = op.output(0) - conv_out = op.input_tensors[0] - kernel_vec = conv_out.op.input_tensors[1] - kernel = kernel_vec.op.input_tensors[0] - if isinstance(kernel.op, tvm.tensor.ComputeOp) and "dilate" in kernel.op.tag: - s[kernel].compute_inline() - data_vec = conv_out.op.input_tensors[0] - data = data_vec.op.input_tensors[0] - data_pad = None - if isinstance(data.op, tvm.tensor.ComputeOp) and "pad" in data.op.tag: - data_pad = data - data = data_pad.op.input_tensors[0] - - _schedule_spatial_conv2d(s, data, data_pad, data_vec, - kernel, kernel_vec, - conv_out, output, outs[0]) - - if 'im2col_conv_output' in op.tag: - output = op.output(0) - conv_out = op.input_tensors[0] - kernel_vec = conv_out.op.input_tensors[1] - kernel = kernel_vec.op.input_tensors[0] - data_vec = conv_out.op.input_tensors[0] - data_col = data_vec.op.input_tensors[0] - data = data_col.op.input_tensors[0] - data_pad = None - if isinstance(data.op, tvm.tensor.ComputeOp) and "pad" in data.op.tag: - data_pad = data - data = data_pad.op.input_tensors[0] - _schedule_im2col_conv2d(s, data, data_pad, data_col, data_vec, - kernel, kernel_vec, - conv_out, output, outs[0]) - - traverse(outs[0].op) - return s diff --git a/topi/python/topi/rasp/depthwise_conv2d.py b/topi/python/topi/rasp/depthwise_conv2d.py deleted file mode 100644 index b2ff78e46d88..000000000000 --- a/topi/python/topi/rasp/depthwise_conv2d.py +++ /dev/null @@ -1,207 +0,0 @@ -# pylint: disable=invalid-name,unused-variable, unused-argument -"""Schedule for depthwise_conv2d with auto fusion""" -from __future__ import absolute_import as _abs -from collections import namedtuple -import tvm -from .. import tag -from ..nn.util import infer_pad, infer_stride, get_pad_tuple -from .. import generic - -_Workload = namedtuple('Workload', - ['in_dtype', 'out_dtype', 'height', 'width', 'channel', 'multiplier', - 'hkernel', 'wkernel', 'hpad', 'wpad', 'hstride', 'wstride']) - -_Schedule = namedtuple('Schedule', ['vh', 'vw', 'vc', 'bc', 'unroll']) - -# workloads of depthwise conv mobile net on imagenet -_WORKLOADS = [ - _Workload('float32', 'float32', 112, 112, 32, 1, 3, 3, 1, 1, 1, 1), - _Workload('float32', 'float32', 112, 112, 64, 1, 3, 3, 1, 1, 2, 2), - _Workload('float32', 'float32', 56, 56, 128, 1, 3, 3, 1, 1, 1, 1), - _Workload('float32', 'float32', 56, 56, 128, 1, 3, 3, 1, 1, 2, 2), - _Workload('float32', 'float32', 28, 28, 256, 1, 3, 3, 1, 1, 1, 1), - _Workload('float32', 'float32', 28, 28, 256, 1, 3, 3, 1, 1, 2, 2), - _Workload('float32', 'float32', 14, 14, 512, 1, 3, 3, 1, 1, 1, 1), - _Workload('float32', 'float32', 14, 14, 512, 1, 3, 3, 1, 1, 2, 2), - _Workload('float32', 'float32', 7, 7, 1024, 1, 3, 3, 1, 1, 1, 1), - _Workload('int16', 'int32', 112, 112, 32, 1, 3, 3, 1, 1, 1, 1), - _Workload('int16', 'int32', 112, 112, 64, 1, 3, 3, 1, 1, 2, 2), - _Workload('int16', 'int32', 56, 56, 128, 1, 3, 3, 1, 1, 1, 1), - _Workload('int16', 'int32', 56, 56, 128, 1, 3, 3, 1, 1, 2, 2), - _Workload('int16', 'int32', 28, 28, 256, 1, 3, 3, 1, 1, 1, 1), - _Workload('int16', 'int32', 28, 28, 256, 1, 3, 3, 1, 1, 2, 2), - _Workload('int16', 'int32', 14, 14, 512, 1, 3, 3, 1, 1, 1, 1), - _Workload('int16', 'int32', 14, 14, 512, 1, 3, 3, 1, 1, 2, 2), - _Workload('int16', 'int32', 7, 7, 1024, 1, 3, 3, 1, 1, 1, 1), -] - -_SCHEDULES = [ - _Schedule(2, 1, 4, 1, True), - _Schedule(2, 4, 4, 2, True), - _Schedule(2, 1, 4, 2, False), - _Schedule(2, 4, 4, 1, True), - _Schedule(4, 1, 4, 8, True), - _Schedule(1, 1, 4, 2, True), - _Schedule(1, 1, 8, 8, True), - _Schedule(1, 1, 4, 1, False), - _Schedule(1, 1, 4, 4, False), - _Schedule(2, 4, 4, 2, False), - _Schedule(2, 7, 4, 1, True), - _Schedule(2, 4, 4, 4, False), - _Schedule(2, 2, 4, 4, False), - _Schedule(2, 2, 8, 4, False), - _Schedule(2, 2, 4, 4, True), - _Schedule(2, 2, 8, 4, False), - _Schedule(1, 2, 8, 4, True), - _Schedule(1, 1, 4, 8, True), -] - -def _get_workload(data, kernel, stride, padding, out_dtype): - _, C, IH, IW = [x.value for x in data.shape] - _, MT, KH, KW = [x.value for x in kernel.shape] - HPAD, WPAD, _, _ = get_pad_tuple(padding, kernel) - if isinstance(stride, (tuple, list)): - HSTR, WSTR = stride - else: - HSTR, WSTR = stride, stride - return _Workload(data.dtype, out_dtype, IH, IW, C, MT, KH, KW, HPAD, WPAD, HSTR, WSTR) - - -def _schedule(s, data, data_pad, kernel, output, last): - padding = infer_pad(data, data_pad) - if data_pad is None: - stride = infer_stride(data, kernel, output) - else: - stride = infer_stride(data_pad, kernel, output) - wkl = _get_workload(data, kernel, stride, padding, output.dtype) - - if wkl not in _WORKLOADS: - return s - - # use specified schedule - sch = _SCHEDULES[_WORKLOADS.index(wkl)] - - H, W = wkl.height, wkl.width - CN = wkl.channel - MT = wkl.multiplier - - HK, WK = wkl.hkernel, wkl.wkernel - HPAD, WPAD = wkl.hpad, wkl.wpad - HSTR, WSTR = wkl.hstride, wkl.wstride - - VH, VW = sch.vh, sch.vw - BC = sch.bc - VC = sch.vc - - TH = H + 2*HPAD - TW = W + 2*WPAD - OH = (H + 2*HPAD - HK) / HSTR + 1 - OW = (W + 2*WPAD - WK) / WSTR + 1 - - - A, B, C = data, kernel, output - A0 = data_pad - - A1 = s.cache_read(A0, "global", C) - _, c, h, w = s[A1].op.axis - c, vc = s[A1].split(c, VC) - s[A1].reorder(c, h, w, vc) - - A2 = s.cache_write(A1, 'global') - s[A0].compute_inline() - s[A1].compute_inline() - - B0 = s.cache_read(B, "global", C) - c, m, h, w = s[B0].op.axis - c, vc = s[B0].split(c, VC) - s[B0].reorder(c, m, h, w, vc) - - B1 = s.cache_write(B0, 'global') - s[B0].compute_inline() - - _, c, h, w = s[C].op.axis - c, vc = s[C].split(c, VC) - s[C].reorder(c, h, w, vc) - - - C0 = s.cache_write(C, 'global') - _, c, h, w, vc = s[C0].op.axis - dh, dw = s[C0].op.reduce_axis - oh, ow, ih, iw = s[C0].tile(h, w, VH, VW) - s[C0].reorder(c, oh, ow, dh, dw, ih, iw, vc) - if sch.unroll: - s[C0].unroll(iw) - s[C0].vectorize(vc) - - - # # s[C0].compute_at(s[C0], ow) - launch, c, _, _ = s[C].op.axis - s[C].pragma(launch, "parallel_launch_point") - - s[C].parallel(c) - s[C].pragma(c, "parallel_stride_pattern") - s[C].pragma(c, "parallel_barrier_when_finish") - - - s[C0].compute_at(s[C], launch) - _, c, h, w, vc = s[C0].op.axis - s[C0].parallel(c) - s[C0].pragma(c, "parallel_stride_pattern") - s[C0].pragma(c, "parallel_barrier_when_finish") - - - s[A2].compute_at(s[C0], oh) - # parallel(s[A2], s[A2].op.axis[1], BC) - - # # s[B0].compute_at(s[C0], ow) - s[B1].compute_at(s[C], launch) - c, m, h, w, vc = s[B1].op.axis - s[B1].parallel(c) - s[B1].pragma(c, "parallel_stride_pattern") - s[B1].pragma(c, "parallel_barrier_when_finish") - - return s - - -@generic.schedule_depthwise_conv2d_nchw.register(["cpu", "rasp"]) -def schedule_depthwise_conv2d_nchw(outs): - """Schedule for depthwise_conv2d nchw forward. - - Parameters - ---------- - outs: Array of Tensor - The computation graph description of depthwise_conv2d - in the format of an array of tensors. - - Returns - ------- - s: Schedule - The computation schedule for depthwise_conv2d nchw. - """ - outs = [outs] if isinstance(outs, tvm.tensor.Tensor) else outs - s = tvm.create_schedule([x.op for x in outs]) - - def traverse(op): - """Internal travserse function""" - # inline all one-to-one-mapping operators except the last stage (output) - if tag.is_broadcast(op.tag): - if op not in s.outputs: - s[op].compute_inline() - for tensor in op.input_tensors: - if tensor.op.input_tensors: - traverse(tensor.op) - # schedule depthwise_conv2d - if op.tag == 'depthwise_conv2d_nchw': - output = op.output(0) - kernel = op.input_tensors[1] - if isinstance(kernel.op, tvm.tensor.ComputeOp) and "dilate" in kernel.op.tag: - s[kernel].compute_inline() - data = op.input_tensors[0] - data_pad = None - if isinstance(data.op, tvm.tensor.ComputeOp) and "pad" in data.op.tag: - data_pad = data - data = data_pad.op.input_tensors[0] - _schedule(s, data, data_pad, kernel, output, outs[0]) - - traverse(outs[0].op) - return s diff --git a/topi/python/topi/util.py b/topi/python/topi/util.py index 3625f6aaefaa..b5d5dd2b99ad 100644 --- a/topi/python/topi/util.py +++ b/topi/python/topi/util.py @@ -1,7 +1,30 @@ +# pylint: disable=invalid-name """Common topi utilities""" from __future__ import absolute_import as _abs import tvm +from . import tag + +def traverse_inline(s, op, callback): + """Traverse computation graph and do auto inline + + Parameters + ---------- + s: schedule + The schedule + op: Operation + The final output operator. + callback: callable + The callback function on each op + """ + if tag.is_injective(op.tag): + if op not in s.outputs: + s[op].compute_inline() + for tensor in op.input_tensors: + if tensor.op.input_tensors: + traverse_inline(s, tensor.op, callback) + callback(op) + def prod(x): """Get the product of every items in the tuple. @@ -151,3 +174,33 @@ def unravel_index(idx, shape): idx = idx // shape[i] indices = indices[::-1] return indices + + +def const_matrix(matrix, name="const_matrix"): + """convert a const numpy 2-dimensional matrix to tvm tensor + + Parameters + ---------- + matrix: numpy.ndarray + Const input array + name: str, optional + The name of output op + + Returns + ------- + tensor: Tensor + The created tensor + """ + row, col = matrix.shape + dtype = str(matrix.dtype) + + def select_array(i, j): + now = tvm.const(0.0, dtype) + for ii in range(row): + for jj in range(col): + now = tvm.select(tvm.all(i % row == ii, j % col == jj), + tvm.const(matrix[ii][jj], dtype), + now) + return now + + return tvm.compute(matrix.shape, select_array, name=name) From 197b5e66e756502535257cedc4589786daee44c3 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 01:48:56 -0700 Subject: [PATCH 03/76] remove two warnings --- src/codegen/build_module.cc | 14 -------------- src/pass/vectorize_loop.cc | 1 - 2 files changed, 15 deletions(-) diff --git a/src/codegen/build_module.cc b/src/codegen/build_module.cc index f9b6226f86c4..4e210b47ac9d 100644 --- a/src/codegen/build_module.cc +++ b/src/codegen/build_module.cc @@ -258,15 +258,6 @@ Target metal(const std::vector& options) { return CreateTarget("metal", options); } -Target rasp(const std::vector& options) { - return CreateTarget("llvm", MergeOptions(options, { - "-device=rasp", - "-mtriple=armv7l-none-linux-gnueabihf", - "-mcpu=cortex-a53", - "-mattr=+neon" - })); -} - Target mali(const std::vector& options) { return CreateTarget("opencl", MergeOptions(options, { "-device=mali" @@ -728,11 +719,6 @@ TVM_REGISTER_API("_GetCurrentTarget") TVM_REGISTER_API("_EnterTargetScope") .set_body([](TVMArgs args, TVMRetValue* ret) { Target target = args[0]; - auto current = Target::current_target(); - if (current.defined() && target->str() != current->str()) { - LOG(WARNING) << "Overriding target " << current->str() - << " with new target scope " << target->str(); - } Target::EnterTargetScope(target); }); diff --git a/src/pass/vectorize_loop.cc b/src/pass/vectorize_loop.cc index 206b75ed068d..62b4afdaa41f 100644 --- a/src/pass/vectorize_loop.cc +++ b/src/pass/vectorize_loop.cc @@ -300,7 +300,6 @@ class Vectorizer : public IRMutator { CHECK(!op->condition.type().is_vector()); Expr condition = this->Mutate(op->condition); if (condition.type().is_vector()) { - LOG(WARNING) << "Detect vector condition in Vectorized Loop, scalarizing..."; return Scalarize(s); } Stmt then_case = this->Mutate(op->then_case); From 5b29ee9d591605bf3ff9c3651de7f150bc378ca7 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 01:49:34 -0700 Subject: [PATCH 04/76] update tutorials & docs --- apps/benchmark/README.md | 5 + apps/benchmark/arm_cpu_imagenet_bench.py | 111 +++++++++ apps/benchmark/rasp_imagenet_bench.py | 76 ------ docs/api/python/autotvm.rst | 3 + docs/install/from_source.rst | 4 +- tutorials/autotvm/tune_cuda_conv2d.py | 1 + tutorials/autotvm/tune_nnvm_arm.py | 256 +++++++++++++++++++++ tutorials/cross_compilation_and_rpc.py | 229 +++++++----------- tutorials/nnvm/README.txt | 2 + tutorials/nnvm/deploy_model_on_mali_gpu.py | 124 +++++----- tutorials/nnvm/deploy_model_on_rasp.py | 120 +++++----- tutorials/nnvm/from_mxnet.py | 2 + tutorials/nnvm/imagenet_inference_gpu.py | 89 ------- tutorials/nnvm_quick_start.py | 156 +++++-------- 14 files changed, 627 insertions(+), 551 deletions(-) create mode 100644 apps/benchmark/README.md create mode 100644 apps/benchmark/arm_cpu_imagenet_bench.py delete mode 100644 apps/benchmark/rasp_imagenet_bench.py create mode 100644 tutorials/autotvm/tune_nnvm_arm.py delete mode 100644 tutorials/nnvm/imagenet_inference_gpu.py diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md new file mode 100644 index 000000000000..4457ed9e44f9 --- /dev/null +++ b/apps/benchmark/README.md @@ -0,0 +1,5 @@ +# Performance Benchark + +## ARM CPU + + diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py new file mode 100644 index 000000000000..79c778e343f5 --- /dev/null +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -0,0 +1,111 @@ +"""Benchmark script for performance on ARM CPU. +see README.md for the usage and results of this script. +""" + +import argparse + +import numpy as np + +import nnvm.testing +import nnvm.compiler +import tvm +from tvm import autotvm +from tvm.contrib.util import tempdir +import tvm.contrib.graph_runtime as runtime + +def get_network(name, batch_size): + """Get the symbol definition and random weight of a network + + Parameters + ---------- + name: str + The name of network + batch_size: int + The batch size + + Returns + ------- + net: Symbol + params: dict + shape: dict + output_shape: tuple + """ + shape = {"data": (batch_size, 3, 224, 224)} + output_shape = (batch_size, 1000) + if name == 'resnet-18': + net, params = nnvm.testing.resnet.get_workload(num_layers=18, + batch_size=batch_size, image_shape=(3, 224, 224)) + elif name == 'mobilenet': + net, params = nnvm.testing.mobilenet.get_workload(batch_size=batch_size) + elif name == 'squeezenet v1.1': + net, params = nnvm.testing.squeezenet.get_workload(batch_size=batch_size, + version='1.1') + elif name == 'vgg-16': + net, params = nnvm.testing.vgg.get_workload(batch_size=batch_size, num_layers=16) + else: + raise RuntimeError("Unsupported network: " + name) + + return net, params, shape, output_shape + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--network", type=str, choices=['resnet-18', 'mobilenet', 'squeezenet v1.1', 'vgg-16']) + parser.add_argument("--device", type=str, required=True, choices=['rk3399', 'mate10', 'mate10pro', 'p20', 'p20pro', + 'pixel2', 'rasp3b', 'pynq']) + parser.add_argument("--host", type=str, required=True) + parser.add_argument("--port", type=int, required=True) + parser.add_argument("--rpc-key", type=str, required=True) + parser.add_argument("--number", type=int, default=5) + args = parser.parse_args() + + dtype = 'float32' + + if args.network is None: + networks = ['mobilenet', 'squeezenet v1.1', 'resnet-18', 'vgg-16'] + else: + networks = [args.network] + + target = tvm.target.arm_cpu(model=args.device) + + print("--------------------------------------------------") + print("%-20s %-20s" % ("Network Name", "Mean Inference Time (std dev)")) + print("--------------------------------------------------") + for network in networks: + net, params, shape, out_shape = get_network(network, batch_size=1) + + with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): + graph, lib, params = nnvm.compiler.build( + net, target=target, shape=shape, params=params, dtype=dtype) + + tmp = tempdir() + if 'android' in str(target): + from tvm.contrib import ndk + filename = "net.so" + path_name = tmp.relpath(filename) + lib.export_library(path_name, ndk.create_shared) + else: + filename = "net.tar" + path_name = tmp.relpath(filename) + lib.export_library(path_name) + + # get remote device session + tracker = tvm.rpc.connect_tracker(args.host, args.port) + remote = tracker.request(args.rpc_key) + + # upload library and params + ctx = remote.context(str(target), 0) + remote.upload(path_name) + rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} + + rlib = remote.load_module(filename) + module = runtime.create(graph, rlib, ctx) + data_tvm = tvm.nd.array((np.random.uniform(size=shape['data'])).astype(dtype)) + module.set_input('data', data_tvm) + module.set_input(**rparams) + + # evaluate + ftimer = module.module.time_evaluator("run", ctx, number=args.number, repeat=3) + prof_res = np.array(ftimer().results) * 1000 # multiply 1000 for converting to millionsecond + print("%-20s %-19s (%s)" % (network, "%.2f ms" % np.mean(prof_res), "%.2f ms" % np.std(prof_res))) + diff --git a/apps/benchmark/rasp_imagenet_bench.py b/apps/benchmark/rasp_imagenet_bench.py deleted file mode 100644 index 098ae721da40..000000000000 --- a/apps/benchmark/rasp_imagenet_bench.py +++ /dev/null @@ -1,76 +0,0 @@ -""" Benchmark script for performance on Raspberry Pi. For example, run the file with: -`python rasp_imagenet_bench.py --model='modbilenet' --host='rasp0' --port=9090`. For -more details about how to set up the inference environment on Raspberry Pi, Please -refer to NNVM Tutorial: Deploy the Pretrained Model on Raspberry Pi """ -import time -import argparse -import numpy as np -import tvm -import nnvm.compiler -import nnvm.testing -from tvm.contrib import util, rpc -from tvm.contrib import graph_runtime as runtime - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--model', type=str, required=True, choices=['resnet', 'mobilenet'], - help="The model type.") - parser.add_argument('--host', type=str, required=True, help="The host address of your Raspberry Pi.") - parser.add_argument('--port', type=int, required=True, help="The port number of your Raspberry Pi.") - parser.add_argument('--opt-level', type=int, default=1, help="Level of optimization.") - parser.add_argument('--num-iter', type=int, default=50, help="Number of iteration during benchmark.") - args = parser.parse_args() - - opt_level = args.opt_level - - num_iter = args.num_iter - batch_size = 1 - num_classes = 1000 - image_shape = (3, 224, 224) - - data_shape = (batch_size,) + image_shape - out_shape = (batch_size, num_classes) - if args.model == 'resnet': - net, params = nnvm.testing.resnet.get_workload( - batch_size=1, image_shape=image_shape) - elif args.model == 'mobilenet': - net, params = nnvm.testing.mobilenet.get_workload( - batch_size=1, image_shape=image_shape) - else: - raise ValueError('no benchmark prepared for {}.'.format(args.model)) - - - with nnvm.compiler.build_config(opt_level=opt_level): - graph, lib, params = nnvm.compiler.build( - net, tvm.target.rasp(), shape={"data": data_shape}, params=params) - - tmp = util.tempdir() - lib_fname = tmp.relpath('net.o') - lib.save(lib_fname) - - remote = rpc.connect(args.host, args.port) - remote.upload(lib_fname) - - ctx = remote.cpu(0) - rlib = remote.load_module('net.o') - rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} - - module = runtime.create(graph, rlib, ctx) - module.set_input('data', tvm.nd.array(np.random.uniform(size=(data_shape)).astype("float32"))) - module.set_input(**rparams) - module.run() - out = module.get_output(0, tvm.nd.empty(out_shape, ctx=ctx)) - out.asnumpy() - - print('benchmark args: {}'.format(args)) - ftimer = module.module.time_evaluator("run", ctx, num_iter) - for i in range(3): - prof_res = ftimer() - print(prof_res) - # sleep for avoiding cpu overheat - time.sleep(45) - - -if __name__ == '__main__': - main() diff --git a/docs/api/python/autotvm.rst b/docs/api/python/autotvm.rst index e1f4906af0e7..fc0c754ee624 100644 --- a/docs/api/python/autotvm.rst +++ b/docs/api/python/autotvm.rst @@ -44,6 +44,9 @@ tvm.autotvm.tuner .. automodule:: tvm.autotvm.tuner.callback :members: +.. automodule:: tvm.autotvm.tuner.graph_tuning + :members: + tvm.autotvm.task ~~~~~~~~~~~~~~~~ .. automodule:: tvm.autotvm.task diff --git a/docs/install/from_source.rst b/docs/install/from_source.rst index 5709ed7f0bab..edeba1ccfadc 100644 --- a/docs/install/from_source.rst +++ b/docs/install/from_source.rst @@ -60,6 +60,8 @@ The configuration of tvm can be modified by `config.cmake`. - Edit ``build/config.cmake`` to customize the compilation options - On macOS, for some versions of XCode, you need to add ``-lc++abi`` in the LDFLAGS or you'll get link errors. + - Change ``set(USE_CUDA OFF)`` to ``set(USE_CUDA ON)`` to enable CUDA backend. So do other backends and libraries + (OpenCL, RCOM, METAL, VULKAN, ...). - TVM optionally depends on LLVM. LLVM is required for CPU codegen that needs LLVM. @@ -84,7 +86,7 @@ The configuration of tvm can be modified by `config.cmake`. cmake .. make -j4 -If everything goes well, we can go to :ref:`python-package-installation`_ +If everything goes well, we can go to :ref:`python-package-installation` Building on Windows ~~~~~~~~~~~~~~~~~~~ diff --git a/tutorials/autotvm/tune_cuda_conv2d.py b/tutorials/autotvm/tune_cuda_conv2d.py index b6096aa47a5b..821b7e2bd2bc 100644 --- a/tutorials/autotvm/tune_cuda_conv2d.py +++ b/tutorials/autotvm/tune_cuda_conv2d.py @@ -189,3 +189,4 @@ def conv2d_no_batching(N, H, W, CI, CO, KH, KW, stride, padding): evaluator = func.time_evaluator(func.entry_name, ctx, number=200) print('Time cost of this operator: %f' % evaluator(a_tvm, w_tvm, c_tvm).mean) + diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py new file mode 100644 index 000000000000..96b45ba1dc1b --- /dev/null +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -0,0 +1,256 @@ +""" +Auto-tuning a convolutional network for ARM CPU +==================================================== +**Author**: `Lianmin Zheng `_ + +Auto-tuning for a specific ARM device is critical for getting the best +performance. This is a tutorial about how to tune a whole convolutional +network. + +The operator implementation for ARM CPU in TVM is wrote in template form. +It has many tunable knobs (tile factor, vectorization, unrolling, etc). +We will do tuning for all convolution and depthwise convolution operators +in the neural network. After the tuning, we can get a log file which stores +the best knob values for all required operators. When the tvm compiler compiles +these operators, it will query this log file to get the best knob values. + +We also released pre-tuned parameters for some arm devices. You can go to +`ARM CPU Benchmark `_ to see the results. +""" + +###################################################################### +# Install dependencies and import packages +# ---------------------------------------- +# To use autotvm package in tvm, we need to install some extra dependencies. +# +# .. code-block:: bash +# +# pip install psutil xgboost +# + +import time +import os + +import numpy as np + +import nnvm.testing +import nnvm.compiler +import tvm +from tvm import autotvm +from tvm.contrib.util import tempdir +import tvm.contrib.graph_runtime as runtime + +################################################################# +# Define network +# -------------- +# First we need to define the network in nnvm symbol API. +# We can load some pre-defined network from :code:`nnvm.testing`. +# We can also load models from mxnet, ONNX and tensorflow (see NNVM +# tutorials :ref:`tutorial-nnvm` for more details). + +def get_network(name, batch_size): + """Get the symbol definition and random weight of a network""" + shape = {"data": (batch_size, 3, 224, 224)} + output_shape = (batch_size, 1000) + + if name =='resnet-18': + net, params = nnvm.testing.resnet.get_workload(num_layers=18, batch_size=batch_size) + elif name =='mobilenet': + net, params = nnvm.testing.mobilenet.get_workload(batch_size=batch_size) + elif name =='squeezenet v1.1': + net, params = nnvm.testing.squeezenet.get_workload(batch_size=batch_size, version='1.1') + elif name =='vgg-16': + net, params = nnvm.testing.vgg.get_workload(num_layers=16, batch_size=batch_size) + elif name =='custom': + # an example for custom network + from nnvm.testing import utils + net = nnvm.sym.Variable('data') + net = nnvm.sym.conv2d(net, channels=4, kernel_size=(3,3), padding=(1,1)) + net = nnvm.sym.flatten(net) + net = nnvm.sym.dense(net, units=1000) + net, params = utils.create_workload(net, batch_size, (3, 224, 224)) + elif name == 'mxnet': + # an example for mxnet model + from mxnet.gluon.model_zoo.vision import get_model + block = get_model('resnet18_v1', pretrained=True) + net, params = nnvm.frontend.from_mxnet(block) + net = nnvm.sym.softmax(net) + else: + raise ValueError("Unsupported network: " + name) + + return net, params, shape, output_shape + +################################################################# +# Start RPC Tracker +# ----------------- +# TVM uses RPC session to communicate with ARM boards. +# During tuning, the tuner will send the generated code to the board and +# measure the speed of code on the board. +# +# To scale up the tuning, TVM uses RPC Tracker to manage distributed devices. +# The RPC Tracker is a centralized master node. We can register all devices to +# the tracker. For example, if we have 10 phones, we can register all of them +# to the tracker, then we can run 10 measurements in parallle, which accelerates +# the tuning process. +# +# To start an RPC tracker, run this command in the host machine. The tracker is +# required during the whole tuning process, so we need to open a new terminal for +# this command: +# +# .. code-block:: bash +# +# python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 +# +# The expected output is +# +# .. code-block:: bash +# +# INFO:RPCTracker:bind to 0.0.0.0:9190 + +################################################################# +# Register devices to RPC Tracker +# ----------------------------------- +# Now we can register our devices to the tracker. The first step is to +# build tvm runtime for the ARM devices. +# +# * For Linux: +# Follow this section :ref:`build-tvm-runtime-on-device` to build +# tvm runtime on the device. Then register the device to tracker by +# +# .. code-block:: bash +# +# python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=rk3399 +# +# (replace :code:`[HOST_IP]` with the IP address of your host machine) +# +# * For Android: +# Follow this `readme page `_ to +# install tvm rpc apk on the android device. Make sure you can pass the android rpc test. +# +# After registering devices, we can confirm it by querying rpc_tracker +# +# .. code-block:: bash +# +# python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 +# +# For exmpale, if we have 2 Huawei mate10 pro, 11 Raspberry Pi 3B and 2 rk3399, +# the output can be +# +# .. code-block:: bash +# +# Queue Status +# ---------------------------- +# key free pending +# ---------------------------- +# mate10pro 2 0 +# rk3399 2 0 +# rpi3b 11 0 +# ---------------------------- + +########################################### +# Begin Tuning +# ------------ +# Now we can extract tuning tasks from the network and begin tuning. + +# Replace "aarch64-linux-gnu" with the correct target of your board. +# This target is used for cross compilation. +target = tvm.target.create('llvm -device=arm_cpu -target=aarch64-linux-gnu') + +# Also replace this with the device key in your tracker +device_key = 'rk3399' + +network = 'resnet-18' +log_file = "%s.%s.log" % (device_key, network) + +dtype = 'float32' + +# tuning option +tuning_option = { + 'log_filename': log_file, + 'rpc_device_key': device_key, + + 'tuner':'xgb', + 'n_trial': 1000, + 'early_stopping': 200, + + 'mea_number': 4, + 'mea_parallel_num': 1, + 'mea_timeout': 10, + + 'use_transfer_learning': True, +} + +#################################################################### +# +# .. note:: How to set tuning options +# +# In general, the default value provided here works well. It is the same +# value that we used to generate pre-tuned parameters. +# If you have multiple devices, you can set :code:`mea_parallel_num` to +# the number of devices you have. (e.g. set it to 3 if you register 3 rk3399 +# boards to the tracker). +# +# You can also refer to our doc :any:`tune_tasks` (click this) to see some comments. +# + +def tune_and_evaluate(): + # extract workloads from nnvm graph + net, params, shape, out_shape = get_network(network, batch_size=1) + tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, + symbols=(nnvm.sym.conv2d,), + target=target) + autotvm.tune_tasks(tasks, **tuning_option) + + # compile kernels with history best records + with autotvm.apply_history_best(log_file): + print("Compile...") + with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): + graph, lib, params = nnvm.compiler.build( + net, target=target, + shape=shape, params=params, dtype=dtype) + + # export library + tmp = tempdir() + filename = "net.so" + path_name = tmp.relpath(filename) + + if tuning_option.get('use_ndk', False): + # for android + from tvm.contrib import ndk + lib.export_library(path_name, ndk.create_shared) + else: + lib.export_library(path_name) + + # upload module to device + print("Upload...") + remote = autotvm.measure.request_remote(device_key, timeout=10000) + remote.upload(path_name) + rlib = remote.load_module(filename) + + # upload parameters to device + ctx = remote.context(str(target), 0) + rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} + data_tvm = tvm.nd.array((np.random.uniform(size=shape['data'])).astype(dtype)) + module = runtime.create(graph, rlib, ctx) + module.set_input('data', data_tvm) + module.set_input(**rparams) + + # evaluate + print("Evaluate inference time cost...") + ftimer = module.module.time_evaluator("run", ctx, number=1, repeat=10) + prof_res = np.array(ftimer().results) * 1000 # convert to millionsecond + print("Mean inference time (std dev): %.2f ms (%.2f ms)" % + (np.mean(prof_res), np.std(prof_res))) + +# We do not run the tuning in our webpage server. Uncomment this line to run by yourself. +# tune_and_evaluate() + +###################################################################### +# Sample Output +# ------------- +# The tuning takes about 1 hour on a 32 threads AMD server. +# One sample output is +# +# .. code-block:: bash +# +# diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index a3c01deb4518..55a3c3de8227 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -3,113 +3,78 @@ Cross Compilation and RPC ========================= -**Author**: `Ziheng Jiang `_ +**Author**: `Ziheng Jiang `_, `Lianmin Zheng `_ This tutorial introduces cross compilation and remote device execution with RPC in TVM. With cross compilation and RPC, you can **compile program on your -local machine then run it on remote device**. It is useful when the -resource of remote device is limited, like Raspberry Pi and mobile -platforms, so you do not wish to put the compilation procedure on -the device in order to save time and space. -In this tutorial, I will take Raspberry Pi as our target platform -for example. +local machine then run it on the remote device**. It is useful when +the resource of remote devices is limited, like Raspberry Pi and mobile +platforms. In this tutorial, we will take Raspberry Pi for CPU example +and Firefly-RK3399 for opencl example. """ ###################################################################### +# .. _build-tvm-runtime-on-device: +# # Build TVM Runtime on Device # --------------------------- # -# There're some prerequisites: similar as compiling TVM on your -# local machine, we need build runtime on remote device. +# The first step is to build tvm runtime on the remote device. # # .. note:: # # All instructions in both this section and next section should be # executed on the target device, e.g. Raspberry Pi. And we assume it # has Linux running. +# +# Since we do compilaton on local machine, the remote device is only used +# for runing the generated code. We only need to build tvm runtime on +# the remote device. # -# To get started, clone tvm repo from github. It is important to clone -# the submodules along, with --recursive option (Assuming you are in -# your home directory): -# -# .. code-block:: bash -# -# git clone --recursive https://github.com/dmlc/tvm -# -# .. note:: +# .. code-block:: bash # -# Usually device has limited resources and we only need to build -# runtime. The idea is we will use TVM compiler on the local server -# to compile and upload the compiled program to the device and run -# the device function remotely. -# -# .. code-block:: bash -# -# cd tvm -# cp make/config.mk . -# echo USE_RPC=1>> config.mk -# -# Also make sure that you have set :code:`USE_RPC=1` in your -# :code:`config.mk`. We don't need LLVM when building runtime, so -# :code:`LLVM_CONFIG = llvm-config` in :code:`config.mk` is commented -# out by default. After that, build runtime! -# -# .. code-block:: bash -# -# make runtime +# git clone --recursive https://github.com/dmlc/tvm +# cd tvm +# cp cmake/config.cmake . +# make runtime # # After building runtime successfully, we need to set environment varibles -# in :code:`~/.bashrc` file of yourself account or :code:`/etc/profile` -# of system enviroment variables. Assuming your TVM directory is in -# :code:`~/tvm` and set environment variables below your account. -# -# .. code-block:: bash -# -# vi ~/.bashrc +# in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` +# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM +# directory is in :code:`~/tvm`): # -# We need to edit :code:`~/.bashrc` using :code:`vi ~/.bashrc` and add -# lines below (Assuming your TVM directory is in :code:`~/tvm`): +# .. code-block:: bash # -# .. code-block:: bash -# -# export TVM_HOME=~/tvm -# export PATH=$PATH:$TVM_HOME/lib -# export PYTHONPATH=$PYTHONPATH:$TVM_HOME/python +# export PYTHONPATH=$PYTHONPATH:~/tvm/python # -# To enable updated :code:`~/.bashrc`, execute :code:`source ~/.bashrc`. +# To update the environment variables, execute :code:`source ~/.bashrc`. ###################################################################### # Set Up RPC Server on Device # --------------------------- -# To set up a TVM RPC server on the Raspberry Pi (our remote device), -# we have prepared a one-line script so you only need to run this -# command after following the installation guide to install TVM on -# your device: +# To start an RPC server, run the following command on your remote device +# (Which is Raspberry Pi in our example). # # .. code-block:: bash # # python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090 # -# After executing the command above, if you see these lines below, it means -# the RPC server started successfully on your device. +# If you see the line below, it means the RPC server started +# successfully on your device. # # .. code-block:: bash # -# Loading runtime library /home/YOURNAME/code/tvm/lib/libtvm_runtime.so... exec only # INFO:root:RPCServer: bind to 0.0.0.0:9090 # -# In the following code block, we simply start an RPC server on the -# same machine, for demonstration. This line can be omitted if we -# started an remote server. -# -from __future__ import absolute_import, print_function +# In our webpage building server (the machine that built this tutorial webpage), +# we do not have access to Raspberry Pi. +# So we simply start a "fake" RPC server on the same machine for demonstration. -import tvm -import numpy as np +# These two lines can be omitted if you started an RPC erver on your device. from tvm import rpc -from tvm.contrib import util +server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) ###################################################################### # Declare and Cross Compile Kernel on Local Machine @@ -117,36 +82,41 @@ # # .. note:: # -# Now we back to the local machine, which has a full TVM installed. +# Now we back to the local machine, which has a full TVM installed +# (with LLVM). # # Here we will declare a simple kernel with TVM on the local machine: +import tvm +import numpy as np +from tvm.contrib import util n = tvm.convert(1024) A = tvm.placeholder((n,), name='A') -B = tvm.compute(A.shape, lambda *i: A(*i) + 1.0, name='B') +B = tvm.compute((n,), lambda i: A[i] + 1.0, name='B') s = tvm.create_schedule(B.op) ###################################################################### # Then we cross compile the kernel: -# +# The target should be 'llvm -target=armv7l-linux-gnueabihf' for +# Raspberry Pi 3B, but we use 'llvm' here to make example runable on +# our webpage building server. See the detailed note in the following block. -# the target here should be 'llvm -target=armv7l-none-linux-gnueabihf', -# and we use 'llvm' here to make example run locally, see the detailed -# note in the following block -f = tvm.build(s, [A, B], target='llvm', name='myadd') -# save the lib at local temp folder +func = tvm.build(s, [A, B], target='llvm', name='add_one') +# save the lib at a local temp folder temp = util.tempdir() -path = temp.relpath('mylib.o') -f.save(path) +path = temp.relpath('lib.so') +func.export_library(path) ###################################################################### # .. note:: # -# the argument :code:`target` in :code:`build` should be replaced -# :code:`'llvm'` with the target triple of your device, which might be +# The argument :code:`target` in :code:`build` should be replaced +# with the true target triple of your device, which might be # different for different device. For example, it is -# :code:`'llvm -target=armv7l-none-linux-gnueabihf'` for my Raspberry -# Pi. Here we use :code:`'llvm'` directly to make the tutorial runable. +# :code:`'llvm -target=armv7l-linux-gnueabihf'` for Raspberry Pi 3B and +# :code:`'llvm -target=aarch64-linux-gnu'` for RK3399. +# Here we use :code:`'llvm'` directly to make the tutorial runable on our x86 +# server. # # Usually, you can query the target by execute :code:`gcc -v` on your # device, and look for the line starting with :code:`Target:` @@ -155,8 +125,6 @@ # Besides :code:`-target`, you can also set other compilation options # like: # -# * -mtriple= -# Specify the target triple, same as '-target'. # * -mcpu= # Specify a specific chip in the current architecture to generate code for. By default this is inferred from the target triple and autodetected to the current architecture. # * -mattr=a1,+a2,-a3,... @@ -168,13 +136,6 @@ # llc -mtriple= -mattr=help # # These options are consistent with `llc `_. -# So for my board, to get the best performance, the complete compilation -# option would be: -# -# .. code-block:: bash -# -# llvm -mtriple=armv7l-none-linux-gnueabihf -mcpu=cortex-a53 -mattr=+neon -# # It is recommended to set target triple and feature set to contain specific # feature available, so we can take full advantage of the features of the # board. @@ -184,43 +145,36 @@ ###################################################################### # Run CPU Kernel Remotely by RPC # ------------------------------ -# Here we will show you how to run the kernel on the remote device: -# -# .. note:: -# In order to have this tutorial runs locally to build the nice HTML, we -# start a RPC server on the local machine. You can ignore it if you already -# started the server on the target device. And then change host IP properly. +# We show how to run the cpu kernel on the remote device: -# Can be ignored if you already started the RPC server -server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) host = '0.0.0.0' # Change to your target device IP port = 9090 # connect the remote device remote = rpc.connect(host, port) ###################################################################### -# Here we upload the lib to the remote device, then invoke a device local -# compiler for shared lib and load it into device memory. now `f` is a -# remote module object. +# We upload the lib to the remote device, then invoke a device local +# compiler to relink them. now `func` is a remote module object. + remote.upload(path) -f = remote.load_module('mylib.o') +func = remote.load_module('lib.so') -# create array on the remote device -ctx = remote.cpu(0) +# create arrays on the remote device +ctx = remote.cpu() a = tvm.nd.array(np.random.uniform(size=1024).astype(A.dtype), ctx) b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), ctx) # the function will run on the remote device -f(a, b) +func(a, b) np.testing.assert_equal(b.asnumpy(), a.asnumpy() + 1) ###################################################################### # When you want to evaluate the performance of the kernel on the remote -# device, it is important to avoid overhead of remote function calls. +# device, it is important to avoid the overhead of network. # :code:`time_evaluator` will returns a remote function that runs the # function over number times, measures the cost per run on the remote -# device and returns the measured cost. -# -time_f = f.time_evaluator(f.entry_name, ctx, number=10) +# device and returns the measured cost. Network overhead is excluded. + +time_f = func.time_evaluator(func.entry_name, ctx, number=10) cost = time_f(a, b).mean print('%g secs/op' % cost) @@ -228,9 +182,7 @@ # Run OpenCL Kernel Remotely by RPC # --------------------------------- # As for remote OpenCL devices, the workflow is almost the same as above. -# You can define the kernel, upload files, and run by RPC. The files -# include host object, kernel source code and module meta file. We rely -# on remote compiler to re-link them. +# You can define the kernel, upload files, and run by RPC. # # .. note:: # @@ -239,57 +191,37 @@ # to setup the RK3399 OS and OpenCL driver. # # The target_host should be 'llvm -target=aarch64-linux-gnu'. -# But here we set 'llvm' to enable this tutorial to run locally. +# But here we set 'llvm' to make this tutorial runable on our x86 server. # -# Also we need to build the runtime with the flag `USE_OPENCL=1` to -# build the kernel (different from cpu, we need bind axis for OpenCL) +# Also we need to build the runtime with OpenCL enabled in rk3399 board. +# You need to modify `set(USE_OPENCL OFF)` to `set(USE_OPENCL_ON)` in `config.cmake`, +# and execute :code:`make runtime -j4` # -# The following functions shows how we can deploy CL -def deploy_cl(): +# The following function shows how we run OpenCL kernel remotely +def run_opencl(): s = tvm.create_schedule(B.op) xo, xi = s[B].split(B.op.axis[0], factor=32) s[B].bind(xo, tvm.thread_axis("blockIdx.x")) s[B].bind(xi, tvm.thread_axis("threadIdx.x")) - f = tvm.build(s, [A, B], "opencl", target_host="llvm", name="myadd") + func = tvm.build(s, [A, B], "opencl", target_host="llvm", name="add_cl") - # save files - path_o = temp.relpath("myadd.o") - path_cl = temp.relpath("myadd.cl") - path_json = temp.relpath("myadd.tvm_meta.json") - f.save(path_o) - f.imported_modules[0].save(path_cl) - - # upload files - remote.upload(path_o) - remote.upload(path_cl) - remote.upload(path_json) - - # load files on remote device - fhost = remote.load_module("myadd.o") - fdev = remote.load_module("myadd.cl") - fhost.import_module(fdev) + # export and upload + path = temp.relpath('lib_cl.so') + func.export_library(path) + remote.upload(path) + func = remote.load_module('lib_cl.so') # run - ctx = remote.cl(0) + ctx = remote.cl() a = tvm.nd.array(np.random.uniform(size=1024).astype(A.dtype), ctx) b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), ctx) - fhost(a, b) + func(a, b) np.testing.assert_equal(b.asnumpy(), a.asnumpy() + 1) - ##################################################################### -# Instead of uploading files separately, there is a more convinient way. -# You can export libraray as a tar ball. -# The following functions shows how we can deploy by tar ball -def deploy_cl_by_tar(): - path_tar = temp.relpath("myadd.tar") - f.export_library(path_tar) - remote.upload(path_tar) - fhost = remote.load_module("myadd.tar") - fhost(a, b) - np.testing.assert_equal(b.asnumpy(), a.asnumpy() + 1) -# terminate the server after experiment +# Terminate the "fake" server after experiment +# You can delete this line if you started "real" rpc server on your remote device server.terminate() ###################################################################### @@ -302,3 +234,4 @@ def deploy_cl_by_tar(): # - Set up target device configuration to cross compile kernel on the # local machine. # - Upload and run the kernel remotely by RPC API. + diff --git a/tutorials/nnvm/README.txt b/tutorials/nnvm/README.txt index d6629821c2ef..772953ce96ac 100644 --- a/tutorials/nnvm/README.txt +++ b/tutorials/nnvm/README.txt @@ -1,2 +1,4 @@ +.. _tutorial-nnvm: + Compile Deep Learning Models ---------------------------- diff --git a/tutorials/nnvm/deploy_model_on_mali_gpu.py b/tutorials/nnvm/deploy_model_on_mali_gpu.py index 51caf8dcbd26..70f2f892c285 100644 --- a/tutorials/nnvm/deploy_model_on_mali_gpu.py +++ b/tutorials/nnvm/deploy_model_on_mali_gpu.py @@ -6,14 +6,10 @@ **Author**: `Lianmin Zheng `_, `Ziheng Jiang `_ This is an example of using NNVM to compile a ResNet model and -deploy it on Firefly-RK3399 with ARM Mali GPU. We will use the +deploy it on Firefly-RK3399 with ARM Mali GPU. We will use the Mali-T860 MP4 GPU on this board to accelerate the inference. - -This tutorial is based on the tutorial for deploying on Raspberry Pi by `Ziheng Jiang `_. -Great thanks to the original author, I only do several lines of modification. - -To begin with, we import nnvm (for compilation) and TVM (for deployment). """ + import tvm import nnvm.compiler import nnvm.testing @@ -24,92 +20,81 @@ # Build TVM Runtime on Device # --------------------------- # -# There're some prerequisites: we need build tvm runtime and set up -# a RPC server on remote device. -# -# To get started, clone tvm repo from github. It is important to clone -# the submodules along, with --recursive option (Assuming you are in -# your home directory): -# -# .. code-block:: bash -# -# git clone --recursive https://github.com/dmlc/tvm +# The first step is to build tvm runtime on the remote device. # # .. note:: # -# Usually device has limited resources and we only need to build -# runtime. The idea is we will use TVM compiler on the local server -# to compile and upload the compiled program to the device and run -# the device function remotely. -# -# .. code-block:: bash -# -# make runtime +# All instructions in both this section and next section should be +# executed on the target device, e.g. Raspberry Pi. And we assume it +# has Linux running. +# +# Since we do compilaton on local machine, the remote device is only used +# for runing the generated code. We only need to build tvm runtime on +# the remote device. Make sure you have opencl driver in your board. +# You can refer to `tutorial `_ +# to setup OS and opencl driver for rk3399. # -# After success of buildind runtime, we need set environment varibles -# in :code:`~/.bashrc` file of yourself account or :code:`/etc/profile` -# of system enviroment variables. Assuming your TVM directory is in -# :code:`~/tvm` and set environment variables below your account. +# .. code-block:: bash # -# .. code-block:: bash +# git clone --recursive https://github.com/dmlc/tvm +# cd tvm +# cp cmake/config.cmake . +# sed -i "s/USE_OPENCL OFF/USE_OPENCL ON/" config.cmake +# make runtime # -# vi ~/.bashrc +# After building runtime successfully, we need to set environment varibles +# in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` +# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM +# directory is in :code:`~/tvm`): # -# We need edit :code:`~/.bashrc` using :code:`vi ~/.bashrc` and add -# lines below (Assuming your TVM directory is in :code:`~/tvm`): +# .. code-block:: bash # -# .. code-block:: bash +# export PYTHONPATH=$PYTHONPATH:~/tvm/python # -# export TVM_HOME=~/tvm -# export PATH=$PATH:$TVM_HOME/lib -# export PYTHONPATH=$PYTHONPATH:$TVM_HOME/python -# -# To enable updated :code:`~/.bashrc`, execute :code:`source ~/.bashrc`. +# To update the environment variables, execute :code:`source ~/.bashrc`. ###################################################################### # Set Up RPC Server on Device # --------------------------- -# To set up a TVM RPC server on the your ARM device (our remote device), -# we have prepared a one-line script so you only need to run this -# command after following the installation guide to install TVM on -# your device: +# To start an RPC server, run the following command on your remote device +# (Which is Raspberry Pi in our example). # # .. code-block:: bash # # python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090 # -# After executing command above, if you see these lines below, it's -# successful to start RPC server on your device. +# If you see the line below, it means the RPC server started +# successfully on your device. # # .. code-block:: bash # -# Loading runtime library /home/YOURNAME/code/tvm/lib/libtvm_runtime.so... exec only # INFO:root:RPCServer: bind to 0.0.0.0:9090 # - -###################################################################### -# For demonstration, we simply start an RPC server on the same machine, -# if :code:`use_mali` is False. If you have set up the remote -# environment, please change the three lines below: change the -# :code:`use_mali` to True, also change the :code:`host` and :code:`port` +# In our webpage building server (the machine that built this tutorial webpage), +# we do not have access to Raspberry Pi. +# So we simply start a "fake" RPC server on the same machine for demonstration. +# If you have set up the remote environment, please change the three lines below: +# change the :code:`use_mali` to True, also change the :code:`host` and :code:`port` # with your device's host address and port number. use_mali = False -host = '10.42.0.96' +host = '10.77.1.xxx' port = 9090 if not use_mali: # run server locally host = 'localhost' - port = 9095 + port = 9091 server = rpc.Server(host=host, port=port, use_popen=True) ###################################################################### # Prepare the Pretrained Model # ---------------------------- -# Back to the host machine, firstly, we need to download a MXNet Gluon -# ResNet model from model zoo, which is pretrained on ImageNet. You -# can found more details about this part at `Compile MXNet Models` +# Back to the host machine, which should have a full TVM installed (with LLVM). +# +# We will use pre-trained model from +# `MXNet Gluon model zoo `_. +# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet` from mxnet.gluon.model_zoo.vision import get_model from mxnet.gluon.utils import download @@ -135,7 +120,6 @@ def transform_image(image): x = transform_image(image) - ###################################################################### # synset is used to transform the label from number of ImageNet class to # the word human can understand. @@ -143,6 +127,7 @@ def transform_image(image): '4d0b62f3d01426887599d4f7ede23ee5/raw/', '596b27d23537e5a1b5751d2b0481ef172f58b539/', 'imagenet1000_clsid_to_human.txt']) + synset_name = 'synset.txt' download(synset_url, synset_name) with open(synset_name) as f: @@ -176,21 +161,25 @@ def transform_image(image): # triplet for host ARM device by setting the parameter :code:`target_host`. ###################################################################### -# If we run the example locally for demonstration, we can simply set -# it as :code:`llvm`. If to run it on the ARM device, you need to specify -# its instruction set. Here is the option I use for my Firefly-RK3399. +# If we run the example on our x86 server for demonstration, we can simply +# set it as :code:`llvm`. If running it on the RK3399, we need to +# specify its instruction set. if use_mali: - target_host = "llvm -target=aarch64-linux-gnu -mattr=+neon" + # Here is the setting for my rk3399 board + # If you don't use rk3399, you can query your target triple by + # execute `gcc -v` on your board. + target_host = "llvm -target=aarch64-linux-gnu" target = tvm.target.mali() else: target_host = "llvm" - target = tvm.target.cuda() + target = "llvm" # set target as `tvm.target.mali` instead of 'opencl' to enable # target-specified optimization -graph, lib, params = nnvm.compiler.build(net, target=target, - shape={"data": data_shape}, params=params, target_host=target_host) +with nnvm.compiler.build_config(opt_level=3): + graph, lib, params = nnvm.compiler.build(net, target=target, + shape={"data": data_shape}, params=params, target_host=target_host) # After `nnvm.compiler.build`, you will get three return values: graph, # library and the new parameter, since we do some optimization that will @@ -198,7 +187,7 @@ def transform_image(image): # Save the library at local temporary directory. tmp = util.tempdir() -lib_fname = tmp.relpath('net.tar') +lib_fname = tmp.relpath('net.so') lib.export_library(lib_fname) ###################################################################### @@ -212,9 +201,9 @@ def transform_image(image): # upload the library to remote device and load it remote.upload(lib_fname) -rlib = remote.load_module('net.tar') +rlib = remote.load_module('net.so') -ctx = remote.cl(0) if use_mali else remote.gpu(0) +ctx = remote.cl(0) if use_mali else remote.cpu(0) # upload the parameter rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} @@ -235,3 +224,4 @@ def transform_image(image): if not use_mali: # terminate the local server server.terminate() + diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index 37354e7a3363..92fb4bad70d5 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -7,9 +7,8 @@ This is an example of using NNVM to compile a ResNet model and deploy it on raspberry pi. - -To begin with, we import nnvm(for compilation) and TVM(for deployment). """ + import tvm import nnvm.compiler import nnvm.testing @@ -20,78 +19,62 @@ # Build TVM Runtime on Device # --------------------------- # -# There're some prerequisites: we need build tvm runtime and set up -# a RPC server on remote device. -# -# To get started, clone tvm repo from github. It is important to clone -# the submodules along, with --recursive option (Assuming you are in -# your home directory): -# -# .. code-block:: bash -# -# git clone --recursive https://github.com/dmlc/tvm +# The first step is to build tvm runtime on the remote device. # # .. note:: # -# Usually device has limited resources and we only need to build -# runtime. The idea is we will use TVM compiler on the local server -# to compile and upload the compiled program to the device and run -# the device function remotely. +# All instructions in both this section and next section should be +# executed on the target device, e.g. Raspberry Pi. And we assume it +# has Linux running. +# +# Since we do compilaton on local machine, the remote device is only used +# for runing the generated code. We only need to build tvm runtime on +# the remote device. # -# .. code-block:: bash -# -# make runtime -# -# After success of buildind runtime, we need set environment varibles -# in :code:`~/.bashrc` file of yourself account or :code:`/etc/profile` -# of system enviroment variables. Assuming your TVM directory is in -# :code:`~/tvm` and set environment variables below your account. -# -# .. code-block:: bash +# .. code-block:: bash # -# vi ~/.bashrc +# git clone --recursive https://github.com/dmlc/tvm +# cd tvm +# cp cmake/config.cmake . +# make runtime # -# We need edit :code:`~/.bashrc` using :code:`vi ~/.bashrc` and add -# lines below (Assuming your TVM directory is in :code:`~/tvm`): +# After building runtime successfully, we need to set environment varibles +# in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` +# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM +# directory is in :code:`~/tvm`): # -# .. code-block:: bash +# .. code-block:: bash # -# export TVM_HOME=~/tvm -# export PATH=$PATH:$TVM_HOME/lib -# export PYTHONPATH=$PYTHONPATH:$TVM_HOME/python +# export PYTHONPATH=$PYTHONPATH:~/tvm/python # -# To enable updated :code:`~/.bashrc`, execute :code:`source ~/.bashrc`. +# To update the environment variables, execute :code:`source ~/.bashrc`. ###################################################################### # Set Up RPC Server on Device # --------------------------- -# To set up a TVM RPC server on the Raspberry Pi (our remote device), -# we have prepared a one-line script so you only need to run this -# command after following the installation guide to install TVM on -# your device: +# To start an RPC server, run the following command on your remote device +# (Which is Raspberry Pi in our example). # # .. code-block:: bash # # python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090 # -# After executing command above, if you see these lines below, it's -# successful to start RPC server on your device. +# If you see the line below, it means the RPC server started +# successfully on your device. # # .. code-block:: bash # -# Loading runtime library /home/YOURNAME/code/tvm/lib/libtvm_runtime.so... exec only # INFO:root:RPCServer: bind to 0.0.0.0:9090 - - -###################################################################### -# For demonstration, we simply start an RPC server on the same machine, -# if :code:`use_rasp` is False. If you have set up the remote -# environment, please change the three lines below: change the -# :code:`use_rasp` to True, also change the :code:`host` and :code:`port` +# +# In our webpage building server (the machine that built this tutorial webpage), +# we do not have access to Raspberry Pi. +# So we simply start a "fake" RPC server on the same machine for demonstration. +# If you have set up the remote environment, please change the three lines below: +# change the :code:`use_rasp` to True, also change the :code:`host` and :code:`port` # with your device's host address and port number. use_rasp = False -host = 'rasp0' +host = '10.77.1.xxx' port = 9090 if not use_rasp: @@ -103,16 +86,18 @@ ###################################################################### # Prepare the Pretrained Model # ---------------------------- -# Back to the host machine, firstly, we need to download a MXNet Gluon -# ResNet model from model zoo, which is pretrained on ImageNet. You -# can found more details about this part at `Compile MXNet Models` +# Back to the host machine, which should have a full TVM installed (with LLVM). +# +# We will use pre-trained model from +# `MXNet Gluon model zoo `_. +# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet` from mxnet.gluon.model_zoo.vision import get_model from mxnet.gluon.utils import download from PIL import Image import numpy as np -# only one line to get the model +# one line to get the model block = get_model('resnet18_v1', pretrained=True) ###################################################################### @@ -131,7 +116,6 @@ def transform_image(image): x = transform_image(image) - ###################################################################### # synset is used to transform the label from number of ImageNet class to # the word human can understand. @@ -173,29 +157,30 @@ def transform_image(image): # will lead to very different performance. ###################################################################### -# If we run the example locally for demonstration, we can simply set -# it as :code:`llvm`. If to run it on the Raspberry Pi, you need to -# specify its instruction set. Here is the option I use for my Raspberry -# Pi, which has been proved as a good compilation configuration. +# If we run the example on our x86 server for demonstration, we can simply +# set it as :code:`llvm`. If running it on the Raspberry Pi, we need to +# specify its instruction set. We also need to add :code:`-device=arm_cpu` +# to the target string to enable optimizations for arm_cpu. if use_rasp: - target = tvm.target.rasp() + target = tvm.target.arm_cpu('rasp3b') + # The above line is a simple form of + # target = tvm.target.create('llvm -devcie=arm_cpu -target=armv7l-linux-gnueabihf') else: target = tvm.target.create('llvm') -graph, lib, params = nnvm.compiler.build( - net, target, shape={"data": data_shape}, params=params) +with nnvm.compiler.build_config(opt_level=3): + graph, lib, params = nnvm.compiler.build( + net, target, shape={"data": data_shape}, params=params) # After `nnvm.compiler.build`, you will get three return values: graph, # library and the new parameter, since we do some optimization that will # change the parameters but keep the result of model as the same. - # Save the library at local temporary directory. tmp = util.tempdir() -lib_fname = tmp.relpath('net.o') -lib.save(lib_fname) - +lib_fname = tmp.relpath('net.so') +lib.export_library(lib_fname) ###################################################################### # Deploy the Model Remotely by RPC @@ -208,10 +193,10 @@ def transform_image(image): # upload the library to remote device and load it remote.upload(lib_fname) -rlib = remote.load_module('net.o') +rlib = remote.load_module('net.so') -ctx = remote.cpu(0) # upload the parameter +ctx = remote.cpu(0) rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} # create the remote runtime module @@ -231,3 +216,4 @@ def transform_image(image): if not use_rasp: # terminate the local server server.terminate() + diff --git a/tutorials/nnvm/from_mxnet.py b/tutorials/nnvm/from_mxnet.py index 5ea6acbd3a9b..cce3bc37126a 100644 --- a/tutorials/nnvm/from_mxnet.py +++ b/tutorials/nnvm/from_mxnet.py @@ -1,4 +1,6 @@ """ +.. _tutorial-from-mxnet: + Compile MXNet Models ==================== **Author**: `Joshua Z. Zhang `_ diff --git a/tutorials/nnvm/imagenet_inference_gpu.py b/tutorials/nnvm/imagenet_inference_gpu.py deleted file mode 100644 index 9179dedfb6fc..000000000000 --- a/tutorials/nnvm/imagenet_inference_gpu.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Compile GPU Inference -===================== -**Author**: `Yuwei Hu `_ - -This is an example of using NNVM to compile MobileNet/ResNet model and deploy its inference on GPU. - -To begin with, we import nnvm(for compilation) and TVM(for deployment). -""" -import tvm -import numpy as np -from tvm.contrib import nvcc, graph_runtime -import nnvm.compiler -import nnvm.testing - -###################################################################### -# Register the NVCC Compiler Option -# --------------------------------- -# NNVM optimizes the graph and relies on TVM to generate fast GPU code. -# To get the maximum performance, we need to enable nvcc's compiler hook. -# This usually gives better performance than nvrtc mode. - -@tvm.register_func("tvm_callback_cuda_compile", override=True) -def tvm_callback_cuda_compile(code): - ptx = nvcc.compile_cuda(code, target="ptx") - return ptx - -###################################################################### -# Prepare the Benchmark -# --------------------- -# We construct a standard imagenet inference benchmark. -# NNVM needs two things to compile a deep learning model: -# -# - net: the graph representation of the computation -# - params: a dictionary of str to parameters -# -# We use nnvm's testing utility to produce the model description and random parameters -# so that the example does not depend on a specific front-end framework. -# -# .. note:: -# -# In a typical workflow, we can get this pair from :any:`nnvm.frontend` -# -target = "cuda" -ctx = tvm.gpu(0) -batch_size = 1 -num_classes = 1000 -image_shape = (3, 224, 224) -data_shape = (batch_size,) + image_shape -out_shape = (batch_size, num_classes) -# To use ResNet to do inference, run the following instead -#net, params = nnvm.testing.resnet.get_workload( -# batch_size=1, image_shape=image_shape) -net, params = nnvm.testing.mobilenet.get_workload( - batch_size=1, image_shape=image_shape) - -###################################################################### -# Compile the Graph -# ----------------- -# To compile the graph, we call the build function with the graph -# configuration and parameters. -# When parameters are provided, NNVM will pre-compute certain part of the graph if possible (e.g. simplify batch normalization to scale shift), -# and return the updated parameters. - -graph, lib, params = nnvm.compiler.build( - net, target, shape={"data": data_shape}, params=params) - - -###################################################################### -# Run the Compiled Module -# ----------------------- -# -# To deploy the module, we call :any:`tvm.contrib.graph_runtime.create` passing in the graph, the lib, and context. -# Thanks to TVM, we can deploy the compiled module to many platforms and languages. -# The deployment module is designed to contain minimum dependencies. -# This example runs on the same machine. -# -# Note that the code below no longer depends on NNVM, and only relies TVM's runtime to run(deploy). -data = np.random.uniform(-1, 1, size=data_shape).astype("float32") -module = graph_runtime.create(graph, lib, ctx) -# set input -module.set_input(**params) -module.set_input("data", data) -# run -module.run() -# get output -out = module.get_output(0, tvm.nd.empty(out_shape)) -# convert to numpy -out.asnumpy() diff --git a/tutorials/nnvm_quick_start.py b/tutorials/nnvm_quick_start.py index 563d71b5e179..c1eef7ae04e6 100644 --- a/tutorials/nnvm_quick_start.py +++ b/tutorials/nnvm_quick_start.py @@ -6,9 +6,8 @@ **Author**: `Yao Wang `_ This example shows how to build a neural network with NNVM python frontend and -generate runtime library for Nvidia GPU and Raspberry Pi with TVM. -To run this notebook, you need to install tvm and nnvm. -Notice that you need to build tvm with cuda and llvm. +generate runtime library for Nvidia GPU with TVM. +Notice that you need to build TVM with cuda and llvm enabled. """ ###################################################################### @@ -22,10 +21,13 @@ # # In this tutorial, we'll choose cuda and llvm as target backends. # To begin with, let's import NNVM and TVM. -import tvm + +import numpy as np + import nnvm.compiler import nnvm.testing - +import tvm +from tvm.contrib import graph_runtime ###################################################################### # Define Neural Network in NNVM @@ -33,7 +35,8 @@ # First, let's define a neural network with nnvm python frontend. # For simplicity, we'll use pre-defined resnet-18 network in NNVM. # Parameters are initialized with Xavier initializer. -# NNVM also supports other model formats such as MXNet, CoreML and ONNX. +# NNVM also supports other model formats such as MXNet, CoreML, ONNX and +# Tensorflow. # # In this tutorial, we assume we will do inference on our device # and the batch size is set to be 1. Input images are RGB color @@ -46,7 +49,8 @@ data_shape = (batch_size,) + image_shape out_shape = (batch_size, num_class) -net, params = nnvm.testing.resnet.get_workload(batch_size=batch_size, image_shape=image_shape) +net, params = nnvm.testing.resnet.get_workload(layers=18, + batch_size=batch_size, image_shape=image_shape) print(net.debug_str()) ###################################################################### @@ -54,10 +58,8 @@ # ----------- # Next step is to compile the model using the NNVM/TVM pipeline. # Users can specify the optimization level of the compilation. -# Currently this value can be 0 to 2, which corresponds to -# "SimplifyInference", "OpFusion" and "PrecomputePrune" respectively. -# In this example we set optimization level to be 0 -# and use Raspberry Pi as compile target. +# Currently this value can be 0 to 3. The optimization passes include +# operator fusion, pre-computation, layout transformation and so on. # # :any:`nnvm.compiler.build` returns three components: the execution graph in # json format, the TVM module library of compiled functions specifically @@ -70,22 +72,48 @@ # first does a number of graph-level optimizations, e.g. pruning, fusing, etc., # then registers the operators (i.e. the nodes of the optmized graphs) to # TVM implementations to generate a `tvm.module`. -# To generate the module library, TVM will first transfer the HLO IR into the lower -# intrinsic IR of the specified target backend, which is CUDA in this example. -# Then the machine code will be generated as the module library. +# To generate the module library, TVM will first transfer the High level IR +# into the lower intrinsic IR of the specified target backend, which is CUDA +# in this example. Then the machine code will be generated as the module library. -opt_level = 0 +opt_level = 3 target = tvm.target.cuda() with nnvm.compiler.build_config(opt_level=opt_level): graph, lib, params = nnvm.compiler.build( net, target, shape={"data": data_shape}, params=params) +##################################################################### +# Run the generate library +# ------------------------ +# Now we can create graph runtime and run the module on Nvidia GPU. + +# create random input +ctx = tvm.gpu() +data = np.random.uniform(-1, 1, size=data_shape).astype("float32") +# create module +module = graph_runtime.create(graph, lib, ctx) +# set input and parameters +module.set_input("data", data) +module.set_input(**params) +# run +module.run() +# get output +out = module.get_output(0, tvm.nd.empty(out_shape)) +# convert to numpy +out.asnumpy() + +# Print first 10 elements of output +print(out.asnumpy().flatten()[0:10]) + ###################################################################### -# Save Compiled Module -# ---------------------------- -# After compilation, we can save the graph, lib and params into separate files -# and deploy them to Nvidia GPU. +# Save and Load Compiled Module +# ----------------------------- +# We can also save the graph, lib and parameters into files and load them +# back in deploment environment. +#################################################### + +# save the graph, lib and params into separate files from tvm.contrib import util temp = util.tempdir() @@ -97,95 +125,17 @@ fo.write(nnvm.compiler.save_param_dict(params)) print(temp.listdir()) -###################################################################### -# Deploy locally to Nvidia GPU -# ------------------------------ -# Now we can load the module back. +#################################################### -import numpy as np -from tvm.contrib import graph_runtime - -loaded_lib = tvm.module.load(path_lib) +# load the module back. loaded_json = open(temp.relpath("deploy_graph.json")).read() +loaded_lib = tvm.module.load(path_lib) loaded_params = bytearray(open(temp.relpath("deploy_param.params"), "rb").read()) +input_data = tvm.nd.array(np.random.uniform(size=data_shape).astype("float32")) + module = graph_runtime.create(loaded_json, loaded_lib, tvm.gpu(0)) module.load_params(loaded_params) - -input_data = tvm.nd.array(np.random.uniform(size=data_shape).astype("float32")) module.run(data=input_data) -out = module.get_output(0, out=tvm.nd.empty(out_shape)) -# Print first 10 elements of output -print(out.asnumpy()[0][0:10]) - -###################################################################### -# Compile and Deploy the Model to Raspberry Pi Remotely with RPC -# -------------------------------------------------------------- -# Following the steps above, we can also compile the model for Raspberry Pi. -# TVM provides rpc module to help with remote deploying. -# -# For demonstration, we simply start an RPC server on the same machine, -# if :code:`use_rasp` is False. If you have set up the remote -# environment, please change the three lines below: change the -# :code:`use_rasp` to True, also change the host and port with your -# device's host address and port number. - -# If we run the example locally for demonstration, we can simply set the -# compilation target as `llvm`. -# To run it on the Raspberry Pi, you need to specify its instruction set. -# `llvm -target=armv7l-none-linux-gnueabihf -mcpu=cortex-a53 -mattr=+neon` -# is the recommended compilation configuration, thanks to Ziheng's work. - -from tvm import rpc - -use_rasp = False -host = 'rasp0' -port = 9090 - -if not use_rasp: - # run server locally - host = 'localhost' - port = 9099 - server = rpc.Server(host=host, port=port, use_popen=True) - -# compile and save model library -if use_rasp: - target = "llvm -target=armv7l-none-linux-gnueabihf -mcpu=cortex-a53 -mattr=+neon" -else: - target = "llvm" -# use `with tvm.target.rasp` for some target-specified optimization -with tvm.target.rasp(): - graph, lib, params = nnvm.compiler.build( - net, target, shape={"data": data_shape}, params=params) -temp = util.tempdir() -path_lib = temp.relpath("deploy_lib_rasp.o") -lib.save(path_lib) - -# connect the server -remote = rpc.connect(host, port) - -# upload the library to remote device and load it -remote.upload(path_lib) -rlib = remote.load_module('deploy_lib_rasp.o') - -ctx = remote.cpu(0) -# upload the parameter -rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} - -# create the remote runtime module -module = graph_runtime.create(graph, rlib, ctx) -# set parameter -module.set_input(**rparams) -# set input data -input_data = np.random.uniform(size=data_shape) -module.set_input('data', tvm.nd.array(input_data.astype('float32'))) -# run -module.run() - -out = module.get_output(0, out=tvm.nd.empty(out_shape, ctx=ctx)) -# Print first 10 elements of output -print(out.asnumpy()[0][0:10]) +out = module.get_output(0, out=tvm.nd.empty(out_shape)) -if not use_rasp: - # terminate the local server - server.terminate() From d71d137873c82b5b096252b053945406b7304bf3 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 01:50:06 -0700 Subject: [PATCH 05/76] self use script --- script/consistency_check.py | 95 ++++++++++++ script/tune_all_mobile.py | 44 ++++++ script/tune_cuda_topi.py | 41 +++++ script/tune_nnvm.py | 292 ++++++++++++++++++++++++++++++++++++ script/util.py | 61 ++++++++ 5 files changed, 533 insertions(+) create mode 100644 script/consistency_check.py create mode 100644 script/tune_all_mobile.py create mode 100644 script/tune_cuda_topi.py create mode 100644 script/tune_nnvm.py create mode 100644 script/util.py diff --git a/script/consistency_check.py b/script/consistency_check.py new file mode 100644 index 000000000000..976725d6f56e --- /dev/null +++ b/script/consistency_check.py @@ -0,0 +1,95 @@ +"""Extract tunable operators from nnvm graph and tune them""" + +import argparse +import logging +import time +import json + +import numpy as np + +import nnvm.testing +import nnvm.compiler +import tvm +from tvm.contrib import util +from tvm import autotvm +import tvm.contrib.graph_runtime as runtime + +from tune_nnvm import get_network, get_target, get_tuning_option, tune_tasks + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--network", type=str, default='resnet-18') + parser.add_argument("--target", type=str, default='rpi3b-cpu') + parser.add_argument("--target-host", type=str) + parser.add_argument("--n-trial", type=int, default=10) + parser.add_argument("--seed", type=int, default=0x93) + parser.add_argument("--cache-file", type=str) + parser.add_argument("--mode", type=str, default='infer') + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + dtype = 'float32' + + args.cache_file = args.cache_file or args.network + "." + args.target + ".tsv" + + # device related + device_key, target, target_host = get_target(args.target) + tuning_option, n_times = get_tuning_option(device_key, args) + + # network + net, params, shape, out_shape = get_network(args.network, batch_size=1) + tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, + symbols=tuning_option['tuning_symbols'], + target=target, target_host=target_host) + + task = tasks[0] + task = autotvm.task.create(task.name, task.args, task.target, task.target_host, 'vanilla') + + measure_option = autotvm.measure_option(mode='local' if tuning_option['device_key'] == 'local' else 'rpc', + number=tuning_option['number'], + repeat=3, + rpc_device_key=tuning_option['device_key'], + parallel_num=tuning_option['parallel_num'], + timeout=tuning_option['timeout'], + rpc_timeout=tuning_option['rpc_timeout'], + use_ndk=tuning_option.get('use_ndk', False)) + + if args.mode == 'tune': + print(task.config_space) + print(task.workload) + tuner = autotvm.tuner.XGBTuner(task) + tuner.tune(n_trial=1000, + measure_option=measure_option, + callbacks=[autotvm.callback.log_to_file('cache.tsv')]) + + dispatch_context = autotvm.apply_history_best("cache.tsv") + config = dispatch_context.query(task.target, task.workload) + + measure_batch = autotvm.measure.create_measure_batch(task, measure_option) + + gflops = [] + n_trial = 1000000 + tmp = [] + ct = {} + for i in range(n_trial // tuning_option['parallel_num']): + inputs = [autotvm.MeasureInput(task.target, task, config)] * tuning_option['parallel_num'] + results = measure_batch(inputs) + + for res in results: + if res.error_no != 0: + pass + else: + gflops.append(task.flop / np.mean(res.costs) / 1e9) + tmp.append(gflops[-1]) + +# url = res.timestamp +# if url not in ct: +# ct[url] = 0 +# ct[url] += res.all_cost + + if len(tmp) >= 5: + print("[" + " ".join(["%.2f" % x for x in tmp]) + "]") + print("var: %.4f min: %.2f max: %.2f\n" % (np.std(gflops) / np.mean(gflops), np.min(gflops), np.max(gflops))) + tmp = [] + + diff --git a/script/tune_all_mobile.py b/script/tune_all_mobile.py new file mode 100644 index 000000000000..92ca7e17d85c --- /dev/null +++ b/script/tune_all_mobile.py @@ -0,0 +1,44 @@ +import sys +import argparse +import os +import multiprocessing + +networks = [ + 'squeezenet', + 'mobilenet', + 'resnet-18', + 'vgg-16', +] + +targets = [ + 'rk3399-cpu', #'rk3399-gpu', + 'rpi3b-cpu', #'pynq-cpu', + 'hikey960-cpu', #'hikey960-gpu', + 'mate10pro-cpu', #'mate10pro-gpu', + 'pixel2-cpu', #'rk3399-gpu', + 'p20pro-cpu', #'pynq-cpu', +] + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--target", type=str, default='') + parser.add_argument("--mode", type=str, default='tune') + args = parser.parse_args() + + targets = list(filter(lambda x: args.target in x, targets)) + + cmds = [] + for network in networks: + for target in targets: + cmd = "python3 tune_nnvm.py --network %s --target %s --cache-file %s --n-trial 1000 --mode %s" \ + % (network, target, network + "." + target + ".log", args.mode) + + print(cmd) + if args.mode == 'infer': + cmds.append(cmd) + else: + os.system(cmd) + + pool = multiprocessing.Pool() + pool.map(os.system, cmds) + diff --git a/script/tune_cuda_topi.py b/script/tune_cuda_topi.py new file mode 100644 index 000000000000..4e7acc5aee67 --- /dev/null +++ b/script/tune_cuda_topi.py @@ -0,0 +1,41 @@ +import tvm +import logging +import topi +from tvm import autotvm + +# task function +def conv2d(): + A = tvm.placeholder((1, 512, 7, 7), 'float32') + B = tvm.placeholder((512, 512, 3, 3), 'float32') + D = topi.nn.conv2d(A, B, (1,1), (1,1), 'NCHW', 'float32') + s = topi.generic.schedule_conv2d_nchw(D) + + return s, [A, B, D] + + +# create task +task = autotvm.task.create(conv2d, + args=(), + target='cuda', + template_key='vanilla') +print(task.config_space) + +# begin tuing +logging.basicConfig(level=logging.INFO) +measure_option = autotvm.measure_option(mode='local', + number=10, + parallel_num=8, + timeout=20) +tuner = autotvm.tuner.XGBTuner(task) +tuner.tune(n_trial=100, + measure_option=measure_option, + callbacks=[autotvm.callback.log_to_file('cache.tsv')]) + +# find history best +with autotvm.apply_history_best('cache.tsv'): + with tvm.target.create("cuda"): + s, arg_bufs = conv2d() + func = tvm.build(s, arg_bufs) + +print(tvm.lower(s, arg_bufs, simple_mode=True)) + diff --git a/script/tune_nnvm.py b/script/tune_nnvm.py new file mode 100644 index 000000000000..ebc75f029810 --- /dev/null +++ b/script/tune_nnvm.py @@ -0,0 +1,292 @@ +"""Extract tunable operators from nnvm graph and tune them""" + +import argparse +import logging +import time +import os + +import numpy as np + +import nnvm.testing +import nnvm.compiler +import tvm +from tvm import autotvm +from tvm.contrib import util +import tvm.contrib.graph_runtime as runtime + +from util import save_curve, VisLogger + +def tune_tasks(tasks, tuning_option): + for i, task in enumerate(tasks): + print("========== Task %d/%d ==========" % (i + 1, len(tasks))) + print("Workload: ", task.workload) + print("GFLOP:", task.flop) + print(task.config_space) + with open("status", "a") as fout: + fout.write("\t".join([tuning_option['log_filename'], + str(i+1), str(len(tasks)), time.asctime()]) +'\n') + + if tuning_option['device_key'] =='local': + mode ='local' + else: + mode ='rpc' + + measure_option = autotvm.measure_option(mode=mode, + repeat=3, + number=tuning_option['number'], + rpc_device_key=tuning_option['device_key'], + parallel_num=tuning_option['parallel_num'], + timeout=tuning_option['timeout'], + rpc_timeout=tuning_option['rpc_timeout'], + use_ndk=tuning_option.get('use_ndk', False)) + + monitor = autotvm.callback.Monitor() + visloger = VisLogger(task, args.target, tuning_option['tuner'], + 'vanilla', tuning_option['n_trial'], "vis/" + tuning_option['device_key'] + "/vis.tsv") + + # tuning + if tuning_option['tuner'] =='xgb-rank': + tuner = autotvm.tuner.XGBTuner(task, loss_type='rank') + elif tuning_option['tuner'] =='ga': + tuner = autotvm.tuner.GATuner(task, pop_size=50) + else: + raise RuntimeError("Invalid tuner") + + if tuning_option['transfer_learning']: + if os.path.isfile(tuning_option['log_filename']): + tuner.load_history(autotvm.record.load_from_file(tuning_option['log_filename'])) + + tuner.tune(n_trial=min(tuning_option['n_trial'], len(task.config_space)), + early_stopping=tuning_option['early_stopping'], + measure_option=measure_option, + callbacks=[autotvm.callback.log_to_file(tuning_option['log_filename']), + monitor, visloger]) + + # write log + device = tuning_option['device_key'] + backend = str(task.target).split(" ")[0] + workload = task.workload + tuner = tuning_option['tuner'] + template_key = task.config_space.template_key + + save_curve(args.target, backend,'op', workload, tuner, template_key, + {'flops': [float(x) for x in monitor.trial_scores()], + 'timestamp': [float(x) for x in monitor.trial_timestamps()]}) + +def get_target(target): + # target device + target_table = { + 'local': ('local','llvm -model=rpi3b -device=arm_cpu','llvm'), + 'rk3399-cpu': ('rk3399', + tvm.target.arm_cpu('rk3399'), None), + 'rk3399-gpu': ('rk3399', + 'opencl -model=rk3399 -device=mali', tvm.target.arm_cpu('rk3399')), + + 'rpi3b-cpu': ('rpi3b', + tvm.target.arm_cpu('rasp3b'), None), + 'pynq-cpu': ('pynq', + tvm.target.arm_cpu('pynq'), None), + + 'hikey960-cpu': ('hikey960', + 'llvm -model=hikey960 -device=arm_cpu -mtriple=aarch64-linux-gnu', + None), + 'hikey960-gpu': ('hikey960', + 'opencl -model=hikey960 -device=mali', + 'llvm -mtriple=aarch64-linux-gnu'), + + 'mate10pro-cpu': ('mate10pro', + tvm.target.arm_cpu('mate10pro'), None), + 'mate10pro-gpu': ('mate10pro', + 'opencl -model=mate10pro -device=mali', + tvm.target.arm_cpu('mate10pro')), + 'p20pro-cpu': ('p20pro', + tvm.target.arm_cpu('p20pro'), None), + 'p20pro-gpu': ('p20pro', + 'opencl -model=p20pro -device=mali', tvm.target.arm_cpu('p20pro')), + 'pixel2-cpu': ('pixel2', + tvm.target.arm_cpu('pixel2'), None), + 'pixel2-gpu': ('pixel2', + 'opencl -model=pixel2 -device=mali', tvm.target.arm_cpu('pixel2')), + + 'mi6-cpu': ('mi6', + 'llvm -model=mi6 -device=arm_cpu -mtriple=arm64-linux-android', None), + 'mi6-gpu': ('mi6', + 'opencl -model=mi6 -device=mali', 'llvm -target=arm64-linux-android'), + } + + device_key, target, target_host = target_table[target] + target = tvm.target.create(target) + + return device_key, target, target_host + +def get_tuning_option(device_key, args): + # extract tasks and tuning + tuning_option = { + 'log_filename': args.cache_file, + + 'device_key': device_key, + + 'tuner':'xgb-rank', + 'n_trial': args.n_trial, + 'early_stopping': 300, + + 'tuning_symbols': (nnvm.sym.conv2d,), + + 'transfer_learning': True, + } + + table = { + 'local': (2, 20, 8, 10, 10, False), + 'rk3399-cpu': (2, 20, 8, 8, 10, False), + 'rk3399-gpu': (2, 20, 8, 10, 50, False), + 'rpi3b-cpu': (8, 20, 8, 4, 10, False), + 'pynq-cpu': (2, 20, 8, 2, 10, False), + 'hikey960-cpu': (1, 20, 8, 10, 10, False), + 'hikey960-gpu': (1, 20, 8, 10, 50, False), + + 'p20pro-cpu': (2, 20, 8, 8, 10, True), + 'p20pro-gpu': (2, 20, 8, 10, 50, True), + 'pixel2-cpu': (2, 20, 8, 8, 10, True), + 'pixel2-gpu': (2, 20, 8, 10, 50, True), + + 'mi6-cpu': (1, 200, 100, 6, 10, True), + 'mi6-gpu': (1, 200, 100, 6, 10, True), + } + + table['mate10pro-cpu'] = table['p20pro-cpu'] + table['mate10pro-gpu'] = table['p20pro-gpu'] + + tuning_option['parallel_num'], tuning_option['timeout'], tuning_option['rpc_timeout'], \ + tuning_option['number'], n_times, tuning_option['use_ndk'] = table[args.target] + + return tuning_option, n_times + + +def get_network(name, batch_size): + shape = {"data": (batch_size, 3, 224, 224)} + output_shape = (batch_size, 1000) + if name =='resnet-18': + net, params = nnvm.testing.resnet.get_workload(num_layers=18, + batch_size=batch_size, image_shape=(3, 224, 224)) + elif name =='nature-dqn': + shape = {"data": (batch_size, 4, 84, 84)} + output_shape = (batch_size, 18) + net, params = nnvm.testing.dqn.get_workload(batch_size=batch_size) + elif name =='mobilenet': + net, params = nnvm.testing.mobilenet.get_workload(batch_size=batch_size) + elif name =='squeezenet': + net, params = nnvm.testing.squeezenet.get_workload(batch_size=batch_size, + version='1.1') + elif name =='vgg-16': + net, params = nnvm.testing.vgg.get_workload(batch_size=batch_size, num_layers=16) + elif name =='test': + from nnvm.testing import utils + net = nnvm.sym.Variable('data') + net = nnvm.sym.conv2d(net, channels=4, kernel_size=(3,3), padding=(1,1)) + net = nnvm.sym.flatten(net) + net = nnvm.sym.dense(net, units=1000) + net, params = utils.create_workload(net, 1, (3, 224, 224)) + else: + raise RuntimeError("Unsupported network") + + return net, params, shape, output_shape + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--network", type=str, default='dqn') + parser.add_argument("--target", type=str, default='rpi3b-cpu') + parser.add_argument("--target-host", type=str) + parser.add_argument("--n-trial", type=int, default=10) + parser.add_argument("--seed", type=int, default=0x93) + parser.add_argument("--cache-file", type=str) + parser.add_argument("--mode", type=str, default='tune') + parser.add_argument("--check", action='store_true') + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + dtype ='float32' + + args.cache_file = args.cache_file or args.network + "." + args.target + ".log" + + # device related + device_key, target, target_host = get_target(args.target) + tuning_option, n_times = get_tuning_option(device_key, args) + + # network + net, params, shape, out_shape = get_network(args.network, batch_size=1) + if args.mode =='tune': + tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, + symbols=tuning_option['tuning_symbols'], + target=target, target_host=target_host) + for i in range(len(tasks)): + try: # try winograd template + task = autotvm.task.create(tasks[i].name, tasks[i].args, tasks[i].target, tasks[i].target_host, + 'winograd') + tasks.append(task) + print("try winograd for ", i) + except Exception as e: + pass + tune_tasks(tasks, tuning_option) + elif args.mode =='infer': + # compile kernels with history best records + with autotvm.apply_history_best(args.cache_file): + raw_params = params + with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): + graph, lib, params = nnvm.compiler.build( + net, target=target, target_host=target_host, + shape=shape, params=params, dtype="float32") + + tmp = util.tempdir() + if tuning_option.get('use_ndk', False): + from tvm.contrib import ndk + filename = "net.so" + path_name = tmp.relpath(filename) + lib.export_library(path_name, ndk.create_shared) + else: + filename = "net.tar" + path_name = tmp.relpath(filename) + lib.export_library(path_name) + + if device_key =='local': + ctx = tvm.context(str(target), 0) + rlib = lib + else: + remote = autotvm.measure.request_remote(device_key, timeout=10000) + remote.upload(path_name) + ctx = remote.context(str(target), 0) + rlib = remote.load_module(filename) + + rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} + module = runtime.create(graph, rlib, ctx) + data_tvm = tvm.nd.array((np.random.uniform(size=shape['data'])).astype(dtype)) + module.set_input('data', data_tvm) + module.set_input(**rparams) + module.run() + module.run() + output = module.get_output(0, tvm.nd.empty(out_shape, ctx=ctx, dtype=dtype)).asnumpy() + + if args.check: + with nnvm.compiler.build_config(): + graph, lib, params = nnvm.compiler.build( + net, target='llvm', + shape=shape, params=raw_params, dtype="float32") + + ref_ctx = tvm.cpu() + ref_module = runtime.create(graph, lib, ref_ctx) + ref_module.set_input('data', data_tvm) + ref_module.set_input(**params) + ref_module.run() + out_reference = ref_module.get_output(0, + tvm.nd.empty(out_shape, ctx=ref_ctx, dtype=dtype)).asnumpy() + np.testing.assert_allclose(out_reference, output, rtol=1e-2) + + # evaluate + ftimer = module.module.time_evaluator("run", ctx, number=n_times, repeat=2) + prof_res = ftimer() + print("\n" + args.network + " " + args.target + " " + str(prof_res), "\n") + save_curve(args.target, str(target).split()[0],'network', args.network,'tvm','vanilla', + {'cost': prof_res.results}, outfile='network.tsv') + else: + raise RuntimeError("Invalid mode: " + args.mode) + diff --git a/script/util.py b/script/util.py new file mode 100644 index 000000000000..5c59532974b7 --- /dev/null +++ b/script/util.py @@ -0,0 +1,61 @@ +import json +import os +import time +from random import getrandbits + +import numpy as np + +import tvm +import topi +from tvm import autotvm + +def save_curve(device, backend, workload_type, workload, + tuner, template_key, value, outfile='vis.tsv'): + with open(outfile, 'a') as fout: + fout.write("\t".join([str(x) for x in + (device, backend, workload_type, workload, + tuner, template_key, json.dumps(value), time.time())]) + '\n') + +def save_point(curve_id, device, backend, tuner, template, + workload_type, workload, workload_op_count, iter_num, iter_total, + time_cost, timestamp, measure_pair, outfile='vis.tsv'): + if not os.path.isfile(outfile): + with open(outfile, 'w') as fout: + fout.write("\t".join(["curve_id", "device", "backend", + "tuner", "template", "workload_type", "workload", "workload_op_count", + "iter", "iter_total", "time_cost", "timestamp", "measure_pair"]) + "\n") + with open(outfile, 'a') as fout: + fout.write("\t".join([str(x) for x in [curve_id, device, backend, tuner, template, workload_type, + workload, workload_op_count, iter_num, iter_total, time_cost, timestamp, measure_pair]]) + "\n") + + +class VisLogger(object): + def __init__(self, task, device, tuner, template, iter_total, outfile, curve_id=None): + self.device = device + self.backend = str(task.target).split(" ")[0] + self.tuner = tuner + self.template = template + self.workload_type = 'op' + self.workload = str(task.workload) + self.workload_op_count = task.flop + self.iter_total = iter_total + self.curve_id = "%0x" % getrandbits(128) + self.outfile = outfile + + self.ct = 0 + + def __call__(self, tuner, inputs, results): + for inp, res in zip(inputs, results): + if res.error_no == 0: + cost = np.mean(res.costs) + else: + cost = float("+inf") + + save_point(self.curve_id, self.device, self.backend, + self.tuner, self.template, self.workload_type, self.workload, + self.workload_op_count, self.ct, self.iter_total, cost, time.time(), + json.dumps({'input': None, 'result': None}), + outfile=self.outfile) + + self.ct += 1 + From 4360500267e1bc6f93da1ea9f4b32354ea4b31a3 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 03:16:25 -0700 Subject: [PATCH 06/76] update benchmark --- apps/benchmark/README.md | 41 +++++++++++++++++++ apps/benchmark/arm_cpu_imagenet_bench.py | 21 +++++----- python/tvm/autotvm/measure/measure_methods.py | 6 ++- python/tvm/exec/pick_best.py | 3 +- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 4457ed9e44f9..9e2f1ae88174 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -2,4 +2,45 @@ ## ARM CPU +### How to run + +### Results +If a board is big.lITTLE archtiecture, we will use big cores only. + +* Firefly-RK3399 : 2 x Cortex A73 1.8Ghz+ 4 x Cortex A53 1.5Ghz + +```bash +$ python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 +-------------------------------------------------- +Network Name Mean Inference Time (std dev) +-------------------------------------------------- +squeezenet v1.1 44.15 ms (0.64 ms) +mobilenet 82.23 ms (0.67 ms) +resnet-18 168.71 ms (0.05 ms) +vgg-16 969.63 ms (0.75 ms) +``` + +* Raspberry Pi 3B : 4 x Cortex A53 1.2Ghz + +```bash +$ python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b +-------------------------------------------------- +Network Name Mean Inference Time (std dev) +-------------------------------------------------- +squeezenet v1.1 93.59 ms (0.04 ms) +mobilenet 147.82 ms (0.18 ms) +resnet-18 347.30 ms (0.25 ms) +``` + +* Huawei P20 Pro / Mate10 Pro (Soc: HiSilicon Kirin 970) : (4 x Cortex A73 2.36GHz + 4 x Cortex A53 1.8GHz) + +```bash +$ python3 arm_cpu_imagenet_bench.py --device p20pro --rpc-key p20pro +``` + +* Google Pixel 2 (Soc: Qualcomm Snapdragon 835) : (4 × Kyro 2.35 GHz, 4 × Kyro 1.9 GHz) + +```bash +$ python3 arm_cpu_imagenet_bench.py --device pixel2 --rpc-key pixel2 +``` diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 79c778e343f5..7364e0f00911 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -3,6 +3,7 @@ """ import argparse +import time import numpy as np @@ -53,21 +54,25 @@ def get_network(name, batch_size): parser.add_argument("--network", type=str, choices=['resnet-18', 'mobilenet', 'squeezenet v1.1', 'vgg-16']) parser.add_argument("--device", type=str, required=True, choices=['rk3399', 'mate10', 'mate10pro', 'p20', 'p20pro', 'pixel2', 'rasp3b', 'pynq']) - parser.add_argument("--host", type=str, required=True) - parser.add_argument("--port", type=int, required=True) + parser.add_argument("--host", type=str, default='localhost') + parser.add_argument("--port", type=int, default=9190) parser.add_argument("--rpc-key", type=str, required=True) - parser.add_argument("--number", type=int, default=5) + parser.add_argument("--number", type=int, default=6) args = parser.parse_args() dtype = 'float32' if args.network is None: - networks = ['mobilenet', 'squeezenet v1.1', 'resnet-18', 'vgg-16'] + networks = ['squeezenet v1.1', 'mobilenet', 'resnet-18', 'vgg-16'] else: networks = [args.network] target = tvm.target.arm_cpu(model=args.device) + # get remote device session + tracker = tvm.rpc.connect_tracker(args.host, args.port) + remote = tracker.request(args.rpc_key) + print("--------------------------------------------------") print("%-20s %-20s" % ("Network Name", "Mean Inference Time (std dev)")) print("--------------------------------------------------") @@ -81,18 +86,14 @@ def get_network(name, batch_size): tmp = tempdir() if 'android' in str(target): from tvm.contrib import ndk - filename = "net.so" + filename = "%s.so" % network path_name = tmp.relpath(filename) lib.export_library(path_name, ndk.create_shared) else: - filename = "net.tar" + filename = "%s.tar" % network path_name = tmp.relpath(filename) lib.export_library(path_name) - # get remote device session - tracker = tvm.rpc.connect_tracker(args.host, args.port) - remote = tracker.request(args.rpc_key) - # upload library and params ctx = remote.context(str(target), 0) remote.upload(path_name) diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index e04b944e1910..4529fe6ca8c1 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -210,11 +210,13 @@ def _fbuild(inp): func, args = _build_func(inp, build_option, kwargs) tmp_dir = util.tempdir() - file_name = "tmp_func_%0x.so" % getrandbits(64) - path = tmp_dir.relpath(file_name) if kwargs.get('use_ndk', False): # for Android NDK + file_name = "tmp_func_%0x.so" % getrandbits(64) + path = tmp_dir.relpath(file_name) func.export_library(path, ndk.create_shared) else: + file_name = "tmp_func_%0x.tar" % getrandbits(64) + path = tmp_dir.relpath(file_name) func.export_library(path) remote = request_remote(rpc_device_key, rpc_tracker_addr, rpc_priority, rpc_timeout) remote.upload(path) diff --git a/python/tvm/exec/pick_best.py b/python/tvm/exec/pick_best.py index ea09c2ead003..c402048b0d87 100644 --- a/python/tvm/exec/pick_best.py +++ b/python/tvm/exec/pick_best.py @@ -10,7 +10,7 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument("--i", type=str, help="The input file") + parser.add_argument("--i", type=str, help="The input file or directory") parser.add_argument("--o", type=str, help="The output file") args = parser.parse_args() @@ -33,5 +33,6 @@ logging.info("Run final filter...") autotvm.record.pick_best(tmp_filename, args.o) os.remove(tmp_filename) + logging.info("Output to %s ...", args.o) else: raise ValueError("Invalid input file: " + args.i) From 47a6a62a29bf6186c631ba2b87d56a87eafef6b7 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 04:34:07 -0700 Subject: [PATCH 07/76] update tracker info --- python/tvm/rpc/client.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/python/tvm/rpc/client.py b/python/tvm/rpc/client.py index 70af8c48538a..d457399571d4 100644 --- a/python/tvm/rpc/client.py +++ b/python/tvm/rpc/client.py @@ -225,18 +225,24 @@ def text_summary(self): res += item["key"] + "\n" res += "----------------------------\n" res += "\n" - res += "Queue Status\n" - res += "----------------------------\n" - res += "key\tfree\tpending\n" - res += "----------------------------\n" + + # compute max length of device key queue_info = data['queue_info'] keys = list(queue_info.keys()) if keys: keys.sort() max_key_len = max([len(k) for k in keys]) - for k in keys: - res += ("%%-%d" % max_key_len + "s\t%d\t%g\n") % \ - (k, queue_info[k]["free"], queue_info[k]["pending"]) + else: + max_ken_len = 0 + + res += "Queue Status\n" + res += "----------------------------\n" + res += ("%%-%ds" % max_key_len + "\tfree\tpending\n") % 'key' + res += "----------------------------\n" + for k in keys: + res += ("%%-%ds" % max_key_len + "\t%d\t%g\n") % \ + (k, queue_info[k]["free"], queue_info[k]["pending"]) + res += "----------------------------\n" return res From 6cbd02527d7e92c1de37a090578e501972cb37be Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 04:34:57 -0700 Subject: [PATCH 08/76] update readme --- apps/benchmark/README.md | 84 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 9e2f1ae88174..4128fe7e0b0b 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -2,12 +2,11 @@ ## ARM CPU -### How to run - ### Results -If a board is big.lITTLE archtiecture, we will use big cores only. +Note: If a board has big.LITTLE archtiecture, we will use all big cores. +Otherwise, we will use all cores. -* Firefly-RK3399 : 2 x Cortex A73 1.8Ghz+ 4 x Cortex A53 1.5Ghz +- **Firefly-RK3399 : 2 x Cortex A73 1.8Ghz+ 4 x Cortex A53 1.5Ghz** ```bash $ python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 @@ -20,7 +19,7 @@ resnet-18 168.71 ms (0.05 ms) vgg-16 969.63 ms (0.75 ms) ``` -* Raspberry Pi 3B : 4 x Cortex A53 1.2Ghz +- **Raspberry Pi 3B : 4 x Cortex A53 1.2Ghz** ```bash $ python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b @@ -30,17 +29,88 @@ Network Name Mean Inference Time (std dev) squeezenet v1.1 93.59 ms (0.04 ms) mobilenet 147.82 ms (0.18 ms) resnet-18 347.30 ms (0.25 ms) +vgg-16 crashed due to out of memeory ``` -* Huawei P20 Pro / Mate10 Pro (Soc: HiSilicon Kirin 970) : (4 x Cortex A73 2.36GHz + 4 x Cortex A53 1.8GHz) +- **Huawei P20 Pro / Mate10 Pro (Soc: HiSilicon Kirin 970) : (4 x Cortex A73 2.36GHz + 4 x Cortex A53 1.8GHz)** ```bash $ python3 arm_cpu_imagenet_bench.py --device p20pro --rpc-key p20pro +-------------------------------------------------- +Network Name Mean Inference Time (std dev) +------------------------------------------------- +squeezenet v1.1 29.33 ms (0.61 ms) +mobilenet 47.47 ms (0.65 ms) +resnet-18 84.71 ms (0.32 ms) +vgg-16 574.62 ms (2.14 ms) + ``` -* Google Pixel 2 (Soc: Qualcomm Snapdragon 835) : (4 × Kyro 2.35 GHz, 4 × Kyro 1.9 GHz) +- **Google Pixel 2 (Soc: Qualcomm Snapdragon 835) : (4 × Kyro 2.35 GHz, 4 × Kyro 1.9 GHz)** ```bash $ python3 arm_cpu_imagenet_bench.py --device pixel2 --rpc-key pixel2 +-------------------------------------------------- +Network Name Mean Inference Time (std dev) +-------------------------------------------------- +squeezenet v1.1 27.74 ms (0.41 ms) +mobilenet 42.05 ms (0.08 ms) +resnet-18 67.28 ms (0.05 ms) +vgg-16 427.75 ms (8.58 ms) ``` +### How to run + +1. Start an RPC Tracker on the host machine +```bash +python3 -m tvm.exec.rpc_tracker +``` + +2. Register your device to the tracker +* For Linux device + * Build tvm runtime on your device [Help](https://docs.tvm.ai/tutorials/cross_compilation_and_rpc.html#build-tvm-runtime-on-device). + * Register your device to tracker by + ```bash + python3 -m tvm.exec.rpc_sever --tracker=[HOST_IP]:9190 --key=[DEVICE_KEY] + ``` + replace `[HOST_IP]` with the IP address of the host machine, `[DEVICE_KEY]` with the name of device. + + E.g. For my RK3399, I use `python3 -m tvm.exec.rpc_sever --tracker=10.77.1.123:9190 --key=rk3399` + +* For Andoird device + * Build and install tvm rpc apk on your device [Help](https://github.com/dmlc/tvm/tree/master/apps/android_rpc). + Make sure you can pass the android rpc test. Then you have alreadly known how to register. + +3. Verify the device registration + We can query all registered devices by + ```bash + python3 -m tvm.exec.query_rpc_tracker + ``` + You should be able to find your devices in `Queue Status`. Make sure + the registration is correct before go ahead. + + For our test environment, one sample output can be + ```bash + Queue Status + ------------------------------ + key free pending + ------------------------------ + mate10pro 1 0 + p20pro 2 0 + pixel2 2 0 + rk3399 2 0 + rasp3b 8 0 + ``` + 4. Run benchmark + We did auto-tuning for the above devices, and release pre-tuned parameters in [this repo](https://github.com/uwsaml/tvm-distro) + So we can only run benchmark for the above devices. (TVM will download the operator parameters automatically.) + ```bash + python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b + python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 + python3 arm_cpu_imagenet_bench.py --device pixel2 --rpc-key pixel2 + python3 arm_cpu_imagenet_bench.py --device p20pro --rpc-key p20pro + python3 arm_cpu_imagenet_bench.py --device mate10pro --rpc-key mate10pro + ``` + + If you do not do tuning and run the benchmark for other devices, the performance is not gauranteed. + In order to get the best performance, you need to tune for you own device, please follow [tutorial](404.html). From 85a56c09b04e2c16da7432b18e74580f7de4a9ae Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 05:06:33 -0700 Subject: [PATCH 09/76] update benchmark --- apps/benchmark/README.md | 23 +++++++++++++++-------- apps/benchmark/arm_cpu_imagenet_bench.py | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 4128fe7e0b0b..c327849b060e 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -16,7 +16,7 @@ Network Name Mean Inference Time (std dev) squeezenet v1.1 44.15 ms (0.64 ms) mobilenet 82.23 ms (0.67 ms) resnet-18 168.71 ms (0.05 ms) -vgg-16 969.63 ms (0.75 ms) +vgg-16 972.03 ms (1.75 ms) ``` - **Raspberry Pi 3B : 4 x Cortex A53 1.2Ghz** @@ -26,8 +26,8 @@ $ python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b -------------------------------------------------- Network Name Mean Inference Time (std dev) -------------------------------------------------- -squeezenet v1.1 93.59 ms (0.04 ms) -mobilenet 147.82 ms (0.18 ms) +squeezenet v1.1 94.59 ms (0.04 ms) +mobilenet 148.82 ms (0.18 ms) resnet-18 347.30 ms (0.25 ms) vgg-16 crashed due to out of memeory ``` @@ -101,9 +101,12 @@ python3 -m tvm.exec.rpc_tracker rk3399 2 0 rasp3b 8 0 ``` - 4. Run benchmark - We did auto-tuning for the above devices, and release pre-tuned parameters in [this repo](https://github.com/uwsaml/tvm-distro) - So we can only run benchmark for the above devices. (TVM will download the operator parameters automatically.) + 4. Run benchmark + We did auto-tuning for the above devices, and release pre-tuned + parameters in [this repo](https://github.com/uwsaml/tvm-distro). + During compilation, TVM will download these operator parameters automatically. + + But we don't tune for other devices, so you can only run benchmark for these devices. ```bash python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 @@ -112,5 +115,9 @@ python3 -m tvm.exec.rpc_tracker python3 arm_cpu_imagenet_bench.py --device mate10pro --rpc-key mate10pro ``` - If you do not do tuning and run the benchmark for other devices, the performance is not gauranteed. - In order to get the best performance, you need to tune for you own device, please follow [tutorial](404.html). + If you do not do tuning and run the benchmark for other devices directly, + the performance is not gauranteed (This is still doable, you can pick a most + similar device and reuse its parameter). + In order to get the best performance, you need to tune for you own device, + please follow [tutorial](404.html). + diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 7364e0f00911..f36b78881b57 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -53,7 +53,7 @@ def get_network(name, batch_size): parser = argparse.ArgumentParser() parser.add_argument("--network", type=str, choices=['resnet-18', 'mobilenet', 'squeezenet v1.1', 'vgg-16']) parser.add_argument("--device", type=str, required=True, choices=['rk3399', 'mate10', 'mate10pro', 'p20', 'p20pro', - 'pixel2', 'rasp3b', 'pynq']) + 'pixel2', 'rasp3b']) parser.add_argument("--host", type=str, default='localhost') parser.add_argument("--port", type=int, default=9190) parser.add_argument("--rpc-key", type=str, required=True) From d8372454e94adb8785e941761f90f5368e4155bd Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 05:07:52 -0700 Subject: [PATCH 10/76] Update README.md --- apps/benchmark/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index c327849b060e..5b6c1efc2118 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -1,4 +1,4 @@ -# Performance Benchark +# Performance Benchmark ## ARM CPU From 7a72be7c80d6a0c063478b8e00355a593c3a06a7 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 05:21:59 -0700 Subject: [PATCH 11/76] update --- tutorials/autotvm/tune_nnvm_arm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 96b45ba1dc1b..286624384397 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -15,7 +15,8 @@ these operators, it will query this log file to get the best knob values. We also released pre-tuned parameters for some arm devices. You can go to -`ARM CPU Benchmark `_ to see the results. +`ARM CPU Benchmark `_ +to see the results. """ ###################################################################### From 91d1ffa3e7ab542f1623e29c8b2134c0f46eec15 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 05:25:22 -0700 Subject: [PATCH 12/76] fix --- tutorials/cross_compilation_and_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index 55a3c3de8227..2a28777da852 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -38,7 +38,7 @@ # git clone --recursive https://github.com/dmlc/tvm # cd tvm # cp cmake/config.cmake . -# make runtime +# make runtime -j4 # # After building runtime successfully, we need to set environment varibles # in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` From ec26ffbc743f134931ed2ce23ebaa8a8b33d2f14 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 06:00:34 -0700 Subject: [PATCH 13/76] update tutorials --- tutorials/cross_compilation_and_rpc.py | 47 +++++++++++++++++----- tutorials/nnvm/deploy_model_on_mali_gpu.py | 29 ++++++------- tutorials/nnvm/deploy_model_on_rasp.py | 22 +++++----- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index 2a28777da852..fdaa9800f77a 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -73,8 +73,17 @@ # So we simply start a "fake" RPC server on the same machine for demonstration. # These two lines can be omitted if you started an RPC erver on your device. -from tvm import rpc -server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) + +local_demo = True + +if local_demo: + from tvm import rpc + server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) + host = '0.0.0.0' + port = 9090 +else: + host = '10.77.1.162' # Change this to your target device IP + port = 9090 ###################################################################### # Declare and Cross Compile Kernel on Local Machine @@ -101,10 +110,15 @@ # Raspberry Pi 3B, but we use 'llvm' here to make example runable on # our webpage building server. See the detailed note in the following block. -func = tvm.build(s, [A, B], target='llvm', name='add_one') +if local_demo: + target = 'llvm' +else: + target = 'llvm -target=armv7l-linux-gnueabihf' + +func = tvm.build(s, [A, B], target=target, name='add_one') # save the lib at a local temp folder temp = util.tempdir() -path = temp.relpath('lib.so') +path = temp.relpath('lib.tar') func.export_library(path) ###################################################################### @@ -146,9 +160,8 @@ # Run CPU Kernel Remotely by RPC # ------------------------------ # We show how to run the cpu kernel on the remote device: +from tvm import rpc -host = '0.0.0.0' # Change to your target device IP -port = 9090 # connect the remote device remote = rpc.connect(host, port) @@ -157,7 +170,7 @@ # compiler to relink them. now `func` is a remote module object. remote.upload(path) -func = remote.load_module('lib.so') +func = remote.load_module('lib.tar') # create arrays on the remote device ctx = remote.cpu() @@ -198,18 +211,28 @@ # and execute :code:`make runtime -j4` # # The following function shows how we run OpenCL kernel remotely + def run_opencl(): + # This is the setting for my rk3399 board. You need to modify + # them according to your environment. + target_host = "llvm -target=aarch64-linux-gnu" + opencl_device_host = '10.77.1.145' + opencl_device_port = 9090 + + # create scheule for the above "add one" compute decleration s = tvm.create_schedule(B.op) xo, xi = s[B].split(B.op.axis[0], factor=32) s[B].bind(xo, tvm.thread_axis("blockIdx.x")) s[B].bind(xi, tvm.thread_axis("threadIdx.x")) - func = tvm.build(s, [A, B], "opencl", target_host="llvm", name="add_cl") + func = tvm.build(s, [A, B], "opencl", target_host=target_host) + + remote = rpc.connect(opencl_device_host, opencl_device_port) # export and upload - path = temp.relpath('lib_cl.so') + path = temp.relpath('lib_cl.tar') func.export_library(path) remote.upload(path) - func = remote.load_module('lib_cl.so') + func = remote.load_module('lib_cl.tar') # run ctx = remote.cl() @@ -222,7 +245,9 @@ def run_opencl(): # Terminate the "fake" server after experiment # You can delete this line if you started "real" rpc server on your remote device -server.terminate() + +if local_demo: + server.terminate() ###################################################################### # Summary diff --git a/tutorials/nnvm/deploy_model_on_mali_gpu.py b/tutorials/nnvm/deploy_model_on_mali_gpu.py index 70f2f892c285..e33df9b01dd5 100644 --- a/tutorials/nnvm/deploy_model_on_mali_gpu.py +++ b/tutorials/nnvm/deploy_model_on_mali_gpu.py @@ -74,18 +74,19 @@ # we do not have access to Raspberry Pi. # So we simply start a "fake" RPC server on the same machine for demonstration. # If you have set up the remote environment, please change the three lines below: -# change the :code:`use_mali` to True, also change the :code:`host` and :code:`port` +# change the :code:`local_demo` to False, also change the :code:`host` and :code:`port` # with your device's host address and port number. -use_mali = False -host = '10.77.1.xxx' -port = 9090 +local_demo = False -if not use_mali: +if local_demo: # run server locally host = 'localhost' port = 9091 server = rpc.Server(host=host, port=port, use_popen=True) +else: + host = '10.77.1.145' + port = 9090 ###################################################################### # Prepare the Pretrained Model @@ -165,19 +166,19 @@ def transform_image(image): # set it as :code:`llvm`. If running it on the RK3399, we need to # specify its instruction set. -if use_mali: +if local_demo: + target_host = "llvm" + target = "llvm" +else: # Here is the setting for my rk3399 board # If you don't use rk3399, you can query your target triple by # execute `gcc -v` on your board. target_host = "llvm -target=aarch64-linux-gnu" target = tvm.target.mali() -else: - target_host = "llvm" - target = "llvm" # set target as `tvm.target.mali` instead of 'opencl' to enable # target-specified optimization -with nnvm.compiler.build_config(opt_level=3): +with nnvm.compiler.build_config(opt_level=2): graph, lib, params = nnvm.compiler.build(net, target=target, shape={"data": data_shape}, params=params, target_host=target_host) @@ -187,7 +188,7 @@ def transform_image(image): # Save the library at local temporary directory. tmp = util.tempdir() -lib_fname = tmp.relpath('net.so') +lib_fname = tmp.relpath('net.tar') lib.export_library(lib_fname) ###################################################################### @@ -201,9 +202,9 @@ def transform_image(image): # upload the library to remote device and load it remote.upload(lib_fname) -rlib = remote.load_module('net.so') +rlib = remote.load_module('net.tar') -ctx = remote.cl(0) if use_mali else remote.cpu(0) +ctx = remote.cpu(0) if local_demo else remote.cl(0) # upload the parameter rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} @@ -221,7 +222,7 @@ def transform_image(image): top1 = np.argmax(out.asnumpy()) print('TVM prediction top-1: {}'.format(synset[top1])) -if not use_mali: +if local_demo: # terminate the local server server.terminate() diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index 92fb4bad70d5..3d11a2cde6b0 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -70,14 +70,14 @@ # we do not have access to Raspberry Pi. # So we simply start a "fake" RPC server on the same machine for demonstration. # If you have set up the remote environment, please change the three lines below: -# change the :code:`use_rasp` to True, also change the :code:`host` and :code:`port` +# change the :code:`local_demo` to True, also change the :code:`host` and :code:`port` # with your device's host address and port number. -use_rasp = False -host = '10.77.1.xxx' +local_demo = False +host = '10.77.1.162' port = 9090 -if not use_rasp: +if local_demo: # run server locally host = 'localhost' port = 9091 @@ -162,14 +162,14 @@ def transform_image(image): # specify its instruction set. We also need to add :code:`-device=arm_cpu` # to the target string to enable optimizations for arm_cpu. -if use_rasp: +if local_demo: + target = tvm.target.create('llvm') +else: target = tvm.target.arm_cpu('rasp3b') # The above line is a simple form of # target = tvm.target.create('llvm -devcie=arm_cpu -target=armv7l-linux-gnueabihf') -else: - target = tvm.target.create('llvm') -with nnvm.compiler.build_config(opt_level=3): +with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): graph, lib, params = nnvm.compiler.build( net, target, shape={"data": data_shape}, params=params) @@ -179,7 +179,7 @@ def transform_image(image): # Save the library at local temporary directory. tmp = util.tempdir() -lib_fname = tmp.relpath('net.so') +lib_fname = tmp.relpath('net.tar') lib.export_library(lib_fname) ###################################################################### @@ -193,7 +193,7 @@ def transform_image(image): # upload the library to remote device and load it remote.upload(lib_fname) -rlib = remote.load_module('net.so') +rlib = remote.load_module('net.tar') # upload the parameter ctx = remote.cpu(0) @@ -213,7 +213,7 @@ def transform_image(image): top1 = np.argmax(out.asnumpy()) print('TVM prediction top-1: {}'.format(synset[top1])) -if not use_rasp: +if local_demo: # terminate the local server server.terminate() From 4da75c30e4471282124a7e71a8fd5f647c26d743 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 25 Jul 2018 06:28:58 -0700 Subject: [PATCH 14/76] update tutorials --- tutorials/cross_compilation_and_rpc.py | 37 ++++++++++++---------- tutorials/nnvm/deploy_model_on_mali_gpu.py | 23 ++++++++------ tutorials/nnvm/deploy_model_on_rasp.py | 28 +++++++++------- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index fdaa9800f77a..d50caf102754 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -70,19 +70,24 @@ # # In our webpage building server (the machine that built this tutorial webpage), # we do not have access to Raspberry Pi. -# So we simply start a "fake" RPC server on the same machine for demonstration. - -# These two lines can be omitted if you started an RPC erver on your device. +# So for local demonstration, we simply start a "fake" RPC server on the same machine. +# +# .. note:: +# +# If you have real remote device, you should change :code:`local_demo` to False, and +# set the host and port correctly. local_demo = True if local_demo: + # start a "fake" RPC server from tvm import rpc server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) host = '0.0.0.0' port = 9090 else: - host = '10.77.1.162' # Change this to your target device IP + # The following is my environment, change this to your target device IP + host = '10.77.1.162' port = 9090 ###################################################################### @@ -94,7 +99,7 @@ # Now we back to the local machine, which has a full TVM installed # (with LLVM). # -# Here we will declare a simple kernel with TVM on the local machine: +# Here we will declare a simple kernel on the local machine: import tvm import numpy as np from tvm.contrib import util @@ -107,8 +112,8 @@ ###################################################################### # Then we cross compile the kernel: # The target should be 'llvm -target=armv7l-linux-gnueabihf' for -# Raspberry Pi 3B, but we use 'llvm' here to make example runable on -# our webpage building server. See the detailed note in the following block. +# Raspberry Pi 3B, but we use 'llvm' for local demo. +# See the detailed note in the following block. if local_demo: target = 'llvm' @@ -159,15 +164,16 @@ ###################################################################### # Run CPU Kernel Remotely by RPC # ------------------------------ -# We show how to run the cpu kernel on the remote device: +# We show how to run the generated cpu kernel on the remote device + from tvm import rpc # connect the remote device remote = rpc.connect(host, port) ###################################################################### -# We upload the lib to the remote device, then invoke a device local -# compiler to relink them. now `func` is a remote module object. +# Upload the lib to the remote device, then invoke a device local +# compiler to relink them. Now `func` is a remote module object. remote.upload(path) func = remote.load_module('lib.tar') @@ -201,19 +207,16 @@ # # Raspberry Pi does not support OpenCL, the following code is tested on # Firefly-RK3399. You may follow this `tutorial `_ -# to setup the RK3399 OS and OpenCL driver. -# -# The target_host should be 'llvm -target=aarch64-linux-gnu'. -# But here we set 'llvm' to make this tutorial runable on our x86 server. +# to setup the OS and OpenCL driver for RK3399. # # Also we need to build the runtime with OpenCL enabled in rk3399 board. -# You need to modify `set(USE_OPENCL OFF)` to `set(USE_OPENCL_ON)` in `config.cmake`, -# and execute :code:`make runtime -j4` +# You need to modify :code:`set(USE_OPENCL OFF)` to :code:`set(USE_OPENCL_ON)` in +# :code:`tvm/config.cmake`, and execute :code:`make runtime -j4`. # # The following function shows how we run OpenCL kernel remotely def run_opencl(): - # This is the setting for my rk3399 board. You need to modify + # NOTE: This is the setting for my rk3399 board. You need to modify # them according to your environment. target_host = "llvm -target=aarch64-linux-gnu" opencl_device_host = '10.77.1.145' diff --git a/tutorials/nnvm/deploy_model_on_mali_gpu.py b/tutorials/nnvm/deploy_model_on_mali_gpu.py index e33df9b01dd5..d5351aa24d18 100644 --- a/tutorials/nnvm/deploy_model_on_mali_gpu.py +++ b/tutorials/nnvm/deploy_model_on_mali_gpu.py @@ -71,20 +71,24 @@ # INFO:root:RPCServer: bind to 0.0.0.0:9090 # # In our webpage building server (the machine that built this tutorial webpage), -# we do not have access to Raspberry Pi. -# So we simply start a "fake" RPC server on the same machine for demonstration. -# If you have set up the remote environment, please change the three lines below: -# change the :code:`local_demo` to False, also change the :code:`host` and :code:`port` -# with your device's host address and port number. +# we do not have access to RK3399 board. +# +# So for local demonstration, we simply start a "fake" RPC server on the same machine. +# +# .. note:: +# +# If you have real remote device, you should change :code:`local_demo` to False, and +# set the host and port correctly. -local_demo = False +local_demo = True if local_demo: - # run server locally + # start a "fake" RPC server locally host = 'localhost' port = 9091 server = rpc.Server(host=host, port=port, use_popen=True) else: + # The following is my environment, change this to your target device IP host = '10.77.1.145' port = 9090 @@ -174,10 +178,11 @@ def transform_image(image): # If you don't use rk3399, you can query your target triple by # execute `gcc -v` on your board. target_host = "llvm -target=aarch64-linux-gnu" + + # set target as `tvm.target.mali` instead of 'opencl' to enable + # optimization for mali target = tvm.target.mali() -# set target as `tvm.target.mali` instead of 'opencl' to enable -# target-specified optimization with nnvm.compiler.build_config(opt_level=2): graph, lib, params = nnvm.compiler.build(net, target=target, shape={"data": data_shape}, params=params, target_host=target_host) diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index 3d11a2cde6b0..35eb16469a22 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -68,20 +68,26 @@ # # In our webpage building server (the machine that built this tutorial webpage), # we do not have access to Raspberry Pi. -# So we simply start a "fake" RPC server on the same machine for demonstration. -# If you have set up the remote environment, please change the three lines below: -# change the :code:`local_demo` to True, also change the :code:`host` and :code:`port` -# with your device's host address and port number. +# So for local demonstration, we simply start a "fake" RPC server on the same machine. +# +# .. note:: +# +# If you have real remote device, you should change :code:`local_demo` to False, and +# set the host and port correctly. -local_demo = False -host = '10.77.1.162' -port = 9090 +local_demo = True if local_demo: - # run server locally - host = 'localhost' - port = 9091 - server = rpc.Server(host=host, port=port, use_popen=True) + # start a "fake" RPC server locally + from tvm import rpc + server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) + host = '0.0.0.0' + port = 9090 +else: + # The following is my environment, change this to your target device IP + host = '10.77.1.162' + port = 9090 + ###################################################################### # Prepare the Pretrained Model From 803a646942e1d0139a1af461eb5d02c7d1f31c5d Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 06:36:26 -0700 Subject: [PATCH 15/76] update tutorial --- tutorials/autotvm/tune_nnvm_arm.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 286624384397..538eb2534b84 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -158,7 +158,7 @@ def get_network(name, batch_size): target = tvm.target.create('llvm -device=arm_cpu -target=aarch64-linux-gnu') # Also replace this with the device key in your tracker -device_key = 'rk3399' +device_key = 'rk3399-other' network = 'resnet-18' log_file = "%s.%s.log" % (device_key, network) @@ -249,9 +249,16 @@ def tune_and_evaluate(): ###################################################################### # Sample Output # ------------- -# The tuning takes about 1 hour on a 32 threads AMD server. +# The tuning takes about 1.5 hour on a 32 threads AMD server. # One sample output is # # .. code-block:: bash # -# +# [Task 1/16] Current/Best: 15.48/ 21.21 GFLOPS | Progress: (412/1000) | 531.53 s Done. +# [Task 2/16] Current/Best: 18.85/ 23.81 GFLOPS | Progress: (269/1000) | 261.59 s Done. +# [Task 3/16] Current/Best: 10.58/ 14.46 GFLOPS | Progress: (406/1000) | 317.72 s Done. +# [Task 4/16] Current/Best: 14.74/ 21.69 GFLOPS | Progress: (268/1000) | 246.84 s Done. +# [Task 5/16] Current/Best: 6.58/ 16.31 GFLOPS | Progress: (376/1000) | 301.62 s Done. +# [Task 6/16] Current/Best: 9.70/ 25.04 GFLOPS | Progress: (127/1000) | 154.13 s +# .... + From 41e651797f99099dd93cdb57f6dc1fa24a9b2927 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 25 Jul 2018 06:37:06 -0700 Subject: [PATCH 16/76] clean up --- script/consistency_check.py | 95 ------------ script/tune_all_mobile.py | 44 ------ script/tune_cuda_topi.py | 41 ----- script/tune_nnvm.py | 292 ------------------------------------ script/util.py | 61 -------- 5 files changed, 533 deletions(-) delete mode 100644 script/consistency_check.py delete mode 100644 script/tune_all_mobile.py delete mode 100644 script/tune_cuda_topi.py delete mode 100644 script/tune_nnvm.py delete mode 100644 script/util.py diff --git a/script/consistency_check.py b/script/consistency_check.py deleted file mode 100644 index 976725d6f56e..000000000000 --- a/script/consistency_check.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Extract tunable operators from nnvm graph and tune them""" - -import argparse -import logging -import time -import json - -import numpy as np - -import nnvm.testing -import nnvm.compiler -import tvm -from tvm.contrib import util -from tvm import autotvm -import tvm.contrib.graph_runtime as runtime - -from tune_nnvm import get_network, get_target, get_tuning_option, tune_tasks - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--network", type=str, default='resnet-18') - parser.add_argument("--target", type=str, default='rpi3b-cpu') - parser.add_argument("--target-host", type=str) - parser.add_argument("--n-trial", type=int, default=10) - parser.add_argument("--seed", type=int, default=0x93) - parser.add_argument("--cache-file", type=str) - parser.add_argument("--mode", type=str, default='infer') - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - dtype = 'float32' - - args.cache_file = args.cache_file or args.network + "." + args.target + ".tsv" - - # device related - device_key, target, target_host = get_target(args.target) - tuning_option, n_times = get_tuning_option(device_key, args) - - # network - net, params, shape, out_shape = get_network(args.network, batch_size=1) - tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, - symbols=tuning_option['tuning_symbols'], - target=target, target_host=target_host) - - task = tasks[0] - task = autotvm.task.create(task.name, task.args, task.target, task.target_host, 'vanilla') - - measure_option = autotvm.measure_option(mode='local' if tuning_option['device_key'] == 'local' else 'rpc', - number=tuning_option['number'], - repeat=3, - rpc_device_key=tuning_option['device_key'], - parallel_num=tuning_option['parallel_num'], - timeout=tuning_option['timeout'], - rpc_timeout=tuning_option['rpc_timeout'], - use_ndk=tuning_option.get('use_ndk', False)) - - if args.mode == 'tune': - print(task.config_space) - print(task.workload) - tuner = autotvm.tuner.XGBTuner(task) - tuner.tune(n_trial=1000, - measure_option=measure_option, - callbacks=[autotvm.callback.log_to_file('cache.tsv')]) - - dispatch_context = autotvm.apply_history_best("cache.tsv") - config = dispatch_context.query(task.target, task.workload) - - measure_batch = autotvm.measure.create_measure_batch(task, measure_option) - - gflops = [] - n_trial = 1000000 - tmp = [] - ct = {} - for i in range(n_trial // tuning_option['parallel_num']): - inputs = [autotvm.MeasureInput(task.target, task, config)] * tuning_option['parallel_num'] - results = measure_batch(inputs) - - for res in results: - if res.error_no != 0: - pass - else: - gflops.append(task.flop / np.mean(res.costs) / 1e9) - tmp.append(gflops[-1]) - -# url = res.timestamp -# if url not in ct: -# ct[url] = 0 -# ct[url] += res.all_cost - - if len(tmp) >= 5: - print("[" + " ".join(["%.2f" % x for x in tmp]) + "]") - print("var: %.4f min: %.2f max: %.2f\n" % (np.std(gflops) / np.mean(gflops), np.min(gflops), np.max(gflops))) - tmp = [] - - diff --git a/script/tune_all_mobile.py b/script/tune_all_mobile.py deleted file mode 100644 index 92ca7e17d85c..000000000000 --- a/script/tune_all_mobile.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys -import argparse -import os -import multiprocessing - -networks = [ - 'squeezenet', - 'mobilenet', - 'resnet-18', - 'vgg-16', -] - -targets = [ - 'rk3399-cpu', #'rk3399-gpu', - 'rpi3b-cpu', #'pynq-cpu', - 'hikey960-cpu', #'hikey960-gpu', - 'mate10pro-cpu', #'mate10pro-gpu', - 'pixel2-cpu', #'rk3399-gpu', - 'p20pro-cpu', #'pynq-cpu', -] - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("--target", type=str, default='') - parser.add_argument("--mode", type=str, default='tune') - args = parser.parse_args() - - targets = list(filter(lambda x: args.target in x, targets)) - - cmds = [] - for network in networks: - for target in targets: - cmd = "python3 tune_nnvm.py --network %s --target %s --cache-file %s --n-trial 1000 --mode %s" \ - % (network, target, network + "." + target + ".log", args.mode) - - print(cmd) - if args.mode == 'infer': - cmds.append(cmd) - else: - os.system(cmd) - - pool = multiprocessing.Pool() - pool.map(os.system, cmds) - diff --git a/script/tune_cuda_topi.py b/script/tune_cuda_topi.py deleted file mode 100644 index 4e7acc5aee67..000000000000 --- a/script/tune_cuda_topi.py +++ /dev/null @@ -1,41 +0,0 @@ -import tvm -import logging -import topi -from tvm import autotvm - -# task function -def conv2d(): - A = tvm.placeholder((1, 512, 7, 7), 'float32') - B = tvm.placeholder((512, 512, 3, 3), 'float32') - D = topi.nn.conv2d(A, B, (1,1), (1,1), 'NCHW', 'float32') - s = topi.generic.schedule_conv2d_nchw(D) - - return s, [A, B, D] - - -# create task -task = autotvm.task.create(conv2d, - args=(), - target='cuda', - template_key='vanilla') -print(task.config_space) - -# begin tuing -logging.basicConfig(level=logging.INFO) -measure_option = autotvm.measure_option(mode='local', - number=10, - parallel_num=8, - timeout=20) -tuner = autotvm.tuner.XGBTuner(task) -tuner.tune(n_trial=100, - measure_option=measure_option, - callbacks=[autotvm.callback.log_to_file('cache.tsv')]) - -# find history best -with autotvm.apply_history_best('cache.tsv'): - with tvm.target.create("cuda"): - s, arg_bufs = conv2d() - func = tvm.build(s, arg_bufs) - -print(tvm.lower(s, arg_bufs, simple_mode=True)) - diff --git a/script/tune_nnvm.py b/script/tune_nnvm.py deleted file mode 100644 index ebc75f029810..000000000000 --- a/script/tune_nnvm.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Extract tunable operators from nnvm graph and tune them""" - -import argparse -import logging -import time -import os - -import numpy as np - -import nnvm.testing -import nnvm.compiler -import tvm -from tvm import autotvm -from tvm.contrib import util -import tvm.contrib.graph_runtime as runtime - -from util import save_curve, VisLogger - -def tune_tasks(tasks, tuning_option): - for i, task in enumerate(tasks): - print("========== Task %d/%d ==========" % (i + 1, len(tasks))) - print("Workload: ", task.workload) - print("GFLOP:", task.flop) - print(task.config_space) - with open("status", "a") as fout: - fout.write("\t".join([tuning_option['log_filename'], - str(i+1), str(len(tasks)), time.asctime()]) +'\n') - - if tuning_option['device_key'] =='local': - mode ='local' - else: - mode ='rpc' - - measure_option = autotvm.measure_option(mode=mode, - repeat=3, - number=tuning_option['number'], - rpc_device_key=tuning_option['device_key'], - parallel_num=tuning_option['parallel_num'], - timeout=tuning_option['timeout'], - rpc_timeout=tuning_option['rpc_timeout'], - use_ndk=tuning_option.get('use_ndk', False)) - - monitor = autotvm.callback.Monitor() - visloger = VisLogger(task, args.target, tuning_option['tuner'], - 'vanilla', tuning_option['n_trial'], "vis/" + tuning_option['device_key'] + "/vis.tsv") - - # tuning - if tuning_option['tuner'] =='xgb-rank': - tuner = autotvm.tuner.XGBTuner(task, loss_type='rank') - elif tuning_option['tuner'] =='ga': - tuner = autotvm.tuner.GATuner(task, pop_size=50) - else: - raise RuntimeError("Invalid tuner") - - if tuning_option['transfer_learning']: - if os.path.isfile(tuning_option['log_filename']): - tuner.load_history(autotvm.record.load_from_file(tuning_option['log_filename'])) - - tuner.tune(n_trial=min(tuning_option['n_trial'], len(task.config_space)), - early_stopping=tuning_option['early_stopping'], - measure_option=measure_option, - callbacks=[autotvm.callback.log_to_file(tuning_option['log_filename']), - monitor, visloger]) - - # write log - device = tuning_option['device_key'] - backend = str(task.target).split(" ")[0] - workload = task.workload - tuner = tuning_option['tuner'] - template_key = task.config_space.template_key - - save_curve(args.target, backend,'op', workload, tuner, template_key, - {'flops': [float(x) for x in monitor.trial_scores()], - 'timestamp': [float(x) for x in monitor.trial_timestamps()]}) - -def get_target(target): - # target device - target_table = { - 'local': ('local','llvm -model=rpi3b -device=arm_cpu','llvm'), - 'rk3399-cpu': ('rk3399', - tvm.target.arm_cpu('rk3399'), None), - 'rk3399-gpu': ('rk3399', - 'opencl -model=rk3399 -device=mali', tvm.target.arm_cpu('rk3399')), - - 'rpi3b-cpu': ('rpi3b', - tvm.target.arm_cpu('rasp3b'), None), - 'pynq-cpu': ('pynq', - tvm.target.arm_cpu('pynq'), None), - - 'hikey960-cpu': ('hikey960', - 'llvm -model=hikey960 -device=arm_cpu -mtriple=aarch64-linux-gnu', - None), - 'hikey960-gpu': ('hikey960', - 'opencl -model=hikey960 -device=mali', - 'llvm -mtriple=aarch64-linux-gnu'), - - 'mate10pro-cpu': ('mate10pro', - tvm.target.arm_cpu('mate10pro'), None), - 'mate10pro-gpu': ('mate10pro', - 'opencl -model=mate10pro -device=mali', - tvm.target.arm_cpu('mate10pro')), - 'p20pro-cpu': ('p20pro', - tvm.target.arm_cpu('p20pro'), None), - 'p20pro-gpu': ('p20pro', - 'opencl -model=p20pro -device=mali', tvm.target.arm_cpu('p20pro')), - 'pixel2-cpu': ('pixel2', - tvm.target.arm_cpu('pixel2'), None), - 'pixel2-gpu': ('pixel2', - 'opencl -model=pixel2 -device=mali', tvm.target.arm_cpu('pixel2')), - - 'mi6-cpu': ('mi6', - 'llvm -model=mi6 -device=arm_cpu -mtriple=arm64-linux-android', None), - 'mi6-gpu': ('mi6', - 'opencl -model=mi6 -device=mali', 'llvm -target=arm64-linux-android'), - } - - device_key, target, target_host = target_table[target] - target = tvm.target.create(target) - - return device_key, target, target_host - -def get_tuning_option(device_key, args): - # extract tasks and tuning - tuning_option = { - 'log_filename': args.cache_file, - - 'device_key': device_key, - - 'tuner':'xgb-rank', - 'n_trial': args.n_trial, - 'early_stopping': 300, - - 'tuning_symbols': (nnvm.sym.conv2d,), - - 'transfer_learning': True, - } - - table = { - 'local': (2, 20, 8, 10, 10, False), - 'rk3399-cpu': (2, 20, 8, 8, 10, False), - 'rk3399-gpu': (2, 20, 8, 10, 50, False), - 'rpi3b-cpu': (8, 20, 8, 4, 10, False), - 'pynq-cpu': (2, 20, 8, 2, 10, False), - 'hikey960-cpu': (1, 20, 8, 10, 10, False), - 'hikey960-gpu': (1, 20, 8, 10, 50, False), - - 'p20pro-cpu': (2, 20, 8, 8, 10, True), - 'p20pro-gpu': (2, 20, 8, 10, 50, True), - 'pixel2-cpu': (2, 20, 8, 8, 10, True), - 'pixel2-gpu': (2, 20, 8, 10, 50, True), - - 'mi6-cpu': (1, 200, 100, 6, 10, True), - 'mi6-gpu': (1, 200, 100, 6, 10, True), - } - - table['mate10pro-cpu'] = table['p20pro-cpu'] - table['mate10pro-gpu'] = table['p20pro-gpu'] - - tuning_option['parallel_num'], tuning_option['timeout'], tuning_option['rpc_timeout'], \ - tuning_option['number'], n_times, tuning_option['use_ndk'] = table[args.target] - - return tuning_option, n_times - - -def get_network(name, batch_size): - shape = {"data": (batch_size, 3, 224, 224)} - output_shape = (batch_size, 1000) - if name =='resnet-18': - net, params = nnvm.testing.resnet.get_workload(num_layers=18, - batch_size=batch_size, image_shape=(3, 224, 224)) - elif name =='nature-dqn': - shape = {"data": (batch_size, 4, 84, 84)} - output_shape = (batch_size, 18) - net, params = nnvm.testing.dqn.get_workload(batch_size=batch_size) - elif name =='mobilenet': - net, params = nnvm.testing.mobilenet.get_workload(batch_size=batch_size) - elif name =='squeezenet': - net, params = nnvm.testing.squeezenet.get_workload(batch_size=batch_size, - version='1.1') - elif name =='vgg-16': - net, params = nnvm.testing.vgg.get_workload(batch_size=batch_size, num_layers=16) - elif name =='test': - from nnvm.testing import utils - net = nnvm.sym.Variable('data') - net = nnvm.sym.conv2d(net, channels=4, kernel_size=(3,3), padding=(1,1)) - net = nnvm.sym.flatten(net) - net = nnvm.sym.dense(net, units=1000) - net, params = utils.create_workload(net, 1, (3, 224, 224)) - else: - raise RuntimeError("Unsupported network") - - return net, params, shape, output_shape - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--network", type=str, default='dqn') - parser.add_argument("--target", type=str, default='rpi3b-cpu') - parser.add_argument("--target-host", type=str) - parser.add_argument("--n-trial", type=int, default=10) - parser.add_argument("--seed", type=int, default=0x93) - parser.add_argument("--cache-file", type=str) - parser.add_argument("--mode", type=str, default='tune') - parser.add_argument("--check", action='store_true') - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - dtype ='float32' - - args.cache_file = args.cache_file or args.network + "." + args.target + ".log" - - # device related - device_key, target, target_host = get_target(args.target) - tuning_option, n_times = get_tuning_option(device_key, args) - - # network - net, params, shape, out_shape = get_network(args.network, batch_size=1) - if args.mode =='tune': - tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, - symbols=tuning_option['tuning_symbols'], - target=target, target_host=target_host) - for i in range(len(tasks)): - try: # try winograd template - task = autotvm.task.create(tasks[i].name, tasks[i].args, tasks[i].target, tasks[i].target_host, - 'winograd') - tasks.append(task) - print("try winograd for ", i) - except Exception as e: - pass - tune_tasks(tasks, tuning_option) - elif args.mode =='infer': - # compile kernels with history best records - with autotvm.apply_history_best(args.cache_file): - raw_params = params - with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): - graph, lib, params = nnvm.compiler.build( - net, target=target, target_host=target_host, - shape=shape, params=params, dtype="float32") - - tmp = util.tempdir() - if tuning_option.get('use_ndk', False): - from tvm.contrib import ndk - filename = "net.so" - path_name = tmp.relpath(filename) - lib.export_library(path_name, ndk.create_shared) - else: - filename = "net.tar" - path_name = tmp.relpath(filename) - lib.export_library(path_name) - - if device_key =='local': - ctx = tvm.context(str(target), 0) - rlib = lib - else: - remote = autotvm.measure.request_remote(device_key, timeout=10000) - remote.upload(path_name) - ctx = remote.context(str(target), 0) - rlib = remote.load_module(filename) - - rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} - module = runtime.create(graph, rlib, ctx) - data_tvm = tvm.nd.array((np.random.uniform(size=shape['data'])).astype(dtype)) - module.set_input('data', data_tvm) - module.set_input(**rparams) - module.run() - module.run() - output = module.get_output(0, tvm.nd.empty(out_shape, ctx=ctx, dtype=dtype)).asnumpy() - - if args.check: - with nnvm.compiler.build_config(): - graph, lib, params = nnvm.compiler.build( - net, target='llvm', - shape=shape, params=raw_params, dtype="float32") - - ref_ctx = tvm.cpu() - ref_module = runtime.create(graph, lib, ref_ctx) - ref_module.set_input('data', data_tvm) - ref_module.set_input(**params) - ref_module.run() - out_reference = ref_module.get_output(0, - tvm.nd.empty(out_shape, ctx=ref_ctx, dtype=dtype)).asnumpy() - np.testing.assert_allclose(out_reference, output, rtol=1e-2) - - # evaluate - ftimer = module.module.time_evaluator("run", ctx, number=n_times, repeat=2) - prof_res = ftimer() - print("\n" + args.network + " " + args.target + " " + str(prof_res), "\n") - save_curve(args.target, str(target).split()[0],'network', args.network,'tvm','vanilla', - {'cost': prof_res.results}, outfile='network.tsv') - else: - raise RuntimeError("Invalid mode: " + args.mode) - diff --git a/script/util.py b/script/util.py deleted file mode 100644 index 5c59532974b7..000000000000 --- a/script/util.py +++ /dev/null @@ -1,61 +0,0 @@ -import json -import os -import time -from random import getrandbits - -import numpy as np - -import tvm -import topi -from tvm import autotvm - -def save_curve(device, backend, workload_type, workload, - tuner, template_key, value, outfile='vis.tsv'): - with open(outfile, 'a') as fout: - fout.write("\t".join([str(x) for x in - (device, backend, workload_type, workload, - tuner, template_key, json.dumps(value), time.time())]) + '\n') - -def save_point(curve_id, device, backend, tuner, template, - workload_type, workload, workload_op_count, iter_num, iter_total, - time_cost, timestamp, measure_pair, outfile='vis.tsv'): - if not os.path.isfile(outfile): - with open(outfile, 'w') as fout: - fout.write("\t".join(["curve_id", "device", "backend", - "tuner", "template", "workload_type", "workload", "workload_op_count", - "iter", "iter_total", "time_cost", "timestamp", "measure_pair"]) + "\n") - with open(outfile, 'a') as fout: - fout.write("\t".join([str(x) for x in [curve_id, device, backend, tuner, template, workload_type, - workload, workload_op_count, iter_num, iter_total, time_cost, timestamp, measure_pair]]) + "\n") - - -class VisLogger(object): - def __init__(self, task, device, tuner, template, iter_total, outfile, curve_id=None): - self.device = device - self.backend = str(task.target).split(" ")[0] - self.tuner = tuner - self.template = template - self.workload_type = 'op' - self.workload = str(task.workload) - self.workload_op_count = task.flop - self.iter_total = iter_total - self.curve_id = "%0x" % getrandbits(128) - self.outfile = outfile - - self.ct = 0 - - def __call__(self, tuner, inputs, results): - for inp, res in zip(inputs, results): - if res.error_no == 0: - cost = np.mean(res.costs) - else: - cost = float("+inf") - - save_point(self.curve_id, self.device, self.backend, - self.tuner, self.template, self.workload_type, self.workload, - self.workload_op_count, self.ct, self.iter_total, cost, time.time(), - json.dumps({'input': None, 'result': None}), - outfile=self.outfile) - - self.ct += 1 - From 4823969acb58e5a47a854cb89b5cad919583740e Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 09:52:13 -0700 Subject: [PATCH 17/76] fix tutorial --- tutorials/autotvm/tune_nnvm_arm.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 538eb2534b84..134bdcbf801e 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -212,14 +212,14 @@ def tune_and_evaluate(): # export library tmp = tempdir() - filename = "net.so" - path_name = tmp.relpath(filename) - - if tuning_option.get('use_ndk', False): - # for android + if tuning_option.get('use_ndk', False): # for android from tvm.contrib import ndk + filename = "net.so" + path_name = tmp.relpath(filename) lib.export_library(path_name, ndk.create_shared) else: + filename = "net.tar" + path_name = tmp.relpath(filename) lib.export_library(path_name) # upload module to device @@ -249,7 +249,9 @@ def tune_and_evaluate(): ###################################################################### # Sample Output # ------------- -# The tuning takes about 1.5 hour on a 32 threads AMD server. +# The tuning needs to train xgboost models and use them for prediction. +# So a high performance CPU is recommended. +# It takes about 1.5 hour on a 32T AMD Ryzen CPU. # One sample output is # # .. code-block:: bash From d4cd78fe51b08b9c8e858be073d20d99fd9d7377 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 11:08:30 -0700 Subject: [PATCH 18/76] fix vta --- apps/benchmark/README.md | 4 +- nnvm/src/top/nn/convolution.cc | 12 ++--- python/tvm/autotvm/__init__.py | 2 + python/tvm/autotvm/env.py | 1 + python/tvm/autotvm/tuner/tuner.py | 3 ++ python/tvm/rpc/client.py | 2 +- tests/python/unittest/test_autotvm_feature.py | 2 +- tests/python/unittest/test_lang_target.py | 2 +- topi/python/topi/arm_cpu/conv2d.py | 49 +++++++++++-------- topi/python/topi/generic/nn.py | 10 ++-- vta/python/vta/top/__init__.py | 1 - .../integration/test_benchmark_topi_conv2d.py | 9 ++-- 12 files changed, 56 insertions(+), 41 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 5b6c1efc2118..50ae184681a1 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -77,7 +77,7 @@ python3 -m tvm.exec.rpc_tracker E.g. For my RK3399, I use `python3 -m tvm.exec.rpc_sever --tracker=10.77.1.123:9190 --key=rk3399` -* For Andoird device +* For Android device * Build and install tvm rpc apk on your device [Help](https://github.com/dmlc/tvm/tree/master/apps/android_rpc). Make sure you can pass the android rpc test. Then you have alreadly known how to register. @@ -116,7 +116,7 @@ python3 -m tvm.exec.rpc_tracker ``` If you do not do tuning and run the benchmark for other devices directly, - the performance is not gauranteed (This is still doable, you can pick a most + the performance is not guaranteed (This is still doable, you can pick a most similar device and reuse its parameter). In order to get the best performance, you need to tune for you own device, please follow [tutorial](404.html). diff --git a/nnvm/src/top/nn/convolution.cc b/nnvm/src/top/nn/convolution.cc index bcb317d3ccfc..20b09dca6a81 100644 --- a/nnvm/src/top/nn/convolution.cc +++ b/nnvm/src/top/nn/convolution.cc @@ -176,13 +176,11 @@ inline bool WinogradConv2DInferShape(const nnvm::NodeAttrs& attrs, << "output channels must divide group size"; // NOTE: Do not check weight shape here! - // TShape wshape({param.channels / param.groups, - // dshape[1] / param.groups, - // param.kernel_size[0], - // param.kernel_size[1]}); - // wshape = ConvertLayout(wshape, kOIHW, kernel_layout); - // wshape[kernel_layout.indexof('O')] *= param.groups; - // NNVM_ASSIGN_INPUT_SHAPE(attrs, *in_shape, Conv2DParam::kWeight, wshape); + // Different backend requires different layout to compute + // the batch gemm stage in winograd efficiently, but we want to + // make this NNVM symbol work for all backends. + // So we accept all weight shapes, and assume the TOPI developers + // can handle this correctly in alter_op_layout. if (param.use_bias) { static const Layout default_bias_layout("C"); diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index 13b730e8ebae..27df4493224d 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -18,9 +18,11 @@ from . import task from . import tuner from . import util +from . import env # some shortcuts from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo from .tuner import callback, tune_tasks from .task import template, get_config, create, ConfigSpace, ConfigEntity from .record import ApplyHistoryBest as apply_history_best, load_op_param +from .env import GLOBAL_SCOPE diff --git a/python/tvm/autotvm/env.py b/python/tvm/autotvm/env.py index 3b04c98ea7ce..dc559a7bce1d 100644 --- a/python/tvm/autotvm/env.py +++ b/python/tvm/autotvm/env.py @@ -8,5 +8,6 @@ def __init__(self): AutotvmGlobalScope.current = self self.cuda_target_arch = None + self.in_tuning = False GLOBAL_SCOPE = AutotvmGlobalScope() diff --git a/python/tvm/autotvm/tuner/tuner.py b/python/tvm/autotvm/tuner/tuner.py index 3e4d5cb83cc6..361608a464f8 100644 --- a/python/tvm/autotvm/tuner/tuner.py +++ b/python/tvm/autotvm/tuner/tuner.py @@ -7,6 +7,7 @@ from ..measure import MeasureInput from ..measure import create_measure_batch +from ..env import GLOBAL_SCOPE class Tuner(object): """Base class for tuners @@ -89,6 +90,7 @@ def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callback parallel_num = getattr(measure_batch, 'parallel_num', 1) early_stopping = early_stopping or 1e9 + GLOBAL_SCOPE.in_tuning = True i = 0 while i < n_trial: if not self.has_next(): @@ -128,6 +130,7 @@ def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callback if verbose >= 1: logging.info("Early stopped. Best iter: %d.", self.best_iter) break + GLOBAL_SCOPE.in_tuning = False del measure_batch diff --git a/python/tvm/rpc/client.py b/python/tvm/rpc/client.py index d457399571d4..57f368b0e660 100644 --- a/python/tvm/rpc/client.py +++ b/python/tvm/rpc/client.py @@ -233,7 +233,7 @@ def text_summary(self): keys.sort() max_key_len = max([len(k) for k in keys]) else: - max_ken_len = 0 + max_key_len = 0 res += "Queue Status\n" res += "----------------------------\n" diff --git a/tests/python/unittest/test_autotvm_feature.py b/tests/python/unittest/test_autotvm_feature.py index 1b806ded4393..43754c27e0ea 100644 --- a/tests/python/unittest/test_autotvm_feature.py +++ b/tests/python/unittest/test_autotvm_feature.py @@ -84,7 +84,7 @@ def get_gemm_feature(target): targets = [ tvm.target.cuda(), tvm.target.mali(), - tvm.target.rasp(), + tvm.target.arm_cpu(), ] for target in targets: diff --git a/tests/python/unittest/test_lang_target.py b/tests/python/unittest/test_lang_target.py index 5f77010fa751..f7309fc30819 100644 --- a/tests/python/unittest/test_lang_target.py +++ b/tests/python/unittest/test_lang_target.py @@ -28,7 +28,7 @@ def test_target_dispatch(): with tvm.target.create("cuda"): assert mygeneric(1) == 3 - with tvm.target.rasp(): + with tvm.target.arm_cpu(): assert mygeneric(1) == 11 with tvm.target.create("metal"): diff --git a/topi/python/topi/arm_cpu/conv2d.py b/topi/python/topi/arm_cpu/conv2d.py index 329700a56cb6..ae35cf89b588 100644 --- a/topi/python/topi/arm_cpu/conv2d.py +++ b/topi/python/topi/arm_cpu/conv2d.py @@ -93,11 +93,11 @@ def _decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype, n n, co, oh, ow = cfg.axis(N), cfg.axis(CO), cfg.axis(OH), cfg.axis(OW) ci, kh, kw = cfg.reduce_axis(CI), cfg.reduce_axis(KH), cfg.reduce_axis(KW) - if num_tile == 2: # for cpu + if num_tile == 2: # for arm cpu co, vc = cfg.define_split('tile_co', co, num_outputs=2) oh, vh = cfg.define_split('tile_oh', oh, num_outputs=2) ow, vw = cfg.define_split('tile_ow', ow, num_outputs=2) - elif num_tile == 3: # for gpu + elif num_tile == 3: # for mali gpu co, _, vc = cfg.define_split('tile_co', co, num_outputs=3) oh, _, vh = cfg.define_split('tile_oh', oh, num_outputs=3) ow, _, vw = cfg.define_split('tile_ow', ow, num_outputs=3) @@ -194,9 +194,12 @@ def _schedule_spatial_pack(cfg, s, data_vec, kernel_vec, if kernel_vec.op.name == 'kernel_vec': co, _, _, _, _ = s[kernel_vec].op.axis - s[kernel_vec].pragma(co, 'debug_skip_region') - # kernel packing will be pre-computed during compliation, so we skip this part - # to make tuning records correct + if autotvm.GLOBAL_SCOPE.in_tuning: + # kernel packing will be pre-computed during compliation, so we skip + # this part to make tuning records correct + s[kernel_vec].pragma(co, 'debug_skip_region') + else: + s[kernel_vec].parallel(co) return s @@ -209,10 +212,10 @@ def decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype): def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_size): N, CI, IH, IW = get_const_tuple(data.shape) if len(kernel.shape) == 4: - pre_packed = False + pre_computed = False CO, _, KH, KW = get_const_tuple(kernel.shape) else: - pre_packed = True + pre_computed = True H_CAT, W_CAT, CO, CI, VC = get_const_tuple(kernel.shape) CO *= VC KH, KW = H_CAT - tile_size + 1, W_CAT - tile_size + 1 @@ -295,23 +298,23 @@ def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_ name='d') # transform kernel - if pre_packed: + if pre_computed: U = kernel else: G = const_matrix(G_data, 'G') r_kh = tvm.reduce_axis((0, KH), 'r_kh') r_kw = tvm.reduce_axis((0, KW), 'r_kw') U = tvm.compute((alpha, alpha, K // VK, C, VK), lambda eps, nu, k, c, kk: - tvm.sum(kernel[k * VK + kk][c][r_kh][r_kw] * G[eps][r_kh] * G[nu][r_kw], - axis=[r_kh, r_kw]), name='U') + tvm.sum(kernel[k * VK + kk][c][r_kh][r_kw].astype(out_dtype) * + G[eps][r_kh] * G[nu][r_kw], axis=[r_kh, r_kw]), name='U') # transform image B = const_matrix(B_data, 'B') r_eps = tvm.reduce_axis((0, alpha), 'r_eps') r_nu = tvm.reduce_axis((0, alpha), 'r_nu') V = tvm.compute((alpha, alpha, P // VP, C, VP), lambda eps, nu, b, c, bb: - tvm.sum(input_tile[c][b][r_eps][r_nu][bb] * B[r_eps][eps] * B[r_nu][nu], - axis=[r_eps, r_nu]), name='V') + tvm.sum(input_tile[c][b][r_eps][r_nu][bb].astype(out_dtype) * + B[r_eps][eps] * B[r_nu][nu], axis=[r_eps, r_nu]), name='V') # batch gemm c = tvm.reduce_axis((0, C), name='c') @@ -362,9 +365,12 @@ def _schedule_winograd(cfg, s, output, last): s[U].unroll(nu) s[U].unroll(r_kh) s[U].unroll(r_kw) - s[U].pragma(k, 'debug_skip_region') - # kernel transformation will be pre-computed during compliation, so we skip this part - # to make tuning records correct + if autotvm.GLOBAL_SCOPE.in_tuning: + # kernel transformation will be pre-computed during compliation, so we skip + # this part to make tuning records correct + s[U].pragma(k, 'debug_skip_region') + else: + s[U].parallel(k) # transform image DD = s.cache_read(d, 'global', [V]) @@ -483,13 +489,14 @@ def _alter_conv2d_layout(attrs, inputs, tinfos): if groups == 1: # query config of this workload - workload = _conv_arg_to_workload(tinfos[0], tinfos[1], strides, padding, layout, out_dtype) + workload = _conv_arg_to_workload(tinfos[0], tinfos[1], strides, padding, + layout, out_dtype) cfg = autotvm.task.DispatchContext.current.query(tvm.target.current_target(), workload) - if cfg.template_key == 'vanilla': + if cfg.template_key == 'vanilla': # simple packing new_attrs['kernel_layout'] = 'OIHW%do' % (cfg['tile_co'].size[-1]) return sym.conv2d(*copy_inputs, **new_attrs) - else: + else: # pre-compute weight transformation in winograd tile_size = 4 weight = sym.contrib.conv2d_winograd_weight_transform(copy_inputs[1], @@ -503,6 +510,6 @@ def _alter_conv2d_layout(attrs, inputs, tinfos): copy_inputs[1] = weight new_attrs['tile_size'] = tile_size return sym.contrib.conv2d_winograd_without_weight_transform(*copy_inputs, **new_attrs) - else: - # do nothing for depthwise convolution - return sym.conv2d(*copy_inputs, **new_attrs) + + # do nothing for depthwise convolution + return None diff --git a/topi/python/topi/generic/nn.py b/topi/python/topi/generic/nn.py index 3f73bf3a35e3..1e01adb899b7 100644 --- a/topi/python/topi/generic/nn.py +++ b/topi/python/topi/generic/nn.py @@ -93,12 +93,12 @@ def schedule_conv2d_NCHWc(num_filter, kernel_size, strides, @tvm.target.generic_func def schedule_conv2d_winograd_weight_transform(outs): - """Schedule for conv2d_nhwc + """Schedule for weight transformation of winograd Parameters ---------- outs: Array of Tensor - The computation graph description of conv2d_nchw + The computation graph description of this operator in the format of an array of tensors. Returns @@ -106,6 +106,8 @@ def schedule_conv2d_winograd_weight_transform(outs): sch: Schedule The computation schedule for the op. """ + # Typically this is computed in nnvm PreCompute pass + # so we make a schedule here for cpu llvm s = tvm.create_schedule([x.op for x in outs]) output = outs[0] _, G = s[output].op.input_tensors @@ -121,12 +123,12 @@ def schedule_conv2d_winograd_weight_transform(outs): @tvm.target.generic_func def schedule_conv2d_winograd_without_weight_transform(outs): - """Schedule for conv2d_nhwc + """Schedule for winograd without weight transformation Parameters ---------- outs: Array of Tensor - The computation graph description of conv2d_nchw + The computation graph description of this operator in the format of an array of tensors. Returns diff --git a/vta/python/vta/top/__init__.py b/vta/python/vta/top/__init__.py index 614ed2347181..d0ccef9a3bf3 100644 --- a/vta/python/vta/top/__init__.py +++ b/vta/python/vta/top/__init__.py @@ -2,4 +2,3 @@ from .vta_conv2d import packed_conv2d, schedule_packed_conv2d from . import vta_conv2d -from . import arm_conv2d diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 9cf8909bc9ba..6ee6c0d8ef34 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -1,6 +1,7 @@ """Testing if we can generate code in topi style""" import tvm +from tvm import autotvm from tvm.contrib import util from tvm.contrib.pickle_memoize import memoize import topi @@ -21,6 +22,9 @@ def my_clip(x, a_min, a_max): return x def test_cpu_conv2d(): + # load pre-tuned parameters + autotvm.load_op_param() + def run_cpu_conv2d(env, remote, key, batch_size, wl, profile=True): data_shape = (batch_size, wl.in_filter, wl.height, wl.width) kernel_shape = (wl.out_filter, wl.in_filter, wl.hkernel, wl.wkernel) @@ -62,8 +66,7 @@ def get_ref_data(): def verify(s, check_correctness): mod = tvm.build(s, [data, kernel, res], - "llvm -device=vtacpu", - env.target_host, + target_host=env.target_host, name="conv2d") temp = util.tempdir() mod.save(temp.relpath("conv2d.o")) @@ -124,7 +127,7 @@ def _run(env, remote): key = "resnet-cfg[%d]" % i print("key=%s" % key) print(wl) - with tvm.target.create("llvm -device=vtacpu"): + with tvm.target.arm_cpu("pynq"): run_cpu_conv2d(env, remote, key, batch_size, wl) vta.testing.run(_run) From 87e9e91400237a6dcb5bf18553a1e45204a5c6fe Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 11:24:29 -0700 Subject: [PATCH 19/76] fix lint --- nnvm/src/top/nn/convolution.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nnvm/src/top/nn/convolution.cc b/nnvm/src/top/nn/convolution.cc index 20b09dca6a81..85f23b0cd8f4 100644 --- a/nnvm/src/top/nn/convolution.cc +++ b/nnvm/src/top/nn/convolution.cc @@ -371,10 +371,10 @@ weight transformation in advance. NNVM_ASSIGN_OUTPUT_SHAPE(attrs, *out_shape, 0, oshape); return true; }) -.set_attr("FCorrectLayot",[](const NodeAttrs& attrs, - std::vector *ilayouts, - const std::vector *last_ilayouts, - std::vector *olayouts) { +.set_attr("FCorrectLayot", [](const NodeAttrs& attrs, + std::vector *ilayouts, + const std::vector *last_ilayouts, + std::vector *olayouts) { Layout layout("OIHW"); NNVM_ASSIGN_LAYOUT(*ilayouts, 0, layout); NNVM_ASSIGN_LAYOUT(*olayouts, 0, layout); From cb6b8a07b13d9da89d36a17d26d9e8cfefe4357e Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 16:19:21 -0700 Subject: [PATCH 20/76] update readme --- apps/benchmark/README.md | 85 +++++++--------------------------------- 1 file changed, 15 insertions(+), 70 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 50ae184681a1..2e307d6099de 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -1,74 +1,22 @@ # Performance Benchmark -## ARM CPU +## Results -### Results -Note: If a board has big.LITTLE archtiecture, we will use all big cores. -Otherwise, we will use all cores. +See results on wiki page https://github.com/dmlc/tvm/wiki/Benchmark -- **Firefly-RK3399 : 2 x Cortex A73 1.8Ghz+ 4 x Cortex A53 1.5Ghz** +## How to reporduce -```bash -$ python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 --------------------------------------------------- -Network Name Mean Inference Time (std dev) --------------------------------------------------- -squeezenet v1.1 44.15 ms (0.64 ms) -mobilenet 82.23 ms (0.67 ms) -resnet-18 168.71 ms (0.05 ms) -vgg-16 972.03 ms (1.75 ms) -``` - -- **Raspberry Pi 3B : 4 x Cortex A53 1.2Ghz** - -```bash -$ python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b --------------------------------------------------- -Network Name Mean Inference Time (std dev) --------------------------------------------------- -squeezenet v1.1 94.59 ms (0.04 ms) -mobilenet 148.82 ms (0.18 ms) -resnet-18 347.30 ms (0.25 ms) -vgg-16 crashed due to out of memeory -``` - -- **Huawei P20 Pro / Mate10 Pro (Soc: HiSilicon Kirin 970) : (4 x Cortex A73 2.36GHz + 4 x Cortex A53 1.8GHz)** - -```bash -$ python3 arm_cpu_imagenet_bench.py --device p20pro --rpc-key p20pro --------------------------------------------------- -Network Name Mean Inference Time (std dev) -------------------------------------------------- -squeezenet v1.1 29.33 ms (0.61 ms) -mobilenet 47.47 ms (0.65 ms) -resnet-18 84.71 ms (0.32 ms) -vgg-16 574.62 ms (2.14 ms) - -``` - -- **Google Pixel 2 (Soc: Qualcomm Snapdragon 835) : (4 × Kyro 2.35 GHz, 4 × Kyro 1.9 GHz)** - -```bash -$ python3 arm_cpu_imagenet_bench.py --device pixel2 --rpc-key pixel2 --------------------------------------------------- -Network Name Mean Inference Time (std dev) --------------------------------------------------- -squeezenet v1.1 27.74 ms (0.41 ms) -mobilenet 42.05 ms (0.08 ms) -resnet-18 67.28 ms (0.05 ms) -vgg-16 427.75 ms (8.58 ms) -``` - -### How to run +### ARM CPU +We use RPC infrasturecture in TVM to make device management easy. So you need to use it for reproducing benchmark results. 1. Start an RPC Tracker on the host machine ```bash python3 -m tvm.exec.rpc_tracker ``` -2. Register your device to the tracker +2. Register devices to the tracker * For Linux device - * Build tvm runtime on your device [Help](https://docs.tvm.ai/tutorials/cross_compilation_and_rpc.html#build-tvm-runtime-on-device). + * Build tvm runtime on your device [Help](https://docs.tvm.ai/tutorials/nnvm/deploy_model_on_rasp.html#build-tvm-runtime-on-device) * Register your device to tracker by ```bash python3 -m tvm.exec.rpc_sever --tracker=[HOST_IP]:9190 --key=[DEVICE_KEY] @@ -86,8 +34,7 @@ python3 -m tvm.exec.rpc_tracker ```bash python3 -m tvm.exec.query_rpc_tracker ``` - You should be able to find your devices in `Queue Status`. Make sure - the registration is correct before go ahead. + You should be able to find your devices in `Queue Status`. Make sure the registration is correct before going ahead. For our test environment, one sample output can be ```bash @@ -101,12 +48,12 @@ python3 -m tvm.exec.rpc_tracker rk3399 2 0 rasp3b 8 0 ``` + 4. Run benchmark - We did auto-tuning for the above devices, and release pre-tuned - parameters in [this repo](https://github.com/uwsaml/tvm-distro). + We did auto-tuning for Huawei P20/Mate10 Pro, Google Pixel2, Raspberry Pi3 and Firefly-RK3399, + and release pre-tuned parameters in [this repo](https://github.com/uwsaml/tvm-distro). During compilation, TVM will download these operator parameters automatically. - But we don't tune for other devices, so you can only run benchmark for these devices. ```bash python3 arm_cpu_imagenet_bench.py --device rasp3b --rpc-key rasp3b python3 arm_cpu_imagenet_bench.py --device rk3399 --rpc-key rk3399 @@ -114,10 +61,8 @@ python3 -m tvm.exec.rpc_tracker python3 arm_cpu_imagenet_bench.py --device p20pro --rpc-key p20pro python3 arm_cpu_imagenet_bench.py --device mate10pro --rpc-key mate10pro ``` - - If you do not do tuning and run the benchmark for other devices directly, - the performance is not guaranteed (This is still doable, you can pick a most - similar device and reuse its parameter). - In order to get the best performance, you need to tune for you own device, - please follow [tutorial](404.html). + + If your device has a same SoC of the above device, you can resue these parameters + (e.g. use `llvm -device=arm_cpu -mode=rk3399 -target=aarch64-linux-gnu` as target). + Otherwise, you need to tune for your own devcie, please follow this [tutorial](please_fix_this_later.html). From 767cbcfe2ac129851932b4d0cba7cffd53018ded Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 17:06:26 -0700 Subject: [PATCH 21/76] fix verbose setting --- python/tvm/autotvm/record.py | 17 ++++++----------- python/tvm/autotvm/tuner/graph_tuning.py | 1 - python/tvm/autotvm/tuner/tuner.py | 16 +++++----------- python/tvm/autotvm/tuner/xgboost_cost_model.py | 13 ++++++------- python/tvm/contrib/download.py | 3 +-- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/python/tvm/autotvm/record.py b/python/tvm/autotvm/record.py index 20d56ede8dca..5dcfa5c557d5 100644 --- a/python/tvm/autotvm/record.py +++ b/python/tvm/autotvm/record.py @@ -194,7 +194,7 @@ def __init__(self, records, default=None): self.load(records) - def load(self, records, verbose=0): + def load(self, records): """Load records to this dispatch context Parameters @@ -204,9 +204,6 @@ def load(self, records, verbose=0): If is str, then it should be the filename of a records log file. Each row of this file is an encoded record pair. Otherwise, it is an iterator. - verbose: int, optional - If is 0, output nothing - If is 1, output some debug information """ if isinstance(records, str): records = load_from_file(records) @@ -245,8 +242,7 @@ def load(self, records, verbose=0): best_by_model[key] = (inp, res) break - if verbose: - logging.info("Finish loading %d records", counter) + logging.debug("Finish loading %d records", counter) def query(self, target, workload): if target is None: @@ -354,7 +350,7 @@ def pick_best(in_file, out_file): best_set.remove(measure_str_key(inp)) -def load_op_param(rootpath=_target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH, verbose=0): +def load_op_param(rootpath=_target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH): """Load pre-tuned parameters of operators. This function will load all "*.log" file under root path and select best configs. @@ -362,15 +358,12 @@ def load_op_param(rootpath=_target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH, verbose=0): ---------- rootpath: str, optional The root path of stored parameters - verbose: int, optional - If is 0, output nothing - If is 1, output some debug information """ best_context = ApplyHistoryBest([]) for dirpath, _, filenames in os.walk(rootpath): for filename in filenames: if filename.endswith('.log'): - best_context.load(os.path.join(dirpath, filename), verbose) + best_context.load(os.path.join(dirpath, filename)) assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" DispatchContext.current = best_context @@ -397,6 +390,7 @@ def download_pretuned_op_param(backend): download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/op_param/%s.log" % backend, os.path.join(root_path, backend + ".log"), True, verbose=0) + def list_pretuned_op_param(): """List all available pre-tuned op parameters for targets @@ -420,6 +414,7 @@ def list_pretuned_op_param(): return [(k, info[k]) for k in keys] + """ Usage: This record executable module has three modes. diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py index 9135d5ed1ba1..4cebdc8d12af 100644 --- a/python/tvm/autotvm/tuner/graph_tuning.py +++ b/python/tvm/autotvm/tuner/graph_tuning.py @@ -96,7 +96,6 @@ def tune_tasks(tasks, tuner_obj.tune(n_trial=min(n_trial, len(tsk.config_space)), early_stopping=early_stopping, measure_option=measure_option, - verbose=0, callbacks=[ callback.progress_bar(n_trial, prefix=prefix), callback.log_to_file(tmp_log_file)]) diff --git a/python/tvm/autotvm/tuner/tuner.py b/python/tvm/autotvm/tuner/tuner.py index 361608a464f8..7a3d50debdfc 100644 --- a/python/tvm/autotvm/tuner/tuner.py +++ b/python/tvm/autotvm/tuner/tuner.py @@ -65,7 +65,7 @@ def update(self, inputs, results): """ pass - def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callbacks=()): + def tune(self, n_trial, measure_option, early_stopping=None, callbacks=()): """Begin tuning Parameters @@ -77,9 +77,6 @@ def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callback You should use the return value ot autotvm.measure_option for this argument. early_stopping: int Early stop the tuning when not finding better configs in this number of trials - verbose: int - 0: silent mode, no output - 1: print every measurement result callbacks: List of callable A list of callback functions. The signature of callback function is (Tuner, List of MeasureInput, List of MeasureResult) @@ -114,10 +111,9 @@ def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callback self.best_measure_pair = (inp, res) self.best_iter = i + k - if verbose >= 1: - logging.info("No: %d\tGFLOPS: %.2f/%.2f\tresult: %s\t%s", - i + k + 1, flops / 1e9, self.best_flops / 1e9, - res, config) + logging.debug("No: %d\tGFLOPS: %.2f/%.2f\tresult: %s\t%s", + i + k + 1, flops / 1e9, self.best_flops / 1e9, + res, config) i += len(results) @@ -127,9 +123,7 @@ def tune(self, n_trial, measure_option, early_stopping=None, verbose=1, callback callback(self, inputs, results) if i > self.best_iter + early_stopping: - if verbose >= 1: - logging.info("Early stopped. Best iter: %d.", self.best_iter) - break + logging.debug("Early stopped. Best iter: %d.", self.best_iter) GLOBAL_SCOPE.in_tuning = False del measure_batch diff --git a/python/tvm/autotvm/tuner/xgboost_cost_model.py b/python/tvm/autotvm/tuner/xgboost_cost_model.py index 0a44e72223cc..b49587fd5809 100644 --- a/python/tvm/autotvm/tuner/xgboost_cost_model.py +++ b/python/tvm/autotvm/tuner/xgboost_cost_model.py @@ -164,19 +164,18 @@ def fit(self, xs, ys, plan_size): ], verbose_eval=self.verbose)]) - if self.verbose: - logging.info("train: %.2f\tobs: %d\terror: %d\tn_cache: %d", - time.time() - tic, len(xs), - len(xs) - np.sum(valid_index), - self.feature_cache.size(self.fea_type)) + logging.debug("train: %.2f\tobs: %d\terror: %d\tn_cache: %d", + time.time() - tic, len(xs), + len(xs) - np.sum(valid_index), + self.feature_cache.size(self.fea_type)) def fit_log(self, records, plan_size): tic = time.time() self._reset_pool() args = list(records) - if self.verbose: - logging.info("Load %d entries from history log file", len(args)) + logging.debug("Load %d entries from history log file", len(args)) + if self.fea_type == 'itervar': feature_extract_func = _extract_itervar_feature_log elif self.fea_type == 'knob': diff --git a/python/tvm/contrib/download.py b/python/tvm/contrib/download.py index e9bc8144a45b..434216a2652c 100644 --- a/python/tvm/contrib/download.py +++ b/python/tvm/contrib/download.py @@ -27,8 +27,6 @@ def download(url, path, overwrite=False, size_compare=False, verbose=1): verbose: int, optional Verbose level """ - - import requests if sys.version_info >= (3,): import urllib.request as urllib2 else: @@ -36,6 +34,7 @@ def download(url, path, overwrite=False, size_compare=False, verbose=1): if os.path.isfile(path) and not overwrite: if size_compare: + import requests file_size = os.path.getsize(path) res_head = requests.head(url) res_get = requests.get(url, stream=True) From ab33990b1a9da59c26aced8000dd91904df5bf96 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 17:18:03 -0700 Subject: [PATCH 22/76] fix tutorial --- tutorials/autotvm/{tune_cuda_conv2d.py => tune_conv2d_cuda.py} | 2 +- tutorials/autotvm/tune_simple_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename tutorials/autotvm/{tune_cuda_conv2d.py => tune_conv2d_cuda.py} (99%) diff --git a/tutorials/autotvm/tune_cuda_conv2d.py b/tutorials/autotvm/tune_conv2d_cuda.py similarity index 99% rename from tutorials/autotvm/tune_cuda_conv2d.py rename to tutorials/autotvm/tune_conv2d_cuda.py index 821b7e2bd2bc..c08881345029 100644 --- a/tutorials/autotvm/tune_cuda_conv2d.py +++ b/tutorials/autotvm/tune_conv2d_cuda.py @@ -133,7 +133,7 @@ def conv2d_no_batching(N, H, W, CI, CO, KH, KW, stride, padding): # for this template # logging config (for printing tuning log to screen) -logging.basicConfig(level=logging.INFO, stream=sys.stdout) +logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) # the last layer in resnet N, H, W, CO, CI, KH, KW, strides, padding = 1, 7, 7, 512, 512, 3, 3, (1, 1), (1, 1) diff --git a/tutorials/autotvm/tune_simple_template.py b/tutorials/autotvm/tune_simple_template.py index 747d4fb4d146..382a3e43eaa9 100644 --- a/tutorials/autotvm/tune_simple_template.py +++ b/tutorials/autotvm/tune_simple_template.py @@ -247,7 +247,7 @@ def matmul(N, L, M, dtype): # used to get the best config later. # logging config (for printing tuning log to screen) -logging.basicConfig(level=logging.INFO, stream=sys.stdout) +logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) # use local cpu, measure 5 times for every config to reduce variance measure_option = autotvm.measure_option(mode='local', From 0ffacc9155deecdaab7d445021fe769c8947326a Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 21:38:10 -0700 Subject: [PATCH 23/76] fix tutorial --- python/tvm/autotvm/tuner/tuner.py | 2 ++ tutorials/autotvm/tune_nnvm_arm.py | 11 ++++++----- .../python/integration/test_benchmark_topi_conv2d.py | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/python/tvm/autotvm/tuner/tuner.py b/python/tvm/autotvm/tuner/tuner.py index 7a3d50debdfc..b737a9fc5966 100644 --- a/python/tvm/autotvm/tuner/tuner.py +++ b/python/tvm/autotvm/tuner/tuner.py @@ -124,6 +124,8 @@ def tune(self, n_trial, measure_option, early_stopping=None, callbacks=()): if i > self.best_iter + early_stopping: logging.debug("Early stopped. Best iter: %d.", self.best_iter) + break + GLOBAL_SCOPE.in_tuning = False del measure_batch diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 134bdcbf801e..a4702ef8d6a3 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -15,7 +15,7 @@ these operators, it will query this log file to get the best knob values. We also released pre-tuned parameters for some arm devices. You can go to -`ARM CPU Benchmark `_ +`ARM CPU Benchmark https://github.com/dmlc/tvm/wiki/Benchmark#arm-cpu`_ to see the results. """ @@ -158,14 +158,13 @@ def get_network(name, batch_size): target = tvm.target.create('llvm -device=arm_cpu -target=aarch64-linux-gnu') # Also replace this with the device key in your tracker -device_key = 'rk3399-other' +device_key = 'rk3399' +# tuning option network = 'resnet-18' log_file = "%s.%s.log" % (device_key, network) - dtype = 'float32' -# tuning option tuning_option = { 'log_filename': log_file, 'rpc_device_key': device_key, @@ -200,6 +199,8 @@ def tune_and_evaluate(): tasks = autotvm.task.extract_from_graph(net, shape=shape, dtype=dtype, symbols=(nnvm.sym.conv2d,), target=target) + + # run tuning tasks autotvm.tune_tasks(tasks, **tuning_option) # compile kernels with history best records @@ -244,7 +245,7 @@ def tune_and_evaluate(): (np.mean(prof_res), np.std(prof_res))) # We do not run the tuning in our webpage server. Uncomment this line to run by yourself. -# tune_and_evaluate() +#tune_and_evaluate() ###################################################################### # Sample Output diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 6ee6c0d8ef34..94b8a5d67228 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -22,7 +22,8 @@ def my_clip(x, a_min, a_max): return x def test_cpu_conv2d(): - # load pre-tuned parameters + # download pre-tuned parameters + autotvm.record.download_pretuned_op_param('arm_cpu') autotvm.load_op_param() def run_cpu_conv2d(env, remote, key, batch_size, wl, profile=True): From 2a7fca9785c80de34be71e952b3c55b9fb235e85 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 21:52:12 -0700 Subject: [PATCH 24/76] fix typos --- apps/benchmark/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index 2e307d6099de..bfff7cbacbf6 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -4,10 +4,10 @@ See results on wiki page https://github.com/dmlc/tvm/wiki/Benchmark -## How to reporduce +## How to Reproduce ### ARM CPU -We use RPC infrasturecture in TVM to make device management easy. So you need to use it for reproducing benchmark results. +We use RPC infrastructure in TVM to make device management easy. So you need to use it for reproducing benchmark results. 1. Start an RPC Tracker on the host machine ```bash @@ -26,7 +26,7 @@ python3 -m tvm.exec.rpc_tracker E.g. For my RK3399, I use `python3 -m tvm.exec.rpc_sever --tracker=10.77.1.123:9190 --key=rk3399` * For Android device - * Build and install tvm rpc apk on your device [Help](https://github.com/dmlc/tvm/tree/master/apps/android_rpc). + * Build and install tvm RPC apk on your device [Help](https://github.com/dmlc/tvm/tree/master/apps/android_rpc). Make sure you can pass the android rpc test. Then you have alreadly known how to register. 3. Verify the device registration @@ -62,7 +62,7 @@ python3 -m tvm.exec.rpc_tracker python3 arm_cpu_imagenet_bench.py --device mate10pro --rpc-key mate10pro ``` - If your device has a same SoC of the above device, you can resue these parameters + If your device has a same SoC of the above device, you can reuse these parameters (e.g. use `llvm -device=arm_cpu -mode=rk3399 -target=aarch64-linux-gnu` as target). - Otherwise, you need to tune for your own devcie, please follow this [tutorial](please_fix_this_later.html). + Otherwise, you need to tune for your own device, please follow this [tutorial](please_fix_this_later.html). From 8f25be85f06d14907489c4b2b8e88c0444d3468f Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 23:05:53 -0700 Subject: [PATCH 25/76] add error msg to space --- python/tvm/autotvm/task/space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tvm/autotvm/task/space.py b/python/tvm/autotvm/task/space.py index dd0efdc61635..5127cf0601b8 100644 --- a/python/tvm/autotvm/task/space.py +++ b/python/tvm/autotvm/task/space.py @@ -114,7 +114,7 @@ def __init__(self, var, name=None): elif isinstance(var, VirtualAxis): self.length = var.length else: - raise RuntimeError("Invalid type of axis") + raise RuntimeError("Invalid type of axis: " + type(var)) @staticmethod def get_num_output(var, name=None): From d8e54f85cd11cddeff0144b0f910ddc067e40a47 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Thu, 26 Jul 2018 23:59:41 -0700 Subject: [PATCH 26/76] introduce tophub --- nnvm/python/nnvm/compiler/build_module.py | 4 +- python/tvm/autotvm/__init__.py | 3 +- python/tvm/autotvm/record.py | 75 +----------- python/tvm/autotvm/tophub.py | 107 ++++++++++++++++++ ...download_op_param.py => tophub_manager.py} | 16 +-- python/tvm/target.py | 15 +-- .../integration/test_benchmark_topi_conv2d.py | 5 +- 7 files changed, 130 insertions(+), 95 deletions(-) create mode 100644 python/tvm/autotvm/tophub.py rename python/tvm/exec/{download_op_param.py => tophub_manager.py} (71%) diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index 1de0cef8156a..c08dfd4a7099 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -240,8 +240,8 @@ def build(graph, target=None, shape=None, dtype="float32", target = tvm.target.create(target) if autotvm.task.DispatchContext.current is None: - # load pre-tuned parameters from default directory - autotvm.load_op_param() + # load pre-tuned parameters of operators from the default directory of TopHub + autotvm.tophub.load_context(target) shape = shape if shape else {} if not isinstance(shape, dict): diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index 27df4493224d..c1c43def44bc 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -19,10 +19,11 @@ from . import tuner from . import util from . import env +from . import tophub # some shortcuts from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo from .tuner import callback, tune_tasks from .task import template, get_config, create, ConfigSpace, ConfigEntity -from .record import ApplyHistoryBest as apply_history_best, load_op_param +from .record import ApplyHistoryBest as apply_history_best from .env import GLOBAL_SCOPE diff --git a/python/tvm/autotvm/record.py b/python/tvm/autotvm/record.py index 5dcfa5c557d5..aa25e672df6e 100644 --- a/python/tvm/autotvm/record.py +++ b/python/tvm/autotvm/record.py @@ -9,7 +9,6 @@ import pickle import json import time -import os from collections import OrderedDict import numpy as np @@ -20,9 +19,6 @@ from .task import DispatchContext, ConfigEntity from .measure import MeasureInput, MeasureResult -from ..contrib.util import tempdir -from ..contrib.download import download - AUTOTVM_LOG_VERSION = 0.1 try: # convert unicode to str for python2 @@ -123,8 +119,8 @@ def decode(row, protocol='json'): tgt = _target.create(str(tgt)) def clean_json_to_python(x): - """1. convert all list in x to tuple (hashable) - 2. convert unicode to str for python2 + """1. Convert all list in x to tuple (hashable) + 2. Convert unicode to str for python2 """ if isinstance(x, list): return tuple([clean_json_to_python(a) for a in x]) @@ -154,6 +150,7 @@ def clean_json_to_python(x): else: raise RuntimeError("Invalid log protocol: " + protocol) + def load_from_file(filename): """Generator: load records from file. This is a generator that yields the records. @@ -349,72 +346,6 @@ def pick_best(in_file, out_file): fout.write(encode(inp, res) + "\n") best_set.remove(measure_str_key(inp)) - -def load_op_param(rootpath=_target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH): - """Load pre-tuned parameters of operators. - This function will load all "*.log" file under root path and select best configs. - - Parameters - ---------- - rootpath: str, optional - The root path of stored parameters - """ - best_context = ApplyHistoryBest([]) - for dirpath, _, filenames in os.walk(rootpath): - for filename in filenames: - if filename.endswith('.log'): - best_context.load(os.path.join(dirpath, filename)) - - assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" - DispatchContext.current = best_context - - -def download_pretuned_op_param(backend): - """Download pre-tuned parameters of operators for a backend - - Parameters - ---------- - backend: str - The compilation target - """ - root_path = _target.AUTOTVM_PRETUNED_PARAM_ROOT_PATH - if not os.path.isdir(root_path): - # make directory - splits = os.path.split(root_path) - for j in range(1, len(splits)+1): - path = os.path.join(*splits[:j]) - if not os.path.isdir(path): - os.mkdir(path) - - print("Download pre-tuned parameters for %s" % backend) - download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/op_param/%s.log" % backend, - os.path.join(root_path, backend + ".log"), True, verbose=0) - - -def list_pretuned_op_param(): - """List all available pre-tuned op parameters for targets - - Returns - ------- - ret: List - All available packets - """ - path = tempdir() - filename = path.relpath("info.json") - print("Download meta info for pre-tuned parameters") - download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/op_param/info.json", - filename, True, verbose=0) - print("") - - with open(filename, "r") as fin: - text = "".join(fin.readlines()) - info = json.loads(text) - keys = list(info.keys()) - keys.sort() - - return [(k, info[k]) for k in keys] - - """ Usage: This record executable module has three modes. diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py new file mode 100644 index 000000000000..c4f5506cc660 --- /dev/null +++ b/python/tvm/autotvm/tophub.py @@ -0,0 +1,107 @@ +""" +TopHub: Tensor Operator Hub +To get the best performance, we typically need auto-tuning for the specific devices. +TVM releases pre-tuned parameters in TopHub (https://github.com/uwsaml/tvm-distro) +for some common networks and hardware targets. +TVM will donwload these parameters for you when you create the target for the first time. +""" +import os +import json + +from .task import DispatchContext +from .record import ApplyHistoryBest +from ..contrib.util import tempdir +from ..contrib.download import download + +AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") + +def load_context(target, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): + """Load dispatch context with pre-tuned parameters. + This function will load corresponding "*.log" files under root path + and select the best configs. + + Parameters + ---------- + target: Target + The compilation target + rootpath: str, optional + The root path of stored parameters + """ + best_context = ApplyHistoryBest([]) + + big_target = str(target).split()[0] + if os.path.isfile(os.path.join(rootpath, big_target + ".log")): + best_context.load(os.path.join(rootpath, big_target + ".log")) + + for opt in target.options: + if opt.startswith("-device"): + model = opt[8:] + if os.path.isfile(os.path.join(rootpath, model) + ".log"): + best_context.load(os.path.join(rootpath, model) + ".log") + + assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" + DispatchContext.current = best_context + + +def download_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): + """Download pre-tuned parameters of operators for a backend + + Parameters + ---------- + backend: str + The name of package + rootpath: str, optional + The root path of stored parameters + """ + if not os.path.isdir(rootpath): + # make directory + splits = os.path.split(rootpath) + for j in range(1, len(splits)+1): + path = os.path.join(*splits[:j]) + if not os.path.isdir(path): + os.mkdir(path) + + print("Download pre-tuned parameters for %s" % backend) + download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/%s.log" % backend, + os.path.join(rootpath, backend + ".log"), True, verbose=0) + + +def check_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): + """Check whether have pre-tuned parameters of the certain target. + If not, will download it. + + Parameters + ---------- + backend: str + The name of package + rootpath: str, optional + The root path of stored parameters + """ + if os.path.isfile(os.path.join(rootpath, backend + ".log")): + return + + download_package(backend) + + +def list_packages(): + """List all available pre-tuned op parameters for targets + + Returns + ------- + ret: List + All available packets + """ + path = tempdir() + filename = path.relpath("info.json") + print("Download meta info for pre-tuned parameters") + download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/info.json", + filename, True, verbose=0) + print("") + + with open(filename, "r") as fin: + text = "".join(fin.readlines()) + info = json.loads(text) + keys = list(info.keys()) + keys.sort() + + return [(k, info[k]) for k in keys] diff --git a/python/tvm/exec/download_op_param.py b/python/tvm/exec/tophub_manager.py similarity index 71% rename from python/tvm/exec/download_op_param.py rename to python/tvm/exec/tophub_manager.py index 4e00c929b731..ad198a8e537e 100644 --- a/python/tvm/exec/download_op_param.py +++ b/python/tvm/exec/tophub_manager.py @@ -4,11 +4,11 @@ import argparse import logging -from ..autotvm.record import list_pretuned_op_param, download_pretuned_op_param +from ..autotvm.tophub import list_packages, download_package if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument("--target", type=str, nargs='+', + parser.add_argument("--download", type=str, nargs='+', help="Target to download. Use 'all' to download for all targets") parser.add_argument("-l", action='store_true', help="List available packages") args = parser.parse_args() @@ -16,21 +16,21 @@ logging.basicConfig(level=logging.INFO) if args.l: - info = list_pretuned_op_param() + info = list_packages() print("\n%-20s %-20s" % ("Target", "Size")) print("-" * 41) for target, info in info: print("%-20s %-20s" % (target, "%.2f MB" % (info['size']/1000000))) - if args.target: - info = list_pretuned_op_param() + if args.download: + info = list_packages() all_targets = [x[0] for x in info] - if 'all' in args.target: + if 'all' in args.download: targets = all_targets else: - targets = args.target + targets = args.download for t in targets: if t not in all_targets: print("Warning : cannot find tuned parameters of " + t + ". (ignored)") - download_pretuned_op_param(t) + download_package(t) diff --git a/python/tvm/target.py b/python/tvm/target.py index cc85e56e9e25..224614839d3e 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -40,8 +40,6 @@ """ from __future__ import absolute_import -import os - from ._ffi.base import _LIB_NAME from ._ffi.node import NodeBase, register_node from . import _api_internal @@ -53,8 +51,6 @@ if _LIB_NAME != "libtvm_runtime.so": raise err_msg -AUTOTVM_PRETUNED_PARAM_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "op_param") - def _merge_opts(opts, new_opts): """Helper function to merge options""" if isinstance(new_opts, str): @@ -426,7 +422,9 @@ def arm_cpu(model='unknown', options=None): options : str or list of str Additional options """ - opt_table = { + from . import autotvm + + trans_table = { "pixel2": ["-model=snapdragon835", "-target=arm64-linux-android"], "mate10": ["-model=kirin970", "-target=arm64-linux-android"], "mate10pro": ["-model=kirin970", "-target=arm64-linux-android"], @@ -436,11 +434,10 @@ def arm_cpu(model='unknown', options=None): "rk3399": ["-model=rk3399", "-target=aarch64-linux-gnu"], "pynq": ["-model=pynq", "-target=armv7a-linux-eabi"], } - pre_defined_opt = opt_table.get(model, []) + pre_defined_opt = trans_table.get(model, []) - if not os.path.isfile(os.path.join(AUTOTVM_PRETUNED_PARAM_ROOT_PATH, "arm_cpu.log")): - from .autotvm.record import download_pretuned_op_param - download_pretuned_op_param("arm_cpu") + # download pre-tuned parameters for arm_cpu if there is not any. + autotvm.tophub.check_package('arm_cpu') opts = ["-device=arm_cpu"] + pre_defined_opt opts = _merge_opts(opts, options) diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 94b8a5d67228..56fb463d94be 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -22,9 +22,8 @@ def my_clip(x, a_min, a_max): return x def test_cpu_conv2d(): - # download pre-tuned parameters - autotvm.record.download_pretuned_op_param('arm_cpu') - autotvm.load_op_param() + # download pre-tuned parameters and load the dispatch context + autotvm.tophub.load_context(tvm.target.arm_cpu()) def run_cpu_conv2d(env, remote, key, batch_size, wl, profile=True): data_shape = (batch_size, wl.in_filter, wl.height, wl.width) From 1dd107a57ddb1eb89bd913d5eb1480e760f0f2bf Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Fri, 27 Jul 2018 00:49:05 -0700 Subject: [PATCH 27/76] fix doc --- python/tvm/autotvm/task/space.py | 2 +- topi/python/topi/nn/conv2d.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/python/tvm/autotvm/task/space.py b/python/tvm/autotvm/task/space.py index 5127cf0601b8..06119d13d384 100644 --- a/python/tvm/autotvm/task/space.py +++ b/python/tvm/autotvm/task/space.py @@ -114,7 +114,7 @@ def __init__(self, var, name=None): elif isinstance(var, VirtualAxis): self.length = var.length else: - raise RuntimeError("Invalid type of axis: " + type(var)) + raise RuntimeError("Invalid type of axis: " + str(type(var))) @staticmethod def get_num_output(var, name=None): diff --git a/topi/python/topi/nn/conv2d.py b/topi/python/topi/nn/conv2d.py index 897ebfcaa110..e0d2c403d4b4 100644 --- a/topi/python/topi/nn/conv2d.py +++ b/topi/python/topi/nn/conv2d.py @@ -244,7 +244,7 @@ def conv2d_nhwc(Input, Filter, stride, padding, out_dtype='float32'): Returns ------- output : tvm.Tensor - 4-D with shape [batch, out_height, out_width, out_channel] + 4-D with shape [batch, out_height, out_width, out_channel] """ assert isinstance(stride, int) or len(stride) == 2 batch, in_height, in_width, in_channel = Input.shape @@ -327,9 +327,14 @@ def conv2d_winograd_weight_transform(kernel, tile_size): Parameters ---------- kernel: Tensor - The raw kernel tensor + The raw kernel tensor with layout "NCHW". Only 3x3 kernel is supported for now tile_size: int Tile size of winograd transform. e.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3) + + Returns + ------- + output : tvm.Tensor + 4-D with shape [alpha, alpha, CO, CI] """ K = 3 @@ -385,5 +390,10 @@ def conv2d_winograd_without_weight_transform(input, filter, strides, padding, Padding size, or ['VALID', 'SAME'] tile_size: int Tile size of winograd transform. e.g. 2 for F(2x2, 3x3) and 4 for F(4x4, 3x3) + + Returns + ------- + output : tvm.Tensor + 4-D with shape [batch, out_height, out_width, out_channel] """ raise ValueError("missing register for topi.nn.conv2d_winograd_without_weight_transform") From e112643d7b43026bd9723fad99b2792245be8fac Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Fri, 27 Jul 2018 11:22:27 -0700 Subject: [PATCH 28/76] update type for python2 --- .../_ffi/_cy3/.nfs0000000000b253d70000104d | Bin 0 -> 654640 bytes python/tvm/autotvm/task/space.py | 7 +++- python/tvm/autotvm/task/task.py | 3 +- python/tvm/contrib/util.py | 33 ++++++++++++++++++ topi/python/topi/arm_cpu/conv2d.py | 16 +++++---- tutorials/cross_compilation_and_rpc.py | 6 +--- tutorials/nnvm/deploy_model_on_mali_gpu.py | 2 +- tutorials/nnvm/deploy_model_on_rasp.py | 6 ++-- 8 files changed, 54 insertions(+), 19 deletions(-) create mode 100755 python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d diff --git a/python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d b/python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d new file mode 100755 index 0000000000000000000000000000000000000000..802616b23fc49f453f790f355db3e39aa518738f GIT binary patch literal 654640 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

MpH}|PEN~hd-E$Qg`ef|goO1EpfA}AW<6^y> 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 From 7a41c805d3287558b41a612ff0fc5f608a7c9bd6 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sun, 29 Jul 2018 23:56:44 -0700 Subject: [PATCH 42/76] undo x86 change --- topi/python/topi/x86/conv2d.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/topi/python/topi/x86/conv2d.py b/topi/python/topi/x86/conv2d.py index afcb4b73f731..801a264d306f 100644 --- a/topi/python/topi/x86/conv2d.py +++ b/topi/python/topi/x86/conv2d.py @@ -215,8 +215,7 @@ def default_schedule(op): s[C].reorder(fused, rc, h, wo, ry, rx, wi) # move rc to outer loop s[C].unroll(rx) s[C].unroll(ry) - if w.dom.extent.value % 16 == 0: - s[C].vectorize(wi) + s[C].vectorize(wi) def traverse(op): """Traverse operators from computation graph""" From aa72bb32d4f60eff1e3cf57862465050f2fd2baa Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 01:05:50 -0700 Subject: [PATCH 43/76] fix naming --- python/tvm/autotvm/task/__init__.py | 4 +- python/tvm/autotvm/task/nnvm_integration.py | 150 +++--------------- python/tvm/autotvm/task/topi_integration.py | 141 ++++++++++++++++ python/tvm/autotvm/tuner/graph_tuning.py | 2 +- .../tvm/autotvm/tuner/sa_model_optimizer.py | 30 ++-- .../tvm/autotvm/tuner/xgboost_cost_model.py | 25 ++- python/tvm/autotvm/tuner/xgboost_tuner.py | 10 +- .../tvm/exec/{tophub_manager.py => tophub.py} | 4 +- 8 files changed, 196 insertions(+), 170 deletions(-) create mode 100644 python/tvm/autotvm/task/topi_integration.py rename python/tvm/exec/{tophub_manager.py => tophub.py} (90%) diff --git a/python/tvm/autotvm/task/__init__.py b/python/tvm/autotvm/task/__init__.py index 3bdfb7ea7399..1247d5625397 100644 --- a/python/tvm/autotvm/task/__init__.py +++ b/python/tvm/autotvm/task/__init__.py @@ -11,5 +11,5 @@ from .code_hash import attach_code_hash, attach_code_hash_to_arg from .dispatcher import DispatchContext, ApplyConfig, dispatcher -from .nnvm_integration import register_topi_compute, register_topi_schedule,\ - extract_from_graph +from .topi_integration import register_topi_compute, register_topi_schedule +from .nnvm_integration import extract_from_graph diff --git a/python/tvm/autotvm/task/nnvm_integration.py b/python/tvm/autotvm/task/nnvm_integration.py index 7f545eece310..3e6301482b6c 100644 --- a/python/tvm/autotvm/task/nnvm_integration.py +++ b/python/tvm/autotvm/task/nnvm_integration.py @@ -1,147 +1,23 @@ # pylint: disable=unused-variable,invalid-name """ Decorator and utilities for the integration with TOPI and NNVM + """ import warnings -from ... import _api_internal, tensor, placeholder, target as _target - -from ..util import get_const_tuple, get_func_name -from .task import args_to_workload, dispatcher, create, register - -# Decorators for registering templates to topi -# a table that records all registered dispatcher for all targets -_REGISTED_DISPATHCER = { -} +from ... import tensor, placeholder, target as _target -def register_topi_compute(topi_compute, target_keys, template_keys): - """Register a tunable template for a topi compute function +from ..util import get_const_tuple +from .task import create, register - Parameters - ---------- - topi_compute: callable - The overloaded topi compute call - target_keys: str or list of str - The compilation target - template_keys: str or list of str - The template key - Returns - ------- - decorator: callable - A decorator - """ - fname = get_func_name(topi_compute) - - def _decorator(func=None): - """If call this function without argument, then we will reuse the function body - of original function""" - targets = [target_keys] if isinstance(target_keys, str) else target_keys - for target_key in targets: - if target_key not in _REGISTED_DISPATHCER: - _REGISTED_DISPATHCER[target_key] = {} - if topi_compute not in _REGISTED_DISPATHCER: - @topi_compute.register(target_key) - @dispatcher - def config_dispatcher(*args, **kwargs): - """override topi call as a config dispatcher""" - assert not kwargs, "Do not support kwargs in template function call" - return (fname, ) + args_to_workload(args) - _REGISTED_DISPATHCER[target_key][topi_compute] = config_dispatcher - - config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_compute] - - @config_dispatcher.register(template_keys) - def template_call(cfg, *args, **kwargs): - """call the topi func and attach workload to compute node""" - assert not kwargs, "Do not support kwargs in template function call" - if func is None: - node = topi_compute.fdefault(*args, **kwargs) - else: - node = func(cfg, *args, **kwargs) - - # attach workload to return op - op = node.op - attrs = {} - for k, v in node.op.attrs.items(): - attrs[k] = v - attrs['workload'] = (fname, ) + args_to_workload(args) - if isinstance(op, tensor.ComputeOp): - op = _api_internal._ComputeOp( - op.name, op.tag, attrs, op.axis, op.body) - elif isinstance(op, tensor.ExternOp): - op = _api_internal._ExternOp( - op.name, op.tag, attrs, - op.inputs, op.input_placeholders, - op.output_placeholders, op.body) - else: - raise RuntimeError("Unsupported op type: " + type(op)) - - if isinstance(node, tensor.Tensor): - return op.output(0) - return [op.output(i) for i in range(len(node))] - - return _decorator - -def register_topi_schedule(topi_schedule, target_keys, template_keys): - """Register a tunable template for a topi schedule function +def serialize_args(args): + """serialize arguments of a topi function to a hashable tuple. Parameters ---------- - topi_schedule: callable - The overloaded topi schedule call - target_keys: str or list of str - The compilation target - template_keys: str or list of str - The template key - - Returns - ------- - decorator: callable - A decorator + args: list of hashable or Tensor """ - def _decorator(func): - targets = [target_keys] if isinstance(target_keys, str) else target_keys - for target_key in targets: - if target_key not in _REGISTED_DISPATHCER: - _REGISTED_DISPATHCER[target_key] = {} - if topi_schedule not in _REGISTED_DISPATHCER[target_key]: - @topi_schedule.register(target_key) - @dispatcher - def config_dispatcher(outs): - """override topi call as a workload dispatcher""" - def traverse(tensors): - """traverse all ops to find attached workload""" - for t in tensors: - op = t.op - if 'workload' in op.attrs: - return op.attrs['workload'] - wkl = traverse(op.input_tensors) - if wkl: - return wkl - return None - - outs = [outs] if isinstance(outs, tensor.Tensor) else outs - workload = traverse(outs) - - if workload is None: - raise RuntimeError("Cannot find workload in attribute of this schedule") - - return args_to_workload(workload) - - _REGISTED_DISPATHCER[target_key][topi_schedule] = config_dispatcher - - config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_schedule] - - @config_dispatcher.register(template_keys) - def template_call(cfg, outs): - """call the schedule func""" - return func(cfg, outs) - - return _decorator - - -def serialize_args(args): ret = [] for t in args: if isinstance(t, tensor.Tensor): @@ -152,6 +28,12 @@ def serialize_args(args): def deserialize_args(args): + """The inverse function of :code:`serialize_args`. + + Parameters + ---------- + args: list of hashable or Tensor + """ ret = [] for t in args: if isinstance(t, tuple) and t[0] == 'TENSOR': @@ -243,8 +125,12 @@ def get(): TaskExtractEnv.current = TaskExtractEnv() return TaskExtractEnv.current + def extract_from_graph(graph, shape, dtype, target, symbols, target_host=None): - """ Extract tuning tasks from a nnvm graph + """ Extract tuning tasks from a nnvm graph. + + This function collects tunning tasks by building the graph + with a "dummy" target and tracing all the calls to topi. Parameters ---------- diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py new file mode 100644 index 000000000000..b6e0eb12463e --- /dev/null +++ b/python/tvm/autotvm/task/topi_integration.py @@ -0,0 +1,141 @@ +# pylint: disable=unused-variable,invalid-name +""" +Decorators for registering tunable templates to topi +""" + +from ... import _api_internal, tensor + +from ..util import get_func_name +from .task import args_to_workload, dispatcher, register + + +# A table that records all registered dispatcher for all targets +_REGISTED_DISPATHCER = { +} + + +def register_topi_compute(topi_compute, target_keys, template_keys): + """Register a tunable template for a topi compute function + + Parameters + ---------- + topi_compute: GenericFunc + The overloaded topi compute call + target_keys: str or list of str + The compilation target + template_keys: str or list of str + The template key + + Returns + ------- + decorator: callable + A decorator + """ + fname = get_func_name(topi_compute) + + def _decorator(func=None): + """If call this function without argument, then we will reuse the function body + of original function""" + targets = [target_keys] if isinstance(target_keys, str) else target_keys + for target_key in targets: + if target_key not in _REGISTED_DISPATHCER: + _REGISTED_DISPATHCER[target_key] = {} + if topi_compute not in _REGISTED_DISPATHCER: + @topi_compute.register(target_key) + @dispatcher + def config_dispatcher(*args, **kwargs): + """override topi call as a config dispatcher""" + assert not kwargs, "Do not support kwargs in template function call" + return (fname, ) + args_to_workload(args) + _REGISTED_DISPATHCER[target_key][topi_compute] = config_dispatcher + + config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_compute] + + @config_dispatcher.register(template_keys) + def template_call(cfg, *args, **kwargs): + """call the topi func and attach workload to compute node""" + assert not kwargs, "Do not support kwargs in template function call" + if func is None: + node = topi_compute.fdefault(*args, **kwargs) + else: + node = func(cfg, *args, **kwargs) + + # attach workload to return op + op = node.op + attrs = {} + for k, v in node.op.attrs.items(): + attrs[k] = v + attrs['workload'] = (fname, ) + args_to_workload(args) + if isinstance(op, tensor.ComputeOp): + op = _api_internal._ComputeOp( + op.name, op.tag, attrs, op.axis, op.body) + elif isinstance(op, tensor.ExternOp): + op = _api_internal._ExternOp( + op.name, op.tag, attrs, + op.inputs, op.input_placeholders, + op.output_placeholders, op.body) + else: + raise RuntimeError("Unsupported op type: " + type(op)) + + if isinstance(node, tensor.Tensor): + return op.output(0) + return [op.output(i) for i in range(len(node))] + + return _decorator + +def register_topi_schedule(topi_schedule, target_keys, template_keys): + """Register a tunable template for a topi schedule function + + Parameters + ---------- + topi_schedule: GenericFunc + The overloaded topi schedule call + target_keys: str or list of str + The compilation target + template_keys: str or list of str + The template key + + Returns + ------- + decorator: callable + A decorator + """ + def _decorator(func): + targets = [target_keys] if isinstance(target_keys, str) else target_keys + for target_key in targets: + if target_key not in _REGISTED_DISPATHCER: + _REGISTED_DISPATHCER[target_key] = {} + if topi_schedule not in _REGISTED_DISPATHCER[target_key]: + @topi_schedule.register(target_key) + @dispatcher + def config_dispatcher(outs): + """override topi call as a workload dispatcher""" + def traverse(tensors): + """traverse all ops to find attached workload""" + for t in tensors: + op = t.op + if 'workload' in op.attrs: + return op.attrs['workload'] + wkl = traverse(op.input_tensors) + if wkl: + return wkl + return None + + outs = [outs] if isinstance(outs, tensor.Tensor) else outs + workload = traverse(outs) + + if workload is None: + raise RuntimeError("Cannot find workload in attribute of this schedule") + + return args_to_workload(workload) + + _REGISTED_DISPATHCER[target_key][topi_schedule] = config_dispatcher + + config_dispatcher = _REGISTED_DISPATHCER[target_key][topi_schedule] + + @config_dispatcher.register(template_keys) + def template_call(cfg, outs): + """call the schedule func""" + return func(cfg, outs) + + return _decorator diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py index 797f305255cc..c569590862c7 100644 --- a/python/tvm/autotvm/tuner/graph_tuning.py +++ b/python/tvm/autotvm/tuner/graph_tuning.py @@ -58,7 +58,7 @@ def tune_tasks(tasks, # create tuner if tuner == 'xgb' or tuner == 'xgb-rank': - tuner_obj = XGBTuner(tsk, loss_type='rank', verbose=0) + tuner_obj = XGBTuner(tsk, loss_type='rank') elif tuner == 'ga': tuner_obj = GATuner(tsk, pop_size=50) elif tuner == 'random': diff --git a/python/tvm/autotvm/tuner/sa_model_optimizer.py b/python/tvm/autotvm/tuner/sa_model_optimizer.py index 5ba305fcfbee..32e7400d3ac1 100644 --- a/python/tvm/autotvm/tuner/sa_model_optimizer.py +++ b/python/tvm/autotvm/tuner/sa_model_optimizer.py @@ -26,11 +26,11 @@ class SimulatedAnnealingOptimizer(ModelOptimizer): If is an Array, then perform linear cooling from temp[0] to temp[1] early_stop: int, optional Stop iteration if the optimal set do not change in `early_stop` rounds - verbose: int, optional - Print log every `verbose` iterations + log_interval: int, optional + Print log every `log_interval` iterations """ def __init__(self, task, n_iter=500, temp=(1, 0), persistent=True, parallel_size=128, - early_stop=50, verbose=50): + early_stop=50, log_interval=50): super(SimulatedAnnealingOptimizer, self).__init__() self.task = task @@ -41,12 +41,13 @@ def __init__(self, task, n_iter=500, temp=(1, 0), persistent=True, parallel_size self.persistent = persistent self.parallel_size = min(parallel_size, len(self.task.config_space)) self.early_stop = early_stop or 1e9 - self.verbose = verbose + self.log_interval = log_interval self.points = None def find_maximums(self, model, num, exclusive): tic = time.time() - temp, n_iter, early_stop, verbose = self.temp, self.n_iter, self.early_stop, self.verbose + temp, n_iter, early_stop, log_interval = \ + self.temp, self.n_iter, self.early_stop, self.log_interval if self.persistent and self.points is not None: points = self.points @@ -100,19 +101,18 @@ def find_maximums(self, model, num, exclusive): k += 1 t -= cool - if verbose >= 1 and k % verbose == 0: + if log_interval and k % log_interval == 0: t_str = "%.2f" % t - logging.info("SA iter: %d\tlast_update: %d\tmax-0: %.2f\tmax-1: %.2f\ttemp: %s\t" - "elapsed: %.2f", - k, k_last_modify, heap_items[0][0], - np.max([v for v, _ in heap_items]), t_str, - time.time() - tic) + logging.debug("SA iter: %d\tlast_update: %d\tmax-0: %.2f\tmax-1: %.2f\ttemp: %s\t" + "elapsed: %.2f", + k, k_last_modify, heap_items[0][0], + np.max([v for v, _ in heap_items]), t_str, + time.time() - tic) heap_items.sort(key=lambda item: -item[0]) - if verbose: - logging.info("SA iter: %d\tlast_update: %d\tmax-0: %.2f\tmax-1: %.2f\telapsed: %.2f", - k, k_last_modify, heap_items[-1][0], heap_items[0][0], time.time() - tic) - logging.info("SA Maximums: %s", heap_items) + logging.debug("SA iter: %d\tlast_update: %d\tmax-0: %.2f\tmax-1: %.2f\telapsed: %.2f", + k, k_last_modify, heap_items[-1][0], heap_items[0][0], time.time() - tic) + logging.debug("SA Maximums: %s", heap_items) if self.persistent: self.points = points diff --git a/python/tvm/autotvm/tuner/xgboost_cost_model.py b/python/tvm/autotvm/tuner/xgboost_cost_model.py index b49587fd5809..7190fab04d01 100644 --- a/python/tvm/autotvm/tuner/xgboost_cost_model.py +++ b/python/tvm/autotvm/tuner/xgboost_cost_model.py @@ -42,10 +42,10 @@ class XGBoostCostModel(CostModel): The cost model predicts relative rank score. num_threads: int, optional The number of threads. - verbose: int, optional - If is not none, the cost model will print training log every `verbose` iterations. + log_interval: int, optional + If is not none, the cost model will print training log every `log_interval` iterations. """ - def __init__(self, task, feature_type, loss_type, num_threads=None, verbose=20): + def __init__(self, task, feature_type, loss_type, num_threads=None, log_interval=25): super(XGBoostCostModel, self).__init__() if xgb is None: @@ -60,7 +60,7 @@ def __init__(self, task, feature_type, loss_type, num_threads=None, verbose=20): self.fea_type = feature_type self.loss_type = loss_type self.num_threads = num_threads - self.verbose = verbose + self.log_interval = log_interval if loss_type == 'reg': self.xgb_params = { @@ -162,9 +162,9 @@ def fit(self, xs, ys, plan_size): fevals=[ xgb_average_recalln_curve_score(plan_size), ], - verbose_eval=self.verbose)]) + verbose_eval=self.log_interval)]) - logging.debug("train: %.2f\tobs: %d\terror: %d\tn_cache: %d", + logging.debug("XGB train: %.2f\tobs: %d\terror: %d\tn_cache: %d", time.time() - tic, len(xs), len(xs) - np.sum(valid_index), self.feature_cache.size(self.fea_type)) @@ -174,7 +174,7 @@ def fit_log(self, records, plan_size): self._reset_pool() args = list(records) - logging.debug("Load %d entries from history log file", len(args)) + logging.debug("XGB load %d entries from history log file", len(args)) if self.fea_type == 'itervar': feature_extract_func = _extract_itervar_feature_log @@ -208,10 +208,9 @@ def fit_log(self, records, plan_size): fevals=[ xgb_average_recalln_curve_score(plan_size), ], - verbose_eval=self.verbose)]) + verbose_eval=self.log_interval)]) - if self.verbose: - logging.info("train: %.2f\tobs: %d", time.time() - tic, len(xs)) + logging.debug("XGB train: %.2f\tobs: %d", time.time() - tic, len(xs)) def predict(self, xs, output_margin=False): feas = self._get_feature(xs) @@ -238,7 +237,7 @@ def load_basemodel(self, base_model): def clone_new(self): return XGBoostCostModel(self.task, self.fea_type, self.loss_type, - self.num_threads, self.verbose) + self.num_threads, self.log_interval) def _get_feature(self, indexes): """get features for indexes, run extraction if we do not have cache for them""" @@ -406,7 +405,7 @@ def callback(env): infos.append("%s: %.6f" % (item[0], item[1])) if not isinstance(verbose_eval, bool) and verbose_eval and i % verbose_eval == 0: - logging.info("\t".join(infos)) + logging.debug("\t".join(infos)) if log_file: with open(log_file, "a") as fout: fout.write("\t".join(infos) + '\n') @@ -438,7 +437,7 @@ def callback(env): elif env.iteration - best_iteration >= stopping_rounds: best_msg = state['best_msg'] if verbose_eval and env.rank == 0: - logging.info("Stopping. Best iteration: %s ", best_msg) + logging.debug("XGB stopped. Best iteration: %s ", best_msg) raise EarlyStopException(best_iteration) return callback diff --git a/python/tvm/autotvm/tuner/xgboost_tuner.py b/python/tvm/autotvm/tuner/xgboost_tuner.py index a8e12d6164d3..237ac4e19ab1 100644 --- a/python/tvm/autotvm/tuner/xgboost_tuner.py +++ b/python/tvm/autotvm/tuner/xgboost_tuner.py @@ -40,21 +40,21 @@ class XGBTuner(ModelBasedTuner): If is not None, the tuner will first select top-(plan_size * diversity_filter_ratio) candidates according to the cost model and then pick batch_size of them according to the diversity metric. - verbose: int - The Verbose Level. + log_interval: int, optional + The verbose level. If is 0, output nothing. Otherwise, output debug information every `verbose` iterations. """ def __init__(self, task, plan_size=32, feature_type='itervar', loss_type='rank', num_threads=None, - optimizer='sa', diversity_filter_ratio=None, verbose=50): + optimizer='sa', diversity_filter_ratio=None, log_interval=50): cost_model = XGBoostCostModel(task, feature_type=feature_type, loss_type=loss_type, num_threads=num_threads, - verbose=verbose // 2) + log_interval=log_interval // 2) if optimizer == 'sa': - optimizer = SimulatedAnnealingOptimizer(task, verbose=verbose) + optimizer = SimulatedAnnealingOptimizer(task, log_interval=log_interval) else: assert isinstance(optimizer, ModelOptimizer), "Optimizer must be " \ "a supported name string" \ diff --git a/python/tvm/exec/tophub_manager.py b/python/tvm/exec/tophub.py similarity index 90% rename from python/tvm/exec/tophub_manager.py rename to python/tvm/exec/tophub.py index ad198a8e537e..9dd951a52701 100644 --- a/python/tvm/exec/tophub_manager.py +++ b/python/tvm/exec/tophub.py @@ -10,12 +10,12 @@ parser = argparse.ArgumentParser() parser.add_argument("--download", type=str, nargs='+', help="Target to download. Use 'all' to download for all targets") - parser.add_argument("-l", action='store_true', help="List available packages") + parser.add_argument("-l", "--list", action='store_true', help="List available packages") args = parser.parse_args() logging.basicConfig(level=logging.INFO) - if args.l: + if args.list: info = list_packages() print("\n%-20s %-20s" % ("Target", "Size")) print("-" * 41) From 2492a89b822c9fcc2054511f9c2de7c0af431e6f Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 01:12:43 -0700 Subject: [PATCH 44/76] fix lint --- python/tvm/autotvm/task/nnvm_integration.py | 2 +- python/tvm/autotvm/task/topi_integration.py | 2 +- python/tvm/autotvm/tuner/sa_model_optimizer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/tvm/autotvm/task/nnvm_integration.py b/python/tvm/autotvm/task/nnvm_integration.py index 3e6301482b6c..01bfaad302fa 100644 --- a/python/tvm/autotvm/task/nnvm_integration.py +++ b/python/tvm/autotvm/task/nnvm_integration.py @@ -128,7 +128,7 @@ def get(): def extract_from_graph(graph, shape, dtype, target, symbols, target_host=None): """ Extract tuning tasks from a nnvm graph. - + This function collects tunning tasks by building the graph with a "dummy" target and tracing all the calls to topi. diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index b6e0eb12463e..e0327ac54bca 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -6,7 +6,7 @@ from ... import _api_internal, tensor from ..util import get_func_name -from .task import args_to_workload, dispatcher, register +from .task import args_to_workload, dispatcher # A table that records all registered dispatcher for all targets diff --git a/python/tvm/autotvm/tuner/sa_model_optimizer.py b/python/tvm/autotvm/tuner/sa_model_optimizer.py index 32e7400d3ac1..2084e0cb0da6 100644 --- a/python/tvm/autotvm/tuner/sa_model_optimizer.py +++ b/python/tvm/autotvm/tuner/sa_model_optimizer.py @@ -111,7 +111,7 @@ def find_maximums(self, model, num, exclusive): heap_items.sort(key=lambda item: -item[0]) logging.debug("SA iter: %d\tlast_update: %d\tmax-0: %.2f\tmax-1: %.2f\telapsed: %.2f", - k, k_last_modify, heap_items[-1][0], heap_items[0][0], time.time() - tic) + k, k_last_modify, heap_items[-1][0], heap_items[0][0], time.time() - tic) logging.debug("SA Maximums: %s", heap_items) if self.persistent: From 1ea4eb9fa4758978ebd98936c85a0e0918f92883 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 01:24:56 -0700 Subject: [PATCH 45/76] -> log editor --- python/tvm/exec/autotvm_log_editor.py | 44 +++++++++++++++++++++++++++ python/tvm/exec/pick_best.py | 38 ----------------------- 2 files changed, 44 insertions(+), 38 deletions(-) create mode 100644 python/tvm/exec/autotvm_log_editor.py delete mode 100644 python/tvm/exec/pick_best.py diff --git a/python/tvm/exec/autotvm_log_editor.py b/python/tvm/exec/autotvm_log_editor.py new file mode 100644 index 000000000000..951dfa1b2e35 --- /dev/null +++ b/python/tvm/exec/autotvm_log_editor.py @@ -0,0 +1,44 @@ +# pylint: disable=invalid-name +"""Pick best log entries from a large file and store them to a small file""" + +import argparse +import os +import logging +import warnings + +from .. import autotvm + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--act", type=str, choices=['pick-best'], + help="The action") + parser.add_argument("--i", type=str, help="The input file or directory") + parser.add_argument("--o", type=str, help="The output file") + + args = parser.parse_args() + logging.basicConfig(level=logging.INFO) + + if args.act == 'pick-best': + if os.path.isfile(args.i): + args.o = args.o or args.i + ".best.log" + autotvm.record.pick_best(args.i, args.o) + elif os.path.isdir(args.i): + args.o = args.o or "best.log" + tmp_filename = args.o + ".tmp" + + with open(tmp_filename, 'w') as tmp_fout: + for filename in os.listdir(args.i): + if filename.endswith(".log"): + try: + autotvm.record.pick_best(filename, tmp_fout) + except Exception: # pylint: disable=broad-except + warnings.warn("Ignore invalid file %s", filename) + + logging.info("Run final filter...") + autotvm.record.pick_best(tmp_filename, args.o) + os.remove(tmp_filename) + logging.info("Output to %s ...", args.o) + else: + raise ValueError("Invalid input file: " + args.i) + else: + raise ValueError("Invalid action " + args.act) diff --git a/python/tvm/exec/pick_best.py b/python/tvm/exec/pick_best.py deleted file mode 100644 index c402048b0d87..000000000000 --- a/python/tvm/exec/pick_best.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=invalid-name -"""Pick best log entries from a large file and store them to a small file""" - -import argparse -import os -import logging -import warnings - -from .. import autotvm - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("--i", type=str, help="The input file or directory") - parser.add_argument("--o", type=str, help="The output file") - - args = parser.parse_args() - logging.basicConfig(level=logging.INFO) - - if os.path.isfile(args.i): - args.o = args.o or args.i + ".best.log" - autotvm.record.pick_best(args.i, args.o) - elif os.path.isdir(args.i): - args.o = args.o or "best.log" - tmp_filename = args.o + ".tmp" - - with open(tmp_filename, 'w') as tmp_fout: - for filename in os.listdir(args.i): - if filename.endswith(".log"): - try: - autotvm.record.pick_best(filename, tmp_fout) - except Exception: # pylint: disable=broad-except - warnings.warn("Ignore invalid file %s", filename) - logging.info("Run final filter...") - autotvm.record.pick_best(tmp_filename, args.o) - os.remove(tmp_filename) - logging.info("Output to %s ...", args.o) - else: - raise ValueError("Invalid input file: " + args.i) From d30a039aad2fe3d4a6310a46062f756f3eda468a Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 14:02:11 -0700 Subject: [PATCH 46/76] fix topi registration --- python/tvm/autotvm/__init__.py | 2 +- python/tvm/autotvm/task/nnvm_integration.py | 2 +- python/tvm/autotvm/task/topi_integration.py | 78 +++++++++++++----- python/tvm/autotvm/tuner/__init__.py | 2 - python/tvm/autotvm/tuner/graph_tuning.py | 85 -------------------- python/tvm/exec/autotvm_log_editor.py | 2 +- topi/python/topi/arm_cpu/conv2d.py | 6 +- topi/python/topi/arm_cpu/depthwise_conv2d.py | 7 +- tutorials/autotvm/tune_nnvm_arm.py | 79 ++++++++++++++++-- 9 files changed, 145 insertions(+), 118 deletions(-) delete mode 100644 python/tvm/autotvm/tuner/graph_tuning.py diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index c3f279ff264a..391b9827ab4c 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -23,7 +23,7 @@ # some shortcuts from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo, use_rpc -from .tuner import callback, tune_tasks +from .tuner import callback from .task import template, get_config, create, ConfigSpace, ConfigEntity from .record import ApplyHistoryBest as apply_history_best from .env import GLOBAL_SCOPE diff --git a/python/tvm/autotvm/task/nnvm_integration.py b/python/tvm/autotvm/task/nnvm_integration.py index 01bfaad302fa..a16527f9cb01 100644 --- a/python/tvm/autotvm/task/nnvm_integration.py +++ b/python/tvm/autotvm/task/nnvm_integration.py @@ -172,6 +172,6 @@ def extract_from_graph(graph, shape, dtype, target, symbols, target_host=None): for task_name, args in env.get_tasks(): tasks.append(create(task_name, args, target=target, target_host=target_host, - template_key='vanilla')) + template_key='direct')) return tasks diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index e0327ac54bca..f9dff664e477 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -1,6 +1,14 @@ # pylint: disable=unused-variable,invalid-name """ -Decorators for registering tunable templates to topi +Decorators for registering tunable templates to TOPI. + +These decorators can make your simple implementation be able to use different configurations +for different workloads. +Here we directly use all arguments to the TOPI call as "workload", so make sure all the arguments +(except tvm.Tensor) in you calls are hashable. For tvm.Tensor, we will serialize it to a hashable +tuple. + +See tvm/topi/python/topi/arm_cpu/depthwise_conv2d.py for example usage. """ from ... import _api_internal, tensor @@ -14,28 +22,42 @@ } -def register_topi_compute(topi_compute, target_keys, template_keys): - """Register a tunable template for a topi compute function +def register_topi_compute(topi_compute, target_keys, template_keys, func=None): + """Register a tunable template for a topi compute function. + + After the registration. This topi compute will become a configuration dispatcher. It uses + all its argument as workload and dispatches configurations according to the input workload. + + It also stores this "workload" to its final ComputeOp, which can be used to reconstruct + "workload" in the following topi_schedule call. Parameters ---------- topi_compute: GenericFunc The overloaded topi compute call target_keys: str or list of str - The compilation target + The compilation target. The same as the argument of GenericFunc.register. template_keys: str or list of str - The template key + The template key. + We might have several strategies for a single operator (e.g. direct, im2col, winograd). + The template key is used to identity the algorithm strategy. + Every operator must have a "direct" template, which is used by default. + func: None or callable + If it is None, return a decorator. + If is callable, decorate this function. Returns ------- decorator: callable A decorator + + Examples + -------- + See tvm/topi/python/topi/arm_cpu/depthwise_conv2d.py for example usage. """ fname = get_func_name(topi_compute) - def _decorator(func=None): - """If call this function without argument, then we will reuse the function body - of original function""" + def _decorator(f): targets = [target_keys] if isinstance(target_keys, str) else target_keys for target_key in targets: if target_key not in _REGISTED_DISPATHCER: @@ -55,10 +77,7 @@ def config_dispatcher(*args, **kwargs): def template_call(cfg, *args, **kwargs): """call the topi func and attach workload to compute node""" assert not kwargs, "Do not support kwargs in template function call" - if func is None: - node = topi_compute.fdefault(*args, **kwargs) - else: - node = func(cfg, *args, **kwargs) + node = f(cfg, *args, **kwargs) # attach workload to return op op = node.op @@ -75,16 +94,26 @@ def template_call(cfg, *args, **kwargs): op.inputs, op.input_placeholders, op.output_placeholders, op.body) else: - raise RuntimeError("Unsupported op type: " + type(op)) + raise RuntimeError("Unsupported op type: " + str(type(op))) if isinstance(node, tensor.Tensor): return op.output(0) return [op.output(i) for i in range(len(node))] + if func: + _decorator(func) + return _decorator -def register_topi_schedule(topi_schedule, target_keys, template_keys): - """Register a tunable template for a topi schedule function + +def register_topi_schedule(topi_schedule, target_keys, template_keys, func=None): + """Register a tunable template for a topi schedule function. + + After the registration. This topi schedule will become a configuration dispatcher. It dispatches + configurations according to the input workload. + + Note that this function will try to find "workload" from all the ComputeOp in the input. + You can attach "workload" to your compute op by using :any:`register_topi_compute`. Parameters ---------- @@ -93,14 +122,24 @@ def register_topi_schedule(topi_schedule, target_keys, template_keys): target_keys: str or list of str The compilation target template_keys: str or list of str - The template key + The template key. + We might have several strategies for a single operator (e.g. direct, im2col, winograd). + The template key is used to identity the algorithm strategy. + Every operator must have a "direct" template, which is used by default. + func: None or callable + If it is None, return a decorator. + If is callable, decorate this function. Returns ------- decorator: callable A decorator + + Examples + -------- + See tvm/topi/python/topi/arm_cpu/depthwise_conv2d.py for example usage. """ - def _decorator(func): + def _decorator(f): targets = [target_keys] if isinstance(target_keys, str) else target_keys for target_key in targets: if target_key not in _REGISTED_DISPATHCER: @@ -136,6 +175,9 @@ def traverse(tensors): @config_dispatcher.register(template_keys) def template_call(cfg, outs): """call the schedule func""" - return func(cfg, outs) + return f(cfg, outs) + + if func: + _decorator(func) return _decorator diff --git a/python/tvm/autotvm/tuner/__init__.py b/python/tvm/autotvm/tuner/__init__.py index c3647ac7dfba..af81442e79f4 100644 --- a/python/tvm/autotvm/tuner/__init__.py +++ b/python/tvm/autotvm/tuner/__init__.py @@ -12,5 +12,3 @@ from .gridsearch_tuner import GridSearchTuner, RandomTuner from .ga_tuner import GATuner from .xgboost_tuner import XGBTuner - -from .graph_tuning import tune_tasks diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py deleted file mode 100644 index c569590862c7..000000000000 --- a/python/tvm/autotvm/tuner/graph_tuning.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Utilities for tuning a whole graph (a set of tasks)""" - -import os -from . import callback, XGBTuner, GATuner, RandomTuner, GridSearchTuner -from .. import record, task - -def tune_tasks(tasks, - measure_option, - tuner='xgb', - n_trial=500, - early_stopping=200, - log_filename='tuning.log', - use_transfer_learning=True, - try_winograd=True): - """ - Tune a set of tasks - - Parameters - ---------- - tasks: Array of Task - A list of tasks to tune - rpc_device_key: str - The key of devices in rpc tracker - tuner: str - The type of tuner. - If is 'xgb', use :any:`XGBTuner`. - If is 'ga', use :any:`GATuner`. - If is 'random', use :any:`RandomTuner`. - If is 'gridsearch', use :any:`GridSearchTuner`. - n_trial: int - The maximum number of trials for a workload - early_stopping: int - The early stopping metric. The tuner will stop when it cannot find better - config after `early_stopping` trials - log_filename: str - The filename of output log file to store best configs - use_transfer_learning: bool - Whether reuse history tuning log to accelerate tuning - try_winograd: bool - Whether try to use winograd template - """ - if try_winograd: - for i in range(len(tasks)): # pylint:disable=consider-using-enumerate - try: # try winograd template - tsk = task.create(tasks[i].name, tasks[i].args, - tasks[i].target, tasks[i].target_host, - 'winograd') - tasks.append(tsk) - except Exception: # pylint:disable=broad-except - pass - - tmp_log_file = log_filename + ".tmp" - if os.path.exists(tmp_log_file): - os.remove(tmp_log_file) - - for i, tsk in enumerate(tasks): - prefix = "[Task %2d/%2d] " %(i+1, len(tasks)) - - # create tuner - if tuner == 'xgb' or tuner == 'xgb-rank': - tuner_obj = XGBTuner(tsk, loss_type='rank') - elif tuner == 'ga': - tuner_obj = GATuner(tsk, pop_size=50) - elif tuner == 'random': - tuner_obj = RandomTuner(tsk) - elif tuner == 'gridsearch': - tuner_obj = GridSearchTuner(tsk) - else: - raise ValueError("Invalid tuner: " + tuner) - - if use_transfer_learning: - if os.path.isfile(tmp_log_file): - tuner_obj.load_history(record.load_from_file(tmp_log_file)) - - # do tuning - tuner_obj.tune(n_trial=min(n_trial, len(tsk.config_space)), - early_stopping=early_stopping, - measure_option=measure_option, - callbacks=[ - callback.progress_bar(n_trial, prefix=prefix), - callback.log_to_file(tmp_log_file)]) - - # pick best records to a cache file - record.pick_best(tmp_log_file, log_filename) - os.remove(tmp_log_file) diff --git a/python/tvm/exec/autotvm_log_editor.py b/python/tvm/exec/autotvm_log_editor.py index 951dfa1b2e35..3256944ac811 100644 --- a/python/tvm/exec/autotvm_log_editor.py +++ b/python/tvm/exec/autotvm_log_editor.py @@ -11,7 +11,7 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument("--act", type=str, choices=['pick-best'], - help="The action") + help="The action") parser.add_argument("--i", type=str, help="The input file or directory") parser.add_argument("--o", type=str, help="The output file") diff --git a/topi/python/topi/arm_cpu/conv2d.py b/topi/python/topi/arm_cpu/conv2d.py index 4a2ee6576e43..23c907c7f3dd 100644 --- a/topi/python/topi/arm_cpu/conv2d.py +++ b/topi/python/topi/arm_cpu/conv2d.py @@ -30,12 +30,12 @@ def config_dispatcher(data, kernel, strides, padding, layout, out_dtype): this template can assign config according to workload""" return _conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype) -@config_dispatcher.register(['vanilla']) +@config_dispatcher.register(['direct']) def decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype): """spatial packing template""" return _decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype, num_tile=2) -@autotvm.task.register_topi_schedule(schedule_conv2d_nchw, 'arm_cpu', ['vanilla', 'winograd']) +@autotvm.task.register_topi_schedule(schedule_conv2d_nchw, 'arm_cpu', ['direct', 'winograd']) def schedule_conv2d_nchw_(cfg, outs): """TOPI schedule callback""" s = tvm.create_schedule([x.op for x in outs]) @@ -495,7 +495,7 @@ def _alter_conv2d_layout(attrs, inputs, tinfos): layout, out_dtype) cfg = autotvm.task.DispatchContext.current.query(tvm.target.current_target(), workload) - if cfg.template_key == 'vanilla': # simple packing + if cfg.template_key == 'direct': # packing weight tensor new_attrs['kernel_layout'] = 'OIHW%do' % (cfg['tile_co'].size[-1]) return sym.conv2d(*copy_inputs, **new_attrs) else: # pre-compute weight transformation in winograd diff --git a/topi/python/topi/arm_cpu/depthwise_conv2d.py b/topi/python/topi/arm_cpu/depthwise_conv2d.py index 55ca24269a5f..65fabddb34df 100644 --- a/topi/python/topi/arm_cpu/depthwise_conv2d.py +++ b/topi/python/topi/arm_cpu/depthwise_conv2d.py @@ -8,9 +8,12 @@ from ..nn import depthwise_conv2d_nchw from ..util import traverse_inline -autotvm.task.register_topi_compute(depthwise_conv2d_nchw, 'arm_cpu', 'vanilla')() +# register original implementation of depthwise_conv2d_nchw since we don't need to change this part +autotvm.task.register_topi_compute(depthwise_conv2d_nchw, 'arm_cpu', 'direct', + depthwise_conv2d_nchw.fdefault) -@autotvm.task.register_topi_schedule(schedule_depthwise_conv2d_nchw, 'arm_cpu', 'vanilla') +# register customized schedule for arm cpu. +@autotvm.task.register_topi_schedule(schedule_depthwise_conv2d_nchw, 'arm_cpu', 'direct') def schedule_depthwise_conv2d_nchw_(cfg, outs): """Schedule depthwise conv2d""" outs = [outs] if isinstance(outs, tvm.tensor.Tensor) else outs diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index c80210a6a383..396bd7bf67c0 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -40,6 +40,7 @@ import nnvm.compiler import tvm from tvm import autotvm +from tvm.autotvm.tuner import XGBTuner, GATuner, RandomTuner, GridSearchTuner from tvm.contrib.util import tempdir import tvm.contrib.graph_runtime as runtime @@ -151,8 +152,8 @@ def get_network(name, batch_size): # ---------------------------- ########################################### -# Begin Tuning -# ------------ +# Set Tuning Options +# ------------------ # Now we can extract tuning tasks from the network and begin tuning. # Replace "aarch64-linux-gnu" with the correct target of your board. @@ -199,6 +200,73 @@ def get_network(name, batch_size): # to use Android NDK for creating shared library. # + +################################################################### +# Begin Tuning +# ------------ +# Now we can begin tuning. Here we provide a simple utility function to tune a list of tasks. +# This function is just an initial implementation which tune them in sequential order. +# Later we will bring more sophisticated tuner scheduler. + + +# You can skip the implementation of function for this tutorial. +def tune_tasks(tasks, + measure_option, + tuner='xgb', + n_trial=500, + early_stopping=200, + log_filename='tuning.log', + use_transfer_learning=True, + try_winograd=True): + if try_winograd: + for i in range(len(tasks)): + try: # try winograd template + tsk = autotvm.task.create(tasks[i].name, tasks[i].args, + tasks[i].target, tasks[i].target_host, 'winograd') + tasks.append(tsk) + except Exception: + pass + + # create tmp log file + tmp_log_file = log_filename + ".tmp" + if os.path.exists(tmp_log_file): + os.remove(tmp_log_file) + + for i, tsk in enumerate(tasks): + prefix = "[Task %2d/%2d] " %(i+1, len(tasks)) + + # create tuner + if tuner == 'xgb' or tuner == 'xgb-rank': + tuner_obj = XGBTuner(tsk, loss_type='rank') + elif tuner == 'ga': + tuner_obj = GATuner(tsk, pop_size=50) + elif tuner == 'random': + tuner_obj = RandomTuner(tsk) + elif tuner == 'gridsearch': + tuner_obj = GridSearchTuner(tsk) + else: + raise ValueError("Invalid tuner: " + tuner) + + if use_transfer_learning: + if os.path.isfile(tmp_log_file): + tuner_obj.load_history(record.load_from_file(tmp_log_file)) + + # do tuning + tuner_obj.tune(n_trial=min(n_trial, len(tsk.config_space)), + early_stopping=early_stopping, + measure_option=measure_option, + callbacks=[ + autotvm.callback.progress_bar(n_trial, prefix=prefix), + autotvm.callback.log_to_file(tmp_log_file)]) + + # pick best records to a cache file + record.pick_best(tmp_log_file, log_filename) + os.remove(tmp_log_file) + + +######################################################################## +# Finally we launch tuning jobs and evaluate the end-to-end performance. + def tune_and_evaluate(): # extract workloads from nnvm graph net, params, shape, out_shape = get_network(network, batch_size=1) @@ -207,7 +275,7 @@ def tune_and_evaluate(): target=target) # run tuning tasks - autotvm.tune_tasks(tasks, **tuning_option) + tune_tasks(tasks, **tuning_option) # compile kernels with history best records with autotvm.apply_history_best(log_file): @@ -244,11 +312,12 @@ def tune_and_evaluate(): # evaluate print("Evaluate inference time cost...") ftimer = module.module.time_evaluator("run", ctx, number=1, repeat=10) - prof_res = np.array(ftimer().results) * 1000 # convert to millionsecond + prof_res = np.array(ftimer().results) * 1000 # convert to million second print("Mean inference time (std dev): %.2f ms (%.2f ms)" % (np.mean(prof_res), np.std(prof_res))) -# We do not run the tuning in our webpage server. Uncomment this line to run by yourself. +# We do not run the tuning in our webpage server since it takes too long. +# Uncomment the following line to run by yourself. #tune_and_evaluate() ###################################################################### From 9ae7e9aa1da47fd29ce0b6aa97e63f253b56d77c Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 14:07:37 -0700 Subject: [PATCH 47/76] fix --- python/tvm/autotvm/task/topi_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index f9dff664e477..92a21f9f5718 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -5,7 +5,7 @@ These decorators can make your simple implementation be able to use different configurations for different workloads. Here we directly use all arguments to the TOPI call as "workload", so make sure all the arguments -(except tvm.Tensor) in you calls are hashable. For tvm.Tensor, we will serialize it to a hashable +(except tvm.Tensor) in you calls are hashable. For tvm.Tensor, we will serialize it to a hashable tuple. See tvm/topi/python/topi/arm_cpu/depthwise_conv2d.py for example usage. From 7086cffccf5239792bd6b42b655a1c130352947c Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 14:14:03 -0700 Subject: [PATCH 48/76] fix --- tutorials/autotvm/tune_nnvm_arm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 396bd7bf67c0..f8f8c8049ce0 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -318,7 +318,7 @@ def tune_and_evaluate(): # We do not run the tuning in our webpage server since it takes too long. # Uncomment the following line to run by yourself. -#tune_and_evaluate() +# tune_and_evaluate() ###################################################################### # Sample Output From 41cf2565993d039832a04f69c1bf471f1b1135b5 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 14:18:20 -0700 Subject: [PATCH 49/76] fix tests --- tests/scripts/task_python_vta.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/scripts/task_python_vta.sh b/tests/scripts/task_python_vta.sh index cd2e0d0b9bae..5d8c47cfdb1a 100755 --- a/tests/scripts/task_python_vta.sh +++ b/tests/scripts/task_python_vta.sh @@ -2,6 +2,9 @@ export PYTHONPATH=python:nnvm/python:vta/python:topi/python +rm -rf python/tvm/*.pyc python/tvm/*/*.pyc python/tvm/*/*/*.pyc python/tvm/*/*/*/*.pyc +rm -rf ~/.tvm + echo "Running unittest..." python -m nose -v vta/tests/python/unittest || exit -1 python3 -m nose -v vta/tests/python/unittest || exit -1 From 94a2726ae929715c5fa1b0114a62289bbf3aa404 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 14:58:19 -0700 Subject: [PATCH 50/76] fix for psutil --- python/tvm/autotvm/measure/local_executor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/tvm/autotvm/measure/local_executor.py b/python/tvm/autotvm/measure/local_executor.py index 9b6e032f00ed..8c1d58e53fb5 100644 --- a/python/tvm/autotvm/measure/local_executor.py +++ b/python/tvm/autotvm/measure/local_executor.py @@ -8,7 +8,10 @@ except ImportError: from Queue import Empty -import psutil +try: + import psutil +except ImportError: + psutil = None from . import executor @@ -121,6 +124,10 @@ def __init__(self, timeout=None, do_fork=True): self.timeout = timeout or executor.Executor.DEFAULT_TIMEOUT self.do_fork = do_fork + if self.do_fork: + if not psutil: + raise RuntimeError("Python package psutil is missing. please try `pip install psutil`") + def submit(self, func, *args, **kwargs): if not self.do_fork: return LocalFutureNoFork(func(*args, **kwargs)) From e72bb6beef41d4cde7fe3d29f4db344d11d443c8 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 15:01:25 -0700 Subject: [PATCH 51/76] fix --- python/tvm/autotvm/measure/local_executor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/tvm/autotvm/measure/local_executor.py b/python/tvm/autotvm/measure/local_executor.py index 8c1d58e53fb5..8a045ecfb4c0 100644 --- a/python/tvm/autotvm/measure/local_executor.py +++ b/python/tvm/autotvm/measure/local_executor.py @@ -126,7 +126,8 @@ def __init__(self, timeout=None, do_fork=True): if self.do_fork: if not psutil: - raise RuntimeError("Python package psutil is missing. please try `pip install psutil`") + raise RuntimeError("Python package psutil is missing. " + "please try `pip install psutil`") def submit(self, func, *args, **kwargs): if not self.do_fork: From dcf86d3b10af4a0737a6b9d79ce0c61bf7458449 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 15:44:36 -0700 Subject: [PATCH 52/76] fix xgboost model --- python/tvm/autotvm/tuner/xgboost_cost_model.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/python/tvm/autotvm/tuner/xgboost_cost_model.py b/python/tvm/autotvm/tuner/xgboost_cost_model.py index 7190fab04d01..ce28842a4f37 100644 --- a/python/tvm/autotvm/tuner/xgboost_cost_model.py +++ b/python/tvm/autotvm/tuner/xgboost_cost_model.py @@ -139,9 +139,8 @@ def fit(self, xs, ys, plan_size): x_train = self._get_feature(xs) y_train = np.array(ys) - if np.max(y_train) < 1e-6: - return - y_train = y_train / np.max(y_train) + y_max = np.max(y_train) + y_train = y_train / max(y_max, 1e-8) valid_index = y_train > 1e-6 index = np.random.permutation(len(x_train)) @@ -190,9 +189,8 @@ def fit_log(self, records, plan_size): x_train = xs y_train = ys - if np.max(y_train) < 1e-6: - return - y_train /= np.max(y_train) + y_max = np.max(y_train) + y_train = y_train / max(y_max, 1e-8) index = np.random.permutation(len(x_train)) dtrain = xgb.DMatrix(x_train[index], y_train[index]) From 50276daaa9f302e085c51e5dcf7b6cbda0be7ce9 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 16:05:10 -0700 Subject: [PATCH 53/76] fix topi integartion --- python/tvm/autotvm/task/topi_integration.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index 92a21f9f5718..4d51d176176a 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -77,7 +77,11 @@ def config_dispatcher(*args, **kwargs): def template_call(cfg, *args, **kwargs): """call the topi func and attach workload to compute node""" assert not kwargs, "Do not support kwargs in template function call" - node = f(cfg, *args, **kwargs) + + if f == topi_compute.fdefault: + node = f(*args, **kwargs) + else: + node = f(cfg, *args, **kwargs) # attach workload to return op op = node.op @@ -175,7 +179,10 @@ def traverse(tensors): @config_dispatcher.register(template_keys) def template_call(cfg, outs): """call the schedule func""" - return f(cfg, outs) + if f == topi_schedule.fdefault: + return f(outs) + else: + return f(cfg, outs) if func: _decorator(func) From fab5dddb7326794e467e308aab5d05cdc70414e7 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 16:11:55 -0700 Subject: [PATCH 54/76] fix tutorial --- tutorials/autotvm/tune_nnvm_arm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index f8f8c8049ce0..a1a5482d6710 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -249,7 +249,7 @@ def tune_tasks(tasks, if use_transfer_learning: if os.path.isfile(tmp_log_file): - tuner_obj.load_history(record.load_from_file(tmp_log_file)) + tuner_obj.load_history(autotvm.record.load_from_file(tmp_log_file)) # do tuning tuner_obj.tune(n_trial=min(n_trial, len(tsk.config_space)), @@ -260,7 +260,7 @@ def tune_tasks(tasks, autotvm.callback.log_to_file(tmp_log_file)]) # pick best records to a cache file - record.pick_best(tmp_log_file, log_filename) + autotvm.record.pick_best(tmp_log_file, log_filename) os.remove(tmp_log_file) From 30e14106278a9b1c0f49ff5e2b1417dbd39ec2c2 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Mon, 30 Jul 2018 16:25:06 -0700 Subject: [PATCH 55/76] fix lint --- python/tvm/autotvm/task/topi_integration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index 4d51d176176a..e90548fa81f0 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -181,8 +181,7 @@ def template_call(cfg, outs): """call the schedule func""" if f == topi_schedule.fdefault: return f(outs) - else: - return f(cfg, outs) + return f(cfg, outs) if func: _decorator(func) From 41d26eb7fa070004cc1546c34402cc19d62ff449 Mon Sep 17 00:00:00 2001 From: Mercy Date: Mon, 30 Jul 2018 20:33:31 -0700 Subject: [PATCH 56/76] imporove tutorials --- tutorials/autotvm/tune_nnvm_arm.py | 55 ++++++++++++++++-------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index a1a5482d6710..ba7fbaa57ebb 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -4,7 +4,7 @@ **Author**: `Lianmin Zheng `_ Auto-tuning for a specific ARM device is critical for getting the best -performance. This is a tutorial about how to tune a whole convolutional +performance. This is a tutorial about how to tune a whole convolutional network. The operator implementation for ARM CPU in TVM is wrote in template form. @@ -15,7 +15,7 @@ these operators, it will query this log file to get the best knob values. We also released pre-tuned parameters for some arm devices. You can go to -`ARM CPU Benchmark https://github.com/dmlc/tvm/wiki/Benchmark#arm-cpu`_ +`ARM CPU Benchmark `_ to see the results. """ @@ -25,7 +25,7 @@ # To use autotvm package in tvm, we need to install some extra dependencies. # # .. code-block:: bash -# +# # pip install psutil xgboost # @@ -49,7 +49,7 @@ # -------------- # First we need to define the network in nnvm symbol API. # We can load some pre-defined network from :code:`nnvm.testing`. -# We can also load models from mxnet, ONNX and tensorflow (see NNVM +# We can also load models from MXNet, ONNX and TensorFlow (see NNVM # tutorials :ref:`tutorial-nnvm` for more details). def get_network(name, batch_size): @@ -87,22 +87,22 @@ def get_network(name, batch_size): ################################################################# # Start RPC Tracker # ----------------- -# TVM uses RPC session to communicate with ARM boards. +# TVM uses RPC session to communicate with ARM boards. # During tuning, the tuner will send the generated code to the board and # measure the speed of code on the board. -# +# # To scale up the tuning, TVM uses RPC Tracker to manage distributed devices. # The RPC Tracker is a centralized master node. We can register all devices to # the tracker. For example, if we have 10 phones, we can register all of them -# to the tracker, then we can run 10 measurements in parallle, which accelerates +# to the tracker, then we can run 10 measurements in parallel, which accelerates # the tuning process. -# +# # To start an RPC tracker, run this command in the host machine. The tracker is # required during the whole tuning process, so we need to open a new terminal for # this command: # # .. code-block:: bash -# +# # python -m tvm.exec.rpc_tracker --host=0.0.0.0 --port=9190 # # The expected output is @@ -120,11 +120,11 @@ def get_network(name, batch_size): # * For Linux: # Follow this section :ref:`build-tvm-runtime-on-device` to build # tvm runtime on the device. Then register the device to tracker by -# +# # .. code-block:: bash # # python -m tvm.exec.rpc_server --tracker=[HOST_IP]:9190 --key=rk3399 -# +# # (replace :code:`[HOST_IP]` with the IP address of your host machine) # # * For Android: @@ -143,7 +143,7 @@ def get_network(name, batch_size): # .. code-block:: bash # # Queue Status -# ---------------------------- +# ---------------------------- # key free pending # ---------------------------- # mate10pro 2 0 @@ -154,10 +154,12 @@ def get_network(name, batch_size): ########################################### # Set Tuning Options # ------------------ -# Now we can extract tuning tasks from the network and begin tuning. +# Before tuning, we should do some configurations. Here I use an RK3399 board +# in our environment as example. In your setting, you should modify the target +# and device_key accordingly. # Replace "aarch64-linux-gnu" with the correct target of your board. -# This target is used for cross compilation. +# This target is used for cross compilation. you can query it by :code:`gcc -v` on your device. target = tvm.target.create('llvm -device=arm_cpu -target=aarch64-linux-gnu') # Also replace this with the device key in your tracker @@ -185,31 +187,32 @@ def get_network(name, batch_size): } #################################################################### -# +# # .. note:: How to set tuning options # # In general, the default value provided here works well. It is the same # value that we used to generate pre-tuned parameters. -# If you have multiple devices, you can set :code:`mea_parallel_num` to +# If you have multiple devices, you can set :code:`parallel_num` to # the number of devices you have. (e.g. set it to 3 if you register 3 rk3399 # boards to the tracker). +# If you have large time budget, you can set :code:`n_trial`, :code:`early_stopping` larger, +# which makes the tuning run longer. +# If your device is very slow or a single conv2d operator in your network has large FLOPs, +# considering to set timeout larger. # -# You can also refer to our doc :any:`tune_tasks` (click this) to see some comments. +# **For andoird phone**, add :code:`build_func='ndk'` to the argument list of +# :code:`autotvm.measure_option` to use Android NDK for creating shared library. # -# For andoird phone, add :code:`build_func='ndk'` to the argument list of autotvm.measure_option -# to use Android NDK for creating shared library. -# - ################################################################### # Begin Tuning # ------------ -# Now we can begin tuning. Here we provide a simple utility function to tune a list of tasks. +# Now we can extract tuning tasks from the network and begin tuning. +# Here we provide a simple utility function to tune a list of tasks. # This function is just an initial implementation which tune them in sequential order. # Later we will bring more sophisticated tuner scheduler. - -# You can skip the implementation of function for this tutorial. +# You can skip the implementation of this function for this tutorial. def tune_tasks(tasks, measure_option, tuner='xgb', @@ -321,13 +324,13 @@ def tune_and_evaluate(): # tune_and_evaluate() ###################################################################### -# Sample Output +# Sample Output # ------------- # The tuning needs to train xgboost models and use them for prediction. # So a high performance CPU is recommended. # It takes about 1.5 hour on a 32T AMD Ryzen CPU. # One sample output is -# +# # .. code-block:: bash # # [Task 1/16] Current/Best: 15.48/ 21.21 GFLOPS | Progress: (412/1000) | 531.53 s Done. From 1a9e6b60b08586f090f27f618d399b5fda4ed932 Mon Sep 17 00:00:00 2001 From: Mercy Date: Tue, 31 Jul 2018 15:11:24 -0700 Subject: [PATCH 57/76] fix typo --- apps/benchmark/README.md | 3 ++- apps/benchmark/arm_cpu_imagenet_bench.py | 34 +++++++----------------- tutorials/autotvm/tune_nnvm_arm.py | 11 ++++---- tutorials/cross_compilation_and_rpc.py | 1 + tutorials/nnvm/deploy_model_on_rasp.py | 1 + tutorials/nnvm_quick_start.py | 4 +-- 6 files changed, 20 insertions(+), 34 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index d0eea27e8489..e83e47c46eb7 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -23,7 +23,8 @@ python3 -m tvm.exec.rpc_tracker ``` replace `[HOST_IP]` with the IP address of the host machine, `[DEVICE_KEY]` with the name of device. - E.g. For my RK3399, I use `python3 -m tvm.exec.rpc_sever --tracker=10.77.1.123:9190 --key=rk3399` + E.g. Here is an example command for RK3399, + `python3 -m tvm.exec.rpc_sever --tracker=10.77.1.123:9190 --key=rk3399`, where 10.77.1.123 is the IP address of the tracker. * For Android device * Build and install tvm RPC apk on your device [Help](https://github.com/dmlc/tvm/tree/master/apps/android_rpc). diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index f36b78881b57..1525beea4ec2 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -15,24 +15,10 @@ import tvm.contrib.graph_runtime as runtime def get_network(name, batch_size): - """Get the symbol definition and random weight of a network - - Parameters - ---------- - name: str - The name of network - batch_size: int - The batch size - - Returns - ------- - net: Symbol - params: dict - shape: dict - output_shape: tuple - """ - shape = {"data": (batch_size, 3, 224, 224)} + """Get the symbol definition and random weight of a network""" + input_shape = (batch_size, 3, 224, 224) output_shape = (batch_size, 1000) + if name == 'resnet-18': net, params = nnvm.testing.resnet.get_workload(num_layers=18, batch_size=batch_size, image_shape=(3, 224, 224)) @@ -46,7 +32,7 @@ def get_network(name, batch_size): else: raise RuntimeError("Unsupported network: " + name) - return net, params, shape, output_shape + return net, params, input_shape, output_shape if __name__ == "__main__": @@ -77,26 +63,24 @@ def get_network(name, batch_size): print("%-20s %-20s" % ("Network Name", "Mean Inference Time (std dev)")) print("--------------------------------------------------") for network in networks: - net, params, shape, out_shape = get_network(network, batch_size=1) + net, params, input_shape, output_shape = get_network(network, batch_size=1) with nnvm.compiler.build_config(opt_level=2, add_pass=['AlterOpLayout']): graph, lib, params = nnvm.compiler.build( - net, target=target, shape=shape, params=params, dtype=dtype) + net, target=target, shape={'data': input_shape}, params=params, dtype=dtype) tmp = tempdir() if 'android' in str(target): from tvm.contrib import ndk filename = "%s.so" % network - path_name = tmp.relpath(filename) - lib.export_library(path_name, ndk.create_shared) + lib.export_library(tmp.relpath(filename), ndk.create_shared) else: filename = "%s.tar" % network - path_name = tmp.relpath(filename) - lib.export_library(path_name) + lib.export_library(tmp.relpath(filename)) # upload library and params ctx = remote.context(str(target), 0) - remote.upload(path_name) + remote.upload(tmp.relpath(filename)) rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} rlib = remote.load_module(filename) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index ba7fbaa57ebb..7e1aeec94eb6 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -7,7 +7,7 @@ performance. This is a tutorial about how to tune a whole convolutional network. -The operator implementation for ARM CPU in TVM is wrote in template form. +The operator implementation for ARM CPU in TVM is written in template form. It has many tunable knobs (tile factor, vectorization, unrolling, etc). We will do tuning for all convolution and depthwise convolution operators in the neural network. After the tuning, we can get a log file which stores @@ -137,7 +137,7 @@ def get_network(name, batch_size): # # python -m tvm.exec.query_rpc_tracker --host=0.0.0.0 --port=9190 # -# For exmpale, if we have 2 Huawei mate10 pro, 11 Raspberry Pi 3B and 2 rk3399, +# For example, if we have 2 Huawei mate10 pro, 11 Raspberry Pi 3B and 2 rk3399, # the output can be # # .. code-block:: bash @@ -159,7 +159,7 @@ def get_network(name, batch_size): # and device_key accordingly. # Replace "aarch64-linux-gnu" with the correct target of your board. -# This target is used for cross compilation. you can query it by :code:`gcc -v` on your device. +# This target is used for cross compilation. You can query it by :code:`gcc -v` on your device. target = tvm.target.create('llvm -device=arm_cpu -target=aarch64-linux-gnu') # Also replace this with the device key in your tracker @@ -178,7 +178,7 @@ def get_network(name, batch_size): 'early_stopping': 200, 'measure_option': autotvm.measure_option( - autotvm.use_rpc(device_key), + autotvm.use_rpc(device_key, host='localhost', port=9190), number=4, parallel_num=1, timeout=10), @@ -197,8 +197,7 @@ def get_network(name, batch_size): # boards to the tracker). # If you have large time budget, you can set :code:`n_trial`, :code:`early_stopping` larger, # which makes the tuning run longer. -# If your device is very slow or a single conv2d operator in your network has large FLOPs, -# considering to set timeout larger. +# If your device is very slow or a single conv2d operator in your network has large FLOPs, consider setting timeout larger. # # **For andoird phone**, add :code:`build_func='ndk'` to the argument list of # :code:`autotvm.measure_option` to use Android NDK for creating shared library. diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index 3f5e68baf4e6..136942805501 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -34,6 +34,7 @@ # .. code-block:: bash # # git clone --recursive https://github.com/dmlc/tvm +# cd tvm # make runtime -j2 # # After building runtime successfully, we need to set environment varibles diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index 0d48c0870f13..d559de69da86 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -36,6 +36,7 @@ # .. code-block:: bash # # git clone --recursive https://github.com/dmlc/tvm +# cd tvm # make runtime -j4 # # After building runtime successfully, we need to set environment varibles diff --git a/tutorials/nnvm_quick_start.py b/tutorials/nnvm_quick_start.py index c1eef7ae04e6..c9f6c33591d0 100644 --- a/tutorials/nnvm_quick_start.py +++ b/tutorials/nnvm_quick_start.py @@ -70,7 +70,7 @@ # # We'll first compile for Nvidia GPU. Behind the scene, `nnvm.compiler.build` # first does a number of graph-level optimizations, e.g. pruning, fusing, etc., -# then registers the operators (i.e. the nodes of the optmized graphs) to +# then registers the operators (i.e. the nodes of the optimized graphs) to # TVM implementations to generate a `tvm.module`. # To generate the module library, TVM will first transfer the High level IR # into the lower intrinsic IR of the specified target backend, which is CUDA @@ -109,7 +109,7 @@ # Save and Load Compiled Module # ----------------------------- # We can also save the graph, lib and parameters into files and load them -# back in deploment environment. +# back in development environment. #################################################### From 74c4952fe0bb33126ef69c6d5b5c25de7c922382 Mon Sep 17 00:00:00 2001 From: Mercy Date: Tue, 31 Jul 2018 15:56:51 -0700 Subject: [PATCH 58/76] update tophub context loading --- nnvm/python/nnvm/compiler/build_module.py | 129 +++++++++++----------- python/tvm/autotvm/tophub.py | 9 ++ 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index c08dfd4a7099..b5ac0ac306dd 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -239,71 +239,74 @@ def build(graph, target=None, shape=None, dtype="float32", raise ValueError("Target is not set in env or passed as argument.") target = tvm.target.create(target) + # if not inside an autotvm config dispatch context, load pre-tuned parameters from TopHub. if autotvm.task.DispatchContext.current is None: - # load pre-tuned parameters of operators from the default directory of TopHub - autotvm.tophub.load_context(target) - - shape = shape if shape else {} - if not isinstance(shape, dict): - raise TypeError("require shape to be dict") - for value in shape.values(): - if not all(isinstance(x, int) for x in value): - raise TypeError("shape value must be int iterator") - - cfg = BuildConfig.current - graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) - shape, dtype = _update_shape_dtype(shape, dtype, params) - - # correct layout if necessary - layout = layout if layout else {} - graph = graph_attr.set_layout_inputs(graph, layout) - graph = graph.apply("CorrectLayout") - index = graph.index - layouts = graph.json_attr("layout") - layout = {x : layouts[index.entry_id(x)] for x in index.input_names} - - # Initial pass do shape type inference - ishape, _ = graph_util.infer_shape(graph, **shape) - shape.update(zip(graph.index.input_names, ishape)) - if not isinstance(dtype, str): - idtype, _ = graph_util.infer_dtype(graph, **dtype) - dtype.update(zip(graph.index.input_names, idtype)) - # Initialize all variables specified in _all_var_init - init_var = {} - if _all_var_init: - init_var = initialize_variables(shape, dtype) - # Apply optimization - with target: - graph = optimize(graph, shape, dtype, layout) - - # Clear extra params without nodes. - _remove_noref_params(params, graph) - - # Precompute prune - if params and cfg.pass_enabled("PrecomputePrune"): - graph, params = precompute_prune(graph, params) - shape, dtype = _update_shape_dtype(shape, dtype, params) - # Operator Fusion and generation - graph = graph_attr.set_shape_inputs(graph, shape) - graph = graph.apply("InferShape") - graph = graph_attr.set_dtype_inputs(graph, dtype) - graph._set_json_attr("target", str(target), "str") - if target_host is not None: - graph._set_json_attr("target_host", str(target_host), "str") - if cfg.pass_enabled("OpFusion"): - graph._set_json_attr("opt_level", 1, "int") + tophub_context = autotvm.tophub.context(target) else: - graph._set_json_attr("opt_level", 0, "int") - graph = graph.apply("InferShape").apply("InferType") - with target: - graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") - libmod = graph_attr._move_out_module(graph, "module") - # Write variable initial values into params - if init_var: - if params is None: - params = {} - params.update(init_var) - return graph, libmod, params + tophub_context = autotvm.tophub.EmptyContext() + + with tophub_context: + shape = shape if shape else {} + if not isinstance(shape, dict): + raise TypeError("require shape to be dict") + for value in shape.values(): + if not all(isinstance(x, int) for x in value): + raise TypeError("shape value must be int iterator") + + cfg = BuildConfig.current + graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) + shape, dtype = _update_shape_dtype(shape, dtype, params) + + # correct layout if necessary + layout = layout if layout else {} + graph = graph_attr.set_layout_inputs(graph, layout) + graph = graph.apply("CorrectLayout") + index = graph.index + layouts = graph.json_attr("layout") + layout = {x: layouts[index.entry_id(x)] for x in index.input_names} + + # Initial pass do shape type inference + ishape, _ = graph_util.infer_shape(graph, **shape) + shape.update(zip(graph.index.input_names, ishape)) + if not isinstance(dtype, str): + idtype, _ = graph_util.infer_dtype(graph, **dtype) + dtype.update(zip(graph.index.input_names, idtype)) + # Initialize all variables specified in _all_var_init + init_var = {} + if _all_var_init: + init_var = initialize_variables(shape, dtype) + # Apply optimization + with target: + graph = optimize(graph, shape, dtype, layout) + + # Clear extra params without nodes. + _remove_noref_params(params, graph) + + # Precompute prune + if params and cfg.pass_enabled("PrecomputePrune"): + graph, params = precompute_prune(graph, params) + shape, dtype = _update_shape_dtype(shape, dtype, params) + # Operator Fusion and generation + graph = graph_attr.set_shape_inputs(graph, shape) + graph = graph.apply("InferShape") + graph = graph_attr.set_dtype_inputs(graph, dtype) + graph._set_json_attr("target", str(target), "str") + if target_host is not None: + graph._set_json_attr("target_host", str(target_host), "str") + if cfg.pass_enabled("OpFusion"): + graph._set_json_attr("opt_level", 1, "int") + else: + graph._set_json_attr("opt_level", 0, "int") + graph = graph.apply("InferShape").apply("InferType") + with target: + graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") + libmod = graph_attr._move_out_module(graph, "module") + # Write variable initial values into params + if init_var: + if params is None: + params = {} + params.update(init_var) + return graph, libmod, params def _remove_noref_params(params, graph): """ Helper to clear non referenced params diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index c89054c8dd09..e539259b75db 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -17,6 +17,15 @@ AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") +class EmptyContext(object): + """An empty context""" + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def context(target, extra_files=None): """Return the dispatch context with pre-tuned parameters. The corresponding downloaded *.log files under tophub root path will be loaded. From 13fc955dfe93dfda2b2cf9d4f7e0504e226a9985 Mon Sep 17 00:00:00 2001 From: Mercy Date: Tue, 31 Jul 2018 16:10:16 -0700 Subject: [PATCH 59/76] update doc --- python/tvm/autotvm/measure/measure.py | 4 +-- python/tvm/autotvm/measure/measure_methods.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 8c98e46b466a..39d0063e9cdd 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -63,7 +63,7 @@ def measure_option(measure_func, and a RPC server silently for the user. callable: It is a callable function for measurement. - + See the return value of measure/measure_methods.py::use_rpc for example. number : int, optional Number of times to do the measurement for average repeat : int, optional @@ -88,7 +88,7 @@ def measure_option(measure_func, 'ndk': use Android NDK to create shared library. Use this for android target. callable: customized build function for other backends (e.g. VTA). - See measure/measure_methods.py default_build_func for example. + See measure/measure_methods.py::default_build_func for example. check_correctness: bool Whether check correctness after measurement. This will use llvm cpu as reference. replay_db : Database, optional diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index 6882fb60579d..bf1222777bb2 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -209,6 +209,33 @@ def use_rpc(key, rpc connection, set this higher. """ def fmeasure(input_pack, build_func, build_kwargs, number, repeat, ref_input, ref_output): + """Do measurement for a list of inputs inside a same RPC session. + + Parameters + ---------- + input_pack: List of MeasureInput + The inputs of measurement + build_func: callable + Function for building the code. see :any:`default_build_func` for example + build_kwargs: dict + Extra arguments for build_func + number : int, optional + Number of times to do the measurement for average + repeat : int, optional + Number of times to repeat the measurement. + In total, the generated code will be run (1 + number x repeat) times, + where the first one is warm up. The returned result contains `repeat` costs, + each of which is the average of `number` test run. + ref_input: List of numpy array + Reference input for correctness check + ref_output: List of numpy array + Reference output for correctness check + + Returns + ------- + results: List of MeasureResult + The results for input_pack + """ remote = request_remote(key, (host, port), priority, session_timeout) res = _measure_common(input_pack, build_func, build_kwargs, number, repeat, From 3433d8d5940566688a949fa62ab8185118e0e98c Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 16:14:17 -0700 Subject: [PATCH 60/76] fix benchmark --- apps/benchmark/arm_cpu_imagenet_bench.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 1525beea4ec2..1ae6b40cd529 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -85,7 +85,7 @@ def get_network(name, batch_size): rlib = remote.load_module(filename) module = runtime.create(graph, rlib, ctx) - data_tvm = tvm.nd.array((np.random.uniform(size=shape['data'])).astype(dtype)) + data_tvm = tvm.nd.array((np.random.uniform(size=input_shape)).astype(dtype)) module.set_input('data', data_tvm) module.set_input(**rparams) From 4ded8560928aa03659d032e1a6de5fb0437d4241 Mon Sep 17 00:00:00 2001 From: Mercy Date: Tue, 31 Jul 2018 16:20:22 -0700 Subject: [PATCH 61/76] fix vta test --- python/tvm/autotvm/tophub.py | 18 ------------------ topi/tests/python/test_topi_conv2d.py | 6 ++---- .../integration/test_benchmark_topi_conv2d.py | 9 ++++----- 3 files changed, 6 insertions(+), 27 deletions(-) diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index e539259b75db..2082ef5f518a 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -9,7 +9,6 @@ import os import json -from .task import DispatchContext from .record import ApplyHistoryBest from ..contrib.util import tempdir from ..contrib.download import download @@ -58,23 +57,6 @@ def context(target, extra_files=None): return best_context -def load_context(target, extra_files=None): - """Load the dispatch context with pre-tuned parameters. - The corresponding downloaded *.log files under tophub root path will be loaded. - Users can also add their own files in argument `extra_files`. - - Parameters - ---------- - target: Target - The compilation target - extra_files: list of str, optional - Extra log files to load - """ - best_context = context(target, extra_files) - assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" - best_context.__enter__() - - def download_package(backend): """Download pre-tuned parameters of operators for a backend diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index 4d284e5eccbf..a042041b8088 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -12,9 +12,6 @@ def verify_conv2d(batch, in_size, in_channel, num_filter, kernel, stride, padding): in_height = in_width = in_size - # load pre-tuned parameters - autotvm.tophub.load_context(tvm.target.arm_cpu()) - with tvm.target.arm_cpu(): A = tvm.placeholder((batch, in_channel, in_height, in_width), name='A') W = tvm.placeholder((num_filter, in_channel, kernel, kernel), name='W') @@ -46,4 +43,5 @@ def test_conv2d(): verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": - test_conv2d() + with autotvm.tophub.context(tvm.target.arm_cpu()): + test_conv2d() diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 56fb463d94be..135ca71c4186 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -22,9 +22,6 @@ def my_clip(x, a_min, a_max): return x def test_cpu_conv2d(): - # download pre-tuned parameters and load the dispatch context - autotvm.tophub.load_context(tvm.target.arm_cpu()) - def run_cpu_conv2d(env, remote, key, batch_size, wl, profile=True): data_shape = (batch_size, wl.in_filter, wl.height, wl.width) kernel_shape = (wl.out_filter, wl.in_filter, wl.hkernel, wl.wkernel) @@ -264,5 +261,7 @@ def _run(env, remote): if __name__ == "__main__": - test_cpu_conv2d() - test_vta_conv2d() + # download pre-tuned parameters and load the dispatch context + with autotvm.tophub.context(tvm.target.arm_cpu()): + test_cpu_conv2d() + test_vta_conv2d() From 20aed18db166cfe78a1866a3d29d7b02279433e8 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 16:24:48 -0700 Subject: [PATCH 62/76] fix typo --- tutorials/autotvm/tune_nnvm_arm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 7e1aeec94eb6..20bddc2da9cd 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -199,7 +199,7 @@ def get_network(name, batch_size): # which makes the tuning run longer. # If your device is very slow or a single conv2d operator in your network has large FLOPs, consider setting timeout larger. # -# **For andoird phone**, add :code:`build_func='ndk'` to the argument list of +# **For android phone**, add :code:`build_func='ndk'` to the argument list of # :code:`autotvm.measure_option` to use Android NDK for creating shared library. # From 0b3e2e0b0df52dc3b8d4b2238edf20f9afb72e36 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 16:27:50 -0700 Subject: [PATCH 63/76] use less memory in benchmark --- apps/benchmark/arm_cpu_imagenet_bench.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 1ae6b40cd529..4e8fb10e45f3 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -55,10 +55,6 @@ def get_network(name, batch_size): target = tvm.target.arm_cpu(model=args.device) - # get remote device session - tracker = tvm.rpc.connect_tracker(args.host, args.port) - remote = tracker.request(args.rpc_key) - print("--------------------------------------------------") print("%-20s %-20s" % ("Network Name", "Mean Inference Time (std dev)")) print("--------------------------------------------------") @@ -79,6 +75,9 @@ def get_network(name, batch_size): lib.export_library(tmp.relpath(filename)) # upload library and params + tracker = tvm.rpc.connect_tracker(args.host, args.port) + remote = tracker.request(args.rpc_key) + ctx = remote.context(str(target), 0) remote.upload(tmp.relpath(filename)) rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} From a8f61825393db3fc76a05ab72ee47f6595f4b298 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 16:46:32 -0700 Subject: [PATCH 64/76] improve doc --- python/tvm/autotvm/measure/measure.py | 17 ++++++++++++++++- python/tvm/autotvm/measure/measure_methods.py | 4 +++- tutorials/autotvm/tune_nnvm_arm.py | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 39d0063e9cdd..6a05e1a6a349 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -92,12 +92,27 @@ def measure_option(measure_func, check_correctness: bool Whether check correctness after measurement. This will use llvm cpu as reference. replay_db : Database, optional - The database that we retrieve saved MeasureResults from. + The database that we retrieve saved MeasureResult from. Returns ------- options: dict A dict to store all options + + Note + ---- + To support customized measure, you can pass callable `measure_func` or + `build_func` in. The `measure_func` will call `build_func` to build binary library + and handle the logic of measurement. + + Signature: + * measure_func (see the return value of measure/measure_methods.py::use_rpc for example) + def measure_func(input_pack, build_func, build_kwargs, number, repeat, ref_input, ref_output): + return measure_results + + * build_func (see measure/measure_methods.py::default_build_func for example) + def build_func(inp, tmp_dir, **kwargs): + return func, args, filename """ return { 'measure_func': measure_func, diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index bf1222777bb2..c2ce6ceffe79 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -188,7 +188,9 @@ def use_rpc(key, session_timeout=60, pack_size=1): """ - Create a standard measure_func which uses RPC Tracker for measurement + Create a standard measure_func which uses RPC Tracker for measurement. + This measure_func will request a device from the RPC Tracker and + upload the built binary library to that device for measurement. Parameters ---------- diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 20bddc2da9cd..ffc89005cf5a 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -314,7 +314,7 @@ def tune_and_evaluate(): # evaluate print("Evaluate inference time cost...") ftimer = module.module.time_evaluator("run", ctx, number=1, repeat=10) - prof_res = np.array(ftimer().results) * 1000 # convert to million second + prof_res = np.array(ftimer().results) * 1000 # convert to millisecond print("Mean inference time (std dev): %.2f ms (%.2f ms)" % (np.mean(prof_res), np.std(prof_res))) From 399abd06d4eb0b1e464ebed1669382062043d9a9 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 16:52:50 -0700 Subject: [PATCH 65/76] fix test --- topi/tests/python/test_topi_conv2d.py | 6 +++--- .../python/integration/test_benchmark_topi_conv2d.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index a042041b8088..d1b9256a13b8 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -40,8 +40,8 @@ def get_ref_data(): np.testing.assert_allclose(b.asnumpy(), b_np, rtol=1e-5) def test_conv2d(): - verify_conv2d(1, 56, 64, 64, 3, 1, 1) + with autotvm.tophub.context(tvm.target.arm_cpu()): + verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": - with autotvm.tophub.context(tvm.target.arm_cpu()): - test_conv2d() + test_conv2d() diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 135ca71c4186..01eef5732833 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -126,7 +126,9 @@ def _run(env, remote): print(wl) with tvm.target.arm_cpu("pynq"): run_cpu_conv2d(env, remote, key, batch_size, wl) - vta.testing.run(_run) + + with autotvm.tophub.context(tvm.target.arm_cpu()): + vta.testing.run(_run) def test_vta_conv2d(): @@ -257,11 +259,11 @@ def _run(env, remote): print(wl) run_vta_conv2d(env, remote, key, batch_size, wl) - vta.testing.run(_run) + with autotvm.tophub.context(tvm.target.arm_cpu()): + vta.testing.run(_run) if __name__ == "__main__": # download pre-tuned parameters and load the dispatch context - with autotvm.tophub.context(tvm.target.arm_cpu()): - test_cpu_conv2d() - test_vta_conv2d() + test_cpu_conv2d() + test_vta_conv2d() From 58cb213349a671f8f2e3e501633227f73e52237f Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Tue, 31 Jul 2018 17:11:25 -0700 Subject: [PATCH 66/76] fix benchmark --- apps/benchmark/arm_cpu_imagenet_bench.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 4e8fb10e45f3..a98693afd017 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -55,6 +55,10 @@ def get_network(name, batch_size): target = tvm.target.arm_cpu(model=args.device) + # connect to remote device + tracker = tvm.rpc.connect_tracker(args.host, args.port) + remote = tracker.request(args.rpc_key) + print("--------------------------------------------------") print("%-20s %-20s" % ("Network Name", "Mean Inference Time (std dev)")) print("--------------------------------------------------") @@ -75,9 +79,6 @@ def get_network(name, batch_size): lib.export_library(tmp.relpath(filename)) # upload library and params - tracker = tvm.rpc.connect_tracker(args.host, args.port) - remote = tracker.request(args.rpc_key) - ctx = remote.context(str(target), 0) remote.upload(tmp.relpath(filename)) rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} From 59a462d553918b3a49968e2006ec6060336277c6 Mon Sep 17 00:00:00 2001 From: Mercy Date: Tue, 31 Jul 2018 22:16:08 -0700 Subject: [PATCH 67/76] trigger CI --- vta/tests/python/integration/test_benchmark_topi_conv2d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 01eef5732833..53f4f6ba5e19 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -127,6 +127,7 @@ def _run(env, remote): with tvm.target.arm_cpu("pynq"): run_cpu_conv2d(env, remote, key, batch_size, wl) + # download pre-tuned parameters and load the dispatch context with autotvm.tophub.context(tvm.target.arm_cpu()): vta.testing.run(_run) @@ -259,11 +260,11 @@ def _run(env, remote): print(wl) run_vta_conv2d(env, remote, key, batch_size, wl) + # download pre-tuned parameters and load the dispatch context with autotvm.tophub.context(tvm.target.arm_cpu()): vta.testing.run(_run) if __name__ == "__main__": - # download pre-tuned parameters and load the dispatch context test_cpu_conv2d() test_vta_conv2d() From 040a7ded953561918a4d0d67063e022434df1df9 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 00:51:07 -0700 Subject: [PATCH 68/76] improve tutorials --- tutorials/autotvm/tune_conv2d_cuda.py | 21 +++++++++++++++++++++ tutorials/autotvm/tune_nnvm_arm.py | 18 +++++++++++++----- tutorials/autotvm/tune_simple_template.py | 21 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/tutorials/autotvm/tune_conv2d_cuda.py b/tutorials/autotvm/tune_conv2d_cuda.py index af36511e5ffb..03f319ba57f1 100644 --- a/tutorials/autotvm/tune_conv2d_cuda.py +++ b/tutorials/autotvm/tune_conv2d_cuda.py @@ -8,6 +8,27 @@ vendor provided library CuDNN in many cases. """ +###################################################################### +# Install dependencies +# ---------------------------------------- +# To use autotvm package in tvm, we need to install some extra dependencies. +# (change "3" to "2" if you use python2): +# +# .. code-block:: bash +# +# pip3 install --user psutil xgboost +# +# To make tvm run faster in tuning, it is recommended to use cython +# as FFI of tvm. In the root directory of tvm, execute +# (change "3" to "2" if you use python2): +# +# .. code-block:: bash +# +# pip3 install --user cython +# sudo make cython3 +# +# Now return to python code. Import packages. + import logging import sys import numpy as np diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index ffc89005cf5a..2813117dc62b 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -20,18 +20,26 @@ """ ###################################################################### -# Install dependencies and import packages +# Install dependencies # ---------------------------------------- # To use autotvm package in tvm, we need to install some extra dependencies. +# (change "3" to "2" if you use python2): # # .. code-block:: bash # -# pip install psutil xgboost +# pip3 install --user psutil xgboost # +# To make tvm run faster in tuning, it is recommended to use cython +# as FFI of tvm. In the root directory of tvm, execute +# (change "3" to "2" if you use python2): +# +# .. code-block:: bash +# +# pip3 install --user cython +# sudo make cython3 +# +# Now return to python code. Import packages. -import logging - -import time import os import numpy as np diff --git a/tutorials/autotvm/tune_simple_template.py b/tutorials/autotvm/tune_simple_template.py index 57616dea12ee..f2a2ea9e1266 100644 --- a/tutorials/autotvm/tune_simple_template.py +++ b/tutorials/autotvm/tune_simple_template.py @@ -12,6 +12,27 @@ The whole workflow is illustrated by a matrix multiplication example. """ +###################################################################### +# Install dependencies +# ---------------------------------------- +# To use autotvm package in tvm, we need to install some extra dependencies. +# (change "3" to "2" if you use python2): +# +# .. code-block:: bash +# +# pip3 install --user psutil xgboost +# +# To make tvm run faster in tuning, it is recommended to use cython +# as FFI of tvm. In the root directory of tvm, execute +# (change "3" to "2" if you use python2): +# +# .. code-block:: bash +# +# pip3 install --user cython +# sudo make cython3 +# +# Now return to python code. Import packages. + import logging import sys From f12707d969d40743951a280498b74bbf116747a0 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 08:11:25 -0700 Subject: [PATCH 69/76] update root dispatch context --- apps/benchmark/arm_cpu_imagenet_bench.py | 2 +- docs/api/python/autotvm.rst | 9 ++ nnvm/python/nnvm/compiler/build_module.py | 130 +++++++++-------- python/tvm/autotvm/__init__.py | 4 +- python/tvm/autotvm/record.py | 103 +------------- python/tvm/autotvm/task/__init__.py | 3 +- python/tvm/autotvm/task/dispatcher.py | 146 +++++++++++++++++--- python/tvm/autotvm/task/topi_integration.py | 8 +- python/tvm/autotvm/tophub.py | 33 ++--- topi/python/topi/arm_cpu/conv2d.py | 24 ++-- topi/tests/python/test_topi_conv2d.py | 4 +- 11 files changed, 237 insertions(+), 229 deletions(-) diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index a98693afd017..58b1f620f142 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -39,7 +39,7 @@ def get_network(name, batch_size): parser = argparse.ArgumentParser() parser.add_argument("--network", type=str, choices=['resnet-18', 'mobilenet', 'squeezenet v1.1', 'vgg-16']) parser.add_argument("--device", type=str, required=True, choices=['rk3399', 'mate10', 'mate10pro', 'p20', 'p20pro', - 'pixel2', 'rasp3b']) + 'pixel2', 'rasp3b', 'pynq']) parser.add_argument("--host", type=str, default='localhost') parser.add_argument("--port", type=int, default=9190) parser.add_argument("--rpc-key", type=str, required=True) diff --git a/docs/api/python/autotvm.rst b/docs/api/python/autotvm.rst index fc0c754ee624..0a2ae40f24a9 100644 --- a/docs/api/python/autotvm.rst +++ b/docs/api/python/autotvm.rst @@ -58,6 +58,15 @@ tvm.autotvm.task .. automodule:: tvm.autotvm.task.space :members: +.. automodule:: tvm.autotvm.task.dispatcher + :members: + +.. automodule:: tvm.autotvm.task.topi_integration + :members: + +.. automodule:: tvm.autotvm.task.nnvm_integration + :members: + tvm.autotvm.record ~~~~~~~~~~~~~~~~~~ .. automodule:: tvm.autotvm.record diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index b5ac0ac306dd..8084241a414d 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -239,74 +239,70 @@ def build(graph, target=None, shape=None, dtype="float32", raise ValueError("Target is not set in env or passed as argument.") target = tvm.target.create(target) - # if not inside an autotvm config dispatch context, load pre-tuned parameters from TopHub. - if autotvm.task.DispatchContext.current is None: - tophub_context = autotvm.tophub.context(target) - else: - tophub_context = autotvm.tophub.EmptyContext() - - with tophub_context: - shape = shape if shape else {} - if not isinstance(shape, dict): - raise TypeError("require shape to be dict") - for value in shape.values(): - if not all(isinstance(x, int) for x in value): - raise TypeError("shape value must be int iterator") - - cfg = BuildConfig.current - graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) - shape, dtype = _update_shape_dtype(shape, dtype, params) + # load pre-tuned parameters from TopHub to the root dispatch context + autotvm.tophub.load(target) - # correct layout if necessary - layout = layout if layout else {} - graph = graph_attr.set_layout_inputs(graph, layout) - graph = graph.apply("CorrectLayout") - index = graph.index - layouts = graph.json_attr("layout") - layout = {x: layouts[index.entry_id(x)] for x in index.input_names} - - # Initial pass do shape type inference - ishape, _ = graph_util.infer_shape(graph, **shape) - shape.update(zip(graph.index.input_names, ishape)) - if not isinstance(dtype, str): - idtype, _ = graph_util.infer_dtype(graph, **dtype) - dtype.update(zip(graph.index.input_names, idtype)) - # Initialize all variables specified in _all_var_init - init_var = {} - if _all_var_init: - init_var = initialize_variables(shape, dtype) - # Apply optimization - with target: - graph = optimize(graph, shape, dtype, layout) - - # Clear extra params without nodes. - _remove_noref_params(params, graph) - - # Precompute prune - if params and cfg.pass_enabled("PrecomputePrune"): - graph, params = precompute_prune(graph, params) - shape, dtype = _update_shape_dtype(shape, dtype, params) - # Operator Fusion and generation - graph = graph_attr.set_shape_inputs(graph, shape) - graph = graph.apply("InferShape") - graph = graph_attr.set_dtype_inputs(graph, dtype) - graph._set_json_attr("target", str(target), "str") - if target_host is not None: - graph._set_json_attr("target_host", str(target_host), "str") - if cfg.pass_enabled("OpFusion"): - graph._set_json_attr("opt_level", 1, "int") - else: - graph._set_json_attr("opt_level", 0, "int") - graph = graph.apply("InferShape").apply("InferType") - with target: - graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") - libmod = graph_attr._move_out_module(graph, "module") - # Write variable initial values into params - if init_var: - if params is None: - params = {} - params.update(init_var) - return graph, libmod, params + shape = shape if shape else {} + if not isinstance(shape, dict): + raise TypeError("require shape to be dict") + for value in shape.values(): + if not all(isinstance(x, int) for x in value): + raise TypeError("shape value must be int iterator") + + cfg = BuildConfig.current + graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) + shape, dtype = _update_shape_dtype(shape, dtype, params) + + # correct layout if necessary + layout = layout if layout else {} + graph = graph_attr.set_layout_inputs(graph, layout) + graph = graph.apply("CorrectLayout") + index = graph.index + layouts = graph.json_attr("layout") + layout = {x: layouts[index.entry_id(x)] for x in index.input_names} + + # Initial pass do shape type inference + ishape, _ = graph_util.infer_shape(graph, **shape) + shape.update(zip(graph.index.input_names, ishape)) + if not isinstance(dtype, str): + idtype, _ = graph_util.infer_dtype(graph, **dtype) + dtype.update(zip(graph.index.input_names, idtype)) + # Initialize all variables specified in _all_var_init + init_var = {} + if _all_var_init: + init_var = initialize_variables(shape, dtype) + # Apply optimization + with target: + graph = optimize(graph, shape, dtype, layout) + + # Clear extra params without nodes. + _remove_noref_params(params, graph) + + # Precompute prune + if params and cfg.pass_enabled("PrecomputePrune"): + graph, params = precompute_prune(graph, params) + shape, dtype = _update_shape_dtype(shape, dtype, params) + # Operator Fusion and generation + graph = graph_attr.set_shape_inputs(graph, shape) + graph = graph.apply("InferShape") + graph = graph_attr.set_dtype_inputs(graph, dtype) + graph._set_json_attr("target", str(target), "str") + if target_host is not None: + graph._set_json_attr("target_host", str(target_host), "str") + if cfg.pass_enabled("OpFusion"): + graph._set_json_attr("opt_level", 1, "int") + else: + graph._set_json_attr("opt_level", 0, "int") + graph = graph.apply("InferShape").apply("InferType") + with target: + graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") + libmod = graph_attr._move_out_module(graph, "module") + # Write variable initial values into params + if init_var: + if params is None: + params = {} + params.update(init_var) + return graph, libmod, params def _remove_noref_params(params, graph): """ Helper to clear non referenced params diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index 391b9827ab4c..20426be84aa1 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -24,6 +24,6 @@ # some shortcuts from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo, use_rpc from .tuner import callback -from .task import template, get_config, create, ConfigSpace, ConfigEntity -from .record import ApplyHistoryBest as apply_history_best +from .task import template, get_config, create, ConfigSpace, ConfigEntity, \ + ApplyHistoryBest as apply_history_best from .env import GLOBAL_SCOPE diff --git a/python/tvm/autotvm/record.py b/python/tvm/autotvm/record.py index aa25e672df6e..a46cee9bf998 100644 --- a/python/tvm/autotvm/record.py +++ b/python/tvm/autotvm/record.py @@ -11,12 +11,10 @@ import time from collections import OrderedDict -import numpy as np - from .. import build, lower, target as _target from . import task -from .task import DispatchContext, ConfigEntity +from .task import ConfigEntity, ApplyHistoryBest from .measure import MeasureInput, MeasureResult AUTOTVM_LOG_VERSION = 0.1 @@ -168,105 +166,6 @@ def load_from_file(filename): yield decode(row) -class ApplyHistoryBest(DispatchContext): - """ - Apply the history best config - - Parameters - ---------- - records : str or iterator of (MeasureInput, MeasureResult) - Collection of tuning records. - If is str, then it should be the filename of a records log file. - Each row of this file is an encoded record pair. - Otherwise, it is an iterator. - default: ConfigEntity, optional - The default config to return when no history records - """ - def __init__(self, records, default=None): - super(ApplyHistoryBest, self).__init__() - - self.best_by_targetkey = {} - self.best_by_model = {} - self._default = default - - self.load(records) - - def load(self, records): - """Load records to this dispatch context - - Parameters - ---------- - records : str or iterator of (MeasureInput, MeasureResult) - Collection of tuning records. - If is str, then it should be the filename of a records log file. - Each row of this file is an encoded record pair. - Otherwise, it is an iterator. - """ - if isinstance(records, str): - records = load_from_file(records) - if not records: - return - - best_by_targetkey = self.best_by_targetkey - best_by_model = self.best_by_model - - counter = 0 - for inp, res in records: - counter += 1 - if res.error_no != 0: - continue - - # use target keys in tvm target system as key to build best map - for k in inp.target.keys: - key = (k, inp.task.workload) - if key not in best_by_targetkey: - best_by_targetkey[key] = (inp, res) - else: - _, other_res = best_by_targetkey[key] - if np.mean(other_res.costs) > np.mean(res.costs): - best_by_targetkey[key] = (inp, res) - - # use model as key to build best map - for opt in inp.target.options: - if opt.startswith("-model"): - model = opt[7:] - key = (model, inp.task.workload) - if key not in best_by_model: - best_by_model[key] = (inp, res) - else: - _, other_res = best_by_model[key] - if np.mean(other_res.costs) > np.mean(res.costs): - best_by_model[key] = (inp, res) - break - - logging.debug("Finish loading %d records", counter) - - def query(self, target, workload): - if target is None: - raise RuntimeError("Need a target context to find the history best. " - "Hint: If your target is llvm, use `with tvm.target.create('llvm'):`" - " above the dispatcher call. So does other target. ") - - # first try matching by model - for opt in target.options: - if opt.startswith("-model"): - model = opt[7:] - key = (model, workload) - if key in self.best_by_model: - return self.best_by_model[key][0].config - - # then try matching by target key - for k in target.keys: - key = (k, workload) - if key in self.best_by_targetkey: - return self.best_by_targetkey[key][0].config - - if self._default: - return self._default - raise RuntimeError( - "Cannot find config for target=%s, workload=%s" % (target, workload)) - - def split_workload(in_file, clean=True): """Split a log file into separate files, each of which contains only a single workload This function can also delete duplicated records in log file diff --git a/python/tvm/autotvm/task/__init__.py b/python/tvm/autotvm/task/__init__.py index 1247d5625397..86720da5a975 100644 --- a/python/tvm/autotvm/task/__init__.py +++ b/python/tvm/autotvm/task/__init__.py @@ -9,7 +9,8 @@ from .task import Task, create, register, template, get_config, args_to_workload from .space import ConfigSpace, ConfigEntity from .code_hash import attach_code_hash, attach_code_hash_to_arg -from .dispatcher import DispatchContext, ApplyConfig, dispatcher +from .dispatcher import DispatchContext, ApplyConfig, ApplyHistoryBest, ROOT_DISPATCH_CONTEXT, \ + dispatcher from .topi_integration import register_topi_compute, register_topi_schedule from .nnvm_integration import extract_from_graph diff --git a/python/tvm/autotvm/task/dispatcher.py b/python/tvm/autotvm/task/dispatcher.py index df118e2c69ad..0a96133ef085 100644 --- a/python/tvm/autotvm/task/dispatcher.py +++ b/python/tvm/autotvm/task/dispatcher.py @@ -12,7 +12,10 @@ """ from __future__ import absolute_import as _abs +import logging + from decorator import decorate +import numpy as np from tvm import target as _target @@ -52,25 +55,6 @@ def __exit__(self, ptype, value, trace): DispatchContext.current = self._old_ctx -class ApplyConfig(DispatchContext): - """Apply a specific config entity during query. - - Parameters - ---------- - config : ConfigSpace or ConfigEntity - The specific configuration we care about. - """ - def __init__(self, config): - super(ApplyConfig, self).__init__() - self._config = config - self.workload = None - - def query(self, target, workload): - """Override query""" - self.workload = workload - return self._config - - def dispatcher(fworkload): """Wrap a workload dispatcher function. @@ -137,3 +121,127 @@ def dispatch_func(func, *args, **kwargs): fdecorate = decorate(fworkload, dispatch_func) fdecorate.register = register return fdecorate + + +class ApplyConfig(DispatchContext): + """Apply a specific config entity during query. + + Parameters + ---------- + config : ConfigSpace or ConfigEntity + The specific configuration we care about. + """ + def __init__(self, config): + super(ApplyConfig, self).__init__() + self._config = config + self.workload = None + + def query(self, target, workload): + """Override query""" + self.workload = workload + return self._config + + +class ApplyHistoryBest(DispatchContext): + """ + Apply the history best config + + Parameters + ---------- + records : str or iterator of (MeasureInput, MeasureResult) + Collection of tuning records. + If is str, then it should be the filename of a records log file. + Each row of this file is an encoded record pair. + Otherwise, it is an iterator. + default: ConfigEntity, optional + The default config to return when no history records + """ + def __init__(self, records, default=None): + super(ApplyHistoryBest, self).__init__() + + self.best_by_targetkey = {} + self.best_by_model = {} + self._default = default + + if records: + self.load(records) + + def load(self, records): + """Load records to this dispatch context + + Parameters + ---------- + records : str or iterator of (MeasureInput, MeasureResult) + Collection of tuning records. + If is str, then it should be the filename of a records log file. + Each row of this file is an encoded record pair. + Otherwise, it is an iterator. + """ + from ..record import load_from_file + + if isinstance(records, str): + records = load_from_file(records) + if not records: + return + + best_by_targetkey = self.best_by_targetkey + best_by_model = self.best_by_model + + counter = 0 + for inp, res in records: + counter += 1 + if res.error_no != 0: + continue + + # use target keys in tvm target system as key to build best map + for k in inp.target.keys: + key = (k, inp.task.workload) + if key not in best_by_targetkey: + best_by_targetkey[key] = (inp, res) + else: + _, other_res = best_by_targetkey[key] + if np.mean(other_res.costs) > np.mean(res.costs): + best_by_targetkey[key] = (inp, res) + + # use model as key to build best map + for opt in inp.target.options: + if opt.startswith("-model"): + model = opt[7:] + key = (model, inp.task.workload) + if key not in best_by_model: + best_by_model[key] = (inp, res) + else: + _, other_res = best_by_model[key] + if np.mean(other_res.costs) > np.mean(res.costs): + best_by_model[key] = (inp, res) + break + + logging.debug("Finish loading %d records", counter) + + def query(self, target, workload): + if target is None: + raise RuntimeError("Need a target context to find the history best. " + "Hint: If your target is llvm, use `with tvm.target.create('llvm'):`" + " above the dispatcher call. So does other target. ") + + # first try matching by model + for opt in target.options: + if opt.startswith("-model"): + model = opt[7:] + key = (model, workload) + if key in self.best_by_model: + return self.best_by_model[key][0].config + + # then try matching by target key + for k in target.keys: + key = (k, workload) + if key in self.best_by_targetkey: + return self.best_by_targetkey[key][0].config + + if self._default: + return self._default + raise RuntimeError( + "Cannot find config for target=%s, workload=%s" % (target, workload)) + +ROOT_DISPATCH_CONTEXT = ApplyHistoryBest([]) +DispatchContext.current = ROOT_DISPATCH_CONTEXT diff --git a/python/tvm/autotvm/task/topi_integration.py b/python/tvm/autotvm/task/topi_integration.py index e90548fa81f0..012ca4a214e9 100644 --- a/python/tvm/autotvm/task/topi_integration.py +++ b/python/tvm/autotvm/task/topi_integration.py @@ -34,7 +34,7 @@ def register_topi_compute(topi_compute, target_keys, template_keys, func=None): Parameters ---------- topi_compute: GenericFunc - The overloaded topi compute call + The topi compute function that will be overloaded target_keys: str or list of str The compilation target. The same as the argument of GenericFunc.register. template_keys: str or list of str @@ -104,6 +104,8 @@ def template_call(cfg, *args, **kwargs): return op.output(0) return [op.output(i) for i in range(len(node))] + return f + if func: _decorator(func) @@ -122,7 +124,7 @@ def register_topi_schedule(topi_schedule, target_keys, template_keys, func=None) Parameters ---------- topi_schedule: GenericFunc - The overloaded topi schedule call + The topi schedule function that will be overloaded target_keys: str or list of str The compilation target template_keys: str or list of str @@ -183,6 +185,8 @@ def template_call(cfg, outs): return f(outs) return f(cfg, outs) + return f + if func: _decorator(func) diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index 2082ef5f518a..4fb29485876e 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -9,26 +9,16 @@ import os import json -from .record import ApplyHistoryBest +from .task import ROOT_DISPATCH_CONTEXT from ..contrib.util import tempdir from ..contrib.download import download AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") -class EmptyContext(object): - """An empty context""" - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_val, exc_tb): - pass - - -def context(target, extra_files=None): - """Return the dispatch context with pre-tuned parameters. - The corresponding downloaded *.log files under tophub root path will be loaded. - Users can also add their own files in argument `extra_files`. +def load(target, extra_files=None): + """Load pre-tuned parameters to the root dispatch context. + The corresponding downloaded *.log files under TopHub root path will be loaded. Parameters ---------- @@ -36,25 +26,28 @@ def context(target, extra_files=None): The compilation target extra_files: list of str, optional Extra log files to load + + Note + ---- + If the current dispatch context is not root dispatch context, + this function won't affect the current dispatch context. """ rootpath = AUTOTVM_TOPHUB_ROOT_PATH - best_context = ApplyHistoryBest([]) + root_context = ROOT_DISPATCH_CONTEXT big_target = str(target).split()[0] if os.path.isfile(os.path.join(rootpath, big_target + ".log")): - best_context.load(os.path.join(rootpath, big_target + ".log")) + root_context.load(os.path.join(rootpath, big_target + ".log")) for opt in target.options: if opt.startswith("-device"): model = opt[8:] if os.path.isfile(os.path.join(rootpath, model) + ".log"): - best_context.load(os.path.join(rootpath, model) + ".log") + root_context.load(os.path.join(rootpath, model) + ".log") if extra_files: for filename in extra_files: - best_context.load(filename) - - return best_context + root_context.load(filename) def download_package(backend): diff --git a/topi/python/topi/arm_cpu/conv2d.py b/topi/python/topi/arm_cpu/conv2d.py index 23c907c7f3dd..f5dbec8e552b 100644 --- a/topi/python/topi/arm_cpu/conv2d.py +++ b/topi/python/topi/arm_cpu/conv2d.py @@ -25,18 +25,18 @@ def _conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype): @conv2d.register('arm_cpu') @autotvm.task.dispatcher -def config_dispatcher(data, kernel, strides, padding, layout, out_dtype): +def conv2d_arm_cpu(data, kernel, strides, padding, layout, out_dtype): """TOPI compute callback. Mark this function as a dispatcher, so this template can assign config according to workload""" return _conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype) -@config_dispatcher.register(['direct']) +@conv2d_arm_cpu.register(['direct']) def decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype): """spatial packing template""" return _decl_spatial_pack(cfg, data, kernel, strides, padding, layout, out_dtype, num_tile=2) @autotvm.task.register_topi_schedule(schedule_conv2d_nchw, 'arm_cpu', ['direct', 'winograd']) -def schedule_conv2d_nchw_(cfg, outs): +def schedule_conv2d_nchw_arm_cpu(cfg, outs): """TOPI schedule callback""" s = tvm.create_schedule([x.op for x in outs]) @@ -204,7 +204,7 @@ def _schedule_spatial_pack(cfg, s, data_vec, kernel_vec, return s -@config_dispatcher.register('winograd') +@conv2d_arm_cpu.register('winograd') def decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype): tile_size = 4 return _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_size) @@ -251,24 +251,23 @@ def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_ [1, -2, 4, -8], [0, 0, 0, 1]], out_dtype) elif tile_size == 2: + G_data = np.array([ + [1, 0, 0], + [1.0/2, 1.0/2, 1.0/2], + [1.0/2, -1.0/2, 1.0/2], + [0, 0, 1]], np.float32) + B_data = np.array([ [1, 0, 0, 0], [0, 1, -1, 1], [-1, 1, 1, 0], [0, 0, 0, -1]], out_dtype) - G_data = np.array([ - [1, 0, 0], - [1.0/2, 1.0/2, 1.0/2], - [1.0/2, -1.0/2, 1.0/2], - [0, 0, 1], ], out_dtype) - A_data = np.array([ [1, 0], [1, 1], [1, -1], - [0, -1], - ], out_dtype) + [0, -1]], out_dtype) else: raise ValueError("Unsupported tile size for winograd: " + str(tile_size)) @@ -345,7 +344,6 @@ def _schedule_winograd(cfg, s, output, last): U, V = M.op.input_tensors d, B = V.op.input_tensors data_pad = d.op.input_tensors[0] - data = data_pad.op.input_tensors[0] # padding s[data_pad].compute_inline() diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index d1b9256a13b8..9e587b5368c0 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -40,8 +40,8 @@ def get_ref_data(): np.testing.assert_allclose(b.asnumpy(), b_np, rtol=1e-5) def test_conv2d(): - with autotvm.tophub.context(tvm.target.arm_cpu()): - verify_conv2d(1, 56, 64, 64, 3, 1, 1) + autotvm.tophub.load(tvm.target.arm_cpu('rasp3b')) + verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": test_conv2d() From 9bf3ba4b364944c394160b0123cf6d7c6ae66312 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 08:17:15 -0700 Subject: [PATCH 70/76] update vta --- vta/python/vta/top/__init__.py | 1 + vta/python/vta/top/arm_conv2d.py | 21 +++++++++++ vta/python/vta/top/vta_conv2d.py | 11 +++--- .../integration/test_benchmark_topi_conv2d.py | 14 ++++---- vta/tutorials/resnet.py | 36 +++++++------------ 5 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 vta/python/vta/top/arm_conv2d.py diff --git a/vta/python/vta/top/__init__.py b/vta/python/vta/top/__init__.py index d0ccef9a3bf3..614ed2347181 100644 --- a/vta/python/vta/top/__init__.py +++ b/vta/python/vta/top/__init__.py @@ -2,3 +2,4 @@ from .vta_conv2d import packed_conv2d, schedule_packed_conv2d from . import vta_conv2d +from . import arm_conv2d diff --git a/vta/python/vta/top/arm_conv2d.py b/vta/python/vta/top/arm_conv2d.py new file mode 100644 index 000000000000..634348a87cfe --- /dev/null +++ b/vta/python/vta/top/arm_conv2d.py @@ -0,0 +1,21 @@ +"""Reuse conv2d schedule from ARM CPU""" + +import tvm + +from topi.nn import conv2d, conv2d_alter_layout +from topi import generic + +@conv2d.register(["vtacpu", "vta"]) +def compute(*args, **kwargs): + with tvm.target.arm_cpu("vtacpu"): + return conv2d(*args, **kwargs) + +@generic.schedule_conv2d_nchw.register(["vtacpu", "vta"]) +def schedule(*args, **kwargs): + with tvm.target.arm_cpu("vtacpu"): + return generic.schedule_conv2d_nchw(*args, **kwargs) + +@conv2d_alter_layout.register(["vtacpu", "vta"]) +def alter(*args, **kwargs): + with tvm.target.arm_cpu("vtacpu"): + return conv2d_alter_layout(*args, **kwargs) diff --git a/vta/python/vta/top/vta_conv2d.py b/vta/python/vta/top/vta_conv2d.py index 28cd8a49cb0f..e7d584a791fc 100644 --- a/vta/python/vta/top/vta_conv2d.py +++ b/vta/python/vta/top/vta_conv2d.py @@ -244,8 +244,11 @@ def is_packed_layout(layout): return False @reg.register_alter_op_layout("conv2d", level=15) -def alter_conv2d_layout(*_): - return None +def alter_conv2d_layout(attrs, inputs, out): + layout = attrs['layout'] + if is_packed_layout(layout): + return None + return _nn.alter_conv2d_layout(attrs, inputs, out) @reg.register_compute("conv2d", level=15) @@ -368,7 +371,6 @@ def _traverse(op): oshape = topi.util.get_const_tuple(output.shape) s = tvm.create_schedule(output.op) - # setup pad if pad_data is not None: cdata = pad_data @@ -394,7 +396,6 @@ def _traverse(op): h_factor = (plan.h_factor if plan.h_factor else oshape[2]) w_factor = (plan.w_factor if plan.w_factor else oshape[3]) - x_bo, x_co, x_i, x_j, x_bi, x_ci = s[output].op.axis x_co0, x_co1 = s[output].split(x_co, factor=oc_factor) x_i0, x_i1 = s[output].split(x_i, factor=h_factor) @@ -459,6 +460,7 @@ def __init__(self, self.oc_nthread = oc_nthread self.h_nthread = h_nthread self.debug_sync = debug_sync + def __str__(self): return "{}.{}.{}.{}.{}.{}.{}".format( self.b_factor, self.oc_factor, self.ic_factor, @@ -483,7 +485,6 @@ def __str__(self): 11: Workload(1, 7, 7, 512, 512, 3, 3, 1, 1, 1, 1), } -_WL2PLAN = {} for idx in RESNET: scheds = find_schedules(RESNET[idx], vt_only=True, best_only=True)[0] _WL2PLAN[RESNET[idx]] = scheds diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 53f4f6ba5e19..d5d2385f76d4 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -127,9 +127,10 @@ def _run(env, remote): with tvm.target.arm_cpu("pynq"): run_cpu_conv2d(env, remote, key, batch_size, wl) - # download pre-tuned parameters and load the dispatch context - with autotvm.tophub.context(tvm.target.arm_cpu()): - vta.testing.run(_run) + # load pre-tuned operator parameters for ARM CPU + autotvm.tophub.load(tvm.target.arm_cpu()) + + vta.testing.run(_run) def test_vta_conv2d(): @@ -260,9 +261,10 @@ def _run(env, remote): print(wl) run_vta_conv2d(env, remote, key, batch_size, wl) - # download pre-tuned parameters and load the dispatch context - with autotvm.tophub.context(tvm.target.arm_cpu()): - vta.testing.run(_run) + # load pre-tuned operator parameters for ARM CPU + autotvm.tophub.load(tvm.target.arm_cpu()) + + vta.testing.run(_run) if __name__ == "__main__": diff --git a/vta/tutorials/resnet.py b/vta/tutorials/resnet.py index 7a2b0ab50925..5a9869c8835f 100644 --- a/vta/tutorials/resnet.py +++ b/vta/tutorials/resnet.py @@ -8,7 +8,6 @@ """ - ###################################################################### # Import Libraries # ---------------- @@ -17,26 +16,21 @@ from __future__ import absolute_import, print_function import os -import sys -import nnvm -import nnvm.compiler -import tvm -import vta -import vta.testing +import time +from io import BytesIO + import numpy as np -import json import requests -import time +from matplotlib import pyplot as plt +from PIL import Image -from nnvm.compiler import graph_attr -from tvm import rpc +import tvm +from tvm import rpc, autotvm from tvm.contrib import graph_runtime, util from tvm.contrib.download import download -from vta.testing import simulator - -from io import BytesIO -from matplotlib import pyplot as plt -from PIL import Image +import nnvm.compiler +import vta +import vta.testing # Load VTA parameters from the vta/config/vta_config.json file env = vta.get_env() @@ -76,7 +70,6 @@ def classify(m, image): # Takes in a path to a graph file, params file, and device target # Returns the NNVM graph object, a compiled library object, and the params dict def generate_graph(graph_fn, params_fn, device="vta"): - # Measure build start time build_start = time.time() @@ -90,6 +83,9 @@ def generate_graph(graph_fn, params_fn, device="vta"): elif env.TARGET == "pynq": target_host = "llvm -mtriple=armv7-none-linux-gnueabihf -mcpu=cortex-a9 -mattr=+neon" + # Load pre-tuned parameters of conv2d for ARM CPU + autotvm.tophub.load(tvm.target.arm_cpu('pynq')) + # Load the ResNet-18 graph and parameters sym = nnvm.graph.load_json(open(graph_fn).read()) params = nnvm.compiler.load_param_dict(open(params_fn, 'rb').read()) @@ -100,12 +96,6 @@ def generate_graph(graph_fn, params_fn, device="vta"): shape_dict.update({k: v.shape for k, v in params.items()}) dtype_dict.update({k: str(v.dtype) for k, v in params.items()}) - # Create NNVM graph - graph = nnvm.graph.create(sym) - graph_attr.set_shape_inputs(sym, shape_dict) - graph_attr.set_dtype_inputs(sym, dtype_dict) - graph = graph.apply("InferShape").apply("InferType") - # Apply NNVM graph optimization passes sym = vta.graph.clean_cast(sym) sym = vta.graph.clean_conv_fuse(sym) From 56366cc948e3a74e03ceeadb9ff26281c902e7c0 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 08:30:17 -0700 Subject: [PATCH 71/76] fix target for vta --- python/tvm/target.py | 2 +- vta/tests/python/integration/test_benchmark_topi_conv2d.py | 3 +-- vta/tutorials/resnet.py | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/python/tvm/target.py b/python/tvm/target.py index 224614839d3e..439751db73bd 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -434,7 +434,7 @@ def arm_cpu(model='unknown', options=None): "rk3399": ["-model=rk3399", "-target=aarch64-linux-gnu"], "pynq": ["-model=pynq", "-target=armv7a-linux-eabi"], } - pre_defined_opt = trans_table.get(model, []) + pre_defined_opt = trans_table.get(model, ["-model=%s" % model]) # download pre-tuned parameters for arm_cpu if there is not any. autotvm.tophub.check_package('arm_cpu') diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index d5d2385f76d4..9d60f04768a7 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -124,7 +124,7 @@ def _run(env, remote): key = "resnet-cfg[%d]" % i print("key=%s" % key) print(wl) - with tvm.target.arm_cpu("pynq"): + with tvm.target.arm_cpu("vtacpu"): run_cpu_conv2d(env, remote, key, batch_size, wl) # load pre-tuned operator parameters for ARM CPU @@ -176,7 +176,6 @@ def get_ref_data(): a_np.astype(acc_dtype), w_np.astype(acc_dtype), stride, padding).astype(acc_dtype) return a_np, w_np, b_np - def verify(s, check_correctness): mod = vta.build(s, [data, kernel, bias, res], "ext_dev", env.target_host, name="conv2d") diff --git a/vta/tutorials/resnet.py b/vta/tutorials/resnet.py index 5a9869c8835f..79baec0bf867 100644 --- a/vta/tutorials/resnet.py +++ b/vta/tutorials/resnet.py @@ -172,7 +172,6 @@ def generate_graph(graph_fn, params_fn, device="vta"): # We configure both the bitstream and the runtime system on the Pynq # to match the VTA configuration specified by the vta_config.json file. if env.TARGET == "pynq": - # Make sure that TVM was compiled with RPC=1 assert tvm.module.enabled("rpc") remote = rpc.connect(host, port) @@ -215,7 +214,6 @@ def generate_graph(graph_fn, params_fn, device="vta"): # Set the parameters m.set_input(**params) - ###################################################################### # Run ResNet-18 inference on a sample image # ----------------------------------------- From a3bfcb21eba694e688c1ed5e49beaf446fecd234 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Wed, 1 Aug 2018 11:00:34 -0700 Subject: [PATCH 72/76] trigger CI --- python/tvm/exec/autotvm_log_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tvm/exec/autotvm_log_editor.py b/python/tvm/exec/autotvm_log_editor.py index 3256944ac811..c524fb5dc785 100644 --- a/python/tvm/exec/autotvm_log_editor.py +++ b/python/tvm/exec/autotvm_log_editor.py @@ -32,7 +32,7 @@ try: autotvm.record.pick_best(filename, tmp_fout) except Exception: # pylint: disable=broad-except - warnings.warn("Ignore invalid file %s", filename) + warnings.warn("Ignore invalid file %s" % filename) logging.info("Run final filter...") autotvm.record.pick_best(tmp_filename, args.o) From 5afc50400aee2c53d5be1fa1cb983434df5b3ae2 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 14:18:30 -0700 Subject: [PATCH 73/76] use with for tophub context --- nnvm/python/nnvm/compiler/build_module.py | 130 +++++++++--------- python/tvm/autotvm/task/__init__.py | 3 +- python/tvm/autotvm/task/dispatcher.py | 3 - python/tvm/autotvm/tophub.py | 42 ++++-- python/tvm/autotvm/util.py | 10 ++ python/tvm/target.py | 13 +- topi/tests/python/test_topi_conv2d.py | 4 +- .../integration/test_benchmark_topi_conv2d.py | 11 +- vta/tutorials/resnet.py | 10 +- 9 files changed, 128 insertions(+), 98 deletions(-) diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index 8084241a414d..fd8599bcfa93 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -239,70 +239,74 @@ def build(graph, target=None, shape=None, dtype="float32", raise ValueError("Target is not set in env or passed as argument.") target = tvm.target.create(target) - # load pre-tuned parameters from TopHub to the root dispatch context - autotvm.tophub.load(target) - - shape = shape if shape else {} - if not isinstance(shape, dict): - raise TypeError("require shape to be dict") - for value in shape.values(): - if not all(isinstance(x, int) for x in value): - raise TypeError("shape value must be int iterator") - - cfg = BuildConfig.current - graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) - shape, dtype = _update_shape_dtype(shape, dtype, params) - - # correct layout if necessary - layout = layout if layout else {} - graph = graph_attr.set_layout_inputs(graph, layout) - graph = graph.apply("CorrectLayout") - index = graph.index - layouts = graph.json_attr("layout") - layout = {x: layouts[index.entry_id(x)] for x in index.input_names} - - # Initial pass do shape type inference - ishape, _ = graph_util.infer_shape(graph, **shape) - shape.update(zip(graph.index.input_names, ishape)) - if not isinstance(dtype, str): - idtype, _ = graph_util.infer_dtype(graph, **dtype) - dtype.update(zip(graph.index.input_names, idtype)) - # Initialize all variables specified in _all_var_init - init_var = {} - if _all_var_init: - init_var = initialize_variables(shape, dtype) - # Apply optimization - with target: - graph = optimize(graph, shape, dtype, layout) - - # Clear extra params without nodes. - _remove_noref_params(params, graph) - - # Precompute prune - if params and cfg.pass_enabled("PrecomputePrune"): - graph, params = precompute_prune(graph, params) - shape, dtype = _update_shape_dtype(shape, dtype, params) - # Operator Fusion and generation - graph = graph_attr.set_shape_inputs(graph, shape) - graph = graph.apply("InferShape") - graph = graph_attr.set_dtype_inputs(graph, dtype) - graph._set_json_attr("target", str(target), "str") - if target_host is not None: - graph._set_json_attr("target_host", str(target_host), "str") - if cfg.pass_enabled("OpFusion"): - graph._set_json_attr("opt_level", 1, "int") + # if not inside an autotvm config dispatch context, load pre-tuned parameters from TopHub + if autotvm.task.DispatchContext.current is None: + tophub_context = autotvm.tophub.context(target) else: - graph._set_json_attr("opt_level", 0, "int") - graph = graph.apply("InferShape").apply("InferType") - with target: - graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") - libmod = graph_attr._move_out_module(graph, "module") - # Write variable initial values into params - if init_var: - if params is None: - params = {} - params.update(init_var) - return graph, libmod, params + tophub_context = autotvm.util.EmptyContext() + + with tophub_context: + shape = shape if shape else {} + if not isinstance(shape, dict): + raise TypeError("require shape to be dict") + for value in shape.values(): + if not all(isinstance(x, int) for x in value): + raise TypeError("shape value must be int iterator") + + cfg = BuildConfig.current + graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) + shape, dtype = _update_shape_dtype(shape, dtype, params) + + # correct layout if necessary + layout = layout if layout else {} + graph = graph_attr.set_layout_inputs(graph, layout) + graph = graph.apply("CorrectLayout") + index = graph.index + layouts = graph.json_attr("layout") + layout = {x: layouts[index.entry_id(x)] for x in index.input_names} + + # Initial pass do shape type inference + ishape, _ = graph_util.infer_shape(graph, **shape) + shape.update(zip(graph.index.input_names, ishape)) + if not isinstance(dtype, str): + idtype, _ = graph_util.infer_dtype(graph, **dtype) + dtype.update(zip(graph.index.input_names, idtype)) + # Initialize all variables specified in _all_var_init + init_var = {} + if _all_var_init: + init_var = initialize_variables(shape, dtype) + # Apply optimization + with target: + graph = optimize(graph, shape, dtype, layout) + + # Clear extra params without nodes. + _remove_noref_params(params, graph) + + # Precompute prune + if params and cfg.pass_enabled("PrecomputePrune"): + graph, params = precompute_prune(graph, params) + shape, dtype = _update_shape_dtype(shape, dtype, params) + # Operator Fusion and generation + graph = graph_attr.set_shape_inputs(graph, shape) + graph = graph.apply("InferShape") + graph = graph_attr.set_dtype_inputs(graph, dtype) + graph._set_json_attr("target", str(target), "str") + if target_host is not None: + graph._set_json_attr("target_host", str(target_host), "str") + if cfg.pass_enabled("OpFusion"): + graph._set_json_attr("opt_level", 1, "int") + else: + graph._set_json_attr("opt_level", 0, "int") + graph = graph.apply("InferShape").apply("InferType") + with target: + graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") + libmod = graph_attr._move_out_module(graph, "module") + # Write variable initial values into params + if init_var: + if params is None: + params = {} + params.update(init_var) + return graph, libmod, params def _remove_noref_params(params, graph): """ Helper to clear non referenced params diff --git a/python/tvm/autotvm/task/__init__.py b/python/tvm/autotvm/task/__init__.py index 86720da5a975..0d43f92656cd 100644 --- a/python/tvm/autotvm/task/__init__.py +++ b/python/tvm/autotvm/task/__init__.py @@ -9,8 +9,7 @@ from .task import Task, create, register, template, get_config, args_to_workload from .space import ConfigSpace, ConfigEntity from .code_hash import attach_code_hash, attach_code_hash_to_arg -from .dispatcher import DispatchContext, ApplyConfig, ApplyHistoryBest, ROOT_DISPATCH_CONTEXT, \ - dispatcher +from .dispatcher import DispatchContext, ApplyConfig, ApplyHistoryBest, dispatcher from .topi_integration import register_topi_compute, register_topi_schedule from .nnvm_integration import extract_from_graph diff --git a/python/tvm/autotvm/task/dispatcher.py b/python/tvm/autotvm/task/dispatcher.py index 0a96133ef085..beb4e4dcf204 100644 --- a/python/tvm/autotvm/task/dispatcher.py +++ b/python/tvm/autotvm/task/dispatcher.py @@ -242,6 +242,3 @@ def query(self, target, workload): return self._default raise RuntimeError( "Cannot find config for target=%s, workload=%s" % (target, workload)) - -ROOT_DISPATCH_CONTEXT = ApplyHistoryBest([]) -DispatchContext.current = ROOT_DISPATCH_CONTEXT diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index 4fb29485876e..70a3a511ec61 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -9,16 +9,26 @@ import os import json -from .task import ROOT_DISPATCH_CONTEXT +from .task import ApplyHistoryBest +from .. import target as _target from ..contrib.util import tempdir from ..contrib.download import download AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") -def load(target, extra_files=None): - """Load pre-tuned parameters to the root dispatch context. - The corresponding downloaded *.log files under TopHub root path will be loaded. +def _alias(name): + """convert alias for some packages""" + table = { + 'vtacpu': 'vta', + } + return table.get(name, name) + + +def context(target, extra_files=None): + """Return the dispatch context with pre-tuned parameters. + The corresponding downloaded *.log files under tophub root path will be loaded. + Users can also add their own files in argument `extra_files`. Parameters ---------- @@ -26,28 +36,28 @@ def load(target, extra_files=None): The compilation target extra_files: list of str, optional Extra log files to load - - Note - ---- - If the current dispatch context is not root dispatch context, - this function won't affect the current dispatch context. """ rootpath = AUTOTVM_TOPHUB_ROOT_PATH - root_context = ROOT_DISPATCH_CONTEXT + best_context = ApplyHistoryBest([]) + + if isinstance(target, str): + target = _target.create(target) big_target = str(target).split()[0] if os.path.isfile(os.path.join(rootpath, big_target + ".log")): - root_context.load(os.path.join(rootpath, big_target + ".log")) + best_context.load(os.path.join(rootpath, big_target + ".log")) for opt in target.options: if opt.startswith("-device"): - model = opt[8:] + model = _alias(opt[8:]) if os.path.isfile(os.path.join(rootpath, model) + ".log"): - root_context.load(os.path.join(rootpath, model) + ".log") + best_context.load(os.path.join(rootpath, model) + ".log") if extra_files: for filename in extra_files: - root_context.load(filename) + best_context.load(filename) + + return best_context def download_package(backend): @@ -68,6 +78,7 @@ def download_package(backend): if not os.path.isdir(path): os.mkdir(path) + backend = _alias(backend) logging.info("Download pre-tuned parameters for %s", backend) download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/%s.log" % backend, os.path.join(rootpath, backend + ".log"), True, verbose=0) @@ -82,9 +93,10 @@ def check_package(backend): backend: str The name of package """ + backend = _alias(backend) + if os.path.isfile(os.path.join(AUTOTVM_TOPHUB_ROOT_PATH, backend + ".log")): return - download_package(backend) diff --git a/python/tvm/autotvm/util.py b/python/tvm/autotvm/util.py index df58bde38558..99a2c85aa10e 100644 --- a/python/tvm/autotvm/util.py +++ b/python/tvm/autotvm/util.py @@ -8,6 +8,16 @@ from .. import expr, ir_pass + +class EmptyContext(object): + """An empty context""" + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def get_rank(values): """get rank of items diff --git a/python/tvm/target.py b/python/tvm/target.py index 439751db73bd..1238523d4ede 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -413,7 +413,7 @@ def opengl(options=None): def arm_cpu(model='unknown', options=None): """Returns a ARM CPU target. - This function will also donwload pre-tuned op parameters + This function will also download pre-tuned op parameters when there is none. Parameters ---------- @@ -444,6 +444,17 @@ def arm_cpu(model='unknown', options=None): return _api_internal._TargetCreate("llvm", *opts) +def rasp(options=None): + """Return a Raspberry 3b target. + + Parameters + ---------- + options : str or list of str + Additional options + """ + return arm_cpu('rasp3b', options) + + def create(target_str): """Get a target given target string. diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index 9e587b5368c0..124c98c65c7a 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -40,8 +40,8 @@ def get_ref_data(): np.testing.assert_allclose(b.asnumpy(), b_np, rtol=1e-5) def test_conv2d(): - autotvm.tophub.load(tvm.target.arm_cpu('rasp3b')) - verify_conv2d(1, 56, 64, 64, 3, 1, 1) + with autotvm.tophub.context(tvm.target.arm_cpu('rasp3b')): + verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": test_conv2d() diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 9d60f04768a7..ca2451dec614 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -124,13 +124,13 @@ def _run(env, remote): key = "resnet-cfg[%d]" % i print("key=%s" % key) print(wl) - with tvm.target.arm_cpu("vtacpu"): + with tvm.target.create("llvm -device=vtacpu"): run_cpu_conv2d(env, remote, key, batch_size, wl) # load pre-tuned operator parameters for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu()) - - vta.testing.run(_run) + autotvm.tophub.check_package('vta') + with autotvm.tophub.context('llvm -device=vtacpu'): + vta.testing.run(_run) def test_vta_conv2d(): @@ -260,9 +260,6 @@ def _run(env, remote): print(wl) run_vta_conv2d(env, remote, key, batch_size, wl) - # load pre-tuned operator parameters for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu()) - vta.testing.run(_run) diff --git a/vta/tutorials/resnet.py b/vta/tutorials/resnet.py index 79baec0bf867..8d33a91d5691 100644 --- a/vta/tutorials/resnet.py +++ b/vta/tutorials/resnet.py @@ -83,9 +83,6 @@ def generate_graph(graph_fn, params_fn, device="vta"): elif env.TARGET == "pynq": target_host = "llvm -mtriple=armv7-none-linux-gnueabihf -mcpu=cortex-a9 -mattr=+neon" - # Load pre-tuned parameters of conv2d for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu('pynq')) - # Load the ResNet-18 graph and parameters sym = nnvm.graph.load_json(open(graph_fn).read()) params = nnvm.compiler.load_param_dict(open(params_fn, 'rb').read()) @@ -156,6 +153,9 @@ def generate_graph(graph_fn, params_fn, device="vta"): # Read in ImageNet Categories synset = eval(open(os.path.join(data_dir, categ_fn)).read()) +# Download pre-tuned op parameters of conv2d for ARM CPU used in VTA +autotvm.tophub.check_package('vta') + ###################################################################### # Setup the Pynq Board's RPC Server @@ -198,8 +198,8 @@ def generate_graph(graph_fn, params_fn, device="vta"): # ------------------------ # Build the ResNet graph runtime, and configure the parameters. -# Set ``device=cpu`` to run inference on the CPU, -# or ``device=vtacpu`` to run inference on the FPGA. +# Set ``device=vtacpu`` to run inference on the CPU +# or ``device=vta`` to run inference on the FPGA. device = "vta" # Device context From 7e4c94bcb50c10819e24e03935dd91efb24fc572 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 14:18:30 -0700 Subject: [PATCH 74/76] fix typo --- apps/benchmark/arm_cpu_imagenet_bench.py | 2 +- nnvm/python/nnvm/compiler/build_module.py | 130 +++++++++--------- python/tvm/autotvm/task/__init__.py | 3 +- python/tvm/autotvm/task/dispatcher.py | 3 - python/tvm/autotvm/tophub.py | 42 ++++-- python/tvm/autotvm/util.py | 10 ++ python/tvm/target.py | 13 +- topi/tests/python/test_topi_conv2d.py | 4 +- .../integration/test_benchmark_topi_conv2d.py | 11 +- vta/tutorials/resnet.py | 10 +- 10 files changed, 129 insertions(+), 99 deletions(-) diff --git a/apps/benchmark/arm_cpu_imagenet_bench.py b/apps/benchmark/arm_cpu_imagenet_bench.py index 58b1f620f142..7baf244e0dae 100644 --- a/apps/benchmark/arm_cpu_imagenet_bench.py +++ b/apps/benchmark/arm_cpu_imagenet_bench.py @@ -91,6 +91,6 @@ def get_network(name, batch_size): # evaluate ftimer = module.module.time_evaluator("run", ctx, number=args.number, repeat=3) - prof_res = np.array(ftimer().results) * 1000 # multiply 1000 for converting to millionsecond + prof_res = np.array(ftimer().results) * 1000 # multiply 1000 for converting to millisecond print("%-20s %-19s (%s)" % (network, "%.2f ms" % np.mean(prof_res), "%.2f ms" % np.std(prof_res))) diff --git a/nnvm/python/nnvm/compiler/build_module.py b/nnvm/python/nnvm/compiler/build_module.py index 8084241a414d..fd8599bcfa93 100644 --- a/nnvm/python/nnvm/compiler/build_module.py +++ b/nnvm/python/nnvm/compiler/build_module.py @@ -239,70 +239,74 @@ def build(graph, target=None, shape=None, dtype="float32", raise ValueError("Target is not set in env or passed as argument.") target = tvm.target.create(target) - # load pre-tuned parameters from TopHub to the root dispatch context - autotvm.tophub.load(target) - - shape = shape if shape else {} - if not isinstance(shape, dict): - raise TypeError("require shape to be dict") - for value in shape.values(): - if not all(isinstance(x, int) for x in value): - raise TypeError("shape value must be int iterator") - - cfg = BuildConfig.current - graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) - shape, dtype = _update_shape_dtype(shape, dtype, params) - - # correct layout if necessary - layout = layout if layout else {} - graph = graph_attr.set_layout_inputs(graph, layout) - graph = graph.apply("CorrectLayout") - index = graph.index - layouts = graph.json_attr("layout") - layout = {x: layouts[index.entry_id(x)] for x in index.input_names} - - # Initial pass do shape type inference - ishape, _ = graph_util.infer_shape(graph, **shape) - shape.update(zip(graph.index.input_names, ishape)) - if not isinstance(dtype, str): - idtype, _ = graph_util.infer_dtype(graph, **dtype) - dtype.update(zip(graph.index.input_names, idtype)) - # Initialize all variables specified in _all_var_init - init_var = {} - if _all_var_init: - init_var = initialize_variables(shape, dtype) - # Apply optimization - with target: - graph = optimize(graph, shape, dtype, layout) - - # Clear extra params without nodes. - _remove_noref_params(params, graph) - - # Precompute prune - if params and cfg.pass_enabled("PrecomputePrune"): - graph, params = precompute_prune(graph, params) - shape, dtype = _update_shape_dtype(shape, dtype, params) - # Operator Fusion and generation - graph = graph_attr.set_shape_inputs(graph, shape) - graph = graph.apply("InferShape") - graph = graph_attr.set_dtype_inputs(graph, dtype) - graph._set_json_attr("target", str(target), "str") - if target_host is not None: - graph._set_json_attr("target_host", str(target_host), "str") - if cfg.pass_enabled("OpFusion"): - graph._set_json_attr("opt_level", 1, "int") + # if not inside an autotvm config dispatch context, load pre-tuned parameters from TopHub + if autotvm.task.DispatchContext.current is None: + tophub_context = autotvm.tophub.context(target) else: - graph._set_json_attr("opt_level", 0, "int") - graph = graph.apply("InferShape").apply("InferType") - with target: - graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") - libmod = graph_attr._move_out_module(graph, "module") - # Write variable initial values into params - if init_var: - if params is None: - params = {} - params.update(init_var) - return graph, libmod, params + tophub_context = autotvm.util.EmptyContext() + + with tophub_context: + shape = shape if shape else {} + if not isinstance(shape, dict): + raise TypeError("require shape to be dict") + for value in shape.values(): + if not all(isinstance(x, int) for x in value): + raise TypeError("shape value must be int iterator") + + cfg = BuildConfig.current + graph = graph if isinstance(graph, _graph.Graph) else _graph.create(graph) + shape, dtype = _update_shape_dtype(shape, dtype, params) + + # correct layout if necessary + layout = layout if layout else {} + graph = graph_attr.set_layout_inputs(graph, layout) + graph = graph.apply("CorrectLayout") + index = graph.index + layouts = graph.json_attr("layout") + layout = {x: layouts[index.entry_id(x)] for x in index.input_names} + + # Initial pass do shape type inference + ishape, _ = graph_util.infer_shape(graph, **shape) + shape.update(zip(graph.index.input_names, ishape)) + if not isinstance(dtype, str): + idtype, _ = graph_util.infer_dtype(graph, **dtype) + dtype.update(zip(graph.index.input_names, idtype)) + # Initialize all variables specified in _all_var_init + init_var = {} + if _all_var_init: + init_var = initialize_variables(shape, dtype) + # Apply optimization + with target: + graph = optimize(graph, shape, dtype, layout) + + # Clear extra params without nodes. + _remove_noref_params(params, graph) + + # Precompute prune + if params and cfg.pass_enabled("PrecomputePrune"): + graph, params = precompute_prune(graph, params) + shape, dtype = _update_shape_dtype(shape, dtype, params) + # Operator Fusion and generation + graph = graph_attr.set_shape_inputs(graph, shape) + graph = graph.apply("InferShape") + graph = graph_attr.set_dtype_inputs(graph, dtype) + graph._set_json_attr("target", str(target), "str") + if target_host is not None: + graph._set_json_attr("target_host", str(target_host), "str") + if cfg.pass_enabled("OpFusion"): + graph._set_json_attr("opt_level", 1, "int") + else: + graph._set_json_attr("opt_level", 0, "int") + graph = graph.apply("InferShape").apply("InferType") + with target: + graph = graph.apply("GraphFusePartition").apply("GraphFuseCompile") + libmod = graph_attr._move_out_module(graph, "module") + # Write variable initial values into params + if init_var: + if params is None: + params = {} + params.update(init_var) + return graph, libmod, params def _remove_noref_params(params, graph): """ Helper to clear non referenced params diff --git a/python/tvm/autotvm/task/__init__.py b/python/tvm/autotvm/task/__init__.py index 86720da5a975..0d43f92656cd 100644 --- a/python/tvm/autotvm/task/__init__.py +++ b/python/tvm/autotvm/task/__init__.py @@ -9,8 +9,7 @@ from .task import Task, create, register, template, get_config, args_to_workload from .space import ConfigSpace, ConfigEntity from .code_hash import attach_code_hash, attach_code_hash_to_arg -from .dispatcher import DispatchContext, ApplyConfig, ApplyHistoryBest, ROOT_DISPATCH_CONTEXT, \ - dispatcher +from .dispatcher import DispatchContext, ApplyConfig, ApplyHistoryBest, dispatcher from .topi_integration import register_topi_compute, register_topi_schedule from .nnvm_integration import extract_from_graph diff --git a/python/tvm/autotvm/task/dispatcher.py b/python/tvm/autotvm/task/dispatcher.py index 0a96133ef085..beb4e4dcf204 100644 --- a/python/tvm/autotvm/task/dispatcher.py +++ b/python/tvm/autotvm/task/dispatcher.py @@ -242,6 +242,3 @@ def query(self, target, workload): return self._default raise RuntimeError( "Cannot find config for target=%s, workload=%s" % (target, workload)) - -ROOT_DISPATCH_CONTEXT = ApplyHistoryBest([]) -DispatchContext.current = ROOT_DISPATCH_CONTEXT diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index 4fb29485876e..70a3a511ec61 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -9,16 +9,26 @@ import os import json -from .task import ROOT_DISPATCH_CONTEXT +from .task import ApplyHistoryBest +from .. import target as _target from ..contrib.util import tempdir from ..contrib.download import download AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") -def load(target, extra_files=None): - """Load pre-tuned parameters to the root dispatch context. - The corresponding downloaded *.log files under TopHub root path will be loaded. +def _alias(name): + """convert alias for some packages""" + table = { + 'vtacpu': 'vta', + } + return table.get(name, name) + + +def context(target, extra_files=None): + """Return the dispatch context with pre-tuned parameters. + The corresponding downloaded *.log files under tophub root path will be loaded. + Users can also add their own files in argument `extra_files`. Parameters ---------- @@ -26,28 +36,28 @@ def load(target, extra_files=None): The compilation target extra_files: list of str, optional Extra log files to load - - Note - ---- - If the current dispatch context is not root dispatch context, - this function won't affect the current dispatch context. """ rootpath = AUTOTVM_TOPHUB_ROOT_PATH - root_context = ROOT_DISPATCH_CONTEXT + best_context = ApplyHistoryBest([]) + + if isinstance(target, str): + target = _target.create(target) big_target = str(target).split()[0] if os.path.isfile(os.path.join(rootpath, big_target + ".log")): - root_context.load(os.path.join(rootpath, big_target + ".log")) + best_context.load(os.path.join(rootpath, big_target + ".log")) for opt in target.options: if opt.startswith("-device"): - model = opt[8:] + model = _alias(opt[8:]) if os.path.isfile(os.path.join(rootpath, model) + ".log"): - root_context.load(os.path.join(rootpath, model) + ".log") + best_context.load(os.path.join(rootpath, model) + ".log") if extra_files: for filename in extra_files: - root_context.load(filename) + best_context.load(filename) + + return best_context def download_package(backend): @@ -68,6 +78,7 @@ def download_package(backend): if not os.path.isdir(path): os.mkdir(path) + backend = _alias(backend) logging.info("Download pre-tuned parameters for %s", backend) download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/%s.log" % backend, os.path.join(rootpath, backend + ".log"), True, verbose=0) @@ -82,9 +93,10 @@ def check_package(backend): backend: str The name of package """ + backend = _alias(backend) + if os.path.isfile(os.path.join(AUTOTVM_TOPHUB_ROOT_PATH, backend + ".log")): return - download_package(backend) diff --git a/python/tvm/autotvm/util.py b/python/tvm/autotvm/util.py index df58bde38558..99a2c85aa10e 100644 --- a/python/tvm/autotvm/util.py +++ b/python/tvm/autotvm/util.py @@ -8,6 +8,16 @@ from .. import expr, ir_pass + +class EmptyContext(object): + """An empty context""" + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def get_rank(values): """get rank of items diff --git a/python/tvm/target.py b/python/tvm/target.py index 439751db73bd..1238523d4ede 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -413,7 +413,7 @@ def opengl(options=None): def arm_cpu(model='unknown', options=None): """Returns a ARM CPU target. - This function will also donwload pre-tuned op parameters + This function will also download pre-tuned op parameters when there is none. Parameters ---------- @@ -444,6 +444,17 @@ def arm_cpu(model='unknown', options=None): return _api_internal._TargetCreate("llvm", *opts) +def rasp(options=None): + """Return a Raspberry 3b target. + + Parameters + ---------- + options : str or list of str + Additional options + """ + return arm_cpu('rasp3b', options) + + def create(target_str): """Get a target given target string. diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index 9e587b5368c0..124c98c65c7a 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -40,8 +40,8 @@ def get_ref_data(): np.testing.assert_allclose(b.asnumpy(), b_np, rtol=1e-5) def test_conv2d(): - autotvm.tophub.load(tvm.target.arm_cpu('rasp3b')) - verify_conv2d(1, 56, 64, 64, 3, 1, 1) + with autotvm.tophub.context(tvm.target.arm_cpu('rasp3b')): + verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": test_conv2d() diff --git a/vta/tests/python/integration/test_benchmark_topi_conv2d.py b/vta/tests/python/integration/test_benchmark_topi_conv2d.py index 9d60f04768a7..ca2451dec614 100644 --- a/vta/tests/python/integration/test_benchmark_topi_conv2d.py +++ b/vta/tests/python/integration/test_benchmark_topi_conv2d.py @@ -124,13 +124,13 @@ def _run(env, remote): key = "resnet-cfg[%d]" % i print("key=%s" % key) print(wl) - with tvm.target.arm_cpu("vtacpu"): + with tvm.target.create("llvm -device=vtacpu"): run_cpu_conv2d(env, remote, key, batch_size, wl) # load pre-tuned operator parameters for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu()) - - vta.testing.run(_run) + autotvm.tophub.check_package('vta') + with autotvm.tophub.context('llvm -device=vtacpu'): + vta.testing.run(_run) def test_vta_conv2d(): @@ -260,9 +260,6 @@ def _run(env, remote): print(wl) run_vta_conv2d(env, remote, key, batch_size, wl) - # load pre-tuned operator parameters for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu()) - vta.testing.run(_run) diff --git a/vta/tutorials/resnet.py b/vta/tutorials/resnet.py index 79baec0bf867..8d33a91d5691 100644 --- a/vta/tutorials/resnet.py +++ b/vta/tutorials/resnet.py @@ -83,9 +83,6 @@ def generate_graph(graph_fn, params_fn, device="vta"): elif env.TARGET == "pynq": target_host = "llvm -mtriple=armv7-none-linux-gnueabihf -mcpu=cortex-a9 -mattr=+neon" - # Load pre-tuned parameters of conv2d for ARM CPU - autotvm.tophub.load(tvm.target.arm_cpu('pynq')) - # Load the ResNet-18 graph and parameters sym = nnvm.graph.load_json(open(graph_fn).read()) params = nnvm.compiler.load_param_dict(open(params_fn, 'rb').read()) @@ -156,6 +153,9 @@ def generate_graph(graph_fn, params_fn, device="vta"): # Read in ImageNet Categories synset = eval(open(os.path.join(data_dir, categ_fn)).read()) +# Download pre-tuned op parameters of conv2d for ARM CPU used in VTA +autotvm.tophub.check_package('vta') + ###################################################################### # Setup the Pynq Board's RPC Server @@ -198,8 +198,8 @@ def generate_graph(graph_fn, params_fn, device="vta"): # ------------------------ # Build the ResNet graph runtime, and configure the parameters. -# Set ``device=cpu`` to run inference on the CPU, -# or ``device=vtacpu`` to run inference on the FPGA. +# Set ``device=vtacpu`` to run inference on the CPU +# or ``device=vta`` to run inference on the FPGA. device = "vta" # Device context From e6f6f0fd672f7f42e3d97c7430e94df73b043b85 Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 14:47:48 -0700 Subject: [PATCH 75/76] lint --- python/tvm/target.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/tvm/target.py b/python/tvm/target.py index 2920dee549fd..fed20c3914c6 100644 --- a/python/tvm/target.py +++ b/python/tvm/target.py @@ -454,7 +454,8 @@ def rasp(options=None): options : str or list of str Additional options """ - warnings.warn('tvm.target.rasp() is going to be deprecated. Please use tvm.target.arm_cpu("rasp3b")') + warnings.warn('tvm.target.rasp() is going to be deprecated. ' + 'Please use tvm.target.arm_cpu("rasp3b")') return arm_cpu('rasp3b', options) From a8dbf9c721e6540ba406aa0af621813ad378923b Mon Sep 17 00:00:00 2001 From: Mercy Date: Wed, 1 Aug 2018 15:17:23 -0700 Subject: [PATCH 76/76] use local session in tutorial --- tutorials/autotvm/tune_nnvm_arm.py | 32 +++++--- tutorials/cross_compilation_and_rpc.py | 88 +++++++++------------- tutorials/nnvm/deploy_model_on_mali_gpu.py | 55 +++++--------- tutorials/nnvm/deploy_model_on_rasp.py | 55 +++++--------- 4 files changed, 96 insertions(+), 134 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 2813117dc62b..23bd0f93ff23 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -205,7 +205,8 @@ def get_network(name, batch_size): # boards to the tracker). # If you have large time budget, you can set :code:`n_trial`, :code:`early_stopping` larger, # which makes the tuning run longer. -# If your device is very slow or a single conv2d operator in your network has large FLOPs, consider setting timeout larger. +# If your device is very slow or a single conv2d operator in your network has large FLOPs, +# consider setting timeout larger. # # **For android phone**, add :code:`build_func='ndk'` to the argument list of # :code:`autotvm.measure_option` to use Android NDK for creating shared library. @@ -340,11 +341,24 @@ def tune_and_evaluate(): # # .. code-block:: bash # -# [Task 1/16] Current/Best: 15.48/ 21.21 GFLOPS | Progress: (412/1000) | 531.53 s Done. -# [Task 2/16] Current/Best: 18.85/ 23.81 GFLOPS | Progress: (269/1000) | 261.59 s Done. -# [Task 3/16] Current/Best: 10.58/ 14.46 GFLOPS | Progress: (406/1000) | 317.72 s Done. -# [Task 4/16] Current/Best: 14.74/ 21.69 GFLOPS | Progress: (268/1000) | 246.84 s Done. -# [Task 5/16] Current/Best: 6.58/ 16.31 GFLOPS | Progress: (376/1000) | 301.62 s Done. -# [Task 6/16] Current/Best: 9.70/ 25.04 GFLOPS | Progress: (127/1000) | 154.13 s -# .... - +# [Task 1/16] Current/Best: 13.15/ 20.49 GFLOPS | Progress: (297/1000) | 348.51 s Done. +# [Task 2/16] Current/Best: 16.66/ 22.64 GFLOPS | Progress: (475/1000) | 415.42 s Done. +# [Task 3/16] Current/Best: 10.33/ 14.19 GFLOPS | Progress: (306/1000) | 239.61 s Done. +# [Task 4/16] Current/Best: 13.29/ 20.88 GFLOPS | Progress: (242/1000) | 227.48 s Done. +# [Task 5/16] Current/Best: 13.28/ 15.61 GFLOPS | Progress: (237/1000) | 191.56 s Done. +# [Task 6/16] Current/Best: 20.16/ 23.86 GFLOPS | Progress: (315/1000) | 304.31 s Done. +# [Task 7/16] Current/Best: 9.22/ 22.00 GFLOPS | Progress: (458/1000) | 433.26 s Done. +# [Task 8/16] Current/Best: 14.12/ 17.80 GFLOPS | Progress: (270/1000) | 240.73 s Done. +# [Task 9/16] Current/Best: 14.59/ 24.02 GFLOPS | Progress: (209/1000) | 213.61 s Done. +# [Task 10/16] Current/Best: 9.86/ 21.74 GFLOPS | Progress: (367/1000) | 359.93 s Done. +# [Task 11/16] Current/Best: 5.01/ 18.86 GFLOPS | Progress: (202/1000) | 191.18 s Done. +# [Task 12/16] Current/Best: 8.61/ 25.23 GFLOPS | Progress: (220/1000) | 220.74 s Done. +# [Task 13/16] Current/Best: 10.87/ 25.79 GFLOPS | Progress: (465/1000) | 902.14 s Done. +# [Task 14/16] Current/Best: 15.33/ 29.38 GFLOPS | Progress: (239/1000) | 481.33 s Done. +# [Task 15/16] Current/Best: 12.09/ 38.60 GFLOPS | Progress: (476/1000) | 928.35 s Done. +# [Task 16/16] Current/Best: 16.77/ 47.08 GFLOPS | Progress: (255/1000) | 439.91 s Done. +# Compile... +# Upload... +# Evaluate inference time cost... +# Mean inference time (std dev): 156.51 ms (0.89 ms) +# diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index 136942805501..a770a2758e01 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -27,8 +27,8 @@ # executed on the target device, e.g. Raspberry Pi. And we assume it # has Linux running. # -# Since we do compilaton on local machine, the remote device is only used -# for runing the generated code. We only need to build tvm runtime on +# Since we do compilation on local machine, the remote device is only used +# for running the generated code. We only need to build tvm runtime on # the remote device. # # .. code-block:: bash @@ -37,9 +37,9 @@ # cd tvm # make runtime -j2 # -# After building runtime successfully, we need to set environment varibles +# After building runtime successfully, we need to set environment variables # in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` -# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM +# using :code:`vi ~/.bashrc` and add the line below (Assuming your TVM # directory is in :code:`~/tvm`): # # .. code-block:: bash @@ -52,7 +52,7 @@ # Set Up RPC Server on Device # --------------------------- # To start an RPC server, run the following command on your remote device -# (Which is Raspberry Pi in our example). +# (Which is Raspberry Pi in this example). # # .. code-block:: bash # @@ -65,27 +65,6 @@ # # INFO:root:RPCServer: bind to 0.0.0.0:9090 # -# In our webpage building server (the machine that built this tutorial webpage), -# we do not have access to Raspberry Pi. -# So for local demonstration, we simply start a "fake" RPC server on the same machine. -# -# .. note:: -# -# If you have real remote device, you should change :code:`local_demo` to False, and -# set the host and port correctly. - -local_demo = True - -if local_demo: - # start a "fake" RPC server - from tvm import rpc - server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) - host = '0.0.0.0' - port = 9090 -else: - # The following is my environment, change this to your target device IP - host = '10.77.1.162' - port = 9090 ###################################################################### # Declare and Cross Compile Kernel on Local Machine @@ -97,8 +76,11 @@ # (with LLVM). # # Here we will declare a simple kernel on the local machine: -import tvm + import numpy as np + +import tvm +from tvm import rpc from tvm.contrib import util n = tvm.convert(1024) @@ -107,10 +89,12 @@ s = tvm.create_schedule(B.op) ###################################################################### -# Then we cross compile the kernel: +# Then we cross compile the kernel. # The target should be 'llvm -target=armv7l-linux-gnueabihf' for -# Raspberry Pi 3B, but we use 'llvm' for local demo. -# See the detailed note in the following block. +# Raspberry Pi 3B, but we use 'llvm' here to make this tutorial runnable +# on our webpage building server. See the detailed note in the following block. + +local_demo = True if local_demo: target = 'llvm' @@ -126,13 +110,12 @@ ###################################################################### # .. note:: # -# The argument :code:`target` in :code:`build` should be replaced -# with the true target triple of your device, which might be -# different for different device. For example, it is +# To run this tutorial with real remote device, change :code:`local_demo` +# to False and replace :code:`target` in :code:`build` with the true +# target triple of your device. The target triple which might be +# different for different devices. For example, it is # :code:`'llvm -target=armv7l-linux-gnueabihf'` for Raspberry Pi 3B and # :code:`'llvm -target=aarch64-linux-gnu'` for RK3399. -# Here we use :code:`'llvm'` directly to make the tutorial runable on our x86 -# server. # # Usually, you can query the target by execute :code:`gcc -v` on your # device, and look for the line starting with :code:`Target:` @@ -161,12 +144,16 @@ ###################################################################### # Run CPU Kernel Remotely by RPC # ------------------------------ -# We show how to run the generated cpu kernel on the remote device - -from tvm import rpc +# We show how to run the generated cpu kernel on the remote device. +# First we obtain an RPC session from remote device. -# connect the remote device -remote = rpc.connect(host, port) +if local_demo: + remote = rpc.LocalSession() +else: + # The following is my environment, change this to the IP address of your target device + host = '10.77.1.162' + port = 9090 + remote = rpc.connect(host, port) ###################################################################### # Upload the lib to the remote device, then invoke a device local @@ -206,9 +193,14 @@ # Firefly-RK3399. You may follow this `tutorial `_ # to setup the OS and OpenCL driver for RK3399. # -# Also we need to build the runtime with OpenCL enabled in rk3399 board. -# You need to modify :code:`set(USE_OPENCL OFF)` to :code:`set(USE_OPENCL_ON)` in -# :code:`tvm/config.cmake`, and execute :code:`make runtime -j4`. +# Also we need to build the runtime with OpenCL enabled on rk3399 board. In the tvm +# root directory, execute +# +# .. code-block:: bash +# +# cp cmake/config.cmake . +# sed -i "s/USE_OPENCL OFF/USE_OPENCL ON/" config.cmake +# make runtime -j4 # # The following function shows how we run OpenCL kernel remotely @@ -240,14 +232,7 @@ def run_opencl(): b = tvm.nd.array(np.zeros(1024, dtype=A.dtype), ctx) func(a, b) np.testing.assert_equal(b.asnumpy(), a.asnumpy() + 1) - -##################################################################### - -# Terminate the "fake" server after experiment -# You can delete this line if you started "real" rpc server on your remote device - -if local_demo: - server.terminate() + print("OpenCP test passed!") ###################################################################### # Summary @@ -259,4 +244,3 @@ def run_opencl(): # - Set up target device configuration to cross compile kernel on the # local machine. # - Upload and run the kernel remotely by RPC API. - diff --git a/tutorials/nnvm/deploy_model_on_mali_gpu.py b/tutorials/nnvm/deploy_model_on_mali_gpu.py index c20f946194e8..8aacb8433d3d 100644 --- a/tutorials/nnvm/deploy_model_on_mali_gpu.py +++ b/tutorials/nnvm/deploy_model_on_mali_gpu.py @@ -25,11 +25,11 @@ # .. note:: # # All instructions in both this section and next section should be -# executed on the target device, e.g. Raspberry Pi. And we assume it +# executed on the target device, e.g. Rk3399. And we assume it # has Linux running. # -# Since we do compilaton on local machine, the remote device is only used -# for runing the generated code. We only need to build tvm runtime on +# Since we do compilation on local machine, the remote device is only used +# for running the generated code. We only need to build tvm runtime on # the remote device. Make sure you have opencl driver in your board. # You can refer to `tutorial `_ # to setup OS and opencl driver for rk3399. @@ -57,7 +57,7 @@ # Set Up RPC Server on Device # --------------------------- # To start an RPC server, run the following command on your remote device -# (Which is Raspberry Pi in our example). +# (Which is RK3399 in our example). # # .. code-block:: bash # @@ -70,36 +70,15 @@ # # INFO:root:RPCServer: bind to 0.0.0.0:9090 # -# In our webpage building server (the machine that built this tutorial webpage), -# we do not have access to RK3399 board. -# -# So for local demonstration, we simply start a "fake" RPC server on the same machine. -# -# .. note:: -# -# If you have real remote device, you should change :code:`local_demo` to False, and -# set the host and port correctly. - -local_demo = True - -if local_demo: - # start a "fake" RPC server locally - host = 'localhost' - port = 9091 - server = rpc.Server(host=host, port=port, use_popen=True) -else: - # The following is my environment, change this to your target device IP - host = '10.77.1.145' - port = 9090 ###################################################################### -# Prepare the Pretrained Model -# ---------------------------- +# Prepare the Pre-trained Model +# ----------------------------- # Back to the host machine, which should have a full TVM installed (with LLVM). # # We will use pre-trained model from # `MXNet Gluon model zoo `_. -# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet` +# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet`. from mxnet.gluon.model_zoo.vision import get_model from mxnet.gluon.utils import download @@ -168,7 +147,10 @@ def transform_image(image): ###################################################################### # If we run the example on our x86 server for demonstration, we can simply # set it as :code:`llvm`. If running it on the RK3399, we need to -# specify its instruction set. +# specify its instruction set. Set :code:`local_demo` to False if you +# want to run this tutorial with a real device. + +local_demo = True if local_demo: target_host = "llvm" @@ -202,8 +184,14 @@ def transform_image(image): # With RPC, you can deploy the model remotely from your host machine # to the remote device. -# connect the server -remote = rpc.connect(host, port) +# obtain an RPC session from remote device. +if local_demo: + remote = rpc.LocalSession() +else: + # The following is my environment, change this to the IP address of your target device + host = '10.77.1.145' + port = 9090 + remote = rpc.connect(host, port) # upload the library to remote device and load it remote.upload(lib_fname) @@ -226,8 +214,3 @@ def transform_image(image): # get top1 result top1 = np.argmax(out.asnumpy()) print('TVM prediction top-1: {}'.format(synset[top1])) - -if local_demo: - # terminate the local server - server.terminate() - diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index d559de69da86..c11f202c1251 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -29,8 +29,8 @@ # executed on the target device, e.g. Raspberry Pi. And we assume it # has Linux running. # -# Since we do compilaton on local machine, the remote device is only used -# for runing the generated code. We only need to build tvm runtime on +# Since we do compilation on local machine, the remote device is only used +# for running the generated code. We only need to build tvm runtime on # the remote device. # # .. code-block:: bash @@ -67,37 +67,15 @@ # # INFO:root:RPCServer: bind to 0.0.0.0:9090 # -# In our webpage building server (the machine that built this tutorial webpage), -# we do not have access to Raspberry Pi. -# So for local demonstration, we simply start a "fake" RPC server on the same machine. -# -# .. note:: -# -# If you have real remote device, you should change :code:`local_demo` to False, and -# set the host and port correctly. - -local_demo = True - -if local_demo: - # start a "fake" RPC server locally - from tvm import rpc - server = rpc.Server(host='0.0.0.0', port=9090, use_popen=True) - host = '0.0.0.0' - port = 9090 -else: - # The following is my environment, change this to your target device IP - host = '10.77.1.162' - port = 9090 - ###################################################################### -# Prepare the Pretrained Model -# ---------------------------- +# Prepare the Pre-trained Model +# ----------------------------- # Back to the host machine, which should have a full TVM installed (with LLVM). # # We will use pre-trained model from # `MXNet Gluon model zoo `_. -# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet` +# You can found more details about this part at tutorial :ref:`tutorial-from-mxnet`. from mxnet.gluon.model_zoo.vision import get_model from mxnet.gluon.utils import download @@ -166,8 +144,10 @@ def transform_image(image): ###################################################################### # If we run the example on our x86 server for demonstration, we can simply # set it as :code:`llvm`. If running it on the Raspberry Pi, we need to -# specify its instruction set. We also need to add :code:`-device=arm_cpu` -# to the target string to enable optimizations for arm_cpu. +# specify its instruction set. Set :code:`local_demo` to False if you want +# to run this tutorial with a real device. + +local_demo = True if local_demo: target = tvm.target.create('llvm') @@ -195,14 +175,20 @@ def transform_image(image): # With RPC, you can deploy the model remotely from your host machine # to the remote device. -# connect the server -remote = rpc.connect(host, port) +# obtain an RPC session from remote device. +if local_demo: + remote = rpc.LocalSession() +else: + # The following is my environment, change this to the IP address of your target device + host = '10.77.1.162' + port = 9090 + remote = rpc.connect(host, port) # upload the library to remote device and load it remote.upload(lib_fname) rlib = remote.load_module('net.tar') -# upload the parameter +# upload the parameter (this may take a while) ctx = remote.cpu(0) rparams = {k: tvm.nd.array(v, ctx) for k, v in params.items()} @@ -219,8 +205,3 @@ def transform_image(image): # get top1 result top1 = np.argmax(out.asnumpy()) print('TVM prediction top-1: {}'.format(synset[top1])) - -if local_demo: - # terminate the local server - server.terminate() -

MpH}|PEN~hd-E$Qg`ef|goO1EpfA}AW<6^y> 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 literal 0 HcmV?d00001 diff --git a/python/tvm/autotvm/task/space.py b/python/tvm/autotvm/task/space.py index 06119d13d384..ea823c6f2760 100644 --- a/python/tvm/autotvm/task/space.py +++ b/python/tvm/autotvm/task/space.py @@ -21,6 +21,11 @@ Axis = namedtuple('Axis', ['space', 'index']) +try: + _long = long +except NameError: + _long = int + class InstantiationError(ValueError): """Actively detected error in instantiating a template with a config, @@ -103,7 +108,7 @@ def __init__(self, var, name=None): VirtualAxis.name_ct += 1 self.name = name - if isinstance(var, int): + if isinstance(var, (int, _long)): self.length = var elif isinstance(var, schedule.IterVar): self.name = var.var.name diff --git a/python/tvm/autotvm/task/task.py b/python/tvm/autotvm/task/task.py index 8fbb0ddd7aff..7a386f1f9e67 100644 --- a/python/tvm/autotvm/task/task.py +++ b/python/tvm/autotvm/task/task.py @@ -362,7 +362,7 @@ def traverse(ops): exp = body[0] ret += num_element * _count_flop(exp) - ret += traverse([sch[t].op for t in op.input_tensors]) + ret += traverse([t.op for t in op.input_tensors]) elif isinstance(op, tensor.PlaceholderOp): pass @@ -382,5 +382,4 @@ def traverse(ops): raise RuntimeError("Cannot find float number operation in this operator. " "Please use `cfg.add_flop` to manually set " "FLOP for this operator") - return ret diff --git a/python/tvm/contrib/util.py b/python/tvm/contrib/util.py index fe176dee2791..3e6f90234bcb 100644 --- a/python/tvm/contrib/util.py +++ b/python/tvm/contrib/util.py @@ -102,6 +102,7 @@ def filelock(path): return FileLock(path) +<<<<<<< HEAD def is_source_path(path): """Check if path is source code path. @@ -142,3 +143,35 @@ def which(exec_name): if os.path.isfile(full_path) and os.access(full_path, os.X_OK): return full_path return None + +def get_lower_ir(s): + """Get lower ir code of a schedule. + This is useful for debug, since you don't have to find all inputs/outputs + for a schedule in a fused subgraph. + + Parameters + ---------- + s: Schedule + + Returns + ------- + ir: str + The lower ir + """ + from .. import tensor + from ..build_module import lower + + outputs = s.outputs + + inputs = [] + def find_all(op): + if isinstance(op, tensor.PlaceholderOp): + inputs.append(op.output(0)) + else: + for x in op.input_tensors: + find_all(x.op) + + for out in outputs: + find_all(out) + + return lower(s, inputs, simple_mode=True) diff --git a/topi/python/topi/arm_cpu/conv2d.py b/topi/python/topi/arm_cpu/conv2d.py index ae35cf89b588..4a2ee6576e43 100644 --- a/topi/python/topi/arm_cpu/conv2d.py +++ b/topi/python/topi/arm_cpu/conv2d.py @@ -272,8 +272,6 @@ def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_ else: raise ValueError("Unsupported tile size for winograd: " + str(tile_size)) - - m = A_data.shape[1] r = 3 alpha = m + r - 1 @@ -337,6 +335,7 @@ def _decl_winograd(cfg, data, kernel, strides, padding, layout, out_dtype, tile_ attrs={'workload': _winograd_conv_arg_to_workload( data, kernel, strides, padding, layout, out_dtype, tile_size)}) + # we have to manually assign effective GFLOP for winogard cfg.add_flop(2 * N * K * H * W * KH * KW * C) return output @@ -360,13 +359,14 @@ def _schedule_winograd(cfg, s, output, last): s[G].compute_inline() eps, nu, k, c, kk, = s[U].op.axis r_kh, r_kw = s[U].op.reduce_axis - s[U].reorder(k, c, kk, eps, nu, r_kh, r_kw) + s[U].reorder(k, c, eps, nu, r_kh, r_kw, kk) s[U].unroll(eps) s[U].unroll(nu) s[U].unroll(r_kh) s[U].unroll(r_kw) + s[U].vectorize(kk) if autotvm.GLOBAL_SCOPE.in_tuning: - # kernel transformation will be pre-computed during compliation, so we skip + # kernel transformation will be pre-computed during compilation, so we skip # this part to make tuning records correct s[U].pragma(k, 'debug_skip_region') else: @@ -377,13 +377,14 @@ def _schedule_winograd(cfg, s, output, last): s[B].compute_inline() eps, nu, b, c, bb = s[V].op.axis r_eps, r_nu = s[V].op.reduce_axis - s[V].reorder(b, c, bb, eps, nu) + s[V].reorder(b, c, eps, nu, r_eps, r_nu, bb) s[V].unroll(eps) s[V].unroll(nu) s[V].unroll(r_eps) s[V].unroll(r_nu) + s[DD].compute_at(s[V], c) + s[V].vectorize(bb) s[V].parallel(b) - s[DD].compute_at(s[V], bb) # batch gemm eps, nu, k, b = s[M].op.axis @@ -416,7 +417,7 @@ def _schedule_winograd(cfg, s, output, last): s[last].parallel(co) MM = s.cache_read(M, 'global', [Y]) - m = get_const_int(A.shape[1]) + m = get_const_int(V.shape[0]) + 1 - 3 ho, wo, hi, wi = s[last].tile(h, w, m, m) s[Y].compute_at(s[last], wo) s[MM].compute_at(s[last], wo) @@ -424,6 +425,7 @@ def _schedule_winograd(cfg, s, output, last): if output != last: s[output].compute_inline() + def _winograd_conv_arg_to_workload(data, kernel, strides, padding, layout, out_dtype, tile_size): """convert argument to workload""" K = 3 diff --git a/tutorials/cross_compilation_and_rpc.py b/tutorials/cross_compilation_and_rpc.py index d50caf102754..3f5e68baf4e6 100644 --- a/tutorials/cross_compilation_and_rpc.py +++ b/tutorials/cross_compilation_and_rpc.py @@ -16,8 +16,6 @@ """ ###################################################################### -# .. _build-tvm-runtime-on-device: -# # Build TVM Runtime on Device # --------------------------- # @@ -36,9 +34,7 @@ # .. code-block:: bash # # git clone --recursive https://github.com/dmlc/tvm -# cd tvm -# cp cmake/config.cmake . -# make runtime -j4 +# make runtime -j2 # # After building runtime successfully, we need to set environment varibles # in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` diff --git a/tutorials/nnvm/deploy_model_on_mali_gpu.py b/tutorials/nnvm/deploy_model_on_mali_gpu.py index d5351aa24d18..c20f946194e8 100644 --- a/tutorials/nnvm/deploy_model_on_mali_gpu.py +++ b/tutorials/nnvm/deploy_model_on_mali_gpu.py @@ -40,7 +40,7 @@ # cd tvm # cp cmake/config.cmake . # sed -i "s/USE_OPENCL OFF/USE_OPENCL ON/" config.cmake -# make runtime +# make runtime -j4 # # After building runtime successfully, we need to set environment varibles # in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` diff --git a/tutorials/nnvm/deploy_model_on_rasp.py b/tutorials/nnvm/deploy_model_on_rasp.py index 35eb16469a22..0d48c0870f13 100644 --- a/tutorials/nnvm/deploy_model_on_rasp.py +++ b/tutorials/nnvm/deploy_model_on_rasp.py @@ -16,6 +16,8 @@ from tvm.contrib import util, graph_runtime as runtime ###################################################################### +# .. _build-tvm-runtime-on-device: +# # Build TVM Runtime on Device # --------------------------- # @@ -34,9 +36,7 @@ # .. code-block:: bash # # git clone --recursive https://github.com/dmlc/tvm -# cd tvm -# cp cmake/config.cmake . -# make runtime +# make runtime -j4 # # After building runtime successfully, we need to set environment varibles # in :code:`~/.bashrc` file. We can edit :code:`~/.bashrc` From f43d4faf3113894791776afa201725f133875c57 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Fri, 27 Jul 2018 13:50:53 -0700 Subject: [PATCH 29/76] fix bitserial --- topi/python/topi/arm_cpu/__init__.py | 1 + topi/python/topi/{rasp => arm_cpu}/bitserial_conv2d.py | 6 +++--- topi/python/topi/rasp/__init__.py | 7 ------- topi/tests/python/test_topi_bitserial_conv2d_rasp.py | 2 +- topi/tests/python/test_topi_conv2d.py | 10 +++++++--- 5 files changed, 12 insertions(+), 14 deletions(-) rename topi/python/topi/{rasp => arm_cpu}/bitserial_conv2d.py (99%) delete mode 100644 topi/python/topi/rasp/__init__.py diff --git a/topi/python/topi/arm_cpu/__init__.py b/topi/python/topi/arm_cpu/__init__.py index c42917f8e0e8..bb79769c1adc 100644 --- a/topi/python/topi/arm_cpu/__init__.py +++ b/topi/python/topi/arm_cpu/__init__.py @@ -2,3 +2,4 @@ from . import conv2d from . import depthwise_conv2d +from . import bitserial_conv2d diff --git a/topi/python/topi/rasp/bitserial_conv2d.py b/topi/python/topi/arm_cpu/bitserial_conv2d.py similarity index 99% rename from topi/python/topi/rasp/bitserial_conv2d.py rename to topi/python/topi/arm_cpu/bitserial_conv2d.py index 7d292db8d298..470aea0b4523 100644 --- a/topi/python/topi/rasp/bitserial_conv2d.py +++ b/topi/python/topi/arm_cpu/bitserial_conv2d.py @@ -43,7 +43,7 @@ SpatialPackNCHW(1, 1, 8, 1, 16), ] -@_get_schedule.register("rasp") +@_get_schedule.register("arm_cpu") def _get_schedule_bitserial_conv2d(wkl, layout): if wkl not in _WORKLOADS: raise ValueError("no schedule for such workload: {}".format(wkl)) @@ -55,7 +55,7 @@ def _get_schedule_bitserial_conv2d(wkl, layout): return sch -@bitserial_conv2d.register("rasp") +@bitserial_conv2d.register("arm_cpu") def _declaration_bitserial_conv2d(data, kernel, stride, padding, activation_bits, weight_bits, layout='NCHW', pack_dtype=None, out_dtype=None, dorefa=False): if out_dtype is None: @@ -323,7 +323,7 @@ def _schedule_spatial_conv2d_nhwc(s, data, data_q, data_pad, data_vec, s = s.normalize() return s -@generic.schedule_bitserial_conv2d_nhwc.register(["rasp"]) +@generic.schedule_bitserial_conv2d_nhwc.register(["arm_cpu"]) def schedule_bitserial_conv2d_nhwc(outs): """Raspverry pi schedule for bitserial conv2d""" s = tvm.create_schedule([x.op for x in outs]) diff --git a/topi/python/topi/rasp/__init__.py b/topi/python/topi/rasp/__init__.py deleted file mode 100644 index 270a48504468..000000000000 --- a/topi/python/topi/rasp/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# pylint: disable=redefined-builtin, wildcard-import -"""Raspberry pi specific declaration and schedules.""" -from __future__ import absolute_import as _abs - -from .conv2d import schedule_conv2d_nchw -from .depthwise_conv2d import schedule_depthwise_conv2d_nchw -from .bitserial_conv2d import schedule_bitserial_conv2d_nhwc diff --git a/topi/tests/python/test_topi_bitserial_conv2d_rasp.py b/topi/tests/python/test_topi_bitserial_conv2d_rasp.py index 5789c5496205..3de954abc291 100644 --- a/topi/tests/python/test_topi_bitserial_conv2d_rasp.py +++ b/topi/tests/python/test_topi_bitserial_conv2d_rasp.py @@ -22,7 +22,7 @@ def verify_bitserial_conv2d_nhwc(batch, in_size, in_channel, num_filter, kernel, input_type='uint32' out_dtype='int32' - with tvm.target.rasp(): + with tvm.target.arm_cpu('rasp3b'): A = tvm.placeholder((batch, in_height, in_width, in_channel), dtype=input_type, name='A') W = tvm.placeholder((kernel, kernel, in_channel, num_filter), dtype=input_type, name='W') B = topi.nn.bitserial_conv2d(A, W, stride, padding, activation_bits, weight_bits, out_dtype=out_dtype, diff --git a/topi/tests/python/test_topi_conv2d.py b/topi/tests/python/test_topi_conv2d.py index e7ea956eea78..4d284e5eccbf 100644 --- a/topi/tests/python/test_topi_conv2d.py +++ b/topi/tests/python/test_topi_conv2d.py @@ -2,6 +2,7 @@ import os import numpy as np import tvm +from tvm import autotvm import topi import topi.testing from tvm.contrib.pickle_memoize import memoize @@ -11,10 +12,13 @@ def verify_conv2d(batch, in_size, in_channel, num_filter, kernel, stride, padding): in_height = in_width = in_size - with tvm.target.rasp(): + # load pre-tuned parameters + autotvm.tophub.load_context(tvm.target.arm_cpu()) + + with tvm.target.arm_cpu(): A = tvm.placeholder((batch, in_channel, in_height, in_width), name='A') W = tvm.placeholder((num_filter, in_channel, kernel, kernel), name='W') - B = topi.nn.conv2d(A, W, stride, padding) + B = topi.nn.conv2d(A, W, (stride, stride), (padding, padding), 'NCHW', 'float32') s = topi.generic.schedule_conv2d_nchw([B]) a_shape = get_const_tuple(A.shape) @@ -39,7 +43,7 @@ def get_ref_data(): np.testing.assert_allclose(b.asnumpy(), b_np, rtol=1e-5) def test_conv2d(): - verify_conv2d(1, 56, 64, 64, 3, 1, 1) + verify_conv2d(1, 56, 64, 64, 3, 1, 1) if __name__ == "__main__": test_conv2d() From 7b3a71a26800b19962e23ad7e44b2f6c0af777b1 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Fri, 27 Jul 2018 16:32:01 -0700 Subject: [PATCH 30/76] refactor measure --- python/tvm/autotvm/measure/local_executor.py | 6 +- python/tvm/autotvm/measure/measure.py | 136 +++++++++--------- python/tvm/autotvm/measure/measure_methods.py | 117 ++++++++++----- python/tvm/autotvm/tuner/graph_tuning.py | 50 ++----- tests/python/integration/test_tuning.py | 12 +- .../python/unittest/test_autotvm_database.py | 9 +- tutorials/autotvm/tune_conv2d_cuda.py | 6 +- tutorials/autotvm/tune_nnvm_arm.py | 24 ++-- tutorials/autotvm/tune_simple_template.py | 2 +- 9 files changed, 197 insertions(+), 165 deletions(-) diff --git a/python/tvm/autotvm/measure/local_executor.py b/python/tvm/autotvm/measure/local_executor.py index 2ab09dbb18c0..a9e90d594ab2 100644 --- a/python/tvm/autotvm/measure/local_executor.py +++ b/python/tvm/autotvm/measure/local_executor.py @@ -117,11 +117,11 @@ def submit(self, func, *args, **kwargs): ---------- By default, the executor will fork a new process for a new job But some runtime does not support fork (e.g. cuda runtime, cudnn). - In this circumstance, you should set 'fork_new_process' to False in kwargs + In this circumstance, you should set 'do_fork' to False in kwargs. """ - fork_new_process = kwargs.pop('fork_new_process', True) + do_fork = kwargs.pop('do_fork', True) - if not fork_new_process: + if not do_fork: return LocalFutureNoFork(func(*args, **kwargs)) queue = Queue(1) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 7442136925e4..8cabfd870f68 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -55,37 +55,37 @@ class MeasureErrorNo(object): FLEET_ERROR = 6 # error of measure infrastructure -def measure_option(mode, +def measure_option(measure_func='local', number=1, repeat=1, timeout=60, parallel_num=1, + do_fork=True, pack_size=1, check_correctness=False, - build_option=None, - replay_db=None, - save_to_replay_db=True, + rpc_device_key=None, rpc_priority=1, rpc_timeout=60, rpc_tracker_addr=None, - use_ndk=False, - custom_measure_batch=None): + + build_func='default', + + replay_db=None, + save_to_replay_db=True): """Configure how to do measurement Parameters ---------- - mode: str - 'local': use the local device for measurement. In this mode, - the tuner starts a tracker and a RPC server silently for the user. - - 'rpc': request devices for measurement from rpc tracker. In this mode, - you should start a rpc tracker in a separate processing. + measure_func: str or callable + 'local': use the local device for measurement. The tuner will start a tracker + and a RPC server silently for the user. - 'custom': use custom measure function + 'rpc': request devices for measurement from the rpc tracker. In this mode, + you should start a rpc tracker in a separate processing and register your + device to the tracker. - 'local-nofork': use local device for measure but does not use multiprocessing. - This mode is suitable for debug, but does not support timeout and parallel. + callable: It is a customized function for measurement. number : int, optional Number of times to do the measurement for average @@ -101,20 +101,29 @@ def measure_option(mode, The number of measurement task that can run in parallel. Set this according to the number of cpu cores (for compilation) and the number of devices you have (for measuring generate code). + do_fork: bool, optional + Whether use multiprocessing (based on fork) for parallel measure jobs. + Set this to False if you want to debug or fork is not suitable for you case. + NOTE: If this is False, parallel and timeout do not work. pack_size : int, optional Number of configs to measure in one RPC call. Usually this can be set to 1. If your device has high cost to establish a rpc connection, set this higher. check_correctness: bool - Whether check correctness after measurement. - build_option: Dict, optional - Build options for tvm.build_config + Whether check correctness after measurement. This will use llvm cpu as reference. replay_db : Database, optional The database that we retrieve saved MeasureResults from save_to_replay_db: bool, optional Whether save measure result to database. This is useless when replay_db is None + build_func: str or callable, optional + 'default': call default builder. This works for normal target (llvm, cuda) + + 'ndk': use Android NDK to create shared library. Use this for android target. + + callable; customized build function for other backends (e.g. VTA) + rpc_priority: int, optional Priority of this task, used by scheduler in tracker rpc_device_key: str, optional @@ -126,38 +135,31 @@ def measure_option(mode, If is set, will use this address. If is not set, will use environment variable "TVM_TRACKER_HOST" and "TVM_TRACKER_PORT" - use_ndk: bool, option - Whether use Android NDK. Set this to true for Android target. - custom_measure_batch: callable, optional - custom measure function - Returns ------- options: dict A dict to store all options """ return { - 'mode': mode, + 'measure_func': measure_func, 'number': number, 'repeat': repeat, 'timeout': timeout, 'parallel_num': parallel_num, + 'do_fork': do_fork, 'pack_size': pack_size, 'check_correctness': check_correctness, - 'build_option': build_option, - - 'replay_db': replay_db, - 'save_to_replay_db': save_to_replay_db, 'rpc_device_key': rpc_device_key, 'rpc_priority': rpc_priority, 'rpc_timeout': rpc_timeout, 'rpc_tracker_addr': rpc_tracker_addr, - 'use_ndk': use_ndk, - 'custom_measure_batch': custom_measure_batch - } + 'build_func': build_func, + 'replay_db': replay_db, + 'save_to_replay_db': save_to_replay_db, + } def create_measure_batch(task, options): """Get a standard measure_batch function. @@ -178,51 +180,47 @@ def create_measure_batch(task, options): from . import measure_methods from ..database import filter_inputs - mode = options['mode'] + measure_func = options['measure_func'] number, repeat = options['number'], options['repeat'] - timeout, parallel_num = options['timeout'], options['parallel_num'] + timeout, parallel_num, do_fork = options['timeout'], options['parallel_num'], options['do_fork'] pack_size = options['pack_size'] check_correctness = options['check_correctness'] - build_option = options['build_option'] - replay_db = options['replay_db'] - save_to_replay_db = options['save_to_replay_db'] rpc_device_key = options['rpc_device_key'] rpc_priority, rpc_timeout = options['rpc_priority'], options['rpc_timeout'] - use_ndk = options['use_ndk'] - custom_measure_batch = options['custom_measure_batch'] + build_func = options['build_func'] + replay_db = options['replay_db'] + save_to_replay_db = options['save_to_replay_db'] kwargs = {} executor = LocalExecutor(timeout=timeout) - if mode == 'local': - # start temporary rpc tracker and rpc server for the user - tracker = Tracker('localhost', port=9000, port_end=10000, - silent=True) - rpc_device_key = '$local$device$%d' % tracker.port - server = Server('localhost', port=9000, port_end=10000, - key=rpc_device_key, - use_popen=True, silent=True, - tracker_addr=(tracker.host, tracker.port)) - - fmeasure = measure_methods.measure_rpc - kwargs['rpc_device_key'] = rpc_device_key - kwargs['rpc_tracker_addr'] = (tracker.host, tracker.port) - kwargs['rpc_timeout'] = timeout - elif mode == 'rpc': + if measure_func == 'local': + if do_fork: + # start temporary rpc tracker and rpc server for the user + tracker = Tracker('localhost', port=9000, port_end=10000, + silent=True) + rpc_device_key = '$local$device$%d' % tracker.port + server = Server('localhost', port=9000, port_end=10000, + key=rpc_device_key, + use_popen=True, silent=True, + tracker_addr=(tracker.host, tracker.port)) + + fmeasure = measure_methods.measure_rpc + kwargs['rpc_device_key'] = rpc_device_key + kwargs['rpc_tracker_addr'] = (tracker.host, tracker.port) + kwargs['rpc_timeout'] = timeout + else: + fmeasure = measure_methods.measure_local + elif measure_func == 'rpc': fmeasure = measure_methods.measure_rpc kwargs['rpc_device_key'] = rpc_device_key kwargs['rpc_priority'] = rpc_priority kwargs['rpc_timeout'] = rpc_timeout - kwargs['use_ndk'] = use_ndk assert rpc_device_key, "In rpc mode, a rpc_device_key must be provided" - elif mode == "custom": - assert callable(custom_measure_batch), "In custom mode, custom_measure_func " \ - "must be a callable object" - elif mode == 'local-nofork': - fmeasure = measure_methods.measure_local - kwargs['fork_new_process'] = False else: - raise RuntimeError("Invalid mode: " + mode) + assert callable(measure_func), "In custom mode, custom_measure_func " \ + "must be a callable object" + fmeasure = measure_func if 'cuda' in task.target.keys and 'rpc_device_key' in kwargs: # query cuda device info add_cuda_device_info(kwargs['rpc_device_key'], kwargs.get('rpc_tracker_addr'), kwargs) @@ -230,7 +228,7 @@ def create_measure_batch(task, options): add_opencl_device_info(kwargs['rpc_device_key'], kwargs.get('rpc_tracker_addr'), kwargs) if check_correctness: - # use llvm to generate a reference input/output + # use llvm cpu to generate a reference input/output # this option works for tuning topi, but might not work for you custom op with _target.create("llvm"): s, arg_bufs = task.instantiate(task.config_space.get(0)) @@ -257,10 +255,12 @@ def measure_batch(measure_inputs): futures = [] for input_pack in input_packs: future = executor.submit( - fmeasure, input_pack, + fmeasure, + input_pack, number=number, repeat=repeat, - build_option=build_option, + do_fork=do_fork, + build_func=build_func, **kwargs ) futures.append(future) @@ -270,7 +270,7 @@ def measure_batch(measure_inputs): for future in futures: result = future.get() if isinstance(result, Exception): - if mode == 'local-nofork': + if measure_func == 'local' and not do_fork: # debug usage, raise exception raise result tstamp = time.time() @@ -292,11 +292,9 @@ def measure_batch(measure_inputs): return partial_results return results - if mode == 'custom': - measure_batch = custom_measure_batch - measure_batch.parallel_num = parallel_num - if mode == 'local': + if measure_func == 'local' and do_fork: + # attach server and tracker object to avoid them of being garbage-collected measure_batch.aux_objects = {"server": server, "tracker": tracker} return measure_batch diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index 4529fe6ca8c1..f13a3b9a00cb 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -12,7 +12,7 @@ import numpy as np -from ...contrib import ndk, nvcc, util +from ...contrib import nvcc, util from ... import rpc, ir_pass, build, build_config, nd, context, TVMError, register_func from ..util import get_const_tuple @@ -58,12 +58,15 @@ def request_remote(device_key, tracker_addr=None, priority=1, timeout=60): return remote -def _measure_generic(fbuild, input_pack, ref_input, ref_output): - """Generic measurement function +def _measure_pack(fbuild, input_pack, ref_input, ref_output): + """Do measure for a pack of inputs. + This function mainly does error handling and correctness check. + + (Note: A pack is a list of inputs which will be measured inside a same RPC session) Parameters ---------- - fbuild : function takes MeasureInput returns tuple of (time_func, ctx) + fbuild : function takes MeasureInput returns tuple of (time_func, ctx, args) The build function used to build each input. input_pack : list of MeasureInput The inputs we need to evaluate @@ -135,10 +138,34 @@ def _measure_generic(fbuild, input_pack, ref_input, ref_output): res_pack.append(MeasureResult(costs, errno, tstamp - tic, tstamp)) return res_pack -def _build_func(inp, build_option, kwargs): - """Build function module. Exception will be raised when error occurs""" + +def default_build_func(inp, tmp_dir=None, **kwargs): + """Build function module. Exception will be raised when any error occurs + + Parameters + ---------- + inp: MeasureInput + The input of this measurement + tmp_dir: tvm.contrib.util.TempDirectory, optional + The temporary directory for output built binary library. + In RPC mode, we will upload this library to remote device + kwargs: Dict + Other arguments + + Returns + ------- + func: Function + TVM built function. Typically this is the return value of tvm.build. + args: Array of Buffer or Tensor + The argument list for the function. Typically this is the second argument of tvm.build. + filename: str + The filename of the output build library + """ + # build function with inp.target: s, args = inp.task.instantiate(inp.config) + + # check invalidity of template and code hash consistency if not inp.config.valid(): raise InstantiationError(inp.config.errors) code_hash = getattr(s, 'code_hash', None) @@ -146,7 +173,8 @@ def _build_func(inp, build_option, kwargs): raise HashMismatchError('got {0:s}, expected {1:s}' .format(str(inp.config.code_hash), str(code_hash))) - opts = build_option or {} + opts = {} + if "check_gpu" in kwargs: values = kwargs['check_gpu'] # Add gpu verify pass to filter out invalid configs in advance. @@ -155,24 +183,33 @@ def _build_func(inp, build_option, kwargs): 'max_thread_x', 'max_thread_y', 'max_thread_z'] opts["add_lower_pass"] = [ (2, gpu_verify_pass(**{key: values[key] for key in check_keys}))] - if 'cuda_arch' in kwargs: set_cuda_target_arch(kwargs['cuda_arch']) with build_config(**opts): func = build(s, args, target_host=inp.task.target_host) - return func, args + # export library to temp directory + if tmp_dir: + if kwargs.get('use_ndk', False): # for Android NDK + from ...contrib import ndk + filename = "tmp_func_%0x.so" % getrandbits(64) + func.export_library(tmp_dir.relpath(filename), ndk.create_shared) + else: + filename = "tmp_func_%0x.tar" % getrandbits(64) + func.export_library(tmp_dir.relpath(filename)) + + return func, args, filename def measure_rpc(input_pack, rpc_device_key, number, repeat=1, - build_option=None, rpc_tracker_addr=None, rpc_priority=1, rpc_timeout=60, + build_func='default', **kwargs): """Measure the time cost on a device by rpc @@ -186,8 +223,6 @@ def measure_rpc(input_pack, Number of times to get the running measurement repeat : int, optional How many times we want to repeat the measurement. - build_option: Dict - build options for tvm.build_config rpc_tracker_addr: Tuple(string, int), optional The address of rpc tracker in (host, port) format @@ -197,6 +232,13 @@ def measure_rpc(input_pack, rpc_timeout: int, optional timeout of the rpc session + build_func: str or callable, optional + 'default': call default build_func. This works for normal target (llvm, cuda) + + 'ndk': use Android NDK to create shared library. Use this for android target + + callable; customized build function for other backends (e.g. VTA) + kwargs: dict, optional Additional key word arguments @@ -207,34 +249,32 @@ def measure_rpc(input_pack, """ def _fbuild(inp): """ Local build function.""" - func, args = _build_func(inp, build_option, kwargs) - tmp_dir = util.tempdir() - if kwargs.get('use_ndk', False): # for Android NDK - file_name = "tmp_func_%0x.so" % getrandbits(64) - path = tmp_dir.relpath(file_name) - func.export_library(path, ndk.create_shared) + + if build_func == 'default': + func, args, filename = default_build_func(inp, tmp_dir, **kwargs) + elif build_func == 'ndk': + kwargs['use_ndk'] = True + func, args, filename = default_build_func(inp, tmp_dir, **kwargs) else: - file_name = "tmp_func_%0x.tar" % getrandbits(64) - path = tmp_dir.relpath(file_name) - func.export_library(path) + func, args, filename = build_func(inp, tmp_dir, **kwargs) + remote = request_remote(rpc_device_key, rpc_tracker_addr, rpc_priority, rpc_timeout) - remote.upload(path) - func = remote.load_module(file_name) + remote.upload(tmp_dir.relpath(filename)) + func = remote.load_module(filename) ctx = remote.context(str(inp.target), 0) time_f = func.time_evaluator( func.entry_name, ctx, number=number, repeat=repeat) return time_f, ctx, args - ret = _measure_generic(_fbuild, input_pack, - kwargs.get("ref_input", None), kwargs.get("ref_output", None)) + ret = _measure_pack(_fbuild, input_pack, + kwargs.get("ref_input", None), kwargs.get("ref_output", None)) return ret - def measure_local(input_pack, number, repeat=1, - build_option=None, + build_func='default', **kwargs): """Measure the time cost on a local machine. @@ -246,8 +286,14 @@ def measure_local(input_pack, Number of times to get the running measurement repeat : int, optional How many times we want to repeat the measurement. - build_option: dict, optional - Build options for tvm.build_config + + build_func: str or callable, optional + 'default': call default build_func. This works for normal target (llvm, cuda) + + 'ndk': use Android NDK to create shared library. Use this for android target + + callable; customized build function for other backends (e.g. VTA) + kwargs: dict, optional Additional key word arguments @@ -256,17 +302,22 @@ def measure_local(input_pack, res_pack : Array of MeasureResult The list of execution results of measurement. """ - def _fbuild(inp): """ Local build function """ - func, args = _build_func(inp, build_option, kwargs) + tmp_dir = util.tempdir() + + if build_func == 'default': + func, args, filename = default_build_func(inp, tmp_dir, **kwargs) + else: + func, args, filename = build_func(inp, tmp_dir, **kwargs) + ctx = context(str(inp.target), 0) time_f = func.time_evaluator( func.entry_name, ctx, number=number, repeat=repeat) return time_f, ctx, args - ret = _measure_generic(_fbuild, input_pack, - kwargs.get("ref_input", None), kwargs.get("ref_output", None)) + ret = _measure_pack(_fbuild, input_pack, + kwargs.get("ref_input", None), kwargs.get("ref_output", None)) return ret diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py index 4cebdc8d12af..0ac36bb75698 100644 --- a/python/tvm/autotvm/tuner/graph_tuning.py +++ b/python/tvm/autotvm/tuner/graph_tuning.py @@ -5,19 +5,13 @@ from .. import measure, record, task def tune_tasks(tasks, - rpc_device_key, - - tuner='ga', + measure_option, + tuner='xgb', n_trial=500, early_stopping=200, log_filename='tuning.log', - - mea_number=5, - mea_parallel_num=1, - mea_timeout=20, - mea_use_ndk=False, - - use_transfer_learning=True): + use_transfer_learning=True, + try_winograd=True): """ Tune a set of tasks @@ -40,26 +34,20 @@ def tune_tasks(tasks, config after `early_stopping` trials log_filename: str The filename of output log file to store best configs - mea_number: int - The number of runs for taking average for one measurement. - mea_parallel_num: int - The parallel number in measurement. Set this to the number of devices you have. - mea_timeout: int - The timeout of a measurement. - mea_use_ndk: bool - Whether use Android NDK. The this to true if your target is android system use_transfer_learning: bool Whether reuse history tuning log to accelerate tuning + try_winograd: bool + Whether try to use winograd template """ - - for i in range(len(tasks)): # pylint:disable=consider-using-enumerate - try: # try winograd template - tsk = task.create(tasks[i].name, tasks[i].args, - tasks[i].target, tasks[i].target_host, - 'winograd') - tasks.append(tsk) - except Exception: # pylint:disable=broad-except - pass + if try_winograd: + for i in range(len(tasks)): # pylint:disable=consider-using-enumerate + try: # try winograd template + tsk = task.create(tasks[i].name, tasks[i].args, + tasks[i].target, tasks[i].target_host, + 'winograd') + tasks.append(tsk) + except Exception: # pylint:disable=broad-except + pass tmp_log_file = log_filename + ".tmp" if os.path.exists(tmp_log_file): @@ -68,14 +56,6 @@ def tune_tasks(tasks, for i, tsk in enumerate(tasks): prefix = "[Task %2d/%2d] " %(i+1, len(tasks)) - measure_option = measure.measure_option(mode='rpc', - repeat=3, - number=mea_number, - rpc_device_key=rpc_device_key, - parallel_num=mea_parallel_num, - timeout=mea_timeout, - use_ndk=mea_use_ndk) - # create tuner if tuner == 'xgb' or tuner == 'xgb-rank': tuner_obj = XGBTuner(tsk, loss_type='rank', verbose=0) diff --git a/tests/python/integration/test_tuning.py b/tests/python/integration/test_tuning.py index 23816b2b6f78..82b020724c0b 100644 --- a/tests/python/integration/test_tuning.py +++ b/tests/python/integration/test_tuning.py @@ -108,19 +108,18 @@ def test_task_tuner_without_measurement(): """test task and tuner without measurement""" task, target = get_sample_task() - def measure_batch(inputs): + def custom_measure(input_pack, **kwargs): from tvm.autotvm import MeasureResult results = [] - for inp in inputs: + for inp in input_pack: tic = time.time() # do nothing time.sleep(0.001) results.append(MeasureResult([time.time() - tic], 0, time.time() - tic, time.time())) return results - measure_option = autotvm.measure_option(mode='custom', - custom_measure_batch=measure_batch) + measure_option = autotvm.measure_option(custom_measure) logging.info("%s", task.config_space) @@ -140,7 +139,7 @@ def check(target, target_host): task, target = get_sample_task(target, target_host) logging.info("%s", task.config_space) - measure_option = autotvm.measure_option(mode='local', + measure_option = autotvm.measure_option('local', timeout=4, number=2) @@ -152,7 +151,8 @@ def check(target, target_host): if __name__ == "__main__": # only print log when invoked from main - logging.basicConfig(level=logging.INFO) + logging.basicConfig(level=logging.DEBUG) test_task_tuner_without_measurement() test_tuning_with_measure() + diff --git a/tests/python/unittest/test_autotvm_database.py b/tests/python/unittest/test_autotvm_database.py index 72f0e082ca91..be54ae1f1732 100644 --- a/tests/python/unittest/test_autotvm_database.py +++ b/tests/python/unittest/test_autotvm_database.py @@ -47,7 +47,7 @@ def test_db_filter(): batch_size = 2 - measure_option = autotvm.measure_option(mode='local-nofork', timeout=2) + measure_option = autotvm.measure_option('local', do_fork=False, timeout=2) measure_batch = autotvm.measure.create_measure_batch(task, measure_option) ct = 0 @@ -72,7 +72,7 @@ def test_db_filter(): db.flush() # First setting, memoize one input at a time, check that each is saved and replayed - measure_option = autotvm.measure_option(mode='local-nofork', timeout=2, replay_db=db) + measure_option = autotvm.measure_option('local', do_fork=False, timeout=2, replay_db=db) measure_batch = autotvm.measure.create_measure_batch(task, measure_option) for i in range(len(all_inputs)+1): @@ -160,7 +160,8 @@ def test_db_save_replay(): if not ctx.exist: logging.warning("Skip this test because there is no supported device for test") - measure_option = autotvm.measure_option(mode='local-nofork', + measure_option = autotvm.measure_option('local', + do_fork=False, timeout=2, replay_db=_db, save_to_replay_db=True) measure_batch = autotvm.measure.create_measure_batch(task, measure_option) @@ -207,7 +208,7 @@ def test_check_hashmismatch(): if not ctx.exist: logging.warning("Skip this test because there is no supported device for test") - measure_option = autotvm.measure_option(mode='local-nofork') + measure_option = autotvm.measure_option('local', do_fork=False) measure_batch = autotvm.measure.create_measure_batch(task, measure_option) inputs = list() diff --git a/tutorials/autotvm/tune_conv2d_cuda.py b/tutorials/autotvm/tune_conv2d_cuda.py index c08881345029..0c7b528a50b2 100644 --- a/tutorials/autotvm/tune_conv2d_cuda.py +++ b/tutorials/autotvm/tune_conv2d_cuda.py @@ -144,14 +144,14 @@ def conv2d_no_batching(N, H, W, CI, CO, KH, KW, stride, padding): # use local gpu, measure 5 times for every config to reduce variance # run 8 parallel threads for compilation -measure_option = autotvm.measure_option(mode='local', +measure_option = autotvm.measure_option('local', number=10, parallel_num=8, timeout=20) -# begin tuning, log records to file `conv2d.tsv` +# begin tuning, log records to file `conv2d.log` tuner = autotvm.tuner.XGBTuner(task) -tuner.tune(n_trial=20, +tuner.tune(n_trial=200, measure_option=measure_option, callbacks=[autotvm.callback.log_to_file('conv2d.log')]) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index a4702ef8d6a3..be44e4f482e9 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -167,15 +167,16 @@ def get_network(name, batch_size): tuning_option = { 'log_filename': log_file, - 'rpc_device_key': device_key, 'tuner':'xgb', 'n_trial': 1000, 'early_stopping': 200, - 'mea_number': 4, - 'mea_parallel_num': 1, - 'mea_timeout': 10, + 'measure_option': autotvm.measure_option( + 'rpc', + rpc_device_key=device_key, + timeout=10, + parallel_num=1), 'use_transfer_learning': True, } @@ -192,6 +193,9 @@ def get_network(name, batch_size): # # You can also refer to our doc :any:`tune_tasks` (click this) to see some comments. # +# For andoird phone, add :code:`build_func='ndk'` to the argument list of autotvm.measure_option +# to use Android NDK for creating shared library. +# def tune_and_evaluate(): # extract workloads from nnvm graph @@ -213,20 +217,18 @@ def tune_and_evaluate(): # export library tmp = tempdir() - if tuning_option.get('use_ndk', False): # for android + if tuning_option['measure_option']['build_func'] == 'ndk': # for android from tvm.contrib import ndk filename = "net.so" - path_name = tmp.relpath(filename) - lib.export_library(path_name, ndk.create_shared) + lib.export_library(tmp.relpath(filename), ndk.create_shared) else: filename = "net.tar" - path_name = tmp.relpath(filename) - lib.export_library(path_name) + lib.export_library(tmp.relpath(filename)) # upload module to device print("Upload...") remote = autotvm.measure.request_remote(device_key, timeout=10000) - remote.upload(path_name) + remote.upload(tmp.relpath(filename)) rlib = remote.load_module(filename) # upload parameters to device @@ -245,7 +247,7 @@ def tune_and_evaluate(): (np.mean(prof_res), np.std(prof_res))) # We do not run the tuning in our webpage server. Uncomment this line to run by yourself. -#tune_and_evaluate() +tune_and_evaluate() ###################################################################### # Sample Output diff --git a/tutorials/autotvm/tune_simple_template.py b/tutorials/autotvm/tune_simple_template.py index 382a3e43eaa9..57616dea12ee 100644 --- a/tutorials/autotvm/tune_simple_template.py +++ b/tutorials/autotvm/tune_simple_template.py @@ -250,7 +250,7 @@ def matmul(N, L, M, dtype): logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) # use local cpu, measure 5 times for every config to reduce variance -measure_option = autotvm.measure_option(mode='local', +measure_option = autotvm.measure_option('local', number=5) # begin tuning, log records to file `matmul.log` From ffc8742e1b0e3e92ce85cbf3b4cad04a8ee17026 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Fri, 27 Jul 2018 16:41:13 -0700 Subject: [PATCH 31/76] fix lint --- python/tvm/autotvm/measure/measure.py | 6 +++--- python/tvm/autotvm/measure/measure_methods.py | 15 ++++++++------- python/tvm/autotvm/tuner/graph_tuning.py | 2 +- tutorials/autotvm/tune_nnvm_arm.py | 7 ++++--- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 8cabfd870f68..9988c10fa600 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -82,7 +82,7 @@ def measure_option(measure_func='local', and a RPC server silently for the user. 'rpc': request devices for measurement from the rpc tracker. In this mode, - you should start a rpc tracker in a separate processing and register your + you should start a rpc tracker in a separate processing and register your device to the tracker. callable: It is a customized function for measurement. @@ -102,7 +102,7 @@ def measure_option(measure_func='local', Set this according to the number of cpu cores (for compilation) and the number of devices you have (for measuring generate code). do_fork: bool, optional - Whether use multiprocessing (based on fork) for parallel measure jobs. + Whether use multiprocessing (based on fork) for running measure jobs in parallel. Set this to False if you want to debug or fork is not suitable for you case. NOTE: If this is False, parallel and timeout do not work. pack_size : int, optional @@ -122,7 +122,7 @@ def measure_option(measure_func='local', 'ndk': use Android NDK to create shared library. Use this for android target. - callable; customized build function for other backends (e.g. VTA) + callable: customized build function for other backends (e.g. VTA) rpc_priority: int, optional Priority of this task, used by scheduler in tracker diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index f13a3b9a00cb..e69061a52306 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -141,14 +141,15 @@ def _measure_pack(fbuild, input_pack, ref_input, ref_output): def default_build_func(inp, tmp_dir=None, **kwargs): """Build function module. Exception will be raised when any error occurs - + Parameters ---------- inp: MeasureInput The input of this measurement tmp_dir: tvm.contrib.util.TempDirectory, optional - The temporary directory for output built binary library. - In RPC mode, we will upload this library to remote device + The temporary directory for exporting built binary library. + If is not None (in RPC mode), the library in this direcotyr will be uploaded to + remote devices. kwargs: Dict Other arguments @@ -237,7 +238,7 @@ def measure_rpc(input_pack, 'ndk': use Android NDK to create shared library. Use this for android target - callable; customized build function for other backends (e.g. VTA) + callable: customized build function for other backends (e.g. VTA) kwargs: dict, optional Additional key word arguments @@ -292,7 +293,7 @@ def measure_local(input_pack, 'ndk': use Android NDK to create shared library. Use this for android target - callable; customized build function for other backends (e.g. VTA) + callable: customized build function for other backends (e.g. VTA) kwargs: dict, optional Additional key word arguments @@ -307,9 +308,9 @@ def _fbuild(inp): tmp_dir = util.tempdir() if build_func == 'default': - func, args, filename = default_build_func(inp, tmp_dir, **kwargs) + func, args, _ = default_build_func(inp, tmp_dir, **kwargs) else: - func, args, filename = build_func(inp, tmp_dir, **kwargs) + func, args, _ = build_func(inp, tmp_dir, **kwargs) ctx = context(str(inp.target), 0) time_f = func.time_evaluator( diff --git a/python/tvm/autotvm/tuner/graph_tuning.py b/python/tvm/autotvm/tuner/graph_tuning.py index 0ac36bb75698..797f305255cc 100644 --- a/python/tvm/autotvm/tuner/graph_tuning.py +++ b/python/tvm/autotvm/tuner/graph_tuning.py @@ -2,7 +2,7 @@ import os from . import callback, XGBTuner, GATuner, RandomTuner, GridSearchTuner -from .. import measure, record, task +from .. import record, task def tune_tasks(tasks, measure_option, diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index be44e4f482e9..50342dff64f7 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -175,8 +175,9 @@ def get_network(name, batch_size): 'measure_option': autotvm.measure_option( 'rpc', rpc_device_key=device_key, - timeout=10, - parallel_num=1), + number=4, + parallel_num=1, + timeout=10), 'use_transfer_learning': True, } @@ -247,7 +248,7 @@ def tune_and_evaluate(): (np.mean(prof_res), np.std(prof_res))) # We do not run the tuning in our webpage server. Uncomment this line to run by yourself. -tune_and_evaluate() +# tune_and_evaluate() ###################################################################### # Sample Output From 864c58f1b7ceae6fe2ea6da1593d04e7b29b391c Mon Sep 17 00:00:00 2001 From: Mercy Date: Sat, 28 Jul 2018 10:35:09 -0700 Subject: [PATCH 32/76] address comments --- apps/benchmark/README.md | 3 +- nnvm/src/top/nn/convolution.cc | 4 +-- python/tvm/autotvm/tophub.py | 52 +++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/apps/benchmark/README.md b/apps/benchmark/README.md index bfff7cbacbf6..d0eea27e8489 100644 --- a/apps/benchmark/README.md +++ b/apps/benchmark/README.md @@ -64,5 +64,6 @@ python3 -m tvm.exec.rpc_tracker If your device has a same SoC of the above device, you can reuse these parameters (e.g. use `llvm -device=arm_cpu -mode=rk3399 -target=aarch64-linux-gnu` as target). - Otherwise, you need to tune for your own device, please follow this [tutorial](please_fix_this_later.html). + Otherwise, you need to tune for your own device, please follow this + [tutorial](https://docs.tvm.ai/tutorials/autotvm/tune_nnvm_arm.html). diff --git a/nnvm/src/top/nn/convolution.cc b/nnvm/src/top/nn/convolution.cc index 85f23b0cd8f4..2843bea1f4ad 100644 --- a/nnvm/src/top/nn/convolution.cc +++ b/nnvm/src/top/nn/convolution.cc @@ -131,8 +131,8 @@ inline bool Conv2DInferShape(const nnvm::NodeAttrs& attrs, } inline bool WinogradConv2DInferShape(const nnvm::NodeAttrs& attrs, - std::vector* in_shape, - std::vector* out_shape) { + std::vector* in_shape, + std::vector* out_shape) { static const Layout kNCHW("NCHW"); static const Layout kOIHW("OIHW"); diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index c4f5506cc660..9a62f074541e 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -1,9 +1,8 @@ """ TopHub: Tensor Operator Hub To get the best performance, we typically need auto-tuning for the specific devices. -TVM releases pre-tuned parameters in TopHub (https://github.com/uwsaml/tvm-distro) -for some common networks and hardware targets. -TVM will donwload these parameters for you when you create the target for the first time. +TVM releases pre-tuned parameters in TopHub for some common networks and hardware targets. +TVM will download these parameters for you when you create the target for the first time. """ import os import json @@ -15,18 +14,20 @@ AUTOTVM_TOPHUB_ROOT_PATH = os.path.join(os.path.expanduser('~'), ".tvm", "tophub") -def load_context(target, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): - """Load dispatch context with pre-tuned parameters. - This function will load corresponding "*.log" files under root path - and select the best configs. + +def context(target, extra_files=None): + """Return the dispatch context with pre-tuned parameters. + The corresponding downloaded *.log files under tophub root path will be loaded. + Users can also add their own files in argument `extra_files`. Parameters ---------- target: Target The compilation target - rootpath: str, optional - The root path of stored parameters + extra_files: list of str, optional + Extra log files to load """ + rootpath = AUTOTVM_TOPHUB_ROOT_PATH best_context = ApplyHistoryBest([]) big_target = str(target).split()[0] @@ -39,20 +40,39 @@ def load_context(target, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): if os.path.isfile(os.path.join(rootpath, model) + ".log"): best_context.load(os.path.join(rootpath, model) + ".log") + for filename in extra_files: + best_context.load(filename) + + return best_context + + +def load_context(target, extra_files=None): + """Load the dispatch context with pre-tuned parameters. + The corresponding downloaded *.log files under tophub root path will be loaded. + Users can also add their own files in argument `extra_files`. + + Parameters + ---------- + target: Target + The compilation target + extra_files: list of str, optional + Extra log files to load + """ + best_context = context(target, extra_files) assert not DispatchContext.current, "Cannot load pre-tuned parameters inside a dispatch context" - DispatchContext.current = best_context + best_context.__enter__() -def download_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): +def download_package(backend): """Download pre-tuned parameters of operators for a backend Parameters ---------- backend: str The name of package - rootpath: str, optional - The root path of stored parameters """ + rootpath = AUTOTVM_TOPHUB_ROOT_PATH + if not os.path.isdir(rootpath): # make directory splits = os.path.split(rootpath) @@ -66,7 +86,7 @@ def download_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): os.path.join(rootpath, backend + ".log"), True, verbose=0) -def check_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): +def check_package(backend): """Check whether have pre-tuned parameters of the certain target. If not, will download it. @@ -74,10 +94,8 @@ def check_package(backend, rootpath=AUTOTVM_TOPHUB_ROOT_PATH): ---------- backend: str The name of package - rootpath: str, optional - The root path of stored parameters """ - if os.path.isfile(os.path.join(rootpath, backend + ".log")): + if os.path.isfile(os.path.join(AUTOTVM_TOPHUB_ROOT_PATH, backend + ".log")): return download_package(backend) From 7ffbb0fe82d8d24fcc8c1b13a8379c3d4b3a00cf Mon Sep 17 00:00:00 2001 From: Mercy Date: Sat, 28 Jul 2018 10:41:58 -0700 Subject: [PATCH 33/76] rebase --- python/tvm/contrib/util.py | 1 - vta/python/vta/top/arm_conv2d.py | 57 -------------------------------- 2 files changed, 58 deletions(-) delete mode 100644 vta/python/vta/top/arm_conv2d.py diff --git a/python/tvm/contrib/util.py b/python/tvm/contrib/util.py index 3e6f90234bcb..0d94a8da5058 100644 --- a/python/tvm/contrib/util.py +++ b/python/tvm/contrib/util.py @@ -102,7 +102,6 @@ def filelock(path): return FileLock(path) -<<<<<<< HEAD def is_source_path(path): """Check if path is source code path. diff --git a/vta/python/vta/top/arm_conv2d.py b/vta/python/vta/top/arm_conv2d.py deleted file mode 100644 index 79abbe9e3b21..000000000000 --- a/vta/python/vta/top/arm_conv2d.py +++ /dev/null @@ -1,57 +0,0 @@ -# pylint: disable=invalid-name,unused-variable,invalid-name -"""Conv2D schedule ported from RASP - -Used for CPU conv2d -""" -from __future__ import absolute_import as _abs - -from topi.nn.conv2d import conv2d, _get_schedule -from topi.nn.conv2d import SpatialPack, Im2ColPack, Workload -from topi.rasp import conv2d as _rasp_conv2d -from topi import generic - -_WORKLOADS = [ - Workload('float32', 'float32', 224, 224, 3, 64, 7, 7, 3, 3, 2, 2), - Workload('int8', 'int32', 224, 224, 3, 64, 7, 7, 3, 3, 2, 2), - Workload('int8', 'int32', 56, 56, 64, 64, 3, 3, 1, 1, 1, 1), - Workload('int8', 'int32', 56, 56, 64, 64, 1, 1, 0, 0, 1, 1), - Workload('int8', 'int32', 56, 56, 64, 128, 3, 3, 1, 1, 2, 2), - Workload('int8', 'int32', 56, 56, 64, 128, 1, 1, 0, 0, 2, 2), - Workload('int8', 'int32', 28, 28, 128, 128, 3, 3, 1, 1, 1, 1), - Workload('int8', 'int32', 28, 28, 128, 256, 3, 3, 1, 1, 2, 2), - Workload('int8', 'int32', 28, 28, 128, 256, 1, 1, 0, 0, 2, 2), - Workload('int8', 'int32', 14, 14, 256, 256, 3, 3, 1, 1, 1, 1), - Workload('int8', 'int32', 14, 14, 256, 512, 3, 3, 1, 1, 2, 2), - Workload('int8', 'int32', 14, 14, 256, 512, 1, 1, 0, 0, 2, 2), - Workload('int8', 'int32', 7, 7, 512, 512, 3, 3, 1, 1, 1, 1), -] -_SCHEDULES = [ - # float32 imagenet - SpatialPack(1, 8, 4, 1, 4, True), - SpatialPack(1, 8, 4, 1, 4, True), - SpatialPack(1, 7, 4, 2, 4, True), - SpatialPack(1, 4, 8, 4, 1, True), - SpatialPack(1, 4, 4, 1, 16, False), - SpatialPack(1, 4, 8, 4, 8, False), - SpatialPack(1, 7, 4, 3, 8, True), - SpatialPack(1, 2, 8, 1, 8, True), - SpatialPack(2, 1, 16, 1, 4, True), - SpatialPack(1, 7, 4, 1, 1, True), - Im2ColPack(7, 4, 1, 16, True), - Im2ColPack(7, 4, 1, 8, False), - Im2ColPack(7, 4, 1, 16, False), -] - -@_get_schedule.register(["vtacpu", "vta"]) -def _schedule_conv2d(wkl): - if wkl not in _WORKLOADS: - raise ValueError("no schedule for such workload: {}".format(wkl)) - idx = _WORKLOADS.index(wkl) - sch = _SCHEDULES[idx] - return sch - -conv2d.register(["vtacpu", "vta"], _rasp_conv2d._declaration_conv2d) - -generic.schedule_conv2d_nchw.register( - ["vtacpu", "vta"], - _rasp_conv2d.schedule_conv2d_nchw) From 2061026a158f77e99ad6769af2cb6e2450f13fd7 Mon Sep 17 00:00:00 2001 From: Mercy Date: Sat, 28 Jul 2018 11:27:21 -0700 Subject: [PATCH 34/76] fix --- python/tvm/autotvm/measure/measure.py | 18 ++------- python/tvm/autotvm/tophub.py | 12 +++--- python/tvm/autotvm/tuner/callback.py | 54 +++++---------------------- 3 files changed, 19 insertions(+), 65 deletions(-) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 9988c10fa600..6e7a58b0f543 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -63,16 +63,12 @@ def measure_option(measure_func='local', do_fork=True, pack_size=1, check_correctness=False, - + build_func='default', + replay_db=None, rpc_device_key=None, rpc_priority=1, rpc_timeout=60, - rpc_tracker_addr=None, - - build_func='default', - - replay_db=None, - save_to_replay_db=True): + rpc_tracker_addr=None): """Configure how to do measurement Parameters @@ -114,8 +110,6 @@ def measure_option(measure_func='local', replay_db : Database, optional The database that we retrieve saved MeasureResults from - save_to_replay_db: bool, optional - Whether save measure result to database. This is useless when replay_db is None build_func: str or callable, optional 'default': call default builder. This works for normal target (llvm, cuda) @@ -158,7 +152,6 @@ def measure_option(measure_func='local', 'build_func': build_func, 'replay_db': replay_db, - 'save_to_replay_db': save_to_replay_db, } def create_measure_batch(task, options): @@ -189,7 +182,6 @@ def create_measure_batch(task, options): rpc_priority, rpc_timeout = options['rpc_priority'], options['rpc_timeout'] build_func = options['build_func'] replay_db = options['replay_db'] - save_to_replay_db = options['save_to_replay_db'] kwargs = {} executor = LocalExecutor(timeout=timeout) @@ -280,10 +272,6 @@ def measure_batch(measure_inputs): results.extend(result) if replay_db is not None: - if save_to_replay_db: # save result to database - for measure_input, result in zip(measure_inputs, results): - replay_db.save(measure_input, result) - result_idx = 0 for i in range(len(partial_results)): if partial_results[i] is None: diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index 9a62f074541e..e865f21b0cdd 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -4,6 +4,8 @@ TVM releases pre-tuned parameters in TopHub for some common networks and hardware targets. TVM will download these parameters for you when you create the target for the first time. """ + +import logging import os import json @@ -40,8 +42,9 @@ def context(target, extra_files=None): if os.path.isfile(os.path.join(rootpath, model) + ".log"): best_context.load(os.path.join(rootpath, model) + ".log") - for filename in extra_files: - best_context.load(filename) + if extra_files: + for filename in extra_files: + best_context.load(filename) return best_context @@ -81,7 +84,7 @@ def download_package(backend): if not os.path.isdir(path): os.mkdir(path) - print("Download pre-tuned parameters for %s" % backend) + logging.info("Download pre-tuned parameters for %s" % backend) download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/%s.log" % backend, os.path.join(rootpath, backend + ".log"), True, verbose=0) @@ -111,10 +114,9 @@ def list_packages(): """ path = tempdir() filename = path.relpath("info.json") - print("Download meta info for pre-tuned parameters") + logging.info("Download meta info for pre-tuned parameters") download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/info.json", filename, True, verbose=0) - print("") with open(filename, "r") as fin: text = "".join(fin.readlines()) diff --git a/python/tvm/autotvm/tuner/callback.py b/python/tvm/autotvm/tuner/callback.py index c4eb00ef9361..4737fe510636 100644 --- a/python/tvm/autotvm/tuner/callback.py +++ b/python/tvm/autotvm/tuner/callback.py @@ -7,6 +7,7 @@ from .. import record + def log_to_file(file_out, protocol='json'): """Log the tuning records into file. The rows of the log are stored in the format of autotvm.record.encode. @@ -23,7 +24,6 @@ def log_to_file(file_out, protocol='json'): callback : callable Callback function to do the logging. """ - def _callback(_, inputs, results): """Callback implementation""" if isinstance(file_out, str): @@ -36,53 +36,18 @@ def _callback(_, inputs, results): return _callback -def save_tuner_state(prefix, save_every_sample=100): - """Save the state of tuner - - Parameters - ---------- - prefix : srt - prefix of the filename to store state - save_every_sample: int - save the state every x samples - - Returns - ------- - callback : function - Callback function to do the auto saving. - """ - def _callback(tuner, inputs, results): - for _, __ in zip(inputs, results): - try: - ct = len(tuner.visited) - except AttributeError: - ct = 0 - if ct % save_every_sample == 0: - tuner.save_state(prefix + "_%d.state" % ct) - - return _callback - - -def log_to_redis(host="localhost", port=6379, dbn=11): - """Record the tuning record to a redis DB. +def log_to_database(db): + """Save the tuning records to a database object. Parameters ---------- - host: str, optional - Host address of redis db - port: int, optional - Port of redis db - dbn: int, optional - which redis db to use, default 11 + db: Database + The database """ - # import here so only depend on redis when necessary - import redis - red = redis.StrictRedis(host=host, port=port, db=dbn) - def _callback(_, inputs, results): """Callback implementation""" for inp, result in zip(inputs, results): - red.set(inp, result) + db.save(inp, result) return _callback @@ -125,8 +90,7 @@ def progress_bar(total, prefix=''): prefix: str The prefix of output message """ - - class Context: + class _Context: """Context to store local variables""" def __init__(self): self.best_flops = 0 @@ -137,7 +101,7 @@ def __init__(self): def __del__(self): sys.stdout.write(' Done.\n') - ctx = Context() + ctx = _Context() tic = time.time() def _callback(tuner, inputs, results): @@ -155,6 +119,6 @@ def _callback(tuner, inputs, results): '| %.2f s' % (prefix, ctx.cur_flops/1e9, ctx.best_flops/1e9, ctx.ct, ctx.total, time.time() - tic)) - sys.stdout.flush() # As suggested by Rom Ruben + sys.stdout.flush() return _callback From 3f366ef91143cfbbbc58b78566d9feaffce62e56 Mon Sep 17 00:00:00 2001 From: Mercy Date: Sat, 28 Jul 2018 21:11:50 -0700 Subject: [PATCH 35/76] group rpc arguments --- python/tvm/autotvm/measure/__init__.py | 5 +- python/tvm/autotvm/measure/local_executor.py | 27 +- python/tvm/autotvm/measure/measure.py | 235 +---------- python/tvm/autotvm/measure/measure_methods.py | 399 +++++++++++------- python/tvm/autotvm/tophub.py | 2 +- python/tvm/exec/rpc_server.py | 15 +- python/tvm/exec/rpc_tracker.py | 7 +- python/tvm/rpc/__init__.py | 2 + python/tvm/rpc/proxy.py | 4 + python/tvm/rpc/server.py | 37 +- .../python/unittest/test_autotvm_database.py | 4 +- tutorials/autotvm/tune_conv2d_cuda.py | 1 - 12 files changed, 314 insertions(+), 424 deletions(-) diff --git a/python/tvm/autotvm/measure/__init__.py b/python/tvm/autotvm/measure/__init__.py index ac87194ce3e2..f75fbac61e11 100644 --- a/python/tvm/autotvm/measure/__init__.py +++ b/python/tvm/autotvm/measure/__init__.py @@ -1,8 +1,7 @@ """Distributed executor infrastructure to scale up the tuning""" -from .measure import MeasureInput, MeasureResult, MeasureErrorNo -from .measure import create_measure_batch, measure_option -from .measure_methods import request_remote +from .measure import MeasureInput, MeasureResult, MeasureErrorNo, measure_option +from .measure_methods import request_remote, create_measure_batch, use_rpc from .local_executor import LocalExecutor from .executor import Future, Executor diff --git a/python/tvm/autotvm/measure/local_executor.py b/python/tvm/autotvm/measure/local_executor.py index a9e90d594ab2..9b6e032f00ed 100644 --- a/python/tvm/autotvm/measure/local_executor.py +++ b/python/tvm/autotvm/measure/local_executor.py @@ -106,22 +106,23 @@ def get(self, timeout=None): class LocalExecutor(executor.Executor): - """Local executor that runs workers on the same machine with multiprocessing.""" - def __init__(self, timeout=None): + """Local executor that runs workers on the same machine with multiprocessing. + + Parameters + ---------- + timeout: float, optional + timeout of a job. If time is out. A TimeoutError will be returned (not raised) + do_fork: bool, optional + For some runtime systems that do not support fork after initialization + (e.g. cuda runtime, cudnn). Set this to False if you have used these runtime + before submitting jobs. + """ + def __init__(self, timeout=None, do_fork=True): self.timeout = timeout or executor.Executor.DEFAULT_TIMEOUT + self.do_fork = do_fork def submit(self, func, *args, **kwargs): - """ - - Note - ---------- - By default, the executor will fork a new process for a new job - But some runtime does not support fork (e.g. cuda runtime, cudnn). - In this circumstance, you should set 'do_fork' to False in kwargs. - """ - do_fork = kwargs.pop('do_fork', True) - - if not do_fork: + if not self.do_fork: return LocalFutureNoFork(func(*args, **kwargs)) queue = Queue(1) diff --git a/python/tvm/autotvm/measure/measure.py b/python/tvm/autotvm/measure/measure.py index 6e7a58b0f543..8c98e46b466a 100644 --- a/python/tvm/autotvm/measure/measure.py +++ b/python/tvm/autotvm/measure/measure.py @@ -1,18 +1,7 @@ # pylint: disable=pointless-string-statement,consider-using-enumerate,invalid-name """User facing API for specifying how to measure the generated code""" -import time from collections import namedtuple -import numpy as np - -from ... import build, nd, target as _target -from ...rpc.tracker import Tracker -from ...rpc.server import Server - -from ..util import get_const_tuple -from .local_executor import LocalExecutor - - class MeasureInput(namedtuple("MeasureInput", ["target", "task", "config"])): """ Stores all the necessary inputs for a measurement. @@ -44,6 +33,7 @@ class MeasureResult(namedtuple("MeasureResult", ["costs", "error_no", "all_cost" The absolute time stamp when we finish measurement. """ + class MeasureErrorNo(object): """Error type for MeasureResult""" NO_ERROR = 0 # no error @@ -55,20 +45,15 @@ class MeasureErrorNo(object): FLEET_ERROR = 6 # error of measure infrastructure -def measure_option(measure_func='local', +def measure_option(measure_func, number=1, repeat=1, timeout=60, parallel_num=1, do_fork=True, - pack_size=1, - check_correctness=False, build_func='default', - replay_db=None, - rpc_device_key=None, - rpc_priority=1, - rpc_timeout=60, - rpc_tracker_addr=None): + check_correctness=False, + replay_db=None): """Configure how to do measurement Parameters @@ -77,11 +62,7 @@ def measure_option(measure_func='local', 'local': use the local device for measurement. The tuner will start a tracker and a RPC server silently for the user. - 'rpc': request devices for measurement from the rpc tracker. In this mode, - you should start a rpc tracker in a separate processing and register your - device to the tracker. - - callable: It is a customized function for measurement. + callable: It is a callable function for measurement. number : int, optional Number of times to do the measurement for average @@ -99,35 +80,19 @@ def measure_option(measure_func='local', the number of devices you have (for measuring generate code). do_fork: bool, optional Whether use multiprocessing (based on fork) for running measure jobs in parallel. - Set this to False if you want to debug or fork is not suitable for you case. + Set this to False if you want to debug (see trackback) or using fork is not suitable. NOTE: If this is False, parallel and timeout do not work. - pack_size : int, optional - Number of configs to measure in one RPC call. - Usually this can be set to 1. If your device has high cost to establish a rpc connection, - set this higher. - check_correctness: bool - Whether check correctness after measurement. This will use llvm cpu as reference. - - replay_db : Database, optional - The database that we retrieve saved MeasureResults from - build_func: str or callable, optional 'default': call default builder. This works for normal target (llvm, cuda) 'ndk': use Android NDK to create shared library. Use this for android target. - callable: customized build function for other backends (e.g. VTA) - - rpc_priority: int, optional - Priority of this task, used by scheduler in tracker - rpc_device_key: str, optional - The device key of registered devices in tracker - rpc_timeout: int, optional - Timeout of rpc session - rpc_tracker_addr: Tuple(str, int), optional - The address of rpc tracker in Tuple(host, port) format. - If is set, will use this address. - If is not set, will use environment variable "TVM_TRACKER_HOST" and "TVM_TRACKER_PORT" + callable: customized build function for other backends (e.g. VTA). + See measure/measure_methods.py default_build_func for example. + check_correctness: bool + Whether check correctness after measurement. This will use llvm cpu as reference. + replay_db : Database, optional + The database that we retrieve saved MeasureResults from. Returns ------- @@ -141,181 +106,7 @@ def measure_option(measure_func='local', 'timeout': timeout, 'parallel_num': parallel_num, 'do_fork': do_fork, - 'pack_size': pack_size, - 'check_correctness': check_correctness, - - 'rpc_device_key': rpc_device_key, - 'rpc_priority': rpc_priority, - 'rpc_timeout': rpc_timeout, - 'rpc_tracker_addr': rpc_tracker_addr, - 'build_func': build_func, - + 'check_correctness': check_correctness, 'replay_db': replay_db, } - -def create_measure_batch(task, options): - """Get a standard measure_batch function. - - Parameters - ---------- - task: tvm.autotvm.task.Task - The tuning task - options: dict - The option for measuring generated code. - You should use the return value of :any:`autotvm.measure_option` for this argument - - Returns - ------- - measure_batch: callable - a callback function to measure a batch of configs - """ - from . import measure_methods - from ..database import filter_inputs - - measure_func = options['measure_func'] - number, repeat = options['number'], options['repeat'] - timeout, parallel_num, do_fork = options['timeout'], options['parallel_num'], options['do_fork'] - pack_size = options['pack_size'] - check_correctness = options['check_correctness'] - rpc_device_key = options['rpc_device_key'] - rpc_priority, rpc_timeout = options['rpc_priority'], options['rpc_timeout'] - build_func = options['build_func'] - replay_db = options['replay_db'] - - kwargs = {} - executor = LocalExecutor(timeout=timeout) - - if measure_func == 'local': - if do_fork: - # start temporary rpc tracker and rpc server for the user - tracker = Tracker('localhost', port=9000, port_end=10000, - silent=True) - rpc_device_key = '$local$device$%d' % tracker.port - server = Server('localhost', port=9000, port_end=10000, - key=rpc_device_key, - use_popen=True, silent=True, - tracker_addr=(tracker.host, tracker.port)) - - fmeasure = measure_methods.measure_rpc - kwargs['rpc_device_key'] = rpc_device_key - kwargs['rpc_tracker_addr'] = (tracker.host, tracker.port) - kwargs['rpc_timeout'] = timeout - else: - fmeasure = measure_methods.measure_local - elif measure_func == 'rpc': - fmeasure = measure_methods.measure_rpc - kwargs['rpc_device_key'] = rpc_device_key - kwargs['rpc_priority'] = rpc_priority - kwargs['rpc_timeout'] = rpc_timeout - assert rpc_device_key, "In rpc mode, a rpc_device_key must be provided" - else: - assert callable(measure_func), "In custom mode, custom_measure_func " \ - "must be a callable object" - fmeasure = measure_func - - if 'cuda' in task.target.keys and 'rpc_device_key' in kwargs: # query cuda device info - add_cuda_device_info(kwargs['rpc_device_key'], kwargs.get('rpc_tracker_addr'), kwargs) - if 'opencl' in task.target.keys and 'rpc_device_key' in kwargs: - add_opencl_device_info(kwargs['rpc_device_key'], kwargs.get('rpc_tracker_addr'), kwargs) - - if check_correctness: - # use llvm cpu to generate a reference input/output - # this option works for tuning topi, but might not work for you custom op - with _target.create("llvm"): - s, arg_bufs = task.instantiate(task.config_space.get(0)) - ref_input = [np.random.uniform(size=get_const_tuple(x.shape)).astype(x.dtype) - for x in arg_bufs] - func = build(s, arg_bufs, "llvm") - tvm_buf = [nd.array(x) for x in ref_input] - func(*tvm_buf) - ref_output = [x.asnumpy() for x in tvm_buf] - kwargs['ref_input'], kwargs['ref_output'] = ref_input, ref_output - - def measure_batch(measure_inputs): - """measure the time cost for a batch of configs in real machines""" - if replay_db is not None: - partial_results, measure_inputs =\ - filter_inputs(replay_db, measure_inputs, retry=False) - - # pack configs - input_packs = [] - for i in range(0, len(measure_inputs), pack_size): - input_packs.append(measure_inputs[i:i + pack_size]) - - # send to measure - futures = [] - for input_pack in input_packs: - future = executor.submit( - fmeasure, - input_pack, - number=number, - repeat=repeat, - do_fork=do_fork, - build_func=build_func, - **kwargs - ) - futures.append(future) - - # transform results - results = [] - for future in futures: - result = future.get() - if isinstance(result, Exception): - if measure_func == 'local' and not do_fork: - # debug usage, raise exception - raise result - tstamp = time.time() - results.extend([MeasureResult((result,), MeasureErrorNo.FLEET_ERROR, - timeout, tstamp)] * pack_size) - else: - results.extend(result) - - if replay_db is not None: - result_idx = 0 - for i in range(len(partial_results)): - if partial_results[i] is None: - partial_results[i] = results[result_idx] - result_idx += 1 - return partial_results - return results - - measure_batch.parallel_num = parallel_num - if measure_func == 'local' and do_fork: - # attach server and tracker object to avoid them of being garbage-collected - measure_batch.aux_objects = {"server": server, "tracker": tracker} - return measure_batch - - -def add_cuda_device_info(device_key, rpc_tracker_addr, kwargs): - """Query cuda device info. This is used to set the flags for nvcc compiler - and check the validity of a generated code.""" - from .measure_methods import request_remote - - remote = request_remote(device_key, rpc_tracker_addr) - ctx = remote.context('cuda', 0) - max_dims = ctx.max_thread_dimensions - kwargs['check_gpu'] = { - 'max_shared_memory_per_block': ctx.max_shared_memory_per_block, - 'max_threads_per_block': ctx.max_threads_per_block, - 'max_thread_x': max_dims[0], - 'max_thread_y': max_dims[1], - 'max_thread_z': max_dims[2], - } - - kwargs["cuda_arch"] = "sm_" + "".join(ctx.compute_version.split('.')) - -def add_opencl_device_info(device_key, rpc_tracker_addr, kwargs): - """Query opencl device info. This is used to check the validity of a generated code.""" - from .measure_methods import request_remote - - remote = request_remote(device_key, rpc_tracker_addr) - ctx = remote.context('opencl', 0) - max_dims = ctx.max_thread_dimensions - kwargs['check_gpu'] = { - 'max_shared_memory_per_block': ctx.max_shared_memory_per_block, - 'max_threads_per_block': ctx.max_threads_per_block, - 'max_thread_x': max_dims[0], - 'max_thread_y': max_dims[1], - 'max_thread_z': max_dims[2], - } diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index e69061a52306..d0fcb74579a5 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -12,20 +12,24 @@ import numpy as np -from ...contrib import nvcc, util -from ... import rpc, ir_pass, build, build_config, nd, context, TVMError, register_func +from ... import rpc, ir_pass, build, build_config, nd, context, TVMError, register_func, \ + target as _target +from ...contrib import nvcc, util, ndk from ..util import get_const_tuple from ..env import AutotvmGlobalScope -from .measure import MeasureResult, MeasureErrorNo from ..task.space import InstantiationError +from .measure import MeasureResult, MeasureErrorNo +from .local_executor import LocalExecutor + class HashMismatchError(ValueError): """Raised when the code hash of a submitted config doesn't match that on the measure side """ pass + def request_remote(device_key, tracker_addr=None, priority=1, timeout=60): """request a remote session @@ -34,7 +38,9 @@ def request_remote(device_key, tracker_addr=None, priority=1, timeout=60): device_key: string device key of registered device in tracker tracker_addr: Tuple(string, int), optional - The address of rpc tracker in (host, port) format + The address of rpc tracker in (host, port) format. + If is none, will use environment variable "TVM_TRACKER_HOST" + and "TVM_TRACKER_PORT" priority: int, optional priority of this request, larger is more prior timeout: float, optional @@ -58,33 +64,205 @@ def request_remote(device_key, tracker_addr=None, priority=1, timeout=60): return remote -def _measure_pack(fbuild, input_pack, ref_input, ref_output): - """Do measure for a pack of inputs. - This function mainly does error handling and correctness check. +def create_measure_batch(task, option): + """Get a standard measure_batch function. + + Parameters + ---------- + task: tvm.autotvm.task.Task + The tuning task + option: dict + The option for measuring generated code. + You should use the return value of function :any:`measure_option` for this argument. + + Returns + ------- + measure_batch: callable + a callback function to measure a batch of configs + """ + from ..database import filter_inputs + + measure_func = option['measure_func'] + number, repeat = option['number'], option['repeat'] + timeout, parallel_num, do_fork = option['timeout'], option['parallel_num'], option['do_fork'] + build_func = option['build_func'] + check_correctness = option['check_correctness'] + replay_db = option['replay_db'] + + executor = LocalExecutor(timeout=timeout, do_fork=do_fork) + + # convert convenient string to function object + attach_objects = None + if measure_func == 'local': + # start temporary rpc tracker and rpc server for the user + tracker = rpc.Tracker('localhost', port=9000, port_end=10000, silent=True) + device_key = '$local$device$%d' % tracker.port + server = rpc.Server('localhost', port=9000, port_end=10000, + key=device_key, + use_popen=True, silent=True, + tracker_addr=(tracker.host, tracker.port)) + + measure_func = use_rpc(device_key, tracker.host, tracker.port) + attach_objects = (server, tracker) + + build_kwargs = {} + if build_func == 'default': + build_func = default_build_func + if build_func == 'ndk': + build_func = default_build_func + build_kwargs['use_ndk'] = True + + # add device info of cuda and opencl target + if ('cuda' in task.target.keys or 'opencl' in task.target.keys) \ + and hasattr(measure_func, 'rpc_info'): + rpc_info = measure_func.rpc_info + add_gpu_target_info(task.target, rpc_info["key"], (rpc_info["host"], rpc_info["port"]), + build_kwargs) + + if check_correctness: + # use llvm cpu to generate a reference input/output + # this option works for tuning topi, but might not work for you custom op + with _target.create("llvm"): + s, arg_bufs = task.instantiate(task.config_space.get(0)) + ref_input = [np.random.uniform(size=get_const_tuple(x.shape)).astype(x.dtype) + for x in arg_bufs] + func = build(s, arg_bufs, "llvm") + tvm_buf = [nd.array(x) for x in ref_input] + func(*tvm_buf) + ref_output = [x.asnumpy() for x in tvm_buf] + else: + ref_input = ref_output = None + + def measure_batch(measure_inputs): + """measure the time cost for a batch of configs in real machines""" + if replay_db is not None: + partial_results, measure_inputs = \ + filter_inputs(replay_db, measure_inputs, retry=False) + + # launch measure jobs in parallel + pack_size = getattr(measure_func, "pack_size", 1) # measure `pack_size` inputs in one job + futures = [] + for i in range(0, len(measure_inputs), pack_size): + input_pack = measure_inputs[i:i + pack_size] + ret = executor.submit( + measure_func, + input_pack, + build_func, + build_kwargs, + number, + repeat, + ref_input, + ref_output) + futures.append(ret) + + # transform results + results = [] + for future in futures: + result = future.get() + if isinstance(result, Exception): + tstamp = time.time() + results.extend([MeasureResult((result,), MeasureErrorNo.FLEET_ERROR, + timeout, tstamp)] * pack_size) + else: + results.extend(result) + + if replay_db is not None: + result_idx = 0 + for i in range(len(partial_results)): + if partial_results[i] is None: + partial_results[i] = results[result_idx] + result_idx += 1 + return partial_results + return results + + measure_batch.parallel_num = parallel_num + # attach server and tracker object to avoid them of being garbage-collected + measure_batch.attach_objects = attach_objects + return measure_batch + + +def use_rpc(key, + host=None, + port=None, + priority=1, + session_timeout=60, + pack_size=1): + """ + Create a standard measure_func which uses RPC Tracker for measurement + + Parameters + ---------- + key: str + The registered key of the device in tracker. The tuner will request devices for + measurement by this key. + host: str, optional + The hostname of RPC Tracker. If not set, will use environment variable "TVM_TRACKER_HOST" + port: int, optional + The port of RPC Tracker. If not set, will use environment variable "TVM_TRACKER_PORT" + priority: int, optional + Priority of this task, used by scheduler in tracker + session_timeout: int, optional + Timeout of rpc session + pack_size: int, optional + The number of configs measure in one RPC session. + Usually this can be set to 1. If your device has high overhead to establish a + rpc connection, set this higher. + """ + def fmeasure(input_pack, build_func, build_kwargs, number, repeat, ref_input, ref_output): + remote = request_remote(key, (host, port), priority, session_timeout) + + res = _measure_common(input_pack, build_func, build_kwargs, number, repeat, + ref_input, ref_output, + remote) + return res + + fmeasure.pack_size = pack_size + fmeasure.rpc_info = {"key": key, "host": host, "port": port} + return fmeasure + + +def _measure_common(input_pack, build_func, build_kwargs, number, repeat, + ref_input=None, ref_output=None, remote=None): + """Measure the time cost for a pack of inputs. (Note: A pack is a list of inputs which will be measured inside a same RPC session) Parameters ---------- - fbuild : function takes MeasureInput returns tuple of (time_func, ctx, args) - The build function used to build each input. input_pack : list of MeasureInput The inputs we need to evaluate - ref_input: Array of np.ndarray + build_func : function takes MeasureInput returns tuple of (time_func, ctx, args) + The build function used to build each input. + build_kwargs: Dict + The extra keyword arguments to build_func + number : int, optional + Number of times to do the measurement for average + repeat : int, optional + Number of times to repeat the measurement. + In total, the generated code will be run (1 + number x repeat) times, + where the first one is warm up. The returned result contains `repeat` costs, + each of which is the average of `number` test run. + ref_input: Array of np.ndarray, optional Reference input for checking correctness - ref_output: Array of np.ndarray + ref_output: Array of np.ndarray, optional Reference output for checking correctness + remote: RPCSession, optional + The remote RPC session Returns ------- - res_pack : array of MeasureResult - The list of execution result of measurement. + res_pack : Array of MeasureResult + The list of results of measurement. """ res_pack = [] + tmp_dir = util.tempdir() if remote else None + for inp in input_pack: tic = time.time() + + # build function try: - time_f, ctx, arg_bufs = fbuild(inp) + func, arg_bufs, filename = build_func(inp, tmp_dir, **build_kwargs) except TVMError as exc: tstamp = time.time() msg = str(exc) @@ -95,9 +273,7 @@ def _measure_pack(fbuild, input_pack, ref_input, ref_output): msg = msg.split('\n')[-2].split(": ")[1] except Exception: # pylint: disable=broad-except pass - res_pack.append(MeasureResult((InstantiationError(msg),), - MeasureErrorNo.INSTANTIATION_ERROR, - tstamp - tic, tstamp)) + raise InstantiationError(msg) else: res_pack.append(MeasureResult((RuntimeError(msg),), MeasureErrorNo.COMPILE_HOST, @@ -110,14 +286,26 @@ def _measure_pack(fbuild, input_pack, ref_input, ref_output): tstamp - tic, tstamp)) continue + # upload built module + if remote: + remote.upload(tmp_dir.relpath(filename)) + func = remote.load_module(filename) + ctx = remote.context(str(inp.target), 0) + time_f = func.time_evaluator( + func.entry_name, ctx, number=number, repeat=repeat) + else: + ctx = context(str(inp.target), 0) + time_f = func.time_evaluator( + func.entry_name, ctx, number=number, repeat=repeat) + # measure time errno = MeasureErrorNo.NO_ERROR try: if ref_input: - args = [nd.array(x, ctx) for x in ref_input] + args = [nd.array(x, ctx=ctx) for x in ref_input] else: - args = [nd.empty(get_const_tuple(x.shape), dtype=x.dtype, - ctx=ctx) for x in arg_bufs] + args = [nd.empty(get_const_tuple(x.shape), dtype=x.dtype, ctx=ctx) + for x in arg_bufs] costs = time_f(*args).results if len(costs) > 2: # remove largest and smallest value to reduce variance costs = list(costs) @@ -148,10 +336,10 @@ def default_build_func(inp, tmp_dir=None, **kwargs): The input of this measurement tmp_dir: tvm.contrib.util.TempDirectory, optional The temporary directory for exporting built binary library. - If is not None (in RPC mode), the library in this direcotyr will be uploaded to + If is not None (in RPC mode), the library in this directory will be uploaded to remote devices. - kwargs: Dict - Other arguments + kwargs: Dict, optional + Other extra arguments Returns ------- @@ -175,15 +363,8 @@ def default_build_func(inp, tmp_dir=None, **kwargs): .format(str(inp.config.code_hash), str(code_hash))) opts = {} - - if "check_gpu" in kwargs: - values = kwargs['check_gpu'] - # Add gpu verify pass to filter out invalid configs in advance. - # This can accelerate the tuning process - check_keys = ['max_shared_memory_per_block', 'max_threads_per_block', - 'max_thread_x', 'max_thread_y', 'max_thread_z'] - opts["add_lower_pass"] = [ - (2, gpu_verify_pass(**{key: values[key] for key in check_keys}))] + if "check_gpu" in kwargs: # Add verify pass to filter out invalid configs in advance. + opts["add_lower_pass"] = [(2, gpu_verify_pass(**kwargs['check_gpu']))] if 'cuda_arch' in kwargs: set_cuda_target_arch(kwargs['cuda_arch']) @@ -193,138 +374,49 @@ def default_build_func(inp, tmp_dir=None, **kwargs): # export library to temp directory if tmp_dir: if kwargs.get('use_ndk', False): # for Android NDK - from ...contrib import ndk filename = "tmp_func_%0x.so" % getrandbits(64) func.export_library(tmp_dir.relpath(filename), ndk.create_shared) else: filename = "tmp_func_%0x.tar" % getrandbits(64) func.export_library(tmp_dir.relpath(filename)) + else: + filename = None return func, args, filename -def measure_rpc(input_pack, - rpc_device_key, - number, - repeat=1, - rpc_tracker_addr=None, - rpc_priority=1, - rpc_timeout=60, - build_func='default', - **kwargs): - """Measure the time cost on a device by rpc - - Parameters - ---------- - input_pack : list of MeasureInput - The inputs we need to evaluate - rpc_device_key: str - The device key of registered devices in tracker - number : int - Number of times to get the running measurement - repeat : int, optional - How many times we want to repeat the measurement. - - rpc_tracker_addr: Tuple(string, int), optional - The address of rpc tracker in (host, port) format - If is none, will use environment variable - rpc_priority: int, optional - priority of this task, used by scheduler in tracker - rpc_timeout: int, optional - timeout of the rpc session - - build_func: str or callable, optional - 'default': call default build_func. This works for normal target (llvm, cuda) +def add_gpu_target_info(target, device_key, rpc_tracker_addr, kwargs): + """Add device info for gpu target. + The info will be used to check the validity of generated code.""" + remote = request_remote(device_key, rpc_tracker_addr) + ctx = remote.context(str(target), 0) + max_dims = ctx.max_thread_dimensions + kwargs['check_gpu'] = { + 'max_shared_memory_per_block': ctx.max_shared_memory_per_block, + 'max_threads_per_block': ctx.max_threads_per_block, + 'max_thread_x': max_dims[0], + 'max_thread_y': max_dims[1], + 'max_thread_z': max_dims[2], + } - 'ndk': use Android NDK to create shared library. Use this for android target + if 'cuda' in target.keys: + kwargs["cuda_arch"] = "sm_" + "".join(ctx.compute_version.split('.')) - callable: customized build function for other backends (e.g. VTA) - - kwargs: dict, optional - Additional key word arguments - - Returns - ------- - res_pack : Array of MeasureResult - The list of execution results of measurement. - """ - def _fbuild(inp): - """ Local build function.""" - tmp_dir = util.tempdir() - - if build_func == 'default': - func, args, filename = default_build_func(inp, tmp_dir, **kwargs) - elif build_func == 'ndk': - kwargs['use_ndk'] = True - func, args, filename = default_build_func(inp, tmp_dir, **kwargs) - else: - func, args, filename = build_func(inp, tmp_dir, **kwargs) - - remote = request_remote(rpc_device_key, rpc_tracker_addr, rpc_priority, rpc_timeout) - remote.upload(tmp_dir.relpath(filename)) - func = remote.load_module(filename) - ctx = remote.context(str(inp.target), 0) - time_f = func.time_evaluator( - func.entry_name, ctx, number=number, repeat=repeat) - return time_f, ctx, args - - ret = _measure_pack(_fbuild, input_pack, - kwargs.get("ref_input", None), kwargs.get("ref_output", None)) - return ret - -def measure_local(input_pack, - number, - repeat=1, - build_func='default', - **kwargs): - """Measure the time cost on a local machine. - - Parameters - ---------- - input_pack : list of MeasureInput - The inputs we need to evaluate - number : int - Number of times to get the running measurement - repeat : int, optional - How many times we want to repeat the measurement. - - build_func: str or callable, optional - 'default': call default build_func. This works for normal target (llvm, cuda) - - 'ndk': use Android NDK to create shared library. Use this for android target - - callable: customized build function for other backends (e.g. VTA) - - kwargs: dict, optional - Additional key word arguments - - Returns - ------- - res_pack : Array of MeasureResult - The list of execution results of measurement. - """ - def _fbuild(inp): - """ Local build function """ - tmp_dir = util.tempdir() - - if build_func == 'default': - func, args, _ = default_build_func(inp, tmp_dir, **kwargs) - else: - func, args, _ = build_func(inp, tmp_dir, **kwargs) +def set_cuda_target_arch(arch): + """set target architecture of nvcc compiler""" + AutotvmGlobalScope.current.cuda_target_arch = arch - ctx = context(str(inp.target), 0) - time_f = func.time_evaluator( - func.entry_name, ctx, number=number, repeat=repeat) - return time_f, ctx, args - ret = _measure_pack(_fbuild, input_pack, - kwargs.get("ref_input", None), kwargs.get("ref_output", None)) - return ret +@register_func +def tvm_callback_cuda_compile(code): + """use nvcc to generate ptx code for better optimization""" + ptx = nvcc.compile_cuda(code, target="ptx", arch=AutotvmGlobalScope.current.cuda_target_arch) + return ptx def gpu_verify_pass(**kwargs): - """Verify the validity of a gpu kernel - This pass will check shared memory size and number of threads per block. + """Verify the validity of a gpu kernel. + This pass will check memory usage and number of threads per block. """ def verify_pass(stmt): valid = ir_pass.VerifyGPUCode(stmt, kwargs) @@ -332,14 +424,3 @@ def verify_pass(stmt): raise InstantiationError("Skipped because of invalid gpu kernel") return stmt return verify_pass - - -@register_func -def tvm_callback_cuda_compile(code): - """use nvcc to generate ptx code for better optimization""" - ptx = nvcc.compile_cuda(code, target="ptx", arch=AutotvmGlobalScope.current.cuda_target_arch) - return ptx - -def set_cuda_target_arch(arch): - """set target architecture of nvcc compiler""" - AutotvmGlobalScope.current.cuda_target_arch = arch diff --git a/python/tvm/autotvm/tophub.py b/python/tvm/autotvm/tophub.py index e865f21b0cdd..c89054c8dd09 100644 --- a/python/tvm/autotvm/tophub.py +++ b/python/tvm/autotvm/tophub.py @@ -84,7 +84,7 @@ def download_package(backend): if not os.path.isdir(path): os.mkdir(path) - logging.info("Download pre-tuned parameters for %s" % backend) + logging.info("Download pre-tuned parameters for %s", backend) download("https://raw.githubusercontent.com/uwsaml/tvm-distro/master/tophub/%s.log" % backend, os.path.join(rootpath, backend + ".log"), True, verbose=0) diff --git a/python/tvm/exec/rpc_server.py b/python/tvm/exec/rpc_server.py index c9f0777fad57..209b73b16880 100644 --- a/python/tvm/exec/rpc_server.py +++ b/python/tvm/exec/rpc_server.py @@ -40,20 +40,21 @@ def main(args): help='The port of the PRC') parser.add_argument('--port-end', type=int, default=9199, help='The end search port of the PRC') - parser.add_argument('--key', type=str, default="", - help="RPC key used to identify the connection type.") - parser.add_argument('--load-library', type=str, default="", + parser.add_argument('--tracker', type=str, + help="The address of RPC tracker in host:port format. " + "e.g. (10.77.1.234:9190)") + parser.add_argument('--key', type=str, + help="The key used to identify the device type in tracker.") + parser.add_argument('--silent', action='store_true', + help="Whether run in silent mode.") + parser.add_argument('--load-library', type=str, help="Additional library to load") - parser.add_argument('--tracker', type=str, default="", - help="Report to RPC tracker") parser.add_argument('--no-fork', dest='fork', action='store_false', help="Use spawn mode to avoid fork. This option \ is able to avoid potential fork problems with Metal, OpenCL \ and ROCM compilers.") parser.add_argument('--custom-addr', type=str, help="Custom IP Address to Report to RPC Tracker") - parser.add_argument('--silent', action='store_true', - help="Whether run in silent mode.") parser.set_defaults(fork=True) args = parser.parse_args() diff --git a/python/tvm/exec/rpc_tracker.py b/python/tvm/exec/rpc_tracker.py index 3ac013d649f7..3a89014f77a4 100644 --- a/python/tvm/exec/rpc_tracker.py +++ b/python/tvm/exec/rpc_tracker.py @@ -6,13 +6,12 @@ import argparse import multiprocessing import sys -from ..rpc.tracker import Tracker - +from .. import rpc def main(args): """Main funciton""" - tracker = Tracker(args.host, port=args.port, port_end=args.port_end, - silent=args.silent) + tracker = rpc.Tracker(args.host, port=args.port, port_end=args.port_end, + silent=args.silent) tracker.proc.join() diff --git a/python/tvm/rpc/__init__.py b/python/tvm/rpc/__init__.py index 6a356e2d64ff..974151c1e5b0 100644 --- a/python/tvm/rpc/__init__.py +++ b/python/tvm/rpc/__init__.py @@ -10,4 +10,6 @@ """ from .server import Server +from .tracker import Tracker +from .proxy import Proxy from .client import RPCSession, LocalSession, TrackerSession, connect, connect_tracker diff --git a/python/tvm/rpc/proxy.py b/python/tvm/rpc/proxy.py index 44de99e7e959..9afb9ca1a667 100644 --- a/python/tvm/rpc/proxy.py +++ b/python/tvm/rpc/proxy.py @@ -460,6 +460,10 @@ class Proxy(object): timeout_server : float, optional Timeout of server until it sees a matching connection. + tracker_addr: Tuple (str, int) , optional + The address of RPC Tracker in tuple (host, ip) format. + If is not None, the server will register itself to the tracker. + index_page : str, optional Path to an index page that can be used to display at proxy index. diff --git a/python/tvm/rpc/server.py b/python/tvm/rpc/server.py index 0d6112df6089..1d6c0226f138 100644 --- a/python/tvm/rpc/server.py +++ b/python/tvm/rpc/server.py @@ -20,6 +20,7 @@ import subprocess import time import sys +import signal from .._ffi.function import register_func from .._ffi.base import py_str @@ -257,7 +258,7 @@ def _popen(cmd): class Server(object): - """Start RPC server on a seperate process. + """Start RPC server on a separate process. This is a simple python implementation based on multi-processing. It is also possible to implement a similar C based sever with @@ -284,14 +285,21 @@ class Server(object): This is recommended to switch on if we want to do local RPC demonstration for GPU devices to avoid fork safety issues. - silent: bool, optional - Whether run this server in silent mode. + tracker_addr: Tuple (str, int) , optional + The address of RPC Tracker in tuple(host, ip) format. + If is not None, the server will register itself to the tracker. key : str, optional - The key used to identify the server in Proxy connection. + The key used to identify the device type in tracker. load_library : str, optional List of additional libraries to be loaded during execution. + + custom_addr: str, optional + Custom IP Address to Report to RPC Tracker + + silent: bool, optional + Whether run this server in silent mode. """ def __init__(self, host, @@ -299,11 +307,11 @@ def __init__(self, port_end=9199, is_proxy=False, use_popen=False, - silent=False, tracker_addr=None, key="", load_library=None, - custom_addr=None): + custom_addr=None, + silent=False): try: if base._ServerLoop is None: raise RuntimeError("Please compile with USE_RPC=1") @@ -313,6 +321,7 @@ def __init__(self, self.port = port self.libs = [] self.custom_addr = custom_addr + self.use_popen = use_popen self.logger = logging.getLogger("RPCServer") if silent: @@ -334,10 +343,7 @@ def __init__(self, if silent: cmd += ["--silent"] - self.proc = multiprocessing.Process( - target=subprocess.check_call, args=(cmd,)) - self.proc.deamon = True - self.proc.start() + self.proc = subprocess.Popen(cmd, preexec_fn=os.setsid) time.sleep(0.5) elif not is_proxy: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -371,9 +377,14 @@ def __init__(self, def terminate(self): """Terminate the server process""" - if self.proc: - self.proc.terminate() - self.proc = None + if self.use_popen: + if self.proc: + os.killpg(self.proc.pid, signal.SIGTERM) + self.proc = None + else: + if self.proc: + self.proc.terminate() + self.proc = None def __del__(self): self.terminate() diff --git a/tests/python/unittest/test_autotvm_database.py b/tests/python/unittest/test_autotvm_database.py index be54ae1f1732..af4704d95e51 100644 --- a/tests/python/unittest/test_autotvm_database.py +++ b/tests/python/unittest/test_autotvm_database.py @@ -163,7 +163,7 @@ def test_db_save_replay(): measure_option = autotvm.measure_option('local', do_fork=False, timeout=2, - replay_db=_db, save_to_replay_db=True) + replay_db=_db) measure_batch = autotvm.measure.create_measure_batch(task, measure_option) batch_size = 2 @@ -183,6 +183,8 @@ def test_db_save_replay(): results = measure_batch(inputs) all_results += results ct += 1 + callback = autotvm.callback.log_to_database(_db) + callback(None, all_inputs, all_results) assert len(_db.db.keys()) == batch_size * TRIAL_LIMIT, \ "%d vs %d" % (len(_db.db.keys()), batch_size * TRIAL_LIMIT) diff --git a/tutorials/autotvm/tune_conv2d_cuda.py b/tutorials/autotvm/tune_conv2d_cuda.py index 0c7b528a50b2..e86eae017a1a 100644 --- a/tutorials/autotvm/tune_conv2d_cuda.py +++ b/tutorials/autotvm/tune_conv2d_cuda.py @@ -186,7 +186,6 @@ def conv2d_no_batching(N, H, W, CI, CO, KH, KW, stride, padding): # Evaluate running time. Here we choose a large repeat number (200) to reduce the noise # and the overhead of kernel launch. You can also use nvprof to validate the result. - evaluator = func.time_evaluator(func.entry_name, ctx, number=200) print('Time cost of this operator: %f' % evaluator(a_tvm, w_tvm, c_tvm).mean) From d470233e311faa3dfceae5daa2b2542b55066aa3 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sat, 28 Jul 2018 21:37:18 -0700 Subject: [PATCH 36/76] fix tutorial --- python/tvm/autotvm/__init__.py | 2 +- python/tvm/autotvm/measure/measure_methods.py | 4 ++-- python/tvm/exec/rpc_server.py | 2 +- tutorials/autotvm/tune_nnvm_arm.py | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/python/tvm/autotvm/__init__.py b/python/tvm/autotvm/__init__.py index c1c43def44bc..c3f279ff264a 100644 --- a/python/tvm/autotvm/__init__.py +++ b/python/tvm/autotvm/__init__.py @@ -22,7 +22,7 @@ from . import tophub # some shortcuts -from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo +from .measure import measure_option, MeasureInput, MeasureResult, MeasureErrorNo, use_rpc from .tuner import callback, tune_tasks from .task import template, get_config, create, ConfigSpace, ConfigEntity from .record import ApplyHistoryBest as apply_history_best diff --git a/python/tvm/autotvm/measure/measure_methods.py b/python/tvm/autotvm/measure/measure_methods.py index d0fcb74579a5..6882fb60579d 100644 --- a/python/tvm/autotvm/measure/measure_methods.py +++ b/python/tvm/autotvm/measure/measure_methods.py @@ -52,8 +52,8 @@ def request_remote(device_key, tracker_addr=None, priority=1, timeout=60): """ # connect to the tracker if tracker_addr: - host = tracker_addr[0] - port = tracker_addr[1] + host = tracker_addr[0] or os.environ['TVM_TRACKER_HOST'] + port = tracker_addr[1] or int(os.environ['TVM_TRACKER_PORT']) else: host = os.environ['TVM_TRACKER_HOST'] port = int(os.environ['TVM_TRACKER_PORT']) diff --git a/python/tvm/exec/rpc_server.py b/python/tvm/exec/rpc_server.py index 209b73b16880..5998e9ffe6ac 100644 --- a/python/tvm/exec/rpc_server.py +++ b/python/tvm/exec/rpc_server.py @@ -43,7 +43,7 @@ def main(args): parser.add_argument('--tracker', type=str, help="The address of RPC tracker in host:port format. " "e.g. (10.77.1.234:9190)") - parser.add_argument('--key', type=str, + parser.add_argument('--key', type=str, default="", help="The key used to identify the device type in tracker.") parser.add_argument('--silent', action='store_true', help="Whether run in silent mode.") diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 50342dff64f7..24b6c294b0b0 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -29,6 +29,8 @@ # pip install psutil xgboost # +import logging + import time import os @@ -173,8 +175,7 @@ def get_network(name, batch_size): 'early_stopping': 200, 'measure_option': autotvm.measure_option( - 'rpc', - rpc_device_key=device_key, + autotvm.use_rpc(device_key), number=4, parallel_num=1, timeout=10), @@ -248,7 +249,7 @@ def tune_and_evaluate(): (np.mean(prof_res), np.std(prof_res))) # We do not run the tuning in our webpage server. Uncomment this line to run by yourself. -# tune_and_evaluate() +#tune_and_evaluate() ###################################################################### # Sample Output From b98939e66b1db47dcdc4d6db69557f20c22d3259 Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sat, 28 Jul 2018 22:43:13 -0700 Subject: [PATCH 37/76] fix warning --- python/tvm/autotvm/task/nnvm_integration.py | 4 ++-- src/codegen/opt/build_opencl_off.cc | 1 - src/pass/vectorize_loop.cc | 1 + topi/python/topi/x86/conv2d.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/python/tvm/autotvm/task/nnvm_integration.py b/python/tvm/autotvm/task/nnvm_integration.py index 160163ffb60f..7f545eece310 100644 --- a/python/tvm/autotvm/task/nnvm_integration.py +++ b/python/tvm/autotvm/task/nnvm_integration.py @@ -198,7 +198,7 @@ def _dummy_func(*args, **kwargs): not in self.task_collection: self.task_collection.append((self.topi_to_task[local_func], serialize_args(args))) - with _target.create("llvm"): + with _target.create("opencl"): return local_func(*args) _local_scope(func) @@ -279,7 +279,7 @@ def extract_from_graph(graph, shape, dtype, target, symbols, target_host=None): # run compiler to collect all TOPI calls during compilation env.reset() - dummy_target = _target.create("llvm -device=dummy") + dummy_target = _target.create("opencl -device=dummy") nnvm.compiler.build(graph, target=dummy_target, shape=shape, dtype=dtype) tasks = [] diff --git a/src/codegen/opt/build_opencl_off.cc b/src/codegen/opt/build_opencl_off.cc index fc962d4840e9..adadb84e9b1c 100644 --- a/src/codegen/opt/build_opencl_off.cc +++ b/src/codegen/opt/build_opencl_off.cc @@ -13,7 +13,6 @@ Module OpenCLModuleCreate( std::string fmt, std::unordered_map fmap, std::string source) { - LOG(WARNING) << "OpenCL runtime not enabled, return a source module..."; return codegen::DeviceSourceModuleCreate(data, fmt, fmap, "opencl"); } diff --git a/src/pass/vectorize_loop.cc b/src/pass/vectorize_loop.cc index 62b4afdaa41f..206b75ed068d 100644 --- a/src/pass/vectorize_loop.cc +++ b/src/pass/vectorize_loop.cc @@ -300,6 +300,7 @@ class Vectorizer : public IRMutator { CHECK(!op->condition.type().is_vector()); Expr condition = this->Mutate(op->condition); if (condition.type().is_vector()) { + LOG(WARNING) << "Detect vector condition in Vectorized Loop, scalarizing..."; return Scalarize(s); } Stmt then_case = this->Mutate(op->then_case); diff --git a/topi/python/topi/x86/conv2d.py b/topi/python/topi/x86/conv2d.py index 801a264d306f..afcb4b73f731 100644 --- a/topi/python/topi/x86/conv2d.py +++ b/topi/python/topi/x86/conv2d.py @@ -215,7 +215,8 @@ def default_schedule(op): s[C].reorder(fused, rc, h, wo, ry, rx, wi) # move rc to outer loop s[C].unroll(rx) s[C].unroll(ry) - s[C].vectorize(wi) + if w.dom.extent.value % 16 == 0: + s[C].vectorize(wi) def traverse(op): """Traverse operators from computation graph""" From fb78db8150f4f995b414f09a2bf5fa8ccba1ab7e Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sat, 28 Jul 2018 22:47:20 -0700 Subject: [PATCH 38/76] fix integration test --- tests/python/integration/test_tuning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/python/integration/test_tuning.py b/tests/python/integration/test_tuning.py index 82b020724c0b..87da86a4654f 100644 --- a/tests/python/integration/test_tuning.py +++ b/tests/python/integration/test_tuning.py @@ -108,7 +108,8 @@ def test_task_tuner_without_measurement(): """test task and tuner without measurement""" task, target = get_sample_task() - def custom_measure(input_pack, **kwargs): + def custom_measure(input_pack, build_func, build_args, number, repeat, + ref_input, ref_output): from tvm.autotvm import MeasureResult results = [] @@ -127,6 +128,7 @@ def custom_measure(input_pack, **kwargs): for tuner_class in [autotvm.tuner.RandomTuner, autotvm.tuner.GridSearchTuner]: tuner = tuner_class(task) tuner.tune(n_trial=10, measure_option=measure_option) + assert tuner.best_flops > 1 def test_tuning_with_measure(): def check(target, target_host): From 127000ff98194159b2c95ef22c14eca91f46d2eb Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sat, 28 Jul 2018 22:53:11 -0700 Subject: [PATCH 39/76] fix cuda tutorial --- tutorials/autotvm/tune_conv2d_cuda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/autotvm/tune_conv2d_cuda.py b/tutorials/autotvm/tune_conv2d_cuda.py index e86eae017a1a..af36511e5ffb 100644 --- a/tutorials/autotvm/tune_conv2d_cuda.py +++ b/tutorials/autotvm/tune_conv2d_cuda.py @@ -145,13 +145,13 @@ def conv2d_no_batching(N, H, W, CI, CO, KH, KW, stride, padding): # use local gpu, measure 5 times for every config to reduce variance # run 8 parallel threads for compilation measure_option = autotvm.measure_option('local', - number=10, + number=5, parallel_num=8, timeout=20) # begin tuning, log records to file `conv2d.log` tuner = autotvm.tuner.XGBTuner(task) -tuner.tune(n_trial=200, +tuner.tune(n_trial=20, measure_option=measure_option, callbacks=[autotvm.callback.log_to_file('conv2d.log')]) From 38c9ca4d20609021b12c94c29f1c30b1251a0852 Mon Sep 17 00:00:00 2001 From: Mercy Date: Sat, 28 Jul 2018 22:57:37 -0700 Subject: [PATCH 40/76] fix indent --- tutorials/autotvm/tune_nnvm_arm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tutorials/autotvm/tune_nnvm_arm.py b/tutorials/autotvm/tune_nnvm_arm.py index 24b6c294b0b0..c80210a6a383 100644 --- a/tutorials/autotvm/tune_nnvm_arm.py +++ b/tutorials/autotvm/tune_nnvm_arm.py @@ -261,11 +261,11 @@ def tune_and_evaluate(): # # .. code-block:: bash # -# [Task 1/16] Current/Best: 15.48/ 21.21 GFLOPS | Progress: (412/1000) | 531.53 s Done. -# [Task 2/16] Current/Best: 18.85/ 23.81 GFLOPS | Progress: (269/1000) | 261.59 s Done. -# [Task 3/16] Current/Best: 10.58/ 14.46 GFLOPS | Progress: (406/1000) | 317.72 s Done. -# [Task 4/16] Current/Best: 14.74/ 21.69 GFLOPS | Progress: (268/1000) | 246.84 s Done. -# [Task 5/16] Current/Best: 6.58/ 16.31 GFLOPS | Progress: (376/1000) | 301.62 s Done. -# [Task 6/16] Current/Best: 9.70/ 25.04 GFLOPS | Progress: (127/1000) | 154.13 s -# .... +# [Task 1/16] Current/Best: 15.48/ 21.21 GFLOPS | Progress: (412/1000) | 531.53 s Done. +# [Task 2/16] Current/Best: 18.85/ 23.81 GFLOPS | Progress: (269/1000) | 261.59 s Done. +# [Task 3/16] Current/Best: 10.58/ 14.46 GFLOPS | Progress: (406/1000) | 317.72 s Done. +# [Task 4/16] Current/Best: 14.74/ 21.69 GFLOPS | Progress: (268/1000) | 246.84 s Done. +# [Task 5/16] Current/Best: 6.58/ 16.31 GFLOPS | Progress: (376/1000) | 301.62 s Done. +# [Task 6/16] Current/Best: 9.70/ 25.04 GFLOPS | Progress: (127/1000) | 154.13 s +# .... From e4b2f4bf242fd1a9e5838fdbc6244ff75146ceba Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sun, 29 Jul 2018 23:52:28 -0700 Subject: [PATCH 41/76] update for .nfs* file --- .gitignore | 3 +++ .../tvm/_ffi/_cy3/.nfs0000000000b253d70000104d | Bin 654640 -> 0 bytes 2 files changed, 3 insertions(+) delete mode 100755 python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d diff --git a/.gitignore b/.gitignore index 7080502aaf86..3c968eb3ed47 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,6 @@ build* # Jetbrain .idea + +# tmp file +.nfs* diff --git a/python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d b/python/tvm/_ffi/_cy3/.nfs0000000000b253d70000104d deleted file mode 100755 index 802616b23fc49f453f790f355db3e39aa518738f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 654640 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