| name | using-nshutils |
| description | ML utility library for runtime tensor typechecking, activation saving, and debugging. Use when annotating tensor shapes/dtypes with jaxtyping, using @typecheck or tassert, saving/loading model activations with ActSave, configuring nshutils features, or using snoop/lovely for ML debugging. |
nshutils
ML research utilities for PyTorch, JAX, and NumPy. Import convention: import nshutils.typecheck as tc.
Configuration
Hierarchical config via environment variables and ContextVar overrides. Debug mode auto-enables typecheck.
NSHUTILS_DEBUG=1
NSHUTILS_TYPECHECK=1
NSHUTILS_ACTSAVE=1
NSHUTILS_ACTSAVE="/path"
NSHUTILS_ACTSAVE_FILTERS="layer*,attention*"
Programmatic:
from nshutils import config
config.set(True, "debug")
config.set(True, "typecheck")
config.set({"enabled": True, "save_dir": "/path", "filters": ["layer*"]}, "actsave")
with config.debug_override(False):
...
with config.typecheck_override(True):
...
Runtime Typechecking (jaxtyping + beartype)
nshutils wraps jaxtyping to provide runtime shape and dtype verification for tensors. This is the primary feature of the library.
Quick Start
import nshutils.typecheck as tc
import torch
@tc.typecheck
def attention(
q: tc.Float[torch.Tensor, "batch q_len dim"],
k: tc.Float[torch.Tensor, "batch k_len dim"],
v: tc.Float[torch.Tensor, "batch k_len v_dim"],
) -> tc.Float[torch.Tensor, "batch q_len v_dim"]:
scores = torch.einsum("bqd,bkd->bqk", q, k)
weights = scores.softmax(dim=-1)
return torch.einsum("bqk,bkv->bqv", weights, v)
When typechecking is enabled, this verifies at runtime that:
- All inputs are float tensors
- Dimension names match across arguments (e.g.,
batch must be the same size in q, k, and v)
- Return shape is correct
Annotation Syntax: Dtype[ArrayType, "shape"]
Three parts: dtype category, array type, shape string.
tc.Float[torch.Tensor, "batch seq dim"]
tc.Int[np.ndarray, "height width"]
tc.Bool[jax.Array, "batch"]
tc.Shaped[torch.Tensor, "..."]
Dtype Hierarchy
Use the broadest dtype that satisfies your constraint:
| Category | Matches | Common Use |
|---|
Shaped | Any dtype | When you only care about shape |
Num | Any numeric (int, float, complex) | Generic numeric ops |
Real | Int or float (not complex) | Most ML tensors |
Float | Any float (f16, bf16, f32, f64) | Most common for ML |
Int | Any signed int (i8–i64) | Indices, labels |
Integer | Signed or unsigned int | General integer ops |
UInt | Any unsigned int (u8–u64) | Masks, image pixels |
Bool | Boolean | Masks, conditions |
Complex | Complex types | Signal processing |
Inexact | Float or complex | Differentiable ops |
Specific precisions: Float32, Float64, Float16, BFloat16, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Complex64, Complex128.
Shape Syntax
Shapes are space-separated strings (no commas). Each token is one axis.
Named Dimensions
def f(x: tc.Float[Tensor, "batch seq dim"],
y: tc.Float[Tensor, "batch seq dim"]):
...
Same name across arguments = same size. This is the core feature for catching shape bugs.
Fixed Dimensions
tc.Float[Tensor, "batch 3"]
tc.Float[Tensor, "224 224 3"]
Variadic Dimensions (*)
Matches zero or more axes. Only one variadic per annotation.
tc.Float[Tensor, "*batch dim"]
tc.Float[Tensor, "*batch seq dim"]
Named variadics bind across arguments:
def f(x: tc.Float[Tensor, "*batch seq dim"],
y: tc.Float[Tensor, "*batch dim out"]):
...
Broadcasting Dimensions (#)
Matches size N or size 1 (for broadcasting):
def add(x: tc.Float[Tensor, "#batch dim"],
y: tc.Float[Tensor, "#batch dim"]):
...
Anonymous Dimensions (_)
Matches any size, no cross-argument binding. Can be given a descriptive name for documentation:
tc.Float[Tensor, "batch _ _"]
tc.Float[Tensor, "batch _seq _dim"]
Ellipsis (...)
Equivalent to *_ — matches any number of leading dims without binding:
tc.Float[Tensor, "... dim"]
tc.Float[Tensor, "..."]
Scalar (empty string)
tc.Float[Tensor, ""]
Documentation Names (name=size)
tc.Float[Tensor, "rows=4 cols=8"]
The @typecheck Decorator
Wraps a function with runtime shape/dtype checking. Checks are skipped when typechecking is disabled (zero overhead in production).
@tc.typecheck
def my_fn(x: tc.Float[torch.Tensor, "batch dim"]) -> tc.Float[torch.Tensor, "batch dim"]:
return x * 2
- Dimension names are scoped to the function call
- Same name = same size across all arguments and return value
- Toggled at runtime via
config.set(True/False, "typecheck") or NSHUTILS_TYPECHECK=1
Inline Assertions with tassert
Check shapes mid-function without decorating:
def forward(self, x: torch.Tensor) -> torch.Tensor:
tc.tassert(tc.Float[torch.Tensor, "batch seq dim"], x)
hidden = self.linear(x)
tc.tassert(tc.Float[torch.Tensor, "batch seq hidden"], hidden)
return self.output(hidden)
tassert is a no-op when typechecking is disabled.
PyTree Type
For annotating nested structures (dicts, lists, tuples) of tensors:
def process(data: tc.PyTree[tc.Float[torch.Tensor, "batch dim"]]):
...
At static analysis time, PyTree resolves to Any so it won't cause pyright errors.
Framework-Agnostic Annotations
Works with any object that has .shape and .dtype:
tc.Float[torch.Tensor, "b d"]
tc.Float[np.ndarray, "b d"]
tc.Float[jax.Array, "b d"]
tc.Float[Any, "b d"]
Module-Wide Typechecking
Apply typechecking to an entire module without decorating every function:
from nshutils.typecheck import typecheck_this_module
typecheck_this_module()
Debugging Bindings
tc.print_bindings()
For advanced jaxtyping features (symbolic dims, expressions, nested annotations, custom dtypes, path-dependent axes), see references/jaxtyping-advanced.md.
Activation Saving (ActSave)
Save and load internal model activations for analysis.
from nshutils import ActSave, ActLoad
ActSave.enable(save_dir="path/to/activations", filters=["encoder.*"])
def forward(self, x):
hidden = self.encoder(x)
ActSave({"encoder.hidden": hidden})
with ActSave.context("encoder"):
ActSave(hidden=hidden)
acts = ActLoad.from_latest_version("path/to/activations")
for tensor in acts["encoder.hidden"]:
print(tensor.shape)
Filtering uses fnmatch patterns (*, ?, [seq]). Context prefixes compose: nested ActSave.context calls build dot-separated paths.
Pretty Tensor Printing (lovely)
Monkey-patches tensor __repr__ to show shape, dtype, min/max/mean instead of raw data:
from nshutils.lovely import monkey_patch
monkey_patch()
Enhanced Debugging (snoop)
Wraps pysnooper with ML-aware formatters (shows tensor shapes in traces):
from nshutils.snoop import snoop
@snoop
def train_step(batch):
...
Requires pip install nshutils[snoop].
Rules
-
Every file MUST start with from __future__ import annotations
-
Use import nshutils.typecheck as tc (not individual imports from jaxtyping)
-
Prefer broad dtype categories (Float over Float32) unless precision matters
-
Use semantic dimension names (batch, seq, channels) not generic (a, b, c)
-
Replace shape comments with tc.tassert — shape comments are unchecked and rot; tassert is verified at runtime when typechecking is enabled and is a zero-cost no-op in production:
x = self.linear(x)
x = self.linear(x)
tc.tassert(tc.Float[torch.Tensor, "batch seq hidden"], x)