Skip to content

Commit c50a0e9

Browse files
committed
document the robustness features and run them in CI
1 parent c30fc5f commit c50a0e9

2 files changed

Lines changed: 69 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: Gradient checks (finite difference)
3535
run: cargo test --no-default-features --test gradcheck -- --nocapture
3636

37+
- name: Robustness (resume, anomalies, freezing)
38+
run: cargo test --no-default-features --test robustness
39+
3740
- name: Unit + integration tests
3841
run: cargo test --no-default-features
3942

README.md

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,75 @@ the optimizer hold handles to the same weight and the same gradient, which is wh
101101
without special support: use the same handle twice and both paths' gradients add
102102
up on their own.
103103

104+
## Running unattended
105+
106+
Four things a long run needs, none of which the training loop above shows.
107+
108+
**Resume where you stopped.** `save()` writes weights, which is what you want for
109+
a finished model. Resuming needs the optimizer too — its momentum, Adam's two
110+
moment estimates, and the step count that drives bias correction. Restore weights
111+
alone and the optimizer restarts at step 1, so the first update after resuming
112+
lands far harder than it should.
113+
114+
```rust
115+
let mut step = load_training(&model, &mut opt, "run.fdl").unwrap_or(0);
116+
117+
while step < total {
118+
// ... train ...
119+
step += 1;
120+
if step % 500 == 0 { save_training(&model, &opt, step, "run.fdl")?; }
121+
}
122+
```
123+
124+
Loading a plain checkpoint with `load_training` is an error rather than a silent
125+
optimizer reset. `char_lm` does this, so Ctrl-C costs at most a few hundred steps.
126+
127+
**Find the NaN at the op that made it.** One non-finite gradient becomes a
128+
non-finite weight, and every activation downstream is NaN from then on — the loss
129+
only *prints* as NaN some steps later, by which point the checkpoint is poisoned
130+
too and the culprit is long gone.
131+
132+
```rust
133+
detect_anomaly(|| {
134+
loss.backward(); // panics: "anomaly: Log produced inf in the gradient for input 0"
135+
});
136+
137+
if !opt.gradients_are_finite() { continue; } // cheap guard: skip the batch
138+
```
139+
140+
Both read every gradient, so they are debugging and guard-rail tools, not
141+
something to leave on in a healthy loop.
142+
143+
**Freeze a backbone.** Frozen parameters hand out a detached tensor, so the
144+
backward pass stops there — no gradient is computed only to be discarded.
145+
146+
```rust
147+
backbone.freeze();
148+
let mut opt = Adam::new(model.parameters(), 1e-3); // updates only the head
149+
```
150+
151+
**Report a bad shape instead of dying.** Ordinary ops panic, which is right
152+
inside a model. At the edge of an app — a shape from a config file, a batch from
153+
an upload — use the `try_` variants.
154+
155+
```rust
156+
let y = x.try_matmul(&w)?; // Error::Shape, not a panic
157+
x.try_reshape(&[2, -1])?;
158+
table.try_index_select(&ids)?;
159+
```
160+
161+
They validate and then delegate, so there is still exactly one implementation of
162+
each operation, and they build the same graph.
163+
104164
## Layout
105165

106-
Every file is one idea, and none are long. 67 files, ~7,000 lines.
166+
Every file is one idea, and none are long. 71 files, ~8.0k lines.
107167

108168
```
109169
src/
110170
tensor/ the array type and everything you can do to it
111171
core.rs Tensor: shape, storage, graph link
172+
checked.rs try_* variants that report instead of panicking
112173
shape.rs strides, broadcasting, index math
113174
storage.rs the bytes, on one device or the other
114175
device.rs Device::cuda(0) -> Result
@@ -121,16 +182,17 @@ src/
121182
node.rs Backward trait, graph nodes, gradient slots
122183
engine.rs the reverse pass
123184
mode.rs no_grad
185+
anomaly.rs detect_anomaly
124186
ops/ one backward rule per forward op, same file names
125187
126188
nn/ layers, all implementing Module
127189
module.rs param.rs sequential.rs linear.rs conv.rs pooling.rs
128190
norm.rs activation.rs dropout.rs shape.rs embedding.rs
129191
attention.rs transformer.rs rnn.rs loss.rs
130192
131-
optim/ sgd.rs adam.rs schedule.rs
193+
optim/ sgd.rs adam.rs schedule.rs state.rs
132194
data/ dataset.rs loader.rs mnist.rs
133-
serialize/ checkpoint.rs
195+
serialize/ checkpoint.rs (weights) training.rs (weights + optimizer)
134196
cuda/ ffi.rs (raw bindings) kernels.rs (safe wrappers) buffer.rs
135197
rng.rs error.rs lib.rs
136198
@@ -204,6 +266,7 @@ load(&model, "model.fdl")?; // missing file, or a shape that moved
204266

205267
```bash
206268
cargo test --no-default-features --test gradcheck # every backward rule
269+
cargo test --no-default-features --test robustness # resume, anomalies, freezing
207270
cargo test --test cuda_parity -- --test-threads=1 # CPU vs GPU
208271
cargo test --no-default-features # everything
209272
cargo bench --no-default-features # throughput

0 commit comments

Comments
 (0)