├── docs ├── requirements-rtd.txt ├── source │ ├── notebooks.rst │ ├── utils.py │ ├── api.rst │ ├── conf.py │ ├── index.rst │ ├── 04-benchmark.ipynb │ ├── formats.rst │ └── 01-decode.ipynb ├── Makefile └── make.bat ├── requirements.txt ├── .readthedocs.yaml ├── BUILDING.md ├── etc ├── check-copyright.sh ├── package.sh └── test-check-copyright.sh ├── requirements-dev.txt ├── .pre-commit-config.yaml ├── src └── gfloat │ ├── __init__.py │ ├── printing.py │ ├── encode.py │ ├── encode_ndarray.py │ ├── decode_ndarray.py │ ├── decode.py │ ├── round.py │ ├── block.py │ ├── round_ndarray.py │ ├── formats.py │ └── types.py ├── test ├── test_jax.py ├── test_array_api.py ├── test_block.py ├── test_printing.py ├── test_finfo.py ├── test_p3109_spec.py ├── test_encode.py ├── test_microxcaling.py ├── test_torch.py ├── test_decode.py └── test_round.py ├── ChangeLog ├── .github └── workflows │ └── ci.yaml ├── LICENSE ├── pyproject.toml ├── .gitignore └── README.md /docs/requirements-rtd.txt: -------------------------------------------------------------------------------- 1 | .[dev] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | more_itertools 3 | array-api-compat 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | version: 2 4 | 5 | build: 6 | os: "ubuntu-22.04" 7 | tools: 8 | python: "3.10" 9 | 10 | python: 11 | install: 12 | - requirements: docs/requirements-rtd.txt 13 | 14 | sphinx: 15 | configuration: docs/source/conf.py 16 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## BUILDING 4 | 5 | ``` 6 | pip install -e . 7 | ( cd docs && make html ) 8 | # Install packages for testing - will install JAX, Torch, etc. 9 | pip install -r requirements-dev.txt 10 | pytest . 11 | ``` 12 | 13 | #### Pushing 14 | ``` 15 | sh etc/package.sh 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/source/notebooks.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | Notebooks 4 | ========= 5 | 6 | Some notebooks to illustrate uses of the library 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | 01-decode.ipynb 12 | 02-value-stats.ipynb 13 | 03-value-tables.ipynb 14 | 04-benchmark.ipynb 15 | 05-stochastic-rounding.ipynb 16 | -------------------------------------------------------------------------------- /etc/check-copyright.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 3 | 4 | PATTERN='Copyright \(c\) 202[0-9] Graphcore Ltd\. +All rights reserved\.' 5 | 6 | # We "grep ." so the exit code signals that the first grep generated output 7 | if grep -L -E "$PATTERN" "$@" | grep . 8 | then 9 | # There was output, signal unsuccessful 10 | exit 1 11 | fi 12 | # Normal exit, signalling success 13 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Requirements for tests (see "requirements-direct.txt" also for direct dependencies) 2 | pytest 3 | nbval 4 | ml_dtypes 5 | jaxlib 6 | jax 7 | torch 8 | array-api-strict 9 | torchao==0.10 10 | airium 11 | pandas 12 | matplotlib 13 | 14 | # Requirements for development 15 | pre-commit 16 | black 17 | mypy 18 | black[jupyter] 19 | isort 20 | 21 | # Requirements for docs 22 | sphinx==7.1.2 23 | sphinx-rtd-theme==1.3.0rc1 24 | sphinx_paramlinks 25 | myst_nb 26 | -------------------------------------------------------------------------------- /docs/source/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import pandas 4 | from typing import Callable 5 | from IPython.display import HTML 6 | 7 | 8 | def pandas_render(df: pandas.DataFrame, **kwargs) -> HTML: 9 | """ 10 | Render a dataframe, hiding the index, 11 | and set ID to minimize diffs for notebook regression tests 12 | """ 13 | s = df.style.hide().set_uuid("my_id") 14 | for f, v in kwargs.items(): 15 | if isinstance(getattr(s, f, None), Callable): 16 | s = getattr(s, f)(v) 17 | else: 18 | s = s.format(**{f: v}) 19 | return HTML(s.to_html()) 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /etc/package.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | # Set version numbers, make package, and publish 4 | 5 | set -o errexit 6 | 7 | # This is the master location at which to change version number 8 | VERSION="0.5.2" 9 | 10 | # Run the script to change the version elsewhere 11 | perl -pi -e 's/^(release|version) = "([\d.]+)"/$1 = "'$VERSION'"/' docs/source/conf.py 12 | perl -pi -e 's/^version = "([\d.]+)"/version = "'$VERSION'"/' pyproject.toml 13 | 14 | # Build docs to embed version 15 | ( cd docs && make html ) 16 | 17 | # Build distribution 18 | rm -rf dist 19 | uv pip install build twine 20 | python -m build 21 | echo "Enter PyPI API Token" 22 | echo __token__ | twine upload --repository pypi dist/* --verbose 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.6.0 6 | hooks: 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | 11 | - repo: https://github.com/psf/black 12 | rev: 24.4.0 13 | hooks: 14 | - id: black-jupyter 15 | 16 | - repo: local 17 | hooks: 18 | - id: etc/check-copyright.sh 19 | name: check copyright 20 | entry: etc/check-copyright.sh 21 | language: script 22 | exclude: | 23 | (?x)( 24 | ^docs/Makefile$| 25 | ^docs/make.bat$| 26 | (/|)requirements.*\.txt$ 27 | ) 28 | -------------------------------------------------------------------------------- /etc/test-check-copyright.sh: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | tmpdir=$(mktemp -d) 4 | test -d $tmpdir || exit -1 5 | 6 | cleanup () { 7 | echo "Removing $tmpdir" 8 | rm $tmpdir/t.sh 9 | rmdir $tmpdir 10 | } 11 | 12 | trap cleanup EXIT 13 | 14 | # Passing case 15 | echo "Copyright (c) 2024 Graphcore Ltd. All rights reserved." > $tmpdir/t.sh 16 | if sh etc/check-copyright.sh $tmpdir/t.sh 17 | then 18 | echo Pass: Should have passed 19 | else 20 | echo FAIL: Should have passed 21 | fi 22 | 23 | # Failing case 24 | echo "Copyright (c) 2024 Graphcore Ltd. All rights xreserved." > $tmpdir/t.sh 25 | if sh etc/check-copyright.sh $tmpdir/t.sh 26 | then 27 | echo FAIL: Should have failed, but passed 28 | else 29 | echo Pass: Should have failed 30 | fi 31 | -------------------------------------------------------------------------------- /src/gfloat/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from .block import ( 4 | BlockFormatInfo, 5 | compute_scale_amax, 6 | decode_block, 7 | encode_block, 8 | quantize_block, 9 | ) 10 | from .decode import decode_float 11 | from .printing import float_pow2str, float_tilde_unless_roundtrip_str 12 | from .round import round_float 13 | from .encode import encode_float 14 | from .round_ndarray import round_ndarray 15 | from .encode_ndarray import encode_ndarray 16 | from .decode_ndarray import decode_ndarray 17 | from .types import FloatClass, FloatValue, FormatInfo, Signedness, Domain, RoundMode 18 | 19 | # Don't automatically import from .formats. 20 | # If the user wants them in their namespace, they can explicitly import 21 | # from gfloat.formats import * 22 | -------------------------------------------------------------------------------- /test/test_jax.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import numpy as np 4 | 5 | import jax 6 | import jax.numpy as jnp 7 | 8 | import ml_dtypes 9 | 10 | import gfloat 11 | from gfloat.formats import * 12 | 13 | 14 | def test_jax() -> None: 15 | """ 16 | Test that JAX JIT produces correct output 17 | """ 18 | a = np.random.randn(1024) 19 | 20 | a8 = a.astype(ml_dtypes.float8_e5m2).astype(jnp.float32) 21 | 22 | fi = format_info_ocp_e5m2 23 | j8 = gfloat.round_ndarray(fi, jnp.array(a)).astype(jnp.float32) # type: ignore [arg-type] 24 | 25 | np.testing.assert_equal(a8, j8) 26 | 27 | jax_round_array = jax.jit(lambda x: gfloat.round_ndarray(fi, x)) 28 | j8i = jax_round_array(a).astype(jnp.float32) 29 | 30 | np.testing.assert_equal(a8, j8i) 31 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2 | 0.5: August 21, 2025 3 | - Use array-api for cross-framework compatibility 4 | - Fix rounding issue for large bfloat16s by @blissb-positron in #49 5 | - Replaced microxscaling with torchao 6 | - Update IEEE P3109 implementation to Interim Report v3 7 | 8 | 0.4: Nov 13, 2024 9 | - Add stochastic rounding 10 | - Add vectorized versions of round/encode/decode for near-JAX speed 11 | - Update IEEE P3109 implementation to Interim report v2 12 | 13 | 0.3: Jun 10, 2024 14 | - Use python ints throughout, adding float64 to test 15 | - Simplify round, fix directed rounding 16 | - Rename "ival" to "code" in FloatValue 17 | - Shorten format names from "format_info_*" to "*" 18 | 19 | 0.2: May 21, 2024 20 | - Add MX Formats 21 | - Improved CI 22 | - Add value table pretty-printing 23 | 24 | 0.1: May 2, 2024 25 | - First released version 26 | 27 | Copyright (c) 2024 Graphcore Ltd. All rights reserved. 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /test/test_array_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import array_api_strict as xp 4 | import numpy as np 5 | import pytest 6 | 7 | from gfloat import ( 8 | RoundMode, 9 | FormatInfo, 10 | decode_float, 11 | decode_ndarray, 12 | round_float, 13 | round_ndarray, 14 | ) 15 | from gfloat.formats import * 16 | 17 | xp.set_array_api_strict_flags(api_version="2024.12") 18 | 19 | 20 | @pytest.mark.parametrize("fi", sample_formats) 21 | @pytest.mark.parametrize("rnd", RoundMode) 22 | @pytest.mark.parametrize("sat", [True, False]) 23 | def test_array_api(fi: FormatInfo, rnd: RoundMode, sat: bool) -> None: 24 | a0 = np.random.rand(23, 1, 34) - 0.5 25 | a = xp.asarray(a0) 26 | 27 | srnumbits = 32 28 | srbits0 = np.random.randint(0, 2**srnumbits, a.shape) 29 | srbits = xp.asarray(srbits0) 30 | 31 | round_ndarray(fi, a, rnd, sat, srbits=srbits, srnumbits=srnumbits) # type: ignore 32 | -------------------------------------------------------------------------------- /test/test_block.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from gfloat import ( 7 | decode_float, 8 | decode_block, 9 | quantize_block, 10 | encode_block, 11 | compute_scale_amax, 12 | ) 13 | from gfloat.formats import * 14 | 15 | 16 | @pytest.mark.parametrize("fi", all_block_formats) 17 | def test_blocks(fi: BlockFormatInfo) -> None: 18 | 19 | vals = np.linspace(-37.0, 42.0, 32) 20 | 21 | scale = compute_scale_amax(fi.etype.emax, vals) 22 | block = list(encode_block(fi, scale, vals / scale)) 23 | decoded_vals = list(decode_block(fi, block)) 24 | 25 | etype_next_under_max = decode_float(fi.etype, fi.etype.code_of_max - 1).fval 26 | atol = (fi.etype.max - etype_next_under_max) * scale / 2 27 | np.testing.assert_allclose(decoded_vals, vals, atol=atol) 28 | 29 | via_qb = quantize_block(fi, vals, compute_scale_amax) 30 | np.testing.assert_allclose(via_qb, decoded_vals, atol=0.0) 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | name: CI 4 | on: 5 | pull_request: 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | pytest-container: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.10" 18 | cache: "pip" 19 | 20 | - name: Install requirements 21 | run: | 22 | pip install -U pip 23 | pip install .[dev] 24 | 25 | - name: Log installed environment 26 | run: | 27 | python3 -m pip freeze 28 | 29 | - name: Pre-commit all files 30 | run: | 31 | pre-commit run --all-files 32 | 33 | - name: Run unit tests 34 | run: | 35 | pytest -vv . 36 | 37 | - name: MyPy 38 | run: | 39 | mypy --disallow-untyped-defs --enable-error-code redundant-expr src test 40 | 41 | - name: Ensure that docs build 42 | run: | 43 | cd docs && make html 44 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | API 4 | === 5 | 6 | .. module:: gfloat 7 | 8 | Scalar Functions 9 | ---------------- 10 | 11 | .. autofunction:: round_float 12 | .. autofunction:: encode_float 13 | .. autofunction:: decode_float 14 | 15 | Array Functions 16 | --------------- 17 | 18 | .. autofunction:: round_ndarray 19 | .. autofunction:: encode_ndarray 20 | .. autofunction:: decode_ndarray 21 | 22 | Block format functions 23 | ---------------------- 24 | 25 | .. autofunction:: decode_block 26 | .. autofunction:: encode_block 27 | .. autofunction:: quantize_block 28 | 29 | .. autofunction:: compute_scale_amax 30 | 31 | 32 | Classes 33 | ------- 34 | 35 | .. autoclass:: FormatInfo() 36 | :members: 37 | .. autoclass:: FloatClass() 38 | :members: 39 | .. autoclass:: RoundMode() 40 | :members: 41 | .. autoclass:: FloatValue() 42 | :members: 43 | .. autoclass:: BlockFormatInfo() 44 | :members: 45 | 46 | Pretty printers 47 | --------------- 48 | 49 | .. autofunction:: float_pow2str 50 | .. autofunction:: float_tilde_unless_roundtrip_str 51 | -------------------------------------------------------------------------------- /test/test_printing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import numpy as np 4 | 5 | from gfloat import float_pow2str, float_tilde_unless_roundtrip_str 6 | 7 | 8 | def test_pow2str() -> None: 9 | assert float_pow2str(127) == "127/64*2^6" 10 | assert float_pow2str(1.0625 * 2.0**-12) == "17/16*2^-12" 11 | assert float_pow2str(3.0 * 2.0**-12) == "3/2*2^-11" 12 | assert float_pow2str(3.0 / 16 * 2.0**-8) == "3/2*2^-11" 13 | assert float_pow2str(3.0 / 16 * 2.0**-8, min_exponent=-8) == "3/16*2^-8" 14 | 15 | 16 | def test_tilde_unless_roundtrip() -> None: 17 | assert float_tilde_unless_roundtrip_str(1.52587892525e-05) == "~1.5258789e-05" 18 | assert float_tilde_unless_roundtrip_str(28672.0) == "28672.0" 19 | assert float_tilde_unless_roundtrip_str(0.0009765625) == "0.0009765625" 20 | assert float_tilde_unless_roundtrip_str(120.0) == "120.0" 21 | assert float_tilde_unless_roundtrip_str(0.0010001, width=7, d=4) == "~0.0010" 22 | assert float_tilde_unless_roundtrip_str(np.inf, width=7, d=4) == "inf" 23 | assert float_tilde_unless_roundtrip_str(np.nan, width=7, d=4) == "nan" 24 | -------------------------------------------------------------------------------- /test/test_finfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | # Test that finfo methods on FloatFormat agree with numpy/ml_dtypes 4 | 5 | import ml_dtypes 6 | import numpy as np 7 | import pytest 8 | 9 | from gfloat import decode_float, round_float 10 | from gfloat.formats import * 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "fmt,npfmt", 15 | [ 16 | (format_info_ocp_e5m2, ml_dtypes.float8_e5m2), 17 | (format_info_ocp_e4m3, ml_dtypes.float8_e4m3fn), 18 | (format_info_binary16, np.float16), 19 | (format_info_bfloat16, ml_dtypes.bfloat16), 20 | ], 21 | ) 22 | def test_finfo(fmt: FormatInfo, npfmt: np.dtype) -> None: 23 | assert fmt.eps == ml_dtypes.finfo(npfmt).eps 24 | assert fmt.epsneg == ml_dtypes.finfo(npfmt).epsneg 25 | assert fmt.max == ml_dtypes.finfo(npfmt).max 26 | assert fmt.maxexp == ml_dtypes.finfo(npfmt).maxexp 27 | 28 | 29 | def test_constants() -> None: 30 | assert format_info_p3109(8, 1).smallest_subnormal == 2.0**-63 31 | assert format_info_p3109(8, 4).smallest_subnormal == 2.0**-10 32 | assert format_info_p3109(8, 7).smallest_subnormal == 2.0**-6 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Graphcore Ltd. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | [build-system] 4 | requires = ["setuptools", "setuptools-scm"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [tool.setuptools] 8 | packages = ['gfloat'] 9 | package-dir = {"" = "src"} 10 | 11 | [project] 12 | name = "gfloat" 13 | version = "0.5.2" # Set version in package.sh 14 | authors = [ 15 | {name = "Andrew Fitzgibbon", email = "awf@fitzgibbon.ie"}, 16 | ] 17 | description = "Generic floating point handling in Python" 18 | readme = "README.md" 19 | classifiers = [ 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Programming Language :: Python :: 3", 23 | "Development Status :: 3 - Alpha", 24 | ] 25 | requires-python = ">=3.8.1" 26 | dynamic = ["dependencies", "optional-dependencies"] 27 | 28 | [tool.setuptools.dynamic] 29 | # version = {attr = "gfloat.VERSION"} # Wow: https://github.com/pypa/setuptools/issues/1724 30 | dependencies = {file = ["requirements.txt"]} 31 | optional-dependencies = {dev = {file = ["requirements-dev.txt"]}} 32 | 33 | [tool.black] 34 | line-length = 90 35 | fast = true 36 | 37 | [tool.mypy] 38 | [[tool.mypy.overrides]] 39 | module = ["torchao.*", "array_api_compat.*", "array_api_strict.*"] 40 | ignore_missing_imports = true 41 | 42 | [tool.pytest.ini_options] 43 | addopts = "--nbval" 44 | testpaths = ["docs", "test"] 45 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | # Configuration file for the Sphinx documentation builder. 4 | 5 | # -- Project information 6 | 7 | project = "GFloat" 8 | copyright = "2024, Graphcore Ltd" 9 | author = "Andrew Fitzgibbon" 10 | release = "0.5.2" # Set version in package.sh 11 | version = "0.5.2" # Set version in package.sh 12 | 13 | # -- General configuration 14 | 15 | extensions = [ 16 | "sphinx.ext.duration", 17 | "sphinx.ext.doctest", 18 | "sphinx.ext.autodoc", 19 | "sphinx.ext.autosummary", 20 | "sphinx.ext.intersphinx", 21 | "sphinx.ext.viewcode", 22 | "sphinx.ext.napoleon", 23 | "sphinx_paramlinks", 24 | "myst_nb", 25 | ] 26 | 27 | autodoc_typehints = "none" # We have them in the parameter descriptors 28 | autodoc_typehints_format = "short" 29 | python_use_unqualified_type_names = True 30 | 31 | autodoc_type_aliases = { 32 | "Iterable": "Iterable", 33 | "npt.ArrayLike": "ArrayLike", 34 | "npt.NDArray": "NDArray", 35 | } 36 | 37 | autodoc_default_options = { 38 | "member-order": "bysource", 39 | } 40 | 41 | intersphinx_mapping = { 42 | "python": ("https://docs.python.org/3/", None), 43 | "sphinx": ("https://www.sphinx-doc.org/en/master/", None), 44 | } 45 | intersphinx_disabled_domains = ["std"] 46 | 47 | templates_path = ["_templates"] 48 | 49 | # -- Options for HTML output 50 | 51 | html_theme = "sphinx_rtd_theme" 52 | 53 | # -- Options for EPUB output 54 | epub_show_urls = "footnote" 55 | 56 | # -- Options for myst_nb 57 | nb_execution_mode = "off" 58 | -------------------------------------------------------------------------------- /test/test_p3109_spec.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | from typing import Callable 3 | import ml_dtypes 4 | import numpy as np 5 | import pytest 6 | 7 | from gfloat import FloatClass, Domain, decode_float, decode_ndarray 8 | from gfloat.formats import * 9 | 10 | 11 | def spec_is_normal(fi: FormatInfo, x: int) -> bool: 12 | r""" 13 | Copy from spec: 14 | 15 | \Case{\isNormal*(x \in \{0, \NInf, \Inf, \NaN\}) \gives \False}\\ 16 | \Case{\isNormal*(x) \gives 17 | \begin{cases} 18 | (x \mod 2^{\k_f - 1}) \div 2^{\p_f - 1} > 0 & \If \s_f = \Signed \\ 19 | x \div 2^{\p_f - 1} > 0 & \If \s_f = \Unsigned 20 | \end{cases} 21 | } 22 | """ 23 | if x in (fi.code_of_zero, fi.code_of_nan): 24 | return False 25 | if fi.num_neginfs > 0 and x == fi.code_of_neginf: 26 | return False 27 | if fi.num_posinfs > 0 and x == fi.code_of_posinf: 28 | return False 29 | 30 | k_f = fi.k 31 | p_f = fi.precision 32 | if fi.is_signed: 33 | # (x \mod 2^{\k_f - 1}) \div 2^{\p_f - 1} > 0 34 | return (x % 2 ** (k_f - 1)) // 2 ** (p_f - 1) > 0 35 | else: 36 | # x \div 2^{\p_f - 1} > 0 37 | return x // 2 ** (p_f - 1) > 0 38 | 39 | 40 | _p3109_formats_to_test = ( 41 | (2, 1), 42 | (2, 2), 43 | (3, 1), 44 | (3, 2), 45 | (3, 3), 46 | (4, 1), 47 | (4, 2), 48 | (4, 3), 49 | (4, 4), 50 | (6, 1), 51 | (6, 5), 52 | (8, 3), 53 | (8, 1), 54 | (11, 3), 55 | ) 56 | 57 | 58 | @pytest.mark.parametrize("k,p", _p3109_formats_to_test) 59 | @pytest.mark.parametrize("signedness", Signedness) 60 | def test_p3109_specials_signed(k: int, p: int, signedness: Signedness) -> None: 61 | fi = format_info_p3109(k, p, signedness, Domain.Extended) 62 | 63 | for i in range(2**fi.k): 64 | fv = decode_float(fi, i) 65 | assert spec_is_normal(fi, i) == (fv.fclass == FloatClass.NORMAL) 66 | -------------------------------------------------------------------------------- /src/gfloat/printing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import fractions 4 | 5 | import numpy as np 6 | 7 | 8 | def float_pow2str(v: float, min_exponent: float = -np.inf) -> str: 9 | """ 10 | Render floating point values as exact fractions times a power of two. 11 | 12 | Example: float_pow2str(127.0) is "127/64*2^6", 13 | 14 | That is (a significand between 1 and 2) times (a power of two). 15 | 16 | If `min_exponent` is supplied, then values with exponent below `min_exponent`, 17 | are printed as fractions less than 1, with exponent set to `min_exponent`. 18 | This is typically used to represent subnormal values. 19 | 20 | """ 21 | if not np.isfinite(v): 22 | return str(v) 23 | 24 | signstr = "-" if np.signbit(v) else "" 25 | 26 | x = np.abs(v) 27 | e = int(np.floor(np.log2(x))) 28 | sig = np.ldexp(x, -e) 29 | if e < min_exponent: 30 | sig = np.ldexp(sig, e - min_exponent) 31 | e = int(min_exponent) 32 | 33 | pow2str = f"2^{e:d}" 34 | 35 | significand = fractions.Fraction(sig) 36 | if significand == 1: 37 | return signstr + pow2str 38 | else: 39 | return signstr + f"{significand}*{pow2str}" 40 | 41 | 42 | def float_tilde_unless_roundtrip_str(v: float, width: int = 14, d: int = 8) -> str: 43 | """ 44 | Return a string representation of :paramref:`v`, in base 10, 45 | with maximum width :paramref:`width` and decimal digits :paramref:`d` 46 | 47 | 48 | """ 49 | # valstr: string representation of value in base 10 50 | # If the representation does not roundtrip to the value, 51 | # it is preceded by a "~" to indicate "approximately equal to" 52 | s = f"{v}" 53 | if len(s) > width: 54 | if abs(v) < 1 and "e" not in s: 55 | s = f"{v:.{d}f}" 56 | else: 57 | s = f"{v:.{d}}" 58 | if np.isfinite(v) and float(s) != v: 59 | s = "~" + s 60 | 61 | return s 62 | -------------------------------------------------------------------------------- /test/test_encode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from typing import Callable 4 | 5 | import numpy as np 6 | import numpy.typing as npt 7 | import pytest 8 | 9 | from gfloat import decode_float, encode_float, encode_ndarray 10 | from gfloat.formats import * 11 | 12 | 13 | @pytest.mark.parametrize("fi", sample_formats) 14 | def test_encode(fi: FormatInfo) -> None: 15 | dec = lambda v: decode_float(fi, v).fval 16 | 17 | if fi.bits <= 8: 18 | step = 1 19 | elif fi.bits <= 16: 20 | step = 13 21 | elif fi.bits <= 32: 22 | step = 73013 23 | elif fi.bits <= 64: 24 | step = (73013 << 32) + 39 25 | 26 | for i in range(0, 2**fi.bits, step): 27 | fv = decode_float(fi, i) 28 | code = encode_float(fi, fv.fval) 29 | assert (i == code) or (np.isnan(fv.fval) and code == fi.code_of_nan) 30 | fv2 = decode_float(fi, code) 31 | np.testing.assert_equal(fv2.fval, fv.fval) 32 | 33 | codes = np.arange(0, 2**fi.bits, step, dtype=np.uint64) 34 | fvals = np.array([decode_float(fi, int(i)).fval for i in codes]) 35 | enc_codes = encode_ndarray(fi, fvals) 36 | expected_codes: npt.NDArray 37 | if fi.num_nans == 0: 38 | assert not np.any(np.isnan(fvals)) 39 | expected_codes = codes 40 | else: 41 | expected_codes = np.where(np.isnan(fvals), fi.code_of_nan, codes) 42 | np.testing.assert_equal(enc_codes, expected_codes) 43 | 44 | 45 | @pytest.mark.parametrize("fi", sample_formats) 46 | @pytest.mark.parametrize("enc", (encode_float, encode_ndarray)) 47 | def test_encode_edges(fi: FormatInfo, enc: Callable) -> None: 48 | if enc == encode_ndarray: 49 | enc = lambda fi, x: encode_ndarray(fi, np.array([x])).item() 50 | 51 | assert enc(fi, fi.max) == fi.code_of_max 52 | 53 | assert enc(fi, fi.max * 1.25) == ( 54 | fi.code_of_posinf 55 | if fi.domain == Domain.Extended 56 | else fi.code_of_nan if fi.num_nans > 0 else fi.code_of_max 57 | ) 58 | 59 | if fi.is_signed: 60 | assert enc(fi, fi.min * 1.25) == ( 61 | fi.code_of_neginf 62 | if fi.domain == Domain.Extended 63 | else fi.code_of_nan if fi.num_nans > 0 else fi.code_of_min 64 | ) 65 | -------------------------------------------------------------------------------- /test/test_microxcaling.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import pytest 4 | 5 | import numpy as np 6 | from numpy.typing import NDArray 7 | 8 | import torch 9 | 10 | from torchao.prototype.mx_formats.mx_tensor import MXTensor 11 | from torchao.prototype.mx_formats.constants import ( 12 | DTYPE_FP6_E2M3, 13 | DTYPE_FP6_E3M2, 14 | DTYPE_FP4, 15 | ) 16 | 17 | from gfloat import ( 18 | BlockFormatInfo, 19 | FormatInfo, 20 | RoundMode, 21 | quantize_block, 22 | compute_scale_amax, 23 | encode_block, 24 | ) 25 | from gfloat.formats import ( 26 | # format_info_ocp_int8, 27 | format_info_ocp_e3m2, 28 | format_info_ocp_e2m1, 29 | format_info_ocp_e8m0, 30 | ) 31 | 32 | 33 | @pytest.mark.parametrize( 34 | ("mx_etype,gf_etype"), 35 | [ 36 | # (ElemFormat.int8, format_info_ocp_int8), 37 | (DTYPE_FP6_E3M2, format_info_ocp_e3m2), 38 | (DTYPE_FP4, format_info_ocp_e2m1), 39 | ], 40 | ) 41 | @pytest.mark.parametrize( 42 | "A", 43 | [ 44 | np.arange(32) / 2 - 5, 45 | np.zeros(32), 46 | ], 47 | ids=[ 48 | "tennish", 49 | "zeros", 50 | ], 51 | ) 52 | def test_mx( 53 | mx_etype: str, 54 | gf_etype: FormatInfo, 55 | A: NDArray[np.float64], 56 | ) -> None: 57 | ta = torch.tensor(A, dtype=torch.float32) 58 | 59 | # MX: Quantize 60 | mx_dq = MXTensor.to_mx(ta, mx_etype).to_dtype(ta.dtype) 61 | 62 | # GFloat: Declare block format 63 | fi = BlockFormatInfo("test", gf_etype, 32, format_info_ocp_e8m0) 64 | 65 | # GFloat: Quantize 66 | gf_dq = quantize_block(fi, ta, compute_scale_amax) # type: ignore [arg-type] 67 | 68 | # Compare 69 | np.testing.assert_allclose(gf_dq, mx_dq) 70 | 71 | 72 | def test_mx_exceptions() -> None: 73 | fi = BlockFormatInfo("test", format_info_ocp_e2m1, 32, format_info_ocp_e8m0) 74 | 75 | A = np.ones(32) * 2.0**-139 76 | 77 | s = compute_scale_amax(fi.etype.emax, A) 78 | assert s == 2.0**-127 79 | 80 | with pytest.raises(ValueError, match="out of range"): 81 | list(encode_block(fi, fi.stype.max * 2, A)) 82 | 83 | assert not fi.stype.is_signed 84 | scale = fi.stype.min / 2 85 | assert scale != 0 86 | with pytest.raises(ValueError, match="out of range"): 87 | list(encode_block(fi, scale, A)) 88 | -------------------------------------------------------------------------------- /src/gfloat/encode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import math 4 | 5 | import numpy as np 6 | 7 | from .types import FormatInfo, Domain 8 | 9 | 10 | def encode_float(fi: FormatInfo, v: float) -> int: 11 | """ 12 | Encode input to the given :py:class:`FormatInfo`. 13 | 14 | Will round toward zero if :paramref:`v` is not in the value set. 15 | Will saturate to `Inf`, `NaN`, `fi.max` in order of precedence. 16 | Encode -0 to 0 if not `fi.has_nz` 17 | 18 | For other roundings and saturations, call :func:`round_float` first. 19 | 20 | Args: 21 | fi (FormatInfo): Describes the target format 22 | v (float): The value to be encoded. 23 | 24 | Returns: 25 | The integer code point 26 | """ 27 | 28 | # Format Constants 29 | k = fi.bits 30 | p = fi.precision 31 | t = p - 1 32 | 33 | # Encode 34 | if np.isnan(v): 35 | return fi.code_of_nan 36 | 37 | # Overflow/underflow 38 | if v > fi.max: 39 | if fi.domain == Domain.Extended: 40 | return fi.code_of_posinf 41 | if fi.num_nans > 0: 42 | return fi.code_of_nan 43 | return fi.code_of_max 44 | 45 | if v < fi.min: 46 | if fi.domain == Domain.Extended: 47 | return fi.code_of_neginf 48 | if fi.num_nans > 0: 49 | return fi.code_of_nan 50 | return fi.code_of_min 51 | 52 | # Finite values 53 | sign = fi.is_signed and np.signbit(v) 54 | vpos = -v if sign else v 55 | 56 | if fi.has_subnormals and vpos <= fi.smallest_subnormal / 2: 57 | isig = 0 58 | biased_exp = 0 59 | else: 60 | sig, exp = np.frexp(vpos) 61 | exp = int(exp) # All calculations in Python ints 62 | 63 | # sig in range [0.5, 1) 64 | sig *= 2 65 | exp -= 1 66 | # now sig in range [1, 2) 67 | 68 | biased_exp = exp + fi.bias 69 | if biased_exp < 1 and fi.has_subnormals: 70 | # subnormal 71 | sig *= 2.0 ** (biased_exp - 1) 72 | biased_exp = 0 73 | assert vpos == sig * 2 ** (1 - fi.bias) 74 | else: 75 | if sig > 0: 76 | sig -= 1.0 77 | 78 | isig = math.floor(sig * 2**t) 79 | 80 | # Zero 81 | if isig == 0 and biased_exp == 0 and fi.has_zero: 82 | if sign and fi.has_nz: 83 | return fi.code_of_negzero 84 | else: 85 | return fi.code_of_zero 86 | 87 | # Nonzero 88 | assert isig < 2**t 89 | assert biased_exp < 2**fi.expBits or fi.is_twos_complement 90 | 91 | # Handle two's complement encoding 92 | if fi.is_twos_complement and sign: 93 | isig = (1 << t) - isig 94 | 95 | # Pack values into a single integer 96 | code = (int(sign) << (k - 1)) | (biased_exp << t) | (isig << 0) 97 | 98 | return code 99 | -------------------------------------------------------------------------------- /src/gfloat/encode_ndarray.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from .types import FormatInfo, Domain 4 | import numpy as np 5 | import numpy.typing as npt 6 | 7 | 8 | def encode_ndarray(fi: FormatInfo, v: npt.NDArray) -> npt.NDArray: 9 | """ 10 | Vectorized version of :meth:`encode_float`. 11 | 12 | Encode inputs to the given :py:class:`FormatInfo`. 13 | 14 | Will round toward zero if :paramref:`v` is not in the value set. 15 | Will saturate to `Inf`, `NaN`, `fi.max` in order of precedence. 16 | Encode -0 to 0 if not `fi.has_nz` 17 | 18 | For other roundings and saturations, call :func:`round_ndarray` first. 19 | 20 | Args: 21 | fi (FormatInfo): Describes the target format 22 | v (float array): The value to be encoded. 23 | 24 | Returns: 25 | The integer code point 26 | """ 27 | k = fi.bits 28 | p = fi.precision 29 | t = p - 1 30 | 31 | sign = np.signbit(v) & fi.is_signed 32 | vpos = np.where(sign, -v, v) 33 | 34 | nan_mask = np.isnan(v) 35 | 36 | code = np.zeros_like(v, dtype=np.uint64) 37 | 38 | if fi.num_nans > 0: 39 | code[nan_mask] = fi.code_of_nan 40 | else: 41 | assert not np.any(nan_mask) 42 | 43 | if fi.domain == Domain.Extended: 44 | code[v > fi.max] = fi.code_of_posinf 45 | if fi.is_signed: 46 | code[v < fi.min] = fi.code_of_neginf 47 | else: 48 | code[v > fi.max] = fi.code_of_nan if fi.num_nans > 0 else fi.code_of_max 49 | if fi.is_signed: 50 | code[v < fi.min] = fi.code_of_nan if fi.num_nans > 0 else fi.code_of_min 51 | 52 | if fi.has_zero: 53 | if fi.has_nz: 54 | code[v == 0] = np.where(sign[v == 0], fi.code_of_negzero, fi.code_of_zero) 55 | else: 56 | code[v == 0] = fi.code_of_zero 57 | 58 | finite_mask = (code == 0) & (v != 0) 59 | assert not np.any(np.isnan(vpos[finite_mask])) 60 | if np.any(finite_mask): 61 | finite_vpos = vpos[finite_mask] 62 | finite_sign = sign[finite_mask] 63 | 64 | sig, exp = np.frexp(finite_vpos) 65 | 66 | biased_exp = exp.astype(np.int64) + (fi.bias - 1) 67 | subnormal_mask = (biased_exp < 1) & fi.has_subnormals 68 | 69 | biased_exp_safe = np.where(subnormal_mask, biased_exp, 0) 70 | tsig = np.where(subnormal_mask, np.ldexp(sig, biased_exp_safe), sig * 2 - 1.0) 71 | biased_exp[subnormal_mask] = 0 72 | 73 | isig = np.floor(np.ldexp(tsig, t)).astype(np.int64) 74 | 75 | zero_mask = fi.has_zero & (isig == 0) & (biased_exp == 0) 76 | if not fi.has_nz: 77 | finite_sign[zero_mask] = False 78 | 79 | # Handle two's complement encoding 80 | if fi.is_twos_complement: 81 | isig[finite_sign] = (1 << t) - isig[finite_sign] 82 | 83 | code[finite_mask] = ( 84 | (finite_sign.astype(int) << (k - 1)) | (biased_exp << t) | (isig << 0) 85 | ) 86 | 87 | return code 88 | -------------------------------------------------------------------------------- /src/gfloat/decode_ndarray.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from types import ModuleType 4 | import numpy as np 5 | import numpy.typing as npt 6 | from .types import FormatInfo, Domain 7 | 8 | 9 | def decode_ndarray( 10 | fi: FormatInfo, codes: npt.NDArray, np: ModuleType = np 11 | ) -> npt.NDArray: 12 | r""" 13 | Vectorized version of :meth:`decode_float` 14 | 15 | Args: 16 | fi (FormatInfo): Floating point format descriptor. 17 | i (array of int): Integer code points, in the range :math:`0 \le i < 2^{k}`, 18 | where :math:`k` = ``fi.k`` 19 | 20 | Returns: 21 | Decoded float values 22 | 23 | Raises: 24 | ValueError: 25 | If any :paramref:`i` is outside the range of valid code points in :paramref:`fi`. 26 | """ 27 | assert np.issubdtype(codes.dtype, np.integer) 28 | 29 | k = fi.k 30 | p = fi.precision 31 | t = p - 1 # Trailing significand field width 32 | num_signbits = 1 if fi.is_signed else 0 33 | w = k - t - num_signbits # Exponent field width 34 | 35 | if np.any(codes < 0) or np.any(codes >= 2**k): 36 | raise ValueError(f"Code point not in range [0, 2**{k})") 37 | 38 | if fi.is_signed: 39 | signmask = 1 << (k - 1) 40 | sign = np.where(codes & signmask, -1.0, 1.0) 41 | else: 42 | signmask = None 43 | sign = 1.0 44 | 45 | exp = ((codes >> t) & ((1 << w) - 1)).astype(np.int64) 46 | significand = codes & ((1 << t) - 1) 47 | if fi.is_twos_complement: 48 | significand = np.where(sign < 0, (1 << t) - significand, significand) 49 | 50 | bias = fi.bias 51 | 52 | fval = np.zeros_like(codes, dtype=np.float64) 53 | isspecial = np.zeros_like(codes, dtype=bool) 54 | 55 | if fi.domain == Domain.Extended: 56 | fval = np.where(codes == fi.code_of_posinf, np.inf, fval) 57 | isspecial |= codes == fi.code_of_posinf 58 | if fi.is_signed: 59 | fval = np.where(codes == fi.code_of_neginf, -np.inf, fval) 60 | isspecial |= codes == fi.code_of_neginf 61 | 62 | if fi.num_nans > 0: 63 | code_is_nan = codes == fi.code_of_nan 64 | if w > 0: 65 | # All-bits-special exponent (ABSE) 66 | abse = exp == 2**w - 1 67 | min_code_with_nan = 2 ** (p - 1) - fi.num_high_nans 68 | code_is_nan |= abse & (significand >= min_code_with_nan) 69 | 70 | fval = np.where(code_is_nan, np.nan, fval) 71 | isspecial |= code_is_nan 72 | 73 | # Zero 74 | iszero = ~isspecial & (exp == 0) & (significand == 0) & fi.has_zero 75 | fval = np.where(iszero, 0.0, fval) 76 | if fi.has_nz: 77 | fval = np.where(iszero & (sign < 0), -0.0, fval) 78 | 79 | issubnormal = (exp == 0) & (significand != 0) & fi.has_subnormals 80 | expval = np.where(issubnormal, 1 - bias, exp - bias) 81 | fsignificand = np.where(issubnormal, 0.0, 1.0) + np.ldexp(significand, -t) 82 | 83 | # Normal/Subnormal/Zero case, other values will be overwritten 84 | expval_safe = np.where(isspecial | iszero, 0, expval) 85 | fval_finite_safe = sign * np.ldexp(fsignificand, expval_safe) 86 | fval = np.where(~(iszero | isspecial), fval_finite_safe, fval) 87 | 88 | return fval 89 | -------------------------------------------------------------------------------- /test/test_torch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Graphcore Ltd. All rights reserved. 2 | 3 | import pytest 4 | 5 | import numpy.typing as npt 6 | import torch 7 | 8 | from gfloat import FormatInfo, RoundMode, round_ndarray 9 | from gfloat.formats import format_info_ocp_e5m2, format_info_p3109 10 | 11 | 12 | def test_torch() -> None: 13 | """ 14 | Test that Torch tensors agree with e5m2 15 | """ 16 | a = torch.randn(1024) 17 | 18 | a8 = a.to(dtype=torch.float8_e5m2).to(dtype=torch.float32) 19 | 20 | fi = format_info_ocp_e5m2 21 | t8 = round_ndarray(fi, a) # type: ignore [arg-type] 22 | 23 | torch.testing.assert_close(a8, t8, atol=0.0, rtol=0.0) 24 | 25 | # Check torch.compile 26 | tc = torch.compile(lambda x: round_ndarray(fi, x)) 27 | t8i = tc(a) 28 | 29 | torch.testing.assert_close(a8, t8i, atol=0.0, rtol=0.0) 30 | 31 | 32 | @pytest.mark.parametrize( 33 | "rnd", 34 | ( 35 | RoundMode.TowardZero, 36 | RoundMode.TowardNegative, 37 | RoundMode.TowardPositive, 38 | RoundMode.TiesToEven, 39 | RoundMode.TiesToAway, 40 | ), 41 | ) 42 | @pytest.mark.parametrize("fi", (format_info_ocp_e5m2, format_info_p3109(8, 3))) 43 | @pytest.mark.parametrize("sat", (True, False)) 44 | def test_torch_compile_agrees(fi: FormatInfo, rnd: RoundMode, sat: bool) -> None: 45 | """ 46 | Test that Torch compile output agrees with eager 47 | """ 48 | a = torch.randn(1024) 49 | a[18] = torch.inf 50 | a[19] = -torch.inf 51 | 52 | t8 = round_ndarray(fi, a, rnd, sat) # type: ignore [arg-type] 53 | 54 | # Check torch.compile 55 | tc = torch.compile(lambda x: round_ndarray(fi, x, rnd, sat)) 56 | t8i = tc(a) 57 | 58 | torch.testing.assert_close(t8, t8i, atol=0.0, rtol=0.0) 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "rnd", 63 | ( 64 | RoundMode.Stochastic, 65 | RoundMode.StochasticOdd, 66 | RoundMode.StochasticFast, 67 | RoundMode.StochasticFastest, 68 | ), 69 | ) 70 | @pytest.mark.parametrize("fi", (format_info_ocp_e5m2, format_info_p3109(8, 3))) 71 | def test_torch_compile_agrees_sr(fi: FormatInfo, rnd: RoundMode) -> None: 72 | """ 73 | Test that Torch tensors don't crash 74 | """ 75 | a = torch.randn(1024) 76 | a[18] = torch.inf 77 | a[19] = -torch.inf 78 | 79 | srnumbits = 5 80 | srbits = torch.randint(0, 2**srnumbits, a.shape) 81 | 82 | t8 = round_ndarray(fi, a, rnd, srbits=srbits, srnumbits=srnumbits) # type: ignore [arg-type] 83 | 84 | # Check torch.compile 85 | @torch.compile 86 | def tc(x: npt.NDArray) -> npt.NDArray: 87 | return round_ndarray(fi, x, rnd, srbits=srbits, srnumbits=srnumbits) # type: ignore [arg-type] 88 | 89 | t8_tc = tc(a) # type: ignore [arg-type] 90 | 91 | torch.testing.assert_close(t8, t8_tc, atol=0.0, rtol=0.0) 92 | 93 | # Check torch.compile dynamic 94 | @torch.compile(dynamic=True) 95 | def tc2(x: npt.NDArray) -> npt.NDArray: 96 | return round_ndarray(fi, x, rnd, srbits=srbits, srnumbits=srnumbits) # type: ignore [arg-type] 97 | 98 | t8_tc2 = tc2(a) # type: ignore [arg-type] 99 | 100 | torch.testing.assert_close(t8, t8_tc2, atol=0.0, rtol=0.0) 101 | -------------------------------------------------------------------------------- /src/gfloat/decode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import numpy as np 4 | 5 | from .types import FloatClass, FloatValue, FormatInfo, Domain 6 | 7 | 8 | def decode_float(fi: FormatInfo, i: int) -> FloatValue: 9 | r""" 10 | Given :py:class:`FormatInfo` and integer code point, decode to a :py:class:`FloatValue` 11 | 12 | Args: 13 | fi (FormatInfo): Floating point format descriptor. 14 | i (int): Integer code point, in the range :math:`0 \le i < 2^{k}`, 15 | where :math:`k` = ``fi.k`` 16 | 17 | Returns: 18 | Decoded float value 19 | 20 | Raises: 21 | ValueError: 22 | If :paramref:`i` is outside the range of valid code points in :paramref:`fi`. 23 | """ 24 | assert isinstance(i, int) 25 | 26 | k = fi.k 27 | p = fi.precision 28 | t = p - 1 # Trailing significand field width 29 | num_signbits = 1 if fi.is_signed else 0 30 | w = k - t - num_signbits # Exponent field width 31 | 32 | if i < 0 or i >= 2**k: 33 | raise ValueError(f"Code point {i} not in range [0, 2**{k})") 34 | 35 | if fi.is_signed: 36 | signmask = 1 << (k - 1) 37 | signbit = 1 if i & signmask else 0 38 | sign = -1 if signbit else 1 39 | else: 40 | signmask = None 41 | signbit = 0 42 | sign = 1 43 | 44 | exp = (i >> t) & ((1 << w) - 1) 45 | significand = i & ((1 << t) - 1) 46 | if fi.is_twos_complement and signbit: 47 | significand = (1 << t) - significand 48 | 49 | bias = fi.bias 50 | 51 | iszero = exp == 0 and significand == 0 and fi.has_zero 52 | issubnormal = fi.has_subnormals and (exp == 0) and (significand != 0) 53 | isnormal = not iszero and not issubnormal 54 | if iszero or issubnormal: 55 | expval = 1 - bias 56 | fsignificand = significand * 2**-t 57 | else: 58 | expval = exp - bias 59 | fsignificand = 1.0 + significand * 2**-t 60 | 61 | # Handle specials: Infs, NaN, -0, NaN_0 62 | 63 | # High NaNs 64 | fval = None 65 | max_positive_code = (1 << (k - fi.signBits)) - 1 66 | code_without_sign = i & max_positive_code 67 | if code_without_sign > max_positive_code - fi.num_high_nans: 68 | # Return nan, ignore sign 69 | fval = np.nan 70 | 71 | # Infinities 72 | if fi.domain == Domain.Extended: 73 | if code_without_sign == max_positive_code - fi.num_high_nans: 74 | fval = -np.inf if signbit else np.inf 75 | 76 | # Negative zero or NaN 77 | if iszero and i == signmask and not fi.is_twos_complement: 78 | if fi.has_nz: 79 | fval = -0.0 80 | else: 81 | fval = np.nan 82 | 83 | # In range - compute value 84 | if fval is None: 85 | fval = sign * fsignificand * 2.0**expval 86 | 87 | # Compute FloatClass 88 | fclass = None 89 | if fval == 0: 90 | fclass = FloatClass.ZERO 91 | elif np.isnan(fval): 92 | fclass = FloatClass.NAN 93 | elif np.isfinite(fval): 94 | if isnormal: 95 | fclass = FloatClass.NORMAL 96 | else: 97 | fclass = FloatClass.SUBNORMAL 98 | else: 99 | fclass = FloatClass.INFINITE 100 | 101 | return FloatValue(i, fval, exp, expval, significand, fsignificand, signbit, fclass) 102 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | .. note:: 4 | 5 | Check the version number of this documentation against the `gfloat` version 6 | you are using. "Latest" refers to the head on https://github.com/graphcore-research/gfloat, 7 | while pypi versions installed using `pip install` will have corresponding `vX.Y.Z` tags. 8 | 9 | GFloat: Generic floating point formats in Python 10 | ================================================ 11 | 12 | GFloat is designed to allow experimentation with a variety of floating-point 13 | formats in Python. Headline features: 14 | 15 | * A wide variety of floating point formats defined in :py:class:`gfloat.formats` 16 | 17 | - IEEE 754 and P3109, BFloat, OCP FP8 and MX 18 | 19 | * Conversion between floats under numerous rounding modes 20 | 21 | - Scalar code is optimized for readability 22 | - Array code is faster, and can operate on Numpy, JAX, or PyTorch arrays. 23 | 24 | * Notebooks useful for teaching and exploring float formats 25 | 26 | Provided Formats 27 | ---------------- 28 | 29 | Formats are parameterized by the primary parameters of: 30 | 31 | * Width in bits (k) 32 | * Precision (p) 33 | * Exponent bias (bias) 34 | 35 | with additional fields defining the presence/encoding of: 36 | 37 | * Domain (Finite vs Extended) 38 | * Signed/unsigned 39 | * Not-a-number (NaN) values 40 | * Negative zero 41 | * Subnormal numbers 42 | * Two's complement encoding (of the significand) 43 | 44 | This allows an implementation of generic floating point encode/decode logic, 45 | handling various current and proposed floating point types: 46 | 47 | - `IEEE 754 `_: Binary16, Binary32 48 | - `Brain floating point `_: BFloat16 49 | - |p3109_link|: P3109_{K}p{P}{s}{d} for K > 2, and 1 <= P <= K, Signedness, and Domain 50 | - |ocp_link|: E5M2, E4M3 51 | - Types from the |ocp_mx_link| spec: E8M0, INT8, and FP4, FP6 types 52 | 53 | As well as block formats from |ocp_mx_link|. 54 | 55 | .. |ocp_mx_link| raw:: html 56 | 57 | 58 | OCP MX 59 | 60 | 61 | .. |ocp_link| raw:: html 62 | 63 | 64 | OCP Float8 65 | 66 | 67 | .. |p3109_link| raw:: html 68 | 69 | 70 | IEEE P3109 71 | 72 | 73 | Rounding modes 74 | -------------- 75 | 76 | Various rounding modes: 77 | * Directed modes: Toward Zero, Toward Positive, Toward Negative 78 | * Round-to-nearest, with Ties to Even or Ties to Away 79 | * Stochastic rounding, with specified numbers of random bits 80 | 81 | 82 | See Also 83 | -------- 84 | 85 | GFloat, being a pure Python library, favours readability and extensibility over speed 86 | (although the `*_ndarray` functions are reasonably fast for large arrays). 87 | For fast implementations of these datatypes see, for example, 88 | `ml_dtypes `_, 89 | `bitstring `_, 90 | `MX PyTorch Emulation Library `_, 91 | `APyTypes `_, 92 | `TorchAO `_. 93 | 94 | To get started with the library, we recommend perusing the notebooks, 95 | otherwise you may wish to jump straight into the API. 96 | 97 | Contents 98 | ======== 99 | 100 | .. toctree:: 101 | :hidden: 102 | 103 | self 104 | 105 | .. toctree:: 106 | 107 | notebooks 108 | api 109 | formats 110 | 111 | 112 | Index and Search 113 | ================ 114 | 115 | * :ref:`genindex` 116 | * :ref:`search` 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | .vscode/settings.json 164 | .vscode/launch.json 165 | 166 | # Local 167 | tmp/ 168 | -------------------------------------------------------------------------------- /docs/source/04-benchmark.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Copyright (c) 2024 Graphcore Ltd. All rights reserved.\n", 10 | "\n", 11 | "import numpy as np\n", 12 | "import jax\n", 13 | "import jax.numpy as jnp\n", 14 | "import ml_dtypes\n", 15 | "import gfloat\n", 16 | "from gfloat.formats import format_info_ocp_e5m2\n", 17 | "from timeit import Timer\n", 18 | "\n", 19 | "jax.config.update(\"jax_enable_x64\", True)" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "# Timing tests\n", 27 | "\n", 28 | "The `gfloat` library is designed for readability over performance, and the reference code for computations is the (slow) scalar code e.g. `round_float`.\n", 29 | "\n", 30 | "There are vectorized implementations (e.g. `round_ndarray`), and when combined with JAX, these can go reasonably fast.\n", 31 | "\n", 32 | "Let's see how long it takes to encode some values to FP8..." 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 2, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "name": "stderr", 42 | "output_type": "stream", 43 | "text": [ 44 | "WARNING:2025-08-20 15:40:01,949:jax._src.xla_bridge:872: An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" 45 | ] 46 | }, 47 | { 48 | "name": "stdout", 49 | "output_type": "stream", 50 | "text": [ 51 | "GFloat scalar : 2605.38 nsec (50 runs at size 10000)\n", 52 | "GFloat vectorized, numpy arrays: 50.20 nsec (25 runs at size 1000000)\n", 53 | "GFloat vectorized, JAX JIT : 3.79 nsec (500 runs at size 1000000)\n", 54 | "ML_dtypes : 2.60 nsec (500 runs at size 1000000)\n" 55 | ] 56 | } 57 | ], 58 | "source": [ 59 | "# NBVAL_IGNORE_OUTPUT\n", 60 | "\n", 61 | "N = 1_000_000\n", 62 | "a = np.random.rand(N)\n", 63 | "\n", 64 | "jax_round_jit = jax.jit(lambda x: gfloat.round_ndarray(format_info_ocp_e5m2, x))\n", 65 | "ja = jnp.array(a)\n", 66 | "jax_round_jit(ja) # Cache compilation\n", 67 | "\n", 68 | "\n", 69 | "def slow_round_ndarray(fi, a):\n", 70 | " return np.array([gfloat.round_float(fi, x) for x in a])\n", 71 | "\n", 72 | "\n", 73 | "# About how many seconds to run for (autorange will take at least .2 sec)\n", 74 | "ACCURACY = 1.0\n", 75 | "\n", 76 | "\n", 77 | "def time(f, problem_size=1.0):\n", 78 | " units = 1e9 # nsec\n", 79 | " t = Timer(f)\n", 80 | " f() # pre-run\n", 81 | " n = int(t.autorange()[0] * ACCURACY / 0.2)\n", 82 | " ts = t.repeat(repeat=3, number=n) # best of 3\n", 83 | " ts = [((t / n) / problem_size) * units for t in ts] # per run\n", 84 | " return f\"{min(ts):8.2f} nsec ({n} runs at size {problem_size})\"\n", 85 | "\n", 86 | "\n", 87 | "# fmt: off\n", 88 | "print(\"GFloat scalar :\", time(lambda: slow_round_ndarray(format_info_ocp_e5m2, a[: N // 100]), N // 100))\n", 89 | "print(\"GFloat vectorized, numpy arrays:\", time(lambda: gfloat.round_ndarray(format_info_ocp_e5m2, a), N))\n", 90 | "print(\"GFloat vectorized, JAX JIT :\", time(lambda: jax_round_jit(ja), N))\n", 91 | "print(\"ML_dtypes :\", time(lambda: a.astype(ml_dtypes.float8_e5m2), N))" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "On one CPU platform the timings were:\n", 99 | "```\n", 100 | "GFloat scalar : 6996.75 nsec (50 runs at size 10000)\n", 101 | "GFloat vectorized, numpy arrays: 75.04 nsec (50 runs at size 1000000)\n", 102 | "GFloat vectorized, JAX JIT : 3.18 nsec (1000 runs at size 1000000)\n", 103 | "ML_dtypes : 3.13 nsec (1000 runs at size 1000000)\n", 104 | "```\n", 105 | "So the JAX JIT code is ~1000x faster than the scalar code, and comparable to `ml_dtypes`'s C++ CPU implementation." 106 | ] 107 | } 108 | ], 109 | "metadata": { 110 | "kernelspec": { 111 | "display_name": "gfloat", 112 | "language": "python", 113 | "name": "python3" 114 | }, 115 | "language_info": { 116 | "codemirror_mode": { 117 | "name": "ipython", 118 | "version": 3 119 | }, 120 | "file_extension": ".py", 121 | "mimetype": "text/x-python", 122 | "name": "python", 123 | "nbconvert_exporter": "python", 124 | "pygments_lexer": "ipython3", 125 | "version": "3.12.3" 126 | } 127 | }, 128 | "nbformat": 4, 129 | "nbformat_minor": 2 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # gfloat: Generic floating-point types in Python 4 | 5 | An implementation of generic floating point encode/decode logic, 6 | handling various current and proposed floating point types: 7 | 8 | - [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754): Binary16, Binary32 9 | - [OCP Float8](https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-06-20-pdf): E5M2, E4M3 10 | - [IEEE WG P3109](https://github.com/P3109/Public/blob/main/Shared%20Reports/IEEE%20WG%20P3109%20Interim%20Report.pdf): P3109_{K}p{P} for K > 2, and 1 <= P < K. 11 | - [OCP MX Formats](https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf): E2M1, M2M3, E3M2, E8M0, INT8, and the MX block formats. 12 | 13 | The library favours readability and extensibility over speed (although the *_ndarray functions are reasonably fast for large arrays, see the [benchmarking notebook](docs/source/04-benchmark.ipynb)). 14 | For other implementations of these datatypes more focused on speed see, for example, [ml_dtypes](https://github.com/jax-ml/ml_dtypes), 15 | [bitstring](https://github.com/scott-griffiths/bitstring), 16 | [MX PyTorch Emulation Library](https://github.com/microsoft/microxcaling). 17 | 18 | See https://gfloat.readthedocs.io for documentation, or dive into the notebooks to explore the formats. 19 | 20 | For example, here's a table from the [02-value-stats](docs/source/02-value-stats.ipynb) notebook: 21 | 22 | |name|B: Bits in the format|P: Precision in bits|E: Exponent field width in bits|Exact in float16?|Exact in float32?|0 bool: 12 | return v & 0x1 == 1 13 | 14 | 15 | def round_float( 16 | fi: FormatInfo, 17 | v: float, 18 | rnd: RoundMode = RoundMode.TiesToEven, 19 | sat: bool = False, 20 | srbits: int = -1, 21 | srnumbits: int = 0, 22 | ) -> float: 23 | """ 24 | Round input to the given :py:class:`FormatInfo`, given rounding mode and saturation flag 25 | 26 | An input NaN will convert to a NaN in the target. 27 | An input Infinity will convert to the largest float if :paramref:`sat`, 28 | otherwise to an Inf, if present, otherwise to a NaN. 29 | Negative zero will be returned if the format has negative zero, otherwise zero. 30 | 31 | Args: 32 | fi (FormatInfo): Describes the target format 33 | v (float): Input value to be rounded 34 | rnd (RoundMode): Rounding mode to use 35 | sat (bool): Saturation flag: if True, round overflowed values to `fi.max` 36 | srbits (int): Bits to use for stochastic rounding if rnd == Stochastic. 37 | srnumbits (int): How many bits are in srbits. Implies srbits < 2**srnumbits. 38 | 39 | Returns: 40 | A float which is one of the values in the format. 41 | 42 | Raises: 43 | ValueError: The target format cannot represent the input 44 | (e.g. converting a `NaN`, or an `Inf` when the target has no 45 | `NaN` or `Inf`, and :paramref:`sat` is false) 46 | ValueError: Inconsistent arguments, e.g. srnumbits >= 2**srnumbits 47 | """ 48 | 49 | # Constants 50 | p = fi.precision 51 | bias = fi.bias 52 | 53 | if rnd in (RoundMode.Stochastic, RoundMode.StochasticFast): 54 | if srbits >= 2**srnumbits: 55 | raise ValueError(f"srnumbits={srnumbits} >= 2**srnumbits={2**srnumbits}") 56 | 57 | if math.isnan(v): 58 | if fi.num_nans == 0: 59 | raise ValueError(f"No NaN in format {fi}") 60 | 61 | # Note that this does not preserve the NaN payload 62 | return np.nan 63 | 64 | # Extract sign 65 | sign = np.signbit([v]).item() and fi.is_signed 66 | vpos = -v if sign else v 67 | 68 | if math.isinf(vpos): 69 | result = np.inf 70 | 71 | elif vpos == 0: 72 | result = 0 73 | 74 | else: 75 | # Extract exponent 76 | expval = math.frexp(vpos)[1] - 1 77 | 78 | # Effective precision, accounting for right shift for subnormal values 79 | if fi.has_subnormals: 80 | expval = max(expval, 1 - bias) 81 | 82 | # Lift to "integer * 2^e" 83 | expval = expval - p + 1 84 | 85 | # use ldexp instead of vpos*2**-expval to avoid overflow 86 | fsignificand = math.ldexp(vpos, -expval) 87 | 88 | # Round 89 | isignificand = math.floor(fsignificand) 90 | delta = fsignificand - isignificand 91 | 92 | code_is_odd = ( 93 | _isodd(isignificand) 94 | if fi.precision > 1 95 | else (isignificand != 0 and _isodd(expval + bias)) 96 | ) 97 | 98 | match rnd: 99 | case RoundMode.TowardZero: 100 | should_round_away = False 101 | case RoundMode.TowardPositive: 102 | should_round_away = not sign and delta > 0 103 | case RoundMode.TowardNegative: 104 | should_round_away = sign and delta > 0 105 | case RoundMode.TiesToAway: 106 | should_round_away = delta + 0.5 >= 1.0 107 | case RoundMode.TiesToEven: 108 | should_round_away = delta > 0.5 or (delta == 0.5 and code_is_odd) 109 | case RoundMode.Stochastic: 110 | ## RTNE delta to srbits 111 | d = delta * 2.0**srnumbits 112 | floord = np.floor(d).astype(np.int64) 113 | d = floord + ( 114 | (d - floord > 0.5) or ((d - floord == 0.5) and _isodd(floord)) 115 | ) 116 | 117 | should_round_away = d + srbits >= 2.0**srnumbits 118 | case RoundMode.StochasticOdd: 119 | ## RTNE delta to srbits 120 | d = delta * 2.0**srnumbits 121 | floord = np.floor(d).astype(np.int64) 122 | d = floord + ( 123 | (d - floord > 0.5) or ((d - floord == 0.5) and not _isodd(floord)) 124 | ) 125 | 126 | should_round_away = d + srbits >= 2.0**srnumbits 127 | case RoundMode.StochasticFast: 128 | should_round_away = delta + (0.5 + srbits) * 2.0**-srnumbits >= 1.0 129 | case RoundMode.StochasticFastest: 130 | should_round_away = delta + srbits * 2.0**-srnumbits >= 1.0 131 | 132 | if should_round_away: 133 | # This may increase isignificand to 2**p, 134 | # which would require special casing in encode, 135 | # but not here, where we reconstruct a rounded value. 136 | isignificand += 1 137 | 138 | # Reconstruct rounded result to float 139 | result = isignificand * (2.0**expval) 140 | 141 | if result == 0: 142 | if sign and fi.has_nz: 143 | return -0.0 144 | else: 145 | return 0.0 146 | 147 | # Overflow 148 | amax = -fi.min if sign else fi.max 149 | if result > amax: 150 | if ( 151 | sat 152 | or (rnd == RoundMode.TowardNegative and not sign and np.isfinite(v)) 153 | or (rnd == RoundMode.TowardPositive and sign and np.isfinite(v)) 154 | or (rnd == RoundMode.TowardZero and np.isfinite(v)) 155 | ): 156 | result = amax 157 | else: 158 | if fi.domain == Domain.Extended: 159 | result = np.inf 160 | elif fi.num_nans > 0: 161 | result = np.nan 162 | else: 163 | raise ValueError(f"No Infs or NaNs in format {fi}, and sat=False") 164 | 165 | # Set sign 166 | if sign: 167 | result = -result 168 | 169 | return result 170 | -------------------------------------------------------------------------------- /src/gfloat/block.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | # Block floating point formats 4 | # https://en.wikipedia.org/wiki/Block_floating_point 5 | 6 | from dataclasses import dataclass 7 | from typing import Callable, Iterable 8 | 9 | import numpy as np 10 | import numpy.typing as npt 11 | 12 | import array_api_compat 13 | 14 | from .decode import decode_float 15 | from .round import RoundMode, round_float 16 | from .encode import encode_float 17 | from .types import FormatInfo 18 | 19 | 20 | @dataclass 21 | class BlockFormatInfo: 22 | 23 | #: Short name for the format, e.g. BlockFP8 24 | name: str 25 | 26 | #: Element data type 27 | etype: FormatInfo 28 | 29 | #: Scaling block size 30 | k: int 31 | 32 | #: Scale datatype 33 | stype: FormatInfo 34 | 35 | #: ## Derived values 36 | 37 | @property 38 | def element_bits(self) -> int: 39 | """The number of bits in each element, d""" 40 | return self.etype.k 41 | 42 | @property 43 | def scale_bits(self) -> int: 44 | """The number of bits in the scale, w""" 45 | return self.stype.k 46 | 47 | @property 48 | def block_size_bytes(self) -> int: 49 | """The number of bytes in a block""" 50 | bits = self.element_bits * self.k + self.scale_bits 51 | assert bits % 8 == 0 52 | return bits // 8 53 | 54 | @property 55 | def __name__(self) -> str: 56 | return self.name 57 | 58 | def __str__(self) -> str: 59 | return f"BlockFormatInfo:{self.name})" 60 | 61 | 62 | def decode_block(fi: BlockFormatInfo, block: Iterable[int]) -> Iterable[float]: 63 | """ 64 | Decode a :paramref:`block` of integer codepoints in Block Format :paramref:`fi` 65 | 66 | The scale is encoded in the first value of :paramref:`block`, 67 | with the remaining values encoding the block elements. 68 | 69 | The size of the iterable is not checked against the format descriptor. 70 | 71 | Args: 72 | fi (BlockFormatInfo): Describes the block format 73 | block (Iterable[int]): Input block 74 | 75 | Returns: 76 | A sequence of floats representing the encoded values. 77 | """ 78 | it = iter(block) 79 | 80 | scale_encoding = next(it) 81 | scale = decode_float(fi.stype, scale_encoding).fval 82 | 83 | for val_encoding in it: 84 | val = scale * decode_float(fi.etype, val_encoding).fval 85 | yield val 86 | 87 | # TODO: Assert length of block was k+1? Messy unless block is len()able 88 | 89 | 90 | def encode_block( 91 | fi: BlockFormatInfo, 92 | scale: float, 93 | vals: Iterable[float], 94 | round: RoundMode = RoundMode.TiesToEven, 95 | ) -> Iterable[int]: 96 | """ 97 | Encode float :paramref:`vals` into block Format described by :paramref:`fi` 98 | 99 | The :paramref:`scale` is explicitly passed, and the :paramref:`vals` are 100 | assumed to already be multiplied by `1/scale`. 101 | That is, this is pure encoding, scaling is computed and applied elsewhere 102 | (see e.g. :func:`quantize_block`). 103 | 104 | It is checked for overflow in the target format, 105 | and will raise an exception if it does. 106 | 107 | Args: 108 | fi (BlockFormatInfo): Describes the target block format 109 | scale (float): Scale to be recorded in the block 110 | vals (Iterable[float]): Input block 111 | round (RoundMode): Rounding mode to use, defaults to `TiesToEven` 112 | 113 | Returns: 114 | A sequence of ints representing the encoded values. 115 | 116 | Raises: 117 | ValueError: The scale overflows the target scale encoding format. 118 | """ 119 | 120 | if scale > fi.stype.max or scale < fi.stype.min: 121 | raise ValueError(f"Scaled {scale} out of range for {fi.stype}") 122 | 123 | sat = True # Saturate elements if out of range 124 | 125 | def enc(ty: FormatInfo, x: float) -> int: 126 | return encode_float(ty, round_float(ty, x, round, sat)) 127 | 128 | yield enc(fi.stype, scale) 129 | 130 | for val in vals: 131 | yield enc(fi.etype, val) 132 | 133 | 134 | ComputeScaleCallable = Callable[[float, npt.ArrayLike], float] 135 | 136 | 137 | def compute_scale_amax(emax: float, vals: npt.ArrayLike) -> float: 138 | """ 139 | Compute a scale factor such that :paramref:`vals` can be scaled to the 140 | range [0, 2**emax]. That is, `scale` is computed such that the largest 141 | exponent in the array `vals * scale` will be `emax`. 142 | 143 | The scale is clipped to the range 2**[-127, 127]. 144 | 145 | If all values are zero, any scale value smaller than emax would be accurate, 146 | but returning the smallest possible means that quick checks on the magnitude 147 | to identify near-zero blocks will also find the all-zero blocks. 148 | 149 | Args: 150 | emax (float): Maximum exponent to appear in `vals * scale` 151 | vals (ArrayLike): Input block 152 | 153 | Returns: 154 | A float such that `vals * scale` has exponents less than or equal to `emax`. 155 | 156 | Note: 157 | If all vals are zero, 1.0 is returned. 158 | """ 159 | xp = array_api_compat.array_namespace(vals) 160 | 161 | amax = xp.max(xp.abs(vals)) 162 | if amax == 0.0: 163 | q_log2scale = -127.0 164 | else: 165 | q_log2scale = xp.floor(xp.log2(amax)) - emax 166 | q_log2scale = xp.clip(q_log2scale, -127.0, 127.0) 167 | return 2.0**q_log2scale 168 | 169 | 170 | def quantize_block( 171 | fi: BlockFormatInfo, 172 | vals: npt.NDArray, 173 | compute_scale: ComputeScaleCallable, 174 | round: RoundMode = RoundMode.TiesToEven, 175 | ) -> npt.NDArray: 176 | """ 177 | Encode and decode a block of :paramref:`vals` of bytes into 178 | block format described by :paramref:`fi` 179 | 180 | Args: 181 | fi (BlockFormatInfo): Describes the target block format 182 | vals (numpy.array): Input block 183 | compute_scale ((float, ArrayLike) -> float): 184 | Callable to compute the scale, defaults to :func:`compute_scale_amax` 185 | round (RoundMode): Rounding mode to use, defaults to `TiesToEven` 186 | 187 | Returns: 188 | An array of floats representing the quantized values. 189 | 190 | Raises: 191 | ValueError: The scale overflows the target scale encoding format. 192 | """ 193 | 194 | q_scale = compute_scale(fi.etype.emax, vals) 195 | scaled_vals = vals / q_scale 196 | enc = encode_block(fi, q_scale, scaled_vals, round) 197 | return np.fromiter(decode_block(fi, enc), float) 198 | -------------------------------------------------------------------------------- /docs/source/formats.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | Defined Formats 4 | =============== 5 | 6 | .. module:: gfloat.formats 7 | 8 | Format parameters 9 | ----------------- 10 | 11 | This table (from example notebook :doc:`value-stats <02-value-stats>`) shows how 12 | gfloat has been used to tabulate properties of various floating point formats. 13 | 14 | - name: Format 15 | - B: Bits in the format 16 | - P: Precision in bits 17 | - E: Exponent field width in bits 18 | - smallest: Smallest positive value 19 | - smallest_normal: Smallest positive normal value, n/a if no finite values are normal 20 | - max: Largest finite value 21 | - num_nans: Number of NaN values 22 | - num_infs: Number of infinities (2 or 0) 23 | 24 | ========= === === === =========== ================= ============ =========== ====== 25 | name B P E smallest smallest_normal max num_nans infs 26 | ========= === === === =========== ================= ============ =========== ====== 27 | ocp_e2m1 4 2 2 0.5 1 6 0 0 28 | ocp_e2m3 6 4 2 0.125 1 7.5 0 0 29 | ocp_e3m2 6 3 3 0.0625 0.25 28 0 0 30 | ocp_e4m3 8 4 4 ≈0.0019531 0.015625 448 2 0 31 | ocp_e5m2 8 3 5 ≈1.5259e-05 ≈6.1035e-05 57344 6 2 32 | p3109_8p1 8 1 7 ≈2.1684e-19 ≈2.1684e-19 ≈9.2234e+18 1 2 33 | p3109_8p2 8 2 6 ≈2.3283e-10 ≈4.6566e-10 ≈2.1475e+09 1 2 34 | p3109_8p3 8 3 5 ≈7.6294e-06 ≈3.0518e-05 49152 1 2 35 | p3109_8p4 8 4 4 ≈0.00097656 0.0078125 224 1 2 36 | p3109_8p5 8 5 3 0.0078125 0.125 15 1 2 37 | p3109_8p6 8 6 2 0.015625 0.5 3.875 1 2 38 | binary16 16 11 5 ≈5.9605e-08 ≈6.1035e-05 65504 2046 2 39 | bfloat16 16 8 8 ≈9.1835e-41 ≈1.1755e-38 ≈3.3895e+38 254 2 40 | binary32 32 24 8 ≈1.4013e-45 ≈1.1755e-38 ≈3.4028e+38 ≈1.6777e+07 2 41 | binary64 64 53 11 4.9407e-324 ≈2.2251e-308 ≈1.7977e+308 ≈9.0072e+15 2 42 | ocp_e8m0 8 1 8 ≈5.8775e-39 ≈5.8775e-39 ≈1.7014e+38 1 0 43 | ocp_int8 8 8 0 0.015625 n/a ≈ 1.9844 0 0 44 | ========= === === === =========== ================= ============ =========== ====== 45 | 46 | In the above table, values which are not exact are indicated with the "≈" symbol. 47 | And here's the same table, but with values which don't render exactly as short floats 48 | printed as rationals times powers of 2: 49 | 50 | ========= === === === =========== ================= ======================================== ====================================== ====== 51 | name B P E smallest smallest_normal max num_nans infs 52 | ========= === === === =========== ================= ======================================== ====================================== ====== 53 | ocp_e2m1 4 2 2 0.5 1 6 0 0 54 | ocp_e2m3 6 4 2 0.125 1 7.5 0 0 55 | ocp_e3m2 6 3 3 0.0625 0.25 28 0 0 56 | ocp_e4m3 8 4 4 2^-9 0.015625 448 2 0 57 | ocp_e5m2 8 3 5 2^-16 2^-14 57344 6 2 58 | p3109_8p1 8 1 7 2^-62 2^-62 2^63 1 2 59 | p3109_8p2 8 2 6 2^-32 2^-31 2^31 1 2 60 | p3109_8p3 8 3 5 2^-17 2^-15 49152 1 2 61 | p3109_8p4 8 4 4 2^-10 0.0078125 224 1 2 62 | p3109_8p5 8 5 3 0.0078125 0.125 15 1 2 63 | p3109_8p6 8 6 2 0.015625 0.5 3.875 1 2 64 | binary16 16 11 5 2^-24 2^-14 65504 2046 2 65 | bfloat16 16 8 8 2^-133 2^-126 255/128*2^127 254 2 66 | binary32 32 24 8 2^-149 2^-126 16777215/8388608*2^127 8388607/4194304*2^23 2 67 | binary64 64 53 11 4.9407e-324 2^-1022 9007199254740991/9007199254740992*2^1024 4503599627370495/4503599627370496*2^53 2 68 | ocp_e8m0 8 1 8 2^-127 2^-127 2^127 1 0 69 | ocp_int8 8 8 0 0.015625 n/a 127/64*2^0 0 0 70 | ========= === === === =========== ================= ======================================== ====================================== ====== 71 | 72 | 73 | IEEE 754 Formats 74 | ---------------- 75 | 76 | .. autodata:: format_info_binary16 77 | .. autodata:: format_info_binary32 78 | .. autodata:: format_info_binary64 79 | 80 | BFloat16 81 | ---------------- 82 | 83 | .. autodata:: format_info_bfloat16 84 | 85 | Open Compute Platform (OCP) Formats 86 | ----------------------------------- 87 | 88 | .. autodata:: format_info_ocp_e5m2 89 | .. autodata:: format_info_ocp_e4m3 90 | .. autodata:: format_info_ocp_e3m2 91 | .. autodata:: format_info_ocp_e2m3 92 | .. autodata:: format_info_ocp_e2m1 93 | .. autodata:: format_info_ocp_e8m0 94 | .. autodata:: format_info_ocp_int8 95 | 96 | IEEE WG P3109 Formats 97 | --------------------- 98 | 99 | .. autofunction:: format_info_p3109 100 | 101 | Block Formats 102 | --------------------- 103 | 104 | .. autodata:: format_info_mxfp8_e5m2 105 | .. autodata:: format_info_mxfp8_e4m3 106 | .. autodata:: format_info_mxfp6_e3m2 107 | .. autodata:: format_info_mxfp6_e2m3 108 | .. autodata:: format_info_mxfp4_e2m1 109 | .. autodata:: format_info_mxint8 110 | -------------------------------------------------------------------------------- /src/gfloat/round_ndarray.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from typing import Optional, Tuple 4 | from .types import FormatInfo, RoundMode, Domain 5 | 6 | import numpy.typing as npt 7 | import array_api_compat 8 | 9 | 10 | def _isodd(v: npt.NDArray) -> npt.NDArray: 11 | return v & 0x1 == 1 12 | 13 | 14 | def _ldexp(v: npt.NDArray, s: npt.NDArray) -> npt.NDArray: 15 | xp = array_api_compat.array_namespace(v, s) 16 | if ( 17 | array_api_compat.is_torch_array(v) # type: ignore 18 | or array_api_compat.is_jax_array(v) # type: ignore 19 | or array_api_compat.is_numpy_array(v) 20 | ): 21 | return xp.ldexp(v, s) 22 | 23 | # Scale away from subnormal/infinite ranges 24 | offset = 24 25 | vlo = (v * 2.0**+offset) * 2.0 ** xp.astype(s - offset, v.dtype) 26 | vhi = (v * 2.0**-offset) * 2.0 ** xp.astype(s + offset, v.dtype) 27 | return xp.where(v < 1.0, vlo, vhi) 28 | 29 | 30 | def _frexp(v: npt.NDArray) -> Tuple[None, npt.NDArray]: 31 | xp = array_api_compat.array_namespace(v) 32 | if ( 33 | array_api_compat.is_torch_array(v) # type: ignore 34 | or array_api_compat.is_jax_array(v) # type: ignore 35 | or array_api_compat.is_numpy_array(v) 36 | ): 37 | return xp.frexp(v) 38 | 39 | # Beware #49 40 | expval = xp.astype(xp.floor(xp.log2(v)), xp.int64) 41 | return (None, expval) 42 | 43 | 44 | def round_ndarray( 45 | fi: FormatInfo, 46 | v: npt.NDArray, 47 | rnd: RoundMode = RoundMode.TiesToEven, 48 | sat: bool = False, 49 | srbits: Optional[npt.NDArray] = None, 50 | srnumbits: int = 0, 51 | ) -> npt.NDArray: 52 | """ 53 | Vectorized version of :meth:`round_float`. 54 | 55 | Round inputs to the given :py:class:`FormatInfo`, given rounding mode and 56 | saturation flag 57 | 58 | Input NaNs will convert to NaNs in the target, not necessarily preserving payload. 59 | An input Infinity will convert to the largest float if :paramref:`sat`, 60 | otherwise to an Inf, if present, otherwise to a NaN. 61 | Negative zero will be returned if the format has negative zero, otherwise zero. 62 | 63 | Args: 64 | fi (FormatInfo): Describes the target format 65 | v (float array): Input values to be rounded 66 | rnd (RoundMode): Rounding mode to use 67 | sat (bool): Saturation flag: if True, round overflowed values to `fi.max` 68 | srbits (int array): Bits to use for stochastic rounding if rnd == Stochastic. 69 | srnumbits (int): How many bits are in srbits. Implies srbits < 2**srnumbits. 70 | 71 | Returns: 72 | An array of floats which is a subset of the format's value set. 73 | 74 | Raises: 75 | ValueError: The target format cannot represent an input 76 | (e.g. converting a `NaN`, or an `Inf` when the target has no 77 | `NaN` or `Inf`, and :paramref:`sat` is false) 78 | """ 79 | xp = array_api_compat.array_namespace(v, srbits) 80 | 81 | # Until https://github.com/data-apis/array-api/issues/807 82 | xp_where = lambda a, t, f: xp.where(a, xp.asarray(t), xp.asarray(f)) 83 | xp_maximum = lambda a, b: xp.maximum(xp.asarray(a), xp.asarray(b)) 84 | 85 | p = fi.precision 86 | bias = fi.bias 87 | 88 | is_negative = xp.signbit(v) & fi.is_signed 89 | absv = xp_where(is_negative, -v, v) 90 | 91 | finite_nonzero = ~(xp.isnan(v) | xp.isinf(v) | (v == 0)) 92 | 93 | # Place 1.0 where finite_nonzero is False, to avoid log of {0,inf,nan} 94 | absv_masked = xp_where(finite_nonzero, absv, 1.0) 95 | 96 | int_type = xp.int64 if fi.k > 8 or srnumbits > 8 else xp.int16 97 | 98 | def to_int(x: npt.NDArray) -> npt.NDArray: 99 | return xp.astype(x, int_type) 100 | 101 | def to_float(x: npt.NDArray) -> npt.NDArray: 102 | return xp.astype(x, v.dtype) 103 | 104 | expval = _frexp(absv_masked)[1] - 1 105 | 106 | if fi.has_subnormals: 107 | expval = xp_maximum(expval, 1 - bias) 108 | 109 | expval = expval - p + 1 110 | fsignificand = _ldexp(absv_masked, -expval) 111 | 112 | floorfsignificand = xp.floor(fsignificand) 113 | isignificand = to_int(floorfsignificand) 114 | delta = fsignificand - floorfsignificand 115 | 116 | if fi.precision > 1: 117 | code_is_odd = _isodd(isignificand) 118 | else: 119 | code_is_odd = (isignificand != 0) & _isodd(expval + bias) 120 | 121 | match rnd: 122 | case RoundMode.TowardZero: 123 | should_round_away = xp.zeros_like(delta, dtype=xp.bool) 124 | 125 | case RoundMode.TowardPositive: 126 | should_round_away = ~is_negative & (delta > 0) 127 | 128 | case RoundMode.TowardNegative: 129 | should_round_away = is_negative & (delta > 0) 130 | 131 | case RoundMode.TiesToAway: 132 | should_round_away = delta >= 0.5 133 | 134 | case RoundMode.TiesToEven: 135 | should_round_away = (delta > 0.5) | ((delta == 0.5) & code_is_odd) 136 | 137 | case RoundMode.Stochastic: 138 | assert srbits is not None 139 | ## RTNE delta to srbits 140 | d = delta * 2.0 ** float(srnumbits) 141 | floord = to_int(xp.floor(d)) 142 | dd = d - xp.floor(d) 143 | should_round_away_tne = (dd > 0.5) | ((dd == 0.5) & _isodd(floord)) 144 | drnd = floord + xp.astype(should_round_away_tne, floord.dtype) 145 | 146 | should_round_away = drnd + srbits >= int(2.0 ** float(srnumbits)) 147 | 148 | case RoundMode.StochasticOdd: 149 | assert srbits is not None 150 | ## RTNO delta to srbits 151 | d = delta * 2.0 ** float(srnumbits) 152 | floord = to_int(xp.floor(d)) 153 | dd = d - xp.floor(d) 154 | should_round_away_tno = (dd > 0.5) | ((dd == 0.5) & ~_isodd(floord)) 155 | drnd = floord + xp.astype(should_round_away_tno, floord.dtype) 156 | 157 | should_round_away = drnd + srbits >= int(2.0 ** float(srnumbits)) 158 | 159 | case RoundMode.StochasticFast: 160 | assert srbits is not None 161 | should_round_away = ( 162 | delta + to_float(2 * srbits + 1) * 2.0 ** -float(1 + srnumbits) >= 1.0 163 | ) 164 | 165 | case RoundMode.StochasticFastest: 166 | assert srbits is not None 167 | should_round_away = delta + to_float(srbits) * 2.0**-srnumbits >= 1.0 168 | 169 | isignificand = xp_where(should_round_away, isignificand + 1, isignificand) 170 | 171 | fresult = _ldexp(to_float(isignificand), expval) 172 | 173 | result = xp_where(finite_nonzero, fresult, absv) 174 | 175 | amax = xp_where(is_negative, -fi.min, fi.max) 176 | 177 | if sat: 178 | result = xp_where(result > amax, amax, result) 179 | else: 180 | match rnd: 181 | case RoundMode.TowardNegative: 182 | put_amax_at = (result > amax) & ~is_negative 183 | case RoundMode.TowardPositive: 184 | put_amax_at = (result > amax) & is_negative 185 | case RoundMode.TowardZero: 186 | put_amax_at = result > amax 187 | case _: 188 | put_amax_at = xp.zeros_like(result, dtype=xp.bool) 189 | 190 | result = xp_where(finite_nonzero & put_amax_at, amax, result) 191 | 192 | # Now anything larger than amax goes to infinity or NaN 193 | if fi.domain == Domain.Extended: 194 | result = xp_where(result > amax, xp.inf, result) 195 | elif fi.num_nans > 0: 196 | result = xp_where(result > amax, xp.nan, result) 197 | else: 198 | if xp.any(result > amax): 199 | raise ValueError(f"No Infs or NaNs in format {fi}, and sat=False") 200 | 201 | result = xp_where(is_negative, -result, result) 202 | 203 | # Make negative zeros negative if has_nz, else make them not negative. 204 | if fi.has_nz: 205 | result = xp_where((result == 0) & is_negative, -0.0, result) 206 | else: 207 | result = xp_where(result == 0, 0.0, result) 208 | 209 | return result 210 | -------------------------------------------------------------------------------- /src/gfloat/formats.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from .block import BlockFormatInfo 4 | from .types import FormatInfo, Domain, Signedness 5 | 6 | import math 7 | 8 | #: FormatInfo for IEEE-754 Binary64 format 9 | format_info_binary64 = FormatInfo( 10 | name="binary64", 11 | k=64, 12 | precision=53, 13 | bias=2 ** (64 - 53 - 1) - 1, 14 | has_nz=True, 15 | domain=Domain.Extended, 16 | num_high_nans=2**52 - 1, 17 | has_subnormals=True, 18 | is_signed=True, 19 | is_twos_complement=False, 20 | ) 21 | 22 | #: FormatInfo for IEEE-754 Binary32 format 23 | format_info_binary32 = FormatInfo( 24 | name="binary32", 25 | k=32, 26 | precision=24, 27 | bias=2 ** (32 - 24 - 1) - 1, 28 | has_nz=True, 29 | domain=Domain.Extended, 30 | num_high_nans=2**23 - 1, 31 | has_subnormals=True, 32 | is_signed=True, 33 | is_twos_complement=False, 34 | ) 35 | 36 | #: FormatInfo for IEEE-754 Binary16 format 37 | format_info_binary16 = FormatInfo( 38 | name="binary16", 39 | k=16, 40 | precision=11, 41 | bias=2 ** (16 - 11 - 1) - 1, 42 | has_nz=True, 43 | domain=Domain.Extended, 44 | num_high_nans=2**10 - 1, 45 | has_subnormals=True, 46 | is_signed=True, 47 | is_twos_complement=False, 48 | ) 49 | 50 | #: FormatInfo for Google BFloat16 format 51 | format_info_bfloat16 = FormatInfo( 52 | name="bfloat16", 53 | k=16, 54 | precision=8, 55 | bias=2 ** (16 - 8 - 1) - 1, 56 | has_nz=True, 57 | domain=Domain.Extended, 58 | num_high_nans=2**7 - 1, 59 | has_subnormals=True, 60 | is_signed=True, 61 | is_twos_complement=False, 62 | ) 63 | 64 | #: FormatInfo for OCP E5M2 format 65 | format_info_ocp_e5m2 = FormatInfo( 66 | name="ocp_e5m2", 67 | k=8, 68 | precision=3, 69 | bias=2 ** (8 - 3 - 1) - 1, 70 | has_nz=True, 71 | domain=Domain.Extended, 72 | num_high_nans=2**2 - 1, 73 | has_subnormals=True, 74 | is_signed=True, 75 | is_twos_complement=False, 76 | ) 77 | 78 | #: FormatInfo for OCP E4M3 format 79 | format_info_ocp_e4m3 = FormatInfo( 80 | name="ocp_e4m3", 81 | k=8, 82 | precision=4, 83 | bias=2 ** (8 - 4 - 1) - 1, 84 | has_nz=True, 85 | domain=Domain.Finite, 86 | num_high_nans=1, 87 | has_subnormals=True, 88 | is_signed=True, 89 | is_twos_complement=False, 90 | ) 91 | 92 | #: FormatInfo for OCP MX E2M3 format 93 | format_info_ocp_e2m3 = FormatInfo( 94 | name="ocp_e2m3", 95 | k=6, 96 | precision=4, 97 | bias=2 ** (6 - 4 - 1) - 1, 98 | has_nz=True, 99 | domain=Domain.Finite, 100 | num_high_nans=0, 101 | has_subnormals=True, 102 | is_signed=True, 103 | is_twos_complement=False, 104 | ) 105 | 106 | #: FormatInfo for OCP MX E3M2 format 107 | format_info_ocp_e3m2 = FormatInfo( 108 | name="ocp_e3m2", 109 | k=6, 110 | precision=3, 111 | bias=2 ** (6 - 3 - 1) - 1, 112 | has_nz=True, 113 | domain=Domain.Finite, 114 | num_high_nans=0, 115 | has_subnormals=True, 116 | is_signed=True, 117 | is_twos_complement=False, 118 | ) 119 | 120 | #: FormatInfo for OCP MX E2M1 format 121 | format_info_ocp_e2m1 = FormatInfo( 122 | name="ocp_e2m1", 123 | k=4, 124 | precision=2, 125 | bias=2 ** (4 - 2 - 1) - 1, 126 | has_nz=True, 127 | domain=Domain.Finite, 128 | num_high_nans=0, 129 | has_subnormals=True, 130 | is_signed=True, 131 | is_twos_complement=False, 132 | ) 133 | 134 | #: FormatInfo for OCP MX E8M0 format 135 | format_info_ocp_e8m0 = FormatInfo( 136 | name="ocp_e8m0", 137 | k=8, 138 | precision=1, 139 | bias=2 ** (8 - 1) - 1, 140 | has_nz=False, 141 | domain=Domain.Finite, 142 | num_high_nans=1, 143 | has_subnormals=False, 144 | is_signed=False, 145 | is_twos_complement=False, 146 | ) 147 | 148 | #: FormatInfo for OCP MX INT8 format 149 | format_info_ocp_int8 = FormatInfo( 150 | name="ocp_int8", 151 | k=8, 152 | precision=8, 153 | bias=0, 154 | has_nz=False, 155 | domain=Domain.Finite, 156 | num_high_nans=0, 157 | has_subnormals=True, 158 | is_signed=True, 159 | is_twos_complement=True, 160 | ) 161 | 162 | 163 | def format_info_p3109( 164 | k: int, 165 | precision: int, 166 | signedness: Signedness = Signedness.Signed, 167 | domain: Domain = Domain.Extended, 168 | ) -> FormatInfo: 169 | """ 170 | FormatInfo for P3109 K{k} P{p} [su] [ef] formats 171 | 172 | Args: 173 | k (int): Format width in bits 174 | p (int): Precision in bits 175 | signedness (Signedness): Signed (default) or Unsigned 176 | domain (Domain): Extended (default) or finite 177 | 178 | Returns: 179 | FormatInfo class describing the format 180 | 181 | Raises: 182 | ValueError: If p is not in 1..k 183 | ValueError: If k is < 2 184 | """ 185 | if precision < 1 or precision > k: 186 | raise ValueError(f"P3109 format not defined for k={k}, p={precision}") 187 | 188 | if k < 2: 189 | raise ValueError(f"P3109 format not defined for k={k} < 2") 190 | is_signed = signedness == Signedness.Signed 191 | sstr = "s" if is_signed else "u" 192 | estr = "e" if domain == Domain.Extended else "f" 193 | name = f"p3109_k{k}p{precision}{sstr}{estr}" 194 | if is_signed: 195 | bias = math.floor(2 ** (k - precision - 1)) 196 | else: 197 | bias = 2 ** (k - precision) 198 | 199 | return FormatInfo( 200 | name, 201 | k=k, 202 | precision=precision, 203 | bias=bias, 204 | has_nz=False, 205 | domain=domain, 206 | num_high_nans=0 if is_signed else 1, 207 | has_subnormals=True, 208 | is_signed=is_signed, 209 | is_twos_complement=False, 210 | ) 211 | 212 | 213 | # Collections of formats 214 | _tiny_formats = [ 215 | format_info_p3109(3, 2, Signedness.Signed, Domain.Finite), 216 | format_info_ocp_e2m1, 217 | format_info_p3109(4, 2, Signedness.Signed, Domain.Finite), 218 | format_info_ocp_e2m3, 219 | format_info_ocp_e3m2, 220 | format_info_p3109(6, 3, Signedness.Signed, Domain.Finite), 221 | format_info_p3109(6, 4, Signedness.Signed, Domain.Finite), 222 | ] 223 | 224 | p3109_binary8_formats = ( 225 | [ 226 | format_info_p3109(8, 1, Signedness.Signed, Domain.Extended), 227 | format_info_p3109(8, 1, Signedness.Unsigned, Domain.Extended), 228 | ] 229 | + [ 230 | format_info_p3109(8, p, signedness, domain) 231 | for p in (3, 4) 232 | for signedness in (Signedness.Signed, Signedness.Unsigned) 233 | for domain in (Domain.Extended, Domain.Finite) 234 | ] 235 | + [ 236 | format_info_p3109(8, 7, Signedness.Signed, Domain.Finite), 237 | format_info_p3109(8, 8, Signedness.Unsigned, Domain.Finite), 238 | ] 239 | ) 240 | 241 | _fp8_formats = [ 242 | format_info_ocp_e4m3, 243 | format_info_ocp_e5m2, 244 | *p3109_binary8_formats, 245 | ] 246 | 247 | _fp16_formats = [ 248 | format_info_binary16, 249 | format_info_bfloat16, 250 | ] 251 | 252 | sample_formats = [ 253 | *_tiny_formats, 254 | *_fp8_formats, 255 | *_fp16_formats, 256 | format_info_binary32, 257 | format_info_binary64, 258 | format_info_ocp_e8m0, 259 | format_info_ocp_int8, 260 | ] 261 | 262 | # ------ 263 | # Block formats 264 | 265 | format_info_mxfp8_e5m2 = BlockFormatInfo( 266 | "mxfp8_e5m2", format_info_ocp_e5m2, 32, format_info_ocp_e8m0 267 | ) 268 | 269 | format_info_mxfp8_e4m3 = BlockFormatInfo( 270 | "mxfp8_e4m3", format_info_ocp_e4m3, 32, format_info_ocp_e8m0 271 | ) 272 | 273 | format_info_mxfp6_e3m2 = BlockFormatInfo( 274 | "mxfp6_e3m2", format_info_ocp_e3m2, 32, format_info_ocp_e8m0 275 | ) 276 | 277 | format_info_mxfp6_e2m3 = BlockFormatInfo( 278 | "mxfp6_e2m3", format_info_ocp_e2m3, 32, format_info_ocp_e8m0 279 | ) 280 | 281 | format_info_mxfp4_e2m1 = BlockFormatInfo( 282 | "mxfp4_e2m1", format_info_ocp_e2m1, 32, format_info_ocp_e8m0 283 | ) 284 | 285 | format_info_mxfp4_e2m1 = BlockFormatInfo( 286 | "mxfp4_e2m1", format_info_ocp_e2m1, 32, format_info_ocp_e8m0 287 | ) 288 | 289 | format_info_mxint8 = BlockFormatInfo( 290 | "mxint8", format_info_ocp_int8, 32, format_info_ocp_e8m0 291 | ) 292 | 293 | all_block_formats = [ 294 | format_info_mxfp8_e5m2, 295 | format_info_mxfp8_e4m3, 296 | format_info_mxfp6_e3m2, 297 | format_info_mxfp6_e2m3, 298 | format_info_mxfp4_e2m1, 299 | format_info_mxint8, 300 | ] 301 | -------------------------------------------------------------------------------- /test/test_decode.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | from typing import Callable 3 | import ml_dtypes 4 | import numpy as np 5 | import pytest 6 | 7 | from gfloat import FloatClass, Domain, decode_float, decode_ndarray 8 | from gfloat.formats import * 9 | 10 | 11 | def _isnegzero(x: float) -> bool: 12 | return (x == 0) and (np.signbit(x) == 1) 13 | 14 | 15 | methods = ["scalar", "array"] 16 | 17 | 18 | def decode_for_method(method: str, fi: FormatInfo) -> Callable: 19 | if method == "scalar": 20 | 21 | def dec(code: int) -> float: 22 | return decode_float(fi, code).fval 23 | 24 | if method == "array": 25 | 26 | def dec(code: int) -> float: 27 | asnp = np.tile(np.array(code, dtype=np.uint64), (2, 3)) 28 | vals = decode_ndarray(fi, asnp) 29 | val: float = vals.flatten()[0] 30 | np.testing.assert_equal(val, vals) 31 | return val 32 | 33 | return dec 34 | 35 | 36 | @pytest.mark.parametrize("method", methods) 37 | def test_spot_check_ocp_e5m2(method: str) -> None: 38 | fi = format_info_ocp_e5m2 39 | dec = decode_for_method(method, fi) 40 | fclass = lambda code: decode_float(fi, code).fclass 41 | assert dec(0x01) == 2.0**-16 42 | assert dec(0x40) == 2.0 43 | assert _isnegzero(dec(0x80)) 44 | assert dec(0x7B) == 57344.0 45 | assert dec(0x7C) == np.inf 46 | assert np.floor(np.log2(dec(0x7B))) == fi.emax 47 | assert dec(0xFC) == -np.inf 48 | assert np.isnan(dec(0x7F)) 49 | assert fclass(0x80) == FloatClass.ZERO 50 | assert fclass(0x00) == FloatClass.ZERO 51 | 52 | 53 | @pytest.mark.parametrize("method", methods) 54 | def test_spot_check_ocp_e4m3(method: str) -> None: 55 | fi = format_info_ocp_e4m3 56 | dec = decode_for_method(method, fi) 57 | assert dec(0x40) == 2.0 58 | assert dec(0x01) == 2.0**-9 59 | assert _isnegzero(dec(0x80)) 60 | assert np.isnan(dec(0x7F)) 61 | assert dec(0x7E) == 448.0 62 | assert np.floor(np.log2(dec(0x7E))) == fi.emax 63 | 64 | 65 | @pytest.mark.parametrize("method", methods) 66 | def test_spot_check_p3109_8p3(method: str) -> None: 67 | fi = format_info_p3109(8, 3) 68 | dec = decode_for_method(method, fi) 69 | 70 | assert dec(0x01) == 2.0**-17 71 | assert dec(0x40) == 1.0 72 | assert np.isnan(dec(0x80)) 73 | assert dec(0xFF) == -np.inf 74 | assert np.floor(np.log2(dec(0x7E))) == fi.emax 75 | 76 | 77 | @pytest.mark.parametrize("method", methods) 78 | def test_spot_check_p3109_8p1(method: str) -> None: 79 | fi = format_info_p3109(8, 1) 80 | dec = decode_for_method(method, fi) 81 | 82 | assert dec(0x01) == 2.0**-63 83 | assert dec(0x40) == 1.0 84 | assert np.isnan(dec(0x80)) 85 | assert dec(0xFF) == -np.inf 86 | assert np.floor(np.log2(dec(0x7E))) == fi.emax 87 | 88 | 89 | @pytest.mark.parametrize("method", methods) 90 | def test_spot_check_binary16(method: str) -> None: 91 | fi = format_info_binary16 92 | dec = decode_for_method(method, fi) 93 | 94 | assert dec(0x3C00) == 1.0 95 | assert dec(0x3C01) == 1.0 + 2**-10 96 | assert dec(0x4000) == 2.0 97 | assert dec(0x0001) == 2**-24 98 | assert dec(0x7BFF) == 65504.0 99 | assert np.isinf(dec(0x7C00)) 100 | assert np.isnan(dec(0x7C01)) 101 | assert np.isnan(dec(0x7FFF)) 102 | 103 | 104 | @pytest.mark.parametrize("method", methods) 105 | def test_spot_check_bfloat16(method: str) -> None: 106 | fi = format_info_bfloat16 107 | dec = decode_for_method(method, fi) 108 | 109 | assert dec(0x3F80) == 1 110 | assert dec(0x4000) == 2 111 | assert dec(0x0001) == 2**-133 112 | assert dec(0x4780) == 65536.0 113 | assert np.isinf(dec(0x7F80)) 114 | assert np.isnan(dec(0x7F81)) 115 | assert np.isnan(dec(0x7FFF)) 116 | 117 | 118 | @pytest.mark.parametrize("method", methods) 119 | def test_spot_check_ocp_e2m3(method: str) -> None: 120 | # Test against Table 4 in "OCP Microscaling Formats (MX) v1.0 Spec" 121 | fi = format_info_ocp_e2m3 122 | dec = decode_for_method(method, fi) 123 | 124 | assert fi.max == 7.5 125 | assert fi.smallest_subnormal == 0.125 126 | assert fi.smallest_normal == 1.0 127 | assert fi.domain == Domain.Finite 128 | assert fi.num_nans == 0 129 | assert fi.has_nz 130 | 131 | assert dec(0b000000) == 0 132 | assert dec(0b011111) == 7.5 133 | assert _isnegzero(dec(0b100000)) 134 | 135 | 136 | @pytest.mark.parametrize("method", methods) 137 | def test_spot_check_ocp_e3m2(method: str) -> None: 138 | # Test against Table 4 in "OCP Microscaling Formats (MX) v1.0 Spec" 139 | fi = format_info_ocp_e3m2 140 | dec = decode_for_method(method, fi) 141 | 142 | assert fi.max == 28.0 143 | assert fi.smallest_subnormal == 0.0625 144 | assert fi.smallest_normal == 0.25 145 | assert fi.domain == Domain.Finite 146 | assert fi.num_nans == 0 147 | assert fi.has_nz 148 | 149 | assert dec(0b000000) == 0 150 | assert dec(0b011111) == 28.0 151 | assert _isnegzero(dec(0b100000)) 152 | 153 | 154 | @pytest.mark.parametrize("method", methods) 155 | def test_spot_check_ocp_e2m1(method: str) -> None: 156 | # Test against Table 5 in "OCP Microscaling Formats (MX) v1.0 Spec" 157 | fi = format_info_ocp_e2m1 158 | dec = decode_for_method(method, fi) 159 | 160 | assert fi.max == 6.0 161 | assert fi.smallest_subnormal == 0.5 162 | assert fi.smallest_normal == 1.0 163 | assert fi.domain == Domain.Finite 164 | assert fi.num_nans == 0 165 | assert fi.has_nz 166 | 167 | assert dec(0b0000) == 0 168 | assert dec(0b0001) == 0.5 169 | assert dec(0b0010) == 1.0 170 | assert dec(0b0011) == 1.5 171 | assert dec(0b0100) == 2.0 172 | assert dec(0b0101) == 3.0 173 | assert dec(0b0110) == 4.0 174 | assert dec(0b0111) == 6.0 175 | assert _isnegzero(dec(0b1000)) 176 | 177 | 178 | @pytest.mark.parametrize("method", methods) 179 | def test_spot_check_ocp_e8m0(method: str) -> None: 180 | # Test against Table 7 in "OCP Microscaling Formats (MX) v1.0 Spec" 181 | fi = format_info_ocp_e8m0 182 | dec = decode_for_method(method, fi) 183 | fclass = lambda code: decode_float(fi, code).fclass 184 | assert fi.bias == 127 185 | assert fi.max == 2.0**127 186 | assert fi.smallest == 2.0**-127 187 | assert fi.domain == Domain.Finite 188 | assert fi.num_nans == 1 189 | 190 | assert dec(0x00) == 2.0**-127 191 | assert dec(0x01) == 2.0**-126 192 | assert dec(0x7F) == 1.0 193 | assert np.isnan(dec(0xFF)) 194 | assert fclass(0x80) == FloatClass.NORMAL 195 | assert fclass(0x00) == FloatClass.NORMAL 196 | 197 | 198 | @pytest.mark.parametrize("method", methods) 199 | def test_spot_check_ocp_int8(method: str) -> None: 200 | # Test against Table TODO in "OCP Microscaling Formats (MX) v1.0 Spec" 201 | fi = format_info_ocp_int8 202 | dec = decode_for_method(method, fi) 203 | 204 | assert fi.max == 1.0 + 63.0 / 64 205 | assert fi.smallest == 2.0**-6 206 | assert fi.domain == Domain.Finite 207 | assert fi.num_nans == 0 208 | 209 | assert dec(0x00) == 0.0 210 | assert dec(0x01) == fi.smallest 211 | assert dec(0x7F) == fi.max 212 | assert dec(0x80) == -2.0 213 | assert dec(0x80) == fi.min 214 | assert dec(0xFF) == -fi.smallest 215 | 216 | 217 | @pytest.mark.parametrize("fi", p3109_binary8_formats) 218 | def test_p3109_k8_specials(fi: FormatInfo) -> None: 219 | if fi.is_signed: 220 | assert fi.code_of_nan == 0x80 221 | assert fi.code_of_zero == 0x00 222 | if fi.domain == Domain.Extended: 223 | assert fi.code_of_posinf == 0x7F 224 | assert fi.code_of_neginf == 0xFF 225 | else: 226 | assert fi.code_of_nan == 0xFF 227 | assert fi.code_of_zero == 0x00 228 | if fi.domain == Domain.Extended: 229 | assert fi.code_of_posinf == 0xFE 230 | 231 | 232 | p3109_formats_to_test = ( 233 | (3, 1), 234 | (3, 2), 235 | (3, 3), 236 | (4, 1), 237 | (4, 2), 238 | (4, 3), 239 | (4, 4), 240 | (6, 1), 241 | (6, 5), 242 | (8, 3), 243 | (8, 1), 244 | (11, 3), 245 | ) 246 | 247 | 248 | @pytest.mark.parametrize("k,p", p3109_formats_to_test) 249 | def test_p3109_specials_signed(k: int, p: int) -> None: 250 | fi = format_info_p3109(k, p, Signedness.Signed, Domain.Extended) 251 | assert fi.code_of_nan == 2 ** (k - 1) 252 | assert fi.code_of_zero == 0 253 | assert fi.code_of_posinf == 2 ** (k - 1) - 1 254 | assert fi.code_of_neginf == 2**k - 1 255 | assert decode_float(fi, 2 ** (k - 2)).fval == 1.0 256 | 257 | fi = format_info_p3109(k, p, Signedness.Signed, Domain.Finite) 258 | assert fi.code_of_nan == 2 ** (k - 1) 259 | assert fi.code_of_zero == 0 260 | assert decode_float(fi, 2 ** (k - 2)).fval == 1.0 261 | with pytest.raises(ValueError): 262 | fi.code_of_posinf 263 | with pytest.raises(ValueError): 264 | fi.code_of_neginf 265 | 266 | 267 | @pytest.mark.parametrize("k,p", p3109_formats_to_test) 268 | def test_p3109_specials_unsigned(k: int, p: int) -> None: 269 | fi = format_info_p3109(k, p, Signedness.Unsigned, Domain.Extended) 270 | assert fi.code_of_nan == 2**k - 1 271 | assert fi.code_of_zero == 0 272 | assert fi.code_of_posinf == 2**k - 2 273 | assert decode_float(fi, 2 ** (k - 1)).fval == 1.0 274 | with pytest.raises(ValueError): 275 | fi.code_of_neginf 276 | 277 | 278 | @pytest.mark.parametrize("fi", sample_formats) 279 | @pytest.mark.parametrize("method", methods) 280 | def test_specials_decode(method: str, fi: FormatInfo) -> None: 281 | dec = decode_for_method(method, fi) 282 | 283 | if fi.has_zero: 284 | assert dec(fi.code_of_zero) == 0 285 | 286 | if fi.num_nans > 0: 287 | assert np.isnan(dec(fi.code_of_nan)) 288 | 289 | if fi.domain == Domain.Extended: 290 | assert dec(fi.code_of_posinf) == np.inf 291 | if fi.is_signed: 292 | assert dec(fi.code_of_neginf) == -np.inf 293 | 294 | assert dec(fi.code_of_max) == fi.max 295 | assert dec(fi.code_of_min) == fi.min 296 | 297 | if fi.has_zero: 298 | assert dec(1) == fi.smallest 299 | else: 300 | assert dec(0) == fi.smallest 301 | 302 | 303 | @pytest.mark.parametrize( 304 | "fmt,npfmt,int_dtype", 305 | [ 306 | (format_info_binary16, np.float16, np.uint16), 307 | (format_info_bfloat16, ml_dtypes.bfloat16, np.uint16), 308 | (format_info_ocp_e4m3, ml_dtypes.float8_e4m3fn, np.uint8), 309 | ], 310 | ) 311 | def test_consistent_decodes_all_values( 312 | fmt: FormatInfo, npfmt: np.dtype, int_dtype: np.dtype 313 | ) -> None: 314 | npivals = np.arange( 315 | np.iinfo(int_dtype).min, int(np.iinfo(int_dtype).max) + 1, dtype=int_dtype 316 | ) 317 | 318 | with np.errstate(invalid="ignore"): 319 | # Warning here when converting bfloat16 NaNs to float64 320 | npfvals = npivals.view(dtype=npfmt).astype(np.float64) 321 | 322 | # Scalar version 323 | for i, npfval in zip(npivals, npfvals): 324 | val = decode_float(fmt, int(i)) 325 | np.testing.assert_equal(val.fval, npfval) 326 | 327 | # Vector version 328 | vals = decode_ndarray(fmt, npivals) 329 | np.testing.assert_equal(vals, npfvals) 330 | 331 | 332 | @pytest.mark.parametrize("v", [-1, 0x10000]) 333 | def test_except(v: int) -> None: 334 | with pytest.raises(ValueError): 335 | decode_float(format_info_binary16, v) 336 | 337 | 338 | @pytest.mark.parametrize("fi", [fi for fi in sample_formats if fi.bits <= 8]) 339 | def test_dense(fi: FormatInfo) -> None: 340 | fvs = [decode_float(fi, i) for i in range(0, 2**fi.bits)] 341 | 342 | vals = np.array([fv.fval for fv in fvs]) 343 | 344 | assert np.min(vals[np.isfinite(vals)]) == fi.min 345 | assert np.max(vals[np.isfinite(vals)]) == fi.max 346 | assert np.min(vals[np.isfinite(vals) & (vals > 0)]) == fi.smallest 347 | 348 | if fi.has_subnormals: 349 | vals_subnormal = np.array( 350 | [fv.fval for fv in fvs if fv.fclass == FloatClass.SUBNORMAL and fv.fval > 0] 351 | ) 352 | if len(vals_subnormal): 353 | # In some formats, zero is the only "subnormal" 354 | assert np.min(vals_subnormal) == fi.smallest_subnormal 355 | -------------------------------------------------------------------------------- /src/gfloat/types.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from dataclasses import dataclass 4 | from enum import Enum 5 | import math 6 | 7 | 8 | class RoundMode(Enum): 9 | """ 10 | Enum for IEEE-754 rounding modes. 11 | 12 | Result :math:`r` is obtained from input :math:`v` depending on rounding mode as follows 13 | 14 | Notes on stochastic rounding: 15 | 16 | StochasticFast implements a stochastic rounding scheme that is unbiased in 17 | infinite precision, but biased when the quantity to be rounded is computed to 18 | a finite precision. 19 | 20 | StochasticFastest implements a stochastic rounding scheme that is biased 21 | (the rounded value is on average farther from zero than the true value). 22 | 23 | With a lot of SRbits (say 8 or more), these biases are negligible, and there 24 | may be some efficiency advantage in using StochasticFast or StochasticFastest. 25 | 26 | """ 27 | 28 | TowardZero = 1 #: Return the largest :math:`r` such that :math:`|r| \le |v|` 29 | TowardNegative = 2 #: Return the largest :math:`r` such that :math:`r \le v` 30 | TowardPositive = 3 #: Return the smallest :math:`r` such that :math:`r \ge v` 31 | TiesToEven = 4 #: Round to nearest, ties to even 32 | TiesToAway = 5 #: Round to nearest, ties away from zero 33 | Stochastic = 6 #: Stochastic rounding, RTNE before comparison 34 | StochasticOdd = 7 #: Stochastic rounding, RTNO before comparison 35 | StochasticFast = 8 #: Stochastic rounding - faster, but biased 36 | StochasticFastest = 9 #: Stochastic rounding - even faster, but more biased 37 | 38 | 39 | class Domain(Enum): 40 | """ 41 | Enum for domain of values 42 | """ 43 | 44 | Finite = 1 #: Finite values only 45 | Extended = 2 #: Finite values and infinities 46 | 47 | 48 | class Signedness(Enum): 49 | """ 50 | Enum for domain of values 51 | """ 52 | 53 | Signed = 1 #: Positive and negative values 54 | Unsigned = 2 #: Positive values only 55 | 56 | 57 | class FloatClass(Enum): 58 | """ 59 | Enum for the classification of a FloatValue. 60 | """ 61 | 62 | NORMAL = 1 #: A positive or negative normalized non-zero value 63 | SUBNORMAL = 2 #: A positive or negative subnormal value 64 | ZERO = 3 #: A positive or negative zero value 65 | INFINITE = 4 #: A positive or negative infinity (+/-Inf) 66 | NAN = 5 #: Not a Number (NaN) 67 | 68 | 69 | @dataclass 70 | class FloatValue: 71 | """ 72 | A floating-point value decoded in great detail. 73 | """ 74 | 75 | code: int #: Integer code point 76 | 77 | #: Value. Assumed to be exactly round-trippable to python float. 78 | #: This is true for all <64bit formats known in 2023. 79 | fval: float 80 | 81 | exp: int #: Raw exponent without bias 82 | expval: int #: Exponent, bias subtracted 83 | significand: int #: Significand as an integer 84 | fsignificand: float #: Significand as a float in the range [0,2) 85 | signbit: int #: Sign bit: 1 => negative, 0 => positive 86 | fclass: FloatClass #: See FloatClass 87 | 88 | 89 | @dataclass 90 | class FormatInfo: 91 | """ 92 | Class describing a floating-point format, parametrized 93 | by width, precision, and special value encoding rules. 94 | 95 | """ 96 | 97 | #: Short name for the format, e.g. binary32, bfloat16 98 | name: str 99 | 100 | #: Number of bits in the format 101 | k: int 102 | 103 | #: Number of significand bits (including implicit leading bit) 104 | precision: int 105 | 106 | #: Exponent bias 107 | bias: int 108 | 109 | #: Signedness: True if the format encodes negative numbers 110 | is_signed: bool 111 | 112 | #: Domain: Finite or Extended 113 | #: If Extended, the non-nan value with the highest encoding for each sign (s) 114 | #: is replaced by (s)Inf. 115 | domain: Domain 116 | 117 | #: Set if format encodes -0 at (sgn=1,exp=0,significand=0). 118 | #: If False, that encoding decodes to a NaN labelled NaN_0 119 | has_nz: bool 120 | 121 | #: Number of NaNs that are encoded in the highest encodings for each sign 122 | num_high_nans: int 123 | 124 | #: Set if format encodes subnormals 125 | has_subnormals: bool 126 | 127 | #: Set if the format uses two's complement encoding for the significand 128 | is_twos_complement: bool 129 | 130 | def __init__( 131 | self, 132 | name: str, 133 | k: int, 134 | precision: int, 135 | *, 136 | bias: int, 137 | is_signed: bool, 138 | domain: Domain, 139 | has_nz: bool, 140 | num_high_nans: int, 141 | has_subnormals: bool, 142 | is_twos_complement: bool, 143 | ): 144 | self.name = name 145 | self.k = k 146 | self.precision = precision 147 | self.bias = bias 148 | self.is_signed = is_signed 149 | self.domain = domain 150 | self.has_nz = has_nz 151 | self.num_high_nans = num_high_nans 152 | self.has_subnormals = has_subnormals 153 | self.is_twos_complement = is_twos_complement 154 | 155 | #: ## Derived values 156 | 157 | @property 158 | def tSignificandBits(self) -> int: 159 | """The number of trailing significand bits, t""" 160 | return self.precision - 1 161 | 162 | @property 163 | def expBits(self) -> int: 164 | """The number of exponent bits, w""" 165 | return self.k - self.tSignificandBits - self.signBits 166 | 167 | @property 168 | def signBits(self) -> int: 169 | """The number of sign bits, s""" 170 | return 1 if self.is_signed else 0 171 | 172 | @property 173 | def emax(self) -> int: 174 | """Return 175 | :math:`floor(log_2(maxFinite)) = emax` 176 | Note that for an all-subnormal format, this is not necessarily the 177 | largest value in the exponent field. 178 | """ 179 | return math.floor(math.log2(self.max)) 180 | 181 | # numpy finfo properties 182 | @property 183 | def bits(self) -> int: 184 | """ 185 | The number of bits occupied by the type. 186 | """ 187 | return self.k 188 | 189 | # @property 190 | # def dtype(self) -> np.dtype: 191 | # """ 192 | # Returns the dtype for which `finfo` returns information. For complex 193 | # input, the returned dtype is the associated ``float*`` dtype for its 194 | # real and complex components. 195 | # """ 196 | 197 | @property 198 | def eps(self) -> float: 199 | """ 200 | The difference between 1.0 and the smallest representable float 201 | larger than 1.0. For example, for 64-bit binary floats in the IEEE-754 202 | standard, ``eps = 2**-52``, approximately 2.22e-16. 203 | """ 204 | # TODO: Check if 1.0 is subnormal for any reasonable format, e.g. p3109(7)? 205 | return 2**self.machep 206 | 207 | @property 208 | def epsneg(self) -> float: 209 | """ 210 | The difference between 1.0 and the largest representable float 211 | less than 1.0. For example, for 64-bit binary floats in the IEEE-754 212 | standard, ``epsneg = 2**-53``, approximately 1.11e-16. 213 | """ 214 | return self.eps / 2 215 | 216 | @property 217 | def iexp(self) -> int: 218 | """ 219 | The number of bits in the exponent portion of the floating point 220 | representation. 221 | """ 222 | return self.expBits 223 | 224 | @property 225 | def machep(self) -> int: 226 | """ 227 | The exponent that yields `eps`. 228 | """ 229 | return -self.tSignificandBits 230 | 231 | @property 232 | def max(self) -> float: 233 | """ 234 | The largest representable number. 235 | """ 236 | num_non_finites = self.num_high_nans + self.num_posinfs 237 | if num_non_finites == 2**self.tSignificandBits: 238 | # All-bits-one exponent field is full, value is in the 239 | # binade below, so significand is 0xFFF..F 240 | isig = 2**self.tSignificandBits - 1 241 | emax = 2**self.expBits - 2 242 | elif num_non_finites == 2 ** (self.tSignificandBits + 1): 243 | # Top two binades are full, value is in the 244 | # binade below them. Significand is still 0xFFF..F 245 | isig = 2**self.tSignificandBits - 1 246 | emax = 2**self.expBits - 3 247 | else: 248 | assert num_non_finites < 2**self.tSignificandBits 249 | # All-bits-one exponent field is not full, value is in the 250 | # final binade, so significand is 0xFFF..F - num_non_finites 251 | isig = 2**self.tSignificandBits - 1 - num_non_finites 252 | emax = 2**self.expBits - 1 253 | 254 | if self.is_all_subnormal: 255 | return 2 ** (emax - self.bias) * (isig * 2 ** (1 - self.tSignificandBits)) 256 | else: 257 | return 2 ** (emax - self.bias) * (1.0 + isig * 2**-self.tSignificandBits) 258 | 259 | @property 260 | def maxexp(self) -> int: 261 | """ 262 | The smallest positive power of the base (2) that causes overflow. 263 | """ 264 | return self.emax + 1 265 | 266 | @property 267 | def min(self) -> float: 268 | """ 269 | The smallest representable number, typically ``-max``. 270 | """ 271 | if self.is_signed: 272 | if not self.is_twos_complement: 273 | return -self.max 274 | else: 275 | assert ( 276 | (self.domain == Domain.Finite) 277 | and (self.num_high_nans == 0) 278 | and not self.has_nz 279 | ) 280 | return -(2.0 ** (self.emax + 1)) 281 | else: 282 | # Unsigned 283 | if self.has_zero: 284 | return 0.0 285 | else: 286 | return 2**-self.bias 287 | 288 | @property 289 | def num_nans(self) -> int: 290 | """ 291 | The number of code points which decode to NaN 292 | """ 293 | if not self.is_signed: 294 | return self.num_high_nans 295 | 296 | # Signed 297 | if self.is_twos_complement: 298 | assert ( 299 | (self.domain == Domain.Finite) 300 | and (self.num_high_nans == 0) 301 | and not self.has_nz 302 | ) 303 | return 0 304 | 305 | return (0 if self.has_nz else 1) + 2 * self.num_high_nans 306 | 307 | @property 308 | def code_of_nan(self) -> int: 309 | """ 310 | Return a codepoint for a NaN 311 | """ 312 | if self.num_high_nans > 0: 313 | return 2 ** (self.k) - 1 314 | if not self.has_nz: 315 | return 2 ** (self.k - 1) 316 | raise ValueError(f"No NaN in {self}") 317 | 318 | @property 319 | def code_of_posinf(self) -> int: 320 | """ 321 | Return a codepoint for positive infinity 322 | """ 323 | if self.domain != Domain.Extended: 324 | raise ValueError(f"No Inf in {self}") 325 | 326 | return 2 ** (self.k - self.signBits) - 1 - self.num_high_nans 327 | 328 | @property 329 | def code_of_neginf(self) -> int: 330 | """ 331 | Return a codepoint for negative infinity 332 | """ 333 | if not (self.domain == Domain.Extended and self.is_signed): 334 | raise ValueError(f"No -Inf in {self}") 335 | 336 | return 2**self.k - 1 - self.num_high_nans 337 | 338 | @property 339 | def code_of_zero(self) -> int: 340 | """ 341 | Return a codepoint for (non-negative) zero 342 | """ 343 | assert self.has_zero 344 | return 0 345 | 346 | @property 347 | def has_zero(self) -> bool: 348 | """ 349 | Does the format have zero? 350 | 351 | This is false if the mantissa is 0 width and we don't have subnormals - 352 | essentially the mantissa is always decoded as 1. 353 | If we have subnormals, the only subnormal is zero, and the mantissa is 354 | always decoded as 0. 355 | """ 356 | return self.precision > 1 or self.has_subnormals 357 | 358 | @property 359 | def code_of_negzero(self) -> int: 360 | """ 361 | Return a codepoint for negative zero 362 | """ 363 | if not self.has_nz: 364 | raise ValueError(f"No negative zero in {self}") 365 | 366 | return 2 ** (self.k - 1) 367 | 368 | @property 369 | def num_posinfs(self) -> int: 370 | """ 371 | Return the number of positive infinities 372 | """ 373 | return 1 if self.domain == Domain.Extended else 0 374 | 375 | @property 376 | def num_neginfs(self) -> int: 377 | """ 378 | Return the number of negative infinities 379 | """ 380 | return 1 if self.domain == Domain.Extended and self.is_signed else 0 381 | 382 | @property 383 | def num_infs(self) -> int: 384 | """ 385 | Return the number of infinities 386 | """ 387 | return self.num_posinfs + self.num_neginfs 388 | 389 | @property 390 | def code_of_max(self) -> int: 391 | """ 392 | Return a codepoint for fi.max 393 | """ 394 | return 2 ** (self.k - self.signBits) - 1 - self.num_high_nans - self.num_posinfs 395 | 396 | @property 397 | def code_of_min(self) -> int: 398 | """ 399 | Return a codepoint for fi.min 400 | """ 401 | if self.is_signed and not self.is_twos_complement: 402 | return 2**self.k - self.num_high_nans - self.num_posinfs - 1 403 | elif self.is_signed and self.is_twos_complement: 404 | return 2 ** (self.k - 1) 405 | else: 406 | return 0 # codepoint of smallest value, whether 0 or 2^-bias 407 | 408 | # @property 409 | # def minexp(self) -> int: 410 | # """ 411 | # The most negative power of the base (2) consistent with there 412 | # being no leading 0's in the mantissa. 413 | # """ 414 | 415 | # @property 416 | # def negep(self) -> int: 417 | # """ 418 | # The exponent that yields `epsneg`. 419 | # """ 420 | 421 | # @property 422 | # def nexp(self) -> int: 423 | # """ 424 | # The number of bits in the exponent including its sign and bias. 425 | # """ 426 | 427 | # @property 428 | # def nmant(self) -> int: 429 | # """ 430 | # The number of bits in the mantissa. 431 | # """ 432 | 433 | # @property 434 | # def precision(self) -> int: 435 | # """ 436 | # The approximate number of decimal digits to which this kind of 437 | # float is precise. 438 | # """ 439 | 440 | # @property 441 | # def resolution(self) -> float: 442 | # """ 443 | # The approximate decimal resolution of this type, i.e., 444 | # ``10**-precision``. 445 | # """ 446 | 447 | # @property 448 | # def tiny(self) -> float: 449 | # """ 450 | # An alias for `smallest_normal`, kept for backwards compatibility. 451 | # """ 452 | 453 | @property 454 | def smallest_normal(self) -> float: 455 | """ 456 | The smallest positive floating point number with 1 as leading bit in 457 | the significand following IEEE-754. 458 | """ 459 | if self.has_subnormals: 460 | return 2 ** (1 - self.bias) 461 | elif self.has_zero: 462 | return 2**-self.bias + 2 ** (-self.bias - self.tSignificandBits) 463 | else: 464 | return 2**-self.bias 465 | 466 | @property 467 | def smallest_subnormal(self) -> float: 468 | """ 469 | The smallest positive floating point number with 0 as leading bit in 470 | the significand following IEEE-754. 471 | """ 472 | assert self.has_subnormals, "not implemented" 473 | return 2 ** -(self.bias + self.tSignificandBits - 1) 474 | 475 | @property 476 | def smallest(self) -> float: 477 | """ 478 | The smallest positive floating point number. 479 | """ 480 | if self.has_subnormals: 481 | return self.smallest_subnormal 482 | else: 483 | return self.smallest_normal 484 | 485 | @property 486 | def is_all_subnormal(self) -> bool: 487 | """ 488 | Are all encoded values subnormal? 489 | """ 490 | return (self.expBits == 0) and self.has_subnormals 491 | 492 | @property 493 | def __name__(self) -> str: 494 | return self.name 495 | 496 | def __str__(self) -> str: 497 | return f"{self.name}" 498 | -------------------------------------------------------------------------------- /test/test_round.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from typing import Type, Callable, Iterator, Tuple 4 | 5 | import ml_dtypes 6 | import numpy as np 7 | import pytest 8 | 9 | from gfloat import RoundMode, decode_float, decode_ndarray, round_float, round_ndarray 10 | from gfloat.formats import * 11 | 12 | 13 | def rnd_scalar( 14 | fi: FormatInfo, v: float, mode: RoundMode = RoundMode.TiesToEven, sat: bool = False 15 | ) -> float: 16 | return round_float(fi, v, mode, sat) 17 | 18 | 19 | def rnd_array( 20 | fi: FormatInfo, v: float, mode: RoundMode = RoundMode.TiesToEven, sat: bool = False 21 | ) -> float: 22 | a = round_ndarray(fi, np.asarray([v]), mode, sat) 23 | return float(a[0]) 24 | 25 | 26 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 27 | def test_round_p3109(round_float: Callable) -> None: 28 | fi = format_info_p3109(8, 4) 29 | assert round_float(fi, 0.0068359375) == 0.0068359375 30 | assert round_float(fi, 0.0029296875) == 0.0029296875 31 | assert round_float(fi, 0.0078125) == 0.0078125 32 | assert round_float(fi, 0.017578125) == 0.017578125 33 | assert round_float(fi, 224.0) == 224.0 34 | assert round_float(fi, 240.0) == np.inf 35 | 36 | assert round_float(fi, 224.1, RoundMode.TowardPositive) == np.inf 37 | 38 | assert round_float(fi, 232.0) == 224.0 39 | assert round_float(fi, 232.0, RoundMode.TiesToAway) == np.inf 40 | assert round_float(fi, 232.0, RoundMode.TowardZero) == 224.0 41 | assert round_float(fi, 232.0, RoundMode.TowardNegative) == 224.0 42 | assert round_float(fi, 232.0, RoundMode.TowardPositive) == np.inf 43 | 44 | assert round_float(fi, -232.0) == -224.0 45 | assert round_float(fi, -232.0, RoundMode.TiesToAway) == -np.inf 46 | assert round_float(fi, -232.0, RoundMode.TowardZero) == -224.0 47 | assert round_float(fi, -232.0, RoundMode.TowardNegative) == -np.inf 48 | assert round_float(fi, -232.0, RoundMode.TowardPositive) == -224.0 49 | 50 | assert round_float(fi, 232.1) == np.inf 51 | 52 | 53 | p4min = 2**-10 # smallest subnormal in 8p4 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "mode, vals", 58 | ( 59 | ( 60 | RoundMode.TowardZero, 61 | ( 62 | (p4min, p4min), 63 | (p4min / 4, 0.0), 64 | (p4min / 2, 0.0), 65 | (-p4min, -p4min), 66 | (-p4min / 4, 0.0), 67 | (-p4min / 2, 0.0), 68 | (64.0, 64.0), 69 | (63.0, 60.0), 70 | (62.0, 60.0), 71 | (-64.0, -64.0), 72 | (-63.0, -60.0), 73 | (-62.0, -60.0), 74 | ), 75 | ), 76 | ( 77 | RoundMode.TowardPositive, 78 | ( 79 | (p4min, p4min), 80 | (p4min / 4, p4min), 81 | (p4min / 2, p4min), 82 | (-p4min, -p4min), 83 | (-p4min / 4, 0.0), 84 | (-p4min / 2, 0.0), 85 | (64.0, 64.0), 86 | (63.0, 64.0), 87 | (62.0, 64.0), 88 | (-64.0, -64.0), 89 | (-63.0, -60.0), 90 | (-62.0, -60.0), 91 | ), 92 | ), 93 | ( 94 | RoundMode.TowardNegative, 95 | ( 96 | (p4min, p4min), 97 | (p4min / 4, 0.0), 98 | (p4min / 2, 0.0), 99 | (-p4min, -p4min), 100 | (-p4min / 4, -p4min), 101 | (-p4min / 2, -p4min), 102 | (64.0, 64.0), 103 | (63.0, 60.0), 104 | (62.0, 60.0), 105 | (-64.0, -64.0), 106 | (-63.0, -64.0), 107 | (-62.0, -64.0), 108 | ), 109 | ), 110 | ( 111 | RoundMode.TiesToEven, 112 | ( 113 | (p4min, p4min), 114 | (p4min / 4, 0.0), 115 | (p4min / 2, 0.0), 116 | (-p4min, -p4min), 117 | (-p4min / 4, 0.0), 118 | (-p4min / 2, 0.0), 119 | (64.0, 64.0), 120 | (63.0, 64.0), 121 | (62.0, 64.0), 122 | (61.0, 60.0), 123 | (-64.0, -64.0), 124 | (-63.0, -64.0), 125 | (-62.0, -64.0), 126 | (-61.0, -60.0), 127 | (-58.0, -56.0), 128 | ), 129 | ), 130 | ( 131 | RoundMode.TiesToAway, 132 | ( 133 | (p4min, p4min), 134 | (p4min / 4, 0.0), 135 | (p4min / 2, p4min), 136 | (-p4min, -p4min), 137 | (-p4min / 4, 0.0), 138 | (-p4min / 2, -p4min), 139 | (64.0, 64.0), 140 | (63.0, 64.0), 141 | (62.0, 64.0), 142 | (61.0, 60.0), 143 | (-64.0, -64.0), 144 | (-63.0, -64.0), 145 | (-62.0, -64.0), 146 | (-61.0, -60.0), 147 | (-58.0, -60.0), 148 | ), 149 | ), 150 | ), 151 | ) 152 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 153 | def test_round_p3109b(round_float: Callable, mode: RoundMode, vals: list) -> None: 154 | fi = format_info_p3109(8, 4) 155 | 156 | for sat in (True, False): 157 | for val, expected in vals: 158 | assert round_float(fi, val, mode, sat) == expected 159 | 160 | 161 | p4max = 224.0 162 | p4maxup = 240.0 163 | p4maxhalfup = (p4max + p4maxup) / 2 164 | 165 | 166 | @pytest.mark.parametrize( 167 | "modesat, vals", 168 | ( 169 | ( 170 | (RoundMode.TowardZero, True), 171 | ( 172 | (p4max, p4max), 173 | (p4maxhalfup, p4max), 174 | (p4maxup, p4max), 175 | (np.inf, p4max), 176 | (-p4max, -p4max), 177 | (-p4maxhalfup, -p4max), 178 | (-p4maxup, -p4max), 179 | (-np.inf, -p4max), 180 | ), 181 | ), 182 | ( 183 | (RoundMode.TowardZero, False), 184 | ( 185 | (p4max, p4max), 186 | (p4maxhalfup, p4max), 187 | (p4maxup, p4max), 188 | (np.inf, np.inf), 189 | (-p4max, -p4max), 190 | (-p4maxhalfup, -p4max), 191 | (-p4maxup, -p4max), 192 | (-np.inf, -np.inf), 193 | ), 194 | ), 195 | ( 196 | (RoundMode.TowardPositive, True), 197 | ( 198 | (p4max, p4max), 199 | (p4maxhalfup, p4max), 200 | (p4maxup, p4max), 201 | (np.inf, p4max), 202 | (-p4max, -p4max), 203 | (-p4maxhalfup, -p4max), 204 | (-p4maxup, -p4max), 205 | (-np.inf, -p4max), 206 | ), 207 | ), 208 | ( 209 | (RoundMode.TowardPositive, False), 210 | ( 211 | (p4max, p4max), 212 | (p4maxhalfup, np.inf), 213 | (p4maxup, np.inf), 214 | (np.inf, np.inf), 215 | (-p4max, -p4max), 216 | (-p4maxhalfup, -p4max), 217 | (-p4maxup, -p4max), 218 | (-np.inf, -np.inf), 219 | ), 220 | ), 221 | ( 222 | (RoundMode.TowardNegative, True), 223 | ( 224 | (p4max, p4max), 225 | (p4maxhalfup, p4max), 226 | (p4maxup, p4max), 227 | (np.inf, p4max), 228 | (-p4max, -p4max), 229 | (-p4maxhalfup, -p4max), 230 | (-p4maxup, -p4max), 231 | (-np.inf, -p4max), 232 | ), 233 | ), 234 | ( 235 | (RoundMode.TowardNegative, False), 236 | ( 237 | (p4max, p4max), 238 | (p4maxhalfup, p4max), 239 | (p4maxup, p4max), 240 | (np.inf, np.inf), 241 | (-p4max, -p4max), 242 | (-p4maxhalfup, -np.inf), 243 | (-p4maxup, -np.inf), 244 | (-np.inf, -np.inf), 245 | ), 246 | ), 247 | ( 248 | (RoundMode.TiesToEven, True), 249 | ( 250 | (p4max, p4max), 251 | (p4maxhalfup, p4max), 252 | (p4maxup, p4max), 253 | (np.inf, p4max), 254 | (-p4max, -p4max), 255 | (-p4maxhalfup, -p4max), 256 | (-p4maxup, -p4max), 257 | (-np.inf, -p4max), 258 | ), 259 | ), 260 | ( 261 | (RoundMode.TiesToEven, False), 262 | ( 263 | (p4max, p4max), 264 | (p4maxhalfup, p4max), 265 | (p4maxup, np.inf), 266 | (np.inf, np.inf), 267 | (-p4max, -p4max), 268 | (-p4maxhalfup, -p4max), 269 | (-p4maxup, -np.inf), 270 | (-np.inf, -np.inf), 271 | ), 272 | ), 273 | ( 274 | (RoundMode.TiesToAway, True), 275 | ( 276 | (p4max, p4max), 277 | (p4maxhalfup, p4max), 278 | (p4maxup, p4max), 279 | (np.inf, p4max), 280 | (-p4max, -p4max), 281 | (-p4maxhalfup, -p4max), 282 | (-p4maxup, -p4max), 283 | (-np.inf, -p4max), 284 | ), 285 | ), 286 | ( 287 | (RoundMode.TiesToAway, False), 288 | ( 289 | (p4max, p4max), 290 | (p4maxhalfup, np.inf), 291 | (p4maxup, np.inf), 292 | (np.inf, np.inf), 293 | (-p4max, -p4max), 294 | (-p4maxhalfup, -np.inf), 295 | (-p4maxup, -np.inf), 296 | (-np.inf, -np.inf), 297 | ), 298 | ), 299 | ), 300 | ids=lambda x: f"{str(x[0])}-{'Sat' if x[1] else 'Inf'}" if len(x) == 2 else None, 301 | ) 302 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 303 | def test_round_p3109_sat( 304 | round_float: Callable, modesat: tuple[RoundMode, bool], vals: list 305 | ) -> None: 306 | fi = format_info_p3109(8, 4) 307 | 308 | for val, expected in vals: 309 | assert round_float(fi, val, *modesat) == expected 310 | 311 | 312 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 313 | def test_round_e5m2(round_float: Callable) -> None: 314 | fi = format_info_ocp_e5m2 315 | 316 | assert fi.max == 57344 317 | 318 | assert round_float(fi, 1.5258789e-05) == 2**-16 319 | 320 | # Default NONSAT rounding 321 | assert round_float(fi, 57344.0) == 57344 322 | assert round_float(fi, 57344.1) == 57344 323 | assert round_float(fi, 61439.9) == 57344 324 | assert round_float(fi, 61440.0) == np.inf 325 | assert round_float(fi, np.inf, sat=False) == np.inf 326 | assert round_float(fi, -np.inf, sat=False) == -np.inf 327 | assert np.isnan(round_float(fi, np.nan, sat=False)) 328 | 329 | # SAT rounding 330 | assert round_float(fi, 57344.0, sat=True) == 57344 331 | assert round_float(fi, 57344.1, sat=True) == 57344 332 | assert round_float(fi, 61439.9, sat=True) == 57344 333 | assert round_float(fi, 61440.0, sat=True) == 57344 334 | assert round_float(fi, np.inf, sat=True) == 57344 335 | assert round_float(fi, -np.inf, sat=True) == -57344 336 | assert np.isnan(round_float(fi, np.nan, sat=True)) 337 | 338 | 339 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 340 | def test_round_e4m3(round_float: Callable) -> None: 341 | fi = format_info_ocp_e4m3 342 | 343 | assert fi.max == 448 344 | 345 | # Default NONSAT rounding 346 | assert round_float(fi, 448.0) == 448 347 | assert round_float(fi, 448.1) == 448 348 | assert round_float(fi, 464.0) == 448 349 | assert np.isnan(round_float(fi, 464.01)) 350 | assert np.isnan(round_float(fi, np.inf, sat=False)) 351 | assert np.isnan(round_float(fi, -np.inf, sat=False)) 352 | assert np.isnan(round_float(fi, np.nan, sat=False)) 353 | 354 | # SAT rounding 355 | assert round_float(fi, 448.0, sat=True) == 448 356 | assert round_float(fi, 448.1, sat=True) == 448 357 | assert round_float(fi, 464.0, sat=True) == 448 358 | assert round_float(fi, 464.01, sat=True) == 448 359 | assert round_float(fi, np.inf, sat=True) == 448 360 | assert round_float(fi, -np.inf, sat=True) == -448 361 | assert np.isnan(round_float(fi, np.nan, sat=True)) 362 | 363 | 364 | some_positive_codepoints = ( 365 | 0x00, 366 | 0x01, 367 | 0x02, 368 | 0x03, 369 | 0x07, 370 | 0x0F, 371 | 0x17, 372 | 0x21, 373 | 0x33, 374 | 0x40, 375 | 0x53, 376 | 0x65, 377 | 0x70, 378 | ) 379 | 380 | 381 | @pytest.mark.parametrize( 382 | "fi", 383 | [ 384 | format_info_ocp_e5m2, 385 | format_info_ocp_e4m3, 386 | *p3109_binary8_formats, 387 | ], 388 | ) 389 | def test_round(fi: FormatInfo) -> None: 390 | """ 391 | Test rounding from values between exact binary8 values 392 | For integer code point i, let 393 | v0 = the float value at i 394 | v1 = the float value at i+1, i.e. nextUp(v0) 395 | dv = v1 - v0 396 | Then check that: 397 | round(v0) == v0 398 | round(v0 + 0.3*dv) == v0 399 | round(v0 + 0.6*dv) == v1 400 | """ 401 | 402 | def get_vals() -> Iterator[Tuple[float, float]]: 403 | for i in some_positive_codepoints: 404 | v0 = decode_float(fi, i + 0).fval 405 | v1 = decode_float(fi, i + 1).fval 406 | if np.isfinite([v0, v1]).all(): 407 | dv = v1 - v0 408 | nearest_even = v0 if (i & 1 == 0) else v1 409 | yield v0, v0 410 | yield v0 + 0.3 * dv, v0 411 | yield v0 + 0.49 * dv, v0 412 | yield v0 + 0.51 * dv, v1 413 | yield v0 + 0.99 * dv, v1 414 | yield v0 + 0.50 * dv, nearest_even 415 | 416 | for v, expected in get_vals(): 417 | assert round_float(fi, v) == expected 418 | 419 | vs = np.array([v for v, _ in get_vals()]) 420 | expecteds = np.array([expected for _, expected in get_vals()]) 421 | 422 | got = round_ndarray(fi, vs) 423 | np.testing.assert_equal(got, expecteds) 424 | 425 | 426 | test_formats = [ 427 | (format_info_ocp_e5m2, ml_dtypes.float8_e5m2), 428 | (format_info_ocp_e4m3, ml_dtypes.float8_e4m3fn), 429 | ] 430 | 431 | 432 | def _linterp(a, b, t): # type: ignore[no-untyped-def] 433 | return a * (1 - t) + b * t 434 | 435 | 436 | def _mlround(v: float, dty: Type) -> float: 437 | """ 438 | Round `v` using ml_dtypes library 439 | """ 440 | return np.array([v]).astype(dty).astype(float).item() 441 | 442 | 443 | @pytest.mark.parametrize("fi,mldtype", test_formats) 444 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 445 | def test_ml_dtype_compatible( 446 | round_float: Callable, fi: FormatInfo, mldtype: Type 447 | ) -> None: 448 | """ 449 | Test that rounding is compatible with ml_dtypes 450 | """ 451 | for i in range(255): 452 | # For each float v, check values at various interpolations 453 | # between v and nextUp(v) 454 | v0 = decode_float(fi, i + 0).fval 455 | v1 = decode_float(fi, i + 1).fval 456 | 457 | for alpha in (0, 0.3, 0.5, 0.6, 0.9, 1.25): 458 | v = _linterp(v0, v1, alpha) 459 | if np.isfinite(v): 460 | val = round_float(fi, v, RoundMode.TiesToEven) 461 | 462 | mlval = _mlround(v, mldtype) 463 | np.testing.assert_equal(val, mlval) 464 | 465 | 466 | @pytest.mark.parametrize("fi,mldtype", test_formats) 467 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 468 | def test_round_ints(round_float: Callable, fi: FormatInfo, mldtype: Type) -> None: 469 | for v in np.arange(289).astype(float): 470 | val = round_float(fi, v) 471 | 472 | mlval = _mlround(v, mldtype) 473 | np.testing.assert_equal(val, mlval) 474 | 475 | 476 | @pytest.mark.parametrize("fi", sample_formats) 477 | @pytest.mark.parametrize("round_float", (rnd_scalar, rnd_array)) 478 | def test_round_roundtrip(round_float: Callable, fi: FormatInfo) -> None: 479 | if fi.bits <= 8: 480 | step = 1 481 | elif fi.bits <= 16: 482 | step = 13 483 | elif fi.bits <= 32: 484 | step = 73013 485 | elif fi.bits <= 64: 486 | step = (73013 << 32) + 39 487 | 488 | for i in range(0, 2**fi.bits, step): 489 | fv = decode_float(fi, i) 490 | fval2 = round_float(fi, fv.fval) 491 | np.testing.assert_equal(fval2, fv.fval) 492 | 493 | 494 | @pytest.mark.parametrize( 495 | "v, srnumbits, expected_up", 496 | ( 497 | (259, 3, 0.0 / 8), 498 | (259, 5, 2.0 / 32), 499 | (277, 3, 3.0 / 8), 500 | (288, 3, 0.5), 501 | (311, 3, 7.0 / 8), 502 | ), 503 | ) 504 | @pytest.mark.parametrize("impl", ("scalar", "array")) 505 | def test_stochastic_rounding( 506 | impl: bool, v: float, srnumbits: int, expected_up: float 507 | ) -> None: 508 | fi = format_info_ocp_e5m2 509 | 510 | v0 = round_float(fi, v, RoundMode.TowardNegative) 511 | v1 = round_float(fi, v, RoundMode.TowardPositive) 512 | 513 | n = 10_000 514 | expected_up_count = expected_up * n 515 | 516 | srbits = np.random.randint(0, 2**srnumbits, size=(n,)) 517 | if impl == "scalar": 518 | count_v1 = 0 519 | for k in range(n): 520 | r = round_float( 521 | fi, 522 | v, 523 | RoundMode.Stochastic, 524 | sat=False, 525 | srbits=srbits[k], 526 | srnumbits=srnumbits, 527 | ) 528 | if r == v1: 529 | count_v1 += 1 530 | else: 531 | assert r == v0 532 | else: 533 | vs = np.full(n, v) 534 | rs = round_ndarray(fi, vs, RoundMode.Stochastic, False, srbits, srnumbits) 535 | assert np.all((rs == v0) | (rs == v1)) 536 | count_v1 = np.sum(rs == v1) 537 | 538 | print(f"SRBits={srnumbits}, observed = {count_v1}, expected = {expected_up_count} ") 539 | # e.g. if expected is 1250/10000, want to be within 0.75,1.25 540 | # this is loose, but should still catch logic errors 541 | atol = n * 2.0 ** (-1 - srnumbits) 542 | np.testing.assert_allclose(count_v1, expected_up_count, atol=atol / 2) 543 | 544 | 545 | @pytest.mark.parametrize( 546 | "rnd", 547 | ( 548 | RoundMode.Stochastic, 549 | RoundMode.StochasticOdd, 550 | RoundMode.StochasticFast, 551 | RoundMode.StochasticFastest, 552 | ), 553 | ) 554 | @pytest.mark.parametrize("srnumbits", [3, 8, 9, 16, 32]) 555 | @pytest.mark.parametrize("sat", (True, False)) 556 | def test_stochastic_rounding_scalar_eq_array( 557 | rnd: RoundMode, srnumbits: int, sat: bool 558 | ) -> None: 559 | fi = format_info_ocp_e5m2 560 | 561 | v0 = decode_ndarray(fi, np.arange(255)) 562 | v1 = decode_ndarray(fi, np.arange(255) + 1) 563 | ok = np.isfinite(v0) & np.isfinite(v1) 564 | v0 = v0[ok] 565 | v1 = v1[ok] 566 | 567 | for alpha in (0, 0.3, 0.5, 0.6, 0.7, 0.9, 1.25): 568 | v = _linterp(v0, v1, alpha) 569 | assert np.isfinite(v).all() 570 | srbits = np.random.randint(0, 2**srnumbits, v.shape) 571 | 572 | val_array = round_ndarray( 573 | fi, 574 | v, 575 | rnd, 576 | sat=sat, 577 | srbits=srbits, 578 | srnumbits=srnumbits, 579 | ) 580 | 581 | val_scalar = [ 582 | round_float( 583 | fi, 584 | vi, 585 | rnd, 586 | sat=sat, 587 | srbits=srbitsi, 588 | srnumbits=srnumbits, 589 | ) 590 | for vi, srbitsi in zip(v, srbits) 591 | ] 592 | 593 | np.testing.assert_equal(val_array, val_scalar) 594 | 595 | # Ensure faithful rounding 596 | if alpha < 1.0: 597 | assert ((val_array == v0) | (val_array == v1)).all() 598 | 599 | 600 | def test_large_bfloat() -> None: 601 | # from https://github.com/graphcore-research/gfloat/pull/49 602 | 603 | a = 6.6461399789245764e35 604 | b = 6.620178494631905e35 605 | 606 | assert b < a 607 | rounded_a = round_float(format_info_bfloat16, a, RoundMode.TowardZero) 608 | rounded_b = round_float(format_info_bfloat16, b, RoundMode.TowardZero) 609 | 610 | assert rounded_b <= rounded_a 611 | 612 | rounded_a_array = round_ndarray( 613 | format_info_bfloat16, np.array([a]), RoundMode.TowardZero 614 | ) 615 | rounded_b_array = round_ndarray( 616 | format_info_bfloat16, np.array([b]), RoundMode.TowardZero 617 | ) 618 | 619 | assert all(rounded_b_array <= rounded_a_array) 620 | -------------------------------------------------------------------------------- /docs/source/01-decode.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\n", 8 | "\n", 9 | "# GFloat Basics\n", 10 | "\n", 11 | "This notebook shows the use of `decode_float` to explore properties of some float formats.\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "# Install packages\n", 21 | "from pandas import DataFrame\n", 22 | "import numpy as np\n", 23 | "\n", 24 | "from gfloat import decode_float\n", 25 | "from gfloat.formats import *" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## List all the values in a format\n", 33 | "\n", 34 | "The first example shows how to list all values in a given format.\n", 35 | "We will choose the [OCP](https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1) E5M2 format.\n", 36 | "\n", 37 | "The object `format_info_ocp_e5m2` is from the `gfloat.formats` package, and describes the characteristics of that format:" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "data": { 47 | "text/plain": [ 48 | "FormatInfo(name='ocp_e5m2', k=8, precision=3, bias=15, is_signed=True, domain=, has_nz=True, num_high_nans=3, has_subnormals=True, is_twos_complement=False)" 49 | ] 50 | }, 51 | "execution_count": 2, 52 | "metadata": {}, 53 | "output_type": "execute_result" 54 | } 55 | ], 56 | "source": [ 57 | "format_info_ocp_e5m2" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "We shall use the format to decode all values from 0..255, and gather them in a pandas DataFrame.\n", 65 | "We see that `decode_float` returns a lot more than just the value - it also splits out the exponent, significand, and sign, and returns the `FloatClass`, which allows us to distinguish normal and subnormal numbers, as well as zero, infinity, and nan." 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 3, 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "data": { 75 | "text/html": [ 76 | "
\n", 77 | "\n", 90 | "\n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | "
fvalexpexpvalsignificandfsignificandsignbitfclass
code
00.000000e+000-1400.000FloatClass.ZERO
11.525879e-050-1410.250FloatClass.SUBNORMAL
23.051758e-050-1420.500FloatClass.SUBNORMAL
34.577637e-050-1430.750FloatClass.SUBNORMAL
46.103516e-051-1401.000FloatClass.NORMAL
........................
251-5.734400e+04301531.751FloatClass.NORMAL
252-inf311601.001FloatClass.INFINITE
253NaN311611.251FloatClass.NAN
254NaN311621.501FloatClass.NAN
255NaN311631.751FloatClass.NAN
\n", 226 | "

