添加新的算子,以”nn.vacc_dropout”为例
算子名称说明:

  • 跟神经网络有关的,前面加上”nn.”,例如:_nn.conv2d, n.dense,nn.relu,nn.pad_等等
  • 其它不带”nn.”前缀的是一般的算子,例如:_round,clip,cast, sum,power,reshape_等等

    1 添加relay算子

    1.1 在c++中注册relay算子

    一般地,注册relay算子相关的c++文件在src/relay/op/目录下的某个子目录(没有强制要求)。可以新建一个文件,也可以直接在其它文件中添加。
    vacc_dropout算子对应的c++文件是src/relay/pass/vacc/hw_agnostic/convert_strides.cc,源码如下:

    1. RELAY_REGISTER_OP("nn.vacc_dropout") //算子名为"nn.vacc_dropout"
    2. .describe(R"code(Applies the dropout with H/W stride = 2 to the input array.
    3. Examples::
    4. x = [[ 1., 4., 7., 10.],
    5. [ 2., 5., 8., 11.],
    6. [ 3., 6., 9., 12.]]
    7. vacc_dropout(x) = [[1., 7.],
    8. [3., 9.]]
    9. x = [[[ 1., 2., 3.],
    10. [ 4., 5., 6.],
    11. [ 7., 8., 9.]],
    12. [[ 1., 2., 3.],
    13. [ 4., 5., 6.],
    14. [ 7., 8., 9.]],
    15. [[ 1., 2., 3.],
    16. [ 4., 5., 6.],
    17. [ 7., 8., 9.]]]
    18. vacc_dropout(x) = [[[ 1., 3.],
    19. [ 7., 9.]],
    20. [[ 1., 3.],
    21. [ 7., 9.]],
    22. [[ 1., 3.],
    23. [ 7., 9.]]]
    24. )code" TVM_ADD_FILELINE)
    25. .set_num_inputs(1)
    26. .add_argument("data", "Tensor", "The input tensor.")
    27. .set_support_level(4)
    28. .add_type_rel("VaccDropout", VaccDropoutRel)
    29. .set_attr<TOpPattern>("TOpPattern", kInjective);

    说明:注册算子使用的宏是:RELAY_REGISTER_OP
    C++中的RELAY_REGISTER_OP宏允许开发人员指定以下有关Relay中算子的信息:

  • set_num_inputs:设置输入参数个数
  • add_argument:添加输入参数的名称,类型和描述
  • set_attrs_type:设置属性类型
  • set_support_level:设置支持级别(1表示内部固有;较大的数字表示不够完整或外部支持的算子)
  • add_type_rel:算子的类型关系(输入与输出之间的类型关系)
  • set_attr:设置算子的其它属性

上面几个函数之所以能够一个接一个的采用.的方式连续调用,是因为他们都是OpRegistry的成员函数,而且这些函数的返回值都是OpRegistry&类型的,所以能够使用.的形式继续调用它的成员函数。这些成员函数如下:

set_num_inputs

设置输入参数的个数。

add_argument

添加输入参数的说明:名称,类型和描述。有几个输入参数,就添加几个说明。
大多数算子只有一个或2个输入,少数的有多个。例如:
cast, ``reshape``,``transpose,等只有一个输入;

  1. .set_num_inputs(1)
  2. .add_argument("data", "Tensor", "The input tensor.")

add,``subtract,``divide,``multiply,``power,``mod,``right_shift,``left_shift,``maximum,``minimum,等算子有2个输入:

  1. .set_num_inputs(2) \
  2. .add_argument("lhs", "Tensor", "The left hand side tensor.") \
  3. .add_argument("rhs", "Tensor", "The right hand side tensor.") \

set_support_level

设置支持级别(1表示内部固有;较大的数字表示不够完整或外部支持的算子)
自定义的算子,可以设置为3。

set_attrs_type

设置属性类型。如果算子的参数,除了输入参数之外,还有其它的属性参数,就应该设置属性的类型。

tvm已经定义了很多常用的属性。
c++端参考include/tvm/relay/attrs/
python端参考:python/tvm/relay/op/op_attrs.py

例如,case算子的参数,除了输入的tensor之外,还需要指定需要转换的数据类型,所以,需要一个转换类型属性(CastAttrs),该属性类至少包含一个数据类型参数。CastAttrs属性定义如下:
c++端:include/tvm/relay/attrs/transform.h

  1. /*! \brief data type cast */
  2. struct CastAttrs : public tvm::AttrsNode<CastAttrs> {
  3. DataType dtype;
  4. TVM_DECLARE_ATTRS(CastAttrs, "relay.attrs.CastAttrs") {
  5. TVM_ATTR_FIELD(dtype)
  6. .describe("Target data type");
  7. }
  8. }; // struct CastAttrs.

python端:python/tvm/relay/op/op_attrs.py

  1. @register_relay_attr_node
  2. class CastAttrs(Attrs):
  3. """Attributes for transform.cast"""

然后,算子cast的定义就设置了该类型,

  1. RELAY_REGISTER_OP("cast")
  2. ...
  3. .set_attrs_type<CastAttrs>()
  4. ...

如果已定义的属性类型中,没有符合要求的,就需要自定义一个属性类型,参考:自定义属性类

add_type_rel

给算子添加输入和输出之间的类型约束关系。
需要注意的是,add_type_rel()中的第二个参数是固定格式的类型相关的函数,tvm提供了一些常用的,例如:
AffineBroadcastRel
BroadcastCompRel
BroadcastRel
ConcatenateRel
IdentityCastRel
IdentityConcatenateRel
IdentityRel
除了以上几种,还可以自定义类型关系函数,该函数的一些格式如下:

  • 1 函数名以Rel结尾
  • 2 函数返回值为bool型
  • 3 函数的参数一共4个,依次是:
    • const Array& types, //包括输入和输出
    • int num_inputs, //输入参数的个数
    • const Attrs& attrs, //属性
    • const TypeReporter& reporter //包含类型约束的容器

例如,自定义的VaccDropoutRel如下:

  1. bool VaccDropoutRel(const Array<Type>& types,
  2. int num_inputs,
  3. const Attrs& attrs,
  4. const TypeReporter& reporter) {
  5. CHECK_EQ(types.size(), 2);//1个输入+1个输出,都是TensorType
  6. const auto* data = types[0].as<TensorTypeNode>();
  7. if (data == nullptr) return false;
  8. auto dshape = data->shape;
  9. auto num_axis = dshape.size();//张量的轴数(维度),例如4维的[N,C,H,W]
  10. std::vector<int64_t> stride_vec;//步长
  11. for (size_t i = 0; i < num_axis - 2; ++i) {
  12. stride_vec.push_back(1);
  13. }
  14. stride_vec.push_back(2);//H方向步长为2
  15. stride_vec.push_back(2);//W方向步长为2
  16. //计算输出张量的形状
  17. std::vector<IndexExpr> oshape(dshape.size());
  18. for (size_t i = 0; i < num_axis; ++i) {
  19. int64_t stride_v = stride_vec[i];
  20. const int64_t* p_dim_size = as_const_int(dshape[i]);
  21. CHECK(p_dim_size)
  22. << "vacc_dropout requires dimension to be concrete int";
  23. int64_t dim_size = p_dim_size[0];
  24. oshape[i] = make_const(dshape[i].dtype(), (dim_size + stride_v - 1) / stride_v);
  25. }
  26. reporter->Assign(types[1], TensorTypeNode::make(oshape, data->dtype));
  27. return true;
  28. }

主要是创建输入参数和输出参数的约束关系。

set_attr

set_attr用于设置算子的其它属性。例如:
.set_attr(“TOpPattern”, kOpaque)
.set_attr(“FTVMCompute”, DebugCompute)
.set_attr(“TOpIsStateful”, false)
.set_attr(“FInferCorrectLayout”,ElemwiseArbitraryLayout)
.set_attr(“TNonComputational”, true)
.set_attr(“AnyCodegenStrategy”, kVariableDimensions)
.set_attr(“FScaleAxisForwardPrep”, ReluForwardPrep)
.set_attr(“FScaleAxisForwardRewrite”, ReluForwardRewrite)
.set_attr(“FScaleAxisBackwardTransform”, ReluBackwardTransform)
.set_attr(“FMacCount”, ConvMacCount)
.set_attr(“FTVMQnnCanonicalize”, ConcatenateQnnCanonicalize)

.set_attr(“TOpPattern”, kInjective);
TOpPattern是比较常用的一个属性,用于在图融合时算子的级别。

.set_attr(“FTVMCompute”, DebugCompute)
用于注册计算(c++端),此处设置了计算,那么在python端就不用注册计算。

1.2 定义一个C++函数为算子生成一个CallNode,并为该函数注册一个Python API钩子

