Activation Function Lookup Tables
On embedded systems without standard math libraries, functions like tanh(), sigmoid(), exp(), log(), sin(), and cos() are unavailable. TinyMind replaces these with pre-computed lookup tables (LUTs) stored as constant arrays in memory. At runtime, activation values are retrieved via table lookup with linear interpolation – no floating-point math required.
Why Lookup Tables?
Traditional activation functions require either a hardware FPU or a software floating-point emulation library, both of which may be unavailable or prohibitively expensive on small microcontrollers. LUTs provide:
- Constant-time evaluation – a table index computation and one interpolation, regardless of input value
- No FPU dependency – all arithmetic is integer-only using Q-format fixed-point
- Predictable memory cost – each table is exactly 96 entries, sized by the Q-format’s storage type
- Compile-time selectability – preprocessor switches include only the tables you use
Supported Activation Functions
| Function | LUT Name Prefix | Activation Policy | Derivative |
|---|---|---|---|
| Sigmoid | SigmoidValuesTableQ | SigmoidActivationPolicy<ValueType> | f'(x) = f(x) * (1 - f(x)) |
| Tanh | TanhValuesTableQ | TanhActivationPolicy<ValueType> | f'(x) = 1 - f(x)^2 |
| Exp | ExpValuesTableQ | Used by SoftmaxActivationPolicy, EluActivationPolicy | N/A (used internally) |
| Log | LogValuesTableQ | Available for custom policies | N/A (used internally) |
| Sin | SinValuesTableQ | Available for FFT twiddle factors and custom policies | N/A |
| Cos | CosValuesTableQ | Available for FFT twiddle factors and custom policies | N/A |
Additionally, GeluActivationPolicy reuses the sigmoid LUT with a 1.702x input scaling factor.
Table Parameters
All LUTs share the same domain and resolution, defined in cpp/activation.hpp:
| Parameter | Value | Meaning |
|---|---|---|
NUMBER_OF_ACTIVATION_TABLE_VALUES | 96 | Number of entries per table |
MIN_X_TABLE_VALUE | -6 | Left edge of the input domain |
MAX_X_TABLE_VALUE | 6 | Right edge of the input domain |
ACTIVATION_DELTA_SHIFT | 3 | Spacing = 1 / 2^3 = 0.125 between sample points |
The input domain [-6, 6] covers the full dynamic range where sigmoid and tanh have meaningful variation. Outside this range, sigmoid saturates at 0 or 1, and tanh at -1 or +1. The 96 entries at 0.125 spacing provide 96 * 0.125 = 12 units of range, exactly covering [-6, +6).
Q-Format and Table Sizes
Each activation function has tables generated for every valid Q-format split within each supported bit width. The bit widths and corresponding storage types are:
| Total Bits | Storage Type | Example Q-Formats | Table Size (bytes) |
|---|---|---|---|
| 8 | uint8_t | Q1.7, Q2.6, Q3.5, …, Q7.1 | 96 |
| 16 | uint16_t | Q1.15, Q8.8, Q12.4, … | 192 |
| 32 | uint32_t | Q16.16, Q24.8, … | 384 |
| 64 | uint64_t | Q32.32, Q48.16, … | 768 |
| 128 | uint128_t | Q64.64, … | 1,536 |
For a given total bit width N, tables are generated for every split from Q1.(N-1) through Q(N-1).1 – that is, N-1 tables per activation function per bit width.
Compile-Time Table Selection
Only the LUTs you need are compiled into your binary. Each table is guarded by a preprocessor macro:
TINYMIND_USE_SIGMOID_8_8 // Sigmoid Q8.8
TINYMIND_USE_TANH_8_8 // Tanh Q8.8
TINYMIND_USE_EXP_16_16 // Exp Q16.16
TINYMIND_USE_LOG_24_8 // Log Q24.8
TINYMIND_USE_SIN_8_8 // Sin Q8.8
TINYMIND_USE_COS_8_8 // Cos Q8.8
The naming pattern is TINYMIND_USE_{FUNCTION}_{FixedBits}_{FracBits}. Define these macros in your build system (e.g., -DTINYMIND_USE_TANH_8_8=1) to include the corresponding table.
When you instantiate TanhActivationPolicy<QValue<8,8,true>>, the compiler resolves the table through a template selector chain:
TanhActivationPolicy<ValueType>
-> TanhValuesTableSelector<8, 8, true>
-> TanhTableValueSize<8, 8, true> (template specialization, guarded by #if)
-> TanhValuesTableQ8_8 (the struct containing the values[] array)
If the required TINYMIND_USE_* macro is not defined, compilation fails with a clear error at the selector step.
Runtime Lookup with Linear Interpolation
When an activation function is called at runtime, the LookupTable<ValueType>::getValue() method in cpp/lookupTable.hpp performs:
- Clamp – if the input is outside [-6, +6], return the boundary table entry
- Index – compute the lower table index:
(value - MIN_X) / DELTA_X - Interpolate – linearly interpolate between the lower and upper entries:
result = y0 + (y1 - y0) * ((x - x0) / (x1 - x0))
All arithmetic uses the fixed-point QValue type, so no floating-point operations occur at runtime. The interpolation fills in sub-table-step precision, making the 96-entry table behave like a much smoother curve.
The LUT Generator Application
The apps/activation/ directory contains the standalone application that generates all LUT source code. This is a build-time code generator – you run it once to produce the header and source files that ship with the library.
Building the Generator
cd apps/activation
make # debug build
make release # optimized build
Running the Generator
./output/activationTableGenerator ../../cpp
The single argument is the output directory. The generator produces:
| Output File | Contents |
|---|---|
activation.hpp | Table dimension constants (NUMBER_OF_ACTIVATION_TABLE_VALUES, etc.) |
sigmoid.hpp, tanh.hpp, exp.hpp, log.hpp, sin.hpp, cos.hpp | Template selector structs that map Q-format parameters to the correct table struct |
sigmoidValues{8,16,32,64,128}Bit.hpp | Table struct declarations for each bit width |
tanhValues{8,16,32,64,128}Bit.hpp | (same pattern for tanh) |
expValues{8,16,32,64,128}Bit.hpp | (same pattern for exp) |
logValues{8,16,32,64,128}Bit.hpp | (same pattern for log) |
sinValues{8,16,32,64,128}Bit.hpp | (same pattern for sin) |
cosValues{8,16,32,64,128}Bit.hpp | (same pattern for cos) |
lookupTables.cpp | All table data – the const arrays with pre-computed hex values |
How Values Are Computed
For each Q-format split and each activation function, the generator:
- Iterates over 96 evenly spaced x-values from -6.0 to +5.875 (step 0.125)
- Computes the activation using double-precision floating-point (
std::tanh,std::exp,std::sin,std::cos, or the sigmoid formulae^x / (e^x + 1)) - Converts the result to fixed-point by multiplying by
2^FractionalBits - Casts to the appropriate unsigned integer type and writes as a hex literal
For example, tanh(0.0) = 0.0 in Q8.8 becomes 0x0000, while tanh(3.0) = 0.9951 becomes 0x00FE (254/256 = 0.9922 in Q8.8).
The Visualization Script
The lut_parse.py script parses lookupTables.cpp and plots any table using matplotlib:
cd apps/activation
python lut_parse.py -f tanh -q 8.8 # Plot tanh Q8.8
python lut_parse.py -f sigmoid -q 16.16 # Plot sigmoid Q16.16
Memory Footprint
A typical embedded deployment using Q8.8 with tanh activation needs only one 96-byte table. Here is how table sizes scale:
| Configuration | Tables Needed | Total LUT Memory |
|---|---|---|
| MLP with tanh (Q8.8) | 1 tanh table | 96 bytes |
| MLP with sigmoid (Q8.8) | 1 sigmoid table | 96 bytes |
| MLP with tanh (Q16.16) | 1 tanh table | 192 bytes |
| Softmax output + tanh hidden (Q8.8) | 1 exp + 1 tanh table | 192 bytes |
| GELU activation (Q8.8) | 1 sigmoid table | 96 bytes |
| ELU activation (Q16.16) | 1 exp table | 192 bytes |
Because of the #if guards, only the tables matching your defined macros are compiled. A binary using only TINYMIND_USE_TANH_8_8 pays 96 bytes for activation tables and zero bytes for sigmoid, exp, and log.
Activation Policies That Use LUTs
Each fixed-point activation policy in cpp/activationFunctions.hpp wraps the LUT lookup:
SigmoidActivationPolicy<ValueType>– direct sigmoid table lookupTanhActivationPolicy<ValueType>– direct tanh table lookupSoftmaxActivationPolicy<ValueType>– exp table lookup per neuron, then normalization by sumEluActivationPolicy<ValueType>– exp table lookup for negative inputs, identity for positiveGeluActivationPolicy<ValueType>– sigmoid table lookup with 1.702x input scaling
Policies that do not need LUTs (LinearActivationPolicy, ReluActivationPolicy, CappedReluActivationPolicy, NullActivationPolicy) use direct arithmetic and have no table dependency.
Adding a Custom Q-Format
If you need a Q-format that is not yet enabled:
- Ensure the total bit width is one of 8, 16, 32, 64, or 128
- Add the appropriate
-Dflag to your build:-DTINYMIND_USE_TANH_{Fixed}_{Frac}=1 - The table data and selector specialization already exist in
lookupTables.cppand the generated headers – the#ifguard simply enables them
No code generation step is needed unless you modify the table parameters (domain, resolution, or add a new activation function).