Skip to content

Commit c8bf72c

Browse files
committed
Add OpenVINO backend
1 parent 07f885e commit c8bf72c

File tree

8 files changed

+236
-9
lines changed

8 files changed

+236
-9
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ $ source venv3/bin/activate
2828
(venv3) $ bonito download --models --latest
2929
```
3030

31+
To optimize inference on CPU with Intel OpenVINO:
32+
33+
```bash
34+
(venv3) $ export LD_LIBRARY_PATH=$(pwd)/venv3/lib:$LD_LIBRARY_PATH
35+
(venv3) $ bonito evaluate dna_r9.4.1 --use_openvino --device=cpu
36+
```
37+
3138
## Training your own model
3239

3340
To train a model using your own reads, first basecall the reads with the additional `--save-ctc` flag and use the output directory as the input directory for training.

bonito/cli/basecaller.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def main(args):
2323
exit(1)
2424

2525
sys.stderr.write("> loading model\n")
26-
model = load_model(args.model_directory, args.device, weights=int(args.weights))
26+
model = load_model(args.model_directory, args.device, weights=int(args.weights), use_openvino=args.use_openvino)
2727

2828
if args.reference:
2929
sys.stderr.write("> loading reference\n")
@@ -83,6 +83,7 @@ def argparser():
8383
parser.add_argument("--skip", action="store_true", default=False)
8484
parser.add_argument("--fastq", action="store_true", default=False)
8585
parser.add_argument("--save-ctc", action="store_true", default=False)
86+
parser.add_argument("--use_openvino", action="store_true", default=False)
8687
parser.add_argument("--ctc-min-coverage", default=0.9, type=float)
8788
parser.add_argument("--ctc-min-accuracy", default=0.9, type=float)
8889
return parser

bonito/cli/evaluate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def main(args):
3535
seqs = []
3636

3737
print("* loading model", w)
38-
model = load_model(args.model_directory, args.device, weights=w)
38+
model = load_model(args.model_directory, args.device, weights=w, use_openvino=args.use_openvino)
3939

4040
print("* calling")
4141
t0 = time.perf_counter()
@@ -93,4 +93,5 @@ def argparser():
9393
parser.add_argument("--poa", action="store_true", default=False)
9494
parser.add_argument("--shuffle", action="store_true", default=True)
9595
parser.add_argument("--min-coverage", default=0.5, type=float)
96+
parser.add_argument("--use_openvino", action="store_true", default=False)
9697
return parser

bonito/ctc/model.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import numpy as np
6-
from bonito.nn import Permute, activations
6+
from bonito.nn import Add, Permute, activations
77
from torch.nn.functional import log_softmax
88
from torch.nn import Module, ModuleList, Sequential, Conv1d, BatchNorm1d, Dropout
99

@@ -121,6 +121,7 @@ def __init__(self, in_channels, out_channels, activation, repeat=5, kernel_size=
121121

122122
self.use_res = residual
123123
self.conv = ModuleList()
124+
self.add = Add()
124125

125126
_in_channels = in_channels
126127
padding = self.get_padding(kernel_size[0], stride[0], dilation[0])
@@ -178,7 +179,7 @@ def forward(self, x):
178179
for layer in self.conv:
179180
_x = layer(_x)
180181
if self.use_res:
181-
_x = _x + self.residual(x)
182+
_x = self.add(_x, self.residual(x))
182183
return self.activation(_x)
183184

184185

bonito/openvino/loader.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# This script provides a method which builds OpenVINO network in runtime
2+
import numpy as np
3+
from openvino.inference_engine import IECore, IENetwork
4+
5+
import ngraph.opset4 as ng
6+
from ngraph.impl.op import Parameter
7+
from ngraph.impl import Function, Shape, Type
8+
9+
import torch
10+
from torch.autograd import Variable
11+
12+
13+
nodes = {}
14+
out = None
15+
16+
def forward_hook(self, inputs, output):
17+
global out
18+
layer_type = self.__class__.__name__
19+
20+
params = [value.numpy() for value in self.state_dict().values()]
21+
22+
inp = nodes[inputs[0].data_ptr()]
23+
if layer_type == 'Conv1d':
24+
weights = np.expand_dims(params[0], axis=2)
25+
if self.groups == 1:
26+
out = ng.convolution(inp, weights,
27+
[1, self.stride[0]],
28+
[0, self.padding[0]],
29+
[0, self.padding[0]],
30+
[1, self.dilation[0]])
31+
32+
else:
33+
weights = weights.reshape(self.groups, weights.shape[0] // self.groups, weights.shape[1], weights.shape[2], weights.shape[3])
34+
out = ng.group_convolution(inp, weights,
35+
[1, self.stride[0]],
36+
[0, self.padding[0]],
37+
[0, self.padding[0]],
38+
[1, self.dilation[0]])
39+
if len(params) > 1:
40+
assert(len(params) == 2)
41+
bias = params[1].reshape(1, params[1].shape[0], 1, 1)
42+
out = ng.add(out, bias)
43+
44+
elif layer_type == 'BatchNorm1d':
45+
out = ng.batch_norm_inference(inp, params[0], params[1], params[2], params[3], self.eps)
46+
elif layer_type == 'Swish':
47+
out = ng.swish(inp)
48+
elif layer_type == 'Add':
49+
out = ng.add(inp, nodes[inputs[1].data_ptr()])
50+
elif layer_type == 'Dropout':
51+
return
52+
elif layer_type == 'Permute':
53+
order = []
54+
# 1D to 2D: i.e. (2, 0, 1) -> (2, 3, 0, 1)
55+
for d in self.dims:
56+
assert(d <= 2)
57+
order.append(d)
58+
if d == 2:
59+
order.append(3)
60+
out = ng.transpose(inp, order)
61+
else:
62+
raise Exception('Unknown layer type: ' + layer_type)
63+
64+
nodes[output.data_ptr()] = out
65+
66+
67+
def sanity_check(net, inp, ref):
68+
ie = IECore()
69+
exec_net = ie.load_network(net, 'CPU')
70+
ie_out = exec_net.infer({'input': inp.numpy()})
71+
ie_out = next(iter(ie_out.values()))
72+
73+
ref = ref.numpy().reshape(ie_out.shape)
74+
diff = np.max(np.abs(ie_out - ref))
75+
print('PyTorch / OpenVINO diff:', diff)
76+
print('Reference values range: [{}, {}]'.format(np.min(ref), np.max(ref)))
77+
if diff > 1.1e-4:
78+
raise Exception('Sanity check failed with diff', diff)
79+
80+
81+
def torch2openvino(model):
82+
with torch.no_grad():
83+
model.eval()
84+
hooks = []
85+
for module in model.modules():
86+
if len([m for m in module.modules()]) != 1:
87+
continue
88+
hooks.append(module.register_forward_hook(forward_hook))
89+
90+
# Just a dummy input to make forward pass
91+
inp = Variable(torch.randn([1, 1, 1000]))
92+
93+
param = Parameter(Type.f32, Shape([1, 1, 1, 1000]))
94+
nodes[inp.data_ptr()] = param
95+
ref = model(inp)
96+
97+
for hook in hooks:
98+
hook.remove()
99+
100+
out_node = ng.log(ng.softmax(out, axis=3))
101+
102+
param.set_friendly_name('input')
103+
out_node.set_friendly_name('output')
104+
func = Function([out_node], [param], '')
105+
106+
caps = Function.to_capsule(func)
107+
net = IENetwork(caps)
108+
109+
# Uncomment to perform conversion check
110+
# sanity_check(net, inp, ref)
111+
112+
return net

bonito/openvino/model.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
import numpy as np
3+
import torch
4+
5+
try:
6+
from openvino.inference_engine import IECore, StatusCode
7+
from .loader import torch2openvino
8+
except ImportError:
9+
pass
10+
11+
class OpenVINOModel:
12+
13+
def __init__(self, model, half, dirname):
14+
self.model = model
15+
self.alphabet = model.alphabet
16+
self.parameters = model.parameters
17+
self.stride = model.stride
18+
19+
model_name = 'model' + ('_fp16' if half else '')
20+
xml_path, bin_path = [os.path.join(dirname, model_name) + ext for ext in ['.xml', '.bin']]
21+
self.ie = IECore()
22+
if os.path.exists(xml_path) and os.path.exists(bin_path):
23+
self.net = self.ie.read_network(xml_path, bin_path)
24+
else:
25+
self.net = torch2openvino(model)
26+
self.exec_net = None
27+
28+
29+
def eval(self):
30+
pass
31+
32+
33+
def half(self):
34+
return self
35+
36+
37+
def to(self, device):
38+
self.device = str(device).upper()
39+
40+
41+
def __call__(self, data):
42+
data = np.expand_dims(data, axis=2) # 1D->2D
43+
batch_size = data.shape[0]
44+
inp_shape = list(data.shape)
45+
inp_shape[0] = 1 # We will run the batch asynchronously
46+
if not self.exec_net or self.exec_net.input_info['input'].tensor_desc.dims != inp_shape:
47+
self.net.reshape({'input': inp_shape})
48+
config = {}
49+
if self.device == 'CPU':
50+
config={'CPU_THROUGHPUT_STREAMS': 'CPU_THROUGHPUT_AUTO'}
51+
self.exec_net = self.ie.load_network(self.net, self.device,
52+
config=config, num_requests=0)
53+
54+
# List that maps infer requests to index of processed chunk from batch.
55+
# -1 means that request has not been started yet.
56+
infer_request_input_id = [-1] * len(self.exec_net.requests)
57+
output = np.zeros([batch_size] + self.net.outputs['output'].shape[1:], dtype=np.float32)
58+
59+
for inp_id in range(batch_size):
60+
# Get idle infer request
61+
infer_request_id = self.exec_net.get_idle_request_id()
62+
if infer_request_id < 0:
63+
status = self.exec_net.wait(num_requests=1)
64+
if status != StatusCode.OK:
65+
raise Exception("Wait for idle request failed!")
66+
infer_request_id = self.exec_net.get_idle_request_id()
67+
if infer_request_id < 0:
68+
raise Exception("Invalid request id!")
69+
70+
out_id = infer_request_input_id[infer_request_id]
71+
request = self.exec_net.requests[infer_request_id]
72+
73+
# Copy output prediction
74+
if out_id != -1:
75+
output[out_id] = request.output_blobs['output'].buffer
76+
77+
# Start this request on new data
78+
infer_request_input_id[infer_request_id] = inp_id
79+
request.async_infer({'input': data[inp_id]})
80+
inp_id += 1
81+
82+
# Wait for the rest of requests
83+
status = self.exec_net.wait()
84+
if status != StatusCode.OK:
85+
raise Exception("Wait for idle request failed!")
86+
for infer_request_id, out_id in enumerate(infer_request_input_id):
87+
if out_id == -1:
88+
continue
89+
request = self.exec_net.requests[infer_request_id]
90+
output[out_id] = request.output_blobs['output'].buffer
91+
92+
output = np.squeeze(output, axis=2) # 2D->1D
93+
output = output.transpose(1, 0, 2) # Model should produce WNC (width, batch, features)
94+
return torch.tensor(output)
95+
96+
97+
def decode(self, x, beamsize=5, threshold=1e-3, qscores=False, return_path=False):
98+
return self.model.decode(x, beamsize=beamsize, threshold=threshold,
99+
qscores=qscores, return_path=return_path)

bonito/util.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import parasail
1818
import numpy as np
1919
from torch.cuda import get_device_capability
20+
from bonito.openvino.model import OpenVINOModel
2021

2122
try:
2223
from claragenomics.bindings import cuda
@@ -44,7 +45,7 @@ def init(seed, device):
4445
random.seed(seed)
4546
np.random.seed(seed)
4647
torch.manual_seed(seed)
47-
if device == "cpu": return
48+
if not device.startswith('cuda'): return
4849
torch.backends.cudnn.enabled = True
4950
torch.backends.cudnn.deterministic = True
5051
torch.backends.cudnn.benchmark = False
@@ -263,7 +264,7 @@ def match_names(state_dict, model):
263264
return OrderedDict([(k, remap[k]) for k in state_dict.keys()])
264265

265266

266-
def load_model(dirname, device, weights=None, half=None, chunksize=0):
267+
def load_model(dirname, device, weights=None, half=None, chunksize=0, use_openvino=False):
267268
"""
268269
Load a model from disk
269270
"""
@@ -276,14 +277,15 @@ def load_model(dirname, device, weights=None, half=None, chunksize=0):
276277
raise FileNotFoundError("no model weights found in '%s'" % dirname)
277278
weights = max([int(re.sub(".*_([0-9]+).tar", "\\1", w)) for w in weight_files])
278279

279-
device = torch.device(device)
280+
if not use_openvino:
281+
device = torch.device(device)
280282
config = toml.load(os.path.join(dirname, 'config.toml'))
281283
weights = os.path.join(dirname, 'weights_%s.tar' % weights)
282284

283285
Model = load_symbol(config, "Model")
284286
model = Model(config)
285287

286-
state_dict = torch.load(weights, map_location=device)
288+
state_dict = torch.load(weights, map_location=device if not use_openvino else 'cpu')
287289
state_dict = {k2: state_dict[k1] for k1, k2 in match_names(state_dict, model).items()}
288290
new_state_dict = OrderedDict()
289291
for k, v in state_dict.items():
@@ -292,6 +294,9 @@ def load_model(dirname, device, weights=None, half=None, chunksize=0):
292294

293295
model.load_state_dict(new_state_dict)
294296

297+
if use_openvino:
298+
model = OpenVINOModel(model, half, dirname)
299+
295300
if half is None:
296301
half = half_supported()
297302

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mappy==2.17
22
toml==0.10.0
33
tqdm==4.31.1
4-
numpy<=1.18.5
4+
numpy<=1.16.3
55
torch>=1.1.0,<=1.5
66
optuna==1.1.0
77
parasail==1.2
@@ -12,3 +12,4 @@ ont-fast5-api==3.1.6
1212
fast-ctc-decode==0.2.5
1313
#bonito-cuda-runtime==0.0.2a2
1414
#pyclaragenomics-cuda-10-0==0.4.2
15+
openvino-python==2021.1

0 commit comments

Comments
 (0)