blob: e68f3a481c15aba789b793ca53f90c6fa54e9a3b [file] [log] [blame]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
from __future__ import division
import warnings
from collections import deque
from onnx import helper, checker
from onnx import TensorProto
from onnx import numpy_helper
from onnx.backend.base import BackendRep as backendRep
from onnx.backend.base import Backend as backend
import onnx
from . import singa_wrap as singa
from . import autograd
from . import tensor
class Handle(object):
@staticmethod
def conv(inputs, attrs):
# inputs: a list of the input tensors
kernel = tuple(attrs["kernel_shape"])
padding = tuple(attrs["pads"][0:2])
stride = tuple(attrs["strides"])
group = attrs["group"]
bias = len(inputs) == 3
x = inputs[0]
x_shape = inputs[0].shape
in_channels = x_shape[1]
w_shape = inputs[1].shape
out_channels = w_shape[0]
assert w_shape[1] == in_channels // group
if inputs[0].device.id() == -1:
if group != 1:
raise NotImplementedError
else:
handle = singa.ConvHandle(
x.data,
kernel,
stride,
padding,
in_channels,
out_channels,
bias,
group
)
else:
handle = singa.CudnnConvHandle(
x.data,
kernel,
stride,
padding,
in_channels,
out_channels,
bias,
group
)
return handle
@staticmethod
def max_pool(inputs, attrs):
x = inputs[0]
kernel = tuple(attrs["kernel_shape"])
padding = tuple(attrs["pads"][0:2])
stride = tuple(attrs["strides"])
if x.device.id() == -1:
handle = singa.PoolingHandle(x.data, kernel, stride, padding, True)
else:
handle = singa.CudnnPoolingHandle(
x.data, kernel, stride, padding, True
)
return handle
@staticmethod
def avg_pool(inputs, attrs):
x = inputs[0]
kernel = tuple(attrs["kernel_shape"])
padding = tuple(attrs["pads"][0:2])
stride = tuple(attrs["strides"])
if x.device.id() == -1:
handle = singa.PoolingHandle(
x.data, kernel, stride, padding, False
)
else:
handle = singa.CudnnPoolingHandle(
x.data, kernel, stride, padding, False
)
return handle
@staticmethod
def batchnorm(inputs, attrs):
x = inputs[0]
factor = attrs["momentum"]
if x.device.id() == -1:
raise NotImplementedError
else:
handle = singa.CudnnBatchNormHandle(factor, x.data)
return handle
UnaryOp = {
"Relu": autograd.relu,
"Softmax": autograd.softmax,
"Flatten": autograd.flatten,
"Tanh": autograd.tanh,
"Sigmoid": autograd.sigmoid,
}
BinaryOp = {
"Add": autograd.add_bias,
"Mul": autograd.mul,
"MatMul": autograd.matmul,
}
OtherOp = {
"Conv": (Handle.conv, autograd.conv2d),
"MaxPool": (Handle.max_pool, autograd.pooling_2d),
"AveragePool": (Handle.avg_pool, autograd.pooling_2d),
"BatchNormalization": (Handle.batchnorm, autograd.batchnorm_2d),
}
class SingaBackendRep(backendRep):
def __init__(self, model, device, tensor_dict):
"""
Args:
model: onnx model proto
device: singa device
tensor_dict: dict for weight tensors
"""
self.model = model
self.device = device
self.tensor_dict = tensor_dict
self.handle_dict = {}
@staticmethod
def run_node(node, inputs, handles):
"""
Args:
node: onnx node proto
inputs: a list of input tensors
handles: dict from node name to handle
Return:
a list out output tensors
"""
attrs = attribute2dict(node)
op = node.op_type
if op in UnaryOp:
out = UnaryOp[op](inputs[0])
elif op in BinaryOp:
out = BinaryOp[op](inputs[0], inputs[1])
elif op in OtherOp:
handle, forward = OtherOp[op]
if node.name not in handles:
handles[node.name] = handle(inputs, attrs)
out = forward(handles[node.name], *inputs)
elif op == "Concat":
out = autograd.cat(tuple(inputs), attrs["axis"])
else:
raise NotImplementedError("Not supported op: {}".format(op))
return [out]
def run(self, inputs):
"""
Run the graph with given inputs.
Args:
inputs: a list of tensors whose name and order match the
graph inputs.
Return:
a list of output tensors whose order match the graph outputs.
"""
# input_dict: dict from input name to numpy array
tensors = self.tensor_dict.copy()
for i, x in enumerate(inputs):
tensors[x.name] = x
if x.name != self.model.graph.input[i].name:
warnings.warn("the inputs do not match the graph inputs")
for node in self.model.graph.node:
if node.op_type != "Constant":
inputs = [tensors[x] for x in node.input]
outputs = SingaBackendRep.run_node(
node, inputs, self.handle_dict
)
for (key, val) in zip(node.output, outputs):
tensors[key] = val
y = []
for i in self.model.graph.output:
y.append(tensors[i.name])
return y
def attribute2dict(node):
# create a dictionary from the node attribute name to value
attr = {}
for a in node.attribute:
attr[a.name] = helper.get_attribute_value(a)
return attr
class SingaBackend(backend):
@classmethod
def prepare(
cls,
model, # type: ModelProto
device, # type: singa device
**kwargs # type: Any
): # type: (...) -> Optional[BackendRep]
"""
Args:
model: onnx model proto
device: singa device
Return:
SingaBackendRep instance
"""
super(SingaBackend, cls).prepare(model, device, **kwargs)
name2tensor = {}
for node in model.graph.node:
if node.op_type == "Constant":
data = helper.get_attribute_value(node.attribute[0])
requires_grad, stores_grad = True, True
if len(node.attribute) == 3:
requires_grad = helper.get_attribute_value(
node.attribute[1]
)
stores_grad = helper.get_attribute_value(node.attribute[2])
t = tensor.Tensor(
device=device,
data=numpy_helper.to_array(data),
requires_grad=requires_grad,
stores_grad=stores_grad,
)
name2tensor[node.output[0]] = t
return SingaBackendRep(model, device, name2tensor)
@classmethod
def run_node(cls, node, inputs, device, outputs_info=None, **kwargs):
"""
Args:
node: onnx node proto
inputs: list of singa tensors; the names should match
node inputs
Return:
a list of singa tensors as the node outputs
"""
super(SingaBackend, cls).run_node(node, inputs, device)
handles = {}
outputs = SingaBackendRep.run_node(node, inputs, handles)
return outputs
def to_onnx_model(inputs, y, model_name="sonnx"):
"""
get onnx model from singa computational graph
Args:
inputs: a list of input tensors (each is initialized with a name)
y: a list of tensors, usually the outputs of the graph
Return:
the onnx model
"""
assert len(y) == 1 # assume there is only one output
y = y[0]
node = []
dependency, _ = autograd.infer_dependency(y.creator)
input_ids = set(id(x) for x in inputs)
X = []
for x in inputs:
dtype = TensorProto.FLOAT
if y.dtype == tensor.int32:
dtype = TensorProto.INT
X.append(helper.make_tensor_value_info(x.name, dtype, x.shape))
Y = [helper.make_tensor_value_info(y.name, TensorProto.FLOAT, y.shape)]
ready = deque([y.creator])
while len(ready) > 0:
op = ready.pop()
assert not isinstance(op, autograd.Dummy)
outputs = [op.output_name(idx) for yid, idx in op.y_id2idx.items()]
inputs = [
srcop.output_name(srcop.y_id2idx[yid])
for (srcop, yid, _, _) in op.src
]
opname = op.name
optype = str(op).split(".")[-1].split(" ")[0]
if isinstance(op, autograd.Concat):
node.append(
helper.make_node(
"Concat",
inputs=inputs,
outputs=outputs,
name=opname,
axis=op.axis,
)
)
elif isinstance(op, autograd._Conv2d):
pads = [
op.handle.pad_h,
op.handle.pad_w,
op.handle.pad_w,
op.handle.pad_h,
]
stride = [op.handle.stride_h, op.handle.stride_w]
k = [op.handle.kernel_h, op.handle.kernel_w]
node.append(
helper.make_node(
"Conv",
inputs=inputs,
outputs=outputs,
name=opname,
kernel_shape=k,
pads=pads,
strides=stride,
group=op.handle.group,
)
)
elif isinstance(op, autograd._Pooling2d):
k = [op.handle.kernel_h, op.handle.kernel_w]
s = [op.handle.stride_h, op.handle.stride_w]
p = [
op.handle.pad_h,
op.handle.pad_w,
op.handle.pad_w,
op.handle.pad_h,
]
if op.handle.is_max_pooling:
node.append(
helper.make_node(
"MaxPool",
inputs=inputs,
outputs=outputs,
name=opname,
kernel_shape=k,
pads=p,
strides=s,
)
)
else:
node.append(
helper.make_node(
"AveragePool",
inputs=inputs,
outputs=outputs,
name=opname,
kernel_shape=k,
pads=p,
strides=s,
)
)
elif isinstance(op, autograd._BatchNorm2d):
node.append(
helper.make_node(
"BatchNormalization",
inputs=inputs,
outputs=outputs,
name=opname,
momentum=op.handle.factor,
)
)
# [(<singa.autograd.Sigmoid object at 0x7fd5ec09cb90>, 140556764852432, None, False),
# (<singa.autograd.Dummy object at 0x7fd5ec09c390>, 140556764824208,
# <singa.tensor.Tensor object at 0x7fd5ec09c290>, True),
# (<singa.autograd.Dummy object at 0x7fd5ec09c490>, 140556764824528,
# <singa.tensor.Tensor object at 0x7fd5ec09c3d0>, True),
# (<singa.autograd.Dummy object at 0x7fd5ec09c590>, 140556764824784, None, False),
# (<singa.autograd.Dummy object at 0x7fd5ec09c690>, 140556764825040, None, False)])
# two dummy operators do not have values, so take the values from handle
"""
dummy0 = tensor.to_numpy(
tensor.Tensor(
device=op.running_mean.device(), data=op.running_mean
)
)
dummy1 = tensor.to_numpy(
tensor.Tensor(
device=op.running_var.device(), data=op.running_var
)
)
dummy0 = helper.make_node(
"Constant",
inputs=[],
outputs=[inputs[3]],
value=numpy_helper.from_array(dummy0),
)
dummy1 = helper.make_node(
"Constant",
inputs=[],
outputs=[inputs[4]],
value=numpy_helper.from_array(dummy1),
)
node.append(dummy0)
node.append(dummy1)
"""
else:
singa2onnx = {
"SoftMax": "Softmax",
"AddBias": "Add",
"Add": "Add",
"Matmul": "MatMul",
"ReLU": "Relu",
"ElemMatmul": "Mul",
"Flatten": "Flatten",
"Tanh": "Tanh",
"Sigmoid": "Sigmoid"
}
assert optype in singa2onnx, "Unsupported op:{}".format(optype)
onnx_op = singa2onnx[optype]
node.append(
helper.make_node(
onnx_op, inputs=inputs, outputs=outputs, name=opname
)
)
for srcop, yid, y, _ in op.src:
dependency[srcop] -= 1
if dependency[srcop] == 0:
if isinstance(srcop, autograd.Dummy):
if yid not in input_ids:
tmp = helper.make_node(
"Constant",
inputs=[],
outputs=[srcop.output_name(0)],
value=helper.make_tensor(
name=opname,
data_type=TensorProto.FLOAT,
dims=y.shape,
vals=tensor.to_numpy(y)
.flatten()
.astype(float),
),
)
node.append(tmp)
else:
ready.append(srcop)
# print(node)
onnx_model = helper.make_model(
helper.make_graph(node[::-1], model_name, X, Y)
)
checker.check_model(onnx_model)
return onnx_model
def export(inputs, y, file_path, model_name="sonnx"):
onnx_model = to_onnx_model(inputs, y, model_name)
onnx.save(onnx_model, file_path)
run_model = SingaBackend.run_model
run_node = SingaBackend.run_node
supports_device = SingaBackend.supports_device
prepare = SingaBackend.prepare
save = onnx.save
load = onnx.load