From 091cd8ec65d261a6c436b0ed28e7cbb25b119868 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 20 May 2026 12:08:26 -0500 Subject: [PATCH 01/12] fix overflow for legacy softmax, start looking at others --- hls4ml/backends/fpga/fpga_backend.py | 21 +++++++++--- .../model/optimizer/passes/infer_precision.py | 23 +++++++++++++ .../vivado/nnet_utils/nnet_activation.h | 33 ++++++++++--------- .../nnet_utils/nnet_activation_stream.h | 12 +++---- 4 files changed, 62 insertions(+), 27 deletions(-) diff --git a/hls4ml/backends/fpga/fpga_backend.py b/hls4ml/backends/fpga/fpga_backend.py index db0f256172..1518f1a04b 100644 --- a/hls4ml/backends/fpga/fpga_backend.py +++ b/hls4ml/backends/fpga/fpga_backend.py @@ -129,22 +129,33 @@ def __init__(self, name): ConfigurableAttribute('skip', value_type=bool, default=False, description=descriptions.softmax_skip), TypeAttribute( 'exp_table', - default=FixedPrecisionType(18, 8, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT), + default=FixedPrecisionType( + 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT + ), description=descriptions.table_type, ), TypeAttribute( 'inv_table', - default=FixedPrecisionType(18, 8, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT), + default=FixedPrecisionType( + 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT + ), description=descriptions.table_type, ), TypeAttribute( 'inv_inp', - default=FixedPrecisionType(18, 8, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT), + default=FixedPrecisionType( + 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT + ), + description='What the accumulated value is cast to before accessing the inversion table (only in stable)', ), TypeAttribute( - 'accum', - default=FixedPrecisionType(18, 8, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT), + 'inp_norm', + default=FixedPrecisionType( + 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT + ), + description='The internal width used for the exp table lookup (only in stable)', ), + TypeAttribute('accum', description=descriptions.accum_type), ] self.attribute_map[Softmax] = softmax_attrs diff --git a/hls4ml/model/optimizer/passes/infer_precision.py b/hls4ml/model/optimizer/passes/infer_precision.py index 0bc29a2955..970a8f9ead 100644 --- a/hls4ml/model/optimizer/passes/infer_precision.py +++ b/hls4ml/model/optimizer/passes/infer_precision.py @@ -90,6 +90,9 @@ def _infer_precision(self, node, types_to_infer): if node_class in ['PReLU']: return self._infer_prelu_act_precision(node, types_to_infer) + + if node_class in ['Softmax']: + return self._infer_softmax_precision(node, types_to_infer) # What about quantized activation layer? Setting it to 'auto' manually will break it here. We should prevent # this in config_from_* functions @@ -605,6 +608,26 @@ def _infer_prelu_act_precision(self, node, types_to_infer): return inferred_types + def _infer_softmax_precision(self, node, types_to_infer): + inferred_types = [] + + # for softmax, the table parameters have a default seting, so they don't need to be inferred + # here. We never expect them to be of type auto. + + # For result, we leave it to be set externally (model default if not set). We expect it to + # likely be the output value, in which case the output format would determine it's precision. + # Therefore, only the accum is configured here + + if 'accum_t' in types_to_infer: + exp_w = node.types['exp_table_t'].precision.width + exp_i = node.types['exp_table_t'].precision.integer + exp_s = node.types['exp_table_t'].precision.signed + ceillog = math.ceil(np.log2(node.get_attr('n_in'))) + node.types['accum_t'].precision = FixedPrecisionType(exp_w + ceillog, exp_i + ceillog, signed=exp_s) + inferred_types.append('accum_t') + + return inferred_types + def _get_precision_from_constant(value: int | float, max_width=8): """A utility function to find a fixed type to store the constant diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h index ac85e0b2cc..db771a581a 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h @@ -165,7 +165,7 @@ template void init_invert_table(typename CONFIG_T::inv_table_t table_out[CONFIG_T::inv_table_size]) { // The template data_T is the data type used to address the table for (unsigned i = 0; i < CONFIG_T::inv_table_size; i++) { - float x = softmax_real_val_from_idx(i); + float x = softmax_real_val_from_idx(i); typename CONFIG_T::inv_table_t inv_x = 1 / x; table_out[i] = inv_x; } @@ -196,7 +196,6 @@ void softmax_latency(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice // Calculate all the e^x's typename CONFIG_T::accum_t exp_res[CONFIG_T::n_slice]; #pragma HLS array_partition variable=exp_res complete - typename CONFIG_T::inv_inp_t exp_sum(0); for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { #pragma HLS unroll unsigned x = softmax_idx_from_real_val(data[i]); @@ -205,11 +204,12 @@ void softmax_latency(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice // Explicitly sum the results with an adder tree. // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + typename CONFIG_T::accum_t exp_sum(0); Op_add op_add; exp_sum = reduce>(exp_res, op_add); typename CONFIG_T::inv_table_t inv_exp_sum = - invert_table[softmax_idx_from_real_val(exp_sum)]; + invert_table[softmax_idx_from_real_val(exp_sum)]; for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { #pragma HLS unroll res[i] = exp_res[i] * inv_exp_sum; @@ -251,7 +251,6 @@ void softmax_stable(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] // Calculate all the e^x's typename CONFIG_T::accum_t exp_res[CONFIG_T::n_slice]; #pragma HLS array_partition variable=exp_res complete - typename CONFIG_T::inv_inp_t exp_sum(0); for (unsigned i = 0; i < CONFIG_T::n_slice; i++) { #pragma HLS unroll unsigned x = softmax_idx_from_real_val(d_xi_xmax[i]); @@ -260,6 +259,7 @@ void softmax_stable(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] // Explicitly sum the results with an adder tree. // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing + typename CONFIG_T::inv_inp_t exp_sum(0); Op_add op_add; exp_sum = reduce>(exp_res, op_add); @@ -271,18 +271,18 @@ void softmax_stable(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] } } -template void init_exp_table_legacy(typename CONFIG_T::table_t table_out[N_TABLE]) { +template void init_exp_table_legacy(typename CONFIG_T::exp_table_t table_out[N_TABLE]) { for (int ii = 0; ii < N_TABLE; ii++) { // First, convert from table index to X-value (signed 8-bit, range -8 to +8) float in_val = 2 * 8.0 * (ii - float(N_TABLE) / 2.0) / float(N_TABLE); // Next, compute lookup table function - typename CONFIG_T::table_t real_val = exp_fcn_float(in_val); + typename CONFIG_T::exp_table_t real_val = exp_fcn_float(in_val); // std::cout << "Lookup table In Value: " << in_val << " Result: " << real_val << std::endl; table_out[ii] = real_val; } } -template void init_invert_table_legacy(typename CONFIG_T::table_t table_out[N_TABLE]) { +template void init_invert_table_legacy(typename CONFIG_T::inv_table_t table_out[N_TABLE]) { // Inversion function: // result = 1/x for (int ii = 0; ii < N_TABLE; ii++) { @@ -301,12 +301,12 @@ void softmax_legacy(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] // Initialize the lookup table #ifdef __HLS_SYN__ bool initialized = false; - typename CONFIG_T::table_t exp_table[CONFIG_T::exp_table_size]; - typename CONFIG_T::table_t invert_table[CONFIG_T::inv_table_size]; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; #else static bool initialized = false; - static typename CONFIG_T::table_t exp_table[CONFIG_T::exp_table_size]; - static typename CONFIG_T::table_t invert_table[CONFIG_T::inv_table_size]; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; #endif if (!initialized) { init_exp_table_legacy(exp_table); @@ -317,22 +317,23 @@ void softmax_legacy(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] #pragma HLS PIPELINE // Index into the lookup table based on data for exponentials - typename CONFIG_T::table_t exp_res[CONFIG_T::n_slice]; // different, independent, fixed point precision - typename CONFIG_T::table_t exp_diff_res; // different, independent, fixed point precision + typename CONFIG_T::accum_t exp_res[CONFIG_T::n_slice]; // different, independent, fixed point precision + typename CONFIG_T::exp_table_t exp_diff_res; // different, independent, fixed point precision data_T data_cache[CONFIG_T::n_slice]; - int data_round; int index; + for (int ii = 0; ii < CONFIG_T::n_slice; ii++) { data_cache[ii] = data[ii]; exp_res[ii] = 0; } + // first calculate 1/softmax as a sum over fractions. for (int ii = 0; ii < CONFIG_T::n_slice; ii++) { for (int jj = 0; jj < CONFIG_T::n_slice; jj++) { if (ii == jj) exp_diff_res = 1; else { - data_round = (data_cache[jj] - data_cache[ii]) * CONFIG_T::exp_table_size / 16; + auto data_round = (data_cache[jj] - data_cache[ii]) * CONFIG_T::exp_table_size / 16; index = data_round + 8 * CONFIG_T::exp_table_size / 16; if (index < 0) index = 0; @@ -352,7 +353,7 @@ void softmax_legacy(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] if (exp_res_index > CONFIG_T::inv_table_size - 1) exp_res_index = CONFIG_T::inv_table_size - 1; // typename CONFIG_T::table_t exp_res_invert = invert_table[exp_res_index]; - res[ii] = (res_T)invert_table[exp_res_index]; + res[ii] = static_cast(invert_table[exp_res_index]); } } diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h index 50c6c4068c..e03adc5d58 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h @@ -249,12 +249,12 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { // Initialize the lookup table #ifdef __HLS_SYN__ bool initialized = false; - typename CONFIG_T::table_t exp_table[CONFIG_T::table_size]; - typename CONFIG_T::table_t invert_table[CONFIG_T::table_size]; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::table_size]; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::table_size]; #else static bool initialized = false; - static typename CONFIG_T::table_t exp_table[CONFIG_T::table_size]; - static typename CONFIG_T::table_t invert_table[CONFIG_T::table_size]; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::table_size]; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::table_size]; #endif if (!initialized) { init_exp_table_legacy(exp_table); @@ -263,8 +263,8 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { } // Index into the lookup table based on data for exponentials - typename CONFIG_T::table_t exp_res[data_T::size]; - typename CONFIG_T::table_t exp_diff_res; + typename CONFIG_T::accum_t exp_res[data_T::size]; + typename CONFIG_T::exp_table_t exp_diff_res; typename data_T::value_type data_cache[data_T::size]; SoftmaxInitLoop: From 6c9f9a7330eb44600c995d0df01448bf45d6265c Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 20 May 2026 15:54:33 -0500 Subject: [PATCH 02/12] fix stable and streaming legacy softmax --- .../vivado/nnet_utils/nnet_activation.h | 6 +++--- .../nnet_utils/nnet_activation_stream.h | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h index db771a581a..16e78a1d1e 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h @@ -165,7 +165,7 @@ template void init_invert_table(typename CONFIG_T::inv_table_t table_out[CONFIG_T::inv_table_size]) { // The template data_T is the data type used to address the table for (unsigned i = 0; i < CONFIG_T::inv_table_size; i++) { - float x = softmax_real_val_from_idx(i); + float x = softmax_real_val_from_idx(i); typename CONFIG_T::inv_table_t inv_x = 1 / x; table_out[i] = inv_x; } @@ -259,9 +259,9 @@ void softmax_stable(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice] // Explicitly sum the results with an adder tree. // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing - typename CONFIG_T::inv_inp_t exp_sum(0); Op_add op_add; - exp_sum = reduce>(exp_res, op_add); + typename CONFIG_T::inv_inp_t exp_sum = + reduce>(exp_res, op_add); typename CONFIG_T::inv_table_t inv_exp_sum = invert_table[softmax_idx_from_real_val(exp_sum)]; diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h index e03adc5d58..af1e444f13 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h @@ -216,7 +216,6 @@ void softmax_stable(hls::stream &data, hls::stream &res) { // Calculate all the e^x's typename CONFIG_T::accum_t exp_res[data_T::size]; #pragma HLS ARRAY_PARTITION variable=exp_res complete - typename CONFIG_T::inv_inp_t exp_sum(0); for (unsigned j = 0; j < data_T::size; j++) { #pragma HLS UNROLL unsigned x = softmax_idx_from_real_val(d_xi_xmax[j]); @@ -226,7 +225,8 @@ void softmax_stable(hls::stream &data, hls::stream &res) { // Explicitly sum the results with an adder tree. // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing Op_add op_add; - exp_sum = reduce>(exp_res, op_add); + typename CONFIG_T::inv_inp_t exp_sum = + reduce>(exp_res, op_add); typename CONFIG_T::inv_table_t inv_exp_sum = invert_table[softmax_idx_from_real_val(exp_sum)]; @@ -249,16 +249,16 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { // Initialize the lookup table #ifdef __HLS_SYN__ bool initialized = false; - typename CONFIG_T::exp_table_t exp_table[CONFIG_T::table_size]; - typename CONFIG_T::inv_table_t invert_table[CONFIG_T::table_size]; + typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; #else static bool initialized = false; - static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::table_size]; - static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::table_size]; + static typename CONFIG_T::exp_table_t exp_table[CONFIG_T::exp_table_size]; + static typename CONFIG_T::inv_table_t invert_table[CONFIG_T::inv_table_size]; #endif if (!initialized) { - init_exp_table_legacy(exp_table); - init_invert_table_legacy(invert_table); + init_exp_table_legacy(exp_table); + init_invert_table_legacy(invert_table); initialized = true; } @@ -288,7 +288,7 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { if (i == j) { exp_diff_res = 1; } else { - int data_round = (data_cache[j] - data_cache[i]) * CONFIG_T::table_size / 16; + auto data_round = (data_cache[j] - data_cache[i]) * CONFIG_T::table_size / 16; int index = data_round + 8 * CONFIG_T::table_size / 16; if (index < 0) index = 0; @@ -314,7 +314,7 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { if (exp_res_index > CONFIG_T::table_size - 1) exp_res_index = CONFIG_T::table_size - 1; - out_pack[j] = (typename res_T::value_type)invert_table[exp_res_index]; + out_pack[j] = static_cast(invert_table[exp_res_index]); } res.write(out_pack); } From 7a004b2752afe54b516e1236bf9a4415cd5ae1c4 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 20 May 2026 16:47:54 -0500 Subject: [PATCH 03/12] minor softmax latency fixes --- hls4ml/templates/vivado/nnet_utils/nnet_activation.h | 6 +++--- hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h index 16e78a1d1e..b262c86859 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation.h @@ -189,7 +189,7 @@ void softmax_latency(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice // Note we are exponentiating the inputs, which have type data_T init_exp_table(exp_table); // Note we are inverting the exponentials, which have type exp_table_t - init_invert_table(invert_table); + init_invert_table(invert_table); initialized = true; } @@ -204,9 +204,9 @@ void softmax_latency(data_T data[CONFIG_T::n_slice], res_T res[CONFIG_T::n_slice // Explicitly sum the results with an adder tree. // Rounding & Saturation mode, which improve accuracy, prevent Vivado from expression balancing - typename CONFIG_T::accum_t exp_sum(0); Op_add op_add; - exp_sum = reduce>(exp_res, op_add); + typename CONFIG_T::accum_t exp_sum = + reduce>(exp_res, op_add); typename CONFIG_T::inv_table_t inv_exp_sum = invert_table[softmax_idx_from_real_val(exp_sum)]; diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h index af1e444f13..ad1aa77ec3 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h @@ -150,7 +150,7 @@ void softmax_latency(hls::stream &data, hls::stream &res) { exp_sum = reduce>(exp_res, op_add); typename CONFIG_T::inv_table_t inv_exp_sum = - invert_table[softmax_idx_from_real_val(exp_sum)]; + invert_table[softmax_idx_from_real_val(exp_sum)]; res_T out_pack; PRAGMA_DATA_PACK(out_pack) From d5e3493c124a7c3314dd9e5c66f9f81d02896791 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Mon, 8 Jun 2026 18:29:28 -0500 Subject: [PATCH 04/12] fix copilot review issues (other than adding a test) --- hls4ml/model/optimizer/passes/infer_precision.py | 2 +- .../vivado/nnet_utils/nnet_activation_stream.h | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hls4ml/model/optimizer/passes/infer_precision.py b/hls4ml/model/optimizer/passes/infer_precision.py index 970a8f9ead..189ffb7dda 100644 --- a/hls4ml/model/optimizer/passes/infer_precision.py +++ b/hls4ml/model/optimizer/passes/infer_precision.py @@ -611,7 +611,7 @@ def _infer_prelu_act_precision(self, node, types_to_infer): def _infer_softmax_precision(self, node, types_to_infer): inferred_types = [] - # for softmax, the table parameters have a default seting, so they don't need to be inferred + # for softmax, the table parameters have a default setting, so they don't need to be inferred # here. We never expect them to be of type auto. # For result, we leave it to be set externally (model default if not set). We expect it to diff --git a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h index ad1aa77ec3..814ed2f50a 100644 --- a/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h +++ b/hls4ml/templates/vivado/nnet_utils/nnet_activation_stream.h @@ -120,8 +120,8 @@ void softmax_latency(hls::stream &data, hls::stream &res) { if (!initialized) { // Note we are exponentiating the inputs, which have type data_T init_exp_table(exp_table); - // Note we are inverting the exponentials, which have type exp_table_t - init_invert_table(invert_table); + // Note we are inverting the summed exponentials, which have type accum_t + init_invert_table(invert_table); initialized = true; } @@ -292,8 +292,8 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { int index = data_round + 8 * CONFIG_T::table_size / 16; if (index < 0) index = 0; - if (index > CONFIG_T::table_size - 1) - index = CONFIG_T::table_size - 1; + if (index > CONFIG_T::exp_table_size - 1) + index = CONFIG_T::exp_table_size - 1; exp_diff_res = exp_table[index]; } @@ -311,8 +311,8 @@ void softmax_legacy(hls::stream &data, hls::stream &res) { int exp_res_index = exp_res[j] * CONFIG_T::table_size / 64; if (exp_res_index < 0) exp_res_index = 0; - if (exp_res_index > CONFIG_T::table_size - 1) - exp_res_index = CONFIG_T::table_size - 1; + if (exp_res_index > CONFIG_T::inv_table_size - 1) + exp_res_index = CONFIG_T::inv_table_size - 1; out_pack[j] = static_cast(invert_table[exp_res_index]); } From a56a8d66dce45c40bd951d6b4c0c9f36acc53e18 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Mon, 8 Jun 2026 23:02:35 -0500 Subject: [PATCH 05/12] add test for softmax auto inferrence --- test/pytest/test_auto_precision.py | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/pytest/test_auto_precision.py b/test/pytest/test_auto_precision.py index d3738c8461..57ea268340 100644 --- a/test/pytest/test_auto_precision.py +++ b/test/pytest/test_auto_precision.py @@ -1,3 +1,4 @@ +import math from pathlib import Path import numpy as np @@ -13,10 +14,12 @@ ReLU, SeparableConv1D, SeparableConv2D, + Softmax, ) from tensorflow.keras.models import Sequential import hls4ml +import hls4ml.model.layers from hls4ml.model.optimizer.passes.infer_precision import _get_precision_from_constant test_root_path = Path(__file__).parent @@ -285,3 +288,37 @@ def test_precision_from_constant_unit(val, expected_width): quantum = 2.0**-fp.fractional if expected_width < max_width: assert val % quantum == 0 + + +@pytest.mark.parametrize('n_in', [4, 8, 16]) +@pytest.mark.parametrize('backend', ['Vitis', 'oneAPI']) +def test_auto_precision_softmax(test_case_id, n_in, backend): + """Test that auto accumulator precision is correctly inferred for softmax layers.""" + model = Sequential() + model.add(Softmax(input_shape=(n_in,))) + model.compile() + + config = hls4ml.utils.config_from_keras_model(model, backend=backend, granularity='name') + + odir = str(test_root_path / test_case_id) + hls_model = hls4ml.converters.convert_from_keras_model(model, hls_config=config, output_dir=odir, backend=backend) + + # Find the Softmax layer and verify accum_t precision + softmax_layer = next((layer for layer in hls_model.get_layers() if isinstance(layer, hls4ml.model.layers.Softmax)), None) + assert softmax_layer is not None, 'No Softmax layer found in converted model' + + accum_t = softmax_layer.types['accum_t'].precision + exp_table_t = softmax_layer.types['exp_table_t'].precision + + ceillog = math.ceil(math.log2(n_in)) + expected_width = exp_table_t.width + ceillog + expected_integer = exp_table_t.integer + ceillog + expected_signed = exp_table_t.signed + + assert accum_t.width == expected_width, f'Expected accum_t width {expected_width}, got {accum_t.width} (n_in={n_in})' + assert accum_t.integer == expected_integer, ( + f'Expected accum_t integer {expected_integer}, got {accum_t.integer} (n_in={n_in})' + ) + assert accum_t.signed == expected_signed, ( + f'Expected accum_t signed={expected_signed}, got {accum_t.signed} (n_in={n_in})' + ) From 3d7979aa3256f0c25cf275d4b582419c0aafe22f Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Tue, 7 Jul 2026 16:38:45 -0500 Subject: [PATCH 06/12] remove quartus reqirement for signed softmax --- hls4ml/writer/quartus_writer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hls4ml/writer/quartus_writer.py b/hls4ml/writer/quartus_writer.py index e0d6338ac3..f5b56d9f7c 100644 --- a/hls4ml/writer/quartus_writer.py +++ b/hls4ml/writer/quartus_writer.py @@ -1097,8 +1097,6 @@ def __write_exp_table(self, model, path): except Exception: # FixedPrecisionType wasn't correctly stored in layer attributes, use default values pass - if fp_signed is False: - raise Exception('Softmax types need to be signed') sep = '' N = ceil_log2(table_size) @@ -1143,8 +1141,6 @@ def __write_invert_table(self, model, path): except Exception: # FixedPrecisionType wasn't correctly stored in layer attributes, use default values pass - if fp_signed is False: - raise Exception('Softmax types need to be signed') sep = '' N = ceil_log2(table_size) From aa678e07d26ee5a2a16198a18672a2649830fbf7 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Tue, 7 Jul 2026 18:39:31 -0500 Subject: [PATCH 07/12] add inp_norm_t inferrence --- hls4ml/backends/fpga/fpga_backend.py | 4 +--- hls4ml/model/optimizer/passes/infer_precision.py | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hls4ml/backends/fpga/fpga_backend.py b/hls4ml/backends/fpga/fpga_backend.py index 1518f1a04b..0c37085765 100644 --- a/hls4ml/backends/fpga/fpga_backend.py +++ b/hls4ml/backends/fpga/fpga_backend.py @@ -150,9 +150,7 @@ def __init__(self, name): ), TypeAttribute( 'inp_norm', - default=FixedPrecisionType( - 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT - ), + default=UnspecifiedPrecisionType(), description='The internal width used for the exp table lookup (only in stable)', ), TypeAttribute('accum', description=descriptions.accum_type), diff --git a/hls4ml/model/optimizer/passes/infer_precision.py b/hls4ml/model/optimizer/passes/infer_precision.py index 189ffb7dda..6151712ea8 100644 --- a/hls4ml/model/optimizer/passes/infer_precision.py +++ b/hls4ml/model/optimizer/passes/infer_precision.py @@ -626,6 +626,13 @@ def _infer_softmax_precision(self, node, types_to_infer): node.types['accum_t'].precision = FixedPrecisionType(exp_w + ceillog, exp_i + ceillog, signed=exp_s) inferred_types.append('accum_t') + if 'inp_norm_t' in types_to_infer: + in_type = node.get_input_variable().type.precision + inp_norm_width = in_type.width - in_type.signed + inp_norm_int = in_type.integer - in_type.signed + node.types['inp_norm_t'].precision = FixedPrecisionType(inp_norm_width, inp_norm_int, signed=False) + inferred_types.append('inp_norm_t') + return inferred_types From 5dadf1a4da01083a664615e86006adfa8105d5b8 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 8 Jul 2026 14:01:50 -0500 Subject: [PATCH 08/12] fix test_softmax --- test/pytest/test_softmax.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/pytest/test_softmax.py b/test/pytest/test_softmax.py index 418f64b558..e71b7de778 100644 --- a/test/pytest/test_softmax.py +++ b/test/pytest/test_softmax.py @@ -20,7 +20,7 @@ def generate_data(input_shape): @pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'Catapult']) -@pytest.mark.parametrize('strategy', ['stable', 'latency', 'argmax']) +@pytest.mark.parametrize('implementation', ['stable', 'latency', 'argmax']) @pytest.mark.parametrize( 'input_bits,input_shape,table_bits,io_type,custom_accum', [ @@ -35,26 +35,24 @@ def generate_data(input_shape): ('16,6', (8, 8, 3), '18,8', 'io_stream', False), ], ) -def test_softmax(test_case_id, backend, strategy, generate_data, input_bits, input_shape, table_bits, io_type, custom_accum): +def test_softmax(test_case_id, backend, implementation, generate_data, input_bits, input_shape, table_bits, io_type, custom_accum): X = generate_data model = tf.keras.models.Sequential() model.add(tf.keras.layers.Activation(input_shape=input_shape, activation='softmax', name='softmax')) model.compile() - table_type = f'fixed<{table_bits}, RND, SAT>' + table_type = f'ufixed<{table_bits}, RND, SAT>' cfg = hls4ml.utils.config_from_keras_model(model, granularity='name', backend=backend) - cfg['LayerName']['softmax']['Strategy'] = strategy - cfg['LayerName']['softmax']['inv_table_t'] = table_type - cfg['LayerName']['softmax']['exp_table_t'] = table_type - cfg['LayerName']['softmax']['accum_t'] = table_type - cfg['LayerName']['softmax']['inv_inp_t'] = table_type + cfg['LayerName']['softmax']['Implementation'] = implementation + cfg['LayerName']['softmax']['Precision']['inv_table'] = table_type + cfg['LayerName']['softmax']['Precision']['exp_table'] = table_type + cfg['LayerName']['softmax']['Precision']['inv_inp'] = table_type if custom_accum: if backend not in ['Vivado', 'Vitis']: pytest.skip('Custom accumulators are only supported for Vivado and Vitis backends') W, I = map(int, input_bits.split(',')) # noqa: E741 - cfg['LayerName']['softmax']['accum_t'] = f'fixed<{W + 3},{I + 3}>' - cfg['LayerName']['softmax']['inv_inp_t'] = f'fixed<{W + 2},{I + 2}>' + cfg['LayerName']['softmax']['Precision']['inv_inp'] = f'ufixed<{W + 2},{I + 2}>' inp_layer_name = next(iter(cfg['LayerName'].keys())) cfg['LayerName'][inp_layer_name]['Precision']['result'] = f'fixed<{input_bits}>' From d8abde408e135be9c15818dc181237b269c2e625 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 8 Jul 2026 15:10:48 -0500 Subject: [PATCH 09/12] pre-commit fix --- test/pytest/test_softmax.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/pytest/test_softmax.py b/test/pytest/test_softmax.py index e71b7de778..8175785e10 100644 --- a/test/pytest/test_softmax.py +++ b/test/pytest/test_softmax.py @@ -35,7 +35,9 @@ def generate_data(input_shape): ('16,6', (8, 8, 3), '18,8', 'io_stream', False), ], ) -def test_softmax(test_case_id, backend, implementation, generate_data, input_bits, input_shape, table_bits, io_type, custom_accum): +def test_softmax( + test_case_id, backend, implementation, generate_data, input_bits, input_shape, table_bits, io_type, custom_accum +): X = generate_data model = tf.keras.models.Sequential() model.add(tf.keras.layers.Activation(input_shape=input_shape, activation='softmax', name='softmax')) From 55e7454dd441ff48cf611ec2bb161f5f983dc66e Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Wed, 8 Jul 2026 15:28:05 -0500 Subject: [PATCH 10/12] change randint to rand --- test/pytest/test_softmax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pytest/test_softmax.py b/test/pytest/test_softmax.py index 8175785e10..6c666e7020 100644 --- a/test/pytest/test_softmax.py +++ b/test/pytest/test_softmax.py @@ -14,7 +14,7 @@ def generate_data(input_shape): shape = (5000, *input_shape) d = np.random.normal(0, 2, shape) - modify_entries = np.random.randint(0, 1, shape) < 0.05 + modify_entries = np.random.rand(*shape) < 0.05 d[modify_entries] = d[modify_entries] * 5 + 10 return np.clip(d, -32, 31) From 947f195c6ba67def843a7d95551d4fca6d4ebd77 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Thu, 9 Jul 2026 11:38:43 -0500 Subject: [PATCH 11/12] make sure softmax widths are transfered properly --- hls4ml/backends/fpga/fpga_backend.py | 4 +--- hls4ml/model/layers.py | 9 +++++++++ hls4ml/model/optimizer/passes/infer_precision.py | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/hls4ml/backends/fpga/fpga_backend.py b/hls4ml/backends/fpga/fpga_backend.py index 0c37085765..eebb8d939f 100644 --- a/hls4ml/backends/fpga/fpga_backend.py +++ b/hls4ml/backends/fpga/fpga_backend.py @@ -143,9 +143,7 @@ def __init__(self, name): ), TypeAttribute( 'inv_inp', - default=FixedPrecisionType( - 18, 8, signed=False, rounding_mode=RoundingMode.RND, saturation_mode=SaturationMode.SAT - ), + default=UnspecifiedPrecisionType(), description='What the accumulated value is cast to before accessing the inversion table (only in stable)', ), TypeAttribute( diff --git a/hls4ml/model/layers.py b/hls4ml/model/layers.py index 42afbabd3c..625f62366d 100644 --- a/hls4ml/model/layers.py +++ b/hls4ml/model/layers.py @@ -965,6 +965,9 @@ def initialize(self): if 'n_in' not in self.attributes: self.set_attr('n_in', self.get_input_variable().size()) + # set the needed types if needed + self._set_type_t('table') + class ParametrizedActivation(Activation): _expected_attributes = [ @@ -1031,6 +1034,12 @@ class Softmax(Activation): def initialize(self): super().initialize() + # set the needed types if needed + self._set_type_t('exp_table') + self._set_type_t('inv_table') + self._set_type_t('inv_inp') + self._set_type_t('inv_norm') + class TernaryTanh(Activation): def initialize(self): diff --git a/hls4ml/model/optimizer/passes/infer_precision.py b/hls4ml/model/optimizer/passes/infer_precision.py index 6151712ea8..7d6c6f306e 100644 --- a/hls4ml/model/optimizer/passes/infer_precision.py +++ b/hls4ml/model/optimizer/passes/infer_precision.py @@ -626,6 +626,11 @@ def _infer_softmax_precision(self, node, types_to_infer): node.types['accum_t'].precision = FixedPrecisionType(exp_w + ceillog, exp_i + ceillog, signed=exp_s) inferred_types.append('accum_t') + if 'inv_inp_t' in types_to_infer: + # if not set, just choose the accumulator type + node.types['inv_inp_t'].precision = node.types['accum_t'].precision + inferred_types.append('inv_inp_t') + if 'inp_norm_t' in types_to_infer: in_type = node.get_input_variable().type.precision inp_norm_width = in_type.width - in_type.signed From 10532dadaa66df950fdfb82289d8343f86b4f409 Mon Sep 17 00:00:00 2001 From: Jovan Mitrevski Date: Thu, 9 Jul 2026 12:55:46 -0500 Subject: [PATCH 12/12] Fix issues with skip, auto --- hls4ml/model/types.py | 3 +++ test/pytest/test_softmax.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/hls4ml/model/types.py b/hls4ml/model/types.py index aa69130d9b..85ed1c8241 100644 --- a/hls4ml/model/types.py +++ b/hls4ml/model/types.py @@ -434,6 +434,9 @@ class UnspecifiedPrecisionType(PrecisionType): def __init__(self): super().__init__(width=0, signed=False) + def __str__(self): + return 'auto' + def find_minimum_width(data, signed=True): """ diff --git a/test/pytest/test_softmax.py b/test/pytest/test_softmax.py index 6c666e7020..45e4ad5e25 100644 --- a/test/pytest/test_softmax.py +++ b/test/pytest/test_softmax.py @@ -20,7 +20,7 @@ def generate_data(input_shape): @pytest.mark.parametrize('backend', ['Vivado', 'Vitis', 'Quartus', 'Catapult']) -@pytest.mark.parametrize('implementation', ['stable', 'latency', 'argmax']) +@pytest.mark.parametrize('implementation', ['stable', 'latency', 'argmax', 'stable']) @pytest.mark.parametrize( 'input_bits,input_shape,table_bits,io_type,custom_accum', [ @@ -38,6 +38,10 @@ def generate_data(input_shape): def test_softmax( test_case_id, backend, implementation, generate_data, input_bits, input_shape, table_bits, io_type, custom_accum ): + + if backend == 'Catapult' and implementation == 'argmax': + pytest.skip('Argmax is not supported in cataplut') + X = generate_data model = tf.keras.models.Sequential() model.add(tf.keras.layers.Activation(input_shape=input_shape, activation='softmax', name='softmax'))