A: Manual residual connections work fine for simple cases, but become unwieldy with:
- Multiple skip connections at different depths
- Shape mismatches requiring projection
- Different residual operations (concat, gated, highway)
- Learnable mixing coefficients
torchresidual handles all of this with a clean API.
A: Minimal. The Record/Apply pattern is essentially a named save/restore operation.
Thread-local context lookup adds <0.01% overhead. Most time is spent in actual layer computation.
A: Yes! You can nest them:
regular_block = nn.Sequential(nn.Linear(64, 64), nn.ReLU())
residual_block = ResidualSequential(
Record(),
regular_block, # Works fine
Apply(),
)A:
-
Gated (
operation="gated"): Single learnable scalar α- Formula:
(1-α)·x + α·residual - Interpolates between transformed and residual paths
- Formula:
-
Highway (
operation="highway"): Two learnable gates (transform T, carry C)- Formula:
T·x + C·residual - More expressive but more parameters
- Formula:
A:
- Add: When you want to preserve dimensionality (like ResNet)
- Concat: When you want to keep both paths' information (like DenseNet)
- Warning: concat doubles the last dimension
A: Yes! Common pattern:
ResidualSequential(
Record(name="r"),
layer1,
Apply(record_name="r"), # First use
layer2,
Apply(record_name="r"), # Second use (same record)
)A: Automatically when max_value / min_value > 100 and min_value > 0.
Example:
# Uses log space (ratio = 1000)
alpha = LearnableAlpha(0.01, min_value=0.001, max_value=1.0)
# Uses linear space (ratio = 10)
alpha = LearnableAlpha(0.5, min_value=0.0, max_value=1.0)Override: Set use_log_space=True/False explicitly.
A: Tanh provides:
- Better gradient flow near boundaries
- Symmetric parameterization
- Slightly more stable training
Difference is minor in practice.
A: Yes! It's a standalone module:
alpha = LearnableAlpha(0.5, 0.0, 1.0)
output = base_output * alpha() + bonus * (1 - alpha())A: Direct parent references create circular refs:
ResidualSequential → Apply → ResidualSequential # Breaks pickle/deepcopy
Thread-local storage avoids this while remaining thread-safe for nn.DataParallel.
See DESIGN.md for details.
A: Not in v0.1.0. TorchScript support is planned for v1.1 via a separate
ResidualSequentialScript class.
Workaround: Train with ResidualSequential, then manually reconstruct as
nn.Sequential for export.
A: You called Apply.forward() directly. Apply only works inside ResidualSequential:
# ❌ Wrong
apply = Apply()
apply(x) # Error!
# ✅ Correct
block = ResidualSequential(Record(), ..., Apply())
block(x)A: Enable automatic projection:
Record(need_projection=True) # Add this flagOr ensure shapes match manually.
A: Check that:
- Alpha is part of the optimizer:
optimizer = Adam(block.parameters()) - You're calling
alpha()notalphain forward pass - Loss has gradients flowing through the alpha branch
Debug:
for module in block.modules():
if isinstance(module, LearnableAlpha):
print(f"Alpha: {module().item():.4f}, grad: {module.param.grad}")A: Yes! Thread-local storage makes it safe:
model = ResidualSequential(...)
model = nn.DataParallel(model, device_ids=[0, 1, 2, 3])Each GPU thread gets its own context.
A: Also works, but thread-local storage is unnecessary (each process has separate memory). Still safe to use.
A: Negligible. The residual add is ~0.01% of LSTM compute time.
A: Yes:
transformer_block = ResidualSequential(
Record(name="attn_in"),
nn.MultiheadAttention(embed_dim=512, num_heads=8),
Apply(record_name="attn_in"),
nn.LayerNorm(512),
Record(name="ffn_in"),
nn.Linear(512, 2048),
nn.ReLU(),
nn.Linear(2048, 512),
Apply(record_name="ffn_in"),
nn.LayerNorm(512),
)A: Yes, but standard PyTorch's torchvision.models.resnet50 is more
optimized for that specific architecture. Use torchresidual when you need:
- Custom residual patterns
- Multiple skip connections
- Learnable alpha
- Non-standard operations (gated, highway, concat)
- Open an issue: https://github.com/v-garzon/torchresidual/issues
- Check examples: examples/
- Read the source: The entire library is ~500 lines