A PyTorch utility library for managing extra losses, metrics, and outputs from nested modules
Project description
TorchExtraContext
A PyTorch context manager for collecting auxiliary losses, metrics, intermediate outputs, hooks, and shared state from nested torch.nn.Module objects without changing forward() signatures.
What It Does
In PyTorch, auxiliary losses and intermediate values are often produced deep inside nested torch.nn.Module objects. The traditional approach is to return them layer by layer:
def forward(self, x):
x, loss_a = self.block_a(x)
x, loss_b = self.block_b(x)
logits = self.head(x)
return logits, {"loss_a": loss_a, "loss_b": loss_b}
This approach couples auxiliary data collection to the return values of every module on the call path. Modules that do not create the auxiliary data still need to receive, merge, and return it.
Instead of modifying forward() signatures layer by layer, we prefer to register the data in the module where it is produced:
import torchextractx.keras_style # Enable Keras-style API
def forward(self, x):
x = self.layer1(x)
self.add_loss("loss_a", compute_loss(x))
return x
Then collect all losses in one line:
with ExtraContext(model) as ctx:
output = model(x)
all_losses = ctx.get_losses() # Done
🚀 Installation
From PyPI
pip install torchextractx
From source
git clone https://github.com/5o1/TorchExtraContext.git
cd TorchExtraContext
pip install -e .
With dev tools
git clone https://github.com/5o1/TorchExtraContext.git
cd TorchExtraContext
pip install -e ".[dev]" # pytest, black, mypy
📖 Quick Start
Usage
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchextractx import ExtraContext, get_context # Step #1: Import the context helpers
class FeatureExtractor(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 20)
self.fc2 = nn.Linear(20, 10)
def forward(self, x):
x = self.fc1(x)
x = torch.relu(x)
get_context(self).add_loss("feature_mean_loss", x.mean()) # Step #2: Register the loss where it is created
x = self.fc2(x)
return x
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.extractor = FeatureExtractor()
self.classifier = nn.Linear(10, 2)
def forward(self, x):
x = self.extractor(x)
x = self.classifier(x)
return x
model = Classifier()
optimizer = torch.optim.Adam(model.parameters())
x = torch.randn(32, 10)
targets = torch.randint(0, 2, (32,))
with ExtraContext(model) as ctx: # Step #3: Create the collection context in the training step
logits = model(x)
main_loss = F.cross_entropy(logits, targets)
extra_losses = ctx.get_losses() # Step #4: Collect losses registered inside the model
total_loss = main_loss
for loss in extra_losses.values():
total_loss = total_loss + 0.1 * loss
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
Enable Keras Style
import torchextractx.keras_style # Step #1: Enable torch.nn.Module.add_loss
from torchextractx import ExtraContext
class FeatureExtractor(nn.Module):
def __init__(self):
...
def forward(self, x):
x = self.fc1(x)
x = torch.relu(x)
self.add_loss("feature_mean_loss", x.mean()) # Step #2: Register the loss where it is created
x = self.fc2(x)
return x
...
with ExtraContext(model) as ctx: # Step #3: Create the collection context in the training step
logits = model(x)
main_loss = F.cross_entropy(logits, targets)
extra_losses = ctx.get_losses() # Step #4: Collect losses registered inside the model
...
Use with Lightning
Lightning's LightningModule is a subclass of torch.nn.Module, so the same APIs can be used in Lightning training_step, validation_step, or inside module forward() methods. Wrap one model call with ExtraContext(self) in the step, then collect losses, metrics, hooks, shared objects, and logs from the context.
📚 Other Usage
Export Metrics
loss usually goes into total_loss and participates in backward(). A metric is only for monitoring, such as accuracy. Keeping them separate avoids mixing observation-only values into the optimized loss.
class ClassifierHead(nn.Module):
def __init__(self):
...
def forward(self, x, targets):
logits = self.classifier(x)
with torch.no_grad():
preds = logits.argmax(dim=-1)
acc = (preds == targets).float().mean()
get_context(self).add_metric("train/accuracy", acc) # For monitoring only
return logits
head = ClassifierHead()
with ExtraContext(head) as ctx:
logits = head(x, targets)
main_loss = F.cross_entropy(logits, targets)
metrics = ctx.get_metrics()
Hooks
A hook can store a callback that should run after forward(). ExtraContext only collects these functions; the training step decides when to execute them.
class FeatureBlock(nn.Module):
def __init__(self):
...
def forward(self, x):
y = self.block(x)
get_context(self).add_hook("feature_norm", lambda: y.detach().norm().item())
return y
block = FeatureBlock()
with ExtraContext(block) as ctx:
output = block(x)
hook_results = {
name: [hook() for hook in hooks]
for name, hooks in ctx.hooks.items()
}
Shared Objects
put/get/pop can pass values out to the training step, but it can also share objects between modules in the same forward pass. This bypasses forward() arguments and return values. Note that this method is not very safe: it relies on key names and module call order, so it is best kept to small experimental uses.
class Encoder(nn.Module):
def __init__(self):
...
def forward(self, x):
latent = self.backbone(x)
get_context(self).put("latent", latent)
return latent
class Decoder(nn.Module):
def __init__(self):
...
def forward(self, x):
ctx = get_context(self)
latent = ctx.pop("latent")
return self.head(x + latent)
class AutoEncoder(nn.Module):
def __init__(self):
super().__init__()
self.encoder = Encoder()
self.decoder = Decoder()
def forward(self, x):
encoded = self.encoder(x)
return self.decoder(encoded)
model = AutoEncoder()
with ExtraContext(model):
output = model(x)
Shared Logger
Pass a logger when creating ExtraContext if several modules need to write logs. Modules still call get_context(self).log(...); Note: Keras style does not monkey patch log onto torch.nn.Module.
records = []
def logger(event, **fields):
records.append((event, fields))
class FeatureBlock(nn.Module):
def __init__(self):
...
def forward(self, x):
y = self.block(x)
get_context(self).log("feature_block", mean=y.detach().mean().item())
return y
block = FeatureBlock()
with ExtraContext(block, logger=logger):
output = block(x)
🔧 API
Package Map
| Import path | Public symbols | Notes |
|---|---|---|
torchextractx |
ExtraContext, get_context, NullContext, configure_null_context_behavior, get_null_context_behavior, enable_keras_style_api, disable_keras_style_api, is_keras_style_api_enabled |
Preferred public import path for application code. |
torchextractx.context |
ExtraContext, get_context |
Active context manager and module lookup helper. |
torchextractx.null_context |
NullContext, configure_null_context_behavior, get_null_context_behavior |
No-op fallback and missing-context policy. |
torchextractx.keras_style |
side-effect import | Import this module to enable Keras-style helpers immediately. |
torchextractx.context.ExtraContext
| Member | Kind | Access | Notes |
|---|---|---|---|
ExtraContext(root_module, logger=None) |
class | with ExtraContext(model) as ctx: |
Main context manager for nested modules. |
add_loss(prefix, loss, op="sum") |
method | ctx.add_loss(...) |
Register an auxiliary loss tensor. |
add_metric(prefix, metric, op="mean") |
method | ctx.add_metric(...) |
Register a metric tensor. |
add_output(prefix, output) |
method | ctx.add_output(...) |
Save an intermediate output tensor with shape checks. |
add_hook(prefix, hook) |
method | ctx.add_hook(...) |
Register a callable hook. |
put(key, value) |
method | ctx.put(...) |
Store shared state for module-to-module communication. |
get(key, default=None) |
method | ctx.get(...) |
Read shared state. |
pop(key, default=None) |
method | ctx.pop(...) |
Remove and return shared state. |
has(key) |
method | ctx.has(...) |
Check whether a shared state item exists. |
get_losses(default_op="sum") |
method | ctx.get_losses(...) |
Return reduced losses as a dictionary. |
get_metrics(default_op="mean") |
method | ctx.get_metrics(...) |
Return reduced metrics as a dictionary. |
get_outputs() |
method | ctx.get_outputs(...) |
Return saved outputs as a dictionary. |
get_module_prefixes(module) |
method | ctx.get_module_prefixes(...) |
Return model path names for a module. |
log(*args, **kwargs) |
method | ctx.log(...) |
Forward logging calls to the configured logger. |
losses |
property | ctx.losses |
Raw loss store. |
hooks |
property | ctx.hooks |
Raw hook store. |
torchextractx.null_context.NullContext
| Member | Kind | Access | Notes |
|---|---|---|---|
NullContext(module=None) |
class | NullContext(module) or returned by get_context(module) |
No-op context used when no ExtraContext is active. |
add_loss, add_metric, add_output, add_hook, put, log |
methods | ctx.add_loss(...), ctx.put(...), etc. |
Ignored; may warn, stay silent, or raise based on the configured behavior. |
get, pop, has, get_losses, get_metrics, get_outputs, get_module_prefixes |
methods | ctx.get(...), ctx.get_losses(), etc. |
Return defaults or empty results. |
losses, hooks |
properties | ctx.losses, ctx.hooks |
Return empty stores. |
__getitem__ |
method | ctx[key] |
Raises KeyError because no shared state is active. |
torchextractx.keras_style
| Import | Effect | Notes |
|---|---|---|
import torchextractx.keras_style |
Enables torch.nn.Module.add_loss, add_metric, add_output, and add_hook. |
Process-wide side effect import. |
from torchextractx import enable_keras_style_api |
Same effect explicitly. | Useful when you want the switch in code instead of import side effects. |
disable_keras_style_api() |
Restores the original torch.nn.Module methods. |
Re-exported from torchextractx. |
is_keras_style_api_enabled() |
Reports whether the shim is active. | Re-exported from torchextractx. |
Note: Use get_context(self).log(...) for logging; Keras style does not monkey patch log onto torch.nn.Module.
⚠️ Notes
Thread Safety
Not thread-safe. Don't use from multiple threads simultaneously.
Nested Contexts
Nested ExtraContext on the same model is not allowed and will raise an error:
with ExtraContext(model):
with ExtraContext(model): # Raises ValueError
pass
Memory Management
All data is cleared after exiting the with block. Don't access outside:
with ExtraContext(model) as ctx:
output = model(x)
losses = ctx.get_losses() # OK
losses = ctx.get_losses() # Error
🧪 Running Tests
pytest tests/ -v
Or with unittest:
python -m unittest discover tests/ -v
📋 Requirements
- Python ≥ 3.10
- PyTorch ≥ 2.0.0
📝 License
MIT - see LICENSE
🚢 Release Process
- The canonical version lives in
pyproject.tomlunder[project].version. - Release tags must use
vX.Y.Zand match that version exactly. - GitHub Release notes are sourced from
CHANGELOG.md. - Pushing a matching tag runs tests, builds distributions, creates a GitHub Release, and publishes to PyPI through PyPI Trusted Publishing.
- PyPI publishing does not require a PyPI token; authentication is handled by GitHub Actions OpenID Connect.
🤝 Contributing
Welcome to submit issues and PRs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file torchextractx-0.2.0.tar.gz.
File metadata
- Download URL: torchextractx-0.2.0.tar.gz
- Upload date:
- Size: 19.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3dfb1472ed7c61647751c1448abb64f4e44b8d1eea33ffc4b7e1b4d73d6584e6
|
|
| MD5 |
8b8faa65722fb7652aad2df2253fcb8c
|
|
| BLAKE2b-256 |
26a109da683e5fdeaceac91fefb50f582aa5b26a86da9a31c08fd6be4d713e35
|
Provenance
The following attestation bundles were made for torchextractx-0.2.0.tar.gz:
Publisher:
release.yml on 5o1/TorchExtraContext
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
torchextractx-0.2.0.tar.gz -
Subject digest:
3dfb1472ed7c61647751c1448abb64f4e44b8d1eea33ffc4b7e1b4d73d6584e6 - Sigstore transparency entry: 2200893271
- Sigstore integration time:
-
Permalink:
5o1/TorchExtraContext@a2a6a23e5afb1c49488f8ad238ba1f47843df046 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/5o1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a2a6a23e5afb1c49488f8ad238ba1f47843df046 -
Trigger Event:
push
-
Statement type:
File details
Details for the file torchextractx-0.2.0-py3-none-any.whl.
File metadata
- Download URL: torchextractx-0.2.0-py3-none-any.whl
- Upload date:
- Size: 12.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7ffe3d3dcb40653c09f29bb9523bd25ea927aaf9b8aea4a167a5de18b225731d
|
|
| MD5 |
9a283eea569b50e0714d57b089bae59e
|
|
| BLAKE2b-256 |
442e426df15be3b54d8d2f9aaa1001a8de02ff19a253c4a6b6b1336574c83cfd
|
Provenance
The following attestation bundles were made for torchextractx-0.2.0-py3-none-any.whl:
Publisher:
release.yml on 5o1/TorchExtraContext
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
torchextractx-0.2.0-py3-none-any.whl -
Subject digest:
7ffe3d3dcb40653c09f29bb9523bd25ea927aaf9b8aea4a167a5de18b225731d - Sigstore transparency entry: 2200893299
- Sigstore integration time:
-
Permalink:
5o1/TorchExtraContext@a2a6a23e5afb1c49488f8ad238ba1f47843df046 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/5o1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a2a6a23e5afb1c49488f8ad238ba1f47843df046 -
Trigger Event:
push
-
Statement type: