├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── BUILDING.md ├── ChangeLog ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements-rtd.txt └── source │ ├── 01-decode.ipynb │ ├── 02-value-stats.ipynb │ ├── 03-value-tables.ipynb │ ├── 04-benchmark.ipynb │ ├── 05-stochastic-rounding.ipynb │ ├── api.rst │ ├── conf.py │ ├── formats.rst │ ├── index.rst │ ├── notebooks.rst │ └── utils.py ├── etc ├── check-copyright.sh ├── package.sh └── test-check-copyright.sh ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── src └── gfloat │ ├── __init__.py │ ├── block.py │ ├── decode.py │ ├── decode_ndarray.py │ ├── encode.py │ ├── encode_ndarray.py │ ├── formats.py │ ├── printing.py │ ├── round.py │ ├── round_ndarray.py │ └── types.py └── test ├── test_array_api.py ├── test_block.py ├── test_decode.py ├── test_encode.py ├── test_finfo.py ├── test_jax.py ├── test_microxcaling.py ├── test_printing.py ├── test_round.py └── test_torch.py /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2 | 0.3: Jun 10, 2024 3 | - Use python ints throughout, adding float64 to test 4 | - Simplify round, fix directed rounding 5 | - Rename "ival" to "code" in FloatValue 6 | - Shorten format names from "format_info_*" to "*" 7 | 8 | 9 | 0.2: May 21, 2024 10 | - Add MX Formats 11 | - Improved CI 12 | - Add value table pretty-printing 13 | 14 | 0.1: May 2, 2024 15 | - First released version 16 | 17 | Copyright (c) 2024 Graphcore Ltd. All rights reserved. 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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|0NUL 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 | -------------------------------------------------------------------------------- /docs/requirements-rtd.txt: -------------------------------------------------------------------------------- 1 | .[dev] 2 | -------------------------------------------------------------------------------- /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": 2, 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": 3, 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "data": { 47 | "text/plain": [ 48 | "FormatInfo(name='ocp_e5m2', k=8, precision=3, emax=15, has_nz=True, has_infs=True, num_high_nans=3, has_subnormals=True, is_signed=True, is_twos_complement=False)" 49 | ] 50 | }, 51 | "execution_count": 3, 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": 4, 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": 4, 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 8-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": 5, 286 | "metadata": {}, 287 | "outputs": [ 288 | { 289 | "data": { 290 | "image/png": "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unTpgr179+q3LdNoNNi7dy+mTJlizo+iyqSGrImt7/aJBgYvNmltt6uDq+SQNbH13eGqcCTGJrLaTUREREREjZ7JIbygoAApKSn679PS0nD69Gl4e3sjLCwM06ZNQ0JCArp27YrY2FgsX74chYWF+mnpVAekhqzFThJf323i2m5d27jYkLX4lvGi67tZ7SYiIiIiIpIRwk+cOIF+/frpv9cNRUtISMD69evx9NNP4+bNm5g7dy4yMjLQsWNH7N69u8qwNpLB1Jbz3MuS67vl7N0ttaUY13cTERERERFJMzmE9+3bF4IgSD5nypQpbD83N3UBsG4QcCsF8GsB/LoVOPs18Nxu6S3FAKMVb7l7d0ttKQZwfTcREREREZEUm56OTpUkrdEG8Bd+BALbAhlntevAq2s5h3n37pZqOSciIiIiIiJpDOH1Re5lbQU8sK32+8C2gG/zGrWcm3PvbracExERERERyccQXl94hWtb0DPO3quEZ/8FtHlce1xkyFpd7N3NlnMiIiIiIiJ5GMJtjdjwtdhJ2jXga/trK+DZf2m3GZNoOefe3URERERERLaFIdyWSA1fc3LX/qkL6G0er7blnHt3ExERERER2RaGcFsiNXyt9zSTW865dzcREREREZFtYQi3JVLD1yRw724iIiIiIqL6gSHcllQzfE1s3Tf37iYiIiIiIqofFIIgCNY+CXPKz8+Hp6cn8vLyoFKprH06pqm8Jrzy8LXndqNIqcS4XeNwOf8yIj0jkZaXhnBVOP41+F8AUGVNeJRnFDYN2cSKNxERERERUR0zJYeyEm5LJIavbT6zFpfzL2PjkI2I8Y7B+ZzzeHbns9h8bjNeaPcCW86JiIiIiIjqAYZwWyMyfC29IB2RnpGI8Y4BAMR4xyDSMxLpBekA2HJORERERERUHyitfQJUM8HuwUjLS8P5nPMAgPM555GWl6Zf901ERERERES2j5Vwa1AX3Gs59wrXt5wD4sPX4lvGY3fabjy781mDNeHxLeOtfDFERERERERUUxzMZmmVh6/5tQBuXqjR8DVXB1fRgE5ERERERETWw8FstixpjTaAv/DjvW3I1vYHktZgs5dKcvga130TERERERHVbwzhlpZ7WVsBD2yr/T6wrXY7stzLSLf3lhy+RkRERERERPUbB7NZmle4tgU946z2+4yz2v3AvcI5fI2IiIiI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s7O1T4dsjEajQdeuXfHOO+8AADp16oSzZ89i9erVSEhIsPLZkTX95z//waZNm7B582a0adMGp0+fxtSpU9G0aVPeG0RksrKyMjz11FMQBAGrVq2y9uk0GmxHrwO+vr6ws7OrMsU4MzMTgYGBVjorsqYpU6bgu+++w759+xASEqJ/PDAwEKWlpcjNzTV4Pu+Vhi85ORlZWVno3Lkz7O3tYW9vj59//hkfffQR7O3tERAQwHujEQsKCkLr1q0NHmvVqhWuXLkCAPp7gP+daXxmzJiBWbNmYfTo0WjXrh3Gjh2L1157DYsWLQLAe4O0anIfBAYGVhkYXF5ejpycHN4rjYQugF++fBl79uzRV8EB3h91jSG8Djg6OqJLly7Yu3ev/jGNRoO9e/ciLi7OimdGliYIAqZMmYJt27bhp59+QmRkpMHxLl26wMHBweBeOX/+PK5cucJ7pYF7+OGHcebMGZw+fVr/T9euXTFmzBj917w3Gq+ePXtW2c7wwoULCA8PBwBERkYiMDDQ4P7Iz8/H8ePHeX80cEVFRVAqDf/6ZmdnB41GA4D3BmnV5D6Ii4tDbm4ukpOT9c/56aefoNFo0K1bN4ufM1mWLoD/9ddf+PHHH+Hj42NwnPdHHbP2ZLiGasuWLYKTk5Owfv164Y8//hAmTZokeHl5CRkZGdY+NbKgyZMnC56ensL+/fuFGzdu6P8pKirSP+ell14SwsLChJ9++kk4ceKEEBcXJ8TFxVnxrMlaKk9HFwTeG41ZUlKSYG9vLyxcuFD466+/hE2bNgmurq7Cxo0b9c959913BS8vL+H//u//hN9++0147LHHhMjISKG4uNiKZ051LSEhQQgODha+++47IS0tTfj6668FX19f4fXXX9c/h/dG43Dnzh3h1KlTwqlTpwQAwrJly4RTp07pp1vX5D4YNGiQ0KlTJ+H48ePCoUOHhObNmwvPPPOMtS6JzEjq/igtLRWGDx8uhISECKdPnzb4O6parda/B++PusMQXodWrFghhIWFCY6OjkJsbKxw7Ngxa58SWRgAo/988cUX+ucUFxcLL7/8stCkSRPB1dVVePzxx4UbN25Y76TJau4P4bw3Grdvv/1WaNu2reDk5CS0bNlSWLNmjcFxjUYjzJkzRwgICBCcnJyEhx9+WDh//ryVzpYsJT8/X3j11VeFsLAwwdnZWYiKihLefPNNg784895oHPbt22f07xgJCQmCINTsPrh165bwzDPPCO7u7oJKpRImTJgg3LlzxwpXQ+YmdX+kpaWJ/h113759+vfg/VF3FIIgCJaruxMRERERERE1XlwTTkRERERERGQhDOFEREREREREFsIQTkRERERERGQhDOFEREREREREFsIQTkRERERERGQhDOFEREREREREFsIQTkRERERERGQhDOFEREREREREFsIQTkRERERERGQhDOFEREREREREFsIQTkRERERERGQh/w8l/ofWtG83PAAAAABJRU5ErkJggg==", 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, 4))\n", 304 | "code = np.arange(0, 256)\n", 305 | "for fi in (\n", 306 | " format_info_ocp_e4m3,\n", 307 | " format_info_ocp_e5m2,\n", 308 | " *(format_info_p3109(8, p) for p in (3, 4, 5)),\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 | " nsty = dict(marker=\"o\", markersize=3.5, linestyle=\"None\")\n", 314 | " (p,) = plt.plot(\n", 315 | " code[valid & ~subnormal], val[valid & ~subnormal], label=fi.name, **nsty\n", 316 | " )\n", 317 | " snsty = dict(marker=\"o\", markersize=3.5, linestyle=\"None\", markerfacecolor=\"none\")\n", 318 | " (hsub,) = plt.plot(\n", 319 | " code[valid & subnormal],\n", 320 | " val[valid & subnormal],\n", 321 | " label=None,\n", 322 | " color=p.get_color(),\n", 323 | " **snsty,\n", 324 | " )\n", 325 | "\n", 326 | "plt.plot(np.nan, np.nan, label=\"Subnormal values\", color=\"k\", **snsty)\n", 327 | "plt.yscale(\"log\")\n", 328 | "plt.legend()\n", 329 | "None # suppress output" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": {}, 335 | "source": [ 336 | "## Additional format info: special values, min, max, dynamic range\n", 337 | "\n", 338 | "In addition, `FormatInfo` can tell us about other characteristics of each format.\n", 339 | "To reproduce some of the OCP spec's tables 1 and 2:" 340 | ] 341 | }, 342 | { 343 | "cell_type": "code", 344 | "execution_count": 8, 345 | "metadata": {}, 346 | "outputs": [ 347 | { 348 | "name": "stdout", 349 | "output_type": "stream", 350 | "text": [ 351 | "Format ocp_e4m3 ocp_e5m2 p3109_8p3\n", 352 | "Max exponent (emax) 8 15 15\n", 353 | "Exponent bias 7 15 16\n", 354 | "Infinities 0 2 2\n", 355 | "Number of NaNs 2 6 1\n", 356 | "Number of zeros 2 2 1\n", 357 | "Max normal number 448.0 57344.0 49152.0\n", 358 | "Min normal number 0.015625 6.103515625e-05 3.0517578125e-05\n", 359 | "Min subnormal number 0.001953125 1.52587890625e-05 7.62939453125e-06\n", 360 | "Dynamic range (binades) 18 32 33\n" 361 | ] 362 | } 363 | ], 364 | "source": [ 365 | "def compute_dynamic_range(fi):\n", 366 | " return np.log2(fi.max / fi.smallest)\n", 367 | "\n", 368 | "\n", 369 | "for prop, probe in (\n", 370 | " (\"Format \", lambda fi: fi.name.replace(\"format_info_\", \"\")),\n", 371 | " (\"Max exponent (emax) \", lambda fi: fi.emax),\n", 372 | " (\"Exponent bias \", lambda fi: fi.expBias),\n", 373 | " (\"Infinities \", lambda fi: 2 * int(fi.has_infs)),\n", 374 | " (\"Number of NaNs \", lambda fi: fi.num_nans),\n", 375 | " (\"Number of zeros \", lambda fi: int(fi.has_zero) + int(fi.has_nz)),\n", 376 | " (\"Max normal number \", lambda fi: fi.max),\n", 377 | " (\"Min normal number \", lambda fi: fi.smallest_normal),\n", 378 | " (\"Min subnormal number \", lambda fi: fi.smallest_subnormal),\n", 379 | " (\"Dynamic range (binades)\", lambda x: round(compute_dynamic_range(x))),\n", 380 | "):\n", 381 | " print(\n", 382 | " prop,\n", 383 | " f\"{probe(format_info_ocp_e4m3):<20}\",\n", 384 | " f\"{probe(format_info_ocp_e5m2):<20}\",\n", 385 | " f\"{probe(format_info_p3109(8, 3))}\",\n", 386 | " )" 387 | ] 388 | }, 389 | { 390 | "cell_type": "markdown", 391 | "metadata": {}, 392 | "source": [ 393 | "## How do subnormals affect dynamic range?\n", 394 | "\n", 395 | "Most, if not all, low-precision formats include subnormal numbers, as they increase the number of values near zero, and increase dynamic range.\n", 396 | "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." 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 9, 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "import copy\n", 406 | "\n", 407 | "e4m3_no_subnormals = copy.copy(format_info_ocp_e4m3)\n", 408 | "e4m3_no_subnormals.has_subnormals = False" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "And now compute the dynamic range with and without:" 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": 10, 421 | "metadata": {}, 422 | "outputs": [ 423 | { 424 | "name": "stdout", 425 | "output_type": "stream", 426 | "text": [ 427 | "Dynamic range with subnormals = 17.807354922057606\n", 428 | "Dynamic range without subnormals = 15.637429920615292\n", 429 | "Ratio = 4.5\n" 430 | ] 431 | } 432 | ], 433 | "source": [ 434 | "dr_with = compute_dynamic_range(format_info_ocp_e4m3)\n", 435 | "dr_without = compute_dynamic_range(e4m3_no_subnormals)\n", 436 | "\n", 437 | "print(f\"Dynamic range with subnormals = {dr_with}\")\n", 438 | "print(f\"Dynamic range without subnormals = {dr_without}\")\n", 439 | "print(f\"Ratio = {2**(dr_with - dr_without):.1f}\")" 440 | ] 441 | } 442 | ], 443 | "metadata": { 444 | "kernelspec": { 445 | "display_name": "Python 3", 446 | "language": "python", 447 | "name": "python3" 448 | }, 449 | "language_info": { 450 | "codemirror_mode": { 451 | "name": "ipython", 452 | "version": 3 453 | }, 454 | "file_extension": ".py", 455 | "mimetype": "text/x-python", 456 | "name": "python", 457 | "nbconvert_exporter": "python", 458 | "pygments_lexer": "ipython3", 459 | "version": "3.10.0" 460 | } 461 | }, 462 | "nbformat": 4, 463 | "nbformat_minor": 2 464 | } 465 | -------------------------------------------------------------------------------- /docs/source/04-benchmark.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 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": 4, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "GFloat scalar : 7510.22 nsec (25 runs at size 10000)\n", 45 | "GFloat vectorized, numpy arrays: 43.82 nsec (25 runs at size 1000000)\n", 46 | "GFloat vectorized, JAX JIT : 2.69 nsec (500 runs at size 1000000)\n", 47 | "ML_dtypes : 2.57 nsec (500 runs at size 1000000)\n" 48 | ] 49 | } 50 | ], 51 | "source": [ 52 | "# NBVAL_IGNORE_OUTPUT\n", 53 | "\n", 54 | "N = 1_000_000\n", 55 | "a = np.random.rand(N)\n", 56 | "\n", 57 | "jax_round_jit = jax.jit(lambda x: gfloat.round_ndarray(format_info_ocp_e5m2, x))\n", 58 | "ja = jnp.array(a)\n", 59 | "jax_round_jit(ja) # Cache compilation\n", 60 | "\n", 61 | "\n", 62 | "def slow_round_ndarray(fi, a):\n", 63 | " return np.array([gfloat.round_float(fi, x) for x in a])\n", 64 | "\n", 65 | "\n", 66 | "# About how many seconds to run for (autorange will take at least .2 sec)\n", 67 | "ACCURACY = 1.0\n", 68 | "\n", 69 | "\n", 70 | "def time(f, problem_size=1.0):\n", 71 | " units = 1e9 # nsec\n", 72 | " t = Timer(f)\n", 73 | " f() # pre-run\n", 74 | " n = int(t.autorange()[0] * ACCURACY / 0.2)\n", 75 | " ts = t.repeat(repeat=3, number=n) # best of 3\n", 76 | " ts = [((t / n) / problem_size) * units for t in ts] # per run\n", 77 | " return f\"{min(ts):8.2f} nsec ({n} runs at size {problem_size})\"\n", 78 | "\n", 79 | "\n", 80 | "# fmt: off\n", 81 | "print(\"GFloat scalar :\", time(lambda: slow_round_ndarray(format_info_ocp_e5m2, a[: N // 100]), N // 100))\n", 82 | "print(\"GFloat vectorized, numpy arrays:\", time(lambda: gfloat.round_ndarray(format_info_ocp_e5m2, a), N))\n", 83 | "print(\"GFloat vectorized, JAX JIT :\", time(lambda: jax_round_jit(ja), N))\n", 84 | "print(\"ML_dtypes :\", time(lambda: a.astype(ml_dtypes.float8_e5m2), N))" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "On one CPU platform the timings were:\n", 92 | "```\n", 93 | "GFloat scalar : 6996.75 nsec (50 runs at size 10000)\n", 94 | "GFloat vectorized, numpy arrays: 75.04 nsec (50 runs at size 1000000)\n", 95 | "GFloat vectorized, JAX JIT : 3.18 nsec (1000 runs at size 1000000)\n", 96 | "ML_dtypes : 3.13 nsec (1000 runs at size 1000000)\n", 97 | "```\n", 98 | "So the JAX JIT code is ~1000x faster than the scalar code, and comparable to `ml_dtypes`'s C++ CPU implementation." 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": ".venv", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.10.0" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 2 123 | } 124 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.4" # Set version in package.sh 11 | version = "0.4" # 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 IEEE-754 parameters of: 30 | 31 | * Width in bits (k) 32 | * Precision (p) 33 | * Maximum exponent (emax) 34 | 35 | with additional fields defining the presence/encoding of: 36 | 37 | * Infinities 38 | * Not-a-number (NaN) values 39 | * Negative zero 40 | * Subnormal numbers 41 | * Signed/unsigned 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} for K > 2, and 1 <= P < K. 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 | 93 | To get started with the library, we recommend perusing the notebooks, 94 | otherwise you may wish to jump straight into the API. 95 | 96 | Contents 97 | ======== 98 | 99 | .. toctree:: 100 | :hidden: 101 | 102 | self 103 | 104 | .. toctree:: 105 | 106 | notebooks 107 | api 108 | formats 109 | 110 | 111 | Index and Search 112 | ================ 113 | 114 | * :ref:`genindex` 115 | * :ref:`search` 116 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.4" 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 | pip install build twine 20 | python -m build 21 | echo "Enter PyPI API Token" 22 | echo __token__ | twine upload --repository pypi dist/* --verbose 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.4" # 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 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | more_itertools 3 | array-api-compat 4 | -------------------------------------------------------------------------------- /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, 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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 | expBias = fi.expBias 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 - expBias 56 | fsignificand = significand * 2**-t 57 | else: 58 | expval = exp - expBias 59 | fsignificand = 1.0 + significand * 2**-t 60 | 61 | # Handle specials: Infs, NaN, -0, NaN_0 62 | signed_infinity = -np.inf if signbit else np.inf 63 | 64 | fval = None 65 | # All-bits-special exponent (ABSE) 66 | if w > 0 and exp == 2**w - 1: 67 | min_i_with_nan = 2 ** (p - 1) - fi.num_high_nans 68 | if significand >= min_i_with_nan: 69 | fval = np.nan 70 | if fi.has_infs and significand == min_i_with_nan - 1: 71 | fval = signed_infinity 72 | 73 | # Negative zero or NaN 74 | if iszero and i == signmask and not fi.is_twos_complement: 75 | if fi.has_nz: 76 | fval = -0.0 77 | else: 78 | fval = np.nan 79 | 80 | # In range - compute value 81 | if fval is None: 82 | fval = sign * fsignificand * 2.0**expval 83 | 84 | # Compute FloatClass 85 | fclass = None 86 | if fval == 0: 87 | fclass = FloatClass.ZERO 88 | elif np.isnan(fval): 89 | fclass = FloatClass.NAN 90 | elif np.isfinite(fval): 91 | if isnormal: 92 | fclass = FloatClass.NORMAL 93 | else: 94 | fclass = FloatClass.SUBNORMAL 95 | else: 96 | fclass = FloatClass.INFINITE 97 | 98 | return FloatValue(i, fval, exp, expval, significand, fsignificand, signbit, fclass) 99 | -------------------------------------------------------------------------------- /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 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 | expBias = fi.expBias 51 | 52 | fval = np.zeros_like(codes, dtype=np.float64) 53 | isspecial = np.zeros_like(codes, dtype=bool) 54 | 55 | if fi.has_infs: 56 | fval = np.where(codes == fi.code_of_posinf, np.inf, fval) 57 | isspecial |= codes == fi.code_of_posinf 58 | fval = np.where(codes == fi.code_of_neginf, -np.inf, fval) 59 | isspecial |= codes == fi.code_of_neginf 60 | 61 | if fi.num_nans > 0: 62 | code_is_nan = codes == fi.code_of_nan 63 | if w > 0: 64 | # All-bits-special exponent (ABSE) 65 | abse = exp == 2**w - 1 66 | min_code_with_nan = 2 ** (p - 1) - fi.num_high_nans 67 | code_is_nan |= abse & (significand >= min_code_with_nan) 68 | 69 | fval = np.where(code_is_nan, np.nan, fval) 70 | isspecial |= code_is_nan 71 | 72 | # Zero 73 | iszero = ~isspecial & (exp == 0) & (significand == 0) & fi.has_zero 74 | fval = np.where(iszero, 0.0, fval) 75 | if fi.has_nz: 76 | fval = np.where(iszero & (sign < 0), -0.0, fval) 77 | 78 | issubnormal = (exp == 0) & (significand != 0) & fi.has_subnormals 79 | expval = np.where(issubnormal, 1 - expBias, exp - expBias) 80 | fsignificand = np.where(issubnormal, 0.0, 1.0) + np.ldexp(significand, -t) 81 | 82 | # Normal/Subnormal/Zero case, other values will be overwritten 83 | expval_safe = np.where(isspecial | iszero, 0, expval) 84 | fval_finite_safe = sign * np.ldexp(fsignificand, expval_safe) 85 | fval = np.where(~(iszero | isspecial), fval_finite_safe, fval) 86 | 87 | return fval 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 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.has_infs: 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.has_infs: 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.expBias 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.expBias) 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 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.has_infs: 44 | code[v > fi.max] = fi.code_of_posinf 45 | code[v < fi.min] = fi.code_of_neginf 46 | else: 47 | code[v > fi.max] = fi.code_of_nan if fi.num_nans > 0 else fi.code_of_max 48 | code[v < fi.min] = fi.code_of_nan if fi.num_nans > 0 else fi.code_of_min 49 | 50 | if fi.has_zero: 51 | if fi.has_nz: 52 | code[v == 0] = np.where(sign[v == 0], fi.code_of_negzero, fi.code_of_zero) 53 | else: 54 | code[v == 0] = fi.code_of_zero 55 | 56 | finite_mask = (code == 0) & (v != 0) 57 | assert not np.any(np.isnan(vpos[finite_mask])) 58 | if np.any(finite_mask): 59 | finite_vpos = vpos[finite_mask] 60 | finite_sign = sign[finite_mask] 61 | 62 | sig, exp = np.frexp(finite_vpos) 63 | 64 | biased_exp = exp.astype(np.int64) + (fi.expBias - 1) 65 | subnormal_mask = (biased_exp < 1) & fi.has_subnormals 66 | 67 | biased_exp_safe = np.where(subnormal_mask, biased_exp, 0) 68 | tsig = np.where(subnormal_mask, np.ldexp(sig, biased_exp_safe), sig * 2 - 1.0) 69 | biased_exp[subnormal_mask] = 0 70 | 71 | isig = np.floor(np.ldexp(tsig, t)).astype(np.int64) 72 | 73 | zero_mask = fi.has_zero & (isig == 0) & (biased_exp == 0) 74 | if not fi.has_nz: 75 | finite_sign[zero_mask] = False 76 | 77 | # Handle two's complement encoding 78 | if fi.is_twos_complement: 79 | isig[finite_sign] = (1 << t) - isig[finite_sign] 80 | 81 | code[finite_mask] = ( 82 | (finite_sign.astype(int) << (k - 1)) | (biased_exp << t) | (isig << 0) 83 | ) 84 | 85 | return code 86 | -------------------------------------------------------------------------------- /src/gfloat/formats.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from .block import BlockFormatInfo 4 | from .types import FormatInfo 5 | 6 | #: FormatInfo for IEEE-754 Binary64 format 7 | format_info_binary64 = FormatInfo( 8 | name="binary64", 9 | k=64, 10 | precision=53, 11 | emax=1023, 12 | has_nz=True, 13 | has_infs=True, 14 | num_high_nans=2**52 - 1, 15 | has_subnormals=True, 16 | is_signed=True, 17 | is_twos_complement=False, 18 | ) 19 | 20 | #: FormatInfo for IEEE-754 Binary32 format 21 | format_info_binary32 = FormatInfo( 22 | name="binary32", 23 | k=32, 24 | precision=24, 25 | emax=127, 26 | has_nz=True, 27 | has_infs=True, 28 | num_high_nans=2**23 - 1, 29 | has_subnormals=True, 30 | is_signed=True, 31 | is_twos_complement=False, 32 | ) 33 | 34 | #: FormatInfo for IEEE-754 Binary16 format 35 | format_info_binary16 = FormatInfo( 36 | name="binary16", 37 | k=16, 38 | precision=11, 39 | emax=15, 40 | has_nz=True, 41 | has_infs=True, 42 | num_high_nans=2**10 - 1, 43 | has_subnormals=True, 44 | is_signed=True, 45 | is_twos_complement=False, 46 | ) 47 | 48 | #: FormatInfo for Google BFloat16 format 49 | format_info_bfloat16 = FormatInfo( 50 | name="bfloat16", 51 | k=16, 52 | precision=8, 53 | emax=127, 54 | has_nz=True, 55 | has_infs=True, 56 | num_high_nans=2**7 - 1, 57 | has_subnormals=True, 58 | is_signed=True, 59 | is_twos_complement=False, 60 | ) 61 | 62 | #: FormatInfo for OCP E5M2 format 63 | format_info_ocp_e5m2 = FormatInfo( 64 | name="ocp_e5m2", 65 | k=8, 66 | precision=3, 67 | emax=15, 68 | has_nz=True, 69 | has_infs=True, 70 | num_high_nans=2**2 - 1, 71 | has_subnormals=True, 72 | is_signed=True, 73 | is_twos_complement=False, 74 | ) 75 | 76 | #: FormatInfo for OCP E4M3 format 77 | format_info_ocp_e4m3 = FormatInfo( 78 | name="ocp_e4m3", 79 | k=8, 80 | precision=4, 81 | emax=8, 82 | has_nz=True, 83 | has_infs=False, 84 | num_high_nans=1, 85 | has_subnormals=True, 86 | is_signed=True, 87 | is_twos_complement=False, 88 | ) 89 | 90 | #: FormatInfo for OCP MX E2M3 format 91 | format_info_ocp_e2m3 = FormatInfo( 92 | name="ocp_e2m3", 93 | k=6, 94 | precision=4, 95 | emax=2, 96 | has_nz=True, 97 | has_infs=False, 98 | num_high_nans=0, 99 | has_subnormals=True, 100 | is_signed=True, 101 | is_twos_complement=False, 102 | ) 103 | 104 | #: FormatInfo for OCP MX E3M2 format 105 | format_info_ocp_e3m2 = FormatInfo( 106 | name="ocp_e3m2", 107 | k=6, 108 | precision=3, 109 | emax=4, 110 | has_nz=True, 111 | has_infs=False, 112 | num_high_nans=0, 113 | has_subnormals=True, 114 | is_signed=True, 115 | is_twos_complement=False, 116 | ) 117 | 118 | #: FormatInfo for OCP MX E2M1 format 119 | format_info_ocp_e2m1 = FormatInfo( 120 | name="ocp_e2m1", 121 | k=4, 122 | precision=2, 123 | emax=2, 124 | has_nz=True, 125 | has_infs=False, 126 | num_high_nans=0, 127 | has_subnormals=True, 128 | is_signed=True, 129 | is_twos_complement=False, 130 | ) 131 | 132 | #: FormatInfo for OCP MX E8M0 format 133 | format_info_ocp_e8m0 = FormatInfo( 134 | name="ocp_e8m0", 135 | k=8, 136 | precision=1, 137 | emax=127, 138 | has_nz=False, 139 | has_infs=False, 140 | num_high_nans=1, 141 | has_subnormals=False, 142 | is_signed=False, 143 | is_twos_complement=False, 144 | ) 145 | 146 | #: FormatInfo for OCP MX INT8 format 147 | format_info_ocp_int8 = FormatInfo( 148 | name="ocp_int8", 149 | k=8, 150 | precision=8, 151 | emax=0, 152 | has_nz=False, 153 | has_infs=False, 154 | num_high_nans=0, 155 | has_subnormals=True, 156 | is_signed=True, 157 | is_twos_complement=True, 158 | ) 159 | 160 | 161 | def format_info_p3109(k: int, precision: int) -> FormatInfo: 162 | """ 163 | FormatInfo for P3109 K{k} P{p} formats 164 | 165 | Args: 166 | k (int): Format width in bits 167 | p (int): Precision in bits 168 | 169 | Returns: 170 | FormatInfo class describing the format 171 | 172 | Raises: 173 | ValueError: If p is not in 1..k-1 174 | ValueError: If k is < 2 175 | """ 176 | if precision < 1 or precision > k - 1: 177 | raise ValueError(f"P3109 format not defined for p={precision}") 178 | 179 | name = f"p3109_{k}p{precision}" 180 | emax = 2 ** (k - 1 - precision) - 1 181 | 182 | return FormatInfo( 183 | name, 184 | k=k, 185 | precision=precision, 186 | emax=emax, 187 | has_nz=False, 188 | has_infs=True, 189 | num_high_nans=0, 190 | has_subnormals=True, 191 | is_signed=True, 192 | is_twos_complement=False, 193 | ) 194 | 195 | 196 | # Collections of formats 197 | _tiny_formats = [ 198 | format_info_ocp_e2m1, 199 | format_info_p3109(4, 2), 200 | format_info_ocp_e2m3, 201 | format_info_ocp_e3m2, 202 | format_info_p3109(6, 3), 203 | format_info_p3109(6, 4), 204 | ] 205 | 206 | p3109_binary8_formats = [format_info_p3109(8, p) for p in range(1, 7)] 207 | 208 | _fp8_formats = [ 209 | format_info_ocp_e4m3, 210 | format_info_ocp_e5m2, 211 | *p3109_binary8_formats, 212 | ] 213 | 214 | _fp16_formats = [ 215 | format_info_binary16, 216 | format_info_bfloat16, 217 | ] 218 | 219 | all_formats = [ 220 | *_tiny_formats, 221 | *_fp8_formats, 222 | *_fp16_formats, 223 | format_info_binary32, 224 | format_info_binary64, 225 | format_info_ocp_e8m0, 226 | format_info_ocp_int8, 227 | ] 228 | 229 | # ------ 230 | # Block formats 231 | 232 | format_info_mxfp8_e5m2 = BlockFormatInfo( 233 | "mxfp8_e5m2", format_info_ocp_e5m2, 32, format_info_ocp_e8m0 234 | ) 235 | 236 | format_info_mxfp8_e4m3 = BlockFormatInfo( 237 | "mxfp8_e4m3", format_info_ocp_e4m3, 32, format_info_ocp_e8m0 238 | ) 239 | 240 | format_info_mxfp6_e3m2 = BlockFormatInfo( 241 | "mxfp6_e3m2", format_info_ocp_e3m2, 32, format_info_ocp_e8m0 242 | ) 243 | 244 | format_info_mxfp6_e2m3 = BlockFormatInfo( 245 | "mxfp6_e2m3", format_info_ocp_e2m3, 32, format_info_ocp_e8m0 246 | ) 247 | 248 | format_info_mxfp4_e2m1 = BlockFormatInfo( 249 | "mxfp4_e2m1", format_info_ocp_e2m1, 32, format_info_ocp_e8m0 250 | ) 251 | 252 | format_info_mxfp4_e2m1 = BlockFormatInfo( 253 | "mxfp4_e2m1", format_info_ocp_e2m1, 32, format_info_ocp_e8m0 254 | ) 255 | 256 | format_info_mxint8 = BlockFormatInfo( 257 | "mxint8", format_info_ocp_int8, 32, format_info_ocp_e8m0 258 | ) 259 | 260 | all_block_formats = [ 261 | format_info_mxfp8_e5m2, 262 | format_info_mxfp8_e4m3, 263 | format_info_mxfp6_e3m2, 264 | format_info_mxfp6_e2m3, 265 | format_info_mxfp4_e2m1, 266 | format_info_mxint8, 267 | ] 268 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/gfloat/round.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | import math 4 | 5 | import numpy as np 6 | import math 7 | 8 | from .types import FormatInfo, RoundMode 9 | 10 | 11 | def _isodd(v: int) -> 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.expBias 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 = int(math.floor(math.log2(vpos))) 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.has_infs: 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/round_ndarray.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 Graphcore Ltd. All rights reserved. 2 | 3 | from typing import Optional 4 | from types import ModuleType 5 | from .types import FormatInfo, RoundMode 6 | 7 | import numpy.typing as npt 8 | import array_api_compat 9 | 10 | 11 | def _isodd(v: npt.NDArray) -> npt.NDArray: 12 | return v & 0x1 == 1 13 | 14 | 15 | def _ldexp(v: npt.NDArray, s: npt.NDArray) -> npt.NDArray: 16 | xp = array_api_compat.array_namespace(v, s) 17 | if ( 18 | array_api_compat.is_torch_array(v) 19 | or array_api_compat.is_jax_array(v) 20 | or array_api_compat.is_numpy_array(v) 21 | ): 22 | return xp.ldexp(v, s) 23 | 24 | # Scale away from subnormal/infinite ranges 25 | offset = 24 26 | vlo = (v * 2.0**+offset) * 2.0 ** xp.astype(s - offset, v.dtype) 27 | vhi = (v * 2.0**-offset) * 2.0 ** xp.astype(s + offset, v.dtype) 28 | return xp.where(v < 1.0, vlo, vhi) 29 | 30 | 31 | def round_ndarray( 32 | fi: FormatInfo, 33 | v: npt.NDArray, 34 | rnd: RoundMode = RoundMode.TiesToEven, 35 | sat: bool = False, 36 | srbits: Optional[npt.NDArray] = None, 37 | srnumbits: int = 0, 38 | ) -> npt.NDArray: 39 | """ 40 | Vectorized version of :meth:`round_float`. 41 | 42 | Round inputs to the given :py:class:`FormatInfo`, given rounding mode and 43 | saturation flag 44 | 45 | Input NaNs will convert to NaNs in the target, not necessarily preserving payload. 46 | An input Infinity will convert to the largest float if :paramref:`sat`, 47 | otherwise to an Inf, if present, otherwise to a NaN. 48 | Negative zero will be returned if the format has negative zero, otherwise zero. 49 | 50 | Args: 51 | fi (FormatInfo): Describes the target format 52 | v (float array): Input values to be rounded 53 | rnd (RoundMode): Rounding mode to use 54 | sat (bool): Saturation flag: if True, round overflowed values to `fi.max` 55 | srbits (int array): Bits to use for stochastic rounding if rnd == Stochastic. 56 | srnumbits (int): How many bits are in srbits. Implies srbits < 2**srnumbits. 57 | 58 | Returns: 59 | An array of floats which is a subset of the format's value set. 60 | 61 | Raises: 62 | ValueError: The target format cannot represent an input 63 | (e.g. converting a `NaN`, or an `Inf` when the target has no 64 | `NaN` or `Inf`, and :paramref:`sat` is false) 65 | """ 66 | xp = array_api_compat.array_namespace(v, srbits) 67 | 68 | # Until https://github.com/data-apis/array-api/issues/807 69 | xp_where = lambda a, t, f: xp.where(a, xp.asarray(t), xp.asarray(f)) 70 | xp_maximum = lambda a, b: xp.maximum(xp.asarray(a), xp.asarray(b)) 71 | 72 | p = fi.precision 73 | bias = fi.expBias 74 | 75 | is_negative = xp.signbit(v) & fi.is_signed 76 | absv = xp_where(is_negative, -v, v) 77 | 78 | finite_nonzero = ~(xp.isnan(v) | xp.isinf(v) | (v == 0)) 79 | 80 | # Place 1.0 where finite_nonzero is False, to avoid log of {0,inf,nan} 81 | absv_masked = xp_where(finite_nonzero, absv, 1.0) 82 | 83 | int_type = xp.int64 if fi.k > 8 or srnumbits > 8 else xp.int16 84 | 85 | def to_int(x: npt.NDArray) -> npt.NDArray: 86 | return xp.astype(x, int_type) 87 | 88 | def to_float(x: npt.NDArray) -> npt.NDArray: 89 | return xp.astype(x, v.dtype) 90 | 91 | expval = to_int(xp.floor(xp.log2(absv_masked))) 92 | 93 | if fi.has_subnormals: 94 | expval = xp_maximum(expval, 1 - bias) 95 | 96 | expval = expval - p + 1 97 | fsignificand = _ldexp(absv_masked, -expval) 98 | 99 | floorfsignificand = xp.floor(fsignificand) 100 | isignificand = to_int(floorfsignificand) 101 | delta = fsignificand - floorfsignificand 102 | 103 | if fi.precision > 1: 104 | code_is_odd = _isodd(isignificand) 105 | else: 106 | code_is_odd = (isignificand != 0) & _isodd(expval + bias) 107 | 108 | match rnd: 109 | case RoundMode.TowardZero: 110 | should_round_away = xp.zeros_like(delta, dtype=xp.bool) 111 | 112 | case RoundMode.TowardPositive: 113 | should_round_away = ~is_negative & (delta > 0) 114 | 115 | case RoundMode.TowardNegative: 116 | should_round_away = is_negative & (delta > 0) 117 | 118 | case RoundMode.TiesToAway: 119 | should_round_away = delta >= 0.5 120 | 121 | case RoundMode.TiesToEven: 122 | should_round_away = (delta > 0.5) | ((delta == 0.5) & code_is_odd) 123 | 124 | case RoundMode.Stochastic: 125 | assert srbits is not None 126 | ## RTNE delta to srbits 127 | d = delta * 2.0 ** float(srnumbits) 128 | floord = to_int(xp.floor(d)) 129 | dd = d - xp.floor(d) 130 | should_round_away_tne = (dd > 0.5) | ((dd == 0.5) & _isodd(floord)) 131 | drnd = floord + xp.astype(should_round_away_tne, floord.dtype) 132 | 133 | should_round_away = drnd + srbits >= int(2.0 ** float(srnumbits)) 134 | 135 | case RoundMode.StochasticOdd: 136 | assert srbits is not None 137 | ## RTNO delta to srbits 138 | d = delta * 2.0 ** float(srnumbits) 139 | floord = to_int(xp.floor(d)) 140 | dd = d - xp.floor(d) 141 | should_round_away_tno = (dd > 0.5) | ((dd == 0.5) & ~_isodd(floord)) 142 | drnd = floord + xp.astype(should_round_away_tno, floord.dtype) 143 | 144 | should_round_away = drnd + srbits >= int(2.0 ** float(srnumbits)) 145 | 146 | case RoundMode.StochasticFast: 147 | assert srbits is not None 148 | should_round_away = ( 149 | delta + to_float(2 * srbits + 1) * 2.0 ** -float(1 + srnumbits) >= 1.0 150 | ) 151 | 152 | case RoundMode.StochasticFastest: 153 | assert srbits is not None 154 | should_round_away = delta + to_float(srbits) * 2.0**-srnumbits >= 1.0 155 | 156 | isignificand = xp_where(should_round_away, isignificand + 1, isignificand) 157 | 158 | fresult = _ldexp(to_float(isignificand), expval) 159 | 160 | result = xp_where(finite_nonzero, fresult, absv) 161 | 162 | amax = xp_where(is_negative, -fi.min, fi.max) 163 | 164 | if sat: 165 | result = xp_where(result > amax, amax, result) 166 | else: 167 | match rnd: 168 | case RoundMode.TowardNegative: 169 | put_amax_at = (result > amax) & ~is_negative 170 | case RoundMode.TowardPositive: 171 | put_amax_at = (result > amax) & is_negative 172 | case RoundMode.TowardZero: 173 | put_amax_at = result > amax 174 | case _: 175 | put_amax_at = xp.zeros_like(result, dtype=xp.bool) 176 | 177 | result = xp_where(finite_nonzero & put_amax_at, amax, result) 178 | 179 | # Now anything larger than amax goes to infinity or NaN 180 | if fi.has_infs: 181 | result = xp_where(result > amax, xp.inf, result) 182 | elif fi.num_nans > 0: 183 | result = xp_where(result > amax, xp.nan, result) 184 | else: 185 | if xp.any(result > amax): 186 | raise ValueError(f"No Infs or NaNs in format {fi}, and sat=False") 187 | 188 | result = xp_where(is_negative, -result, result) 189 | 190 | # Make negative zeros negative if has_nz, else make them not negative. 191 | if fi.has_nz: 192 | result = xp_where((result == 0) & is_negative, -0.0, result) 193 | else: 194 | result = xp_where(result == 0, 0.0, result) 195 | 196 | return result 197 | -------------------------------------------------------------------------------- /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 | 6 | 7 | class RoundMode(Enum): 8 | """ 9 | Enum for IEEE-754 rounding modes. 10 | 11 | Result :math:`r` is obtained from input :math:`v` depending on rounding mode as follows 12 | 13 | Notes on stochastic rounding: 14 | 15 | StochasticFast implements a stochastic rounding scheme that is unbiased in 16 | infinite precision, but biased when the quantity to be rounded is computed to 17 | a finite precision. 18 | 19 | StochasticFastest implements a stochastic rounding scheme that is biased 20 | (the rounded value is on average farther from zero than the true value). 21 | 22 | With a lot of SRbits (say 8 or more), these biases are negligible, and there 23 | may be some efficiency advantage in using StochasticFast or StochasticFastest. 24 | 25 | """ 26 | 27 | TowardZero = 1 #: Return the largest :math:`r` such that :math:`|r| \le |v|` 28 | TowardNegative = 2 #: Return the largest :math:`r` such that :math:`r \le v` 29 | TowardPositive = 3 #: Return the smallest :math:`r` such that :math:`r \ge v` 30 | TiesToEven = 4 #: Round to nearest, ties to even 31 | TiesToAway = 5 #: Round to nearest, ties away from zero 32 | Stochastic = 6 #: Stochastic rounding, RTNE before comparison 33 | StochasticOdd = 7 #: Stochastic rounding, RTNO before comparison 34 | StochasticFast = 8 #: Stochastic rounding - faster, but biased 35 | StochasticFastest = 9 #: Stochastic rounding - even faster, but more biased 36 | 37 | 38 | class FloatClass(Enum): 39 | """ 40 | Enum for the classification of a FloatValue. 41 | """ 42 | 43 | NORMAL = 1 #: A positive or negative normalized non-zero value 44 | SUBNORMAL = 2 #: A positive or negative subnormal value 45 | ZERO = 3 #: A positive or negative zero value 46 | INFINITE = 4 #: A positive or negative infinity (+/-Inf) 47 | NAN = 5 #: Not a Number (NaN) 48 | 49 | 50 | @dataclass 51 | class FloatValue: 52 | """ 53 | A floating-point value decoded in great detail. 54 | """ 55 | 56 | code: int #: Integer code point 57 | 58 | #: Value. Assumed to be exactly round-trippable to python float. 59 | #: This is true for all <64bit formats known in 2023. 60 | fval: float 61 | 62 | exp: int #: Raw exponent without bias 63 | expval: int #: Exponent, bias subtracted 64 | significand: int #: Significand as an integer 65 | fsignificand: float #: Significand as a float in the range [0,2) 66 | signbit: int #: Sign bit: 1 => negative, 0 => positive 67 | fclass: FloatClass #: See FloatClass 68 | 69 | 70 | @dataclass 71 | class FormatInfo: 72 | """ 73 | Class describing a floating-point format, parametrized 74 | by width, precision, and special value encoding rules. 75 | 76 | """ 77 | 78 | #: Short name for the format, e.g. binary32, bfloat16 79 | name: str 80 | 81 | #: Number of bits in the format 82 | k: int 83 | 84 | #: Number of significand bits (including implicit leading bit) 85 | precision: int 86 | 87 | #: Largest exponent, emax, which shall equal floor(log_2(maxFinite)) 88 | emax: int 89 | 90 | #: Set if format encodes -0 at (sgn=1,exp=0,significand=0). 91 | #: If False, that encoding decodes to a NaN labelled NaN_0 92 | has_nz: bool 93 | 94 | #: Set if format includes +/- Infinity. 95 | #: If set, the non-nan value with the highest encoding for each sign (s) 96 | #: is replaced by (s)Inf. 97 | has_infs: bool 98 | 99 | #: Number of NaNs that are encoded in the highest encodings for each sign 100 | num_high_nans: int 101 | 102 | #: Set if format encodes subnormals 103 | has_subnormals: bool 104 | 105 | #: Set if the format has a sign bit 106 | is_signed: bool 107 | 108 | #: Set if the format uses two's complement encoding for the significand 109 | is_twos_complement: bool 110 | 111 | #: ## Derived values 112 | 113 | @property 114 | def tSignificandBits(self) -> int: 115 | """The number of trailing significand bits, t""" 116 | return self.precision - 1 117 | 118 | @property 119 | def expBits(self) -> int: 120 | """The number of exponent bits, w""" 121 | return self.k - self.precision + (0 if self.is_signed else 1) 122 | 123 | @property 124 | def signBits(self) -> int: 125 | """The number of sign bits, s""" 126 | return 1 if self.is_signed else 0 127 | 128 | @property 129 | def expBias(self) -> int: 130 | """The exponent bias derived from (p,emax) 131 | 132 | This is the bias that should be applied so that 133 | :math:`floor(log_2(maxFinite)) = emax` 134 | """ 135 | # Calculate whether all of the all-bits-one-exponent values contain specials. 136 | # If so, emax will be obtained for exponent value 2^w-2, otherwise it is 2^w-1 137 | t = self.tSignificandBits 138 | num_posinfs = 1 if self.has_infs else 0 139 | all_bits_one_full = (self.num_high_nans + num_posinfs == 2**t) or ( 140 | self.expBits == 0 and self.has_infs 141 | ) 142 | 143 | # Compute exponent bias. 144 | exp_for_emax = 2**self.expBits - (2 if all_bits_one_full else 1) 145 | return exp_for_emax - self.emax 146 | 147 | # numpy finfo properties 148 | @property 149 | def bits(self) -> int: 150 | """ 151 | The number of bits occupied by the type. 152 | """ 153 | return self.k 154 | 155 | # @property 156 | # def dtype(self) -> np.dtype: 157 | # """ 158 | # Returns the dtype for which `finfo` returns information. For complex 159 | # input, the returned dtype is the associated ``float*`` dtype for its 160 | # real and complex components. 161 | # """ 162 | 163 | @property 164 | def eps(self) -> float: 165 | """ 166 | The difference between 1.0 and the smallest representable float 167 | larger than 1.0. For example, for 64-bit binary floats in the IEEE-754 168 | standard, ``eps = 2**-52``, approximately 2.22e-16. 169 | """ 170 | # TODO: Check if 1.0 is subnormal for any reasonable format, e.g. p3109(7)? 171 | return 2**self.machep 172 | 173 | @property 174 | def epsneg(self) -> float: 175 | """ 176 | The difference between 1.0 and the largest representable float 177 | less than 1.0. For example, for 64-bit binary floats in the IEEE-754 178 | standard, ``epsneg = 2**-53``, approximately 1.11e-16. 179 | """ 180 | return self.eps / 2 181 | 182 | @property 183 | def iexp(self) -> int: 184 | """ 185 | The number of bits in the exponent portion of the floating point 186 | representation. 187 | """ 188 | return self.expBits 189 | 190 | @property 191 | def machep(self) -> int: 192 | """ 193 | The exponent that yields `eps`. 194 | """ 195 | return -self.tSignificandBits 196 | 197 | @property 198 | def max(self) -> float: 199 | """ 200 | The largest representable number. 201 | """ 202 | num_posinfs = 1 if self.has_infs else 0 203 | num_non_finites = self.num_high_nans + num_posinfs 204 | if num_non_finites == 2**self.tSignificandBits: 205 | # All-bits-one exponent field is full, value is in the 206 | # binade below, so significand is 0xFFF..F 207 | isig = 2**self.tSignificandBits - 1 208 | else: 209 | # All-bits-one exponent field is not full, value is in the 210 | # final binade, so significand is 0xFFF..F - num_non_finites 211 | isig = 2**self.tSignificandBits - 1 - num_non_finites 212 | 213 | if self.is_all_subnormal: 214 | return 2**self.emax * (isig * 2 ** (1 - self.tSignificandBits)) 215 | else: 216 | return 2**self.emax * (1.0 + isig * 2**-self.tSignificandBits) 217 | 218 | @property 219 | def maxexp(self) -> int: 220 | """ 221 | The smallest positive power of the base (2) that causes overflow. 222 | """ 223 | return self.emax + 1 224 | 225 | @property 226 | def min(self) -> float: 227 | """ 228 | The smallest representable number, typically ``-max``. 229 | """ 230 | if self.is_signed: 231 | if not self.is_twos_complement: 232 | return -self.max 233 | else: 234 | assert not self.has_infs and self.num_high_nans == 0 and not self.has_nz 235 | return -(2.0 ** (self.emax + 1)) 236 | elif self.has_zero: 237 | return 0.0 238 | else: 239 | return 2**-self.expBias 240 | 241 | @property 242 | def num_nans(self) -> int: 243 | """ 244 | The number of code points which decode to NaN 245 | """ 246 | if not self.is_signed: 247 | return self.num_high_nans 248 | 249 | # Signed 250 | if self.is_twos_complement: 251 | assert not self.has_infs and self.num_high_nans == 0 and not self.has_nz 252 | return 0 253 | 254 | return (0 if self.has_nz else 1) + 2 * self.num_high_nans 255 | 256 | @property 257 | def code_of_nan(self) -> int: 258 | """ 259 | Return a codepoint for a NaN 260 | """ 261 | if self.num_high_nans > 0: 262 | return 2 ** (self.k) - 1 263 | if not self.has_nz: 264 | return 2 ** (self.k - 1) 265 | raise ValueError(f"No NaN in {self}") 266 | 267 | @property 268 | def code_of_posinf(self) -> int: 269 | """ 270 | Return a codepoint for positive infinity 271 | """ 272 | if not self.has_infs: 273 | raise ValueError(f"No Inf in {self}") 274 | 275 | return 2 ** (self.k - 1) - 1 - self.num_high_nans 276 | 277 | @property 278 | def code_of_neginf(self) -> int: 279 | """ 280 | Return a codepoint for negative infinity 281 | """ 282 | if not self.has_infs: 283 | raise ValueError(f"No Inf in {self}") 284 | 285 | return 2**self.k - 1 - self.num_high_nans 286 | 287 | @property 288 | def code_of_zero(self) -> int: 289 | """ 290 | Return a codepoint for (non-negative) zero 291 | """ 292 | assert self.has_zero 293 | return 0 294 | 295 | @property 296 | def has_zero(self) -> bool: 297 | """ 298 | Does the format have zero? 299 | 300 | This is false if the mantissa is 0 width and we don't have subnormals - 301 | essentially the mantissa is always decoded as 1. 302 | If we have subnormals, the only subnormal is zero, and the mantissa is 303 | always decoded as 0. 304 | """ 305 | return self.precision > 1 or self.has_subnormals 306 | 307 | @property 308 | def code_of_negzero(self) -> int: 309 | """ 310 | Return a codepoint for negative zero 311 | """ 312 | if not self.has_nz: 313 | raise ValueError(f"No negative zero in {self}") 314 | 315 | return 2 ** (self.k - 1) 316 | 317 | @property 318 | def code_of_max(self) -> int: 319 | """ 320 | Return a codepoint for fi.max 321 | """ 322 | return 2 ** (self.k - self.signBits) - self.num_high_nans - self.has_infs - 1 323 | 324 | @property 325 | def code_of_min(self) -> int: 326 | """ 327 | Return a codepoint for fi.min 328 | """ 329 | if self.is_signed and not self.is_twos_complement: 330 | return 2**self.k - self.num_high_nans - self.has_infs - 1 331 | elif self.is_signed and self.is_twos_complement: 332 | return 2 ** (self.k - 1) 333 | else: 334 | return 0 # codepoint of smallest value, whether 0 or 2^-expBias 335 | 336 | # @property 337 | # def minexp(self) -> int: 338 | # """ 339 | # The most negative power of the base (2) consistent with there 340 | # being no leading 0's in the mantissa. 341 | # """ 342 | 343 | # @property 344 | # def negep(self) -> int: 345 | # """ 346 | # The exponent that yields `epsneg`. 347 | # """ 348 | 349 | # @property 350 | # def nexp(self) -> int: 351 | # """ 352 | # The number of bits in the exponent including its sign and bias. 353 | # """ 354 | 355 | # @property 356 | # def nmant(self) -> int: 357 | # """ 358 | # The number of bits in the mantissa. 359 | # """ 360 | 361 | # @property 362 | # def precision(self) -> int: 363 | # """ 364 | # The approximate number of decimal digits to which this kind of 365 | # float is precise. 366 | # """ 367 | 368 | # @property 369 | # def resolution(self) -> float: 370 | # """ 371 | # The approximate decimal resolution of this type, i.e., 372 | # ``10**-precision``. 373 | # """ 374 | 375 | # @property 376 | # def tiny(self) -> float: 377 | # """ 378 | # An alias for `smallest_normal`, kept for backwards compatibility. 379 | # """ 380 | 381 | @property 382 | def smallest_normal(self) -> float: 383 | """ 384 | The smallest positive floating point number with 1 as leading bit in 385 | the significand following IEEE-754. 386 | """ 387 | if self.has_subnormals: 388 | return 2 ** (1 - self.expBias) 389 | elif self.has_zero: 390 | return 2**-self.expBias + 2 ** (-self.expBias - self.tSignificandBits) 391 | else: 392 | return 2**-self.expBias 393 | 394 | @property 395 | def smallest_subnormal(self) -> float: 396 | """ 397 | The smallest positive floating point number with 0 as leading bit in 398 | the significand following IEEE-754. 399 | """ 400 | assert self.has_subnormals, "not implemented" 401 | return 2 ** -(self.expBias + self.tSignificandBits - 1) 402 | 403 | @property 404 | def smallest(self) -> float: 405 | """ 406 | The smallest positive floating point number. 407 | """ 408 | if self.has_subnormals: 409 | return self.smallest_subnormal 410 | else: 411 | return self.smallest_normal 412 | 413 | @property 414 | def is_all_subnormal(self) -> bool: 415 | """ 416 | Are all encoded values subnormal? 417 | """ 418 | return (self.expBits == 0) and self.has_subnormals 419 | 420 | @property 421 | def __name__(self) -> str: 422 | return self.name 423 | 424 | def __str__(self) -> str: 425 | return f"{self.name}" 426 | -------------------------------------------------------------------------------- /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", all_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 | a = np.random.rand(23, 1, 34) - 0.5 25 | a = xp.asarray(a) 26 | 27 | srnumbits = 32 28 | srbits = np.random.randint(0, 2**srnumbits, a.shape) 29 | srbits = xp.asarray(srbits) 30 | 31 | round_ndarray(fi, a, rnd, sat, srbits=srbits, srnumbits=srnumbits) 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 | -------------------------------------------------------------------------------- /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, 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 get_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 = get_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 = get_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 = get_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 = get_method(method, fi) 81 | 82 | assert dec(0x01) == 2.0**-62 83 | assert dec(0x40) == 2.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 = get_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 = get_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 = get_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 not fi.has_infs 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 = get_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 not fi.has_infs 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 = get_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 not fi.has_infs 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 = get_method(method, fi) 183 | fclass = lambda code: decode_float(fi, code).fclass 184 | assert fi.expBias == 127 185 | assert fi.max == 2.0**127 186 | assert fi.smallest == 2.0**-127 187 | assert not fi.has_infs 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 = get_method(method, fi) 203 | 204 | assert fi.max == 1.0 + 63.0 / 64 205 | assert fi.smallest == 2.0**-6 206 | assert not fi.has_infs 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 | assert fi.code_of_nan == 0x80 220 | assert fi.code_of_zero == 0x00 221 | assert fi.code_of_posinf == 0x7F 222 | assert fi.code_of_neginf == 0xFF 223 | 224 | 225 | @pytest.mark.parametrize("k,p", [(8, 3), (8, 1), (6, 1), (6, 5), (3, 1), (3, 2), (11, 3)]) 226 | def test_p3109_specials(k: int, p: int) -> None: 227 | fi = format_info_p3109(k, p) 228 | assert fi.code_of_nan == 2 ** (k - 1) 229 | assert fi.code_of_zero == 0 230 | assert fi.code_of_posinf == 2 ** (k - 1) - 1 231 | assert fi.code_of_neginf == 2**k - 1 232 | 233 | 234 | @pytest.mark.parametrize("fi", all_formats) 235 | @pytest.mark.parametrize("method", methods) 236 | def test_specials_decode(method: str, fi: FormatInfo) -> None: 237 | dec = get_method(method, fi) 238 | 239 | if fi.has_zero: 240 | assert dec(fi.code_of_zero) == 0 241 | 242 | if fi.num_nans > 0: 243 | assert np.isnan(dec(fi.code_of_nan)) 244 | 245 | if fi.has_infs: 246 | assert dec(fi.code_of_posinf) == np.inf 247 | assert dec(fi.code_of_neginf) == -np.inf 248 | 249 | assert dec(fi.code_of_max) == fi.max 250 | assert dec(fi.code_of_min) == fi.min 251 | 252 | if fi.has_zero: 253 | assert dec(1) == fi.smallest 254 | else: 255 | assert dec(0) == fi.smallest 256 | 257 | 258 | @pytest.mark.parametrize( 259 | "fmt,npfmt,int_dtype", 260 | [ 261 | (format_info_binary16, np.float16, np.uint16), 262 | (format_info_bfloat16, ml_dtypes.bfloat16, np.uint16), 263 | (format_info_ocp_e4m3, ml_dtypes.float8_e4m3fn, np.uint8), 264 | ], 265 | ) 266 | def test_consistent_decodes_all_values( 267 | fmt: FormatInfo, npfmt: np.dtype, int_dtype: np.dtype 268 | ) -> None: 269 | npivals = np.arange( 270 | np.iinfo(int_dtype).min, int(np.iinfo(int_dtype).max) + 1, dtype=int_dtype 271 | ) 272 | 273 | with np.errstate(invalid="ignore"): 274 | # Warning here when converting bfloat16 NaNs to float64 275 | npfvals = npivals.view(dtype=npfmt).astype(np.float64) 276 | 277 | # Scalar version 278 | for i, npfval in zip(npivals, npfvals): 279 | val = decode_float(fmt, int(i)) 280 | np.testing.assert_equal(val.fval, npfval) 281 | 282 | # Vector version 283 | vals = decode_ndarray(fmt, npivals) 284 | np.testing.assert_equal(vals, npfvals) 285 | 286 | 287 | @pytest.mark.parametrize("v", [-1, 0x10000]) 288 | def test_except(v: int) -> None: 289 | with pytest.raises(ValueError): 290 | decode_float(format_info_binary16, v) 291 | 292 | 293 | @pytest.mark.parametrize("fi", [fi for fi in all_formats if fi.bits <= 8]) 294 | def test_dense(fi: FormatInfo) -> None: 295 | fvs = [decode_float(fi, i) for i in range(0, 2**fi.bits)] 296 | 297 | vals = np.array([fv.fval for fv in fvs]) 298 | 299 | assert np.min(vals[np.isfinite(vals)]) == fi.min 300 | assert np.max(vals[np.isfinite(vals)]) == fi.max 301 | assert np.min(vals[np.isfinite(vals) & (vals > 0)]) == fi.smallest 302 | 303 | if fi.has_subnormals: 304 | vals_subnormal = np.array( 305 | [fv.fval for fv in fvs if fv.fclass == FloatClass.SUBNORMAL and fv.fval > 0] 306 | ) 307 | if len(vals_subnormal): 308 | # In some formats, zero is the only "subnormal" 309 | assert np.min(vals_subnormal) == fi.smallest_subnormal 310 | -------------------------------------------------------------------------------- /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", all_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", all_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.has_infs 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.has_infs 63 | else fi.code_of_nan if fi.num_nans > 0 else fi.code_of_min 64 | ) 65 | -------------------------------------------------------------------------------- /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**-62 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 | -------------------------------------------------------------------------------- /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 | jax.config.update("jax_enable_x64", True) 14 | 15 | 16 | def test_jax() -> None: 17 | """ 18 | Test that JAX JIT produces correct output 19 | """ 20 | a = np.random.randn(1024) 21 | 22 | a8 = a.astype(ml_dtypes.float8_e5m2).astype(jnp.float64) 23 | 24 | fi = format_info_ocp_e5m2 25 | j8 = gfloat.round_ndarray(fi, jnp.array(a)) # type: ignore [arg-type] 26 | 27 | np.testing.assert_equal(a8, j8) 28 | 29 | jax_round_array = jax.jit(lambda x: gfloat.round_ndarray(fi, x)) 30 | j8i = jax_round_array(a) 31 | 32 | np.testing.assert_equal(a8, j8i) 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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", all_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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------