-
Notifications
You must be signed in to change notification settings - Fork 571
Softmax update #1494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Softmax update #1494
Changes from 37 commits
3d463b3
d678573
77258bc
59bd96f
dbb207b
0c59255
51efff0
6067bea
06fda4e
bf38a6b
e27fd11
9f4a448
16ca197
564b692
974e75a
060c398
f78558c
772b93a
d2b8921
a1ad891
d65544d
2d6a5cc
c3a4584
9b1cf17
31b7ad6
70b19d1
091cd8e
6c9f9a7
7a004b2
9d588f2
d5e3493
a56a8d6
29bdbb3
cab4cbc
42ece34
7e2798a
bd4778e
3946858
be76917
189f64a
e0aba71
584c4f7
711083a
6309fdb
b91a330
f990dbd
5bee49a
134787b
a90b79a
07ad4a4
aa5af6b
49d6ccd
3d7979a
aa678e0
5dadf1a
d8abde4
55e7454
947f195
10532da
232b2a3
84e0710
01d70a4
8d8bc8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| from hls4ml.backends.oneapi.oneapi_template import StreamFunctionCallTemplate, TaskSequenceTemplate | ||
| from hls4ml.backends.template import FunctionCallTemplate, LayerConfigTemplate | ||
| from hls4ml.model.layers import Activation, BatchNormalization, Dense, HardActivation, ParametrizedActivation, PReLU, Softmax | ||
| from hls4ml.utils.fixed_point_utils import ceil_log2 | ||
|
|
||
| # Dense templates | ||
|
|
||
|
|
@@ -195,11 +196,32 @@ def format(self, node): | |
| softmax_config_template = """struct {type}_config{index} : nnet::activ_config {{ | ||
| static constexpr unsigned n_in = {n_in}; | ||
| static constexpr unsigned table_size = {table_size}; | ||
| static constexpr unsigned exp_table_size = {exp_table_size}; | ||
| static constexpr unsigned inv_table_size = {inv_table_size}; | ||
| static constexpr unsigned io_type = nnet::{iotype}; | ||
| static constexpr unsigned reuse_factor = {reuse}; | ||
|
|
||
| static constexpr nnet::softmax_implementation implementation = nnet::softmax_implementation::{implementation}; | ||
| typedef {smax_accum_t} accum_t; | ||
| typedef {exp_table_t.name} exp_table_t; | ||
| typedef {inv_table_t.name} inv_table_t; | ||
| typedef {inv_table_t.name} inv_table_t;""" | ||
|
|
||
| softmax_config_table_template = """ | ||
|
|
||
| using {exp_table_name}_arr_t = nnet::array<exp_table_t, exp_table_size>; | ||
| using {inv_table_name}_arr_t = nnet::array<inv_table_t, inv_table_size>; | ||
| static constexpr const {exp_table_name}_arr_t exp_table = {exp_table_name}; | ||
| static constexpr const {inv_table_name}_arr_t invert_table = {inv_table_name}; | ||
|
jmitrevs marked this conversation as resolved.
|
||
| }};\n""" | ||
|
|
||
| softmax_config_table_template_stable = """ | ||
| typedef {inv_inp_t.name} inv_inp_t; | ||
| typedef {inp_norm_t.name} inp_norm_t; | ||
|
|
||
| using {exp_table_name}_arr_t = nnet::array<exp_table_t, exp_table_size>; | ||
| using {inv_table_name}_arr_t = nnet::array<inv_table_t, inv_table_size>; | ||
| static constexpr const {exp_table_name}_arr_t exp_table = {exp_table_name}; | ||
| static constexpr const {inv_table_name}_arr_t invert_table = {inv_table_name}; | ||
| }};\n""" | ||
|
|
||
| activ_function_template = 'nnet::{activation}<{input_t}, {output_t}, {config}>({input}, {output});' | ||
|
|
@@ -221,6 +243,71 @@ def format(self, node): | |
| params = self._default_config_params(node) | ||
| params['type'] = node.get_attr('activation') | ||
|
|
||
| if params['type'] == 'softmax': | ||
| # The lookup input (x - x_max) is always <= 0, so only the negative half | ||
| if 'exp_table_size' in params and params['exp_table_size'] is not None: | ||
| params['exp_table_size'] //= 2 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not divide this in half. The table size as given already takes the unsignedness into account. It doesn't make sense to expect people to give you twice the size of what they want implemented.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed this problem by assuming positive only types for softmax tables |
||
| else: | ||
| # Use the default precision | ||
| params['exp_table_size'] = 2 ** (params['table_t'].precision.width - 1) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default parameters should be defined https://github.com/fastmachinelearning/hls4ml/blob/main/hls4ml/backends/fpga/fpga_backend.py#L130 and similar. Note that the defaults are updated in #1476. I would remove all these updates here. You set the defaults in the attribute, not when reading the attributes.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed this logic, respecting the new defaults |
||
| params['exp_table_t'].precision.width = ceil_log2(params['exp_table_size']) | ||
| params['exp_table_t'].precision.integer = params['table_t'].precision.integer - 1 | ||
| params['exp_table_t'].precision.signed = False | ||
|
|
||
| params.setdefault('table_size', params['exp_table_size']) # Not sure if necessary | ||
|
|
||
| # Determine accumulator type if present, else derive it yourself based on the input size. | ||
| if params['accum_t'].name == 'model_default_t': | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be in
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
| extra_bits_req = ceil_log2(params['n_in']) | ||
| s = 'true' if params['exp_table_t'].precision.signed else 'false' | ||
| w = params['exp_table_t'].precision.width + extra_bits_req | ||
| i = params['exp_table_t'].precision.integer + extra_bits_req | ||
| params['smax_accum_t'] = f'ac_fixed<{str(w)},{str(i)},{s}>' | ||
| else: | ||
| params['smax_accum_t'] = params['accum_t'].name | ||
|
|
||
| if 'inp_norm_t' not in params: | ||
| input_t = node.get_input_variable().type.precision | ||
| width, iwidth, signed = input_t.width, input_t.integer, input_t.signed # noqa: F841 | ||
| width, iwidth = width - signed, iwidth - signed | ||
| import copy | ||
|
|
||
| params['inp_norm_t'] = copy.deepcopy(params['exp_table_t']) # assign type,later override | ||
|
|
||
| # This checks if table sizes will be default, if it is just use the table size to derive precision | ||
| if 'inv_table_size' not in params: | ||
| params['inp_norm_t'].precision.width = params['exp_table_t'].precision.width + 1 | ||
| params['inp_norm_t'].precision.integer = params['exp_table_t'].precision.integer + 1 | ||
| params['inp_norm_t'].precision.signed = True | ||
| params['inp_norm_t'].name = f'{node.name}_inp_norm_t' | ||
| else: | ||
| params[ | ||
| 'inp_norm_t' | ||
| ].name = f'ac_fixed<{width},{iwidth},{"true" if signed else "false"},AC_RND,AC_SAT_SYM>' | ||
|
|
||
| node.set_attr('inp_norm_t', params['inp_norm_t']) | ||
|
|
||
| # Again we only look up 1/sum(e^x) which is >=0 so no need the entie address space | ||
| if 'inv_table_size' in params: | ||
| params['inv_table_size'] //= 2 | ||
| else: | ||
| params['inv_table_size'] = 2 ** (params['table_t'].precision.width - 1) | ||
| params['inv_table_t'].precision.width = ceil_log2(params['inv_table_size']) | ||
| params['inv_table_t'].precision.integer = params['table_t'].precision.integer - 1 | ||
| params['inv_table_t'].precision.signed = False | ||
|
|
||
| params['inv_inp_t'].precision.width = params['inv_table_t'].precision.width + 1 | ||
| params['inv_inp_t'].precision.integer = params['inv_table_t'].precision.integer + 1 | ||
| params['inv_inp_t'].precision.signed = True | ||
|
|
||
| if params['implementation'] == 'stable': | ||
| self.template = softmax_config_template + softmax_config_table_template_stable | ||
| else: | ||
| self.template = softmax_config_template + softmax_config_table_template | ||
|
|
||
| params['exp_table_name'] = node.name + '_exp_table' | ||
| params['inv_table_name'] = node.name + '_inv_table' | ||
|
|
||
| return self.template.format(**params) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,12 +99,13 @@ template <class data_T, class res_T, typename CONFIG_T> void sigmoid(const data_ | |
|
|
||
| enum class softmax_implementation { latency = 0, legacy = 1, stable = 2, argmax = 3 }; | ||
|
|
||
| template <class data_T, typename CONFIG_T> inline unsigned softmax_stable_idx_from_real_val(const data_T x) { | ||
| template <class data_T, unsigned table_size> inline unsigned softmax_stable_idx_from_real_val(const data_T x) { | ||
| // Number of address bits for table | ||
| static constexpr int N = ceillog2<CONFIG_T::table_size>::val; | ||
| static constexpr int N = ceillog2<table_size>::val; | ||
|
|
||
| // Slice the top N bits of the input | ||
| [[intel::fpga_register]] ac_int<N, false> y = x.template slc<N>(x.width - N - 1); | ||
|
|
||
| // If x is the most negative value, the slice will be 0, so we need to set the 0-th bit to ensure correctness | ||
| if (x != 0 && y == 0) | ||
| y[0] = 1; | ||
|
|
@@ -121,38 +122,38 @@ template <class data_T, typename CONFIG_T> inline unsigned softmax_latency_idx_f | |
| } | ||
|
|
||
| template <class data_T, class res_T, typename CONFIG_T> void softmax_stable(const data_T &data, res_T &res) { | ||
| // Look-up tables | ||
| #include "activation_tables/exp_table.tb" | ||
| #include "activation_tables/invert_table.tb" | ||
|
|
||
| // Find maximum | ||
| Op_max<typename data_T::value_type> op_max; | ||
| [[intel::fpga_register]] auto x_max = | ||
| reduce<typename data_T::value_type, CONFIG_T::n_in, Op_max<typename data_T::value_type>>(data.data(), op_max); | ||
|
|
||
| // For the diffs, use the same type as the input but force rounding and saturation | ||
| [[intel::fpga_register]] ac_fixed<data_T::value_type::width, data_T::value_type::i_width, true, AC_RND, AC_SAT> | ||
| d_xi_xmax[CONFIG_T::n_in]; | ||
| [[intel::fpga_register]] typename CONFIG_T::inp_norm_t d_xi_xmax[CONFIG_T::n_in]; | ||
| #pragma unroll | ||
| for (unsigned i = 0; i < CONFIG_T::n_in; i++) { | ||
| d_xi_xmax[i] = data[i] - x_max; | ||
| } | ||
|
|
||
| // Calculate all the e^x's | ||
| [[intel::fpga_register]] typename CONFIG_T::exp_table_t exp_res[CONFIG_T::n_in]; | ||
| [[intel::fpga_register]] typename CONFIG_T::accum_t exp_res[CONFIG_T::n_in]; | ||
| #pragma unroll | ||
| for (unsigned i = 0; i < CONFIG_T::n_in; i++) { | ||
| exp_res[i] = exp_table[softmax_stable_idx_from_real_val<typename data_T::value_type, CONFIG_T>(d_xi_xmax[i])]; | ||
| exp_res[i] = | ||
| CONFIG_T::exp_table[softmax_stable_idx_from_real_val<typename CONFIG_T::inp_norm_t, CONFIG_T::exp_table_size>( | ||
| d_xi_xmax[i])]; // input_t, CONFIG_T | ||
| } | ||
|
|
||
| // Explicitly sum previously calculated exponentials with an adder tree | ||
| Op_add<typename CONFIG_T::exp_table_t> op_add; | ||
| [[intel::fpga_register]] typename CONFIG_T::exp_table_t exp_sum = | ||
| reduce<typename CONFIG_T::exp_table_t, CONFIG_T::n_in, Op_add<typename CONFIG_T::exp_table_t>>(exp_res, op_add); | ||
| Op_add<typename CONFIG_T::accum_t> op_add; | ||
| [[intel::fpga_register]] typename CONFIG_T::inv_inp_t exp_sum = | ||
| reduce<typename CONFIG_T::accum_t, CONFIG_T::n_in, Op_add<typename CONFIG_T::accum_t>>(exp_res, op_add); | ||
|
|
||
| // Multiply previously calculated exponetials with the reciprocal of the sum | ||
| [[intel::fpga_register]] typename CONFIG_T::inv_table_t inv_exp_sum = | ||
| invert_table[softmax_stable_idx_from_real_val<typename CONFIG_T::exp_table_t, CONFIG_T>(exp_sum)]; | ||
| CONFIG_T::invert_table[softmax_stable_idx_from_real_val<typename CONFIG_T::inv_inp_t, CONFIG_T::inv_table_size>( | ||
| exp_sum)]; | ||
|
|
||
| #pragma unroll | ||
| for (unsigned i = 0; i < CONFIG_T::n_in; i++) { | ||
| res[i] = exp_res[i] * inv_exp_sum; | ||
|
|
@@ -265,6 +266,45 @@ template <class data_T, class res_T, typename CONFIG_T> inline void softmax(cons | |
| } | ||
| } | ||
|
|
||
| // ************************************************* | ||
| // Multidimensional Softmax | ||
| // ************************************************* | ||
|
|
||
| // Helper to remap the config for the core softmax function | ||
| template <class CONFIG_T> struct softmax_multidim_slice_config : CONFIG_T { | ||
| static constexpr unsigned n_in = CONFIG_T::n_slice; | ||
| }; | ||
|
Comment on lines
+256
to
+263
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These added now as multidim backend is functional. |
||
|
|
||
| template <class data_T, class res_T, typename CONFIG_T> inline void softmax_multidim(const data_T &data, res_T &res) { | ||
| using buffer_data_t = std::array<typename data_T::value_type, CONFIG_T::n_slice>; | ||
| using buffer_res_t = std::array<typename res_T::value_type, CONFIG_T::n_slice>; | ||
| using slice_config = softmax_multidim_slice_config<CONFIG_T>; | ||
|
|
||
| #pragma unroll | ||
| for (unsigned i = 0; i < CONFIG_T::n_outer; i++) { | ||
| #pragma unroll | ||
| for (unsigned k = 0; k < CONFIG_T::n_inner; k++) { | ||
|
|
||
| [[intel::fpga_register]] buffer_data_t buffer_in; | ||
| [[intel::fpga_register]] buffer_res_t buffer_out; | ||
|
|
||
| // Gather Phase | ||
| #pragma unroll | ||
| for (unsigned j = 0; j < CONFIG_T::n_slice; j++) { | ||
| unsigned idx = (i * CONFIG_T::n_slice * CONFIG_T::n_inner) + (j * CONFIG_T::n_inner) + k; | ||
| buffer_in[j] = data[idx]; | ||
| } | ||
|
|
||
| nnet::softmax<buffer_data_t, buffer_res_t, slice_config>(buffer_in, buffer_out); | ||
|
|
||
| #pragma unroll | ||
| for (unsigned j = 0; j < CONFIG_T::n_slice; j++) { | ||
| unsigned idx = (i * CONFIG_T::n_slice * CONFIG_T::n_inner) + (j * CONFIG_T::n_inner) + k; | ||
| res[idx] = buffer_out[j]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // ************************************************* | ||
| // TanH Activation | ||
| // ************************************************* | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.