256 rows × 7 columns

\n", 227 | "
" 228 | ], 229 | "text/plain": [ 230 | " fval exp expval significand fsignificand signbit \\\n", 231 | "code \n", 232 | "0 0.000000e+00 0 -14 0 0.00 0 \n", 233 | "1 1.525879e-05 0 -14 1 0.25 0 \n", 234 | "2 3.051758e-05 0 -14 2 0.50 0 \n", 235 | "3 4.577637e-05 0 -14 3 0.75 0 \n", 236 | "4 6.103516e-05 1 -14 0 1.00 0 \n", 237 | "... ... ... ... ... ... ... \n", 238 | "251 -5.734400e+04 30 15 3 1.75 1 \n", 239 | "252 -inf 31 16 0 1.00 1 \n", 240 | "253 NaN 31 16 1 1.25 1 \n", 241 | "254 NaN 31 16 2 1.50 1 \n", 242 | "255 NaN 31 16 3 1.75 1 \n", 243 | "\n", 244 | " fclass \n", 245 | "code \n", 246 | "0 FloatClass.ZERO \n", 247 | "1 FloatClass.SUBNORMAL \n", 248 | "2 FloatClass.SUBNORMAL \n", 249 | "3 FloatClass.SUBNORMAL \n", 250 | "4 FloatClass.NORMAL \n", 251 | "... ... \n", 252 | "251 FloatClass.NORMAL \n", 253 | "252 FloatClass.INFINITE \n", 254 | "253 FloatClass.NAN \n", 255 | "254 FloatClass.NAN \n", 256 | "255 FloatClass.NAN \n", 257 | "\n", 258 | "[256 rows x 7 columns]" 259 | ] 260 | }, 261 | "execution_count": 3, 262 | "metadata": {}, 263 | "output_type": "execute_result" 264 | } 265 | ], 266 | "source": [ 267 | "fmt = format_info_ocp_e5m2\n", 268 | "vals = [decode_float(fmt, i) for i in range(256)]\n", 269 | "DataFrame(vals).set_index(\"code\")" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "## Plot the values in some 6-bit formats\n", 277 | "\n", 278 | "This is a plot of the positive values in each format, as a function of their integer \n", 279 | "codepoint. Subnormal values are indicated, illustrating the increased dynamic range \n", 280 | "they offer. (More on this below.)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 4, 286 | "metadata": {}, 287 | "outputs": [ 288 | { 289 | "data": { 290 | "image/png": "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6IhFixahqqoKcXFxcHJygre3Nz777DO9+c6ZMwddunSBtbU1OnTogHnz5qGysrJR3zuNRgNvb298/PHHesd/+eUXiMViXL16FQCwYsUK9OzZEzY2NvDx8cGMGTNQUlJS731rnuVur7zyCsLCwvTGXrp0Kdq3bw+5XA5/f398/fXXuvMFBQUYN24cXF1dIZfL0blz51rPTkRE1Bx52noiJSIFW8O36l4sytbyVGZno/y333Svyuxso41V07fb95uvdS8WZWtZhEwPv1ZQDm0dATgAaLVaXCsoN/iYQjKblXAhZWZmonv37igrK9Mds7a2xu+//462bdsafDxbW1vY2tpi+/btCAkJgaWl5QPdLy4uDh988AE8PDzw+uuvY+TIkfjjjz90K9VlZWV47733sHHjRojFYrzwwguYPXs2Nm/eDAD44IMP8P7772PdunXo1asXEhIS8NRTT+G3335D586ddeO89tpreP/999GrVy9YWVkhLS0Nqamp8Pb2xr59+3Dw4EFMmTIFhw4dwoABA3D06FFs3boVL774IoYMGQJvb28AgJ2dHRITE+Hl5YVTp05h6tSpsLOzw6uvvvq3zyoWizFmzBh88cUXmD59uu745s2b0b9/f7Rr1073udWrV6N9+/a4dOkSZsyYgVdffRUfffTRfX+fly5dik2bNmHt2rXo3Lkz9u3bhxdeeAGurq547LHHMG/ePJw5cwY7d+6Ei4sLLly4gPLylv0/KERE1Hp42noy6G7BTFGtnH27W66708N1q9N/fZ3zza8I8nWCrwFXxL0V8gZXwr0VcoONZQpmsxIupPz8fJSVlWHTpk04ceIENm3ahLKyMuTn5xtlPAsLCyQmJiIpKQmOjo7o378/Xn/9dfz666/3db8333wTQ4YMQc+ePZGUlIS8vDwkJyfrzldWVmLt2rUIDAxE7969MXPmTOzZs0d3/r333sOcOXMwevRodO3aFcuWLUNAQABWrVqlN84rr7yCZ599Fu3bt4enZ/V/pJ2cnLB69Wp07doVkydPRteuXVFWVobXX38dnTt3xty5cyGTyXDgwAHdfd544w3069cPvr6+GDlyJGbPno2vvvqq0c87btw4HDx4UJetoNFosGXLFowbN05vrgMHDoSvry8ef/xxvPXWW00a415KpRJvv/02EhISMGzYMHTo0AETJ07ECy+8gHXr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", 291 | "text/plain": [ 292 | "
" 293 | ] 294 | }, 295 | "metadata": {}, 296 | "output_type": "display_data" 297 | } 298 | ], 299 | "source": [ 300 | "from matplotlib import pyplot as plt\n", 301 | "from gfloat import decode_ndarray\n", 302 | "\n", 303 | "plt.figure(figsize=(12, 6))\n", 304 | "code = np.arange(0, 2**6)\n", 305 | "for fi in (\n", 306 | " format_info_ocp_e3m2,\n", 307 | " format_info_ocp_e2m3,\n", 308 | " *(format_info_p3109(6, p, Signedness.Signed, Domain.Finite) for p in range(1, 6)),\n", 309 | "):\n", 310 | " val = decode_ndarray(fi, code)\n", 311 | " valid = (val > 0) & np.isfinite(val)\n", 312 | " subnormal = val < fi.smallest_normal\n", 313 | " if \"ocp\" in str(fi):\n", 314 | " nsty = dict(marker=\"o\", markersize=5, linestyle=\"None\")\n", 315 | " snsty = dict(marker=\"o\", markersize=5, linestyle=\"None\", markerfacecolor=\"none\")\n", 316 | " else:\n", 317 | " nsty = dict(marker=\"s\", markersize=3.5, linestyle=\"None\")\n", 318 | " snsty = dict(marker=\"s\", markersize=3.5, linestyle=\"None\", markerfacecolor=\"none\")\n", 319 | " (p,) = plt.plot(\n", 320 | " code[valid & ~subnormal], val[valid & ~subnormal], label=fi.name, **nsty\n", 321 | " )\n", 322 | " (hsub,) = plt.plot(\n", 323 | " code[valid & subnormal],\n", 324 | " val[valid & subnormal],\n", 325 | " label=None,\n", 326 | " color=p.get_color(),\n", 327 | " **snsty,\n", 328 | " )\n", 329 | "\n", 330 | "plt.plot(np.nan, np.nan, label=\"Subnormal values\", color=\"k\", **snsty)\n", 331 | "plt.yscale(\"log\")\n", 332 | "plt.legend()\n", 333 | "None # suppress output" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": {}, 339 | "source": [ 340 | "## Additional format info: special values, min, max, dynamic range\n", 341 | "\n", 342 | "In addition, `FormatInfo` can tell us about other characteristics of each format.\n", 343 | "To reproduce some of the OCP spec's tables 1 and 2 (and adding P3109 `binary8p3se`)" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 5, 349 | "metadata": {}, 350 | "outputs": [ 351 | { 352 | "name": "stdout", 353 | "output_type": "stream", 354 | "text": [ 355 | "Format ocp_e4m3 ocp_e5m2 p3109_k8p3se\n", 356 | "Exponent bias 7 15 16\n", 357 | "Infinities 0 2 2\n", 358 | "Number of NaNs 2 6 1\n", 359 | "Number of zeros 2 2 1\n", 360 | "Max exponent (emax) 8 15 15\n", 361 | "Max normal number 448.0 57344.0 49152.0\n", 362 | "Min normal number 0.015625 6.103515625e-05 3.0517578125e-05\n", 363 | "Min subnormal number 0.001953125 1.52587890625e-05 7.62939453125e-06\n", 364 | "Dynamic range (binades) 18 32 33\n" 365 | ] 366 | } 367 | ], 368 | "source": [ 369 | "def compute_dynamic_range(fi):\n", 370 | " return np.log2(fi.max / fi.smallest)\n", 371 | "\n", 372 | "\n", 373 | "for prop, probe in (\n", 374 | " (\"Format \", lambda fi: fi.name.replace(\"format_info_\", \"\")),\n", 375 | " (\"Exponent bias \", lambda fi: fi.bias),\n", 376 | " (\"Infinities \", lambda fi: 2 * fi.num_posinfs),\n", 377 | " (\"Number of NaNs \", lambda fi: fi.num_nans),\n", 378 | " (\"Number of zeros \", lambda fi: int(fi.has_zero) + int(fi.has_nz)),\n", 379 | " (\"Max exponent (emax) \", lambda fi: fi.emax),\n", 380 | " (\"Max normal number \", lambda fi: fi.max),\n", 381 | " (\"Min normal number \", lambda fi: fi.smallest_normal),\n", 382 | " (\"Min subnormal number \", lambda fi: fi.smallest_subnormal),\n", 383 | " (\"Dynamic range (binades)\", lambda x: round(compute_dynamic_range(x))),\n", 384 | "):\n", 385 | " print(\n", 386 | " prop,\n", 387 | " f\"{probe(format_info_ocp_e4m3):<20}\",\n", 388 | " f\"{probe(format_info_ocp_e5m2):<20}\",\n", 389 | " f\"{probe(format_info_p3109(8, 3))}\",\n", 390 | " )" 391 | ] 392 | }, 393 | { 394 | "cell_type": "markdown", 395 | "metadata": {}, 396 | "source": [ 397 | "## How do subnormals affect dynamic range?\n", 398 | "\n", 399 | "Most, if not all, low-precision formats include subnormal numbers, as they increase the number of values near zero, and increase dynamic range.\n", 400 | "A natural question is \"by how much?\". To answer this, we can create a mythical new format, a copy of `e4m3`, but with `has_subnormals` set to true." 401 | ] 402 | }, 403 | { 404 | "cell_type": "code", 405 | "execution_count": 6, 406 | "metadata": {}, 407 | "outputs": [], 408 | "source": [ 409 | "import copy\n", 410 | "\n", 411 | "e4m3_no_subnormals = copy.copy(format_info_ocp_e4m3)\n", 412 | "e4m3_no_subnormals.has_subnormals = False" 413 | ] 414 | }, 415 | { 416 | "cell_type": "markdown", 417 | "metadata": {}, 418 | "source": [ 419 | "And now compute the dynamic range with and without:" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": 7, 425 | "metadata": {}, 426 | "outputs": [ 427 | { 428 | "name": "stdout", 429 | "output_type": "stream", 430 | "text": [ 431 | "Dynamic range with subnormals = 17.807354922057606\n", 432 | "Dynamic range without subnormals = 15.637429920615292\n", 433 | "Ratio = 4.5\n" 434 | ] 435 | } 436 | ], 437 | "source": [ 438 | "dr_with = compute_dynamic_range(format_info_ocp_e4m3)\n", 439 | "dr_without = compute_dynamic_range(e4m3_no_subnormals)\n", 440 | "\n", 441 | "print(f\"Dynamic range with subnormals = {dr_with}\")\n", 442 | "print(f\"Dynamic range without subnormals = {dr_without}\")\n", 443 | "print(f\"Ratio = {2**(dr_with - dr_without):.1f}\")" 444 | ] 445 | } 446 | ], 447 | "metadata": { 448 | "kernelspec": { 449 | "display_name": "gfloat", 450 | "language": "python", 451 | "name": "python3" 452 | }, 453 | "language_info": { 454 | "codemirror_mode": { 455 | "name": "ipython", 456 | "version": 3 457 | }, 458 | "file_extension": ".py", 459 | "mimetype": "text/x-python", 460 | "name": "python", 461 | "nbconvert_exporter": "python", 462 | "pygments_lexer": "ipython3", 463 | "version": "3.12.3" 464 | } 465 | }, 466 | "nbformat": 4, 467 | "nbformat_minor": 2 468 | } 469 | --------------------------------------------------------------------------------