An embedded Python DSL for specifying and simulating numerical algorithms.
Important links:
- PyPI package: fpy2
- Documentation: fpy.readthedocs.io
- GitHub: fpy
- Guide: USAGE.md
FPy is Python with explicit control of both the mathematics and
rounding: every operation is correctly rounded by the rounding context
it appears in, and contexts are ordinary values you can pass around —
including fp.REAL, which never rounds. Here is a blocked dot product:
the products are exact, each block of K elements is summed in a narrow
format, and the running total is float32.
import fpy2 as fp
@fp.fpy(ctx=fp.REAL)
def dot(xs: list[fp.Real], ys: list[fp.Real], K: int, block: fp.Context) -> fp.Real:
"""A blocked dot product: exact products, blocks of `K` summed in `block`.
The length of `xs` must be a multiple of `K`."""
assert len(xs) == len(ys)
acc = 0
for start in range(0, len(xs), K):
with block:
inner_acc = 0
for x, y in zip(xs[start:start + K], ys[start:start + K]):
with fp.REAL:
p = x * y # every product is exact ...
inner_acc += p # ... but block sums round to `block`
with fp.FP32:
acc += inner_acc # one fp32 addition per block
return acc
@fp.fpy(ctx=fp.REAL)
def dot_ref(xs: list[fp.Real], ys: list[fp.Real]) -> fp.Real:
"""The same dot product, with no rounding anywhere."""
return sum([x * y for x, y in zip(xs, ys)])
xs = ys = [0.1] * 4096
exact = dot_ref(xs, ys).as_rational() # the true value, as a Fraction
print(f'{"block":>6} {"format":>9} {"result":>11} {"rel. error":>11}')
for K, name, ctx in [(4096, 'float16', fp.FP16), (32, 'float16', fp.FP16),
(4096, 'bfloat16', fp.BF16), (32, 'bfloat16', fp.BF16),
(32, 'float32', fp.FP32)]:
r = dot(xs, ys, K, ctx)
err = abs(r.as_rational() - exact) / exact
print(f'{K:>6} {name:>9} {float(r):>11.6f} {float(err):>11.3%}')which prints
block format result rel. error
4096 float16 32.000000 21.875%
32 float16 40.968750 0.021%
4096 bfloat16 4.000000 90.234%
32 bfloat16 40.250000 1.733%
32 float32 40.959972 0.000%
A flat float16 accumulator stalls out once the running total dwarfs
the product being added to it; summing 32 elements at a time keeps the
error three orders of magnitude smaller.
See the usage guide for the language and the examples page for more.
FPy can be installed from PyPI with either uv or pip, or built from
source for development. The following instructions assume a bash-like
shell.
Requirements:
- Python 3.11 or later
To install the latest stable release of FPy, run:
uv pip install fpy2or, with pip:
pip install fpy2The Triton backend also needs PyTorch, Triton and a GPU: install fpy2[triton].
Requirements:
- Python 3.11 or later
make
uv is the recommended development workflow — it handles the virtual environment and dependency installation in a single step:
uv syncThis creates .venv/ and installs FPy in editable mode along with the
dev dependency group, pinned by uv.lock. Rerun it to reset .venv/ to
the lockfile; it removes anything installed by hand. Activate the
environment with
source .venv/bin/activateor prefix individual commands with uv run (e.g. uv run pytest tests/unit).
This path is preserved for compatibility with existing tooling; new
contributors should prefer uv above.
If you do not have a Python virtual environment, create one using
python3 -m venv .venv/and activate it using
source .venv/bin/activateTo install an instance of FPy for development, run:
pip install -e .[dev]To uninstall FPy, run:
pip uninstall fpy2There are a number of tests that can be run through
the Makefile including
make lintto ensure formatting and type safety;
make unittestto run the unit tests;
make infratestto run the infrastructure tests.