ulpwise 0.3.3__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.
- {ulpwise-0.3.3 → ulpwise-0.3.4}/CHANGELOG.md +18 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/Cargo.lock +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.4}/Cargo.toml +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.4}/PKG-INFO +19 -4
- {ulpwise-0.3.3 → ulpwise-0.3.4}/README.md +18 -3
- {ulpwise-0.3.3 → ulpwise-0.3.4}/pyproject.toml +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/__init__.py +1 -1
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/corpus/cases.json +19 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/scan.py +80 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/tests/test_scan.py +94 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/.github/dependabot.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/.github/workflows/ci.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/.github/workflows/release.yml +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/.gitignore +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/CONTRIBUTING.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/LICENSE-APACHE +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/LICENSE-MIT +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/examples/sqrt_knife_values.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/examples/torch_sqrt_conformance.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/__main__.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/corpus/__init__.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/plugin.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/python/ulpwise/survey.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/edge.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/exact.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/knife.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/lib.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/py.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/src/ulp.rs +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/README.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/results.csv +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-09/results.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/README.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/results.csv +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/studies/accuracy-survey-2026-10-half/results.md +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/tests/test_core.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/tests/test_corpus.py +0 -0
- {ulpwise-0.3.3 → ulpwise-0.3.4}/tests/test_survey.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
3
21
|
## 0.3.3 (2026-10-08)
|
|
4
22
|
|
|
5
23
|
- Two regression corpus cases for peft, both open upstream with a fix PR: peft #3769, `add_weighted_adapter` with
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ulpwise
|
|
3
|
-
Version: 0.3.
|
|
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 |
|
|
@@ -249,9 +250,9 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
|
|
|
249
250
|
```
|
|
250
251
|
|
|
251
252
|
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.
|
|
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.
|
|
255
256
|
|
|
256
257
|
| rule | severity | pattern |
|
|
257
258
|
|---|---|---|
|
|
@@ -269,6 +270,7 @@ comes from.
|
|
|
269
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) |
|
|
270
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) |
|
|
271
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) |
|
|
272
274
|
|
|
273
275
|
On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
|
|
274
276
|
`one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
|
|
@@ -284,6 +286,19 @@ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where
|
|
|
284
286
|
boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
|
|
285
287
|
inference code that is never differentiated, and nothing in torchvision.
|
|
286
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
|
+
|
|
287
302
|
`--run` adds the dynamic half. The module level functions with the most elementary math are
|
|
288
303
|
imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
|
|
289
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 |
|
|
@@ -220,9 +221,9 @@ ulpwise scan path/to/repo --rules one-minus-cos,small-angle-division --top 40
|
|
|
220
221
|
```
|
|
221
222
|
|
|
222
223
|
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.
|
|
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.
|
|
226
227
|
|
|
227
228
|
| rule | severity | pattern |
|
|
228
229
|
|---|---|---|
|
|
@@ -240,6 +241,7 @@ comes from.
|
|
|
240
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) |
|
|
241
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) |
|
|
242
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) |
|
|
243
245
|
|
|
244
246
|
On kornia `main` at `e05b0ee` the scan takes 4 s for 506 files and reports 35 findings. The
|
|
245
247
|
`one-minus-cos` and `small-angle-division` findings are the four lines of `So3.right_jacobian` and
|
|
@@ -255,6 +257,19 @@ report seven lines: three `clamp` bounds in `_solve_cubic_real`, and four `where
|
|
|
255
257
|
boxes and polygons. On ultralytics `main` at `8df3534` they report three lines, all in metrics or
|
|
256
258
|
inference code that is never differentiated, and nothing in torchvision.
|
|
257
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
|
+
|
|
258
273
|
`--run` adds the dynamic half. The module level functions with the most elementary math are
|
|
259
274
|
imported and called with the same 91 point grid (both signs of `1e-8` to `1e3`, and zero) for every
|
|
260
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.
|
|
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" }
|
|
@@ -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",
|
|
@@ -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
|