【从零开始学深度学习编译器】五,TVM Relay以及Pass简介

2023-05-16

TVM Relay以及Pass简介

  • 0x0. 介绍
  • 0x2. Relay介绍
    • 0x2.1 使用Relay建立一个计算图
    • 0x2.2 Module:支持多个函数(Graphs)
    • 0x2.3 Let Binding and Scopes
    • 0x2.4 为什么我们可能需要Let Binding
    • 0x2.5 对IR转换的影响
  • 0x3. 基于Relay构建一个自定义的神经网络示例
  • 0x4. 初识Pass
  • 0x5. 小结
  • 0x6. 参考资料

0x0. 介绍

在前面几节的介绍中我们了解到了TVM是如何将ONNX前端模型转换为IR Module的,并且还剖析了TVM中的Relay算子和TOPI算子的扭转过程,知道了Relay算子的最终计算也是基于TOPI算子集合完成的。然后我们在基于ONNX模型结构了解TVM的前端那篇文章贴出的示例程序中还有一个很重要的细节即TVM的编译流程没有详细介绍,即下面这几行代码:

######################################################################
# Relay Build
# -----------
# Compile the graph to llvm target with given input specification.
target = "llvm"
target_host = "llvm"
dev = tvm.cpu(0)
with tvm.transform.PassContext(opt_level=3):
    lib = relay.build(mod, target=target, target_host=target_host, params=params)

这几行代码展示了TVM的编译流程,在这个编译流程里面不仅包含了基于Relay IR进行的优化策略来去除冗余的算子(也叫Pass)还包含了将Relay程序编译成特定后端(这里是llvm)可以执行的代码(codegen)。

在这篇文章中我们将简单介绍一下Relay,然后再认识一下TVM中的Pass,也就是解释with tvm.transform.PassContext(opt_level=3)这个类具体完成了什么工作。至于code gen和详细的编译流程,由于TVM的水太深,我还没把握住,下次再探索吧。

0x2. Relay介绍

