A C++ header-only library implementing the Logarithmic Number System (LNS) for 8-, 16-, 32- and 64-bit fixed-point formats, with comparison operators and FP/BF ↔ LNS conversion, a hardware compiler header targeting custom RISC-V LNS instructions, and a software simulation API (plus a Brain Float reference implementation) for testing and development.
A non-zero real
| Condition | |||
|---|---|---|---|
Multiplication reduces to an integer add on the exponent field, division to an integer subtract, and any power-of-two power or root (square, fourth power, square root, fourth root, …) to a logical shift — all exact up to quantization, with no mantissa multiplier required in hardware.
For
Setting
and the logarithmic Gauss subtraction function
so that spline tool (see below).
Of the five possible orderings (
Equality is a plain bitwise/integer comparison. Less-than is:
Given
The two nonlinear pieces,
Multiplication and division are exact. Since values are stored as exponents, multiplying two LNS numbers is a single integer add on the exponent field. Division is a subtract. No mantissa multiplier is needed.
Powers and roots of two are a single shift.
Uniform relative precision. The quantization step is always the same fraction of the value across the entire representable range, unlike floating point which has higher absolute precision near zero.
Addition and subtraction are the weak point. Adding two LNS values
requires evaluating the Gauss function
LNS is most attractive for multiply-heavy workloads such as neural network inference, signal processing filters, and Bayesian computation, where the hardware savings on multiply significantly outweigh the cost of approximate addition.
| Format |
|
|
|
Range |
|---|---|---|---|---|
| lns8 Q4.3 | 1 | 4 | 3 |
|
| lns16 Q8.7 | 1 | 8 | 7 |
|
lns16 Q8.7 has 7 bits of effective mantissa — the same as bf16 — which is exactly what makes a direct comparison between the two formats meaningful.
Intended for use when targeting a custom RISC-V processor with native LNS
instructions. Defines the lns<N> type and maps arithmetic, comparison, and
load/store operators to custom RISC-V instruction mnemonics via inline
assembly, using the .insn directive to emit custom opcodes directly into
the floating-point register file. This is not meant for simulation — use
lnssim.hpp for that.
Predefined type aliases: lns8, lns16, lns32, lns64.
The main API for running LNS computations in software. Defines lns<N, I, F>
parameterised by total bit width, integer exponent bits, and fractional
exponent bits. Implements all arithmetic operators (+, -, *, /),
comparisons, conversion to/from float/double, and exp/sinh/cosh/tanh.
Concrete instantiations used throughout this repository's benchmarks:
using lns32 = lns<32, 8, 23>; // high-precision reference / accumulator
using lns16 = lns<16, 8, 7>; // primary Q8.7 format
using lns8 = lns< 8, 4, 3>; // Q4.3 formatFor lns8 and lns16, addition and subtraction dispatch to the spline LUT
functions in lnsluts.hpp, faithfully reproducing the piecewise-linear
approximation that the hardware unit computes. Square root is exact in both
simulation and hardware — since a value is stored as .sqrt() reduces
to an arithmetic right shift of the exponent by one bit, with no table lookup.
For lns32 and lns64, spline tables are not feasible — the domain of
math.h log2 and exp2 functions, which provide a numerically exact
simulation of the LNS arithmetic without modelling any particular hardware
approximation. This makes lns32 and lns64 suitable as a high-precision
reference baseline in benchmarks — for instance, to isolate quantisation
error from conversion error when comparing lns8 or lns16 against a non-float
reference — rather than as a model of a specific hardware implementation.
Must define either SPLINE_XF or SPLINE_XMB before including
(required for lns8/lns16; ignored for lns32/lns64), and load the
corresponding table files at runtime:
#define SPLINE_XMB
#include <lnssim.hpp>
using lns8 = lns< 8, 4, 3>;
using lns16 = lns<16, 8, 7>;
lns8_read_tables ("lib/spline/lns_tables/lns8_q4_3_xmb.lns");
lns16_read_tables("lib/spline/lns_tables/lns16_q8_7_xmb.lns");
lns16 a(1.5f), b(2.0f);
float result = (float)(a * b); // exact — integer add on exponents
float result2 = (float)(a + b); // approximated via spline LUT
float result3 = (float)a.sqrt(); // exact — exp >>= 1
lns_close();After running sudo make install from the repository root, headers are
installed flat to your compiler's system include path and can be included
directly using angle brackets:
#include <lns>
#include <lnssim>
#include <bfloatsim>Defines the spline structs and provides lns8_read_tables / lns16_read_tables
to load precomputed .lns table files at runtime, and lns_close to free them.
Two spline formats are supported at compile time:
-
SPLINE_XF— stores(x, f)pairs, interpolates linearly between function values. -
SPLINE_XMB— stores(x, m, b)per segment, evaluates$m\cdot x+b$ directly. Faster — avoids the division inherent in XF interpolation.
A self-contained header implementing bf8 (E4M3) and bf16 (E8M7) as C++
templates, exposing the same interface as the LNS types so the same
benchmark code can be instantiated over either family. bf16 is simulated
by truncating the lower 16 bits of an fp32 representation (round-to-zero);
bf8 applies the same strategy with the E4M3 mask.
The tool located in lib/spline/ is a standalone utility that generates the
binary .lns table files for lns8 and lns16. It implements a greedy spline
fitting algorithm over
Given
and the equivalent XMB form precomputes a slope and intercept per segment:
XMB trades memory for speed: it needs
The upper bound on linear-spline error is
A greedy algorithm builds each table: starting from the interval
Build and generate the default tables:
cd lib/spline
make
# Generates files inside lib/spline/lns_tables/This updates or produces four files inside lib/spline/lns_tables/:
| File | Format | Spline type |
|---|---|---|
lns8_q4_3_xf.lns |
lns8 Q4.3 | XF |
lns8_q4_3_xmb.lns |
lns8 Q4.3 | XMB |
lns16_q8_7_xf.lns |
lns16 Q8.7 | XF |
lns16_q8_7_xmb.lns |
lns16 Q8.7 | XMB |
./build/spline <--gen | --test> [config]-
--gen <+> <-> <f2l> <l2f>— exports.lnsfiles. Each value is the number of rows for that sub-table, in$[2, 1024]$ . -
--test <max_lines>— prints approximation error to stdout for a sweep up tomax_linesrows, in$[2, 1024]$ , without writing files. -
[config]—--xf/--xmbselects point-pairs vs. slope/intercept storage;--lns16/--lns8selects format width;<int_digits>selects the number of integer exponent bits (valid range$[4,6]$ for lns8,$[4,14]$ for lns16).
# Custom XMB tables for lns16 (Q8.7) with asymmetric sizes: + - f2l l2f, int:8
./build/spline --gen --xmb 128 256 64 64 --lns16 8
# Sweep up to 128 rows for lns8 (Q4.3) in XF mode, without overwriting tables
./build/spline --test --xf 128 --lns8 4From the repository root directory:
make examples # builds examples targets
sudo make install # installs headers flat to system include path
make uninstall # removes installed headers from system include pathMonte Carlo arithmetic accuracy benchmark comparing lns8 vs bf8 (E4M3) and lns16 vs bf16 across five operations (round-trip, mul, div, add, sub), broken down by operand magnitude interval, with two-sided Mann-Whitney U significance testing (n = 100 000, p < 0.01, |r| ≥ 0.05). lns8/lns16 errors are measured against lns32 as a high-precision in-family reference, while bf8/bf16 errors are measured against fp32 — letting each low-bit format be compared against a higher-resolution reference within its own family. See the bench README for full methodology and execution details.
| Operation | 8-bit winner | 16-bit winner | Reason |
|---|---|---|---|
| mul | bf8 | bf16 (relative) / lns16 (absolute) | LNS multiply is exact on the exponent, but bf16's mantissa still wins on relative error; lns16 regains the lead on absolute error |
| div | bf8 | lns16 | exact integer subtract on the exponent field gives lns16 the edge across all intervals, in both relative and absolute error |
| add | bf8 | bf16 | IEEE 754 correctly-rounded add; LNS add requires a nonlinear spline correction anchored to input scale |
| sub | bf8 | bf16 | same as add |
| round-trip | bf8 | tie | bf8 slightly better across all intervals; lns16 and bf16 statistically indistinguishable |
At 8-bit, bf8 wins across all tested operations and intervals with no exceptions, reflecting lns8's coarser exponent grid and the spline approximation cost at that bit-width. The 16-bit results are more nuanced: the division advantage for lns16 holds across all intervals under both relative and absolute error, while the multiplication result splits by metric — bf16 wins on relative error, but lns16 regains the lead on absolute error. This split is meaningful: lns16 is preferable when operands stay within a limited range and absolute fidelity matters; bf16 is preferable when relative precision has to be preserved across a wide dynamic range.
Nine algorithmic kernels are evaluated: geometric progression, Euclidean
norm, the alternating harmonic (Leibniz) series, forward and backward
accumulation of the Basel-problem series for
In workloads dominated by addition/subtraction accumulation, base lns16
loses precision to bf16 thanks to bf16's correctly-rounded add. Pairing
lns16 with a 32-bit accumulator (lns16_lns32acc or lns16_f32acc) recovers
most of that gap, matching or slightly exceeding bf16 in specific kernels
such as RMSNorm. On pure multiplication chains (geometric progression),
lns16 has better precision than bf16, since LNS multiplication stays exact
while bf16's mantissa rounding accumulates; at 8-bit, lns8 is more
competitive and sometimes matches or beats bf8 on the same kernels, though
its 32-bit-accumulator variants don't always help, due to a constant
conversion error between formats. Among the non-linear composite kernels,
lns16 has higher error than bf16 on GELU but lower error on the softmax
sum — evidence that LNS's relative performance depends on the specific
structure of a function, not just on whether the dominant operation is
addition or multiplication. The 8-bit formats saturate on every kernel
except geometric progression.
Runs Andrej Karpathy's llama2.c
TinyStories inference using lns16 and bf16 as drop-in replacements for the
original float32 weights. Includes weight converters located in convert/
for both formats and supports both XF and XMB spline variants for lns16
execution located under tiny/.
Because LNS addition error compounds with every summed term, accumulation-
heavy operations — RMSNorm's sum of squares, attention's dot products and
weighted sums, the softmax denominator, matrix multiplication, and the
residual stream — use a hybrid scheme: each scalar multiply stays in pure
lns16 (exact by construction), but the running sum is accumulated in a
wider-precision accumulator and only converted back to lns16 at the end.
tiny_lns16.cpp uses an fp32 accumulator; tiny_lns16_lns32acc.cpp swaps
this for a simulated lns32 accumulator as a drop-in replacement;
tiny_bf16.cpp is the bf16 equivalent, also with an fp32 accumulator.
Direct fp32 → lns16 weight conversion, with no re-training, produces coherent text on both stories15M and stories42M, in both the XF and XMB spline variants — these are qualitative spot-checks (one sample per configuration), so they confirm coherence rather than quantify any accuracy gap between formats.
See tinystories README for step-by-step build instructions, model download parameters, conversion steps, and tokenization usage.
The inline-assembly hardware instructions declared in lib/lns.hpp target
the LNS custom functional unit developed for RISC++
(a custom RISC-V soft-core design framework developed at SPeCS, INESC TEC / FEUP).
To decouple the core's architecture development from this library,
the target test suites and toolchain compilation flows reside directly
in the RISC++ core repository. Once this library is installed on your
system via make install, the RISC++ cross-compilation toolchain flags
consume these global headers directly to generate bare-metal ELF validation
binaries and BRAM initialization structures for hardware simulation blocks.
lns32 and lns64 are currently available in simulation via math.h,
providing an exact LNS reference suitable for benchmarking. Hardware-grade
approximation for these wider formats requires a different strategy from the
spline tables used for lns8 and lns16, as the domain of
Candidates under development and tracking inside lib/newtonsdd/:
- Newton's divided differences with non-uniform sample point placement
- Minimax polynomial approximation (Remez algorithm)
- Piecewise Chebyshev approximation
Henrique dos Santos Teixeira


