Skip to main content

A PyTorch utility library for managing extra losses, metrics, and outputs from nested modules

Project description

TorchExtraContext

English | 中文

CI Release Python 3.10+ PyTorch 2.0+ License: MIT

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.toml under [project].version.
  • Release tags must use vX.Y.Z and 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


Download files

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

Source Distribution

torchextractx-0.2.0.tar.gz (19.1 kB view details)

Uploaded Source

Built Distribution

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

torchextractx-0.2.0-py3-none-any.whl (12.6 kB view details)

Uploaded Python 3

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

Hashes for torchextractx-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3dfb1472ed7c61647751c1448abb64f4e44b8d1eea33ffc4b7e1b4d73d6584e6
MD5 8b8faa65722fb7652aad2df2253fcb8c
BLAKE2b-256 26a109da683e5fdeaceac91fefb50f582aa5b26a86da9a31c08fd6be4d713e35

See more details on using hashes here.

Provenance

The following attestation bundles were made for torchextractx-0.2.0.tar.gz:

Publisher: release.yml on 5o1/TorchExtraContext

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for torchextractx-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7ffe3d3dcb40653c09f29bb9523bd25ea927aaf9b8aea4a167a5de18b225731d
MD5 9a283eea569b50e0714d57b089bae59e
BLAKE2b-256 442e426df15be3b54d8d2f9aaa1001a8de02ff19a253c4a6b6b1336574c83cfd

See more details on using hashes here.

Provenance

The following attestation bundles were made for torchextractx-0.2.0-py3-none-any.whl:

Publisher: release.yml on 5o1/TorchExtraContext

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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