向Relay添加算子(operator)
为了在Relay IR中使用TVM算子,需要在Relay中注册算子,以确保将其集成到Relay的类型系统中。
注册算子**需要三个步骤:
- 在C++中使用宏
RELAY_REGISTER_OP
注册算子的arity(参数个数)和类型信息 - 定义一个C++函数为算子生成一个调用节点CallNode,并为该函数注册一个Python API钩子
- 将上述Python API钩子包装在更简洁的接口中
文件src/relay/op/tensor/binary.cc提供了前两个步骤的示例,文件python/tvm/relay/op/tensor.py
提供了最后一个步骤的示例。
注册算子
TVM有算子注册表,但是如果没有其它类型信息,Relay无法正确融合TVM算子。
为了在注册算子时具有更大的灵活性,并在Relay中表达类型时提高表达能力和粒度,使用输入和输出类型之间的关系来对算子进行类型化。这些关系表示为接受输入类型和输出类型列表(这些类型中的任何一个都不完整)并返回满足该关系的输入和输出类型列表的函数。本质上,算子的关系除了计算输出类型外,还可以强制执行所有必要的类型规则(即,通过检查输入类型)。
例如,请参阅src/relay/op/type_relations.h
及其实现(src/relay/op/type_relations.cc
)。例如,BroadcastRel
接受两个输入类型和一个输出类型,检查它们都是具有相同基础数据类型的张量类型,最后确保输出类型的shape是输入类型shape的广播。
bool BroadcastRel(const Array<Type>& types,
int num_inputs,
const Attrs& attrs,
const TypeReporter& reporter) {
CHECK_EQ(types.size(), 3);//2个输入类型和1个输出类型
// DLOG(INFO) << "In1:" << types[0] << ",In2:" << types[1]
// << ",Out:" << types[2] << std::endl;
if (auto t0 = ToTensorType(types[0])) {
if (auto t1 = ToTensorType(types[1])) {
CHECK_EQ(t0->dtype, t1->dtype);
reporter->Assign(types[2],
ConcreteBroadcast(t0, t1, t0->dtype));
return true;
}
}
return false;
}
如果现有的类型关系无法捕获所需算子的行为,则可能需要添加其他类型关系到type_relations.h。
C++中的RELAY_REGISTER_OP宏允许开发人员指定以下有关Relay中算子的信息:
- Arity(参数个数)
- 位置参数的名称和描述
- 支持级别(1表示内部固有;较大的数字表示不够完整或外部支持的算子)
- 算子的类型关系
下面的示例来自binary.cc
张量,并将其用于张量:
RELAY_REGISTER_OP("add")
.set_num_inputs(2) //参数个数
.add_argument("lhs", "Tensor", "The left hand side tensor.")
.add_argument("rhs", "Tensor", "The right hand side tensor.")
.set_support_level(1) //支持级别
.add_type_rel("Broadcast", BroadcastRel); // 类型关系
创建一个调用节点(Call Node)
此步骤仅需要简单地编写一个将参数带给算子的函数(作为Relay表达式),并将CallNode返回给算子(即,应将其放置在打算调用算子的Relay AST中的节点)。
当前不支持调用属性和类型参数(最后两个字段),因此足以用于Op::Get
从算子注册表中获取算子信息,并将参数传递给调用节点,如下所示。
TVM_REGISTER_API("relay.op._make.add")
.set_body_typed<Expr(Expr, Expr)>([](Expr lhs, Expr rhs) {
static const Op &op = Op::Get("add");
return CallNode::make(op, {lhs, rhs}, Attrs(), {});
});
引用一个Python API钩子
在Relay中通常习惯这么做:通过TVM_REGISTER_API导出的函数应该封装在单独的Python函数中,而不是在Python中直接调用。对于产生对算子的调用的函数,将它们绑定起来可能很方便,如python/tvm/relay/op/tensor.py中所示,其中都提供了张量上的元素算子。例如,以下是上一节的add函数在Python中的公开方式:
def add(lhs, rhs):
"""Elementwise addition.
Parameters
----------
lhs : relay.Expr
The left hand side input data
rhs : relay.Expr
The right hand side input data
Returns
-------
result : relay.Expr
The computed result.
"""
return _make.add(lhs, rhs)
请注意,这些Python包装器也可能是向算子提供更简单接口的好机会。例如,concat
算子被注册为仅接受一个参数,即一个由多个张量组合起来的元组,Python包装器将这些张量作为参数并将它们组合成一个元组,然后生成调用节点:
def concat(*args):
"""Concatenate the input tensors along the zero axis.
Parameters
----------
args: list of Tensor
Returns
-------
tensor: The concatenated tensor.
"""
tup = Tuple(list(args))
return _make.concat(tup)
**
梯度算子
梯度算子对于在Relay中编写可区分的程序很重要。尽管Relay的autodiff算法可以区分一流的语言结构,但算子是不透明的。由于Relay无法调查实现,因此必须提供明确的区分规则。
Python和C++均可用于编写梯度算子,但是我们将示例重点放在Python上,因为它更常用。
在Python中添加梯度
可以在python/tvm/relay/op/tensor_grad.py中找到Python梯度算子的集合 。我们将通过两个有代表性的示例:sigmoid
和multiply
。
@register_gradient(“sigmoid”)
def sigmoid_grad(orig, grad):
“””Returns [grad sigmoid(x) (1 - sigmoid(x))].”””
return [grad orig (ones_like(orig) - orig)]
这里的输入是原始算子orig
和要累加的梯度grad。返回的是一个列表list,其中索引i的元素是算子相对于第i个输入的导数。通常,梯度将返回一个列表,其中包含与基本算子输入相同数量的元素。
在进一步分析此定义之前,首先我们应该回顾一下 sigmoid 型函数的导数:
∂σ∂x=σ(x)(1−σ(x))
上面的定义看起来与数学定义相似,但是有一个重要的补充,我们将在下面进行描述。
术语orig (ones_like(orig) - orig)直接与导数匹配,因为orig是sigmoid 函数,但我们不仅对如何计算此函数的梯度感兴趣。我们有兴趣将此梯度与其他梯度组合,因此我们可以在整个程序中累积该梯度。这是术语grad出现的地方。在表达式grad orig (ones_like(orig) - orig)中,乘以grad 指出到目前为止如何用梯度组成导数。
现在,我们来看multiply
一个更有趣的示例:
@register_gradient(“multiply”)
*def multiply_grad(orig, grad):
“””Returns [grad y, grad x]”””_
x, y = orig.args
return [collapse_sum_like(grad y, x),
collapse_sum_like(grad x, y)]
在此示例中,返回列表中有两个元素,因为 multiply
是二进制算子。再回想一下,如果f(x,y)=xy,偏导数是
multiply 有一个必需的步骤,但不是 sigmoid的必要步骤。因为multiply具有广播语义,因此multiply
不需要 。由于grad的形状可能与输入的形状不匹配,因此我们习惯用collapse_sum_like获取术语grad * 的内容,并使形状与我们要区分的输入的形状相匹配。
在C++中添加梯度
在C++中添加梯度类似于在Python中添加梯度,但是注册的接口略有不同。
首先,请确保引用src/relay/pass/pattern_util.h
。它提供了用于在Relay AST中创建节点的辅助功能。然后,以类似于Python示例的方式定义梯度:
tvm::Array
const Call& call = orig_call.Downcast
return { CollapseSumLike(Multiply(output_grad, call.args[1]), call.args[0]),
CollapseSumLike(Multiply(output_grad, call.args[0]), call.args[1]) };
}
请注意,在C++中,我们不能使用与Python中相同的算子重载,并且需要向下转换,因此它的实现更为冗长。即使这样,我们仍可以轻松地验证此定义是否与Python中的先前示例相同。
现在,除了使用Python装饰器之外,我们还需要在基本算子注册的末尾添加set_attr来调用“ FPrimalGradient” ,以注册梯度。
RELAY_REGISTER_OP(“multiply”)
// Set other attributes
// …_
.set_attr
总结
- TVM算子可以使用表示合适类型信息的关系在relay中注册。
- 在relay中使用算子需要一个函数来为生成Call Node。
- 最好有一个简单的Python包装器来生成Call Node。