blob: 1f07242457a005e33510ea9ca91441e5b2748503 [file] [log] [blame]
Tim Hall79d07d22020-04-27 18:20:16 +01001# Copyright (C) 2020 Arm Limited or its affiliates. All rights reserved.
2#
3# SPDX-License-Identifier: Apache-2.0
4#
5# Licensed under the Apache License, Version 2.0 (the License); you may
6# not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an AS IS BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18# Description:
19# Functions used to write to a TensorFlow Lite format file. Supports adding in file identifiers.
20
Diego Russoea6111a2020-04-14 18:41:58 +010021import numpy as np
Tim Hall79d07d22020-04-27 18:20:16 +010022import flatbuffers
Diego Russoea6111a2020-04-14 18:41:58 +010023from flatbuffers.builder import UOffsetTFlags
24
25# ugh, the python flatbuffer interface is missing a method to add in file identifier. patching it in here:
26import flatbuffers.number_types as N
27from flatbuffers import encode
Tim Hall79d07d22020-04-27 18:20:16 +010028
29from .tflite import Tensor
30from .tflite import QuantizationParameters
31from .tflite import Model
32from .tflite import SubGraph
33from .tflite import OperatorCode
34from .tflite import Operator
35from .tflite import Buffer
36from .tflite import Metadata
Tim Hall79d07d22020-04-27 18:20:16 +010037from .tflite_mapping import datatype_inv_map, builtin_operator_inv_map, custom_prefix, BuiltinOperator
38from .nn_graph import PassPlacement
39from .tensor import TensorPurpose, MemArea
Tim Hall79d07d22020-04-27 18:20:16 +010040
41tflite_version = 3
42tflite_file_identifier = "TFL" + str(tflite_version)
43
44
Tim Hall79d07d22020-04-27 18:20:16 +010045def FinishWithFileIdentifier(self, rootTable, fid):
46 if fid is None or len(fid) != 4:
47 raise Exception("fid must be 4 chars")
48
49 flags = N.Uint8Flags
50 prepSize = 4
51 self.Prep(self.minalign, prepSize + len(fid))
52 for i in range(3, -1, -1):
53 self.head = self.head - flags.bytewidth
54 encode.Write(flags.packer_type, self.Bytes, self.Head(), ord(fid[i]))
55
56 return self.Finish(rootTable)
57
58
59flatbuffers.Builder.FinishWithFileIdentifier = FinishWithFileIdentifier
60
61
62def make_vector(v):
63 try:
64 len(v)
65 return v
66 except TypeError:
67 return [v]
68
69
70class TFLiteSerialiser:
71 def __init__(self, nng):
72 self.builder = flatbuffers.Builder(0)
73 self.nng = nng
74
75 self.scratch_buf_id = 0 # Always assign scratch to buffer 0
76 self.buffer_offsets_map = {}
77 self.buffers_to_write = [] # have an empty array there
78
79 self.input_tensors = []
80 self.ops_to_ignore = set(("Const", "Placeholder", "SubgraphInput"))
81
82 self.tensors_to_reshape = {}
83
84 self.subgraphs_to_write = [sg for sg in self.nng.subgraphs if sg.placement == PassPlacement.Cpu]
85
86 all_ops = []
87 for sg in self.subgraphs_to_write:
88 for ps in sg.passes:
89 for op in ps.ops:
90 if op.type not in self.ops_to_ignore:
91 all_ops.append(op)
92 if op.type.startswith("Conv2D") or op.type.startswith("DepthwiseConv2d"):
93 self.tensors_to_reshape[op.inputs[1]] = (3, 0, 1, 2)
94 if op.type.startswith("FullyConnected"):
95 self.tensors_to_reshape[op.inputs[1]] = (1, 0)
96
97 self.operator_codes = list(sorted(set(op.type for op in all_ops)))
98 self.operator_code_map = {}
99
100 def write_byte_vector(self, v, alignment=1):
101 builder = self.builder
102 builder.StartVector(1, len(v), alignment)
103 for e in v[::-1]:
104 builder.PrependByte(e)
105 return builder.EndVector(len(v))
106
107 def write_int_vector(self, v):
108 builder = self.builder
109 builder.StartVector(4, len(v), 4)
110 for e in v[::-1]:
111 builder.PrependInt32(e)
112 return builder.EndVector(len(v))
113
114 def write_long_vector(self, v):
115 builder = self.builder
116 builder.StartVector(8, len(v), 8)
117 for e in v[::-1]:
118 builder.PrependInt64(e)
119 return builder.EndVector(len(v))
120
121 def write_float_vector(self, v):
122 builder = self.builder
123 builder.StartVector(4, len(v), 4)
124 for e in v[::-1]:
125 builder.PrependFloat32(e)
126 return builder.EndVector(len(v))
127
128 def write_offset_vector(self, v):
129 builder = self.builder
130 builder.StartVector(4, len(v), 4)
131 for e in v[::-1]:
132 builder.PrependUOffsetTRelative(e)
133 return builder.EndVector(len(v))
134
135 def assign_buffers_to_tensors(self, tensors):
136 buffer_map = {}
137 scratch_tensor = [tens for tens in tensors if tens.purpose == TensorPurpose.Scratch][0]
138 buf_idx = 1
139
140 for tens in tensors:
141 if tens.mem_area == scratch_tensor.mem_area:
142 buffer_map[tens] = self.scratch_buf_id
143 else:
144 buffer_map[tens] = buf_idx
145 buf_idx += 1
146
147 # Initialize buffers_to_write to a length equal to numer of buffers so
148 # they can be appended at the correct index during tensor serialization
149 self.buffers_to_write = [None] * (buf_idx)
150
151 return buffer_map
152
153 def serialise_operator_code(self, idx, code):
154 builder = self.builder
155 custom_code_offset = None
156 if code.startswith(custom_prefix):
157 tf_code, opt_serializer = builtin_operator_inv_map[custom_prefix]
158 custom_code_offset = builder.CreateString(code[len(custom_prefix) :])
159 else:
160 try:
161 tf_code, opt_serializer = builtin_operator_inv_map[code]
162 except KeyError:
163 print(
Diego Russoea6111a2020-04-14 18:41:58 +0100164 "Warning: Writing operation %s, which does not have a direct TensorFlow Lite mapping,"
165 "as a custom operation" % (code,)
Tim Hall79d07d22020-04-27 18:20:16 +0100166 )
167 tf_code, opt_serializer = builtin_operator_inv_map[custom_prefix]
168
169 if tf_code == BuiltinOperator.CUSTOM:
170 assert code == "NpuOp" # Currently only support serialising NPU operators as a custom op
171 custom_code_offset = builder.CreateString("ethos-u")
172
173 self.operator_code_map[code] = (idx, tf_code, opt_serializer)
174
175 OperatorCode.OperatorCodeStart(builder)
176 OperatorCode.OperatorCodeAddBuiltinCode(builder, tf_code)
177 if custom_code_offset is not None:
178 OperatorCode.OperatorCodeAddCustomCode(builder, custom_code_offset)
179
180 return OperatorCode.OperatorCodeEnd(builder)
181
182 def serialise_quantization_parameters(self, quant):
183 builder = self.builder
184
185 min = None
186 max = None
187 scale = None
188 zero_point = None
189 if quant is not None:
190 if quant.min is not None:
191 min = self.write_float_vector(make_vector(quant.min))
192 if quant.max is not None:
193 max = self.write_float_vector(make_vector(quant.max))
194 if quant.scale_f32 is not None:
195 scale = self.write_float_vector(make_vector(quant.scale_f32))
196 if quant.zero_point is not None:
197 zero_point = self.write_long_vector(make_vector(quant.zero_point))
198
199 QuantizationParameters.QuantizationParametersStart(builder)
200 if min is not None:
201 QuantizationParameters.QuantizationParametersAddMin(builder, min)
202 if max is not None:
203 QuantizationParameters.QuantizationParametersAddMax(builder, max)
204 if scale is not None:
205 QuantizationParameters.QuantizationParametersAddScale(builder, scale)
206 if zero_point is not None:
207 QuantizationParameters.QuantizationParametersAddZeroPoint(builder, zero_point)
208 return QuantizationParameters.QuantizationParametersEnd(builder)
209
210 def serialise_tensor(self, tens):
211 builder = self.builder
212 tens_shape = tens.shape
213 values = tens.quant_values
214 if values is None:
215 values = tens.values
216
217 if values is None:
218 values = np.empty(shape=(0), dtype=np.uint8)
219
220 if tens in self.tensors_to_reshape:
221 reorder = self.tensors_to_reshape[tens]
222 tens_shape = [tens_shape[idx] for idx in reorder]
223 values = values.transpose(reorder)
224
225 if tens.purpose == TensorPurpose.Scratch:
226 tens_shape = [0]
227 self.buffers_to_write[self.scratch_buf_id] = values.flatten().view(np.uint8)
228
229 buf_id = self.buffer_map[tens]
230 if buf_id != self.scratch_buf_id:
231 self.buffers_to_write[buf_id] = values.flatten().view(np.uint8)
232
233 shape = self.write_int_vector(tens_shape)
234
235 name = builder.CreateString(tens.name)
236 quant = self.serialise_quantization_parameters(tens.quantization)
237
238 Tensor.TensorStart(builder)
239 Tensor.TensorAddShape(builder, shape)
240 Tensor.TensorAddType(builder, datatype_inv_map[tens.dtype])
241 # All tensors must have a valid backing buffer, even if it is empty.
242 # Empty buffers should be kept unique for TensorFlow Lite Micro
243 Tensor.TensorAddBuffer(builder, buf_id)
244 Tensor.TensorAddName(builder, name)
245 Tensor.TensorAddQuantization(builder, quant)
246
247 res = Tensor.TensorEnd(builder)
248 return res
249
250 def serialise_operator(self, op):
251 builder = self.builder
252
253 inputs_offset = self.write_int_vector([self.tensor_map[tens] for tens in op.inputs])
254 outputs_offset = self.write_int_vector([self.tensor_map[tens] for tens in op.outputs])
255
256 op_idx, tflop, opt_serializer = self.operator_code_map[op.type]
257
258 builtin_opt_offset = None
259 custom_opt_offset = None
260 if opt_serializer is not None:
261 attrs = dict(op.attrs)
262 if "strides" in attrs:
263 attrs["stride_h"] = attrs["strides"][1]
264 attrs["stride_w"] = attrs["strides"][2]
265 if "ksize" in attrs:
266 attrs["filter_height"] = attrs["ksize"][1]
267 attrs["filter_width"] = attrs["ksize"][2]
268 if "dilation" in attrs:
269 attrs["dilation_h_factor"] = attrs["dilation"][1]
270 attrs["dilation_w_factor"] = attrs["dilation"][2]
271 if "channel_multiplier" in attrs:
272 attrs["depth_multiplier"] = attrs["channel_multiplier"]
273
274 builtin_opt_offset, custom_opt_offset = opt_serializer.serialize(builder, attrs)
275
276 mutating_variable_inputs_offset = self.write_byte_vector([])
277 Operator.OperatorStart(builder)
278 Operator.OperatorAddOpcodeIndex(builder, op_idx)
279 Operator.OperatorAddInputs(builder, inputs_offset)
280 Operator.OperatorAddOutputs(builder, outputs_offset)
281
282 if builtin_opt_offset is not None:
283 Operator.OperatorAddBuiltinOptionsType(builder, opt_serializer.builtin_opt_type)
284 Operator.OperatorAddBuiltinOptions(builder, builtin_opt_offset)
285 if custom_opt_offset is not None:
286 Operator.OperatorAddCustomOptions(builder, custom_opt_offset)
287 Operator.OperatorAddCustomOptionsFormat(builder, opt_serializer.custom_opt_format)
288
289 Operator.OperatorAddMutatingVariableInputs(builder, mutating_variable_inputs_offset)
290 return Operator.OperatorEnd(builder)
291
292 def serialise_subgraph(self, sg):
293 builder = self.builder
294 tensor_set = set()
295
296 all_ops = []
297 for ps in sg.passes:
298 for op in ps.ops:
299 if op.type not in self.ops_to_ignore:
300 all_ops.append(op)
301
302 for op in all_ops:
303 for tens in op.inputs + op.outputs:
304 tensor_set.add(tens)
305
306 all_tensors = [tens for nm, idx, tens in sorted((tens.name, idx, tens) for idx, tens in enumerate(tensor_set))]
307
308 self.tensor_map = {tens: idx for idx, tens in enumerate(all_tensors)}
309 self.buffer_map = self.assign_buffers_to_tensors(all_tensors)
310
311 tensors_offset = self.write_offset_vector([self.serialise_tensor(tens) for tens in all_tensors])
312
313 # Add the Scratch Tensor as input to the NPU subgraph to get it allocated by TensorFlow Lite Micro
314 scratch_tensor_idx = [v for k, v in self.tensor_map.items() if k.name.endswith("scratch")]
315
316 # Make sure the input_tensors haven't been modified
317 assert all(inp in sg.original_inputs for inp in sg.input_tensors)
318 inputs_offset = self.write_int_vector(
319 [self.tensor_map[tens] for tens in sg.original_inputs] + scratch_tensor_idx
320 )
321 outputs_offset = self.write_int_vector([self.tensor_map[tens] for tens in sg.output_tensors])
322
323 operators_offset = self.write_offset_vector([self.serialise_operator(op) for op in all_ops])
324
325 SubGraph.SubGraphStart(builder)
326 SubGraph.SubGraphAddTensors(builder, tensors_offset)
327 SubGraph.SubGraphAddInputs(builder, inputs_offset)
328 SubGraph.SubGraphAddOutputs(builder, outputs_offset)
329
330 SubGraph.SubGraphAddOperators(builder, operators_offset)
331
332 return SubGraph.SubGraphEnd(builder)
333
334 def write_aligned_bytes(self, buf):
335 builder = self.builder
336 builder.nested = True
337 data = bytes(buf)
338 length_bytes = UOffsetTFlags.py_type(len(data))
339 builder.Prep(16, length_bytes) # Reserve aligned storage
340 builder.head = UOffsetTFlags.py_type(builder.Head() - length_bytes) # Update FlatBuffer internal pointer
341 builder.Bytes[builder.Head() : builder.Head() + length_bytes] = data # Assign bytes to aligned area
342 return builder.EndVector(length_bytes)
343
344 def serialise_buffer(self, buf):
345 builder = self.builder
346 data = None
347 if buf is not None:
348 data = self.write_aligned_bytes(buf)
349 Buffer.BufferStart(builder)
350 if data is not None:
351 Buffer.BufferAddData(builder, data)
352 return Buffer.BufferEnd(builder)
353
354 def serialise_metadata(self, metadata):
355 builder = self.builder
356 name = builder.CreateString(metadata[0])
357
358 Metadata.MetadataStart(builder)
359 Metadata.MetadataAddName(builder, name)
360 Metadata.MetadataAddBuffer(builder, metadata[1])
361
362 return Metadata.MetadataEnd(builder)
363
364 def serialise_model(self):
365 builder = self.builder
366 operator_code_offset = self.write_offset_vector(
367 [self.serialise_operator_code(idx, code) for idx, code in enumerate(self.operator_codes)]
368 )
369
370 description = builder.CreateString("Vela Optimised")
371
372 subgraph_offset = self.write_offset_vector([self.serialise_subgraph(sg) for sg in self.subgraphs_to_write])
373
374 # Fill the metadata buffer
375 version = np.int32(0)
376 subgraph_idx = np.int32(len(self.subgraphs_to_write)) # Only 1 supported currently
377 nbr_tensors = np.int32(len(self.tensor_map))
378
379 # An offset of -1 indicates that the tensor will be allocated online by Tensorflow Lite Micro
380 offsets = [np.int32(-1)] * nbr_tensors
381
382 # Ensure that the order of the offsets match the order of the tensors
383 for tens, idx in self.tensor_map.items():
384 if tens.mem_area == MemArea.Sram:
385 offsets[idx] = np.int32(tens.address)
386
387 metadata_buffer = np.array([version, subgraph_idx, nbr_tensors] + offsets)
388 self.buffers_to_write.append(metadata_buffer)
389
390 buffers_offset = self.write_offset_vector([self.serialise_buffer(buf) for buf in self.buffers_to_write])
391
392 metadata_list = [("OfflineMemoryAllocation", len(self.buffers_to_write) - 1)]
393 metadata_offset = self.write_offset_vector([self.serialise_metadata(metadata) for metadata in metadata_list])
394
395 Model.ModelStart(builder)
396 Model.ModelAddVersion(builder, tflite_version)
397 Model.ModelAddOperatorCodes(builder, operator_code_offset)
398 Model.ModelAddSubgraphs(builder, subgraph_offset)
399 Model.ModelAddDescription(builder, description)
400 Model.ModelAddBuffers(builder, buffers_offset)
401 Model.ModelAddMetadata(builder, metadata_offset)
402 return Model.ModelEnd(builder)
403
404 def serialise(self):
405
406 model = self.serialise_model()
407
408 self.builder.FinishWithFileIdentifier(model, tflite_file_identifier)
409
410 return self.builder.Output()
411
412 def write(self, filename):
413 with open(self.filename, "wb") as f:
414 f.write(self.serialised_buf)
415
416
417def write_tflite(nng, filename):
418 writer = TFLiteSerialiser(nng)
419 buf = writer.serialise()
420
421 with open(filename, "wb") as f:
422 f.write(buf)