在c++中调用TVM_REGISTER_GLOBAL或TVM_REGISTER_API 注册一个API函数(该函数在第3步中被Python封装成一个简洁的API),该返回一个CallNode
例如

  1. Expr MakeVaccDropout(Expr data) {
  2. // 获取vacc_dropout算子,并生成一个该算子的CallNode
  3. static const Op& op = Op::Get("nn.vacc_dropout");
  4. return CallNode::make(op, {data}, Attrs{}, {});
  5. }
  6. TVM_REGISTER_API("relay.op._make.vacc_dropout")
  7. .set_body_typed(MakeVaccDropout);

说明:不一定非得写一个函数MakeVaccDropout, 也可以直接在TVM_REGISTER_API中写一个匿名函数。 例如:

  1. TVM_REGISTER_API("relay.op._make.vacc_dropout")
  2. .set_body_typed<Expr(Expr)>([](Expr data) {
  3. static const Op& op = Op::Get("nn.vacc_dropout");
  4. return CallNode::make(op, {data}, Attrs{}, {});
  5. });

注意:
RELAYREGISTER_OP注册新的算子,或设置相应的算子的属性
TVM_REGISTER_API注册一个全局的API函数,等效于 TVM_REGISTER_GLOBAL
#define TVM_REGISTER_API(_OpName
) TVM_REGISTER_GLOBAL(OpName)
TVM_REGISTER_API和的RELAY_REGISTER_OP进一步封装的宏还有:
RELAY_REGISTER_UNARY_OP
RELAY_REGISTER_BINARY_OP
RELAY_REGISTER_CMP_OP

  1. /*! Quick helper macro
  2. * - Expose a positional make function to construct the node.
  3. * - Register op to the registry.
  4. *
  5. * We make the decision to always only expose positional argument.
  6. * We will do rewrapping in the frontend to support language
  7. * sugars such as keyword arguments and default value.
  8. * \param OpName the name of registry.
  9. */
  10. #define RELAY_REGISTER_UNARY_OP(OpName) \
  11. TVM_REGISTER_API("relay.op._make." OpName) \
  12. .set_body_typed<Expr(Expr)>([](Expr data) { \
  13. static const Op& op = Op::Get(OpName); \
  14. return CallNode::make(op, {data}, Attrs(), {}); \
  15. }); \
  16. RELAY_REGISTER_OP(OpName) \
  17. .set_num_inputs(1) \
  18. .add_argument("data", "Tensor", "The input tensor.") \
  19. .add_type_rel("Identity", IdentityRel) \
  20. .set_attr<TOpPattern>("TOpPattern", kElemWise) \
  21. .set_attr<TOpIsStateful>("TOpIsStateful", false) \
  22. .set_attr<FInferCorrectLayout>("FInferCorrectLayout", \
  23. ElemwiseArbitraryLayout) \
  24. /*! Quick helper macro
  25. * - Expose a positional make function to construct the node.
  26. * - Register op to the registry.
  27. *
  28. * We make the decision to always only expose positional argument.
  29. * We will do rewrapping in the frontend to support language
  30. * sugars such as keyword arguments and default value.
  31. *
  32. * \param OpName the name of registry.
  33. */
  34. #define RELAY_REGISTER_BINARY_OP(OpName) \
  35. TVM_REGISTER_API("relay.op._make." OpName) \
  36. .set_body_typed<Expr(Expr, Expr)>([](Expr lhs, Expr rhs) { \
  37. static const Op& op = Op::Get(OpName); \
  38. return CallNode::make(op, {lhs, rhs}, Attrs(), {}); \
  39. }); \
  40. RELAY_REGISTER_OP(OpName) \
  41. .set_num_inputs(2) \
  42. .add_argument("lhs", "Tensor", "The left hand side tensor.") \
  43. .add_argument("rhs", "Tensor", "The right hand side tensor.") \
  44. .add_type_rel("Broadcast", BroadcastRel) \
  45. .set_attr<TOpPattern>("TOpPattern", kBroadcast) \
  46. .set_attr<TOpIsStateful>("TOpIsStateful", false) \
  47. .set_attr<FInferCorrectLayout>("FInferCorrectLayout", \
  48. BinaryBroadcastLayout)
  49. // Comparisons
  50. #define RELAY_REGISTER_CMP_OP(OpName) \
  51. TVM_REGISTER_API("relay.op._make." OpName) \
  52. .set_body_typed<Expr(Expr, Expr)>([](Expr lhs, Expr rhs) { \
  53. static const Op& op = Op::Get(OpName); \
  54. return CallNode::make(op, {lhs, rhs}, Attrs(), {}); \
  55. }); \
  56. RELAY_REGISTER_OP(OpName) \
  57. .set_num_inputs(2) \
  58. .add_argument("lhs", "Tensor", "The left hand side tensor.") \
  59. .add_argument("rhs", "Tensor", "The right hand side tensor.") \
  60. .add_type_rel("BroadcastComp", BroadcastCompRel) \
  61. .set_attr<TOpPattern>("TOpPattern", kBroadcast) \
  62. .set_attr<TOpIsStateful>("TOpIsStateful", false) \
  63. .set_attr<FInferCorrectLayout>("FInferCorrectLayout", \
  64. BinaryBroadcastLayout)

