ulpwise 0.1.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ulpwise-0.1.2 → ulpwise-0.3.0}/.github/workflows/release.yml +19 -1
- ulpwise-0.3.0/CHANGELOG.md +80 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/Cargo.lock +1 -1
- {ulpwise-0.1.2 → ulpwise-0.3.0}/Cargo.toml +1 -1
- ulpwise-0.3.0/PKG-INFO +334 -0
- ulpwise-0.3.0/README.md +305 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/pyproject.toml +2 -1
- ulpwise-0.3.0/python/ulpwise/__init__.py +340 -0
- ulpwise-0.3.0/python/ulpwise/__main__.py +116 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/python/ulpwise/corpus/__init__.py +40 -1
- {ulpwise-0.1.2 → ulpwise-0.3.0}/python/ulpwise/corpus/cases.json +210 -2
- {ulpwise-0.1.2 → ulpwise-0.3.0}/python/ulpwise/plugin.py +4 -3
- ulpwise-0.3.0/python/ulpwise/scan.py +622 -0
- ulpwise-0.3.0/python/ulpwise/survey.py +692 -0
- ulpwise-0.3.0/studies/accuracy-survey-2026-09/README.md +143 -0
- ulpwise-0.3.0/studies/accuracy-survey-2026-09/results.csv +319 -0
- ulpwise-0.3.0/studies/accuracy-survey-2026-09/results.md +139 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/tests/test_core.py +95 -5
- ulpwise-0.3.0/tests/test_corpus.py +77 -0
- ulpwise-0.3.0/tests/test_scan.py +191 -0
- ulpwise-0.3.0/tests/test_survey.py +53 -0
- ulpwise-0.1.2/CHANGELOG.md +0 -28
- ulpwise-0.1.2/PKG-INFO +0 -203
- ulpwise-0.1.2/README.md +0 -177
- ulpwise-0.1.2/python/ulpwise/__init__.py +0 -184
- ulpwise-0.1.2/python/ulpwise/__main__.py +0 -69
- ulpwise-0.1.2/tests/test_corpus.py +0 -41
- {ulpwise-0.1.2 → ulpwise-0.3.0}/.github/workflows/ci.yml +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/.gitignore +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/CONTRIBUTING.md +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/LICENSE-APACHE +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/LICENSE-MIT +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/examples/sqrt_knife_values.py +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/examples/torch_sqrt_conformance.py +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/edge.rs +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/exact.rs +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/knife.rs +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/lib.rs +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/py.rs +0 -0
- {ulpwise-0.1.2 → ulpwise-0.3.0}/src/ulp.rs +0 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# uploads them to PyPI through trusted publishing (no API token stored anywhere):
|
|
3
3
|
# the PyPI project "ulpwise" must list this repository, this workflow file name and the
|
|
4
4
|
# "pypi" environment as its publisher. A manual run only builds and checks the wheels,
|
|
5
|
-
# unless its "publish" input is set, in which case it also uploads
|
|
5
|
+
# unless its "publish" input is set, in which case it also uploads, tags the commit and
|
|
6
|
+
# creates the GitHub release with the CHANGELOG section as notes and the wheels attached.
|
|
6
7
|
name: release
|
|
7
8
|
|
|
8
9
|
on:
|
|
@@ -119,3 +120,20 @@ jobs:
|
|
|
119
120
|
run: |
|
|
120
121
|
git tag "v${{ steps.version.outputs.version }}"
|
|
121
122
|
git push origin "v${{ steps.version.outputs.version }}"
|
|
123
|
+
- name: GitHub release with the CHANGELOG section as notes and the wheels attached
|
|
124
|
+
env:
|
|
125
|
+
GH_TOKEN: ${{ github.token }}
|
|
126
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
127
|
+
run: |
|
|
128
|
+
python3 - > notes.md <<'EOF'
|
|
129
|
+
import os, re
|
|
130
|
+
version = os.environ["VERSION"]
|
|
131
|
+
text = open("CHANGELOG.md", encoding="utf-8").read()
|
|
132
|
+
m = re.search(r"^## " + re.escape(version) + r"\b.*?\n(.*?)(?=^## |\Z)", text, re.M | re.S)
|
|
133
|
+
print(m.group(1).strip() if m else f"ulpwise {version}, see CHANGELOG.md")
|
|
134
|
+
EOF
|
|
135
|
+
if gh release view "v$VERSION" > /dev/null 2>&1; then
|
|
136
|
+
gh release upload "v$VERSION" dist/* --clobber
|
|
137
|
+
else
|
|
138
|
+
gh release create "v$VERSION" dist/* --title "ulpwise $VERSION" --notes-file notes.md
|
|
139
|
+
fi
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (2026-09-26)
|
|
4
|
+
|
|
5
|
+
- `ulpwise scan`: static scan of a repository (a directory, a GitHub URL or `owner/repo`, cloned
|
|
6
|
+
with depth 1) for the floating point patterns behind the corpus bugs. Twelve rules with severity,
|
|
7
|
+
reason, replacement and upstream example: `exp-of-square`, `sin-of-pi-times`, `softplus-by-hand`,
|
|
8
|
+
`logsumexp-by-hand`, `hypot-by-hand`, `sqrt-of-difference`, `one-minus-cos`, `log1p-by-hand`,
|
|
9
|
+
`expm1-by-hand`, `atan-of-quotient`, `small-angle-division`, `acos-for-angle`. Findings carry
|
|
10
|
+
file, line, function and the source line; the report ends with the functions that do the most
|
|
11
|
+
elementary math. `--report` writes Markdown, `--rules` filters, `--fail-on` gates CI,
|
|
12
|
+
`--include-tests` widens the walk. On kornia main the four `small-angle-division` and
|
|
13
|
+
`one-minus-cos` lines are the ones kornia #4897 fixes.
|
|
14
|
+
- `ulpwise scan --run`: imports the math-heavy module level functions and calls them on the same
|
|
15
|
+
grid in float32 and float64, reporting the largest error in ulps of the largest output and the
|
|
16
|
+
worst elementwise ulp distance with its input. Static methods run, instance methods and other
|
|
17
|
+
skips carry their reason. `--run-limit` bounds it, `--run-installed` imports the installed package
|
|
18
|
+
instead of the scanned tree.
|
|
19
|
+
- `ulp_distance`, `ulp_distances`, `ordered`, `max_ulp` and `assert_max_ulp` take `f16` and
|
|
20
|
+
`bf16`, and `flatten` reads the dtype off float16 and bfloat16 numpy arrays and torch tensors.
|
|
21
|
+
Inputs that are not representable are rounded to nearest even first, so a bfloat16 tensor can be
|
|
22
|
+
measured against a float64 reference; without an explicit dtype the less precise of the two
|
|
23
|
+
inputs decides. Cross-checked against the numpy int16 view for float16 and the torch view for
|
|
24
|
+
bfloat16.
|
|
25
|
+
- `special("f16")` and `special("bf16")`: the same 29 named edge values as f32 and f64, so
|
|
26
|
+
`edge_values`, the `edge_f16` and `edge_bf16` pytest fixtures and `ulpwise special f16` work.
|
|
27
|
+
Every value satisfies the same checks as the f32 and f64 tables, run through numpy for float16
|
|
28
|
+
and torch for bfloat16.
|
|
29
|
+
- `ulpwise corpus`: runs the regression corpus against the installed packages without pytest and
|
|
30
|
+
prints one line per case, present, fixed or skipped, with the installed version and the upstream
|
|
31
|
+
reference. `--repo` filters by repository or case id, `--fail-if-present` makes a present bug exit 1.
|
|
32
|
+
Any exception from a repro counts as present, like the pytest run: several corpus bugs are crashes.
|
|
33
|
+
|
|
34
|
+
## 0.2.0 (2026-09-26)
|
|
35
|
+
|
|
36
|
+
- `ulpwise survey` (`ulpwise.survey`): accuracy survey of 61 elementary and special functions of
|
|
37
|
+
torch, numpy, scipy and jax against a 200 bit mpmath reference, in ulps of the dtype, with the
|
|
38
|
+
worst input per row. For torch it also counts inputs where the vectorized kernel and the scalar
|
|
39
|
+
tail disagree and inputs that would fail the `OpInfo` reference test tolerance, default and per op
|
|
40
|
+
override, read from `op_db`. Writes `results.csv` and `results.md`. Optional extra
|
|
41
|
+
`ulpwise[survey]` pulls in mpmath.
|
|
42
|
+
- `studies/accuracy-survey-2026-09`: the first run and its reading notes.
|
|
43
|
+
- Corpus: `max_ulp` check type, and five open cases with the complete patch attached to the issue:
|
|
44
|
+
pytorch #198448 (`torch.sqrt` float64 rounding), pytorch #198583 (Bessel and Airy `p1evl` leading
|
|
45
|
+
1), kornia #4838 and #4897 (small angle series), torchvision #9676 (rotated box clamp). Open cases
|
|
46
|
+
carry `issue`, `pr: null` and `fixed_in_release: null` and stay expected failures until a release
|
|
47
|
+
contains the fix.
|
|
48
|
+
- Corpus: three more open cases, pytorch #198663 (`polygamma(1, x)`: float64 series truncation and
|
|
49
|
+
float32 reflection argument) and pytorch #198664 (`erfcx` negative branch, `exp` at the rounded
|
|
50
|
+
square), both with the complete patch attached to the issue.
|
|
51
|
+
- Corpus: kornia #4768, `ellipse_to_laf` described the wrong ellipse whenever `b != 0`.
|
|
52
|
+
- Corpus: ultralytics #26330, OBB datasets with plain box labels are rejected at load time; the repro
|
|
53
|
+
writes a one-image dataset to a temporary directory, so it needs ultralytics but no weights.
|
|
54
|
+
|
|
55
|
+
## 0.1.2 (2026-09-24)
|
|
56
|
+
|
|
57
|
+
- `ulpwise` console script, so `uvx ulpwise midpoint sqrt 0.85`, `pipx run ulpwise ...` and a plain
|
|
58
|
+
`ulpwise ...` inside a virtualenv work without `python -m`.
|
|
59
|
+
|
|
60
|
+
## 0.1.1 (2026-09-24)
|
|
61
|
+
|
|
62
|
+
No code changes. The AI disclosure in the README was shortened and the package was republished
|
|
63
|
+
from a repository with a fresh history.
|
|
64
|
+
|
|
65
|
+
## 0.1.0 (2026-09-24)
|
|
66
|
+
|
|
67
|
+
First release.
|
|
68
|
+
|
|
69
|
+
- Rust core: ordered float views and ulp distances (`ulp`), named edge values computed from the
|
|
70
|
+
format (`edge`), exact rounding oracles for `sqrt`, reciprocal and division with correctly rounded
|
|
71
|
+
results that do not depend on the platform libm, f64 referenced midpoint reports for 15 more
|
|
72
|
+
`f32` functions (`exact`), and knife-edge scans (`knife`).
|
|
73
|
+
- Python package built with maturin: `ulp_distance`, `assert_max_ulp` and `max_ulp` for floats,
|
|
74
|
+
lists, numpy arrays and torch tensors, `special`, `neighbours`, `binade_edges`, `all_floats`,
|
|
75
|
+
`midpoint`, `knife_edges`, `sqrt_cr`, a CLI (`python -m ulpwise`) and a pytest plugin
|
|
76
|
+
(`edge_f32` / `edge_f64` parametrization, `assert_max_ulp` fixture).
|
|
77
|
+
- Regression corpus of 11 upstream bugs with runnable repros (kornia, pytorch, pytorch/rl, timm,
|
|
78
|
+
peft), run by `tests/test_corpus.py` against whatever is installed.
|
|
79
|
+
- `examples/torch_sqrt_conformance.py`: measures how often a platform's `torch.sqrt` and
|
|
80
|
+
`numpy.sqrt` disagree with correct rounding at knife-edge inputs.
|
ulpwise-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ulpwise
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Framework :: Pytest
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Topic :: Software Development :: Testing
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
12
|
+
Requires-Dist: numpy ; extra == 'survey'
|
|
13
|
+
Requires-Dist: mpmath>=1.3 ; extra == 'survey'
|
|
14
|
+
Requires-Dist: pytest>=7 ; extra == 'test'
|
|
15
|
+
Requires-Dist: numpy ; extra == 'test'
|
|
16
|
+
Provides-Extra: survey
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
License-File: LICENSE-APACHE
|
|
19
|
+
License-File: LICENSE-MIT
|
|
20
|
+
Summary: Numerical conformance testing for ML code: float edge values, exact rounding oracles, knife-edge finders and a pytest plugin
|
|
21
|
+
Keywords: floating-point,ulp,numerics,testing,pytest,pytorch,numpy
|
|
22
|
+
Author: 区梓灏 (Nicholas022400701)
|
|
23
|
+
License: MIT OR Apache-2.0
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
26
|
+
Project-URL: Issues, https://github.com/Nicholas022400701/ulpwise/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/Nicholas022400701/ulpwise
|
|
28
|
+
|
|
29
|
+
# ulpwise
|
|
30
|
+
|
|
31
|
+
Numerical conformance testing for ML code. A Rust core with a Python API and a pytest plugin.
|
|
32
|
+
|
|
33
|
+
`ulpwise` answers five questions that come up every time a numerical test goes red on one
|
|
34
|
+
platform and green on another:
|
|
35
|
+
|
|
36
|
+
1. **Which inputs break first?** Named edge values computed from the float format (the largest
|
|
37
|
+
`x` with `x * x == 0`, the first `x` whose square overflows, the value where `1 / x` overflows,
|
|
38
|
+
binade boundaries, subnormals) and every-float enumerations of a range.
|
|
39
|
+
2. **What is the right answer, exactly?** Rounding oracles that compare the exact result against
|
|
40
|
+
the candidate floats with integer arithmetic on significands, so the correctly rounded `sqrt`,
|
|
41
|
+
reciprocal and quotient, and the distance from the exact result to the rounding midpoint, do
|
|
42
|
+
not depend on any libm.
|
|
43
|
+
3. **Where do two implementations disagree?** Knife-edge scans: the inputs whose exact result sits
|
|
44
|
+
within `tol` ulp of a rounding midpoint, so that two implementations that differ by one ulp
|
|
45
|
+
return different floats. 8.4 million `f32` inputs are scanned in about 0.3 s.
|
|
46
|
+
4. **How accurate are the functions I call, and would the library's own tests notice?**
|
|
47
|
+
`ulpwise survey` measures 61 elementary and special functions of torch, numpy, scipy and jax in
|
|
48
|
+
ulps against a 200 bit mpmath reference and, for torch, checks every error against the tolerance
|
|
49
|
+
of torch's own `OpInfo` reference test, default and per op override.
|
|
50
|
+
5. **Where should I read first in a repository I do not know?** `ulpwise scan` parses every Python
|
|
51
|
+
file and reports the expressions behind the bugs in the corpus (`exp(x * x)`, `sin(pi * x)`,
|
|
52
|
+
`1 - cos(x)`, `sqrt(a * a + b * b)`, `log(1 + exp(x))`, divisions by an unguarded angle, ...)
|
|
53
|
+
with file, line and function, then lists the functions with the most elementary math.
|
|
54
|
+
|
|
55
|
+
## Why this exists
|
|
56
|
+
|
|
57
|
+
On 2026-09-23 a kornia pull request went red on 21 of 39 CI jobs after a maintainer added a test
|
|
58
|
+
around the shape `(1.0, 0.9235056042671204, 0.8528626561164856)`. In float32 the exact value of
|
|
59
|
+
`sqrt(0.8528626561164856)` lies 0.0004 ulp below the midpoint of its two float32 neighbours. The
|
|
60
|
+
macOS runners and my sandbox rounded it down, the ubuntu and windows runners rounded it up, and the
|
|
61
|
+
estimator under test took two different branches. `ulpwise` finds that input, and the 16,995 others
|
|
62
|
+
like it in `[0.5, 1)`, before CI does:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ ulpwise midpoint sqrt 0.8528626561164856 --dtype f32
|
|
66
|
+
rounded 0.9235056042671204, exact result lies above it, 3.771e-04 ulp from the rounding midpoint
|
|
67
|
+
|
|
68
|
+
$ ulpwise knife sqrt --lo 0.85 --hi 0.86 --tol 1e-3 --limit 3
|
|
69
|
+
x rounded midpoint distance (ulp) exact lies
|
|
70
|
+
0.8500027060508728 0.9219558835029602 2.521e-04 above
|
|
71
|
+
0.8500217199325562 0.9219662547111511 5.452e-04 below
|
|
72
|
+
0.8500407338142395 0.9219765067100525 5.924e-04 above
|
|
73
|
+
3 knife edge(s) with distance < 0.001 ulp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Measured with `examples/torch_sqrt_conformance.py` on one Linux x86_64 machine (AVX512, torch
|
|
77
|
+
2.14.0+cpu built with MKL, numpy 2.2.6), at the 16,996 float32 knife edges of `[0.5, 1)` with
|
|
78
|
+
`tol = 1e-3`:
|
|
79
|
+
|
|
80
|
+
| implementation | off by 1 ulp at knife edges | direction |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `numpy.sqrt` float32 | 0 / 16,996 | correctly rounded |
|
|
83
|
+
| `torch.sqrt` float32 | 7,131 / 16,996 (42.0%) | always rounded down instead of up |
|
|
84
|
+
| `torch.pow(x, 0.5)` float32 | 7,131 / 16,996 (42.0%) | same |
|
|
85
|
+
| `torch.sqrt` float64 then cast | 0 / 16,996 | correctly rounded |
|
|
86
|
+
| `torch.sqrt` float64 (20,000 f64 knife edges) | 9,996 / 20,000 (50.0%) | always down |
|
|
87
|
+
|
|
88
|
+
On random inputs the same `torch.sqrt` differs from correct rounding at 0.70% of float32 values,
|
|
89
|
+
which is why this goes unnoticed until a test happens to pin one of them. Other platforms will
|
|
90
|
+
show other numbers; the CI of this repository prints them for ubuntu, macOS and windows on every
|
|
91
|
+
run.
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
pip install ulpwise # or: uv add ulpwise
|
|
97
|
+
uvx ulpwise special f32 # run the command line tool without installing anything
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Wheels on [PyPI](https://pypi.org/project/ulpwise/) cover Linux x86_64 and aarch64, macOS arm64 and
|
|
101
|
+
x86_64, and Windows x64, for Python 3.9 or newer (abi3). To build from source you need a Rust
|
|
102
|
+
toolchain (1.86 or newer) and [maturin](https://www.maturin.rs/):
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
pip install maturin
|
|
106
|
+
pip install . # or: maturin develop --release (inside a virtualenv)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The Rust crate is usable on its own (`cargo add --git https://github.com/Nicholas022400701/ulpwise`).
|
|
110
|
+
|
|
111
|
+
## Use
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import ulpwise
|
|
115
|
+
|
|
116
|
+
# ulp distances, dtype aware: a float32 tensor is measured in float32 ulps, a bfloat16 tensor
|
|
117
|
+
# against a float64 reference in bfloat16 ulps. f64, f32, f16 and bf16.
|
|
118
|
+
ulpwise.ulp_distance(0.9235056042671204, 0.9235056638717651, "f32") # 1
|
|
119
|
+
ulpwise.ulp_distance(1.0, 1.001, "bf16") # 0, both round to 1.0
|
|
120
|
+
ulpwise.assert_max_ulp(torch_out, reference, max_ulp_=2) # floats, lists, numpy, torch
|
|
121
|
+
|
|
122
|
+
# the 29 named edge values of a dtype, f64 f32 f16 bf16
|
|
123
|
+
dict(ulpwise.special("f32"))["square_underflows_to_zero"] # 2.6469779601696886e-23, the largest x with x * x == 0
|
|
124
|
+
dict(ulpwise.special("bf16"))["square_overflows"] # 1.8446744073709552e+19, 2 ** 64
|
|
125
|
+
|
|
126
|
+
# exact placement of a result relative to its rounding midpoint
|
|
127
|
+
ulpwise.midpoint("sqrt", 0.8528626561164856, "f32") # (0.9235056042671204, 0.000377, True, False)
|
|
128
|
+
ulpwise.midpoint("div", 1.0, "f64", 3.0) # (0.3333333333333333, 0.1666..., True, False)
|
|
129
|
+
|
|
130
|
+
# knife edges of a function on a range: (x, rounded, distance_ulp, exact_above)
|
|
131
|
+
ulpwise.knife_edges("exp", 0.5, 1.0, tol_ulp=1e-3, dtype="f32", limit=100)
|
|
132
|
+
|
|
133
|
+
# a correctly rounded sqrt that does not depend on the platform
|
|
134
|
+
ulpwise.sqrt_cr(0.8528626561164856, "f32") # 0.9235056042671204
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`midpoint` and `knife_edges` accept `sqrt`, `recip` and `div` (exact integer oracle, `f32` and
|
|
138
|
+
`f64`) and `rsqrt exp exp2 expm1 log log2 log10 log1p sin cos tan atan tanh sigmoid softplus`
|
|
139
|
+
(`f32` only, `f64` libm as reference, trust the distance down to about `1e-8` ulp).
|
|
140
|
+
|
|
141
|
+
### pytest plugin
|
|
142
|
+
|
|
143
|
+
Installed automatically. A test that takes `edge_f64`, `edge_f32`, `edge_f16` or `edge_bf16` runs
|
|
144
|
+
once per named edge value, and `assert_max_ulp` is available as a fixture:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
def test_my_kernel_survives_the_edges(edge_f32, assert_max_ulp):
|
|
148
|
+
x = torch.tensor([edge_f32])
|
|
149
|
+
assert_max_ulp(my_kernel(x), reference(x), max_ulp_=1)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Failures read `test_my_kernel_survives_the_edges[square_overflows]`.
|
|
153
|
+
|
|
154
|
+
## Regression corpus
|
|
155
|
+
|
|
156
|
+
`python/ulpwise/corpus/cases.json` holds upstream bugs found by the contribution pipeline this
|
|
157
|
+
project grew out of, each with a runnable repro, the buggy behaviour quoted from the merged pull
|
|
158
|
+
request, and the check that tells the two apart. `pytest tests/test_corpus.py` runs every case
|
|
159
|
+
whose packages are installed; a case is an expected failure while the installed release is not
|
|
160
|
+
known to contain the fix, so the run tells you which bugs are present in your environment.
|
|
161
|
+
`ulpwise corpus` does the same without pytest, one line per case with the installed version and
|
|
162
|
+
`present`, `fixed` or `skipped`; `ulpwise corpus --repo pytorch --fail-if-present` is the CI gate
|
|
163
|
+
form.
|
|
164
|
+
|
|
165
|
+
| case | kind | merged |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| kornia #4683 second derivative sign in `spatial_gradient(order=2)` | sign | 2026-09-22 |
|
|
168
|
+
| kornia #4767 mixed second order kernel scale, wrong `hessian_response` determinant | scale | 2026-09-23 |
|
|
169
|
+
| kornia #4768 `ellipse_to_laf` under-tilted every ellipse with `b != 0` | geometry | 2026-09-24 |
|
|
170
|
+
| pytorch #198006 `Multinomial.entropy()` evaluated in the default dtype | dtype | 2026-09-23 |
|
|
171
|
+
| pytorch/rl #4443 `arange(0, 1, 1/n)` gives `n + 1` positions for 140 values of `n` below 2000 | rounding | 2026-09-20 |
|
|
172
|
+
| pytorch/rl #4444 `min_value or -inf` drops `min_value=0` | falsy zero | 2026-09-20 |
|
|
173
|
+
| pytorch/rl #4445 scheduler `state_dict()` contained a module object | crash | 2026-09-20 |
|
|
174
|
+
| timm #2786 Mars kept a reference to `p.grad` as the previous gradient | aliasing | 2026-09-17 |
|
|
175
|
+
| timm #2790 AdafactorBigVision clipped updates in the wrong direction | direction | 2026-09-18 |
|
|
176
|
+
| timm #2791 AdaMuon conv LR scale computed from the wrong dims | scale | 2026-09-18 |
|
|
177
|
+
| timm #2792 Kron `__setstate__` shadowed | crash | 2026-09-18 |
|
|
178
|
+
| peft #3777 pointwise Conv3d took the conv2d 1x1 shortcut | shape | 2026-09-21 |
|
|
179
|
+
| ultralytics #26330 OBB train and val on plain box labels crashed in the validator or the loss instead of at load time | crash | 2026-09-25 |
|
|
180
|
+
| pytorch #198448 `torch.sqrt` float64 not correctly rounded at 27 of 64 knife edges | rounding | open |
|
|
181
|
+
| pytorch #198583 `bessel_j0/j1/y0/y1`, `airy_ai` float64 lose up to 12 digits (`p1evl` leading 1) | digits | open |
|
|
182
|
+
| kornia #4838 `axis_angle_to_rotation_matrix` drops the `theta^2` terms below 1e-3 rad | series | open |
|
|
183
|
+
| kornia #4897 `So3.log`, the `So3` Jacobians and `Se3.exp/log` lose all digits for small angles | series | open |
|
|
184
|
+
| torchvision #9676 `clamp_bounding_boxes` collapses slightly tilted rotated boxes to a point | geometry | open |
|
|
185
|
+
| pytorch #198663 `polygamma(1, x)` float64 keeps 9 digits (series stops at `1/42`), float32 loses all for large negative `x` | truncation, rounding | open |
|
|
186
|
+
| pytorch #198664 `erfcx` off by `x*x/2` ulps for negative `x` (`exp` at the rounded square) | rounding | open |
|
|
187
|
+
|
|
188
|
+
Cases marked `open` have an issue with the complete patch attached and no merged fix yet; they are
|
|
189
|
+
expected failures until a release contains the fix (`fixed_in_release` in `cases.json`), and the
|
|
190
|
+
`max_ulp` check type measures the digits directly. The ultralytics case builds its one-image dataset
|
|
191
|
+
in a temporary directory and needs no weights; two more ultralytics fixes (#26240, #26246) are not in
|
|
192
|
+
the corpus yet because their repros need model weights or the COCO evaluator.
|
|
193
|
+
|
|
194
|
+
## Repository scan
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
ulpwise scan kornia/kornia --report kornia.md # clone with depth 1, write a Markdown report
|
|
198
|
+
ulpwise scan . --fail-on high # CI gate: exit 1 on a high severity finding
|
|
199
|
+
ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The scan is static and needs nothing installed: it parses each file with `ast`, walks every
|
|
203
|
+
function and matches twelve patterns, each with a severity, the reason it loses digits or
|
|
204
|
+
overflows, the usual replacement and, where one exists, the upstream bug it comes from.
|
|
205
|
+
|
|
206
|
+
| rule | severity | pattern |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `exp-of-square` | high | `exp(x * x)`, `exp(x ** 2)`, `(-x.pow(2)).exp()`: the rounding error of the square is multiplied by `x * x / 2` ulps (pytorch #198664) |
|
|
209
|
+
| `sin-of-pi-times` | high | `sin(pi * x)`, `cos(pi * x)`: the product is rounded before the argument reduction (pytorch #198663) |
|
|
210
|
+
| `softplus-by-hand` | high | `log(1 + exp(x))` |
|
|
211
|
+
| `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))` |
|
|
212
|
+
| `hypot-by-hand` | high | `sqrt(a * a + b * b)` |
|
|
213
|
+
| `sqrt-of-difference` | medium | `sqrt(a - b)` |
|
|
214
|
+
| `one-minus-cos` | medium | `1 - cos(x)` (kornia #4897) |
|
|
215
|
+
| `log1p-by-hand`, `expm1-by-hand` | medium | `log(1 + x)`, `exp(x) - 1` |
|
|
216
|
+
| `atan-of-quotient` | medium | `atan(y / x)` |
|
|
217
|
+
| `small-angle-division` | medium | `/ theta`, `/ theta ** 2`, `/ sin(theta)` in a function that takes `sin` or `cos` of `theta` and has no `where`, `clamp`, `eps` or series in sight (kornia #4838, #4897) |
|
|
218
|
+
| `acos-for-angle` | info | `acos`, `asin` used to recover an angle |
|
|
219
|
+
|
|
220
|
+
On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
|
|
221
|
+
`one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
|
|
222
|
+
`So3.left_jacobian` that kornia #4897 fixes, `So3.log` (kornia #4838) is under `acos-for-angle`,
|
|
223
|
+
and `Se3.exp` (also #4897) is under `one-minus-cos`. The scan puts `ellipse_to_laf` (kornia #4768)
|
|
224
|
+
on the list too, for a `sqrt` of a difference; the bug there was a different one, so that entry is
|
|
225
|
+
what the scan is: a reading list, not a verdict.
|
|
226
|
+
|
|
227
|
+
`--run` adds the dynamic half. The module level functions with the most elementary math are
|
|
228
|
+
imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
|
|
229
|
+
required argument, in float32 and in float64, torch first and numpy second, and the float32 result
|
|
230
|
+
is measured against the float64 one. Two numbers per function: `at scale`, the largest absolute
|
|
231
|
+
error in ulps of the largest output, and `elementwise`, the worst per element ulp distance with
|
|
232
|
+
the input where it happens. Read both. A rotation matrix has entries that should be zero, and
|
|
233
|
+
there the elementwise count compares float32 rounding noise with float64 rounding noise and reaches
|
|
234
|
+
`1e9` while the matrix is fine to one ulp at scale. A function whose output spans forty orders of
|
|
235
|
+
magnitude, a Bessel function, has a meaningless `at scale` number and a meaningful elementwise one.
|
|
236
|
+
A small angle formula without a guard, `(1 - cos(theta)) / theta ** 2`, shows `9e6` in both
|
|
237
|
+
columns. Static methods run; instance methods, functions that need other arguments, fail to import
|
|
238
|
+
or return something that is not a float array are counted with the reason and skipped, never
|
|
239
|
+
guessed at. This imports and runs the repository's code, from the scanned tree, or from the
|
|
240
|
+
package installed in the environment with `--run-installed` when the tree has unbuilt extensions.
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
ulpwise scan kornia/kornia --run --run-limit 200 --report kornia.md
|
|
244
|
+
ulpwise scan pytorch/vision --run --run-installed --run-limit 150
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
On kornia main 9 of the 200 busiest functions run as is, the rest are instance methods or want
|
|
248
|
+
shaped inputs. `adjust_log` is on both lists: the static scan flags its `(1 + image).log2()` as
|
|
249
|
+
`log1p-by-hand`, and the run shows `8.7e8` elementwise ulps at `x = 5.6e-8` next to `0.7` ulps at
|
|
250
|
+
scale, which is the right reading for a function defined on images in `[0, 1]`.
|
|
251
|
+
|
|
252
|
+
In ML repositories most `high` findings are learning rate schedules (`cos(pi * progress)` with
|
|
253
|
+
`progress` in `[0, 1]`) and pixel distances (`sqrt(dx * dx + dy * dy)` on coordinates below
|
|
254
|
+
`1e4`), where the argument is bounded and the pattern is harmless. The scan cannot know the bound;
|
|
255
|
+
the reading list is where that judgement happens.
|
|
256
|
+
|
|
257
|
+
## Accuracy survey
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
pip install 'ulpwise[survey]' torch scipy jax # mpmath is the reference, the rest are backends
|
|
261
|
+
ulpwise survey --out survey # results.csv and results.md, about 3 minutes
|
|
262
|
+
ulpwise survey --functions bessel_j0,polygamma_1 --backends torch,scipy --dtypes f64 --points 2000
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
For every function in `ulpwise.survey.REGISTRY` (exp, log, trig and hyperbolic functions, erf and
|
|
266
|
+
friends, gamma family, torch.special Bessel and Airy functions, the activation functions), every
|
|
267
|
+
dtype and every installed backend, the survey evaluates a log spaced grid over the function's domain
|
|
268
|
+
plus the named edge values of the dtype, computes the exact value with mpmath at the rounded input,
|
|
269
|
+
and reports max, p99 and median error in ulps, the fraction of inputs beyond 1 and 10 ulps, non
|
|
270
|
+
finite mismatches and the worst input. For torch it also reports how many inputs the vectorized
|
|
271
|
+
kernel and the scalar tail disagree on, and how many inputs would fail torch's reference test under
|
|
272
|
+
the dtype default tolerance and under the op's `OpInfo` override, read from `op_db`.
|
|
273
|
+
|
|
274
|
+
[`studies/accuracy-survey-2026-09`](https://github.com/Nicholas022400701/ulpwise/blob/main/studies/accuracy-survey-2026-09/README.md) is the first run
|
|
275
|
+
(torch 2.14.0+cpu, numpy 2.2.6, scipy 1.18.1, jax 0.11.2, Linux x86_64 AVX512). The short version:
|
|
276
|
+
|
|
277
|
+
- torch's `bessel_j0/j1/y0/y1` and `airy_ai` in float64 are off by 2.6e9 to 3.9e12 ulps and the
|
|
278
|
+
`precisionOverride({torch.float64: 1e-05})` on their tests hides every failing input
|
|
279
|
+
(pytorch #198583).
|
|
280
|
+
- torch's `polygamma(1, x)` in float64 keeps about 9 digits (4.0e6 ulps, 46 percent of inputs
|
|
281
|
+
beyond 10 ulps) and passes the default float64 tolerance, which at `rtol = atol = 1e-7` tolerates
|
|
282
|
+
about 4.5e8 ulps; in float32 it loses every digit for large negative `x` (pytorch #198663).
|
|
283
|
+
- torch's `erfcx` for negative `x` is off by up to `x*x/2` ulps, 44 in float32 at `x = -8.44` and
|
|
284
|
+
157 in float64 at `x = -23.25`, and scipy's float64 `erfcx` returns the same wrong values
|
|
285
|
+
(pytorch #198664).
|
|
286
|
+
- for 12 of 61 torch functions the AVX512 kernel and the scalar tail return different floats for
|
|
287
|
+
the same input, up to 246 of 619 inputs for `mish`.
|
|
288
|
+
- jax on CPU flushes subnormals to zero, its float64 `erfinv` loses 5 digits near the ends of the
|
|
289
|
+
interval and its float64 `log_ndtr` loses 3 digits between `x = 5.4` and 8.
|
|
290
|
+
- scipy's float64 `lgamma` does not handle the zeros at 1 and 2, and its Bessel functions lose the
|
|
291
|
+
phase at large `x`.
|
|
292
|
+
|
|
293
|
+
## How the exact oracle works
|
|
294
|
+
|
|
295
|
+
For `sqrt(x)` the candidate `r` and the midpoint `m` between `r` and its neighbour are written as
|
|
296
|
+
integers times a power of two, `m^2` is formed in `u128` (at most 110 bits) and compared with `x`
|
|
297
|
+
after aligning exponents. The sign says on which side of `m` the exact root lies, and
|
|
298
|
+
`|x - m^2| / (sqrt(x) + m)` is the distance to the midpoint. If the hardware result turns out to be
|
|
299
|
+
on the wrong side of a midpoint it is stepped one ulp toward the exact value and checked again, so
|
|
300
|
+
`sqrt_cr` is correctly rounded even where the platform `sqrt` is not. Division uses the same
|
|
301
|
+
machinery with `m * b` against `a`, and there the residual is exact. `tests/test_core.py`
|
|
302
|
+
cross-checks both against `fractions.Fraction` and `decimal.Decimal` at 80 digits.
|
|
303
|
+
|
|
304
|
+
## Roadmap
|
|
305
|
+
|
|
306
|
+
- `ulpwise scan --run` for methods and functions with tensor shape requirements, by reading the
|
|
307
|
+
docstring and the checks for the shapes, and an mpmath reference for the scalar functions.
|
|
308
|
+
- Mutation scoring for numerical tests: single token mutants of the code under test (`abs`, a
|
|
309
|
+
dropped `sqrt`, `/ 4` for `/ 16`) run against the test suite, reporting which survive.
|
|
310
|
+
- `float16` and `bfloat16` `spacing`, `next_up`, knife edges and exact oracles (ulp distances and edge values are done).
|
|
311
|
+
- Exact references for transcendental functions in Rust (correctly rounded `exp`, `log`, ...) so
|
|
312
|
+
the `f64` knife-edge scans do not need mpmath.
|
|
313
|
+
- Survey backends for CUDA and MPS, and `float16` / `bfloat16` rows.
|
|
314
|
+
- Zero copy paths for numpy arrays and torch tensors.
|
|
315
|
+
- More corpus entries, with fixtures for the cases that need data.
|
|
316
|
+
|
|
317
|
+
## AI disclosure
|
|
318
|
+
|
|
319
|
+
This project is written with an AI coding agent (Claude) working for
|
|
320
|
+
区梓灏 ([@Nicholas022400701](https://github.com/Nicholas022400701)), who owns the repository and
|
|
321
|
+
reviews what is published. The same pipeline produced the upstream fixes in the corpus; each of
|
|
322
|
+
those pull requests carries the same disclosure.
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
Licensed under either of
|
|
327
|
+
|
|
328
|
+
- the MIT license ([LICENSE-MIT](https://github.com/Nicholas022400701/ulpwise/blob/main/LICENSE-MIT)), or
|
|
329
|
+
- the Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/Nicholas022400701/ulpwise/blob/main/LICENSE-APACHE)),
|
|
330
|
+
|
|
331
|
+
at your option. "At your option" means that whoever uses or redistributes this code picks
|
|
332
|
+
whichever of the two licenses they want to comply with. Nobody has to ask anyone. Unless you say
|
|
333
|
+
otherwise, a contribution you send is dual licensed the same way, without extra terms.
|
|
334
|
+
|