这一节主要结合TVM的文档(https://tvm.apache.org/docs/dev/relay_intro.html)来介绍一下NNVM的第二代Relay。Relay的设计目标有以下几点:

支持传统的数据流(DataFlow)风格编程。
支持functional-style scoping,并融合了编程语言领域的一些知识,带了一些新的特性(支持Let表达式,支持递归等等)
支持数据流风格和函数式风格混合编程。

0x2.1 使用Relay建立一个计算图

传统的深度学习框架使用计算图作为它们的中间表示。 计算图(或数据流图)是代表计算过程的有向无环图(DAG)。 尽管由于缺少控制流,数据流图在计算能力方面受到限制,但它们的简单性使其易于实现自动微分并针对异构执行环境进行编译(例如,在专用硬件上执行计算图的某些部分,即子图)。
使用Relay构建一个简单的计算图示例代码以及对应的文本形式和AST抽象语法树

我们可以使用Relay来构建一个计算(DataFlow)图。具体来说,上面的代码显示了如何构造一个简单的两个节点的计算图,我们可以发现这个示例的代码和现有的Garph IR如NNVMv1没有太大区别,唯一的区别是在术语方面:

  • 现有框架通常使用图和子图
  • Relay使用函数,例如 – fn(%x),表示图
    每个数据流节点都是Relay中的一个CallNode。通过Relay的Python DSL,我们可以快速构建计算图。在上面的代码需要注意的是这里显示构造了一个Add节点,两个输入都指向%1。当一个深度学习框架对上面的计算图进行推理时,它将会按照拓扑序进行计算,并且%1只会被计算一次。虽然这个事实对于深度学习框架的开发者来说是一件很自然的事情,但这或许会使得只关心算法的研究员困惑。如果我们实现一个简单的vistor来打印结果并将结果视为嵌套的Call表达式,它将是log(%x) + log(%x)。

当DAG中存在共享节点时,这种歧义是由程序语义的解释不同而引起的。在正常的函数式编程IR中,嵌套表达式被视为表达式树,并没有考虑%1实际上在%2中被重用了2次的事实。

Relay IR注意到了这个区别。其实深度学习框架用户经常使用这种方式构建计算图,其中经常发生DAG节点重用。然后当我们以文本格式打印Relay程序时,我们每行打印一个CallNode,并为每个CallNode分配一个临时ID(%1, %2),以便可以在程序的后续部分中引用每个公共节点。

0x2.2 Module:支持多个函数(Graphs)

上面介绍了如何构建一个数据流图为一个函数。然后一个很自然的问题是可以做到构建多个函数并相互调用吗?Relay允许将多个函数组合在一个Module中,下面的代码展示了一个函数调用另外一个函数的例子。

def @muladd(%x, %y, %z) {
  %1 = mul(%x, %y)
  %2 = add(%1, %z)
  %2
}
def @myfunc(%x) {
  %1 = @muladd(%x, 1, 2)
  %2 = @muladd(%1, 2, 3)
  %2
}

Module可以被看作Map<GlobalVar, Function>,其中GlobalVar仅仅是一个表示函数名的ID,上面的程序中GlobalVar是@muladd和@myfunc。当一个CallNode被用来调用另外一个函数时,相应的GlobalVar被存在CallNode的OP中。它包含了一个间接的等级关系—我们需要使用相应的GlobalVar从Module中查找被调用函数的主体。在这种情况下,我们也可以直接将引用的函数存储为CallNode中的OP。那么为什么需要引入GlobalVar呢?主要原因是为了解耦定义和声明,并支持了函数的递归和延迟声明。

def @myfunc(%x) {
  %1 = equal(%x, 1)
   if (%1) {
      %x
   } else {
     %2 = sub(%x, 1)
     %3 = @myfunc(%2)
      %4 = add(%3, %3)
      %4
  }
}

在上面的例子中,@myfunc递归调用它自己。使用GlobalVar @myfunc来表示函数避免了数据结构中的循环依赖性。至此,已经介绍完了Relay中的基本概念。值得注意的是,相比NNVM,Relay在如下方面进行了改进:

有文本形式中间表示,便于开发和 debug
支持子图函数、联合模块,便于联合优化
前端用户友好,便于调优

0x2.3 Let Binding and Scopes

至此,已经介绍了如何用深度学习框架中的旧方法来构建计算图。这一节将讨论一个Relay的一个新的构造-let bindings。

Let binding被每一种高级的编程语言应用。在Relay中,他是一个拥有三个字段Let(var, value, body)的数据结构。当我们计算一个Let表达式时,我们首先计算value部分,然后将其绑定到var,最后在body表达式中返回计算结果。

我们可以使用一系列的Let绑定来构造一个逻辑上等效于数据流程序的程序,下面的代码示例显示了这个用法:
Let表达式构造和数据流程序等价的计算图

嵌套的Let Binding被称作A-normal形式,作为函数式编程语言中的常用IR。通过上面的图我们可以发现虽然这两个程序的语义完全等价,它们的文本表示也一样(除了A-norm形式有let的前缀),但AST抽象语法树却不一样。

由于程序的优化使用了这些AST数据结构并对其进行了变换,这两种不同的结构会影响到最终编译器生成的代码。比如,我们想要检测add(log(x), y)这个模式。在数据流程序中,我们可以首先进入add节点,然后直接检查它的第一个参数是不是log。而在A-form的程序中,我们不能直接检查任何东西,因为add节点的输入是%v1-我们需要维护一个映射表将变量和它绑定的值进行映射,然后查表才知道%v1代表的是log。

0x2.4 为什么我们可能需要Let Binding

Let Binding的一种关键用法是它可以指定计算的scope。我们看一下下面这个没有使用Let Binding的例子:
在这里插入图片描述

当我们尝试在该在哪里计算%1节点时,问题就来了。特别的是,虽然文本格式似乎建议我们应该在if的scope之外计算节点%1,但AST却不建议这样做。实际上数据流图永远不会定义它的计算scope,这在语义上产生了一些歧义。

当我们有闭包时,这种歧义更加有趣,考虑下面的程序,该程序返回一个闭包。我们不知道在哪里计算%1,它可以在闭包的内部和外部。

fn (%x) {
  %1 = log(%x)
  %2 = fn(%y) {
    add(%y, %1)
  }
  %2
}

Let Binding解决了这些问题,因为值的计算发生在let节点上。在这两个程序中,如果将%1 = log(%x)改成let %v1 = log(%x),则我们将计算位置明确指定为if scope和闭包之外。可以看到Let Binding为计算端提供了更精确的范围,并且在生成后端代码时会很有用(因为这种范围在IR中)。

另一方面,没有指定计算scope的数据流形式也有其自身的优势,我们不需要担心在生成代码时将let放到哪里。数据流格式还为后面决定将计算节点放到哪里的Passes提供了更大的自由度。因此,在优化的初始阶段如果发现数据流形式还是挺方便的,那么使用数据流图的编码方法可能不是一个坏主意。目前在Relay中也实现了很多针对数据流图的优化方式。

但是,当我们将IR lower到实际的运行时程序时,我们需要精确的计算scope。 特别是当我们使用子函数和闭包时,我们要明确指定计算scope应在哪里发生。 在后期执行特定的优化中,可以使用Let Binding来解决此问题。

0x2.5 对IR转换的影响

希望到目前为止,你们已经熟悉两种表示形式。 大多数函数式编程语言都以A-normal形式进行分析,分析人员无需注意表达式是DAG。

Relay选择同时支持数据流形式和Let Binding。TVM相信让框架开发者选择熟悉的表达形式很重要。但是这确实对我们写通用的Passes产生了一些影响。由于这里还没介绍Passes,以及对Passes理解不深并且我没有使用过Let表达式来构建网络,就不继续介绍具体有哪些影响了。

详细内容可以参考:https://tvm.apache.org/docs/dev/relay_intro.html#let-binding-and-scopes

0x3. 基于Relay构建一个自定义的神经网络示例

我们基于Relay的接口定义一个Conv+BN+ReLU的小网络,展示一下Relay接口应该如何使用,这里TVM版本是0.8.0.dev,代码如下:

#coding=utf-8
import tvm
from tvm import relay
import numpy as np
from tvm.contrib import graph_executor

# 构造BN
def batch_norm(data,
                     gamma=None,
                     beta=None,
                     moving_mean=None,
                     moving_var=None,
                     **kwargs):
    name = kwargs.get("name")
    kwargs.pop("name")
    if not gamma:
        gamma = relay.var(name + "_gamma")
    if not beta:
        beta = relay.var(name + "_beta")
    if not moving_mean:
        moving_mean = relay.var(name + "_moving_mean")
    if not moving_var:
        moving_var = relay.var(name + "_moving_var")
    return relay.nn.batch_norm(data,
                               gamma=gamma,
                               beta=beta,
                               moving_mean=moving_mean,
                               moving_var=moving_var,
                               **kwargs)[0]

# 构造卷积
def conv2d(data, weight=None, **kwargs):
    name = kwargs.get("name")
    kwargs.pop("name")
    if not weight:
        weight = relay.var(name + "_weight")
    return relay.nn.conv2d(data, weight, **kwargs)


# 构造卷积+BN+ReLU的simpleNet
def simplenet(data, name, channels, kernel_size=(3, 3), strides=(1, 1),
               padding=(1, 1), epsilon=1e-5):
    conv = conv2d(
        data=data,
        channels=channels,
        kernel_size=kernel_size,
        strides=strides,
        padding=padding,
        data_layout='NCHW',
        name=name+'_conv')
    bn = batch_norm(data=conv, epsilon=epsilon, name=name + '_bn')
    act = relay.nn.relu(data=bn)
    return act

data_shape = (1, 3, 224, 224)
kernel_shape = (32, 3, 3, 3)
dtype = "float32"
data = relay.var("data", shape=data_shape, dtype=dtype)
act = simplenet(data, "graph", 32, strides=(2, 2))
func = relay.Function(relay.analysis.free_vars(act), act)

print(func)

np_data = np.random.uniform(-1, 1, (1, 3, 224, 224))

params = {
    "graph_conv_weight": tvm.nd.array(np.random.uniform(-1, 1, (32, 3, 3, 3)).astype(dtype)),
    "graph_bn_gamma": tvm.nd.array(np.random.uniform(-1, 1, (32)).astype(dtype)),
    "graph_bn_beta": tvm.nd.array(np.random.uniform(-1, 1, (32)).astype(dtype)),
    "graph_bn_moving_mean": tvm.nd.array(np.random.uniform(-1, 1, (32)).astype(dtype)),
    "graph_bn_moving_var": tvm.nd.array(np.random.uniform(-1, 1, (32)).astype(dtype)),
}

with tvm.transform.PassContext(opt_level=3):
    lib = relay.build(func, "llvm", params=params)

dev = tvm.cpu(0)
dtype = "float32"
m = graph_executor.GraphModule(lib["default"](dev))
# set inputs
m.set_input("data", tvm.nd.array(np_data.astype(dtype)))
# execute
m.run()
# get outputs
tvm_output = m.get_output(0)
就是一个很常规的过程,创建Relay Function,然后将所有的OP的权重信息用params这个字典存起来,注意这里的权重信息是随机初始化的。在编译Relay IR之前可以先看一下优化前的IR长什么样:

fn (%data: Tensor[(1, 3, 224, 224), float32], %graph_conv_weight, %graph_bn_gamma, %graph_bn_beta, %graph_bn_moving_mean, %graph_bn_moving_var) {
  %0 = nn.conv2d(%data, %graph_conv_weight, strides=[2, 2], padding=[1, 1, 1, 1], channels=32, kernel_size=[3, 3]);
  %1 = nn.batch_norm(%0, %graph_bn_gamma, %graph_bn_beta, %graph_bn_moving_mean, %graph_bn_moving_var);
  %2 = %1.0;
  nn.relu(%2)
}

符合我们第二节介绍的规则,Relay IR时一个函数。

0x4. 初识Pass

在上面构造simplenet的代码中,relay.build外部包了一层tvm.transform.PassContext,如下:

with tvm.transform.PassContext(opt_level=3):
    lib = relay.build(func, "llvm", params=params)

实际上tvm.transform.PassContext这个接口就定义了Pass,如文档所示:
tvm.transform.PassContext用来控制对relay IR使用哪些Pass进行优化

Pass是TVM中基于Relay IR进行的一系列优化,类似于onnx-simplifier里面用到的onnxoptimizer,它可以简化计算图,去除一些冗余的算子,提高模型的推理效率。TVM将所有的pass都抽象到了tvm/include/tvm/ir/transform.h这个文件中,主要包含PassContext,PassInfo,Pass,以及Sequential。

这里的PassContext即是上面Python接口对应的C++实现,它包含了Pass执行依赖的一些参数如优化level,依赖的其它特定Pass以及设置不使用某种指定Pass等。PassInfo是用来记录Pass信息的类,包含Pass的opy_level,name,以及当前Pass需要哪些前置Pass。而Pass这个类就执行pass的主体,这是一个基类,每种Pass具体的C++代码实现在tvm/src/relay/transforms中,它们都会继承Pass这个基类。最后,Sequential是一个container,装载所有Pass。

需要说明一下,不是所有的Pass都定义在tvm/src/relay/transforms这里,比如下面的第一个例子就在tvm/src/relay/backend/vm这个文件夹里。接下来我们将几个Pass的例子,看看它到底对Relay IR做了什么?

RemoveUnusedFunctions

首先来看一下定义在tvm/src/relay/backend/vm/removed_unused_funcs.cc这里的RemoveUnusedFunctions 这个pass,核心的代码实现如下:

void VisitExpr_(const FunctionNode* func_node) final {
    auto func = GetRef<Function>(func_node);
    if (visiting_.find(func) == visiting_.end()) {
      visiting_.insert(func);
      for (auto param : func_node->params) {
        ExprVisitor::VisitExpr(param);
      }
      ExprVisitor::VisitExpr(func_node->body);
    }
  }

IRModule RemoveUnusedFunctions(const IRModule& module, Array<runtime::String> entry_funcs) {
  std::unordered_set<std::string> called_funcs{};
  for (auto entry : entry_funcs) {
    auto funcs = CallTracer(module).Trace(entry);
    called_funcs.insert(funcs.cbegin(), funcs.cend());
  }
  auto existing_functions = module->functions;
  for (auto f : existing_functions) {
    auto it = called_funcs.find(f.first->name_hint);
    if (it == called_funcs.end()) {
      module->Remove(f.first);
    }
  }
  return module;
}

比较容易看出这个pass就是去除Relay IR中的冗余节点,VisitExpr_这个函数就是完成了一个图的遍历,然后把没有遍历到的节点删掉。删除发生在RemoveUnusedFunctions这个函数中。

ToBasicBlockNormalForm

这个Pass实现在tvm/src/relay/transforms/to_basic_block_normal_form.cc,代码实现如下:

Expr ToBasicBlockNormalFormAux(const Expr& e) {
  // calculate all the dependency between nodes.
  support::Arena arena;
  DependencyGraph dg = DependencyGraph::Create(&arena, e);
  /* The scope of the whole expr is global.
   * The scope of any subexpr, is the lowest common ancestor of all incoming edge.
   * We also record the set of expressions whose scope is lifted.
   */
  std::pair<NodeScopeMap, ExprSet> scopes = CalcScope(dg);
  return Fill::ToBasicBlockNormalForm(e, dg, &scopes.first, &scopes.second);
}

IRModule ToBasicBlockNormalForm(const IRModule& mod) {
  DLOG(INFO) << "ToBBlock:" << std::endl << mod;

  tvm::Map<GlobalVar, Function> updates;
  auto funcs = mod->functions;
  for (const auto& it : funcs) {
    ICHECK_EQ(FreeVars(it.second).size(), 0) << "Expected no free variables";
    if (const auto* n = it.second.as<FunctionNode>()) {
      if (n->GetAttr<String>(attr::kCompiler).defined()) continue;
    }
    Expr ret = TransformF([&](const Expr& e) { return ToBasicBlockNormalFormAux(e); }, it.second);
    updates.Set(it.first, Downcast<Function>(ret));
  }

  for (auto pair : updates) {
    mod->Add(pair.first, pair.second, true);
  }

  DLOG(INFO) << "ToBBlock: transformed" << std::endl << mod;

  return mod;
}

bool BasicBlockNormalFormCheck(const Expr& e) {
  // calculate all the dependency between nodes.
  support::Arena arena;
  DependencyGraph dg = DependencyGraph::Create(&arena, e);
  std::pair<NodeScopeMap, ExprSet> scopes = CalcScope(dg);
  for (auto expr : scopes.second) {
    LOG(FATAL) << "The expression below violates the basic block normal form in that "
               << "its scope should be lifted:\n"
               << expr;
  }
  return scopes.second.size() == 0;
}

ToBasicBlockNormalForm这个函数通过遍历Relay IR中的function,将每个function转换为基本块形式(即ToBasicBlockNormalFormAux这个函数),ToBasicBlockNormalFormAux这个函数分成以下几个部分:

  • 调用DependencyGraph dg = DependencyGraph::Create(&arena, e)创建一个DependencyGraph,这个数据结构是一个表达式相互依赖的图结构。
  • 通过std::pair<NodeScopeMap, ExprSet> scopes = CalcScope(dg)计算每个节点的scope,这个scope可以简单理解为由跳转指令如Ifnode,FunctionNode,LetNode等隔开的那些子图,因为一旦碰到这些节点在上面通过Relay Function创建DependencyGraph就会为这种节点分配一个new_scope标志。然后CalcScope这个函数具体做了哪些事情,我们需要跟进去看一下:
std::pair<NodeScopeMap, ExprSet> CalcScope(const DependencyGraph& dg) {
  NodeScopeMap expr_scope;
  ExprSet lifted_exprs;
  std::unordered_map<DependencyGraph::Node*, Expr> node_to_expr;
  // 首先让每个节点都属于一个单独的scope
  for (auto expr_node : dg.expr_node) {
    node_to_expr[expr_node.second] = expr_node.first;
  }
  bool global_scope_used = false;
  Scope global_scope = std::make_shared<ScopeNode>();
  // 使用LCA算法来更新每个节点的真正scope
  for (auto it = dg.post_dfs_order.rbegin(); it != dg.post_dfs_order.rend(); ++it) {
    DependencyGraph::Node* n = *it;
    auto iit = n->parents.head;
    Scope s;
    if (iit == nullptr) {
      ICHECK(!global_scope_used);
      s = global_scope;
      global_scope_used = true;
    } else {
      s = expr_scope.at(iit->value);
      const auto original_s = s;
      iit = iit->next;
      for (; iit != nullptr; iit = iit->next) {
        s = LCA(s, expr_scope.at(iit->value));
      }
      if (s != original_s && node_to_expr.find(n) != node_to_expr.end()) {
        // filter out exprs whose scope do not matter
        Expr expr = node_to_expr[n];
        if (!expr.as<OpNode>()) {
          lifted_exprs.insert(expr);
        }
      }
    }
    if (n->new_scope) {
      auto child_scope = std::make_shared<ScopeNode>(s);
      expr_scope.insert({n, child_scope});
    } else {
      expr_scope.insert({n, s});
    }
  }
  ICHECK(global_scope_used);
  return std::make_pair(expr_scope, lifted_exprs);
}

这个函数首先让每个节点都属于一个单独的scope,然后使用LCA算法来更新每个节点的真正scope。这里简单介绍一下LCA算法以及这里具体是如何求取每个节点的scope的。

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。为了方便,我们记某点集 [公式] 的最近公共祖先为 [公式] 或 [公式]。LCA有以下性质,引自OI-wiki:
在这里插入图片描述

其实不看这个性质也没关系,了解LCA可以求图中两个节点的最近公共祖先即可。然后CalcScope这个函数的具体思路就是先将每个节点初始化为一个单独的scope,然后按照后DFS序遍历这些节点,对于每一个遍历到的节点(这里记作n),看一下它的父亲节点iit是否存在,如果不存在则说明当前节点是根节点,它的scope应该为global_scope。如果iit存在,那么遍历iit的子节点,看一下这些节点的scope的LCA表达式,如果这个通过LCA求出来的表达式和iit节点的表达式完全相同,说明这个子图和当前节点是属于同一个scope的,否则就将当前节点插入到lifted_exprs,lifted_exprs是一个集合用来保存这个DependencyGraph里面的那些跳转指令节点,这也是为什么上面再插入节点到lifted_exprs之前需要判断一下这个节点的类型是否为OpNode。另外如果当前枚举的节点有new_scope标志,说明当前节点属于一个新的scope,需要为当前节点分配新的类型为ScopeNode的一个智能指针。

通过上面的算法,DependencyGraph中的节点和scope节点的关系就被映射到了一个map中,并且scope节点也被建立起了一个树结构。最后调用这个Fill::ToBasicBlockNormalForm(e, dg, &scopes.first, &scopes.second);来创建一个Fill类,这个类包含了DependencyGraph以及scope相关的信息,通过ToBasicBlockNormalForm成员函数实现基本块转换。它的实现在tvm/src/relay/transforms/to_a_normal_form.cc这个文件中,没有看得太懂,感兴趣的读者可以自己跟进来看一下,知乎的moon博主对这个Pass也做了解释,这里引用一下:

它(ToBasicBlockNormalForm)的基本逻辑通过VisitExpr函数遍历dependency节点,将具有相同scope的节点压入到同一个let_list中。Let_list文档中是这样解释的:

/*!
 * \file let_list.h
 * \brief LetList record let binding and insert let expression implicitly.
 *  using it, one can treat AST as value instead of expression,
 *  and pass them around freely without fear of AST explosion (or effect duplication).
 *  for example, if one write 'b = a + a; c = b + b; d = c + c', the AST will contain 8 'a'.
 *  if one instead write 'b = ll.Push(a + a); c = ll.Push(b + b); d = ll.Get(c + c);',
 *  the AST will contain 2 'a', as b and c are now variables.

Let_list使得抽象语法树简洁化,不会因为变量的复制导致树的爆炸。具有相同的scope的expr被约束到相同的let_list中,用一个var来表达,这样就将表达式转化为var的形式。一个var也就对应了一个基本块。

EliminateCommonSubexpr
最后再看一个消除公共子表达式的Pass,所谓公共子表达式指的就是具有相同的OP类型以及相同的参数,并且参数的顺序都是完全相同的,那么这些表达式就可以合成一个公共子表达式。举个例子:

a = b + c d = b + c

可以看到这两个表达式时完全一致的,那么经过这个Pass之后计算图就会消除其中一个表达式。代码实现在:tvm/src/relay/transforms/eliminate_common_subexpr.cc。这里定义了一个CommonSubexprEliminator类,这个类重载了两个Rewrite_函数来对expr进行遍历和重写。代码实现如下:

Expr Rewrite_(const CallNode* call, const Expr& post) final {
    static auto op_stateful = Op::GetAttrMap<TOpIsStateful>("TOpIsStateful");
    Expr new_expr = post;
    const CallNode* new_call = new_expr.as<CallNode>();
    ICHECK(new_call);
    const OpNode* op = new_call->op.as<OpNode>();
    StructuralEqual attrs_equal;

    if (new_call->args.size() == 0 || op == nullptr || op_stateful.get(GetRef<Op>(op), false)) {
      return new_expr;
    }
    if (fskip_ != nullptr && fskip_(new_expr)) {
      return new_expr;
    }

    auto it = expr_map_.find(new_call->op);
    if (it != expr_map_.end()) {
      for (const Expr& candidate_expr : it->second) {
        if (const CallNode* candidate = candidate_expr.as<CallNode>()) {
          bool is_equivalent = true;
          // attrs匹配
          if (!attrs_equal(new_call->attrs, candidate->attrs)) {
            continue;
          }
          // args匹配
          for (size_t i = 0; i < new_call->args.size(); i++) {
            if (!new_call->args[i].same_as(candidate->args[i]) &&
                !IsEqualScalar(new_call->args[i], candidate->args[i])) {
              is_equivalent = false;
              break;
            }
          }
          if (!is_equivalent) continue;
          return GetRef<Call>(candidate);
        }
      }
    }
    expr_map_[new_call->op].push_back(new_expr);
    return new_expr;
  }

可以看到大概的思路就是利用expr_map_这个std::unordered_map<Expr, std::vector, ObjectPtrHash, ObjectPtrEqual> expr_map_;来映射遍历过的具有相同op的expr,然后每次碰到相同op的表达式都会对已经记录的expr进行匹配,匹配不仅包含OP的attrs属性还包含参数列表,如果它们完全一样说明这两个表达式就是公共表达式,就不返回新的表达式。这样就可以去掉Relay Function中的公共表达式了。

到这里可能还不是特别清楚我们最开始加载的那个simplenet的Relay Function经过一些Pass之后具体变成什么样,我其实目前也还没搞清楚这个问题,这个问题应该就需要留到后面再解答了。

0x5. 小结

这篇文章主要介绍了一下TVM的Relay并介绍了如何基于Relay构建一个Conv+BN+ReLU的小网络,然后介绍了一下TVM中的Pass的工作机制,并较为详细的介绍了RemoveUnusedFunctions,ToBasicBlockNormalForm,EliminateCommonSubexpr三种Pass。其中Relay部分的详细介绍大部分引用自官方文档:https://tvm.apache.org/docs/tutorials/get_started/introduction.html。

0x6. 参考资料

https://zhuanlan.zhihu.com/p/358437531
https://zhuanlan.zhihu.com/p/91283238
https://tvm.apache.org/docs/tutorials/get_started/introduction.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【从零开始学深度学习编译器】五,TVM Relay以及Pass简介 的相关文章

  • TVM优化原理学习

    没有看论文 xff0c 看了b站陈天奇的视频 xff0c 还有一些博客的分析 xff0c 学习了优化原理 后续需要深入理解再看论文 TVM对于神经网络的优化主要有两部分 xff0c 计算图优化和算子优化 xff0c 下面分开说明 计算图优化
  • Windows安装TVM详细教程及报错提示

    Windows中安装TVM xff0c 网上有各种教程 xff0c 但是由于某些原因 xff0c 总是无法安装成功 xff0c 这大概率是各种工具之间的版本不兼容原因 注 xff1a 虽然本博客是为了教大家在windows中安装tvm xf
  • TVM运行时系统

    本文翻译自TVM Runtime System tvm 0 9 dev0 documentation TVM的编译器栈开发和部署支持多种编程语言 在本文中 xff0c 我们将解释TVM运行时的关键元素 我们需要满足一些有趣的需求 部署 从p
  • 【从零开始学深度学习编译器】五,TVM Relay以及Pass简介

    TVM Relay以及Pass简介 0x0 介绍0x2 Relay介绍0x2 1 使用Relay建立一个计算图0x2 2 Module xff1a 支持多个函数 xff08 Graphs xff09 0x2 3 Let Binding an
  • TVM在Windows10下编译安装

    本教程记录了Windows端安装tvm的过程 xff0c 欢迎交流 教程参考 TVM Windows下全功能编译方法 xff1a 从入门到劝退 https blog csdn net znsoft article details 11503
  • 【KAWAKO】TVM-在ubuntu服务器上的安装

    目录 下载源码 安装依赖库 修改config cmake文件 编译 安装python库 添加tophub 简单验证 官方的安装教程在这里 下载源码 从Download Page中下载下来的源码是0 8版本的 xff0c 亲测该版本不可用 x
  • 面试时不懂得自我介绍的人,最后都被PASS掉了

    做硬件维护的阿润最近后悔得直跳脚 在被公司赔偿清退后 xff0c 他直接开启了HIGH玩模式 xff0c 原计划先玩2个月再做面试准备 xff0c 结果冷不丁接到心仪公司的面试邀约 因为时间紧 邀约急 xff0c 在没做好充分准备的前提下
  • Python中pass的作用

    空语句 do nothing 保证格式完整 保证语义完整 以if语句为例 在c或c java中 if true do nothing else do something 对应于python就要这样写 if true pass do noth
  • TVM 0.9 在 ubuntu(任意版本)上的安装(简单且保姆级!)

    近一年来尝试过TVM在ubuntu16 04 ubuntu18 04 ubuntu20 04 以及windows上的安装 也看了官方教程和网上各种博客 踩坑无数 现在总结在Ubuntu上踩坑几率最小的安装流程如下 建议学习TVM一开始就在u
  • TVM 从入门到精通

    本文首发自 公众号 HyperAI超神经 内容一览 TVM 共有三种安装方法 从源码安装 使用 Docker 镜像安装和 NNPACK Contrib 安装 本文讲解如何通过 Docker 镜像 和 NNPACK Contrib 安装 关键
  • 编译 MXNet 模型

    本篇文章译自英文文档 Compile MXNet Models 作者是 Joshua Z Zhang Kazutaka Morita 更多 TVM 中文文档可访问 TVM 中文站 本文将介绍如何用 Relay 部署 MXNet 模型 首先安
  • 【开发环境】Windows下搭建TVM编译器

    关于搭建TVM编译器的官方文档 Install from Source tvm 0 14 dev0 documentation apache org 1 安装Anaconda 首先我们需要安装Anaconda 因为其中包含着我们所需要的各类
  • TVM概述

    TVM TVM是陈天奇领导的一个DL加速框架项目 它处于DL框架 如tensorflow pytorch 和硬件后端 如CUDA OpenCL 之间 兼顾了前者的易用性和后者的执行效率 官网 https tvm apache org 代码
  • 【TVM 学习资料】用 Schedule 模板和 AutoTVM 优化算子

    本篇文章译自英文文档 Optimizing Operators with Schedule Templates and AutoTVM 作者是 Lianmin Zheng Chris Hoge 更多 TVM 中文文档 访问 TVM 中文站
  • TVM编译安装

    本文是在已大致了解tvm功能和架构的基础上 根据官方文档 在x86 64 Ubuntu 18 04 GPU Pytorch1 8 0上安装TVM的流程 参考文档 TVM安装官方文档 llvm下载链接 1 下载源代码 从tvm官网下载源代码
  • TVM Relay softmax算子调度分析

    https github com apache tvm pull 8909 files 计算 调度分离 TOPI中的算子分为其算子的定义和算子的schedule两部分 算子的定义是唯一的 而对于不同的后端 x86 cuda等 schedul
  • TVM系列---1.开始使用Tensor Expression

    Author Tianqi Chen https docs tvm ai tutorials tensor expr get started html Tensor Expression入门 这是TVM中Tensor表达语言的入门教程 TV
  • Rails 5.1 capistrano 在 webpacker 编译资产时陷入困境

    我正在尝试使用 capistrano 部署 Rails 5 1 应用程序 但它似乎陷入了编译资产的困境 02 38 deploy assets precompile 01 HOME rbenv bin rbenv exec bundle e
  • return 语句中的解构[重复]

    这个问题在这里已经有答案了 我的应用程序中有多个案例 如下所示 getVariables const allowCustomValues budgets budgetsToAdd budgetsToRemove isGlobal isReq
  • 给定一组 GraphQL 变量类型,是否可以使用客户端模式为该组中的每种类型创建所有有效值的映射

    标题主要说明了一切 我正在构建一个反应 中继应用程序 它将允许用户在运行时动态创建图表 显示指定时间范围内的各种收入流 该图表的一个特点是用户能够指定每个收入流的采样间隔 例如 YEAR QUARTER MONTH WEEK等 作为每个流的

随机推荐

  • 阿里云-轻量应用服务器-Ubuntu-mysql安装-mysql外连配置-远程连接mysql

    按照本文的步骤 xff0c 能让你在外面 xff0c 用连接上的阿里云轻应用服务器 傻瓜式步骤演示 1 服务器防火墙设置 1 xff09 点击打开 轻量级应用服务器控制台 安全 防火墙 2 xff09 观察红框内有没有MYSQL设置 如果没
  • 阿里云-轻量应用服务器-防火墙-ufw-gufw

    序言 在服务器上装了VNC和xfce 启动VNC服务后发现每过一段时间VNC就不能连接了 上网查询报错原因 发现是因为密码输入错误次数过多 龟龟 xff0c 网上这么多坏人的吗 为了使VNC一直启动的同时不会被别人乱输入密码导致停止服务 x
  • 以太网二层技术——VPLS详解

    目录 前言 xff1a xff08 由于时间关系 xff0c 本篇仅先写了关于VPLS相关 xff09 一 VPLS简介 xff1a 二 VPLS基本工作原理及步骤 xff1a 三 VPLS的报文转发过程 xff1a 四 VPLS的缺点 x
  • SR技术概述与基本概念(SR-BE&SR-TE)

    目录 一 SR背景 二 SR概述 xff1a SR具有如下特点 xff1a SR优势 xff1a 三 一些名词的基本概念 基本概念 xff1a Segment 基本概念 xff1a Segment ID 简称SID xff0c 用于标识se
  • 网络同步技术

    一 同步技术 时钟同步包括 xff1a 频率同步 和时间同步 频率同步要求 相同的时间间隔 xff0c 时间同步要求 时间的起始点相同 和 相同的时间间隔 二 同步以太网技术 xff08 频率同步 xff09 xff1a SyncE xff
  • 以太网虚拟专用网络的工作流程(重点3张表与4种路由)

    目录 一 前言 二 EVPN四种类型路由的作用 三 EVPN表项简介 xff1a xff08 重点 四 EVPN的工作流程分为两个阶段 xff1a 4 1 EVPN启动阶段 xff1a 4 2 EVPN流量转发 xff1a Type2 控制
  • 网络工程师Python入门学习笔记-01

    目录 一 Python编码规范 xff1a 1 1 符号 xff1a 1 2 注释 xff1a 1 3 代码缩进 xff1a 很重要 二 Python的函数与模块 三 Python的类与方法 3 1 类 3 2 telnetlib介绍 xf
  • NETCONF、RESTCONF和YANG

    目录 一 NETCONF RESTCONF和YANG是之间什么关系 xff1f 二 Netconf简介 2 1 一般使用工具 xff1a MG Soft 简介 三 Netconf YANG 原理与实践 3 1 NETCONF协议 3 2 Y
  • Telemetry原理

    Telemetry 是一种网络设备监控技术 xff0c 提供 周期采样网络设备内的统计数据和状态数据的能力 一 Telemetry概述 1 1 技术背景 xff1a 网络设备的统一监控和性能管理是运维平台的重要功能 xff0c 设备的监控数
  • 二三层网络设备封装与解封装原理

    1 寻址转发 xff08 寻址指的是寻找IP地址 xff09 路由表放在一个公共的地方 xff0c 比如主控板上 xff0c 由主控板 的CPU运行路由协议 xff0c 计算路由 xff0c 生成和维护路由表 转发表与路由表 xff1a 转
  • BLE外围设备在Advertising中添加ServiceData

    startAdvertising失败 xff0c errorCode 61 1 AdvertiseCallback ADVERTISE FAILED DATA TOO LARGE errorCode解释 xff1a Failed to st
  • SBFD(Seamless Bidirectional Forwarding Detection)

    SBFD简介 xff1a 更适合SR隧道 BFD进行大量链路检测 时 xff0c 其状态机的协商时间会变长 xff0c 不适合Segement Routing SBFD xff08 Seamless Bidirectional Forwar
  • 使用CCProxy+Proxifier实现代理

    目录 1 使用场景2 什么是网络代理 xff1f 3 CCProxy3 1 说明3 2 下载安装3 3 使用说明 4 Proxifier4 1 说明4 2 下载安装4 3 使用说明4 4 Proxifier CPU占用率高问题解决 1 使用
  • 5G/NR PDSCH之频域资源分配

    物理下行共享信道 Physical Downlink Shared CHannel PDSCH 是无线通信系统中物理下行信道的一种 xff0c 用于传输下行用户数据 而在PDSCH进行下行数据传输时 xff0c 是需要基站给下行数据指定分配
  • NMS详解及pytorch实现:hard-nms(diou\overlap\merge\batched),soft-nms

    文章目录 NMS详解及pytorch实现 hard nms diou overlap merge batched soft nms1 简介2 原理3 实现3 1 伪代码3 2 pytorch源码3 3 知识点 参考资料 NMS详解及pyto
  • Deep-Sort多目标追踪算法代码解析

    Deep SORT是多目标跟踪 Multi Object Tracking 中常用到的一种算法 xff0c 是一个Detection Based Tracking的方法 这个算法工业界关注度非常高 xff0c 在知乎上有很多文章都是使用了D
  • RTFNet:基于可见光/红外图像的城市自动驾驶道路场景语义分割

    论文下载 xff1a https yuxiangsun github io pub RAL2019 rtfnet pdf 代码下载 xff1a https github com yuxiangsun RT 简介 xff1a 语义分割是自动驾
  • 红黑树的查找时间复杂度O(logn)

    红黑树查找时间复杂度 如果二叉排序树是平衡的 xff0c 则n个节点的二叉排序树的高度为Log2n 43 1 其查找效率为O Log2n xff0c 近似于折半查找 如果二叉排序树完全不平衡 xff0c 则其深度可达到n xff0c 查找效
  • C++版本OpenCv教程(三十五 )Laplacian算子

    上述的边缘检测算子都具有方向性 xff0c 因此需要分别求取X方向的边缘和Y方向的边缘 xff0c 之后将两个方向的边缘综合得到图像的整体边缘 Laplacian算子具有各方向同性的特点 xff0c 能够对任意方向的边缘进行提取 xff0c
  • 【从零开始学深度学习编译器】五,TVM Relay以及Pass简介

    TVM Relay以及Pass简介 0x0 介绍0x2 Relay介绍0x2 1 使用Relay建立一个计算图0x2 2 Module xff1a 支持多个函数 xff08 Graphs xff09 0x2 3 Let Binding an