1.3 定义relay算子relay.vacc_dropout

即在Python中将第2步中公开的函数包装在更简洁的接口中
一般地,第2步中通过TVM_REGISTER_API 或 TVM_REGISTER_GLOBAL公开的函数,在python中不会被直接调用,而是被封装成一个单独的API。该文件一般在**python/tvm/relay/op/**目录下。
例如,在python/tvm/relay/op/tensor.py文件中如下定义:

  1. def vacc_dropout(data):
  2. """vacc_dropout dropout with H/W stride = 2 to the input array
  3. Parameters
  4. ----------
  5. data : tvm.relay.Expr
  6. The input tensor.
  7. Returns
  8. -------
  9. result : tvm.relay.Expr
  10. The shape tensor.
  11. """
  12. return _make.vacc_dropout(data)

该算子即为relay.vacc_dropout

说明:_make.``vacc_dropout就是在第2步中注册的全局函数relay.op._make.vacc_dropout
定义了vacc_dropout函数后,在python中会调用relay.``vacc_dropout就相当于直接调用全局函数relay.op._make.vacc_dropout。

1.4 在python端为算子注册compute, schedule

例如,在python/tvm/relay/op/_tensor.py中代码如下:

(1)注册算子的计算

  1. # 注册算子的计算
  2. @register_compute("nn.vacc_dropout")
  3. def vacc_dropout_compute(attrs, inputs, output_type, target):
  4. assert len(inputs) == 1
  5. print("relay/op: vacc dropout compute is called")
  6. // 返回值是[]封装的list, 如果topi.nn.vacc_dropout(inputs[0])的返回值是list,就不用封装成list
  7. // 直接返回topi.nn.vacc_dropout(inputs[0])即可
  8. return [topi.nn.vacc_dropout(inputs[0])]

注意:也可以在c++端为算子注册compute,,通过.set_attr来实现,例如:
.set_attr(“FTVMCompute”, DebugCompute)
在c++端和python端只能选其一。
注意:
1 如果是在c++端实现计算,那么该relay算子的计算不会调用topi中的算子实现计算。
2 如果是在python端实现计算,注意该返回值的类型是列表类型,用[]封装。如果topi….中定义的算子的返回值本身就是列表,那么在本函数中就不用再包装成一个列表。

(2)注册算子的调度

  1. # 注册算子的调度
  2. @register_schedule("nn.vacc_dropout")
  3. def vacc_dropout_schedule(attrs, outs, target):
  4. print("relay/op: vacc dropout schedule is called, output len %d"%(len(outs)))
  5. return topi.generic.schedule_extern(outs)

也可以直接通过如下方式来注册到已有的调度:
register_schedule
例如:register_schedule(“log”, schedule_broadcast)
schedule_broadcast是已定义的调度:schedule_broadcast = schedule_injective

  1. def schedule_injective(attrs, outputs, target):
  2. """Generic schedule for binary broadcast."""
  3. with target:
  4. return topi.generic.schedule_injective(outputs)

(3)注册pattern

register_pattern(“nn.relu”, OpPattern.ELEMWISE)
它等效于在c++中的.set_attr(“TOpPattern”, kElemWise);

自定义说明:
vacc_dropout_schedule是调度相关的,保持固定格式,只需修改下print内容即可。
vacc_dropout_compute是计算相关的,保持固定格式,需要自定义该函数,例如,topi.nn.vacc_dropout是在topi/python/topi/nn/vacc_op.py中定义的函数,见下一部分。

2 定义topi算子topi.nn.vacc_dropout

所谓通用计算,即没有指定特殊的device target (如cuda),采用cpu计算。
在第4步中注册计算中所调用的topi.nn.vacc_dropout, 定义在topi/python/topi/nn/vacc_op.py,代码如下:

  1. @tvm.target.generic_func
  2. def vacc_dropout(data):
  3. """vacc_dropout operator.
  4. Parameters
  5. ----------
  6. input : tvm.Tensor
  7. 4-D with shape [batch, in_channel, in_height, in_width]
  8. Returns
  9. -------
  10. output : tvm.Tensor
  11. 4-D with shape [batch, in_channel, out_height, out_width]
  12. """
  13. print("topi.nn.generic vacc_dropout")
  14. begin = []
  15. end = data.shape
  16. strides = []
  17. dims = len(data.shape)
  18. for i in range(dims):
  19. begin.append(0)
  20. if i < dims - 2:
  21. strides.append(1)
  22. else:
  23. strides.append(2)
  24. return cpp.strided_slice(data, begin, end, strides) //调用c++中的函数,或python中的函数

说明:@tvm.target.generic_func装饰器是定义在python/tvm/target.py

3 实现该算子的在自定义硬件设备上的计算

3.1 在vacc.py中注册计算的函数

例如,在vacc/python/vacc/top/vacc.py文件中如下代码(我们的硬件叫vacc,所有算子在vacc上的计算都在vacc/python/vacc/top/vacc.py中实现):

  1. @autotvm.register_topi_compute(topi.nn.vacc_dropout, 'vacc', 'direct')
  2. def _declaration_vacc_dropout(cfg, data):
  3. print("vacc.top.nn.vacc_dropout compute")
  4. return vacnn.vacc_dropout(data)

说明:
@autotvm.register_topi_compute:装饰器定义在/home/xxw/dev/tvm/python/tvm/autotvm/task/topi_integration.py
@autotvm.register_topi_compute: 第1个参数对应的是topi.nn.vacc_dropout,就是在第5步中定义的topi算子,2个参数是我们的硬件是vacc。@autotvm.register_topi_compute的作用就是在使用targert是vacc时,将topi.nn.vacc_dropout转到_declaration_vacc_dropout,也即是自定义的vacc的计算。
在此处会调用vacnn实现在vacc设备的计算:vacnn.vacc_dropout(data)。

3.2 在vacnn.py中定义计算的函数

vacnn.vacc_dropout是咱们自己的硬件vacc中实现的,该文件是python/tvm/contrib/vacnn.py,源码如下:

  1. def vacc_dropout(input):
  2. N, C, H, W = topi.util.get_const_tuple(input.shape)
  3. oH = (H + 1) // 2
  4. oW = (W + 1) // 2
  5. oshape = [N, C, oH, oW]
  6. return _api.extern(
  7. oshape, [input],
  8. lambda ins, outs: _intrin.call_packed(
  9. "tvm.contrib.vacnn.vacc_dropout",
  10. ins[0],
  11. outs[0]), name="vacc_dropout")

注意:该函数最后调用了一个ID为tvm.contrib.vacnn.vacc_dropout的函数。

3.3 在vacnn.cc中注册ID为tvm.contrib.vacnn.vacc_dropout的函数

  1. void VacnnDropout(TVMArgs args, TVMRetValue* ret) {
  2. DLTensor* src = args[0];
  3. DLTensor* dst = args[1];
  4. VACNNThreadEntry* entry_ptr = VACNNThreadEntry::ThreadLocal();
  5. vacnnHandle_t handle = entry_ptr->handle;
  6. auto* repo = entry_ptr->container_repo;
  7. auto* queue = reinterpret_cast<
  8. InstructionQueue<InstructionConvolution, InsnFieldsMatrixMani>*>(
  9. repo->GetContainer(kContainerQueue, kComputationMatrix, handle->core_num,
  10. true));
  11. vacnnTensorDescriptor_t t_desc;
  12. vacnnDataType_t src_data_type = vacnnDataType::DLTypeToVACNNType(src->dtype);
  13. for (int i = 0; i < handle->core_num; ++i) {
  14. handle->core_id = i;
  15. // Get descriptor for src
  16. int src_node_id = GetNodeId(src->data);
  17. std::vector<int64_t> src_shape =
  18. GetTensorShapeByCore(src, src_node_id, handle->core_id);
  19. VACNN_CALL(vacnnSetTensor4dDescriptor(
  20. &t_desc, vacnnTensorFormat_t(0), src_data_type, src->ndim,
  21. static_cast<int32_t>(src_shape[0]), static_cast<int32_t>(src_shape[1]),
  22. static_cast<int32_t>(src_shape[2]),
  23. static_cast<int32_t>(src_shape[3])));
  24. // Get address for src
  25. void* src_addr = GetDataAddress(src->data);
  26. // Get address for dst
  27. void* dst_addr = GetDataAddress(dst->data);
  28. InstructionConvolution* insn = queue->GetItem(i);
  29. VACNN_INSN_GEN(vacnnDropout(handle, insn, t_desc, src_addr, dst_addr));
  30. }
  31. // Push the queue to device runtime when instructions all generated.
  32. if (queue->FullLoaded()) {
  33. // VACNN_INSN_PUSH(pad_forward)
  34. VACNN_INSN_CACHE(dropout, VACNN_OP_DROPOUT, VACNN_ISA_CONVOLUTION)
  35. repo->ResetContainer();
  36. }
  37. }
  38. TVM_REGISTER_GLOBAL("tvm.contrib.vacnn.vacc_dropout")
  39. .set_body([](TVMArgs args, TVMRetValue* ret) { VacnnDropout(args, ret); });

而在VacnnDropout中,调用的很多函数都是NNlib中的定义的函数,例如vacnnDropout。

如果在NNlib中没有相关函数的实现,可以暂时使用模拟的函数替代。例如,vacc_slice算子的NNlib中的函数没有完成,就在vacnn_sim.cc中注册了一个ID为tvm.contrib.vacnn.vacc_slice_forward_sim的函数。

  1. TVM_REGISTER_GLOBAL("tvm.contrib.vacnn.vacc_slice_forward_sim")
  2. .set_body([](TVMArgs args, TVMRetValue *ret) {
  3. DLTensor *src = args[0];
  4. DLTensor *dst = args[5];
  5. printf("fake: vacnn.vacc_slice \n");
  6. std::cout << "vacc_slice_forward src:" << src->data << std::endl;
  7. std::cout << "vacc_slice_forward dst:" << dst->data << std::endl;
  8. memcpy(dst->data, src->data, 16 * 4);
  9. printf("dst[0] is: %f\n", ((float *)(dst->data))[0]);
  10. });

然后,在vacc.py中调用的是tvm.contrib.vacnn.vacc_slice_forward_sim

  1. def vacc_slice_forward(x, begin, end):
  2. n = len(x.shape)
  3. oshape = []
  4. for i in range(n):
  5. if i == n - 2:
  6. # H axis
  7. oshape.append(end[0] - begin[0])
  8. elif i == n - 1:
  9. # W axis
  10. oshape.append(end[1] - begin[1])
  11. else:
  12. oshape.append(x.shape[i])
  13. return _api.extern(
  14. oshape, [x],
  15. lambda ins, outs: _intrin.call_packed(
  16. "tvm.contrib.vacnn.vacc_slice_forward_sim", #注意,这是一个模拟的函数,仅供测试
  17. ins[0],
  18. begin[0],
  19. begin[1],
  20. end[0],
  21. end[1],
  22. outs[0]), name="vacc_slice_forward")

4 使用和测试算子

c++

  1. static const Op& dropout_op = Op::Get("nn.vacc_dropout");

python

  1. def test_conv2d_1x1_s2():
  2. n, c, h, w = 4, 3, 224, 224
  3. x = relay.var("x", relay.ty.TensorType((n, c, h, w), "float32"))
  4. w1 = relay.var("w1")
  5. w2 = relay.var("w2")
  6. def before():
  7. y = relay.nn.conv2d(x, w1, strides=(2,2), kernel_size=(1, 1), channels=8)
  8. y = relay.vacc_activation('sigmoid', y, 1.0, 2.0)
  9. y = relay.nn.conv2d(y, w2, strides=(2,2), kernel_size=(1,1), channels=4)
  10. y = relay.nn.relu(y)
  11. return relay.Function([x, w1, w2], y)
  12. def expect():
  13. y = relay.nn.conv2d(x, w1, strides=(1,1), kernel_size=(1, 1), channels=8)
  14. y = relay.vacc_activation('sigmoid', y, 1.0, 2.0)
  15. y = relay.vacc_dropout(y)
  16. y = relay.nn.conv2d(y, w2, strides=(1,1), kernel_size=(1,1), channels=4)
  17. y = relay.nn.relu(y)
  18. y = relay.vacc_dropout(y)
  19. return relay.Function([x, w1, w2], y)
  20. converted = run_convert_strides(before())
  21. expected = run_infer_type(expect())
  22. assert analysis.alpha_equal(converted, expected)