Skip to content

Commit cb22d50

Browse files
FrozenGenetqchen
authored andcommitted
[CoreML] Solve CoreML frontend issue of image scaler and padding so that Mobilenet mlmodel can work correctly. (#3800)
1 parent ebf52df commit cb22d50

4 files changed

Lines changed: 174 additions & 51 deletions

File tree

python/tvm/relay/frontend/common.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,15 @@ def new_const(self, value, shape=None, dtype="float32"):
273273
def get_expr(self, name):
274274
return self.exprs[name]
275275

276-
def set_expr(self, name, expr):
276+
def set_expr(self, name, expr, force_override=False):
277277
assert isinstance(expr, _expr.Expr)
278-
if name not in self.exprs:
278+
# if name exists, we should override the value
279+
# otherwise, we can not get like x = func(x) work.
280+
# One example is CoreML preprocess, which will override
281+
# the same name of input.
282+
# However, according to git log, Find keras frontend depends
283+
# on this property, so we add one force_override to control it.
284+
if name not in self.exprs or force_override:
279285
self.exprs[name] = expr
280286

281287
def has_expr(self, name):

python/tvm/relay/frontend/coreml.py

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# pylint: disable=invalid-name, import-self, unused-argument, unused-variable, inconsistent-return-statements
1818
"""CoreML frontend."""
1919
from __future__ import absolute_import as _abs
20+
import math
2021
import numpy as np
2122
import tvm
2223
from .. import analysis
@@ -26,11 +27,13 @@
2627
from ... import nd as _nd
2728
from ..._ffi import base as _base
2829
from .common import ExprTable
30+
from .common import infer_shape as _infer_shape
2931

3032
__all__ = ['from_coreml']
3133

3234

3335
def _NeuralNetworkImageScaler(op, inexpr, etab):
36+
# TODO: we need to support more colorspace, such as rgb.
3437
# this changes the symbol
3538
biases = np.array([op.blueBias, op.greenBias, op.redBias]).reshape([3, 1, 1])
3639
bias = etab.new_const(biases)
@@ -47,11 +50,16 @@ def _NeuralNetworkMeanImage(op, inexpr, etab):
4750

4851
def _ConvolutionLayerParams(op, inexpr, etab):
4952
"""Convolution layer params."""
50-
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
51-
tuple([op.outputChannels, op.kernelChannels] + list(op.kernelSize))))
53+
if op.isDeconvolution:
54+
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
55+
tuple([op.kernelChannels, op.outputChannels] + list(op.kernelSize))))
56+
else:
57+
weights = etab.new_const(np.array(list(op.weights.floatValue)).reshape(
58+
tuple([op.outputChannels, op.kernelChannels] + list(op.kernelSize))))
5259
dilation = list(op.dilationFactor)
5360
if not dilation:
5461
dilation = [1, 1]
62+
N, C, H, W = _infer_shape(inexpr)
5563
params = {'channels':op.outputChannels,
5664
'kernel_size':list(op.kernelSize),
5765
'strides':list(op.stride),
@@ -60,30 +68,31 @@ def _ConvolutionLayerParams(op, inexpr, etab):
6068

6169
if op.WhichOneof('ConvolutionPaddingType') == 'valid':
6270
valid = op.valid
63-
padding = [b.startEdgeSize for b in valid.paddingAmounts.borderAmounts]
64-
padding2 = [b.endEdgeSize for b in valid.paddingAmounts.borderAmounts]
65-
for i, j in zip(padding, padding2):
66-
assert i == j, "Asymmetry padding not supported"
67-
if padding:
68-
params['padding'] = padding
71+
if valid.paddingAmounts.borderAmounts:
72+
assert len(valid.paddingAmounts.borderAmounts) == 2
73+
pad_t = valid.paddingAmounts.borderAmounts[0].startEdgeSize
74+
pad_l = valid.paddingAmounts.borderAmounts[1].startEdgeSize
75+
pad_b = valid.paddingAmounts.borderAmounts[0].endEdgeSize
76+
pad_r = valid.paddingAmounts.borderAmounts[1].endEdgeSize
77+
inexpr = _op.nn.pad(data=inexpr, pad_width=((0, 0),
78+
(0, 0),
79+
(pad_t, pad_b),
80+
(pad_l, pad_r)))
6981
elif op.WhichOneof('ConvolutionPaddingType') == 'same':
82+
assert op.same.asymmetryMode == 0, "Only support BOTTOM_RIGHT_HEAVY mode, " \
83+
"which is used by tf/caffe and so on"
7084
kernel = params['kernel_size']
71-
pad_h = kernel[0] - 1
72-
pad_w = kernel[1] - 1
73-
pad_t = pad_h // 2
74-
pad_l = pad_w // 2
75-
pad_b = pad_h - pad_t
76-
pad_r = pad_w - pad_l
77-
assert pad_t == pad_r and pad_l == pad_b, "Asymmetry padding not supported"
78-
params['padding'] = [pad_t, pad_l]
85+
strides = params['strides']
86+
pad_t, pad_b = get_pad_value(H, kernel[0], strides[0])
87+
pad_l, pad_r = get_pad_value(W, kernel[1], strides[1])
88+
inexpr = _op.nn.pad(data=inexpr, pad_width=((0, 0),
89+
(0, 0),
90+
(pad_t, pad_b),
91+
(pad_l, pad_r)))
92+
7993
else:
8094
raise NotImplementedError("Valid/Same convolution padding implemented")
8195

82-
# consume padding layer
83-
if etab.in_padding:
84-
params['padding'] = [sum(x) for x in zip(params.get('padding', [0, 0]), etab.paddings)]
85-
etab.clear_padding()
86-
8796
if op.isDeconvolution:
8897
ret = _op.nn.conv2d_transpose(data=inexpr, weight=weights, **params)
8998
else:
@@ -193,11 +202,13 @@ def _PoolingLayerParams(op, inexpr, etab):
193202

194203
if op.WhichOneof('PoolingPaddingType') == 'valid':
195204
valid = op.valid
196-
padding = [b.startEdgeSize for b in valid.paddingAmounts.borderAmounts]
197-
padding2 = [b.endEdgeSize for b in valid.paddingAmounts.borderAmounts]
198-
for i, j in zip(padding, padding2):
199-
assert i == j
200-
params['padding'] = padding
205+
if valid.paddingAmounts.borderAmounts:
206+
assert len(valid.paddingAmounts.borderAmounts) == 2
207+
pad_t = valid.paddingAmounts.borderAmounts[0].startEdgeSize
208+
pad_l = valid.paddingAmounts.borderAmounts[1].startEdgeSize
209+
pad_b = valid.paddingAmounts.borderAmounts[0].endEdgeSize
210+
pad_r = valid.paddingAmounts.borderAmounts[1].endEdgeSize
211+
params['padding'] = [pad_t, pad_l, pad_b, pad_r]
201212
elif op.WhichOneof('PoolingPaddingType') == 'includeLastPixel':
202213
# I don't know if this is correct
203214
valid = op.includeLastPixel
@@ -209,12 +220,6 @@ def _PoolingLayerParams(op, inexpr, etab):
209220
op_name = op.WhichOneof('PoolingPaddingType')
210221
raise tvm.error.OpAttributeUnImplemented(msg.format(op_name))
211222

212-
# consume padding layer
213-
if etab.in_padding:
214-
params['padding'] = [sum(x) for x in zip(
215-
params.get('padding', [0, 0]), etab.paddings)]
216-
etab.clear_padding()
217-
218223
if op.type == 0:
219224
return _op.nn.max_pool2d(inexpr, **params)
220225
if op.type == 1:
@@ -276,21 +281,24 @@ def _FlattenLayerParams(op, inexpr, etab):
276281

277282

278283
def _PaddingLayerParams(op, inexpr, etab):
279-
"""Hacking for padding layer params."""
284+
"""Padding layer params."""
280285
if op.WhichOneof('PaddingType') == 'constant':
281286
constant = op.constant
282287
if constant.value != 0:
283288
raise tvm.error.OpAttributeUnImplemented(
284289
'{} is not supported in operator Padding.'.format(constant.value))
285-
padding = [b.startEdgeSize for b in op.paddingAmounts.borderAmounts]
286-
padding2 = [b.endEdgeSize for b in op.paddingAmounts.borderAmounts]
287-
for i, j in zip(padding, padding2):
288-
assert i == j
289-
etab.set_padding(padding)
290+
pad_t = op.paddingAmounts.borderAmounts[0].startEdgeSize
291+
pad_l = op.paddingAmounts.borderAmounts[1].startEdgeSize
292+
pad_b = op.paddingAmounts.borderAmounts[0].endEdgeSize
293+
pad_r = op.paddingAmounts.borderAmounts[1].endEdgeSize
294+
return _op.nn.pad(data=inexpr, pad_width=((0, 0),
295+
(0, 0),
296+
(pad_t, pad_b),
297+
(pad_l, pad_r)))
298+
290299
else:
291300
raise tvm.error.OpNotImplemented(
292301
'Non-constant padding is not supported in frontend CoreML.')
293-
return inexpr
294302

295303

296304
def _PermuteLayerParams(op, inexpr, etab):
@@ -372,6 +380,32 @@ def _MinLayerParams(op, inexpr, etab):
372380
'MinLayerParams': _MinLayerParams,
373381
}
374382

