Skip to content

Commit ffc25a9

Browse files
committed
Add OpenVINO backend
1 parent 464eab8 commit ffc25a9

File tree

7 files changed

+223
-9
lines changed

7 files changed

+223
-9
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ $ source venv3/bin/activate
7777
(venv3) $ bonito download --all
7878
```
7979

80+
To optimize inference on CPU with Intel OpenVINO:
81+
* Download and install OpenVINO from https://software.seek.intel.com/openvino-toolkit
82+
```bash
83+
(venv3) $ pip install -r /opt/intel/openvino/deployment_tools/model_optimizer/requirements_onnx.txt
84+
(venv3) $ source /opt/intel/openvino/bin/setupvars.sh
85+
(venv3) $ bonito evaluate dna_r9.4.1 --use_openvino --device=cpu
86+
```
87+
8088
## Medaka
8189

8290
The Medaka can be downloaded from [here](https://nanoporetech.box.com/shared/static/u5gncwjbtg2k3dkw26nmvdvck65ab3xh.hdf5).

bonito/evaluate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def main(args):
3030
for w in [int(i) for i in args.weights.split(',')]:
3131

3232
print("* loading model", w)
33-
model = load_model(args.model_directory, args.device, weights=w, half=args.half)
33+
model = load_model(args.model_directory, args.device, weights=w, half=args.half, use_openvino=args.use_openvino)
3434

3535
print("* calling")
3636
predictions = []
@@ -39,9 +39,8 @@ def main(args):
3939
with torch.no_grad():
4040
for data, *_ in dataloader:
4141
if args.half:
42-
data = data.type(torch.float16).to(args.device)
43-
else:
44-
data = data.to(args.device)
42+
data = data.type(torch.float16)
43+
data = data.to(args.device if not args.use_openvino else 'cpu')
4544
log_probs = model(data)
4645
predictions.append(log_probs.exp().cpu().numpy().astype(np.float32))
4746

@@ -90,4 +89,5 @@ def argparser():
9089
parser.add_argument("--poa", action="store_true", default=False)
9190
parser.add_argument("--shuffle", action="store_true", default=True)
9291
parser.add_argument("--min-coverage", default=0.5, type=float)
92+
parser.add_argument("--use_openvino", action="store_true", default=False)
9393
return parser
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# mo_extensions/front/onnx/conv2d.py
2+
import numpy as np
3+
4+
from mo.front.common.replacement import FrontReplacementSubgraph
5+
from mo.graph.graph import Graph, Node
6+
7+
class Conv1dToConv2d(FrontReplacementSubgraph):
8+
enabled = True
9+
10+
def pattern(self):
11+
return dict(
12+
nodes=[
13+
('conv', dict(op='Conv')),
14+
('weights', dict(op='Const')),
15+
],
16+
edges=[
17+
('weights', 'conv', {'in': 1})
18+
])
19+
20+
@staticmethod
21+
def replace_sub_graph(graph: Graph, match: dict):
22+
conv = match['conv']
23+
conv['pad'] = np.insert(conv['pad'], 2, [0, 0], axis=0)
24+
conv['stride'] = np.insert(conv['stride'], 2, 1)
25+
conv['dilation'] = np.insert(conv['dilation'], 2, 1)
26+
conv['kernel_spatial'] = np.insert(conv['kernel_spatial'], 0, 1)
27+
28+
weights = match['weights']
29+
weights['shape'] = np.insert(weights['shape'], 2, 1)
30+
weights['pb'].dims.insert(2, 1)
31+
weights['value'] = np.expand_dims(weights['value'], axis=2)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Copyright (C) 2018-2020 Intel Corporation
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
from extensions.ops.activation_ops import Sigmoid
18+
from extensions.ops.elementwise import Mul
19+
from mo.front.common.replacement import FrontReplacementOp
20+
from mo.graph.graph import Node, Graph
21+
22+
23+
class Swish(FrontReplacementOp):
24+
op = "Swish"
25+
enabled = True
26+
27+
def replace_op(self, graph: Graph, node: Node):
28+
mul_node = Mul(graph, {'name': node.name + '/mul_'}).create_node()
29+
sigmoid_node = Sigmoid(graph, {'name': node.name + '/sigmoid_'}).create_node()
30+
31+
# Connect nodes
32+
node.in_port(0).get_connection().get_source().connect(mul_node.in_port(0))
33+
node.in_port(0).get_connection().get_source().connect(sigmoid_node.in_port(0))
34+
sigmoid_node.out_port(0).connect(mul_node.in_port(1))
35+
36+
# The "explicit" version of the return value is: [(out_node.id, 0)])
37+
return [mul_node.id]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# mo_extensions/front/onnx/conv2d.py
2+
import numpy as np
3+
4+
from mo.front.common.replacement import FrontReplacementSubgraph
5+
from mo.graph.graph import Graph, Node
6+
from extensions.ops.transpose import Transpose
7+
from mo.front.extractor import FrontExtractorOp
8+
9+
class Transpose2d(FrontExtractorOp):
10+
op = 'Transpose'
11+
enabled = True
12+
13+
@classmethod
14+
def extract(cls, node: Node):
15+
Transpose.update_node_stat(node, {'order': [0, 3, 2, 1]})
16+
return cls.enabled

bonito/openvino/model.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import os
2+
import numpy as np
3+
import torch
4+
5+
try:
6+
from openvino.inference_engine import IECore, StatusCode
7+
except ImportError:
8+
pass
9+
10+
class OpenVINOModel:
11+
12+
def __init__(self, model, half, dirname):
13+
self.model = model
14+
self.alphabet = model.alphabet
15+
16+
onnx_path = os.path.join(dirname, model.config['model']) + '.onnx'
17+
model_name = model.config['model'] + ('_fp16' if half else '')
18+
xml_path, bin_path = [os.path.join(dirname, model_name) + ext for ext in ['.xml', '.bin']]
19+
if not os.path.exists(xml_path) or not os.path.exists(bin_path):
20+
21+
# Convert to ONNX
22+
if not os.path.exists(onnx_path):
23+
inp = torch.randn(1, 1, 1000) # Just dummy input shape. We will reshape model later
24+
model.eval()
25+
with torch.no_grad():
26+
torch.onnx.export(model, inp, onnx_path,
27+
input_names=['input'],
28+
output_names=['output'],
29+
operator_export_type=torch.onnx.OperatorExportTypes.ONNX_ATEN_FALLBACK)
30+
31+
# Convert to IR
32+
import mo_onnx
33+
import subprocess
34+
subprocess.call([mo_onnx.__file__,
35+
'--input_model', onnx_path,
36+
'--extension', os.path.join(os.path.dirname(__file__), 'mo_extension'),
37+
'--keep_shape_ops',
38+
'--model_name', model_name,
39+
'--data_type', 'FP16' if half else 'FP32',
40+
'--input_shape=[1,1,1,1000]',
41+
'--output_dir', dirname])
42+
43+
self.ie = IECore()
44+
self.net = self.ie.read_network(xml_path, bin_path)
45+
self.exec_net = None
46+
47+
48+
def eval(self):
49+
pass
50+
51+
52+
def half(self):
53+
return self
54+
55+
56+
def to(self, device):
57+
self.device = str(device).upper()
58+
59+
60+
def __call__(self, data):
61+
data = np.expand_dims(data, axis=2) # 1D->2D
62+
batch_size = data.shape[0]
63+
if not self.exec_net:
64+
inp_shape = list(data.shape)
65+
inp_shape[0] = 1 # We will run the batch asynchronously
66+
self.net.reshape({'input': inp_shape})
67+
config = {}
68+
if self.device == 'CPU':
69+
config={'CPU_THROUGHPUT_STREAMS': 'CPU_THROUGHPUT_AUTO'}
70+
self.exec_net = self.ie.load_network(self.net, self.device,
71+
config=config, num_requests=0)
72+
73+
# List that maps infer requests to index of processed chunk from batch.
74+
# -1 means that request has not been started yet.
75+
infer_request_input_id = [-1] * len(self.exec_net.requests)
76+
output = np.zeros([batch_size] + self.net.outputs['output'].shape[1:], dtype=np.float32)
77+
78+
for inp_id in range(batch_size):
79+
# Get idle infer request
80+
infer_request_id = self.exec_net.get_idle_request_id()
81+
if infer_request_id < 0:
82+
status = self.exec_net.wait(num_requests=1)
83+
if status != StatusCode.OK:
84+
raise Exception("Wait for idle request failed!")
85+
infer_request_id = self.exec_net.get_idle_request_id()
86+
if infer_request_id < 0:
87+
raise Exception("Invalid request id!")
88+
89+
out_id = infer_request_input_id[infer_request_id]
90+
request = self.exec_net.requests[infer_request_id]
91+
92+
# Copy output prediction
93+
if out_id != -1:
94+
output[out_id] = request.output_blobs['output'].buffer
95+
96+
# Start this request on new data
97+
infer_request_input_id[infer_request_id] = inp_id
98+
request.async_infer({'input': data[inp_id]})
99+
inp_id += 1
100+
101+
# Wait for the rest of requests
102+
status = self.exec_net.wait()
103+
if status != StatusCode.OK:
104+
raise Exception("Wait for idle request failed!")
105+
for infer_request_id, out_id in enumerate(infer_request_input_id):
106+
request = self.exec_net.requests[infer_request_id]
107+
output[out_id] = request.output_blobs['output'].buffer
108+
109+
output = np.squeeze(output, axis=2) # 2D->1D
110+
return torch.tensor(output)
111+
112+
113+
def decode(self, post, beamsize):
114+
return self.model.decode(post, beamsize=beamsize)

bonito/util.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from bonito.model import Model
1212
from bonito_cuda_runtime import CuModel
13+
from bonito.openvino.model import OpenVINOModel
1314

1415
import toml
1516
import torch
@@ -75,7 +76,7 @@ def init(seed, device):
7576
random.seed(seed)
7677
np.random.seed(seed)
7778
torch.manual_seed(seed)
78-
if device == "cpu": return
79+
if not device.startswith('cuda'): return
7980
torch.backends.cudnn.enabled = True
8081
torch.backends.cudnn.deterministic = True
8182
torch.backends.cudnn.benchmark = False
@@ -86,7 +87,10 @@ def half_supported():
8687
"""
8788
Returns whether FP16 is support on the GPU
8889
"""
89-
return get_device_capability()[0] >= 7
90+
try:
91+
return get_device_capability()[0] >= 7
92+
except:
93+
return False
9094

9195

9296
def phred(prob, scale=1.0, bias=0.0):
@@ -238,7 +242,7 @@ def load_data(shuffle=False, limit=None, directory=None, validation=False):
238242
return chunks, chunk_lengths, targets, target_lengths
239243

240244

241-
def load_model(dirname, device, weights=None, half=False, chunksize=0, use_rt=False):
245+
def load_model(dirname, device, weights=None, half=False, chunksize=0, use_rt=False, use_openvino=False):
242246
"""
243247
Load a model from disk
244248
"""
@@ -251,21 +255,25 @@ def load_model(dirname, device, weights=None, half=False, chunksize=0, use_rt=Fa
251255
raise FileNotFoundError("no model weights found in '%s'" % dirname)
252256
weights = max([int(re.sub(".*_([0-9]+).tar", "\\1", w)) for w in weight_files])
253257

254-
device = torch.device(device)
258+
if not use_openvino:
259+
device = torch.device(device)
255260
config = os.path.join(dirname, 'config.toml')
256261
weights = os.path.join(dirname, 'weights_%s.tar' % weights)
257262
model = Model(toml.load(config))
258263

259-
state_dict = torch.load(weights, map_location=device)
264+
state_dict = torch.load(weights, map_location=device if not use_openvino else 'cpu')
260265
new_state_dict = OrderedDict()
261266
for k, v in state_dict.items():
262267
name = k.replace('module.', '')
263268
new_state_dict[name] = v
264269

265270
model.load_state_dict(new_state_dict)
266271

272+
assert(not use_rt or not use_openvino)
267273
if use_rt:
268274
model = CuModel(model.config, chunksize, new_state_dict)
275+
elif use_openvino:
276+
model = OpenVINOModel(model, half, dirname)
269277

270278
if half: model = model.half()
271279
model.eval()

0 commit comments

Comments
 (0)