Skip to main content

One-line circuit breaker for PyTorch training — catches NaN gradients, logs spikes, prevents model corruption.

Project description

⚡ StabilityGuard

One-line circuit breaker for PyTorch training.
Add one line. See exactly which layer is about to explode.

License: MIT Python 3.9+ PyTorch 2.0+


The Problem

Training large neural networks fails catastrophically when gradient spikes or NaN explosions occur. A single corrupted gradient can poison your optimizer's state (momentum buffers, Adam's second moments), forcing you to restart from the last checkpoint—often thousands of steps back.

The failure cascade:

  1. A gradient spike hits one layer (10-1000× larger than normal)
  2. Optimizer state gets corrupted with NaN/Inf values
  3. Subsequent steps propagate NaN through the entire model
  4. By the time PyTorch throws RuntimeError: Function returned nan, it's too late—your checkpoint is poisoned

What PyTorch tells you:

RuntimeError: Function 'AddmmBackward0' returned nan values in its 0th output

What you actually need to know:

  • Which layer caused the spike?
  • When did it start (exact step number)?
  • What was the gradient magnitude vs. normal baseline?
  • Can we catch it before it corrupts the optimizer?

The cost: For a 7B LLM on 8×A100 GPUs, a single NaN explosion at 40% training wastes ~72 GPU-hours. You lose days of compute because one gradient went haywire.

StabilityGuard solves this by monitoring every layer's gradients in real-time and catching spikes before they corrupt your training run.

The Fix

pip install stabilityguard

Before StabilityGuard (standard PyTorch):

from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=2e-4)

for batch in dataloader:
    loss = model(batch)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

After StabilityGuard (one-line change):

from torch.optim import AdamW
from stabilityguard import GuardedOptimizer  # ← the only change

base_opt = AdamW(model.parameters(), lr=2e-4)
optimizer = GuardedOptimizer(base_opt, model,
    spike_threshold=10.0,      # gradient norm ratio to trigger alert
    nan_action="skip",         # "skip" | "rollback" | "raise"
    log_every=50               # steps between diagnostic summaries
)

for batch in dataloader:
    loss = model(batch)
    loss.backward()
    optimizer.step()           # GuardedOptimizer intercepts here
    optimizer.zero_grad()

What You See

When a spike hits at step 847:

╔══════════════════════════════════════════════════════════════╗
║  ⚠ STABILITYGUARD — SPIKE DETECTED @ step 847                ║
╠══════════════════════════════════════════════════════════════╣
║  Trigger layer  : transformer.h.11.mlp.c_proj                ║
║  Grad norm      : 847.3  (baseline: 1.2, ratio: 706.1x)      ║
║  Action taken   : optimizer.step() SKIPPED                   ║
║  Loss (pre-skip): 14.71                                      ║
╚══════════════════════════════════════════════════════════════╝
stabilityguard.log written → ./sg_logs/spike_step847.json

Your model weights are untouched. Training continues.

How It Works

  1. Backward hooks on every nn.Module capture per-layer gradient L2 norms
  2. EMA baselines track normal gradient behavior (α=0.01, ~100 step lookback)
  3. Spike detection fires when current_norm / ema_baseline > threshold
  4. NaN/Inf short-circuit catches corrupted gradients via torch.isfinite
  5. Actions execute automatically: skip the step, rollback to checkpoint, or raise

Configuration

Parameter Default Description
spike_threshold 10.0 Ratio of current norm to EMA baseline that triggers a spike
nan_action "skip" Action on spike: "skip", "rollback", or "raise"
log_every 50 Steps between periodic diagnostic summaries
log_dir "./sg_logs" Directory for JSON spike reports
ema_alpha 0.01 EMA smoothing factor (smaller = slower adaptation)
warmup_steps 10 Steps before spike detection activates
verbose True Print diagnostic summaries to stdout

Actions

Action Behavior
skip Skip optimizer.step() — corrupted gradients discarded, weights unchanged
rollback Restore model + optimizer to the last clean checkpoint
raise Throw GradientSpikeError for interactive debugging

Integrations

Weights & Biases

from stabilityguard.integrations.wandb import WandBBridge
bridge = WandBBridge()
# Metrics logged under sg/ namespace automatically

MLflow

from stabilityguard.integrations.mlflow import MLflowBridge
bridge = MLflowBridge()

HuggingFace Transformers

from stabilityguard.integrations.huggingface import StabilityGuardCallback

trainer = Trainer(
    model=model,
    args=training_args,
    callbacks=[StabilityGuardCallback(spike_threshold=10.0)],
)

Performance

Metric Value
Per-step overhead (no spike) < 0.2ms on A100
Per-step overhead (spike detected) ~3ms (includes JSON write)
External dependencies 0 (only PyTorch)
License MIT

Install from source

git clone https://github.com/stabilityguard/stabilityguard.git
cd stabilityguard
pip install -e ".[test]"
pytest tests/ -v

License

MIT — use it anywhere, no restrictions.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stabilityguard-0.1.2.tar.gz (22.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stabilityguard-0.1.2-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file stabilityguard-0.1.2.tar.gz.

File metadata

  • Download URL: stabilityguard-0.1.2.tar.gz
  • Upload date:
  • Size: 22.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for stabilityguard-0.1.2.tar.gz
Algorithm Hash digest
SHA256 568e8af57008949daae9e22a6535f19127ac59b0e7a97ae3e98565da8144a038
MD5 a39b36f60a1df576b0d72c0e2e8db5c4
BLAKE2b-256 6ea1d54b1bdc2eab93c45a30a83707e7a9d4b35af08a7f4df52a90e14b715c06

See more details on using hashes here.

File details

Details for the file stabilityguard-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: stabilityguard-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 21.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for stabilityguard-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d2999bf637b6d704ab7da64855bd3df7127caf5818b432ba013b9e2ac74343c2
MD5 e3c44c9159278819391fa71b5031736e
BLAKE2b-256 8bc2a0efaf57704b5c62ec28920fd1fdd0671414b57e39132ade83417a925c83

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page