Debug the math, not just the code.
A PyTorch toolkit that checks derivatives, watches numerics,
explains gradient geometry, and replays the step that broke training.
Install · 60 seconds · What you can do · Library map · CLI · Docs
PyTorch computes derivatives. NablaGuard verifies them.
Every subsystem emits the same immutable NablaIssue — a stable code, a category,
scalar evidence, and an explicit limitation. There is no LLM in the loop. Results
are numbers you can replay, diff, and put in CI.
Supported today: CPU eager, Python 3.10+, PyTorch ≥ 2.2.
Experimental: CUDA, AMP, torch.compile.
Unsupported: DDP / FSDP, Triton internals, untrusted checkpoints.
See compatibility.
python -m pip install "https://github.com/dhrrishitvdeka/NablaGuard/releases/download/v1.0.1/nablaguard-1.0.1-py3-none-any.whl"
nabla --versionFrom a clone:
git clone https://github.com/dhrrishitvdeka/NablaGuard.git
cd NablaGuard
python -m pip install -e ".[dev]"A custom backward that forgot the factor of two:
import torch
import nablaguard as ng
class WrongSquare(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x)
return x.square()
@staticmethod
def backward(ctx, grad_output):
(x,) = ctx.saved_tensors
return grad_output * x # should be 2 * x
result = ng.check.operator(
candidate=WrongSquare.apply,
reference=lambda x: x.square(),
inputs=[ng.tensor(shape=(32,), dtype=torch.float64)],
vjp_cotangent="random",
check_jvp=True,
check_finite_difference=True,
)
result.print()You get a BACKWARD_MISMATCH (NG3002) with the worst element, the seed, and
an optional NGF artifact you can inspect without unpickling tensors.
| You want to… | Call | What it actually does |
|---|---|---|
Prove a custom autograd.Function |
ng.check.operator |
Forward, VJP, optional JVP / Hessian-vector / FD / determinism |
| Hunt shape / dtype / layout bugs | ng.check.fuzz |
Seeded search + greedy shrinking; backward is checked |
| Watch a training step for NaNs | ng.guard / ng.sanitize |
Eager ATen hooks, overflow heuristics, FP64 shadows |
| Ask “which loss is fighting which?” | ng.trace.losses |
Exact per-loss VJPs, cosine, cancellation |
| Ask “which sample dominates the batch?” | ng.trace.samples |
Bounded per-sample gradients |
| Freeze a run and replay it | ng.capture + ng.replay |
Checkpoints, fingerprints, RNG digests |
| Find the first bad step | ng.bisect |
Monotonic binary search + adjacent-step diagnosis |
| Assert training invariants | ng.contracts.* |
Finite loss, grad-norm caps, exploding-loss windows |
| Pick a dtype per module | ng.precision.audit |
Empirical FP16 / BF16 / FP32 vs FP64 (experimental) |
| Ship a CI report | ng.report.* |
Console, JSON, HTML, JUnit |
Import everything from nablaguard (or nablaguard as ng):
ng.check operator fuzz tensor shapes property equivalent minimize
ng.guard guard sanitize shadow_rule
ng.trace losses gradient samples
ng.capture capture Recorder
ng.replay replay ReplayObservation
ng.bisect first-bad search (function)
nablaguard.bisect metric_greater_than metric_less_than metric_nonfinite
ng.contracts loss gradient tensor parameter training contract
ng.precision audit
ng.report dumps html junit compare
ng Session NablaIssue Severity tensor shapes
| Function | Role |
|---|---|
ng.check.operator(...) |
Compare candidate vs reference: forward, VJP (ones or random cotangent), optional JVP, double backward, central finite differences, same-RNG determinism. Restores Python / NumPy / CPU / CUDA RNG. |
ng.check.fuzz(...) |
Sample TensorStrategy recipes (shape, dtype, layout, distribution). Failed cases shrink greedily. Generated inputs keep requires_grad, so wrong backwards are reported. |
ng.tensor(...) / ng.shapes(...) |
Concrete TensorSpec or a search space (contiguous, transposed, strided, sliced, broadcasted). |
ng.property / ng.equivalent |
Reference-free invariants run on the same trials. |
ng.check.minimize(...) |
Standalone greedy shrinker used by fuzz. |
result = ng.check.fuzz(
candidate=my_kernel,
reference=torch.nn.functional.linear,
inputs=[
ng.tensor(
shape=ng.shapes(ranks=(2,), dimensions=(7, 8, 16, 17, 32)),
dtype=[torch.float64, torch.float32],
layout=["contiguous", "strided", "transposed"],
),
ng.tensor(shape=(32, 16), dtype=torch.float64),
],
trials=100,
artifact_dir="artifacts",
)Opt-in flags on operator: check_jvp, check_double_backward,
check_finite_difference, check_determinism, vjp_cotangent="random".
| Function | Role |
|---|---|
ng.guard(model, mode=...) |
Context manager. light = module / observe only. standard = curated eager ATen ops. deep = plus FP64 shadow compare. |
ng.sanitize(*tensors) |
One-shot inspect of explicit tensors (no dispatch). |
monitor.observe(tensor) |
Record one tensor and run contracts against it. |
ng.shadow_rule("aten::exp") |
Register a custom high-precision shadow. |
with ng.guard(
model,
mode="deep",
modules=["transformer.blocks.10.*"],
operations=["aten.exp*", "aten.sum*", "aten._softmax*"],
) as monitor:
loss = model(batch).sum()
loss.backward()
monitor.print() # NG1001 NaN, NG1002 instability, NG1003 overflow, …Events store scalar stats only (min / max / mean / NaN count), never activations.
| Function | Role |
|---|---|
ng.trace.losses({name: scalar}, parameters=...) |
One autograd.grad per named loss. Pairwise cosine and cancellation. |
ng.trace.gradient(parameter) |
Report for a parameter after the with block (call trace.release() when done). |
ng.trace.samples(model, loss_fn, batch, ...) |
Per-sample VJPs for selected parameters. Ranks dominant / opposing samples and near-duplicate directions. Restores buffers, RNG, and existing .grad. |
Cancellation is always
1 − ‖∑ gᵢ‖ / ∑ ‖gᵢ‖
It measures lost magnitude. It does not assign causality. Cosine is undefined (NaN) when either vector has zero norm.
with ng.trace.losses(
{"ce": ce_loss, "aux": aux_loss},
parameters=[model.classifier.weight],
) as trace:
(ce_loss + aux_loss).backward()
trace.report(model.classifier.weight, name="classifier").print()| Function | Role |
|---|---|
ng.capture(model, optimizer, ...) |
Periodic full checkpoints + per-step JSON (loss, fingerprints, RNG digest, batch ids). |
recorder.record_step(...) |
Record the boundary after a completed step. |
ng.replay(run, model=..., step_fn=...) |
Restore nearest checkpoint, re-run steps, compare fingerprints / RNG. passed requires every step MATCH. |
ng.ReplayObservation(...) |
Optional extra evidence: tensors, data-loader state, batch indices. |
ng.bisect(run, predicate) |
Logarithmic first-bad search. CLI helpers: metric_greater_than, metric_less_than, metric_nonfinite. |
with ng.capture(model, optimizer, checkpoint_every=1000) as rec:
for step, batch in enumerate(loader, start=1):
loss = train_step(batch)
rec.record_step(
step=step,
loss=loss,
batch_indices=batch.indices,
tensors={"layer.weight": model.layer.weight},
)
from nablaguard.bisect import metric_greater_than
ng.replay(rec.run_path, model=fresh_model, optimizer=fresh_opt, step_fn=replay_step)
ng.bisect(rec.run_path, metric_greater_than("loss", 10.0))Checkpoints are pickle (torch.load(..., weights_only=False)).
nabla replay always requires --i-trust-this-run. See SECURITY.md.
| Contract | Checks |
|---|---|
ng.contracts.loss.finite() |
Loss is a finite scalar |
ng.contracts.gradient.norm(max=100) |
Combined L2 of selected grads (magnitude for complex) |
ng.contracts.tensor.finite(module="attention.*") |
Observed tensors stay finite |
ng.contracts.parameter.change(min_relative=1e-8) |
Parameters actually moved |
ng.contracts.training.loss_not_exploding(max_ratio=10, window=20) |
Loss vs a recent window |
ng.contract(name, predicate) |
Your own boolean / scalar-tensor predicate |
Attach them to ng.guard(..., contracts=...) or ng.capture(..., contracts=...).
Set raise_on_failure=True to fail fast (ContractViolation).
report = ng.precision.audit(
model,
inputs,
candidate_dtypes=(torch.float16, torch.bfloat16, torch.float32),
)
report.print()Deep-copies the model per dtype, compares selected module outputs to FP64, and recommends the first dtype inside both absolute and relative budgets. Observed error includes upstream propagation — guidance, not a kernel proof.
| Function | Format |
|---|---|
result.print() / result.format() |
Terminal |
ng.report.dumps(result) |
Strict JSON (NaN / Inf as strings, no default=str) |
ng.report.html(result) |
Self-contained HTML, escaped |
ng.report.junit(result) |
JUnit XML for CI |
ng.report.compare(old, new) |
Stable identity diff of issues |
CLI: --format console|json|html|junit --output path.
Exit codes: 0 pass, 1 check failed, 2 usage, 3 config / internal error.
| Type | Meaning |
|---|---|
NablaIssue |
code, category, severity, message, evidence, optional suggestion |
Session |
Bounded in-process store (max_events, max_issues) |
TensorEvent |
Scalar metadata for one observed tensor |
Severity |
info · low · medium · high · critical |
Set artifact_dir= on operator / fuzz. Artifacts are private by default:
issue JSON, fingerprints, environment, and a JSON-only reproduction.py.
Raw .pt tensors need artifact_raw_tensors=True.
nabla inspect artifacts/NGF-AABBCCDD
nabla artifact sanitize artifacts/NGF-AABBCCDD --output-root shareable/
nabla artifact migrate legacy-dir/ --output-root migrated/Inspect never calls torch.load. It refuses symlinks, junctions, and paths that
escape the artifact root.
nabla check module:fn --reference module:ref [--trials N] [--jvp] [--finite-difference]
nabla sanitize train.py --mode deep
nabla trace train.py --format html --output trace.html
nabla run train.py --capture --format json --output run.json
nabla replay .nabla/runs/run-id --model-factory app:make_model --step-function app:step --i-trust-this-run
nabla bisect .nabla/runs/run-id --metric loss --greater-than 10
nabla inspect artifacts/NGF-…
nabla artifact inspect|sanitize|migrate
nabla benchmark bugbench|overhead
--trials 1 is a single operator check. --trials N (N > 1) is fuzz.
| File | Shows |
|---|---|
examples/bad_backward.py |
Wrong VJP caught by operator |
examples/advanced_operator_check.py |
JVP, double backward, finite difference, determinism |
examples/fuzz_operator.py |
Shape / layout search and shrinking |
examples/unstable_exp.py |
Overflow under guard |
examples/gradient_trace.py |
Multi-loss geometry |
examples/per_sample_gradients.py |
Dominant / conflicting samples |
examples/capture_replay.py |
Capture + exact replay |
examples/training_bisect.py |
First bad step |
examples/precision_audit.py |
Dtype recommendation |
| Page | Contents |
|---|---|
| Mathematical guide | VJP, JVP, cancellation, finite differences, FP16 overflow |
| Architecture | How the subsystems share issues and sessions |
| Compatibility | What is supported vs experimental |
| API stability | Public surface and persisted formats |
| Performance | Measured guard-mode overhead |
| Security | Pickle checkpoints and NGF inspect |
| Contributing | Tests, ruff, mypy |
pytest --cov=nablaguard
ruff check .
mypy nablaguard
python benchmarks/suite.py --output benchmark.json