ulpwise 0.3.2__tar.gz → 0.3.4__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.
Files changed (38) hide show
  1. {ulpwise-0.3.2 → ulpwise-0.3.4}/CHANGELOG.md +73 -0
  2. {ulpwise-0.3.2 → ulpwise-0.3.4}/Cargo.lock +1 -1
  3. {ulpwise-0.3.2 → ulpwise-0.3.4}/Cargo.toml +1 -1
  4. {ulpwise-0.3.2 → ulpwise-0.3.4}/PKG-INFO +35 -6
  5. {ulpwise-0.3.2 → ulpwise-0.3.4}/README.md +34 -5
  6. {ulpwise-0.3.2 → ulpwise-0.3.4}/pyproject.toml +1 -1
  7. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/__init__.py +1 -1
  8. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/corpus/cases.json +83 -0
  9. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/scan.py +410 -9
  10. {ulpwise-0.3.2 → ulpwise-0.3.4}/tests/test_scan.py +213 -4
  11. {ulpwise-0.3.2 → ulpwise-0.3.4}/.github/dependabot.yml +0 -0
  12. {ulpwise-0.3.2 → ulpwise-0.3.4}/.github/workflows/ci.yml +0 -0
  13. {ulpwise-0.3.2 → ulpwise-0.3.4}/.github/workflows/release.yml +0 -0
  14. {ulpwise-0.3.2 → ulpwise-0.3.4}/.gitignore +0 -0
  15. {ulpwise-0.3.2 → ulpwise-0.3.4}/CONTRIBUTING.md +0 -0
  16. {ulpwise-0.3.2 → ulpwise-0.3.4}/LICENSE-APACHE +0 -0
  17. {ulpwise-0.3.2 → ulpwise-0.3.4}/LICENSE-MIT +0 -0
  18. {ulpwise-0.3.2 → ulpwise-0.3.4}/examples/sqrt_knife_values.py +0 -0
  19. {ulpwise-0.3.2 → ulpwise-0.3.4}/examples/torch_sqrt_conformance.py +0 -0
  20. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/__main__.py +0 -0
  21. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/corpus/__init__.py +0 -0
  22. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/plugin.py +0 -0
  23. {ulpwise-0.3.2 → ulpwise-0.3.4}/python/ulpwise/survey.py +0 -0
  24. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/edge.rs +0 -0
  25. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/exact.rs +0 -0
  26. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/knife.rs +0 -0
  27. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/lib.rs +0 -0
  28. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/py.rs +0 -0
  29. {ulpwise-0.3.2 → ulpwise-0.3.4}/src/ulp.rs +0 -0
  30. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/README.md +0 -0
  31. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/results.csv +0 -0
  32. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/results.md +0 -0
  33. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/README.md +0 -0
  34. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/results.csv +0 -0
  35. {ulpwise-0.3.2 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/results.md +0 -0
  36. {ulpwise-0.3.2 → ulpwise-0.3.4}/tests/test_core.py +0 -0
  37. {ulpwise-0.3.2 → ulpwise-0.3.4}/tests/test_corpus.py +0 -0
  38. {ulpwise-0.3.2 → ulpwise-0.3.4}/tests/test_survey.py +0 -0
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4 (2026-10-09)
4
+
5
+ - Regression corpus case for pytorch/rl #4504, merged upstream: `MultiCategorical.to_one_hot` iterated over `self.nvec`,
6
+ a 2-D tensor for a spec with a batch shape, so `one_hot` got a tensor as `num_classes` and raised `TypeError`, and the
7
+ 0-D sample of a shape `[1]` spec raised `IndexError` on `val[..., i]`. The case one-hot encodes a sample of
8
+ `MultiCategorical([3, 2], shape=(4, 2))` and the scalar sample of `MultiCategorical([5])`.
9
+ - `ulpwise scan`: new `dropout-never-applied` rule (medium), the first that reads a whole file rather than one
10
+ function: a `Dropout`, `DropPath` or a `ModuleDict` or `ModuleList` under a dropout name is assigned to `self`
11
+ and then never called, never passed on, never returned or iterated and its rate never read, anywhere in the file.
12
+ The option is accepted and does nothing, so the model trains without the regularisation it reports. peft #3830 is
13
+ the example: `OFTLayer` built `module_dropout` into `self.oft_dropout`, filled it in `update_layer` and never
14
+ called it. Reading the rate counts as use (`scaled_dot_product_attention(dropout_p=self.dropout.p)`), as does
15
+ passing the module to another one; filling it does not, and subclasses of `Sequential`, which run every
16
+ attribute, are not read. On kornia, ultralytics, torchvision, diffusers, torchrl, vllm and detectron2 main the
17
+ rule reports nothing; on peft main it reports the #3830 line, on timm main one `attn_drop` in `coat.py` that a
18
+ comment already calls unused, and on transformers main nine lines, four of them modular files whose `forward`
19
+ lives in another file.
20
+
21
+ ## 0.3.3 (2026-10-08)
22
+
23
+ - Two regression corpus cases for peft, both open upstream with a fix PR: peft #3769, `add_weighted_adapter` with
24
+ `combination_type='svd'` crashed for Conv1d and Conv3d LoRA layers because only Conv2d deltas were flattened before
25
+ the SVD (the case combines two rank 2 adapters at `svd_rank=4` and expects the output of both adapters active, which
26
+ the fix matches to 2e-6), and peft #3830, OFT `module_dropout` was never applied, so ten training forwards of the
27
+ same input were identical where the fix spreads them by 0.56.
28
+ - `ulpwise scan`: new `where-nan-gradient` rule (medium) for `where(d > eps, f(d), other)` with `f` a division by `d`
29
+ or a `sqrt`, `log`, `acos` or `asin` of it. `where` evaluates both branches and hands the discarded one a zero
30
+ gradient, and the backward of `f` at the singularity turns that zero into `0 / 0 = nan`, so the guard protects the
31
+ value and not the gradient. kornia #5579, found by the kornia conventions audit, is the example: the mutual
32
+ information losses divided by the range of the signal inside `where(diff > eps, ...)` and a constant input or
33
+ target got a nan gradient. The condition may be a comparison, a name bound to one, or `~`, `&`, `|` and a subscript
34
+ of those, and the method form `a.where(cond, b)` counts. Quiet for `numpy.where`, which has no autograd, for a
35
+ comparison against a number above 1 (`strength < 50` in kornia's JPEG scale is a piecewise definition), for a
36
+ floor named `eps`, `tol`, `floor`, `thresh`, `tiny`, `bound` or `limit` on the other side of the comparison, and
37
+ once `d` is re-bound to a `where`, `clamp` or `maximum` of itself, which is the recommended fix and the shape of
38
+ kornia's `fit_line` and `rotation_matrix_to_quaternion`. On kornia main d15741e2 the rule reports four lines, on
39
+ ultralytics main 8df3534 two, both in metric code, and nothing in torchvision.
40
+ - `ulpwise scan`: new `clamp-at-singularity` rule (medium) for `clamp(x, min=0).sqrt()`, `sqrt(clamp(x, min=0))`,
41
+ `clamp(c, -1, 1).acos()` and the `clip`, `clamp_min`, `clamp_max`, `asin` and `log` forms: the bound is the point
42
+ where the next function has an infinite derivative, and clamp's derivative at its own bound is 1 on torch 2.5.1 and
43
+ 2.9.1 and 0 on 2.14, so the gradient there is `inf` or `nan` on the older half of a supported range. kornia #4229,
44
+ found by the kornia conventions audit, is the example, `_cdist`'s `clamp(min=0.0).sqrt()` with identical float16
45
+ descriptors, and kornia #5500's `clamp(-1, 1).acos()` the second. A bound strictly inside the domain, `min=1e-8`,
46
+ is a floor and is not reported, a bound given by a name is not read, and numpy is quiet. On kornia main d15741e2
47
+ the rule reports the three lines of `_solve_cubic_real`, on ultralytics main 8df3534 one line of `DepthMetrics`.
48
+ - `ulpwise scan`: a finding inside a nested function is reported once, under the nested function, instead of once
49
+ more under the enclosing one, and the enclosing function's call and division counts no longer include it.
50
+ - `ulpwise scan`: new `eps-floor` rule (medium) for `x * (1 - eps) + eps`, in either order and with `self.eps` too: the
51
+ floor moves every value, 0 becomes `eps` and 1 stays 1, so the entries of a one-hot sum to `1 + (C - 1) eps` and a
52
+ perfect prediction scores a loss that grows with the image. kornia #5538, found by the kornia conventions audit, is
53
+ the example: `one_hot` floored its zeros with 1e-6 and a perfect prediction of a 256 × 384 image with a one-pixel
54
+ class scored a macro Dice loss of 0.0234. On kornia main 9e188e08 the rule reports that line and nothing else, and
55
+ nothing in ultralytics or torchvision; `(p + eps) / (q + eps)` is not a floor and stays quiet.
56
+ - `ulpwise scan`: the `hypot-by-hand` advice no longer offers `norm` as the replacement. `torch.linalg.vector_norm` and
57
+ `numpy.linalg.norm` square the entries first too, so in float32 both return `inf` for a vector with two entries of
58
+ 1.8e19 while `hypot` returns 2.5e19; the advice now says to divide a vector by its largest absolute entry when the
59
+ point is a direction. The kornia maintainer caught the case in review of kornia #5505, where `v / v.norm()` turned
60
+ such vectors into zeros and the angle between them read 0.
61
+ - `ulpwise scan`: `acos-for-angle` moves from `info` to `medium` and names the mechanism: next to 1 the cosine has no
62
+ digits left for a small angle, so `acos` recovers the angle with an absolute error of `sqrt(eps)`, 0.02 degrees in
63
+ float32, whatever the angle. The kornia scan listed `kornia/metrics/pose.py` under this rule, and both functions
64
+ there, `angle_error_mat` and `angle_error_vec`, return exactly 0 for every rotation below 0.03 degrees in float32
65
+ and 180.0 for 179.99 degrees (kornia #5500); the rule cites it.
66
+ - Regression corpus case for kornia #5500: a 0.01 degree rotation scores 0.0 in both metrics on kornia main 050ac77f.
67
+ - The kornia #5500 corpus case records its fix, kornia #5505, merged 2026-10-06: `angle_error_mat` reads the sine from
68
+ the skew part of the relative rotation and `angle_error_vec` from the cross product of the scaled vectors, and both
69
+ return `atan2(sin, cos)`, so a 0.01 degree rotation scores 0.0099990 in float32. The case stays an expected failure
70
+ until a kornia release carries the fix.
71
+ - `ulpwise scan`: `logsumexp-by-hand` no longer fires on the stable form, where every `exp` argument has its
72
+ maximum subtracted first (`log(exp(x - x.max()).sum())`, or `s = x - m` with `m` bound to a `max`, `amax`,
73
+ `maximum` or `torch.max(...).values`). The lm-evaluation-harness scan listed the numerically stable `_log_softmax`
74
+ of `lm_eval/models/_onnx_base.py` next to a real `log(exp(a) + exp(b))`; it now reports only the real one.
75
+
3
76
  ## 0.3.2 (2026-10-06)
4
77
 
5
78
  - Two regression corpus cases for pytorch #199850: `torch.erf` in bfloat16 and float16 on CPU returns 0 at and
@@ -120,7 +120,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
120
120
 
121
121
  [[package]]
122
122
  name = "ulpwise"
123
- version = "0.3.2"
123
+ version = "0.3.4"
124
124
  dependencies = [
125
125
  "pyo3",
126
126
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "ulpwise"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  edition = "2021"
5
5
  rust-version = "1.86"
6
6
  license = "MIT OR Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ulpwise
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: Pytest
6
6
  Classifier: Intended Audience :: Developers
@@ -185,6 +185,7 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
185
185
  | pytorch/rl #4443 `arange(0, 1, 1/n)` gives `n + 1` positions for 140 values of `n` below 2000 | rounding | 2026-09-20 |
186
186
  | pytorch/rl #4444 `min_value or -inf` drops `min_value=0` | falsy zero | 2026-09-20 |
187
187
  | pytorch/rl #4445 scheduler `state_dict()` contained a module object | crash | 2026-09-20 |
188
+ | pytorch/rl #4504 `MultiCategorical.to_one_hot` crashed for a batched spec and a 0-D sample | crash | 2026-10-07 |
188
189
  | timm #2786 Mars kept a reference to `p.grad` as the previous gradient | aliasing | 2026-09-17 |
189
190
  | timm #2790 AdafactorBigVision clipped updates in the wrong direction | direction | 2026-09-18 |
190
191
  | timm #2791 AdaMuon conv LR scale computed from the wrong dims | scale | 2026-09-18 |
@@ -228,6 +229,9 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
228
229
  | pytorch #198664 `erfcx` off by `x*x/2` ulps for negative `x` (`exp` at the rounded square) | rounding | open |
229
230
  | pytorch #199850 `torch.erf` in bfloat16 and float16 on CPU returns 0 at and below 1.8e-7 and loses relative accuracy below 1e-3 (13404 bfloat16 ulps, 5 float16 ulps) | cancellation | open |
230
231
  | pytorch #199867 `torch.special.logit` in float16 and bfloat16 on CPU rounds `1 - x` and `x / (1 - x)` to the input dtype before the log, logit(0.499756) is -0.000488 for -0.000977 (512 float16 ulps, 64 bfloat16 ulps) | rounding | open |
232
+ | kornia #5505 `angle_error_mat` and `angle_error_vec` returned exactly 0 for every rotation below 0.03 degrees in float32 (`acos` next to 1, issue #5500) | cancellation | 2026-10-06 |
233
+ | peft #3769 `add_weighted_adapter` with `combination_type='svd'` crashed for Conv1d and Conv3d LoRA layers | crash | open |
234
+ | peft #3830 OFT `module_dropout` was never applied, every training forward used all rotation blocks | no-op | open |
231
235
 
232
236
  Cases marked `open` have an issue with the complete patch attached and no merged fix yet. Every case
233
237
  is an expected failure until the installed release contains the fix (`fixed_in_release` in
@@ -246,22 +250,27 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
246
250
  ```
247
251
 
248
252
  The scan is static and needs nothing installed: it parses each file with `ast`, walks every
249
- function and matches twelve patterns, each with a severity, the reason it loses digits or
250
- overflows, the usual replacement and, where one exists, the upstream bug it comes from.
253
+ function and matches sixteen patterns, each with a severity, the reason it loses digits,
254
+ overflows or loses its gradient, or in one case does nothing at all, the usual replacement and,
255
+ where one exists, the upstream bug it comes from.
251
256
 
252
257
  | rule | severity | pattern |
253
258
  |---|---|---|
254
259
  | `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) |
255
260
  | `sin-of-pi-times` | high | `sin(pi * x)`, `cos(pi * x)`: the product is rounded before the argument reduction (pytorch #198663) |
256
261
  | `softplus-by-hand` | high | `log(1 + exp(x))` |
257
- | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))`, `log(exp(x).sum(-1))` |
258
- | `hypot-by-hand` | high | `sqrt(a * a + b * b)` |
262
+ | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))`, `log(exp(x).sum(-1))`; quiet when every `exp` argument has its maximum subtracted first, `log(exp(x - x.max()).sum())`, the stable form |
263
+ | `hypot-by-hand` | high | `sqrt(a * a + b * b)`; the advice no longer says `norm`, which squares first too and overflows at 1.8e19 in float32 (kornia #5505 review) |
259
264
  | `sqrt-of-difference` | medium | `sqrt(a - b)` |
260
265
  | `one-minus-cos` | medium | `1 - cos(x)` (kornia #4897) |
261
266
  | `log1p-by-hand`, `expm1-by-hand` | medium | `log(1 + x)`, `exp(x) - 1` |
262
267
  | `atan-of-quotient` | medium | `atan(y / x)` |
263
268
  | `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) |
264
- | `acos-for-angle` | info | `acos`, `asin` used to recover an angle |
269
+ | `acos-for-angle` | medium | `acos`, `asin` used to recover an angle: the angle comes back with an absolute error of `sqrt(eps)`, 0.02 degrees in float32 (kornia #5500) |
270
+ | `eps-floor` | medium | `x * (1 - eps) + eps`: every value moves, 0 becomes `eps`, so a one-hot's entries sum to `1 + (C - 1) eps` and a perfect prediction scores a loss that grows with the image (kornia #5538, found by the kornia conventions audit) |
271
+ | `where-nan-gradient` | medium | `where(d > eps, f(d), other)` with `f` a division by `d` or a `sqrt`, `log`, `acos` or `asin` of it: `where` evaluates both branches and hands the discarded one a zero gradient, and the backward of `f` at the singularity turns that zero into `0 / 0 = nan`, so the guard protects the value and not the gradient; quiet for `numpy.where`, for a comparison against a number above 1, for a floor named `eps`, `tol` or `floor` on the other side, and once `d` is re-bound to a `where`, `clamp` or `maximum` of itself (kornia #5579, found by the kornia conventions audit) |
272
+ | `clamp-at-singularity` | medium | `clamp(x, min=0).sqrt()`, `sqrt(clamp(x, min=0))`, `clamp(c, -1, 1).acos()`: the bound is the point where the next function has an infinite derivative, and clamp's derivative at its own bound is not the same across torch versions, 1 on 2.5.1 and 2.9.1 and 0 on 2.14, so the gradient there is `inf` or `nan` on the older half of a supported range; a bound strictly inside the domain, `min=1e-8`, is a floor and is not reported, and `numpy` is quiet (kornia #4229, found by the kornia conventions audit; kornia #5500) |
273
+ | `dropout-never-applied` | medium | a `Dropout`, `DropPath` or a `ModuleDict` of them assigned to `self` and then never called, never passed on and its rate never read, anywhere in the file: the option is accepted and does nothing, so the model trains without the regularisation it reports (peft #3830) |
265
274
 
266
275
  On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
267
276
  `one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
@@ -270,6 +279,26 @@ and `Se3.exp` (also #4897) is under `one-minus-cos`. The scan puts `ellipse_to_l
270
279
  on the list too, for a `sqrt` of a difference; the bug there was a different one, so that entry is
271
280
  what the scan is: a reading list, not a verdict.
272
281
 
282
+ The two gradient rules are reading lists in the same sense. On kornia `main` at `d15741e2` they
283
+ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where` guards, in
284
+ `PatchDominantGradientOrientation`, `compute_correspond_epilines`, `_crop_scale_translation` and
285
+ `_get_convex_edges`, of which the first two sit on a differentiable path and the last two draw
286
+ boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
287
+ inference code that is never differentiated, and nothing in torchvision.
288
+
289
+ `dropout-never-applied` reads the whole file rather than one function, since the module is built
290
+ in `__init__` and used, if at all, in `forward` or in a sibling class. Calling it, calling it
291
+ through a subscript, passing it to another module, returning it, iterating over it and reading
292
+ its rate (`scaled_dot_product_attention(dropout_p=self.dropout.p)`) all count as use; filling it
293
+ (`self.oft_dropout.update(...)`) does not, and a subclass of `Sequential` runs every attribute and
294
+ is not read. On kornia `main` at `6d579a72`, ultralytics `6d51b7e`, torchvision `9a8d545`,
295
+ diffusers `d961a38`, torchrl `648f50c`, vllm `73c742b` and detectron2 `1e3e13b` it reports
296
+ nothing. On peft `main` at `f6d8480` it reports `OFTLayer.oft_dropout`, which is #3830; on timm
297
+ `83e6eb5` it reports `FactorAttnConvRelPosEnc.attn_drop` in `coat.py`, which a comment on that
298
+ line already calls unused; on transformers `9167f73` it reports nine lines, four of them in
299
+ modular files whose `forward` is inherited from another file, which the rule cannot see, and
300
+ five attention, embedding and head dropouts built in `__init__` and used nowhere in their file.
301
+
273
302
  `--run` adds the dynamic half. The module level functions with the most elementary math are
274
303
  imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
275
304
  required argument, in float32 and in float64, torch first and numpy second, and the float32 result
@@ -156,6 +156,7 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
156
156
  | pytorch/rl #4443 `arange(0, 1, 1/n)` gives `n + 1` positions for 140 values of `n` below 2000 | rounding | 2026-09-20 |
157
157
  | pytorch/rl #4444 `min_value or -inf` drops `min_value=0` | falsy zero | 2026-09-20 |
158
158
  | pytorch/rl #4445 scheduler `state_dict()` contained a module object | crash | 2026-09-20 |
159
+ | pytorch/rl #4504 `MultiCategorical.to_one_hot` crashed for a batched spec and a 0-D sample | crash | 2026-10-07 |
159
160
  | timm #2786 Mars kept a reference to `p.grad` as the previous gradient | aliasing | 2026-09-17 |
160
161
  | timm #2790 AdafactorBigVision clipped updates in the wrong direction | direction | 2026-09-18 |
161
162
  | timm #2791 AdaMuon conv LR scale computed from the wrong dims | scale | 2026-09-18 |
@@ -199,6 +200,9 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
199
200
  | pytorch #198664 `erfcx` off by `x*x/2` ulps for negative `x` (`exp` at the rounded square) | rounding | open |
200
201
  | pytorch #199850 `torch.erf` in bfloat16 and float16 on CPU returns 0 at and below 1.8e-7 and loses relative accuracy below 1e-3 (13404 bfloat16 ulps, 5 float16 ulps) | cancellation | open |
201
202
  | pytorch #199867 `torch.special.logit` in float16 and bfloat16 on CPU rounds `1 - x` and `x / (1 - x)` to the input dtype before the log, logit(0.499756) is -0.000488 for -0.000977 (512 float16 ulps, 64 bfloat16 ulps) | rounding | open |
203
+ | kornia #5505 `angle_error_mat` and `angle_error_vec` returned exactly 0 for every rotation below 0.03 degrees in float32 (`acos` next to 1, issue #5500) | cancellation | 2026-10-06 |
204
+ | peft #3769 `add_weighted_adapter` with `combination_type='svd'` crashed for Conv1d and Conv3d LoRA layers | crash | open |
205
+ | peft #3830 OFT `module_dropout` was never applied, every training forward used all rotation blocks | no-op | open |
202
206
 
203
207
  Cases marked `open` have an issue with the complete patch attached and no merged fix yet. Every case
204
208
  is an expected failure until the installed release contains the fix (`fixed_in_release` in
@@ -217,22 +221,27 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
217
221
  ```
218
222
 
219
223
  The scan is static and needs nothing installed: it parses each file with `ast`, walks every
220
- function and matches twelve patterns, each with a severity, the reason it loses digits or
221
- overflows, the usual replacement and, where one exists, the upstream bug it comes from.
224
+ function and matches sixteen patterns, each with a severity, the reason it loses digits,
225
+ overflows or loses its gradient, or in one case does nothing at all, the usual replacement and,
226
+ where one exists, the upstream bug it comes from.
222
227
 
223
228
  | rule | severity | pattern |
224
229
  |---|---|---|
225
230
  | `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) |
226
231
  | `sin-of-pi-times` | high | `sin(pi * x)`, `cos(pi * x)`: the product is rounded before the argument reduction (pytorch #198663) |
227
232
  | `softplus-by-hand` | high | `log(1 + exp(x))` |
228
- | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))`, `log(exp(x).sum(-1))` |
229
- | `hypot-by-hand` | high | `sqrt(a * a + b * b)` |
233
+ | `logsumexp-by-hand` | high | `log(exp(a) + exp(b))`, `log(sum(exp(x)))`, `log(exp(x).sum(-1))`; quiet when every `exp` argument has its maximum subtracted first, `log(exp(x - x.max()).sum())`, the stable form |
234
+ | `hypot-by-hand` | high | `sqrt(a * a + b * b)`; the advice no longer says `norm`, which squares first too and overflows at 1.8e19 in float32 (kornia #5505 review) |
230
235
  | `sqrt-of-difference` | medium | `sqrt(a - b)` |
231
236
  | `one-minus-cos` | medium | `1 - cos(x)` (kornia #4897) |
232
237
  | `log1p-by-hand`, `expm1-by-hand` | medium | `log(1 + x)`, `exp(x) - 1` |
233
238
  | `atan-of-quotient` | medium | `atan(y / x)` |
234
239
  | `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) |
235
- | `acos-for-angle` | info | `acos`, `asin` used to recover an angle |
240
+ | `acos-for-angle` | medium | `acos`, `asin` used to recover an angle: the angle comes back with an absolute error of `sqrt(eps)`, 0.02 degrees in float32 (kornia #5500) |
241
+ | `eps-floor` | medium | `x * (1 - eps) + eps`: every value moves, 0 becomes `eps`, so a one-hot's entries sum to `1 + (C - 1) eps` and a perfect prediction scores a loss that grows with the image (kornia #5538, found by the kornia conventions audit) |
242
+ | `where-nan-gradient` | medium | `where(d > eps, f(d), other)` with `f` a division by `d` or a `sqrt`, `log`, `acos` or `asin` of it: `where` evaluates both branches and hands the discarded one a zero gradient, and the backward of `f` at the singularity turns that zero into `0 / 0 = nan`, so the guard protects the value and not the gradient; quiet for `numpy.where`, for a comparison against a number above 1, for a floor named `eps`, `tol` or `floor` on the other side, and once `d` is re-bound to a `where`, `clamp` or `maximum` of itself (kornia #5579, found by the kornia conventions audit) |
243
+ | `clamp-at-singularity` | medium | `clamp(x, min=0).sqrt()`, `sqrt(clamp(x, min=0))`, `clamp(c, -1, 1).acos()`: the bound is the point where the next function has an infinite derivative, and clamp's derivative at its own bound is not the same across torch versions, 1 on 2.5.1 and 2.9.1 and 0 on 2.14, so the gradient there is `inf` or `nan` on the older half of a supported range; a bound strictly inside the domain, `min=1e-8`, is a floor and is not reported, and `numpy` is quiet (kornia #4229, found by the kornia conventions audit; kornia #5500) |
244
+ | `dropout-never-applied` | medium | a `Dropout`, `DropPath` or a `ModuleDict` of them assigned to `self` and then never called, never passed on and its rate never read, anywhere in the file: the option is accepted and does nothing, so the model trains without the regularisation it reports (peft #3830) |
236
245
 
237
246
  On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
238
247
  `one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
@@ -241,6 +250,26 @@ and `Se3.exp` (also #4897) is under `one-minus-cos`. The scan puts `ellipse_to_l
241
250
  on the list too, for a `sqrt` of a difference; the bug there was a different one, so that entry is
242
251
  what the scan is: a reading list, not a verdict.
243
252
 
253
+ The two gradient rules are reading lists in the same sense. On kornia `main` at `d15741e2` they
254
+ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where` guards, in
255
+ `PatchDominantGradientOrientation`, `compute_correspond_epilines`, `_crop_scale_translation` and
256
+ `_get_convex_edges`, of which the first two sit on a differentiable path and the last two draw
257
+ boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
258
+ inference code that is never differentiated, and nothing in torchvision.
259
+
260
+ `dropout-never-applied` reads the whole file rather than one function, since the module is built
261
+ in `__init__` and used, if at all, in `forward` or in a sibling class. Calling it, calling it
262
+ through a subscript, passing it to another module, returning it, iterating over it and reading
263
+ its rate (`scaled_dot_product_attention(dropout_p=self.dropout.p)`) all count as use; filling it
264
+ (`self.oft_dropout.update(...)`) does not, and a subclass of `Sequential` runs every attribute and
265
+ is not read. On kornia `main` at `6d579a72`, ultralytics `6d51b7e`, torchvision `9a8d545`,
266
+ diffusers `d961a38`, torchrl `648f50c`, vllm `73c742b` and detectron2 `1e3e13b` it reports
267
+ nothing. On peft `main` at `f6d8480` it reports `OFTLayer.oft_dropout`, which is #3830; on timm
268
+ `83e6eb5` it reports `FactorAttnConvRelPosEnc.attn_drop` in `coat.py`, which a comment on that
269
+ line already calls unused; on transformers `9167f73` it reports nine lines, four of them in
270
+ modular files whose `forward` is inherited from another file, which the rule cannot see, and
271
+ five attention, embedding and head dropouts built in `__init__` and used nowhere in their file.
272
+
244
273
  `--run` adds the dynamic half. The module level functions with the most elementary math are
245
274
  imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
246
275
  required argument, in float32 and in float64, torch first and numpy second, and the float32 result
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "ulpwise"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Numerical conformance testing for ML code: float edge values, exact rounding oracles, knife-edge finders and a pytest plugin"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT OR Apache-2.0" }
@@ -30,7 +30,7 @@ from ._core import special as _special_core
30
30
  from ._core import ulp_distance as _ulp_distance_core
31
31
  from ._core import ulp_distances as _ulp_distances_core
32
32
 
33
- __version__ = "0.3.2"
33
+ __version__ = "0.3.4"
34
34
 
35
35
  __all__ = [
36
36
  "UNARY_OPS",
@@ -143,6 +143,25 @@
143
143
  },
144
144
  "before_fix": "TypeError: cannot pickle 'module' object"
145
145
  },
146
+ {
147
+ "id": "torchrl-4504-multicategorical-to-one-hot-batched-spec",
148
+ "repo": "pytorch/rl",
149
+ "pr": 4504,
150
+ "merged_at": "2026-10-07T10:30:35Z",
151
+ "kind": "crash",
152
+ "title": "MultiCategorical.to_one_hot crashed for a spec with a batch shape and for the scalar sample of a shape [1] spec",
153
+ "symptom": "to_one_hot iterated over self.nvec, which is a 2-D tensor for a spec with a batch shape, so one_hot received a tensor as num_classes and raised TypeError; a shape [1] spec returns 0-D samples and val[..., i] on them raised IndexError.",
154
+ "requires": [
155
+ "torch",
156
+ "torchrl"
157
+ ],
158
+ "fixed_in_release": null,
159
+ "repro": "import torch\nfrom torchrl.data import MultiCategorical\nspec = MultiCategorical([3, 2], shape=(4, 2))\nhot = spec.to_one_hot(spec.rand())\nscalar = MultiCategorical([5]).to_one_hot(torch.tensor(3))\nactual = [list(hot.shape), hot.dtype == torch.bool, int(hot.sum()), list(scalar.shape), int(scalar.sum()), bool(scalar[3])]\nexpected = [[4, 5], True, 8, [5], 1, True]\n",
160
+ "check": {
161
+ "type": "equal"
162
+ },
163
+ "before_fix": "TypeError: one_hot(): argument 'num_classes' (position 2) must be int, not Tensor for the batched spec; IndexError: too many indices for tensor of dimension 0 for the scalar sample"
164
+ },
146
165
  {
147
166
  "id": "timm-2786-mars-last-grad-aliasing",
148
167
  "repo": "huggingface/pytorch-image-models",
@@ -1103,6 +1122,70 @@
1103
1122
  "max_ulp": 1
1104
1123
  },
1105
1124
  "before_fix": "[-0.00048828125, -0.01129150390625, -0.039337158203125, 0.0797119140625] on torch 2.14.0 (512, 72, 23 and 6.7 ulp)"
1125
+ },
1126
+ {
1127
+ "id": "kornia-5500-angle-error-small-angle-acos",
1128
+ "repo": "kornia/kornia",
1129
+ "pr": 5505,
1130
+ "issue": 5500,
1131
+ "merged_at": "2026-10-06T17:27:13Z",
1132
+ "kind": "cancellation",
1133
+ "title": "angle_error_mat and angle_error_vec return exactly 0 for every rotation below 0.03 degrees in float32",
1134
+ "symptom": "Both metrics recover the angle as acos of a cosine, (tr(R1^T R2) - 1) / 2 and v1 . v2 / (|v1| |v2|). Next to 1 the float32 cosine has no digits left for a small angle, so a 0.01 degree rotation scores 0.0, a 0.05 degree one is 3 to 21 % off depending on the rotations and 179.99 degrees comes back as 180.0; the clamp(-1, 1).acos() also has a -inf / nan backward at exactly 0 and 180 degrees on torch 2.5.1. The patch reads sin(theta) from the skew part of the relative rotation, the cross product of the unit vectors for angle_error_vec, and returns atan2(sin, cos).",
1135
+ "requires": [
1136
+ "torch",
1137
+ "kornia"
1138
+ ],
1139
+ "fixed_in_release": null,
1140
+ "repro": "import torch\nimport kornia\nR1 = kornia.geometry.axis_angle_to_rotation_matrix(torch.tensor([[0.3, -0.5, 0.8]], dtype=torch.float64))\nR2 = R1 @ kornia.geometry.axis_angle_to_rotation_matrix(torch.deg2rad(torch.tensor([[0.0, 0.0, 0.01]], dtype=torch.float64)))\nv1 = torch.tensor([[0.0, 0.6, 0.8]], dtype=torch.float64)\nv2 = (kornia.geometry.axis_angle_to_rotation_matrix(torch.deg2rad(torch.tensor([[0.01, 0.0, 0.0]], dtype=torch.float64))) @ v1[..., None])[..., 0]\nactual = [kornia.metrics.angle_error_mat(R1.float(), R2.float()).item(), kornia.metrics.angle_error_vec(v1.float(), v2.float()).item()]\nexpected = [0.01, 0.01]\n",
1141
+ "check": {
1142
+ "type": "allclose",
1143
+ "atol": 0.0,
1144
+ "rtol": 0.001
1145
+ },
1146
+ "before_fix": "[0.0, 0.0] on kornia main 050ac77f"
1147
+ },
1148
+ {
1149
+ "id": "peft-3769-add-weighted-adapter-svd-conv1d-conv3d",
1150
+ "repo": "huggingface/peft",
1151
+ "pr": null,
1152
+ "issue": 3769,
1153
+ "merged_at": null,
1154
+ "kind": "crash",
1155
+ "title": "add_weighted_adapter with combination_type='svd' crashed for Conv1d and Conv3d LoRA layers",
1156
+ "symptom": "_svd_generalized_task_arithmetic_weighted_adapter checked isinstance(target, Conv2d) and flattened only that, so a Conv1d or Conv3d delta weight reached the SVD with its kernel dims in place and the matmul raised. The fix tests _ConvNd and always flattens from dim 1; PR 3903 is open with it.",
1157
+ "requires": [
1158
+ "torch",
1159
+ "peft"
1160
+ ],
1161
+ "fixed_in_release": null,
1162
+ "repro": "import torch\nfrom peft import LoraConfig, get_peft_model\n\nclass Net(torch.nn.Module):\n def __init__(self):\n super().__init__()\n self.conv = torch.nn.Conv1d(4, 8, kernel_size=3)\n\n def forward(self, x):\n return self.conv(x)\n\ntorch.manual_seed(0)\nconfig = LoraConfig(target_modules=['conv'], r=2, init_lora_weights=False)\nmodel = get_peft_model(Net(), config, adapter_name='a')\nmodel.add_adapter('b', config)\nx = torch.randn(1, 4, 10)\nwith torch.no_grad():\n model.base_model.set_adapter(['a', 'b'])\n both = model(x)\n # svd_rank=4 holds the rank 2 + 2 sum of the two deltas exactly\n model.add_weighted_adapter(['a', 'b'], [1.0, 1.0], 'ab', combination_type='svd', svd_rank=4)\n model.set_adapter('ab')\n combined = model(x)\nactual = combined.flatten().tolist()\nexpected = both.flatten().tolist()\n",
1163
+ "check": {
1164
+ "type": "allclose",
1165
+ "atol": 1e-05,
1166
+ "rtol": 1e-05
1167
+ },
1168
+ "before_fix": "RuntimeError inside _svd_generalized_task_arithmetic_weighted_adapter on peft main 91ca7945: Expected size for first two dimensions of batch2 tensor to be: [8, 4] but got: [8, 3]"
1169
+ },
1170
+ {
1171
+ "id": "peft-3830-oft-module-dropout-never-applied",
1172
+ "repo": "huggingface/peft",
1173
+ "pr": null,
1174
+ "issue": 3830,
1175
+ "merged_at": null,
1176
+ "kind": "no-op",
1177
+ "title": "OFT module_dropout was never applied, every training forward used all rotation blocks",
1178
+ "symptom": "OFTLayer built self.oft_dropout from module_dropout but OFTRotationModule.forward never called it, so module_dropout=0.5 behaved like 0 and ten training forwards of the same input were identical. The fix passes the dropout into the rotation module and applies it to the Cayley blocks; PR 3905 is open with it.",
1179
+ "requires": [
1180
+ "torch",
1181
+ "peft"
1182
+ ],
1183
+ "fixed_in_release": null,
1184
+ "repro": "import torch\nfrom peft import OFTConfig, get_peft_model\n\nclass Net(torch.nn.Module):\n def __init__(self):\n super().__init__()\n self.lin = torch.nn.Linear(16, 12)\n\n def forward(self, x):\n return self.lin(x)\n\ntorch.manual_seed(0)\nconfig = OFTConfig(target_modules=['lin'], r=4, oft_block_size=0, module_dropout=0.5, init_weights=False)\nmodel = get_peft_model(Net(), config)\nmodel.train()\nx = torch.randn(8, 16)\nwith torch.no_grad():\n outputs = [model(x) for _ in range(10)]\n# module_dropout replaces half of the four rotation blocks by the identity in each training forward,\n# so ten forwards cannot all agree\nactual = [max((outputs[0] - out).abs().max().item() for out in outputs[1:])]\nexpected = [1.0] # any positive spread\n",
1185
+ "check": {
1186
+ "type": "sign"
1187
+ },
1188
+ "before_fix": "[0.0] on peft main 91ca7945; with the fix the ten forwards spread by 0.56"
1106
1189
  }
1107
1190
  ]
1108
1191
  }