ulpwise 0.3.3__tar.gz → 0.3.5__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.3.3 → ulpwise-0.3.5}/CHANGELOG.md +33 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/Cargo.lock +13 -13
- {ulpwise-0.3.3 → ulpwise-0.3.5}/Cargo.toml +2 -2
- {ulpwise-0.3.3 → ulpwise-0.3.5}/PKG-INFO +24 -7
- {ulpwise-0.3.3 → ulpwise-0.3.5}/README.md +23 -6
- {ulpwise-0.3.3 → ulpwise-0.3.5}/pyproject.toml +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/__init__.py +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/corpus/cases.json +94 -35
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/scan.py +80 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/tests/test_scan.py +94 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/.github/dependabot.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/.github/workflows/ci.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/.github/workflows/release.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/.gitignore +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/CONTRIBUTING.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/LICENSE-APACHE +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/LICENSE-MIT +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/examples/sqrt_knife_values.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/examples/torch_sqrt_conformance.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/__main__.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/corpus/__init__.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/plugin.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/python/ulpwise/survey.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/edge.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/exact.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/knife.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/lib.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/py.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/src/ulp.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-09/README.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-09/results.csv +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-09/results.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-10-half/README.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-10-half/results.csv +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/studies/accuracy-survey-2026-10-half/results.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/tests/test_core.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/tests/test_corpus.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.5}/tests/test_survey.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
- kornia 0.9.0 (released 2026-10-23) contains every kornia fix in the corpus and torchrl 0.14.1 (2026-10-10) the four
|
|
6
|
+
pytorch/rl ones, so `fixed_in_release` is set on those 36 cases and they are plain passes instead of expected
|
|
7
|
+
failures. The weekly strict run had been failing on them as unexpected passes since 2026-10-12, the four torchrl
|
|
8
|
+
cases first and all 36 on 2026-10-26.
|
|
9
|
+
- Two more kornia cases, both merged upstream on 2026-10-09 and in 0.9.0. #5653: `gftt_response` took the smaller
|
|
10
|
+
eigenvalue as `0.5 * (trace - sqrt((trace**2 - 4 * det).abs()))`, and on a flat 9 x 9 footprint the blurred moments
|
|
11
|
+
are exactly zero, so the sqrt backward is `inf * 0 = nan` and the whole flat region gets a nan gradient; the case
|
|
12
|
+
takes the gradient of the summed response of an image with black top and bottom rows (2048 nan pixels on 0.8.3).
|
|
13
|
+
#5655: `equalize_clahe` wrote the clipping residual into the first `residual` histogram bins instead of spreading
|
|
14
|
+
it over every `max(256 // residual, 1)`-th bin as OpenCV does, which lifted the whole lookup table of a small tile
|
|
15
|
+
(up to +38 levels against `cv2.createCLAHE`); the case maps a 2 x 4 tile holding levels 0 and 100 with clip 40,
|
|
16
|
+
where level 100 came out as 255 instead of 159.
|
|
17
|
+
|
|
18
|
+
## 0.3.4 (2026-10-09)
|
|
19
|
+
|
|
20
|
+
- Regression corpus case for pytorch/rl #4504, merged upstream: `MultiCategorical.to_one_hot` iterated over `self.nvec`,
|
|
21
|
+
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
|
|
22
|
+
0-D sample of a shape `[1]` spec raised `IndexError` on `val[..., i]`. The case one-hot encodes a sample of
|
|
23
|
+
`MultiCategorical([3, 2], shape=(4, 2))` and the scalar sample of `MultiCategorical([5])`.
|
|
24
|
+
- `ulpwise scan`: new `dropout-never-applied` rule (medium), the first that reads a whole file rather than one
|
|
25
|
+
function: a `Dropout`, `DropPath` or a `ModuleDict` or `ModuleList` under a dropout name is assigned to `self`
|
|
26
|
+
and then never called, never passed on, never returned or iterated and its rate never read, anywhere in the file.
|
|
27
|
+
The option is accepted and does nothing, so the model trains without the regularisation it reports. peft #3830 is
|
|
28
|
+
the example: `OFTLayer` built `module_dropout` into `self.oft_dropout`, filled it in `update_layer` and never
|
|
29
|
+
called it. Reading the rate counts as use (`scaled_dot_product_attention(dropout_p=self.dropout.p)`), as does
|
|
30
|
+
passing the module to another one; filling it does not, and subclasses of `Sequential`, which run every
|
|
31
|
+
attribute, are not read. On kornia, ultralytics, torchvision, diffusers, torchrl, vllm and detectron2 main the
|
|
32
|
+
rule reports nothing; on peft main it reports the #3830 line, on timm main one `attn_drop` in `coat.py` that a
|
|
33
|
+
comment already calls unused, and on transformers main nine lines, four of them modular files whose `forward`
|
|
34
|
+
lives in another file.
|
|
35
|
+
|
|
3
36
|
## 0.3.3 (2026-10-08)
|
|
4
37
|
|
|
5
38
|
- Two regression corpus cases for peft, both open upstream with a fix PR: peft #3769, `add_weighted_adapter` with
|
|
@@ -37,9 +37,9 @@ dependencies = [
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "pyo3"
|
|
40
|
-
version = "0.
|
|
40
|
+
version = "0.30.1"
|
|
41
41
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
-
checksum = "
|
|
42
|
+
checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4"
|
|
43
43
|
dependencies = [
|
|
44
44
|
"libc",
|
|
45
45
|
"once_cell",
|
|
@@ -51,18 +51,18 @@ dependencies = [
|
|
|
51
51
|
|
|
52
52
|
[[package]]
|
|
53
53
|
name = "pyo3-build-config"
|
|
54
|
-
version = "0.
|
|
54
|
+
version = "0.30.1"
|
|
55
55
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
56
|
-
checksum = "
|
|
56
|
+
checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700"
|
|
57
57
|
dependencies = [
|
|
58
58
|
"target-lexicon",
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
[[package]]
|
|
62
62
|
name = "pyo3-ffi"
|
|
63
|
-
version = "0.
|
|
63
|
+
version = "0.30.1"
|
|
64
64
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
65
|
-
checksum = "
|
|
65
|
+
checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4"
|
|
66
66
|
dependencies = [
|
|
67
67
|
"libc",
|
|
68
68
|
"pyo3-build-config",
|
|
@@ -70,9 +70,9 @@ dependencies = [
|
|
|
70
70
|
|
|
71
71
|
[[package]]
|
|
72
72
|
name = "pyo3-macros"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.30.1"
|
|
74
74
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
75
|
-
checksum = "
|
|
75
|
+
checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60"
|
|
76
76
|
dependencies = [
|
|
77
77
|
"proc-macro2",
|
|
78
78
|
"pyo3-macros-backend",
|
|
@@ -82,9 +82,9 @@ dependencies = [
|
|
|
82
82
|
|
|
83
83
|
[[package]]
|
|
84
84
|
name = "pyo3-macros-backend"
|
|
85
|
-
version = "0.
|
|
85
|
+
version = "0.30.1"
|
|
86
86
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
-
checksum = "
|
|
87
|
+
checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"heck",
|
|
90
90
|
"proc-macro2",
|
|
@@ -103,9 +103,9 @@ dependencies = [
|
|
|
103
103
|
|
|
104
104
|
[[package]]
|
|
105
105
|
name = "syn"
|
|
106
|
-
version = "
|
|
106
|
+
version = "3.0.9"
|
|
107
107
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
108
|
-
checksum = "
|
|
108
|
+
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
|
109
109
|
dependencies = [
|
|
110
110
|
"proc-macro2",
|
|
111
111
|
"quote",
|
|
@@ -120,7 +120,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
120
120
|
|
|
121
121
|
[[package]]
|
|
122
122
|
name = "ulpwise"
|
|
123
|
-
version = "0.3.
|
|
123
|
+
version = "0.3.5"
|
|
124
124
|
dependencies = [
|
|
125
125
|
"pyo3",
|
|
126
126
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "ulpwise"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.86"
|
|
6
6
|
license = "MIT OR Apache-2.0"
|
|
@@ -15,7 +15,7 @@ name = "ulpwise"
|
|
|
15
15
|
crate-type = ["cdylib", "rlib"]
|
|
16
16
|
|
|
17
17
|
[dependencies]
|
|
18
|
-
pyo3 = { version = "0.
|
|
18
|
+
pyo3 = { version = "0.30", features = ["extension-module", "abi3-py39"], optional = true }
|
|
19
19
|
|
|
20
20
|
[features]
|
|
21
21
|
default = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ulpwise
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Framework :: Pytest
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -173,7 +173,7 @@ environment, side-install it and put it first on the path:
|
|
|
173
173
|
|
|
174
174
|
```sh
|
|
175
175
|
pip install --no-deps --target /tmp/kornia-0.8.3 kornia==0.8.3
|
|
176
|
-
PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia #
|
|
176
|
+
PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 34 present, 0 fixed on kornia 0.8.3; 0 present, 34 fixed on 0.9.0
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
| case | kind | merged |
|
|
@@ -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 |
|
|
@@ -229,13 +230,15 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
|
|
|
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 |
|
|
231
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
|
+
| kornia #5653 `gftt_response` had a nan gradient on every flat patch, `sqrt` of a zero discriminant (issue #5652) | gradient | 2026-10-09 |
|
|
234
|
+
| kornia #5655 `equalize_clahe` wrote the clipping residual into the first histogram bins, small tiles tens of levels brighter than OpenCV (issue #5654) | redistribution | 2026-10-09 |
|
|
232
235
|
| peft #3769 `add_weighted_adapter` with `combination_type='svd'` crashed for Conv1d and Conv3d LoRA layers | crash | open |
|
|
233
236
|
| peft #3830 OFT `module_dropout` was never applied, every training forward used all rotation blocks | no-op | open |
|
|
234
237
|
|
|
235
238
|
Cases marked `open` have an issue with the complete patch attached and no merged fix yet. Every case
|
|
236
239
|
is an expected failure until the installed release contains the fix (`fixed_in_release` in
|
|
237
|
-
`cases.json`; the timm and
|
|
238
|
-
pytorch ones are merged but not in a release yet), and the `max_ulp` check type measures the digits
|
|
240
|
+
`cases.json`; the timm, ultralytics, kornia (0.9.0) and pytorch/rl (0.14.1) fixes above are released,
|
|
241
|
+
the peft and pytorch ones are merged but not in a release yet), and the `max_ulp` check type measures the digits
|
|
239
242
|
directly. The ultralytics dataset and label cases build their one-image
|
|
240
243
|
dataset in a temporary directory and need no weights; two more ultralytics fixes (#26240, #26246) are not in
|
|
241
244
|
the corpus yet because their repros need model weights or the COCO evaluator.
|
|
@@ -249,9 +252,9 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
|
|
|
249
252
|
```
|
|
250
253
|
|
|
251
254
|
The scan is static and needs nothing installed: it parses each file with `ast`, walks every
|
|
252
|
-
function and matches
|
|
253
|
-
overflows or loses its gradient,
|
|
254
|
-
comes from.
|
|
255
|
+
function and matches sixteen patterns, each with a severity, the reason it loses digits,
|
|
256
|
+
overflows or loses its gradient, or in one case does nothing at all, the usual replacement and,
|
|
257
|
+
where one exists, the upstream bug it comes from.
|
|
255
258
|
|
|
256
259
|
| rule | severity | pattern |
|
|
257
260
|
|---|---|---|
|
|
@@ -269,6 +272,7 @@ comes from.
|
|
|
269
272
|
| `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) |
|
|
270
273
|
| `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) |
|
|
271
274
|
| `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) |
|
|
275
|
+
| `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) |
|
|
272
276
|
|
|
273
277
|
On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
|
|
274
278
|
`one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
|
|
@@ -284,6 +288,19 @@ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where
|
|
|
284
288
|
boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
|
|
285
289
|
inference code that is never differentiated, and nothing in torchvision.
|
|
286
290
|
|
|
291
|
+
`dropout-never-applied` reads the whole file rather than one function, since the module is built
|
|
292
|
+
in `__init__` and used, if at all, in `forward` or in a sibling class. Calling it, calling it
|
|
293
|
+
through a subscript, passing it to another module, returning it, iterating over it and reading
|
|
294
|
+
its rate (`scaled_dot_product_attention(dropout_p=self.dropout.p)`) all count as use; filling it
|
|
295
|
+
(`self.oft_dropout.update(...)`) does not, and a subclass of `Sequential` runs every attribute and
|
|
296
|
+
is not read. On kornia `main` at `6d579a72`, ultralytics `6d51b7e`, torchvision `9a8d545`,
|
|
297
|
+
diffusers `d961a38`, torchrl `648f50c`, vllm `73c742b` and detectron2 `1e3e13b` it reports
|
|
298
|
+
nothing. On peft `main` at `f6d8480` it reports `OFTLayer.oft_dropout`, which is #3830; on timm
|
|
299
|
+
`83e6eb5` it reports `FactorAttnConvRelPosEnc.attn_drop` in `coat.py`, which a comment on that
|
|
300
|
+
line already calls unused; on transformers `9167f73` it reports nine lines, four of them in
|
|
301
|
+
modular files whose `forward` is inherited from another file, which the rule cannot see, and
|
|
302
|
+
five attention, embedding and head dropouts built in `__init__` and used nowhere in their file.
|
|
303
|
+
|
|
287
304
|
`--run` adds the dynamic half. The module level functions with the most elementary math are
|
|
288
305
|
imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
|
|
289
306
|
required argument, in float32 and in float64, torch first and numpy second, and the float32 result
|
|
@@ -144,7 +144,7 @@ environment, side-install it and put it first on the path:
|
|
|
144
144
|
|
|
145
145
|
```sh
|
|
146
146
|
pip install --no-deps --target /tmp/kornia-0.8.3 kornia==0.8.3
|
|
147
|
-
PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia #
|
|
147
|
+
PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 34 present, 0 fixed on kornia 0.8.3; 0 present, 34 fixed on 0.9.0
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
| case | kind | merged |
|
|
@@ -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 |
|
|
@@ -200,13 +201,15 @@ PYTHONPATH=/tmp/kornia-0.8.3 ulpwise corpus --repo kornia # 31 present, 0 fi
|
|
|
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 |
|
|
202
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
|
+
| kornia #5653 `gftt_response` had a nan gradient on every flat patch, `sqrt` of a zero discriminant (issue #5652) | gradient | 2026-10-09 |
|
|
205
|
+
| kornia #5655 `equalize_clahe` wrote the clipping residual into the first histogram bins, small tiles tens of levels brighter than OpenCV (issue #5654) | redistribution | 2026-10-09 |
|
|
203
206
|
| peft #3769 `add_weighted_adapter` with `combination_type='svd'` crashed for Conv1d and Conv3d LoRA layers | crash | open |
|
|
204
207
|
| peft #3830 OFT `module_dropout` was never applied, every training forward used all rotation blocks | no-op | open |
|
|
205
208
|
|
|
206
209
|
Cases marked `open` have an issue with the complete patch attached and no merged fix yet. Every case
|
|
207
210
|
is an expected failure until the installed release contains the fix (`fixed_in_release` in
|
|
208
|
-
`cases.json`; the timm and
|
|
209
|
-
pytorch ones are merged but not in a release yet), and the `max_ulp` check type measures the digits
|
|
211
|
+
`cases.json`; the timm, ultralytics, kornia (0.9.0) and pytorch/rl (0.14.1) fixes above are released,
|
|
212
|
+
the peft and pytorch ones are merged but not in a release yet), and the `max_ulp` check type measures the digits
|
|
210
213
|
directly. The ultralytics dataset and label cases build their one-image
|
|
211
214
|
dataset in a temporary directory and need no weights; two more ultralytics fixes (#26240, #26246) are not in
|
|
212
215
|
the corpus yet because their repros need model weights or the COCO evaluator.
|
|
@@ -220,9 +223,9 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
|
|
|
220
223
|
```
|
|
221
224
|
|
|
222
225
|
The scan is static and needs nothing installed: it parses each file with `ast`, walks every
|
|
223
|
-
function and matches
|
|
224
|
-
overflows or loses its gradient,
|
|
225
|
-
comes from.
|
|
226
|
+
function and matches sixteen patterns, each with a severity, the reason it loses digits,
|
|
227
|
+
overflows or loses its gradient, or in one case does nothing at all, the usual replacement and,
|
|
228
|
+
where one exists, the upstream bug it comes from.
|
|
226
229
|
|
|
227
230
|
| rule | severity | pattern |
|
|
228
231
|
|---|---|---|
|
|
@@ -240,6 +243,7 @@ comes from.
|
|
|
240
243
|
| `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) |
|
|
241
244
|
| `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) |
|
|
242
245
|
| `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) |
|
|
246
|
+
| `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) |
|
|
243
247
|
|
|
244
248
|
On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
|
|
245
249
|
`one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
|
|
@@ -255,6 +259,19 @@ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where
|
|
|
255
259
|
boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
|
|
256
260
|
inference code that is never differentiated, and nothing in torchvision.
|
|
257
261
|
|
|
262
|
+
`dropout-never-applied` reads the whole file rather than one function, since the module is built
|
|
263
|
+
in `__init__` and used, if at all, in `forward` or in a sibling class. Calling it, calling it
|
|
264
|
+
through a subscript, passing it to another module, returning it, iterating over it and reading
|
|
265
|
+
its rate (`scaled_dot_product_attention(dropout_p=self.dropout.p)`) all count as use; filling it
|
|
266
|
+
(`self.oft_dropout.update(...)`) does not, and a subclass of `Sequential` runs every attribute and
|
|
267
|
+
is not read. On kornia `main` at `6d579a72`, ultralytics `6d51b7e`, torchvision `9a8d545`,
|
|
268
|
+
diffusers `d961a38`, torchrl `648f50c`, vllm `73c742b` and detectron2 `1e3e13b` it reports
|
|
269
|
+
nothing. On peft `main` at `f6d8480` it reports `OFTLayer.oft_dropout`, which is #3830; on timm
|
|
270
|
+
`83e6eb5` it reports `FactorAttnConvRelPosEnc.attn_drop` in `coat.py`, which a comment on that
|
|
271
|
+
line already calls unused; on transformers `9167f73` it reports nine lines, four of them in
|
|
272
|
+
modular files whose `forward` is inherited from another file, which the rule cannot see, and
|
|
273
|
+
five attention, embedding and head dropouts built in `__init__` and used nowhere in their file.
|
|
274
|
+
|
|
258
275
|
`--run` adds the dynamic half. The module level functions with the most elementary math are
|
|
259
276
|
imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
|
|
260
277
|
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.
|
|
7
|
+
version = "0.3.5"
|
|
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" }
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"torch",
|
|
15
15
|
"kornia"
|
|
16
16
|
],
|
|
17
|
-
"fixed_in_release":
|
|
17
|
+
"fixed_in_release": "0.9.0",
|
|
18
18
|
"repro": "import torch\nimport kornia.filters as KF\nxs = torch.arange(-4, 5, dtype=torch.float64)\ny, x = torch.meshgrid(xs, xs, indexing='ij')\ndxx = KF.spatial_gradient((x * x)[None, None], mode='sobel', order=2)[0, 0, 0, 4, 4]\ndxy = KF.spatial_gradient((x * y)[None, None], mode='sobel', order=2)[0, 0, 1, 4, 4]\ndxy_diff = KF.spatial_gradient((x * y)[None, None], mode='diff', order=2)[0, 0, 1, 4, 4]\nactual = [dxx.item(), dxy.item(), dxy_diff.item()]\nexpected = [2.0, 1.0, 1.0]\n",
|
|
19
19
|
"check": {
|
|
20
20
|
"type": "sign"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"torch",
|
|
34
34
|
"kornia"
|
|
35
35
|
],
|
|
36
|
-
"fixed_in_release":
|
|
36
|
+
"fixed_in_release": "0.9.0",
|
|
37
37
|
"repro": "import torch\nimport kornia.feature as KFe\nxs = torch.arange(-4, 5, dtype=torch.float64)\ny, x = torch.meshgrid(xs, xs, indexing='ij')\ndet_sobel_xy = KFe.hessian_response((x * y)[None, None], grads_mode='sobel')[0, 0, 4, 4]\ndet_diff_quad = KFe.hessian_response((x * x + y * y)[None, None], grads_mode='diff')[0, 0, 4, 4]\ndet_sobel_degenerate = KFe.hessian_response(((x + y) ** 2 / 2)[None, None], grads_mode='sobel')[0, 0, 4, 4]\nactual = [det_sobel_xy.item(), det_diff_quad.item(), det_sobel_degenerate.item()]\nexpected = [-1.0, 4.0, 0.0]\n",
|
|
38
38
|
"check": {
|
|
39
39
|
"type": "allclose",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"torch",
|
|
55
55
|
"kornia"
|
|
56
56
|
],
|
|
57
|
-
"fixed_in_release":
|
|
57
|
+
"fixed_in_release": "0.9.0",
|
|
58
58
|
"repro": "import torch\nimport kornia.feature as KF\n# Oxford ellipses [x, y, a, b, c]; the LAF's A must satisfy A @ A.T == inverse([[a, b], [b, c]])\nells = torch.tensor([[[0.0, 0.0, 1.0, 0.5, 1.0], [3.0, -2.0, 2.0, -0.9, 0.5]]], dtype=torch.float64)\nA = KF.ellipse_to_laf(ells)[..., :2]\nregion = torch.linalg.inv(A @ A.transpose(-1, -2))[0]\nactual = region.reshape(-1).tolist()\nexpected = [1.0, 0.5, 0.5, 1.0, 2.0, -0.9, -0.9, 0.5]\n",
|
|
59
59
|
"check": {
|
|
60
60
|
"type": "allclose",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"torch",
|
|
97
97
|
"torchrl"
|
|
98
98
|
],
|
|
99
|
-
"fixed_in_release":
|
|
99
|
+
"fixed_in_release": "0.14.1",
|
|
100
100
|
"repro": "import torch\nfrom torchrl.envs.transforms import ActionDiscretizer\nt = ActionDiscretizer(num_intervals=49)\nt.dtype = torch.float32\nactual = [t.custom_arange(n, torch.device('cpu')).numel() for n in (49, 98, 103, 107, 10)]\nexpected = [49, 98, 103, 107, 10]\n",
|
|
101
101
|
"check": {
|
|
102
102
|
"type": "equal"
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"torch",
|
|
116
116
|
"torchrl"
|
|
117
117
|
],
|
|
118
|
-
"fixed_in_release":
|
|
118
|
+
"fixed_in_release": "0.14.1",
|
|
119
119
|
"repro": "from torchrl.data.replay_buffers import ListStorage, PrioritizedSampler, ReplayBuffer\nfrom torchrl.data.replay_buffers.scheduler import StepScheduler\nrb = ReplayBuffer(storage=ListStorage(16), sampler=PrioritizedSampler(16, alpha=0.5, beta=0.5))\nsched = StepScheduler(rb, 'alpha', gamma=-0.2, n_steps=1, mode='additive', min_value=0)\nseen = []\nfor _ in range(4):\n sched.step()\n seen.append(float(rb.sampler.alpha))\nactual = seen\nexpected = [0.3, 0.1, 0.0, 0.0]\n",
|
|
120
120
|
"check": {
|
|
121
121
|
"type": "allclose",
|
|
@@ -136,13 +136,32 @@
|
|
|
136
136
|
"torch",
|
|
137
137
|
"torchrl"
|
|
138
138
|
],
|
|
139
|
-
"fixed_in_release":
|
|
139
|
+
"fixed_in_release": "0.14.1",
|
|
140
140
|
"repro": "import io\nimport torch\nfrom torchrl.data.replay_buffers import ListStorage, PrioritizedSampler, ReplayBuffer\nfrom torchrl.data.replay_buffers.scheduler import StepScheduler\nrb = ReplayBuffer(storage=ListStorage(16), sampler=PrioritizedSampler(16, alpha=0.5, beta=0.5))\nsched = StepScheduler(rb, 'alpha', gamma=0.5, n_steps=1)\nsched.step()\nsched.step()\nbuf = io.BytesIO()\ntorch.save(sched.state_dict(), buf)\nbuf.seek(0)\nstate = torch.load(buf, weights_only=False)\nactual = [state['_step_cnt']]\nexpected = [2]\n",
|
|
141
141
|
"check": {
|
|
142
142
|
"type": "equal"
|
|
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": "0.14.1",
|
|
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",
|
|
@@ -299,7 +318,7 @@
|
|
|
299
318
|
"torch",
|
|
300
319
|
"kornia"
|
|
301
320
|
],
|
|
302
|
-
"fixed_in_release":
|
|
321
|
+
"fixed_in_release": "0.9.0",
|
|
303
322
|
"repro": "import torch\nimport kornia.geometry.conversions as C\naxis = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float64) / 14.0**0.5\nv = (9.99e-4 * axis)[None]\nactual = C.axis_angle_to_rotation_matrix(v).flatten().tolist()\nexpected = C.quaternion_to_rotation_matrix(C.axis_angle_to_quaternion(v)).flatten().tolist()\n",
|
|
304
323
|
"check": {
|
|
305
324
|
"type": "allclose",
|
|
@@ -321,7 +340,7 @@
|
|
|
321
340
|
"torch",
|
|
322
341
|
"kornia"
|
|
323
342
|
],
|
|
324
|
-
"fixed_in_release":
|
|
343
|
+
"fixed_in_release": "0.9.0",
|
|
325
344
|
"repro": "import torch\nfrom kornia.geometry.liegroup import Se3, So3\nrot = So3.exp(torch.tensor([[1e-4, 0.0, 0.0]], dtype=torch.float32))\nse = Se3.exp(torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 1e-4]], dtype=torch.float32))\nactual = [rot.log()[0, 0].item(), se.t[0, 1].item()]\nexpected = [1e-4, 5e-5]\n",
|
|
326
345
|
"check": {
|
|
327
346
|
"type": "allclose",
|
|
@@ -445,7 +464,7 @@
|
|
|
445
464
|
"torch",
|
|
446
465
|
"kornia"
|
|
447
466
|
],
|
|
448
|
-
"fixed_in_release":
|
|
467
|
+
"fixed_in_release": "0.9.0",
|
|
449
468
|
"repro": "import torch\nfrom kornia.geometry.liegroup import Se2\nv = torch.tensor([[1.0, 2.0, 1e-8]], dtype=torch.float64) # translation (1, 2) and a turn of 1e-8 rad\ng = Se2.exp(v)\nactual = g.matrix()[0, :2, 2].tolist() + g.log().flatten().tolist()\nb = 0.5e-8 # (1 - cos(theta)) / theta = theta / 2 - theta^3 / 24; sin(theta) / theta = 1 - theta^2 / 6 rounds to 1\nexpected = [1.0 - 2.0 * b, 2.0 + b, 1.0, 2.0, 1e-8]\n",
|
|
450
469
|
"check": {
|
|
451
470
|
"type": "allclose",
|
|
@@ -466,7 +485,7 @@
|
|
|
466
485
|
"torch",
|
|
467
486
|
"kornia"
|
|
468
487
|
],
|
|
469
|
-
"fixed_in_release":
|
|
488
|
+
"fixed_in_release": "0.9.0",
|
|
470
489
|
"repro": "import torch\nfrom kornia.geometry.linalg import point_line_distance\nline = torch.tensor([[0.0, 1.0, -1.0]], dtype=torch.float64) # the line y = 1\npoint = torch.tensor([[[3.0, 5.0, 2.0]]], dtype=torch.float64) # the homogeneous point (1.5, 2.5): w = 2\nactual = point_line_distance(point, line).flatten().tolist()\nexpected = [1.5]\n",
|
|
471
490
|
"check": {
|
|
472
491
|
"type": "allclose",
|
|
@@ -487,7 +506,7 @@
|
|
|
487
506
|
"torch",
|
|
488
507
|
"kornia"
|
|
489
508
|
],
|
|
490
|
-
"fixed_in_release":
|
|
509
|
+
"fixed_in_release": "0.9.0",
|
|
491
510
|
"repro": "import torch\nfrom kornia.geometry.quaternion import Quaternion\nq = Quaternion(torch.tensor([[-1.0, 0.0, 0.0, 0.0]], dtype=torch.float64)) # a half turn: angle pi, axis undefined\nroot = q ** 0.5\nactual = root.data.flatten().tolist() + (root * root).data.flatten().tolist()\nexpected = [0.0, 1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0]\n",
|
|
492
511
|
"check": {
|
|
493
512
|
"type": "allclose",
|
|
@@ -508,7 +527,7 @@
|
|
|
508
527
|
"torch",
|
|
509
528
|
"kornia"
|
|
510
529
|
],
|
|
511
|
-
"fixed_in_release":
|
|
530
|
+
"fixed_in_release": "0.9.0",
|
|
512
531
|
"repro": "import torch\nfrom kornia.geometry.liegroup import So2\nangle = torch.tensor([[0.0], [1.5707963267948966]], dtype=torch.float64) # (B, 1): one angle per sample, as a column\npoints = torch.tensor([[1.0, 0.0], [1.0, 0.0]], dtype=torch.float64)\nrotated = So2.exp(angle) * points\nactual = [list(rotated.shape), rotated.flatten().tolist()]\nexpected = [[2, 2], [1.0, 0.0, 0.0, 1.0]]\n",
|
|
513
532
|
"check": {
|
|
514
533
|
"type": "allclose",
|
|
@@ -529,7 +548,7 @@
|
|
|
529
548
|
"torch",
|
|
530
549
|
"kornia"
|
|
531
550
|
],
|
|
532
|
-
"fixed_in_release":
|
|
551
|
+
"fixed_in_release": "0.9.0",
|
|
533
552
|
"repro": "import torch\nfrom kornia.geometry.solvers import solve_cubic\ncoeffs = torch.tensor([[1e-30, 0.0, 2.0, -6.0]], dtype=torch.float64) # 1e-30 x^3 + 2 x - 6: the real root is 3\nactual = solve_cubic(coeffs).flatten().tolist()\nexpected = [3.0, 0.0, 0.0]\n",
|
|
534
553
|
"check": {
|
|
535
554
|
"type": "allclose",
|
|
@@ -550,7 +569,7 @@
|
|
|
550
569
|
"torch",
|
|
551
570
|
"kornia"
|
|
552
571
|
],
|
|
553
|
-
"fixed_in_release":
|
|
572
|
+
"fixed_in_release": "0.9.0",
|
|
554
573
|
"repro": "import torch\nfrom kornia.color import RgbToGrayscale, rgb_to_grayscale\nimage = torch.full((1, 3, 2, 2), 200, dtype=torch.uint8)\nactual = [int(RgbToGrayscale()(image).max()), int(RgbToGrayscale()(image).min())]\nexpected = [int(rgb_to_grayscale(image).max()), int(rgb_to_grayscale(image).min())]\n",
|
|
555
574
|
"check": {
|
|
556
575
|
"type": "equal"
|
|
@@ -569,7 +588,7 @@
|
|
|
569
588
|
"torch",
|
|
570
589
|
"kornia"
|
|
571
590
|
],
|
|
572
|
-
"fixed_in_release":
|
|
591
|
+
"fixed_in_release": "0.9.0",
|
|
573
592
|
"repro": "import torch\nfrom kornia.geometry.subpix import conv_soft_argmax2d\nquiet = torch.zeros(1, 1, 7, 7)\nquiet[0, 0, 6, 6] = 1.0 # a weak bump in the bottom right corner\nloud = quiet.clone()\nloud[0, 0, 0, 0] = 200.0 # a peak far away, 200 temperatures above the bump\nkwargs = dict(kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), temperature=torch.tensor(1.0), normalized_coordinates=True)\nactual = conv_soft_argmax2d(loud, **kwargs)[0, 0, :, 6, 6].tolist()\nexpected = conv_soft_argmax2d(quiet, **kwargs)[0, 0, :, 6, 6].tolist()\n",
|
|
574
593
|
"check": {
|
|
575
594
|
"type": "allclose",
|
|
@@ -590,7 +609,7 @@
|
|
|
590
609
|
"torch",
|
|
591
610
|
"kornia"
|
|
592
611
|
],
|
|
593
|
-
"fixed_in_release":
|
|
612
|
+
"fixed_in_release": "0.9.0",
|
|
594
613
|
"repro": "import torch\nfrom kornia.filters import get_gaussian_discrete_kernel1d\none = get_gaussian_discrete_kernel1d(1, 1.0)\nfive = get_gaussian_discrete_kernel1d(5, 1.0)\nactual = [one.numel(), five.numel(), one.flatten().tolist()]\nexpected = [1, 5, [1.0]]\n",
|
|
595
614
|
"check": {
|
|
596
615
|
"type": "equal"
|
|
@@ -609,7 +628,7 @@
|
|
|
609
628
|
"torch",
|
|
610
629
|
"kornia"
|
|
611
630
|
],
|
|
612
|
-
"fixed_in_release":
|
|
631
|
+
"fixed_in_release": "0.9.0",
|
|
613
632
|
"repro": "import torch\nfrom kornia.geometry.liegroup import Se3, So3\nv = torch.tensor([[1.0, 2.0, 3.0, 0.0, 0.0, 0.0]], dtype=torch.float64) # upsilon = (1, 2, 3), omega = 0\njac = torch.autograd.functional.jacobian(lambda x: Se3.exp(x).matrix()[:, :3, 3], v)[0, :, 0, :]\nactual = jac[:, 3:].flatten().tolist()\nexpected = (-0.5 * So3.hat(v[0, :3])).flatten().tolist()\n",
|
|
614
633
|
"check": {
|
|
615
634
|
"type": "allclose",
|
|
@@ -630,7 +649,7 @@
|
|
|
630
649
|
"torch",
|
|
631
650
|
"kornia"
|
|
632
651
|
],
|
|
633
|
-
"fixed_in_release":
|
|
652
|
+
"fixed_in_release": "0.9.0",
|
|
634
653
|
"repro": "import torch\nfrom kornia.geometry.quaternion import Quaternion\ndata = torch.tensor([[1.0, 0.0, 0.0, 0.0]], dtype=torch.float64, requires_grad=True)\nq = Quaternion(data)\nq.polar_angle.sum().backward()\ngrad = data.grad if data.grad is not None else q.data.grad\nactual = grad.flatten().tolist()\nexpected = [0.0, 0.0, 0.0, 0.0]\n",
|
|
635
654
|
"check": {
|
|
636
655
|
"type": "equal"
|
|
@@ -649,7 +668,7 @@
|
|
|
649
668
|
"torch",
|
|
650
669
|
"kornia"
|
|
651
670
|
],
|
|
652
|
-
"fixed_in_release":
|
|
671
|
+
"fixed_in_release": "0.9.0",
|
|
653
672
|
"repro": "import torch\nfrom kornia.geometry.epipolar import decompose_essential_matrix\nE = torch.tensor([[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) # [t]_x with t = e_z and R = I\nR1, R2, t = decompose_essential_matrix(E)\nactual = [list(R1.shape), list(R2.shape), list(t.shape)]\nexpected = [[3, 3], [3, 3], [3, 1]]\n",
|
|
654
673
|
"check": {
|
|
655
674
|
"type": "equal"
|
|
@@ -668,7 +687,7 @@
|
|
|
668
687
|
"torch",
|
|
669
688
|
"kornia"
|
|
670
689
|
],
|
|
671
|
-
"fixed_in_release":
|
|
690
|
+
"fixed_in_release": "0.9.0",
|
|
672
691
|
"repro": "import torch\nfrom kornia.geometry.plane import Hyperplane\npoints = [torch.tensor([p], dtype=torch.float64, requires_grad=True) for p in ([0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0])]\nplane = Hyperplane.through(*points)\nplane.normal.data.sum().backward()\nactual = [bool(torch.isfinite(p.grad).all()) for p in points] + plane.normal.data.flatten().abs().tolist()\nexpected = [True, True, True, 0.0, 0.0, 1.0]\n",
|
|
673
692
|
"check": {
|
|
674
693
|
"type": "equal"
|
|
@@ -687,7 +706,7 @@
|
|
|
687
706
|
"torch",
|
|
688
707
|
"kornia"
|
|
689
708
|
],
|
|
690
|
-
"fixed_in_release":
|
|
709
|
+
"fixed_in_release": "0.9.0",
|
|
691
710
|
"repro": "import torch\nfrom kornia.geometry.vector import Vector3\nactual = Vector3(torch.zeros(1, 3, dtype=torch.float16)).normalized().data.flatten().tolist()\nexpected = [0.0, 0.0, 0.0]\n",
|
|
692
711
|
"check": {
|
|
693
712
|
"type": "equal"
|
|
@@ -706,7 +725,7 @@
|
|
|
706
725
|
"torch",
|
|
707
726
|
"kornia"
|
|
708
727
|
],
|
|
709
|
-
"fixed_in_release":
|
|
728
|
+
"fixed_in_release": "0.9.0",
|
|
710
729
|
"repro": "import torch\nfrom kornia.filters import otsu_threshold\nx = torch.tensor([[[[-4.0, -3.0], [-2.0, -1.0]]]])\nmask, threshold = otsu_threshold(x, return_mask=True)\nactual = mask.flatten().tolist()\nexpected = [False, False, True, True]\n",
|
|
711
730
|
"check": {
|
|
712
731
|
"type": "equal"
|
|
@@ -725,7 +744,7 @@
|
|
|
725
744
|
"torch",
|
|
726
745
|
"kornia"
|
|
727
746
|
],
|
|
728
|
-
"fixed_in_release":
|
|
747
|
+
"fixed_in_release": "0.9.0",
|
|
729
748
|
"repro": "import torch\nfrom kornia.losses import MS_SSIMLoss\ntorch.manual_seed(0)\nimg1 = torch.rand(1, 3, 16, 20)\nimg2 = torch.rand(1, 3, 16, 20)\nactual = list(MS_SSIMLoss(sigmas=(0.5, 1.3), reduction=\"none\")(img1, img2).shape)\nexpected = [1, 16, 20]\n",
|
|
730
749
|
"check": {
|
|
731
750
|
"type": "equal"
|
|
@@ -744,7 +763,7 @@
|
|
|
744
763
|
"torch",
|
|
745
764
|
"kornia"
|
|
746
765
|
],
|
|
747
|
-
"fixed_in_release":
|
|
766
|
+
"fixed_in_release": "0.9.0",
|
|
748
767
|
"repro": "import torch\nfrom kornia.geometry.epipolar import sampson_epipolar_distance\nF = torch.tensor([[[0.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]]]) # a pure horizontal shift: the epiline of (x, y) is y' = y\npts1 = torch.zeros(1, 1, 2)\npts2 = torch.tensor([[[2.0, 0.0]]]) # on its epiline\nactual = sampson_epipolar_distance(pts1, pts2, F, squared=False).item()\nexpected = 0.0\n",
|
|
749
768
|
"check": {
|
|
750
769
|
"type": "equal"
|
|
@@ -763,7 +782,7 @@
|
|
|
763
782
|
"torch",
|
|
764
783
|
"kornia"
|
|
765
784
|
],
|
|
766
|
-
"fixed_in_release":
|
|
785
|
+
"fixed_in_release": "0.9.0",
|
|
767
786
|
"repro": "import torch\nfrom kornia.geometry.epipolar import sampson_epipolar_distance\nF = torch.tensor([[[0.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]]]) # a pure horizontal shift: the epiline of (x, y) is y' = y\npts1 = torch.zeros(1, 1, 2)\npts2 = torch.tensor([[[2.0, 1.0]]]) # one pixel off its epiline\nactual = (sampson_epipolar_distance(pts1, pts2, 1e-4 * F) / sampson_epipolar_distance(pts1, pts2, F)).item()\nexpected = 1.0\n",
|
|
768
787
|
"check": {
|
|
769
788
|
"type": "allclose",
|
|
@@ -784,7 +803,7 @@
|
|
|
784
803
|
"torch",
|
|
785
804
|
"kornia"
|
|
786
805
|
],
|
|
787
|
-
"fixed_in_release":
|
|
806
|
+
"fixed_in_release": "0.9.0",
|
|
788
807
|
"repro": "import torch\nimport kornia\nfrom kornia.augmentation import RandomHue\ntorch.manual_seed(0)\nimage = torch.rand(1, 3, 4, 4, dtype=torch.float64)\nhue = kornia.color.rgb_to_hsv(image)[:, 0]\nshifted = kornia.color.rgb_to_hsv(RandomHue(hue=(0.5, 0.5), p=1.0)(image))[:, 0]\nactual = (torch.remainder(shifted - hue, 2 * torch.pi) - torch.pi).abs().max().item() # circular distance from a half turn\nexpected = 0.0\n",
|
|
789
808
|
"check": {
|
|
790
809
|
"type": "allclose",
|
|
@@ -805,7 +824,7 @@
|
|
|
805
824
|
"torch",
|
|
806
825
|
"kornia"
|
|
807
826
|
],
|
|
808
|
-
"fixed_in_release":
|
|
827
|
+
"fixed_in_release": "0.9.0",
|
|
809
828
|
"repro": "import torch\nfrom kornia.geometry.liegroup import So3\nzero = torch.zeros(3, dtype=torch.float64)\nactual = torch.autograd.functional.hessian(lambda v: So3.exp(v).q.data[0], zero).flatten().tolist()\nexpected = (-0.25 * torch.eye(3, dtype=torch.float64)).flatten().tolist()\n",
|
|
810
829
|
"check": {
|
|
811
830
|
"type": "allclose",
|
|
@@ -826,7 +845,7 @@
|
|
|
826
845
|
"torch",
|
|
827
846
|
"kornia"
|
|
828
847
|
],
|
|
829
|
-
"fixed_in_release":
|
|
848
|
+
"fixed_in_release": "0.9.0",
|
|
830
849
|
"repro": "import torch\nfrom kornia.losses import MS_SSIMLoss\ntorch.manual_seed(0)\nimg1 = torch.randint(0, 256, (1, 1, 48, 40), dtype=torch.uint8)\nimg2 = torch.randint(0, 256, (1, 1, 48, 40), dtype=torch.uint8)\nactual = MS_SSIMLoss(data_range=255.0)(img1, img2).item()\nexpected = MS_SSIMLoss()(img1.float() / 255, img2.float() / 255).item()\n",
|
|
831
850
|
"check": {
|
|
832
851
|
"type": "allclose",
|
|
@@ -847,7 +866,7 @@
|
|
|
847
866
|
"torch",
|
|
848
867
|
"kornia"
|
|
849
868
|
],
|
|
850
|
-
"fixed_in_release":
|
|
869
|
+
"fixed_in_release": "0.9.0",
|
|
851
870
|
"repro": "import torch\nfrom kornia.geometry.liegroup import So3\nomega = torch.tensor([[0.48, 0.6, 0.64]]) * 45.0 # 45 rad about a unit axis\nactual = So3.right_jacobian(omega.half()).flatten().tolist()\nexpected = So3.right_jacobian(omega.double()).flatten().tolist()\n",
|
|
852
871
|
"check": {
|
|
853
872
|
"type": "allclose",
|
|
@@ -868,7 +887,7 @@
|
|
|
868
887
|
"torch",
|
|
869
888
|
"kornia"
|
|
870
889
|
],
|
|
871
|
-
"fixed_in_release":
|
|
890
|
+
"fixed_in_release": "0.9.0",
|
|
872
891
|
"repro": "import math\nimport torch\nfrom kornia.geometry.quaternion import Quaternion, average_quaternions\nq1 = torch.tensor([1.0, 0.0, 0.0, 0.0])\nq2 = torch.tensor([1.0, 1.0, 0.0, 0.0]) / math.sqrt(2.0) # a quarter turn about x\nmean = average_quaternions(Quaternion(torch.stack([q1, 3.0 * q2]))).data.flatten()\nactual = (mean * torch.sign(mean[0])).tolist() # the sign of a quaternion is free\nexpected = [math.cos(math.pi / 8), math.sin(math.pi / 8), 0.0, 0.0] # the bisector, an eighth of a turn\n",
|
|
873
892
|
"check": {
|
|
874
893
|
"type": "allclose",
|
|
@@ -889,7 +908,7 @@
|
|
|
889
908
|
"torch",
|
|
890
909
|
"kornia"
|
|
891
910
|
],
|
|
892
|
-
"fixed_in_release":
|
|
911
|
+
"fixed_in_release": "0.9.0",
|
|
893
912
|
"repro": "import torch\nfrom kornia.geometry.plane import Hyperplane\nfrom kornia.geometry.vector import Vector3\np0 = Vector3(torch.tensor([0.0, 0.0, 0.0], dtype=torch.float16))\np1 = Vector3(torch.tensor([300.0, 0.0, 0.0], dtype=torch.float16))\np2 = Vector3(torch.tensor([0.0, 300.0, 0.0], dtype=torch.float16))\nactual = Hyperplane.through(p0, p1, p2).normal.data.flatten().tolist()\nexpected = [0.0, 0.0, -1.0] # (p2 - p0) x (p1 - p0)\n",
|
|
894
913
|
"check": {
|
|
895
914
|
"type": "allclose",
|
|
@@ -910,7 +929,7 @@
|
|
|
910
929
|
"torch",
|
|
911
930
|
"kornia"
|
|
912
931
|
],
|
|
913
|
-
"fixed_in_release":
|
|
932
|
+
"fixed_in_release": "0.9.0",
|
|
914
933
|
"repro": "import torch\nfrom kornia.filters import filter2d\ntorch.manual_seed(0)\nimage = torch.rand(2, 3, 6, 6)\nkernel = torch.rand(2, 3, 3) # one kernel per sample\nactual = (filter2d(image.to(memory_format=torch.channels_last), kernel) - filter2d(image, kernel)).abs().max().item()\nexpected = 0.0\n",
|
|
915
934
|
"check": {
|
|
916
935
|
"type": "equal"
|
|
@@ -929,7 +948,7 @@
|
|
|
929
948
|
"torch",
|
|
930
949
|
"kornia"
|
|
931
950
|
],
|
|
932
|
-
"fixed_in_release":
|
|
951
|
+
"fixed_in_release": "0.9.0",
|
|
933
952
|
"repro": "import torch\nfrom kornia.losses import lovasz_hinge_loss\ntorch.manual_seed(0)\npred = torch.randn(2, 1, 4, 4)\ntarget = torch.randint(0, 2, (2, 4, 4))\nactual = [str(lovasz_hinge_loss(pred.half(), target).dtype), str(lovasz_hinge_loss(pred.double(), target).dtype)]\nexpected = [\"torch.float16\", \"torch.float64\"]\n",
|
|
934
953
|
"check": {
|
|
935
954
|
"type": "equal"
|
|
@@ -948,7 +967,7 @@
|
|
|
948
967
|
"torch",
|
|
949
968
|
"kornia"
|
|
950
969
|
],
|
|
951
|
-
"fixed_in_release":
|
|
970
|
+
"fixed_in_release": "0.9.0",
|
|
952
971
|
"repro": "import torch\nfrom kornia.filters import get_box_kernel1d\nkernel = get_box_kernel1d(3)\nkernel[0, 0] = 0.0\nactual = kernel.flatten().tolist()\nexpected = [0.0, 1.0 / 3.0, 1.0 / 3.0]\n",
|
|
953
972
|
"check": {
|
|
954
973
|
"type": "allclose",
|
|
@@ -1117,7 +1136,7 @@
|
|
|
1117
1136
|
"torch",
|
|
1118
1137
|
"kornia"
|
|
1119
1138
|
],
|
|
1120
|
-
"fixed_in_release":
|
|
1139
|
+
"fixed_in_release": "0.9.0",
|
|
1121
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",
|
|
1122
1141
|
"check": {
|
|
1123
1142
|
"type": "allclose",
|
|
@@ -1167,6 +1186,46 @@
|
|
|
1167
1186
|
"type": "sign"
|
|
1168
1187
|
},
|
|
1169
1188
|
"before_fix": "[0.0] on peft main 91ca7945; with the fix the ten forwards spread by 0.56"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
"id": "kornia-5652-gftt-response-flat-patch-nan-gradient",
|
|
1192
|
+
"repo": "kornia/kornia",
|
|
1193
|
+
"pr": 5653,
|
|
1194
|
+
"issue": 5652,
|
|
1195
|
+
"merged_at": "2026-10-09T11:16:37Z",
|
|
1196
|
+
"kind": "gradient",
|
|
1197
|
+
"title": "gftt_response had a nan gradient on every flat patch",
|
|
1198
|
+
"symptom": "The smaller eigenvalue of the structure tensor was 0.5 * (trace - sqrt((trace**2 - 4 * det).abs())). Wherever the 9 x 9 footprint is flat (zero padding, a black border, a saturated region) the blurred moments are exactly zero, the sqrt backward is inf * 0 = nan and the whole flat region gets a nan gradient, while harris_response and hessian_response stay finite. The patch writes the discriminant as the sum of squares (dx2 - dy2)**2 + 4 * dxy**2, which is never negative, and guards its sqrt with a where, so the gradient is zero where the discriminant is zero; the forward moves at rounding level only.",
|
|
1199
|
+
"requires": [
|
|
1200
|
+
"torch",
|
|
1201
|
+
"kornia"
|
|
1202
|
+
],
|
|
1203
|
+
"fixed_in_release": "0.9.0",
|
|
1204
|
+
"repro": "import torch\nimport kornia\ntorch.manual_seed(0)\nimg = torch.zeros(1, 1, 64, 64)\nimg[..., 16:48, :] = torch.rand(1, 1, 32, 64)\nimg.requires_grad_(True)\n(grad,) = torch.autograd.grad(kornia.feature.gftt_response(img).sum(), img)\nactual = [grad[0, 0, 0, 0].item(), grad[0, 0, 63, 63].item(), float(torch.isfinite(grad).all())]\nexpected = [0.0, 0.0, 1.0]\n",
|
|
1205
|
+
"check": {
|
|
1206
|
+
"type": "equal"
|
|
1207
|
+
},
|
|
1208
|
+
"before_fix": "[nan, nan, 0.0] on kornia 0.8.3, 2048 nan pixels in the flat rows"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
"id": "kornia-5654-equalize-clahe-residual-in-first-bins",
|
|
1212
|
+
"repo": "kornia/kornia",
|
|
1213
|
+
"pr": 5655,
|
|
1214
|
+
"issue": 5654,
|
|
1215
|
+
"merged_at": "2026-10-09T10:38:46Z",
|
|
1216
|
+
"kind": "redistribution",
|
|
1217
|
+
"title": "equalize_clahe wrote the clipping residual into the first histogram bins, small tiles came out tens of levels brighter than OpenCV",
|
|
1218
|
+
"symptom": "After clipping a tile histogram at the clip limit, the residual of the clipped pixels was written into the first `residual` bins instead of being spread over every max(256 // residual, 1)-th bin from bin 0 as OpenCV does. The residual piled onto the darkest levels and lifted the whole lookup table, so small tiles came out up to 38 levels brighter than cv2.createCLAHE (mean over a 96 x 96 image with an 8 x 8 grid and clip 2.0). On a 2 x 4 tile holding levels 0 and 100 with clip 40, level 100 mapped to 255 where the spread residual gives 159. The patch spreads the residual in one helper shared by the scalar clip path, the per-sample clip path of RandomClahe and the differentiable path.",
|
|
1219
|
+
"requires": [
|
|
1220
|
+
"torch",
|
|
1221
|
+
"kornia"
|
|
1222
|
+
],
|
|
1223
|
+
"fixed_in_release": "0.9.0",
|
|
1224
|
+
"repro": "import torch\nimport kornia\nimg = torch.tensor([[0.0] * 4, [100.0] * 4]).div(255.0)[None, None]\nout = (kornia.enhance.equalize_clahe(img, 40.0, (1, 1)) * 255).round()[0, 0]\nactual = [out[1, 0].item()]\nexpected = [159.0]\n",
|
|
1225
|
+
"check": {
|
|
1226
|
+
"type": "equal"
|
|
1227
|
+
},
|
|
1228
|
+
"before_fix": "[255.0] on kornia 0.8.3"
|
|
1170
1229
|
}
|
|
1171
1230
|
]
|
|
1172
1231
|
}
|
|
@@ -136,6 +136,12 @@ RULES = {
|
|
|
136
136
|
"take the value from the real argument and the gradient from a safe one, f(where(at_bound, safe, x)) with the value fixed up by where, or atan2 instead of acos for an angle",
|
|
137
137
|
"kornia #4229 (found by the kornia conventions audit), _cdist's clamp(min=0.0).sqrt() gave nan gradients for identical float16 descriptors on torch 2.5.1; kornia #5500, clamp(-1, 1).acos() in angle_error_mat had a -inf backward at exactly 0 and 180 degrees on torch 2.5.1",
|
|
138
138
|
),
|
|
139
|
+
"dropout-never-applied": (
|
|
140
|
+
"medium",
|
|
141
|
+
"a Dropout, DropPath or a ModuleDict of them is 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 and every training forward of one input agrees",
|
|
142
|
+
"apply it in forward, or hand its rate to the function that does (scaled_dot_product_attention(dropout_p=self.dropout.p)), or drop the argument so the caller learns it is not supported",
|
|
143
|
+
"peft #3830, OFTLayer built module_dropout into self.oft_dropout and never called it, so module_dropout=0.5 trained like 0",
|
|
144
|
+
),
|
|
139
145
|
}
|
|
140
146
|
SEVERITY_ORDER = {"high": 0, "medium": 1, "info": 2}
|
|
141
147
|
|
|
@@ -653,6 +659,79 @@ def python_files(root: str, include_tests: bool = False) -> List[str]:
|
|
|
653
659
|
return out
|
|
654
660
|
|
|
655
661
|
|
|
662
|
+
DROPOUT_CLASSES = re.compile(r"^(Dropout\w*|AlphaDropout|FeatureAlphaDropout|DropPath|StochasticDepth|DropBlock\w*)$")
|
|
663
|
+
DROPOUT_NAMES = re.compile(r"drop_?out|drop_?path|stochastic_?depth|drop_?block", re.I)
|
|
664
|
+
DROPOUT_HOLDERS = {"ModuleDict", "ModuleList", "Identity"}
|
|
665
|
+
RATE_ATTRS = {"p", "prob", "rate", "drop_prob", "drop_rate", "dropout_p", "dropout", "drop_path_rate"}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _dropout_like(value: ast.AST, attr: str) -> bool:
|
|
669
|
+
"""Whether ``self.<attr> = value`` builds a dropout: a Dropout-class call, either branch of a conditional
|
|
670
|
+
expression, or a ModuleDict, ModuleList or Identity under a name that says dropout (filled in later)."""
|
|
671
|
+
if isinstance(value, ast.IfExp):
|
|
672
|
+
return _dropout_like(value.body, attr) or _dropout_like(value.orelse, attr)
|
|
673
|
+
if not isinstance(value, ast.Call):
|
|
674
|
+
return False
|
|
675
|
+
name, _ = _callee(value)
|
|
676
|
+
if name is None:
|
|
677
|
+
return False
|
|
678
|
+
return bool(DROPOUT_CLASSES.match(name)) or (name in DROPOUT_HOLDERS and bool(DROPOUT_NAMES.search(attr)))
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _applied(node: ast.Attribute, parents: dict) -> bool:
|
|
682
|
+
"""Whether this read of ``self.x`` uses the module: a call, a call through a subscript, an argument, a return
|
|
683
|
+
or yield, an operand, a loop target, an alias to a local, or a read of its rate. Assigning to it, filling it
|
|
684
|
+
(``self.x.update(...)``, ``self.x[k] = ...``) and reading other attributes of it are not uses."""
|
|
685
|
+
parent = parents.get(node)
|
|
686
|
+
if isinstance(parent, ast.Subscript) and parent.value is node:
|
|
687
|
+
node, parent = parent, parents.get(parent)
|
|
688
|
+
if isinstance(parent, ast.Call):
|
|
689
|
+
return parent.func is node or node in parent.args or any(k.value is node for k in parent.keywords)
|
|
690
|
+
if isinstance(parent, ast.Attribute):
|
|
691
|
+
return parent.attr in RATE_ATTRS
|
|
692
|
+
if isinstance(parent, ast.Assign):
|
|
693
|
+
return parent.value is node
|
|
694
|
+
if isinstance(parent, (ast.AnnAssign, ast.AugAssign)):
|
|
695
|
+
return parent.value is node
|
|
696
|
+
if isinstance(parent, ast.For):
|
|
697
|
+
return parent.iter is node
|
|
698
|
+
return isinstance(parent, (ast.Return, ast.Yield, ast.BinOp, ast.Compare, ast.IfExp, ast.Tuple, ast.List,
|
|
699
|
+
ast.Dict, ast.Starred, ast.keyword, ast.FormattedValue, ast.Await, ast.BoolOp))
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _dropouts_never_applied(tree: ast.Module, path: str, source_lines: Sequence[str]) -> List[Finding]:
|
|
703
|
+
"""The ``dropout-never-applied`` rule: a file-level pass, since the module is built in one method and used, if
|
|
704
|
+
at all, in another, or in a sibling class of the same file. Subclasses of ``Sequential`` run every attribute
|
|
705
|
+
in order and are not read."""
|
|
706
|
+
parents: dict = {}
|
|
707
|
+
for node in ast.walk(tree):
|
|
708
|
+
for child in ast.iter_child_nodes(node):
|
|
709
|
+
parents[child] = node
|
|
710
|
+
used: set = set()
|
|
711
|
+
for node in ast.walk(tree):
|
|
712
|
+
if isinstance(node, ast.Attribute) and _applied(node, parents):
|
|
713
|
+
used.add(node.attr)
|
|
714
|
+
findings: List[Finding] = []
|
|
715
|
+
for cls in ast.walk(tree):
|
|
716
|
+
if not isinstance(cls, ast.ClassDef) or any(ast.unparse(b).endswith("Sequential") for b in cls.bases):
|
|
717
|
+
continue
|
|
718
|
+
for fn in cls.body:
|
|
719
|
+
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
720
|
+
continue
|
|
721
|
+
for node in _own_nodes(fn):
|
|
722
|
+
if not (isinstance(node, ast.Assign) and len(node.targets) == 1):
|
|
723
|
+
continue
|
|
724
|
+
target = node.targets[0]
|
|
725
|
+
if not (isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name)
|
|
726
|
+
and target.value.id == "self"):
|
|
727
|
+
continue
|
|
728
|
+
if target.attr not in used and _dropout_like(node.value, target.attr):
|
|
729
|
+
qualname = cls.name + "." + fn.name
|
|
730
|
+
findings.append(Finding("dropout-never-applied", path, node.lineno, qualname,
|
|
731
|
+
source_lines[node.lineno - 1].strip()[:160]))
|
|
732
|
+
return findings
|
|
733
|
+
|
|
734
|
+
|
|
656
735
|
def scan_source(source: str, path: str = "<string>") -> Tuple[List[Finding], List[HotSpot]]:
|
|
657
736
|
try:
|
|
658
737
|
with warnings.catch_warnings():
|
|
@@ -669,6 +748,7 @@ def scan_source(source: str, path: str = "<string>") -> Tuple[List[Finding], Lis
|
|
|
669
748
|
findings.extend(f)
|
|
670
749
|
if spot.calls:
|
|
671
750
|
spots.append(spot)
|
|
751
|
+
findings.extend(_dropouts_never_applied(tree, path, lines))
|
|
672
752
|
return findings, spots
|
|
673
753
|
|
|
674
754
|
|
|
@@ -165,6 +165,17 @@ SNIPPET = textwrap.dedent(
|
|
|
165
165
|
e = (-(x.pow(2))).exp()
|
|
166
166
|
f = torch.exp(-torch.square(x))
|
|
167
167
|
return a, b, c, d, e, f
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Unregularised(torch.nn.Module):
|
|
171
|
+
def __init__(self, dim, p):
|
|
172
|
+
super().__init__()
|
|
173
|
+
self.proj = torch.nn.Linear(dim, dim)
|
|
174
|
+
self.attn_drop = torch.nn.Dropout(p)
|
|
175
|
+
self.proj_drop = torch.nn.Dropout(p)
|
|
176
|
+
|
|
177
|
+
def forward(self, x):
|
|
178
|
+
return self.proj_drop(self.proj(x))
|
|
168
179
|
'''
|
|
169
180
|
)
|
|
170
181
|
|
|
@@ -182,6 +193,7 @@ EXPECTED = {
|
|
|
182
193
|
("acos-for-angle", "angle_between"),
|
|
183
194
|
("eps-floor", "one_hot_floor"),
|
|
184
195
|
("eps-floor", "Smoother.forward"),
|
|
196
|
+
("dropout-never-applied", "Unregularised.__init__"),
|
|
185
197
|
("one-minus-cos", "so3_jacobian"),
|
|
186
198
|
("small-angle-division", "so3_jacobian"),
|
|
187
199
|
("where-nan-gradient", "so3_jacobian_guarded"),
|
|
@@ -215,6 +227,88 @@ def test_every_rule_fires_once_on_its_snippet_and_the_guard_silences_it():
|
|
|
215
227
|
assert busiest.function == "misc" and busiest.calls == 7 and "exp" in busiest.names
|
|
216
228
|
|
|
217
229
|
|
|
230
|
+
DROPOUTS = textwrap.dedent(
|
|
231
|
+
'''
|
|
232
|
+
import torch
|
|
233
|
+
from torch import nn
|
|
234
|
+
import torch.nn.functional as F
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class Attention(nn.Module):
|
|
238
|
+
def __init__(self, dim, p):
|
|
239
|
+
super().__init__()
|
|
240
|
+
self.dropout = nn.Dropout(p) # its rate is read and handed to the kernel
|
|
241
|
+
|
|
242
|
+
def forward(self, q, k, v):
|
|
243
|
+
p = self.dropout.p if self.training else 0.0
|
|
244
|
+
return F.scaled_dot_product_attention(q, k, v, dropout_p=p)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class Block(nn.Module):
|
|
248
|
+
def __init__(self, dim, p, drop_path):
|
|
249
|
+
super().__init__()
|
|
250
|
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() # never applied
|
|
251
|
+
self.drop = nn.Dropout(p) if p > 0.0 else nn.Identity()
|
|
252
|
+
self.mlp = Mlp(dim, drop=self.drop) # passed on
|
|
253
|
+
|
|
254
|
+
def forward(self, x):
|
|
255
|
+
return x + self.mlp(x)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class Mlp(nn.Sequential):
|
|
259
|
+
def __init__(self, dim, drop):
|
|
260
|
+
super().__init__()
|
|
261
|
+
self.fc = nn.Linear(dim, dim)
|
|
262
|
+
self.drop = nn.Dropout(0.5) # a Sequential runs every attribute
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class Adapters(nn.Module):
|
|
266
|
+
def __init__(self):
|
|
267
|
+
super().__init__()
|
|
268
|
+
self.lora_dropout = nn.ModuleDict({}) # filled and called through a subscript
|
|
269
|
+
self.oft_dropout = nn.ModuleDict({}) # filled and never called
|
|
270
|
+
|
|
271
|
+
def update_layer(self, name, p):
|
|
272
|
+
self.lora_dropout.update(nn.ModuleDict({name: nn.Dropout(p)}))
|
|
273
|
+
self.oft_dropout.update(nn.ModuleDict({name: nn.Dropout(p)}))
|
|
274
|
+
|
|
275
|
+
def forward(self, x, name):
|
|
276
|
+
return self.lora_dropout[name](x)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Head(nn.Module):
|
|
280
|
+
def __init__(self, dim, p):
|
|
281
|
+
super().__init__()
|
|
282
|
+
self.dense = nn.Linear(dim, dim)
|
|
283
|
+
self.dropout = nn.Dropout(p) # applied by a sibling class of this file
|
|
284
|
+
self.drops = nn.ModuleList([nn.Dropout(p) for _ in range(2)]) # iterated
|
|
285
|
+
|
|
286
|
+
def forward(self, x):
|
|
287
|
+
for drop in self.drops:
|
|
288
|
+
x = drop(x)
|
|
289
|
+
return x
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class Wrapped(nn.Module):
|
|
293
|
+
def forward(self, x):
|
|
294
|
+
return self.inner.dropout(self.dense(x))
|
|
295
|
+
'''
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_dropout_never_applied_reads_the_whole_file_and_knows_the_ways_a_module_is_used():
|
|
300
|
+
findings, _ = scan.scan_source(DROPOUTS, "dropouts.py")
|
|
301
|
+
assert [(f.function, f.line) for f in findings if f.rule == "dropout-never-applied"] == [
|
|
302
|
+
("Block.__init__", 20),
|
|
303
|
+
("Adapters.__init__", 39),
|
|
304
|
+
]
|
|
305
|
+
assert findings[0].snippet.startswith("self.drop_path = DropPath(drop_path)")
|
|
306
|
+
assert findings[1].snippet.startswith("self.oft_dropout = nn.ModuleDict({})")
|
|
307
|
+
# a float rate is not a module, and a function is not a class
|
|
308
|
+
quiet = scan.scan_source("class A:\n def __init__(self, p):\n self.dropout = p\n\ndef f(self):\n self.dropout = Dropout(0.1)\n", "q.py")[0]
|
|
309
|
+
assert not quiet
|
|
310
|
+
|
|
311
|
+
|
|
218
312
|
def test_scan_is_quiet_about_the_escape_sequences_of_the_scanned_file():
|
|
219
313
|
source = 'import torch\n\n\ndef f(x):\n return "\\%" + str(torch.exp(x))\n'
|
|
220
314
|
with warnings.catch_warnings():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|