silentbug 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shushant Kumar Choudhary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: silentbug
3
+ Version: 0.1.0
4
+ Summary: Find silent PyTorch training bugs: accidental broadcasting, parameters that never learn, and the line that caused them.
5
+ Author: Shushant Kumar Choudhary
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Shushant17711/silentbug
8
+ Project-URL: Issues, https://github.com/Shushant17711/silentbug/issues
9
+ Project-URL: Changelog, https://github.com/Shushant17711/silentbug/blob/main/CHANGELOG.md
10
+ Keywords: pytorch,debugging,broadcasting,gradients,training
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Classifier: Topic :: Software Development :: Debuggers
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: torch>=2.1
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest; extra == "test"
20
+ Dynamic: license-file
21
+
22
+ # silentbug
23
+
24
+ Your model trains, the loss goes down a little, and nothing errors. But one tensor is
25
+ broadcast against another, half your parameters never get a gradient, or softmax runs
26
+ twice. silentbug watches a few training steps and tells you **which** silent bug it is,
27
+ **on which line**, **why** it matters and **how to fix it**.
28
+
29
+ ```
30
+ $ silentbug run train.py
31
+ silentbug: checked 3 backward pass(es), 3 optimizer step(s) on Net
32
+ 2 likely bug(s), 0 thing(s) to check
33
+
34
+ [BUG ] 1. mse_loss got input (100, 1) and target (100,)
35
+ at train.py:13: loss = crit(yhat, y)
36
+ why: they broadcast to (100, 100), so each prediction is compared with many targets
37
+ instead of its own. The loss still goes down a little, which hides the bug.
38
+ fix: reshape one side so input and target have the same shape (often `.squeeze(-1)`)
39
+
40
+ [BUG ] 2. 2 parameters never get a gradient: cut by detach
41
+ at train.py:21: g = self.gate(h).detach()
42
+ parameters: gate.bias, gate.weight
43
+ fix: remove `.detach()` if these parameters should learn
44
+ ```
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install git+https://github.com/Shushant17711/silentbug@v0.1.0 # Python 3.10+, PyTorch 2.1+
50
+ ```
51
+
52
+ From a clone, for development:
53
+
54
+ ```bash
55
+ git clone https://github.com/Shushant17711/silentbug && cd silentbug
56
+ pip install -e ".[test]" && pytest tests
57
+ ```
58
+
59
+ ## Use it
60
+
61
+ On a script, with no code changes. It runs the first N optimizer steps (default 3), then stops:
62
+
63
+ ```bash
64
+ silentbug run train.py --lr 3e-4 # your script's own arguments go after it
65
+ silentbug run --steps 10 train.py
66
+ silentbug run --json - train.py # JSON report on stdout
67
+ silentbug run -m mypackage.train # like python -m
68
+ ```
69
+
70
+ Exit codes: 0 nothing found, 1 likely bugs found, 2 only things to check.
71
+
72
+ Or around a block of code, for example in a test:
73
+
74
+ ```python
75
+ import silentbug
76
+
77
+ with silentbug.check() as report:
78
+ for step in range(3):
79
+ loss = loss_fn(model(x), y)
80
+ loss.backward()
81
+ opt.step(); opt.zero_grad()
82
+
83
+ assert not report.bugs, str(report)
84
+ ```
85
+
86
+ Models and optimizers are found on their own. Pass them in (`silentbug.check(model,
87
+ optimizer=opt)`) only when the model is never called inside the block.
88
+
89
+ ## What it finds
90
+
91
+ | Kind | Example | Level |
92
+ |---|---|---|
93
+ | `broadcast` | `(N,1) - (N,)` gives an `(N,N)` loss, or `mse_loss(input (B,1), target (B,))` | BUG |
94
+ | `dead-param` | parameters with no gradient, with the cause: `.detach()`, `.data`, `.item()`/NumPy round trip, `torch.no_grad()`, argmax/topk indices, a module never called (LoRA bypassed), an output that never reaches the loss | BUG |
95
+ | `zero-grad` | gradients that exist but are always 0 (`round`, `sign`, `floor`, ...) | BUG |
96
+ | `not-in-optimizer` | layers created after the optimizer was built | BUG |
97
+ | `stale-optimizer` | the optimizer still updates parameters that were replaced | BUG |
98
+ | `double-activation` | softmax fed to `cross_entropy`/`nll_loss`, sigmoid fed to `BCEWithLogits`, softmax then `log_softmax` | BUG |
99
+ | `eval-mode` | the model is still in `eval()` while it trains | BUG |
100
+ | `grad-not-cleared` | `optimizer.step()` uses gradients left over from the previous step | BUG |
101
+ | `batchnorm-batch1` | batch norm in training mode with a batch of 1 | CHECK |
102
+
103
+ BUG means almost certainly wrong. CHECK means often wrong but sometimes intended (for example an
104
+ outer product written as a broadcast).
105
+
106
+ How it works: a `TorchFunctionMode` sees every torch call. It checks the shapes going into
107
+ broadcasting ops and losses, and tags each tensor with a bitmask of the trainable parameters it
108
+ depends on. When a tensor built from parameters comes out of an op without a gradient, that line is
109
+ recorded as a cut. Gradient hooks on every parameter record which ones received a gradient, and
110
+ global module and optimizer hooks find the models and optimizers. After the run, each parameter
111
+ that never got a gradient is matched to the cut or missing call that explains it.
112
+
113
+ ## Tested on real bugs
114
+
115
+ `benchmarks/realworld/` has 15 reproducers of silent bugs from public issues (PyTorch, PEFT,
116
+ torchtitan, kornia, DeepTab, poolformer, Lightning Bolts, simpletransformers and others). Each one
117
+ is a short script that keeps the buggy code pattern from the issue. Each also has a fixed version
118
+ (`f*.py`) that must produce no findings.
119
+
120
+ ```bash
121
+ cd benchmarks/realworld && python score.py $(which silentbug)
122
+ ```
123
+
124
+ | Case | Issue | Bug | Found at the right line |
125
+ |---|---|---|---|
126
+ | r01 | pytorch/pytorch#16045 | `MSELoss` on `(N,1)` vs `(N,)` | yes |
127
+ | r02 | OpenTabular/DeepTab#441 | `(n,1)` target unsqueezed to `(B,1,1)` | yes |
128
+ | r03 | kornia/kornia#6157 | SSIM loss broadcasts a batch-1 target | yes |
129
+ | r04 | trnsci/trnsparse#67 | `y` broadcasts the spmv output to `(12,12)` | yes |
130
+ | r05 | huggingface/peft#1612 | weights read directly, LoRA wrapper never runs | yes |
131
+ | r06 | sail-sg/poolformer#16 | stage-0 output dropped, `norm0` never trains | yes |
132
+ | r07 | pytorch/torchtitan#4802 | top-k indices cut the indexer's gradient | yes |
133
+ | r08 | pytorch/pytorch#14467 | GRU created after the optimizer | yes |
134
+ | r09 | RAI-SCC/jigsaw#224 | reshard replaces parameters, optimizer goes stale | yes |
135
+ | r10 | gitabcworld/MatchingNetworks#5 | softmax, then `cross_entropy` | yes |
136
+ | r11 | cloudtrainerwork/chesstrader#49 | softmax output fed to `CrossEntropyLoss` | yes |
137
+ | r12 | Lightning-Universe/lightning-bolts#654 | `log_softmax(softmax(x))` | yes |
138
+ | r13 | YuanGongND/ssast#2 | sigmoid fed to `BCEWithLogitsLoss` | yes |
139
+ | r14 | ThilinaRajapakse/simpletransformers#941 | `model.eval()` never undone | yes |
140
+ | r15 | juweipku/CoCo#1 | no `optimizer.zero_grad()` | yes |
141
+
142
+ Result: 15 of 15 found at the right line, 15 of 15 fixed versions report nothing.
143
+
144
+ ## Overhead
145
+
146
+ 6-layer MLP, width 1024, batch 256, AdamW, CPU: 30.9 ms/step plain, 33.8 ms/step under
147
+ silentbug (1.09x). It is meant to run for a few steps, not for a whole training run.
148
+
149
+ ## Limits
150
+
151
+ ## Tested setups
152
+
153
+ * PyTorch 2.15 on CPU and PyTorch 2.14 with CUDA 13.0 on an RTX 4060: unit tests and all 15 benchmark
154
+ cases plus their 15 fixed versions, on both devices.
155
+ * Mixed precision (`torch.autocast` with fp16 + `GradScaler`, and bf16): bugs found, clean code stays quiet.
156
+ * `torch.compile` (a compiled model, and a compiled loss function): findings name the original module.
157
+ * DDP with 2 processes (`torchrun`, gloo): DDP's own internal detach is not reported. Only rank 0 prints.
158
+ * Real code, unchanged: karpathy/nanoGPT (CUDA, with and without `compile`, with gradient accumulation)
159
+ and pytorch/examples `word_language_model` (manual SGD with no optimizer). Neither gave a false alarm.
160
+ When `model.zero_grad()` was deleted from the second one, silentbug reported it at the update line.
161
+
162
+ ## Limits
163
+
164
+ * Not tested with FSDP, tensor or pipeline parallelism, or on multiple GPUs at once.
165
+ * `torch.compile`: while silentbug watches, the compiler stops at silentbug's hooks and runs the
166
+ compiled code op by op. That is why findings inside a compiled function point at the exact line
167
+ there. It also means bugs that only happen in compiled graphs (compiler bugs) are not seen, and
168
+ the watched steps run at eager speed. Compilation goes back to normal once the check ends.
169
+ * It only sees what runs during the steps it watches. A branch that runs only every 100 steps is
170
+ not checked.
171
+ * `broadcast` checks the first three tensor arguments of each op.
172
+
173
+ ## Related tools
174
+
175
+ * [nantrace](https://github.com/Optimus2007/nantrace) finds where NaN and Inf start. silentbug
176
+ finds the bugs that never produce a NaN.
177
+ * [jaxtyping](https://github.com/patrick-kidger/jaxtyping) checks shapes if you annotate them.
178
+ silentbug needs no annotations.
179
+ * `TORCH_DISTRIBUTED_DEBUG=DETAIL` names unused parameters under DDP, but not why they are unused.
180
+
181
+ ## License
182
+
183
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,162 @@
1
+ # silentbug
2
+
3
+ Your model trains, the loss goes down a little, and nothing errors. But one tensor is
4
+ broadcast against another, half your parameters never get a gradient, or softmax runs
5
+ twice. silentbug watches a few training steps and tells you **which** silent bug it is,
6
+ **on which line**, **why** it matters and **how to fix it**.
7
+
8
+ ```
9
+ $ silentbug run train.py
10
+ silentbug: checked 3 backward pass(es), 3 optimizer step(s) on Net
11
+ 2 likely bug(s), 0 thing(s) to check
12
+
13
+ [BUG ] 1. mse_loss got input (100, 1) and target (100,)
14
+ at train.py:13: loss = crit(yhat, y)
15
+ why: they broadcast to (100, 100), so each prediction is compared with many targets
16
+ instead of its own. The loss still goes down a little, which hides the bug.
17
+ fix: reshape one side so input and target have the same shape (often `.squeeze(-1)`)
18
+
19
+ [BUG ] 2. 2 parameters never get a gradient: cut by detach
20
+ at train.py:21: g = self.gate(h).detach()
21
+ parameters: gate.bias, gate.weight
22
+ fix: remove `.detach()` if these parameters should learn
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install git+https://github.com/Shushant17711/silentbug@v0.1.0 # Python 3.10+, PyTorch 2.1+
29
+ ```
30
+
31
+ From a clone, for development:
32
+
33
+ ```bash
34
+ git clone https://github.com/Shushant17711/silentbug && cd silentbug
35
+ pip install -e ".[test]" && pytest tests
36
+ ```
37
+
38
+ ## Use it
39
+
40
+ On a script, with no code changes. It runs the first N optimizer steps (default 3), then stops:
41
+
42
+ ```bash
43
+ silentbug run train.py --lr 3e-4 # your script's own arguments go after it
44
+ silentbug run --steps 10 train.py
45
+ silentbug run --json - train.py # JSON report on stdout
46
+ silentbug run -m mypackage.train # like python -m
47
+ ```
48
+
49
+ Exit codes: 0 nothing found, 1 likely bugs found, 2 only things to check.
50
+
51
+ Or around a block of code, for example in a test:
52
+
53
+ ```python
54
+ import silentbug
55
+
56
+ with silentbug.check() as report:
57
+ for step in range(3):
58
+ loss = loss_fn(model(x), y)
59
+ loss.backward()
60
+ opt.step(); opt.zero_grad()
61
+
62
+ assert not report.bugs, str(report)
63
+ ```
64
+
65
+ Models and optimizers are found on their own. Pass them in (`silentbug.check(model,
66
+ optimizer=opt)`) only when the model is never called inside the block.
67
+
68
+ ## What it finds
69
+
70
+ | Kind | Example | Level |
71
+ |---|---|---|
72
+ | `broadcast` | `(N,1) - (N,)` gives an `(N,N)` loss, or `mse_loss(input (B,1), target (B,))` | BUG |
73
+ | `dead-param` | parameters with no gradient, with the cause: `.detach()`, `.data`, `.item()`/NumPy round trip, `torch.no_grad()`, argmax/topk indices, a module never called (LoRA bypassed), an output that never reaches the loss | BUG |
74
+ | `zero-grad` | gradients that exist but are always 0 (`round`, `sign`, `floor`, ...) | BUG |
75
+ | `not-in-optimizer` | layers created after the optimizer was built | BUG |
76
+ | `stale-optimizer` | the optimizer still updates parameters that were replaced | BUG |
77
+ | `double-activation` | softmax fed to `cross_entropy`/`nll_loss`, sigmoid fed to `BCEWithLogits`, softmax then `log_softmax` | BUG |
78
+ | `eval-mode` | the model is still in `eval()` while it trains | BUG |
79
+ | `grad-not-cleared` | `optimizer.step()` uses gradients left over from the previous step | BUG |
80
+ | `batchnorm-batch1` | batch norm in training mode with a batch of 1 | CHECK |
81
+
82
+ BUG means almost certainly wrong. CHECK means often wrong but sometimes intended (for example an
83
+ outer product written as a broadcast).
84
+
85
+ How it works: a `TorchFunctionMode` sees every torch call. It checks the shapes going into
86
+ broadcasting ops and losses, and tags each tensor with a bitmask of the trainable parameters it
87
+ depends on. When a tensor built from parameters comes out of an op without a gradient, that line is
88
+ recorded as a cut. Gradient hooks on every parameter record which ones received a gradient, and
89
+ global module and optimizer hooks find the models and optimizers. After the run, each parameter
90
+ that never got a gradient is matched to the cut or missing call that explains it.
91
+
92
+ ## Tested on real bugs
93
+
94
+ `benchmarks/realworld/` has 15 reproducers of silent bugs from public issues (PyTorch, PEFT,
95
+ torchtitan, kornia, DeepTab, poolformer, Lightning Bolts, simpletransformers and others). Each one
96
+ is a short script that keeps the buggy code pattern from the issue. Each also has a fixed version
97
+ (`f*.py`) that must produce no findings.
98
+
99
+ ```bash
100
+ cd benchmarks/realworld && python score.py $(which silentbug)
101
+ ```
102
+
103
+ | Case | Issue | Bug | Found at the right line |
104
+ |---|---|---|---|
105
+ | r01 | pytorch/pytorch#16045 | `MSELoss` on `(N,1)` vs `(N,)` | yes |
106
+ | r02 | OpenTabular/DeepTab#441 | `(n,1)` target unsqueezed to `(B,1,1)` | yes |
107
+ | r03 | kornia/kornia#6157 | SSIM loss broadcasts a batch-1 target | yes |
108
+ | r04 | trnsci/trnsparse#67 | `y` broadcasts the spmv output to `(12,12)` | yes |
109
+ | r05 | huggingface/peft#1612 | weights read directly, LoRA wrapper never runs | yes |
110
+ | r06 | sail-sg/poolformer#16 | stage-0 output dropped, `norm0` never trains | yes |
111
+ | r07 | pytorch/torchtitan#4802 | top-k indices cut the indexer's gradient | yes |
112
+ | r08 | pytorch/pytorch#14467 | GRU created after the optimizer | yes |
113
+ | r09 | RAI-SCC/jigsaw#224 | reshard replaces parameters, optimizer goes stale | yes |
114
+ | r10 | gitabcworld/MatchingNetworks#5 | softmax, then `cross_entropy` | yes |
115
+ | r11 | cloudtrainerwork/chesstrader#49 | softmax output fed to `CrossEntropyLoss` | yes |
116
+ | r12 | Lightning-Universe/lightning-bolts#654 | `log_softmax(softmax(x))` | yes |
117
+ | r13 | YuanGongND/ssast#2 | sigmoid fed to `BCEWithLogitsLoss` | yes |
118
+ | r14 | ThilinaRajapakse/simpletransformers#941 | `model.eval()` never undone | yes |
119
+ | r15 | juweipku/CoCo#1 | no `optimizer.zero_grad()` | yes |
120
+
121
+ Result: 15 of 15 found at the right line, 15 of 15 fixed versions report nothing.
122
+
123
+ ## Overhead
124
+
125
+ 6-layer MLP, width 1024, batch 256, AdamW, CPU: 30.9 ms/step plain, 33.8 ms/step under
126
+ silentbug (1.09x). It is meant to run for a few steps, not for a whole training run.
127
+
128
+ ## Limits
129
+
130
+ ## Tested setups
131
+
132
+ * PyTorch 2.15 on CPU and PyTorch 2.14 with CUDA 13.0 on an RTX 4060: unit tests and all 15 benchmark
133
+ cases plus their 15 fixed versions, on both devices.
134
+ * Mixed precision (`torch.autocast` with fp16 + `GradScaler`, and bf16): bugs found, clean code stays quiet.
135
+ * `torch.compile` (a compiled model, and a compiled loss function): findings name the original module.
136
+ * DDP with 2 processes (`torchrun`, gloo): DDP's own internal detach is not reported. Only rank 0 prints.
137
+ * Real code, unchanged: karpathy/nanoGPT (CUDA, with and without `compile`, with gradient accumulation)
138
+ and pytorch/examples `word_language_model` (manual SGD with no optimizer). Neither gave a false alarm.
139
+ When `model.zero_grad()` was deleted from the second one, silentbug reported it at the update line.
140
+
141
+ ## Limits
142
+
143
+ * Not tested with FSDP, tensor or pipeline parallelism, or on multiple GPUs at once.
144
+ * `torch.compile`: while silentbug watches, the compiler stops at silentbug's hooks and runs the
145
+ compiled code op by op. That is why findings inside a compiled function point at the exact line
146
+ there. It also means bugs that only happen in compiled graphs (compiler bugs) are not seen, and
147
+ the watched steps run at eager speed. Compilation goes back to normal once the check ends.
148
+ * It only sees what runs during the steps it watches. A branch that runs only every 100 steps is
149
+ not checked.
150
+ * `broadcast` checks the first three tensor arguments of each op.
151
+
152
+ ## Related tools
153
+
154
+ * [nantrace](https://github.com/Optimus2007/nantrace) finds where NaN and Inf start. silentbug
155
+ finds the bugs that never produce a NaN.
156
+ * [jaxtyping](https://github.com/patrick-kidger/jaxtyping) checks shapes if you annotate them.
157
+ silentbug needs no annotations.
158
+ * `TORCH_DISTRIBUTED_DEBUG=DETAIL` names unused parameters under DDP, but not why they are unused.
159
+
160
+ ## License
161
+
162
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "silentbug"
7
+ version = "0.1.0"
8
+ description = "Find silent PyTorch training bugs: accidental broadcasting, parameters that never learn, and the line that caused them."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Shushant Kumar Choudhary" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["torch>=2.1"]
15
+ keywords = ["pytorch", "debugging", "broadcasting", "gradients", "training"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Topic :: Software Development :: Debuggers",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/Shushant17711/silentbug"
24
+ Issues = "https://github.com/Shushant17711/silentbug/issues"
25
+ Changelog = "https://github.com/Shushant17711/silentbug/blob/main/CHANGELOG.md"
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest"]
29
+
30
+ [project.scripts]
31
+ silentbug = "silentbug.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """silentbug: find silent PyTorch training bugs and the line that caused them."""
2
+
3
+ from .report import BUG, CHECK, Finding, Report
4
+ from .session import Session, check
5
+
6
+ __all__ = ["check", "Session", "Report", "Finding", "BUG", "CHECK"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,100 @@
1
+ """Find the line of user code that called the current torch op."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import linecache
6
+ import os
7
+ import sys
8
+ import sysconfig
9
+ from dataclasses import dataclass
10
+
11
+ import torch
12
+
13
+ _TORCH_DIR = os.path.dirname(os.path.realpath(torch.__file__)) + os.sep
14
+ _OWN_DIR = os.path.dirname(os.path.realpath(__file__)) + os.sep
15
+ _LIB_DIRS = tuple(
16
+ {os.path.realpath(p) + os.sep for p in (sysconfig.get_paths().get("purelib"), sysconfig.get_paths().get("platlib")) if p}
17
+ )
18
+ _STDLIB = os.path.realpath(sysconfig.get_paths()["stdlib"]) + os.sep
19
+
20
+ _real_cache: dict[str, str] = {}
21
+
22
+
23
+ def _real(path: str) -> str:
24
+ r = _real_cache.get(path)
25
+ if r is None:
26
+ r = os.path.realpath(path) if not path.startswith("<") else path
27
+ _real_cache[path] = r
28
+ return r
29
+
30
+
31
+ def _skip(path: str) -> bool:
32
+ if path.startswith("<"):
33
+ return True
34
+ p = _real(path)
35
+ return p.startswith(_TORCH_DIR) or p.startswith(_OWN_DIR) or p.startswith(_STDLIB) and not any(
36
+ p.startswith(d) for d in _LIB_DIRS
37
+ )
38
+
39
+
40
+ def _is_lib(path: str) -> bool:
41
+ p = _real(path)
42
+ return any(p.startswith(d) for d in _LIB_DIRS)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Where:
47
+ file: str
48
+ line: int
49
+ func: str
50
+ code: str
51
+ lib: bool = False # True if the line is inside an installed package
52
+
53
+ def short(self) -> str:
54
+ f = self.file
55
+ try:
56
+ rel = os.path.relpath(f)
57
+ if not rel.startswith(".."):
58
+ f = rel
59
+ except ValueError:
60
+ pass
61
+ return f"{f}:{self.line}"
62
+
63
+ def to_dict(self) -> dict:
64
+ return {"file": self.file, "line": self.line, "function": self.func, "code": self.code, "library": self.lib}
65
+
66
+
67
+ # torch packages that copy, sync or update parameters as bookkeeping, not as part of the model
68
+ _INFRA = tuple(_TORCH_DIR + d + os.sep for d in (
69
+ "distributed", os.path.join("nn", "parallel"), "optim", os.path.join("nn", "utils"),
70
+ "amp", os.path.join("cuda", "amp")))
71
+
72
+
73
+ def torch_caller(skip: int = 2) -> str | None:
74
+ """'infra' or 'torch' when the nearest frame outside silentbug is PyTorch's own code."""
75
+ f = sys._getframe(skip)
76
+ while f is not None:
77
+ p = _real(f.f_code.co_filename)
78
+ if not p.startswith(_OWN_DIR):
79
+ if p.startswith(_INFRA) or p.startswith(_TORCH_DIR + "_tensor_str"):
80
+ return "infra"
81
+ return "torch" if p.startswith(_TORCH_DIR) else None
82
+ f = f.f_back
83
+ return None
84
+
85
+
86
+ def where(skip: int = 2) -> tuple[Where | None, Where | None]:
87
+ """Return (first frame outside torch, first frame outside installed packages)."""
88
+ f = sys._getframe(skip)
89
+ first = None
90
+ while f is not None:
91
+ path = f.f_code.co_filename
92
+ if not _skip(path):
93
+ lib = _is_lib(path)
94
+ w = Where(path, f.f_lineno, f.f_code.co_name, linecache.getline(path, f.f_lineno).strip(), lib)
95
+ if first is None:
96
+ first = w
97
+ if not lib:
98
+ return first, (w if w != first else None)
99
+ f = f.f_back
100
+ return first, None
@@ -0,0 +1,80 @@
1
+ """Command line: `silentbug run train.py [args...]`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import runpy
8
+ import sys
9
+
10
+ from .session import Session, _Done
11
+
12
+ USAGE = """silentbug run [--steps N] [--keep-going] [--json FILE] (script.py | -m module) [args...]
13
+
14
+ Runs your training script with silentbug watching the first N optimizer steps
15
+ (default 3), prints what it found, and stops the script. Exit code: 0 nothing
16
+ found, 1 likely bugs found, 2 only things to check."""
17
+
18
+
19
+ def _parse(argv: list[str]):
20
+ ap = argparse.ArgumentParser(prog="silentbug run", usage=USAGE)
21
+ ap.add_argument("--steps", type=int, default=3)
22
+ ap.add_argument("--keep-going", action="store_true", help="let the script keep running after the report")
23
+ ap.add_argument("--json", metavar="FILE", help="also write the report as JSON ('-' for stdout only JSON)")
24
+ ap.add_argument("-m", dest="module", action="store_true", help="run a module, like python -m")
25
+ ap.add_argument("target")
26
+ ap.add_argument("args", nargs=argparse.REMAINDER)
27
+ return ap.parse_args(argv)
28
+
29
+
30
+ def run(argv: list[str]) -> int:
31
+ a = _parse(argv)
32
+ s = Session(max_steps=a.steps, stop_at_limit=not a.keep_going)
33
+ old_argv = sys.argv[:]
34
+ sys.argv = [a.target, *a.args]
35
+ if not a.module:
36
+ sys.path.insert(0, os.path.dirname(os.path.abspath(a.target)))
37
+ error = None
38
+ s.start()
39
+ try:
40
+ if a.module:
41
+ runpy.run_module(a.target, run_name="__main__", alter_sys=True)
42
+ else:
43
+ runpy.run_path(a.target, run_name="__main__")
44
+ except _Done:
45
+ pass
46
+ except SystemExit:
47
+ pass
48
+ except BaseException as e: # the script crashed: still report what we saw
49
+ error = e
50
+ finally:
51
+ sys.argv = old_argv
52
+ rep = s.stop()
53
+ rank = os.environ.get("RANK", "0") # under torchrun every rank sees the same code
54
+ if rank != "0":
55
+ pass
56
+ elif a.json == "-":
57
+ print(rep.to_json())
58
+ else:
59
+ print(rep, file=sys.stderr if a.keep_going else sys.stdout)
60
+ if a.json:
61
+ with open(a.json, "w") as f:
62
+ f.write(rep.to_json())
63
+ if error is not None:
64
+ print(f"\n(your script raised {type(error).__name__}: {error})", file=sys.stderr)
65
+ return 1 if rep.bugs else (2 if rep.findings else 0)
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ argv = sys.argv[1:] if argv is None else argv
70
+ if not argv or argv[0] in ("-h", "--help"):
71
+ print(USAGE)
72
+ return 0
73
+ if argv[0] == "run":
74
+ return run(argv[1:])
75
+ if argv[0] in ("--version", "version"):
76
+ from . import __version__
77
+ print(__version__)
78
+ return 0
79
+ print(f"unknown command {argv[0]!r}\n\n{USAGE}", file=sys.stderr)
80
+ return 64