383+
# SAME padding: https://www.tensorflow.org/api_guides/python/nn
384+
def get_pad_value(data, kernel, stride):
385+
"""Get the pad tuple of value for SAME padding
386+
387+
Parameters
388+
----------
389+
data:
390+
1D input data
391+
392+
kernel:
393+
1D input kernel
394+
395+
stride:
396+
1D input stride
397+
398+
Returns
399+
-------
400+
pad tuple of value
401+
"""
402+
403+
out = int(math.ceil(float(data) / float(stride)))
404+
pad = max(0, (out - 1) * stride + kernel - data)
405+
pad_before = pad // 2
406+
pad_after = pad - pad_before
407+
return pad_before, pad_after
408+
375409

376410
def coreml_op_to_relay(op, inname, outname, etab):
377411
"""Convert coreml layer to a Relay expression and update the expression table.
@@ -399,9 +433,7 @@ def coreml_op_to_relay(op, inname, outname, etab):
399433
insym = [etab.get_expr(i) for i in inname]
400434
ret = _convert_map[classname](op, insym, etab)
401435
if outname:
402-
etab.set_expr(outname, ret)
403-
if classname != 'PaddingLayerParams':
404-
assert not etab.in_padding, "Previous padding not consumed by conv/pool"
436+
etab.set_expr(outname, ret, force_override=True)
405437

406438

407439
def from_coreml(model, shape=None):
@@ -442,10 +474,19 @@ def from_coreml(model, shape=None):
442474
for pp in cc.preprocessing:
443475
whichpp = pp.WhichOneof('preprocessor')
444476
ppmethod = getattr(pp, whichpp)
445-
# the NeuralNetworkImageScalar doesn't seem to have a featureName?
446477
if whichpp == 'scaler':
478+
# Be careful we maybe only preprocess one input when we have multi inputs
479+
# which is stored in pp.featureName. See unit testing verify_image_scaler
480+
# in test_forward.py for CoreML.
447481
for i in spec.description.input:
448-
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
482+
# we have multi inputs
483+
if len(spec.description.input) > 1:
484+
assert pp.featureName != ''
485+
if i.name == pp.featureName:
486+
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
487+
else:
488+
assert pp.featureName == ''
489+
coreml_op_to_relay(ppmethod, i.name, i.name, etab)
449490
else:
450491
coreml_op_to_relay(ppmethod, pp.featureName, pp.featureName, etab)
451492

tests/python/frontend/coreml/test_forward.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import topi.testing
2626
from tvm import relay
2727
from tvm.relay.testing.config import ctx_list
28+
from topi.testing import conv2d_nchw_python
2829

2930
import coremltools as cm
3031
import model_zoo
@@ -95,7 +96,10 @@ def run_tvm_graph(coreml_model, target, ctx, input_data, input_name, output_shap
9596
tvm_output_list.append(tvm_output.asnumpy())
9697
return tvm_output_list
9798
else:
98-
tvm_output = m.get_output(0, tvm.nd.empty((output_shape), output_dtype))
99+
if not output_shape:
100+
tvm_output = m.get_output(0)
101+
else:
102+
tvm_output = m.get_output(0, tvm.nd.empty((output_shape), output_dtype))
99103
return tvm_output.asnumpy()
100104

101105
def verify_AddLayerParams(input_dim, alpha=2):
@@ -330,6 +334,72 @@ def test_forward_min():
330334
verify_min((1, 3, 20, 20))
331335
verify_min((20, 20))
332336

337+
def verify_image_scaler(input_dim, blue_bias=0.0, green_bias=0.0, red_bias=0.0, image_scale=1.0):
338+
dtype = 'float32'
339+
a_np = np.random.uniform(size=input_dim).astype(dtype)
340+
# make sure it is valid image format CHW.
341+
assert len(a_np.shape) == 3 and a_np.shape[0] == 3
342+
b_np = np.zeros(a_np.shape, dtype=dtype)
343+
b_np[0, :, :] = image_scale * a_np[0, :, :] + blue_bias
344+
b_np[1, :, :] = image_scale * a_np[1, :, :] + green_bias
345+
b_np[2, :, :] = image_scale * a_np[2, :, :] + red_bias
346+
b_np = np.add(a_np, b_np)
347+
inputs = [('input1', datatypes.Array(*input_dim)),
348+
('input2', datatypes.Array(*input_dim))]
349+
output = [('output', datatypes.Array(*b_np.shape))]
350+
builder = NeuralNetworkBuilder(inputs, output)
351+
builder.set_pre_processing_parameters(image_input_names=['input1'],
352+
is_bgr=True,
353+
blue_bias=blue_bias,
354+
green_bias=green_bias,
355+
red_bias=red_bias,
356+
image_scale=image_scale)
357+
# add one add layer to make CoreML model format valid
358+
# add layer has been tested before.
359+
builder.add_elementwise(name='add', input_names=['input1', 'input2'],
360+
output_name='output', alpha=0, mode='ADD')
361+
model = cm.models.MLModel(builder.spec)
362+
for target, ctx in ctx_list():
363+
out = run_tvm_graph(model, target, ctx, [a_np, a_np],
364+
['input1', 'input2'], b_np.shape, dtype)
365+
tvm.testing.assert_allclose(out, b_np, rtol=1e-5)
366+
367+
def test_forward_image_scaler():
368+
verify_image_scaler((3, 224, 224), image_scale=0.17)
369+
verify_image_scaler((3, 224, 224),
370+
blue_bias=-1.7669800519943237,
371+
green_bias=-1.985260009765625,
372+
red_bias=-2.102560043334961,
373+
image_scale=0.379)
374+
375+
def verify_convolution(input_dim, filter, padding):
376+
dtype = 'float32'
377+
N, C, H, W = input_dim
378+
OC, _, KH, KW = filter
379+
a_np = np.random.uniform(size=input_dim).astype(dtype)
380+
w_np = np.random.uniform(size=(OC, C, KH, KW)).astype(dtype)
381+
w_np_cm = np.transpose(w_np, axes=(2, 3, 1, 0))
382+
b_np = conv2d_nchw_python(a_np, w_np, [1, 1], padding)
383+
inputs = [('input1', datatypes.Array(C, H, W))]
384+
output = [('output', datatypes.Array(*b_np.shape))]
385+
builder = NeuralNetworkBuilder(inputs, output)
386+
builder.add_convolution(name='conv', kernel_channels=3, output_channels=OC,
387+
height=KH, width=KW, stride_height=1, stride_width=1,
388+
border_mode=padding.lower(), groups=1,
389+
W=w_np_cm, b=None, has_bias=False,
390+
is_deconv=False,
391+
input_name='input1',
392+
output_name='output')
393+
model = cm.models.MLModel(builder.spec)
394+
for target, ctx in ctx_list():
395+
out = run_tvm_graph(model, target, ctx, [a_np],
396+
['input1'], output_shape=None)
397+
tvm.testing.assert_allclose(out, b_np, rtol=1e-5)
398+
399+
def test_forward_convolution():
400+
verify_convolution((1, 3, 224, 224), filter=(32, 3, 3, 3), padding='VALID')
401+
verify_convolution((1, 3, 224, 224), filter=(32, 3, 3, 3), padding='SAME')
402+
333403
if __name__ == '__main__':
334404
test_forward_AddLayerParams()
335405
test_forward_ConcatLayerParams()
@@ -342,3 +412,5 @@ def test_forward_min():
342412
test_forward_min()
343413
test_mobilenet_checkonly()
344414
test_resnet50_checkonly()
415+
test_forward_image_scaler()
416+
test_forward_convolution()

tutorials/frontend/from_coreml.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
Compile CoreML Models
1919
=====================
2020
**Author**: `Joshua Z. Zhang <https://zhreshold.github.io/>`_, \
21-
`Kazutaka Morita <https://github.com/kazum>`_
21+
`Kazutaka Morita <https://github.com/kazum>`_, \
22+
`Zhao Wu <https://github.com/FrozenGene>`_
2223
2324
This article is an introductory tutorial to deploy CoreML models with Relay.
2425
@@ -58,13 +59,15 @@
5859
img_url = 'https://github.com/dmlc/mxnet.js/blob/master/data/cat.png?raw=true'
5960
img_path = download_testdata(img_url, 'cat.png', module='data')
6061
img = Image.open(img_path).resize((224, 224))
61-
x = np.transpose(img, (2, 0, 1))[np.newaxis, :]
62+
# Mobilenet.mlmodel's input is BGR format
63+
img_bgr = np.array(img)[:,:,::-1]
64+
x = np.transpose(img_bgr, (2, 0, 1))[np.newaxis, :]
6265

6366
######################################################################
6467
# Compile the model on Relay
6568
# ---------------------------
6669
# We should be familiar with the process right now.
67-
target = 'cuda'
70+
target = 'llvm'
6871
shape_dict = {'image': x.shape}
6972

7073
# Parse CoreML model and convert into Relay computation graph
@@ -80,7 +83,7 @@
8083
# -------------------
8184
# The process is no different from other example
8285
from tvm.contrib import graph_runtime
83-
ctx = tvm.gpu(0)
86+
ctx = tvm.cpu(0)
8487
dtype = 'float32'
8588
m = graph_runtime.create(graph, lib, ctx)
8689
# set inputs
@@ -104,4 +107,5 @@
104107
synset_path = download_testdata(synset_url, synset_name, module='data')
105108
with open(synset_path) as f:
106109
synset = eval(f.read())
110+
# You should see the following result: Top-1 id 282 class name tiger cat
107111
print('Top-1 id', top1, 'class name', synset[top1])

0 commit comments

Comments
 (0)