def get_output_datatype(self): """Returns FINN DataType of output.""" # we need to set output datatype to the next larger int or uint # enhancement: consider specifying w/ explicit outputDataType attribute # to allow overflow and use the same idt if user wants idt = DataType[self.get_nodeattr("inputDataType")] if idt.signed(): return DataType.get_smallest_possible(2 * idt.min()) else: return DataType.get_smallest_possible(2 * idt.max())
def generate_params(self, model, path): code_gen_dir = path # save thresholds in thresh.h thresholds = model.get_initializer(self.onnx_node.input[1]) threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds) min_threshold = thresholds.min() max_threshold = thresholds.max() min_input = self.get_input_datatype().min() max_input = self.get_input_datatype().max() # get range required by threshold values tdt_min = min(min_input, min_threshold) tdt_max = max(max_input, max_threshold) if tdt_min < 0: if abs(tdt_min) > tdt_max: tdt = DataType.get_smallest_possible(tdt_min) else: tdt = DataType.get_smallest_possible(0 - tdt_max - 1) else: tdt = DataType.get_smallest_possible(tdt_max) assert np.vectorize(tdt.allowed)( threshold_tensor ).all(), "Thresholds can't be expressed with type %s" % str(tdt) thresholds_hls_code = numpy_to_hls_code( threshold_tensor, tdt, "thresholds", False, True ) # write thresholds into thresh.h f_thresh = open("{}/thresh.h".format(code_gen_dir), "w") tdt_hls = tdt.get_hls_datatype_str() # use binary to export bipolar activations export_odt = self.get_output_datatype() if self.get_output_datatype() == DataType.BIPOLAR: export_odt = DataType.BINARY odt_hls = export_odt.get_hls_datatype_str() f_thresh.write( "static ThresholdsActivation<{},{},{},{},{},{},{}> threshs \ = ".format( self.calc_tmem(), self.get_nodeattr("PE"), threshold_tensor.shape[-1], tdt_hls, odt_hls, self.get_nodeattr("ActVal"), "std::less_equal<%s>" % tdt_hls, ) ) f_thresh.write(thresholds_hls_code) f_thresh.close()
def make_labelselect_modelwrapper(labels, pe, k, idt): inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, [1, labels]) outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, [1, k]) labelselect_node = helper.make_node( "LabelSelect_Batch", ["inp"], ["outp"], domain="finn", backend="fpgadataflow", Labels=labels, PE=pe, K=k, inputDataType=idt.name, ) graph = helper.make_graph( nodes=[labelselect_node], name="graph", inputs=[inp], outputs=[outp], ) model = helper.make_model(graph, producer_name="thresholding-model") model = ModelWrapper(model) model.set_tensor_datatype("inp", idt) odt = DataType.get_smallest_possible(labels - 1) model.set_tensor_datatype("outp", odt) return model
def get_smallest_possible(vals): """Returns smallest (fewest bits) possible DataType that can represent value. Prefers unsigned integers where possible.""" vals = np.array(vals) for v in vals: assert int(v) == v, "Error float value" for k in DataType.get_accumulator_dt_cands(): dt = DataType[k] if dt in [DataType["BIPOLAR"], DataType["TERNARY"], DataType["FLOAT32"]]: # not currently supported continue if (dt.min() <= vals).all() and (vals <= dt.max()).all(): return dt warnings.warn( """InferChannelwiseLinearLayer: Output values may not be representable with supported data types. Setting maximum width data type available. This will lead to errors if there are no constrains on the input """ ) if (0 <= vals).all(): return DataType["UINT64"] else: return DataType["INT64"]
def apply(self, model): graph = model.graph node_ind = 0 graph_modified = False for n in graph.node: # search for (MultiThreshold, Add) pair node_ind += 1 if ( n.op_type == "MultiThreshold" and not model.is_fork_node(n) and not model.is_join_node(n) ): consumer = model.find_consumer(n.output[0]) if consumer is not None and consumer.op_type == "Add": mt_node = n add_node = consumer threshold_name = mt_node.input[1] add_weight_name = add_node.input[1] T = model.get_initializer(threshold_name) A = model.get_initializer(add_weight_name) if (A is None) or (T is None): warnings.warn("Threshold or add bias not constant, skipping") continue end_name = add_node.output[0] # we can only absorb scalar adds is_scalar = A.ndim == 0 or all(x == 1 for x in A.shape) if not is_scalar: continue bias = A.flatten()[0] # set MultiThreshold bias property mt_inst = getCustomOp(mt_node) bias += mt_inst.get_nodeattr("out_bias") mt_inst.set_nodeattr("out_bias", bias) graph_modified = True # compute new DataType for MultiThreshold output steps = T.shape[-1] new_min = bias new_max = steps + bias odt = DataType.get_smallest_possible(steps).name.replace( "UINT", "INT" ) odt = DataType[odt] assert odt.allowed(new_max) and odt.allowed( new_min ), """Could not compute new MultiThreshold DataType (min = %d max = %d)""" % ( new_min, new_max, ) mt_inst.set_nodeattr("out_dtype", odt.name) # remove Add node, rewire MultiThreshold graph.node.remove(add_node) mt_node.output[0] = end_name # set datatype model.set_tensor_datatype(end_name, odt) if graph_modified: model = model.transform(InferDataTypes()) return (model, graph_modified)
def get_output_datatype(self): """Returns FINN DataType of output.""" # determine data type from image size and input type idt = DataType[self.get_nodeattr("inputDataType")] vecs = list(self.get_nodeattr("numInputVectors")) npixels = vecs[-1] * vecs[-2] if idt.signed(): extreme_value = npixels * idt.min() else: extreme_value = npixels * idt.max() return DataType.get_smallest_possible(extreme_value)
def get_smallest_possible(self, vals): """Returns smallest (fewest bits) possible DataType that can represent value. Prefers unsigned integers where possible.""" vals = np.array(vals) for v in vals: assert int(v) == v, "Error float value" cands = DataType.get_accumulator_dt_cands() for k in cands: dt = DataType[k] if (dt.min() <= vals).all() and (vals <= dt.max()).all(): return dt
def minimize_accumulator_width(self, model): "Minimize threshold width ('accumulator width' here due to convention)" thresholds = model.get_initializer(self.onnx_node.input[1]) threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds) min_threshold = thresholds.min() max_threshold = thresholds.max() min_input = self.get_input_datatype().min() max_input = self.get_input_datatype().max() # get range required by threshold values tdt_min = min(min_input, min_threshold) tdt_max = max(max_input, max_threshold) if tdt_min < 0: if abs(tdt_min) > tdt_max: tdt = DataType.get_smallest_possible(tdt_min) else: tdt = DataType.get_smallest_possible(0 - tdt_max - 1) else: tdt = DataType.get_smallest_possible(tdt_max) assert np.vectorize(tdt.allowed)(threshold_tensor).all( ), "Thresholds can't be expressed with type %s" % str(tdt) self.set_nodeattr("weightDataType", tdt.name) return DataType[self.get_nodeattr("weightDataType")]
def __init__(self, onnx_node): super().__init__(onnx_node) odt_name = self.get_nodeattr("outputDataType") if odt_name == "": # If not provided compute min size labels = self.get_nodeattr("Labels") odt = DataType.get_smallest_possible(labels - 1) # ensure a datatype divisible by 8-bits in case this is the last node bw = roundup_to_integer_multiple(odt.bitwidth(), 8) new_odt_name = odt.name.replace(str(odt.bitwidth()), str(bw)) odt = DataType[new_odt_name] odt_name = odt.name self.set_nodeattr("outputDataType", odt_name)
def minimize_accumulator_width(self, model): weights = model.get_initializer(self.onnx_node.input[1]) if len(self.onnx_node.input) > 2: thresholds = model.get_initializer(self.onnx_node.input[2]) else: thresholds = None idt = self.get_input_datatype() # calculate minimum and maximum values of accumulator (acc_min, acc_max) = calculate_matvec_accumulator_range(weights, idt) if thresholds is not None: threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds) # set threshold datatype (and accumulator datatype implicitly) min_threshold = thresholds.min() max_threshold = thresholds.max() # get range required by threshold values tdt_min = min(acc_min, min_threshold) tdt_max = max(acc_max, max_threshold) if tdt_min < 0: if abs(tdt_min) > tdt_max: tdt = DataType.get_smallest_possible(tdt_min) else: tdt = DataType.get_smallest_possible(0 - tdt_max) else: tdt = DataType.get_smallest_possible(tdt_max) assert np.vectorize(tdt.allowed)( threshold_tensor ).all(), "Thresholds can't be expressed with type %s" % str(tdt) self.set_nodeattr("accDataType", tdt.name) else: if acc_min < 0: if abs(acc_min) > acc_max: adt = DataType.get_smallest_possible(acc_min) else: adt = DataType.get_smallest_possible(0 - acc_max) else: adt = DataType.get_smallest_possible(acc_max) # ensure a datatype divisible by 8-bits in case this is the last node bw = roundup_to_integer_multiple(adt.bitwidth(), 8) new_adt_name = adt.name.replace(str(adt.bitwidth()), str(bw)) adt = DataType[new_adt_name] self.set_nodeattr("accDataType", adt.name) # for no-activation nodes, output dt = acc dt self.set_nodeattr("outputDataType", adt.name) return DataType[self.get_nodeattr("accDataType")]
def test_smallest_possible(): assert DataType.get_smallest_possible(1) == DataType["BINARY"] assert DataType.get_smallest_possible(1.1) == DataType["FLOAT32"] assert DataType.get_smallest_possible(-1) == DataType["BIPOLAR"] assert DataType.get_smallest_possible(-3) == DataType["INT3"] assert DataType.get_smallest_possible(-3.2) == DataType["FLOAT32"]
def minimize_accumulator_width(self, model): weights = model.get_initializer(self.onnx_node.input[1]) k_h, k_w = self.get_nodeattr("Kernel") fm = self.get_nodeattr("Channels") # put weights into the shape expected by calculate_matvec_accumulator_range weights = weights.reshape(fm, k_h * k_w).transpose() if len(self.onnx_node.input) > 2: thresholds = model.get_initializer(self.onnx_node.input[2]) else: thresholds = None idt = self.get_input_datatype() # calculate minimum and maximum values of accumulator (acc_min, acc_max) = calculate_matvec_accumulator_range(weights, idt) if thresholds is not None: threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds) # set threshold datatype (and accumulator datatype implicitly) min_threshold = thresholds.min() max_threshold = thresholds.max() # clip threshold values clip_upper = None clip_lower = None if max_threshold > acc_max + 1: clip_upper = acc_max + 1 if min_threshold < acc_min: clip_lower = acc_min if (clip_lower is not None) or (clip_upper is not None): warnings.warn("Clipping some thresholds in %s" % self.onnx_node.name) thresholds = np.clip(thresholds, clip_lower, clip_upper) model.set_initializer(self.onnx_node.input[2], thresholds) threshold_tensor = self.get_hls_compatible_threshold_tensor(thresholds) min_threshold = thresholds.min() max_threshold = thresholds.max() # get range required by threshold values tdt_min = min(acc_min, min_threshold) tdt_max = max(acc_max, max_threshold) if tdt_min < 0: if abs(tdt_min) > tdt_max: tdt = DataType.get_smallest_possible(tdt_min) else: tdt = DataType.get_smallest_possible(0 - tdt_max) else: tdt = DataType.get_smallest_possible(tdt_max) assert np.vectorize(tdt.allowed)( threshold_tensor ).all(), "Thresholds in %s can't be expressed with type %s" % ( self.onnx_node.name, str(tdt), ) self.set_nodeattr("accDataType", tdt.name) else: if acc_min < 0: if abs(acc_min) > acc_max: adt = DataType.get_smallest_possible(acc_min) else: adt = DataType.get_smallest_possible(0 - acc_max) else: adt = DataType.get_smallest_possible(acc_max) # ensure a datatype divisible by 8-bits in case this is the last node bw = roundup_to_integer_multiple(adt.bitwidth(), 8) new_adt_name = adt.name.replace(str(adt.bitwidth()), str(bw)) adt = DataType[new_adt_name] self.set_nodeattr("accDataType", adt.name) # for no-activation nodes, output dt = acc dt self.set_nodeattr("outputDataType", adt.name) return DataType[self.get_nodeattr("accDataType")]
def test_smallest_possible(): assert DataType.get_smallest_possible(1) == DataType.BINARY assert DataType.get_smallest_possible(1.1) == DataType.FLOAT32 assert DataType.get_smallest_possible(-1) == DataType.BIPOLAR assert DataType.get_smallest_possible(-3) == DataType.INT3 assert DataType.get_smallest_possible(-3.2) == DataType.FLOAT32