dc1d (DeformConv1d)
A 1D implementation of a deformable convolutional layer implemented in pure Python in PyTorch. The code style is designed to imitate similar classes in PyTorch such as torch.nn.Conv1d and torchvision.ops.DeformConv2d.
See it working: docs/demo.ipynb is a short, plotted walkthrough of what the layer does and why it is correct. It runs in under ten seconds on a laptop CPU and is committed with its outputs, so it renders without executing anything.
The motivation for creating this toolkit is that (as of 19/10/2022) there is no native 1D implementation of deformable convolution in the PyTorch library, and no alternative library which is simple to install (requiring only a basic PyTorch installation, with no additional compilation of C++ or CUDA libraries). The implementation here is written entirely in Python and makes use of torch.autograd for backpropagation.
Requirements
- Python >= 3.10
- PyTorch >= 2.7 (installed automatically as a dependency)
torchvision is not required. Earlier releases imported private torchvision
symbols at module scope for a dead code path; that path has been removed.
Install PyTorch for your platform/CUDA version first if you want a specific build: https://pytorch.org/get-started/locally/.
Installation
N.B. for the version we released for our paper, stick to dc1d==0.0.6 or lower
CHANGELOG.md records what changed since, including why a
0.0.x PackedDeformConv1d checkpoint will load without complaint and then
behave differently.
pip install dc1d
Or from source:
git clone https://github.com/jwr1995/dc1d.git
cd dc1d
pip install .
Development
The project is managed with uv:
uv sync # creates .venv with CPU-only torch and the dev tools
uv run pytest # run the test suite
uv run ruff check .
uv run ruff format --check .
Usage
DeformConv1d
DeformConv1d is the deformable convolution layer designed to imitate torch.nn.Conv1d.
Note: DeformConv1d does not compute the offset values used in its forward(...) call. These must be computed outside the layer. Use PackedDeformConv1d if you want the offsets computed for you.
import torch
from torch import nn
from dc1d.nn import DeformConv1d
# Hyperparameters
batch_size = 16
in_channels = 512
out_channels = 512
kernel_size = 16
stride = 1
padding = "valid"
dilation = 3
groups = 1
length = 128
# Construct layer
model = DeformConv1d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=True,
)
x = torch.rand(batch_size, in_channels, length, requires_grad=True)
# Number of offset positions required. This is the same closed form nn.Conv1d
# uses; forward() raises if the offset tensor disagrees with it.
output_length = model.expected_offset_positions(length)
offsets = nn.Parameter(torch.ones(batch_size, 1, output_length, kernel_size))
y = model(x, offsets) # [batch_size, out_channels, output_length]
The offset tensor has shape [batch_size, offset_groups, output_length, kernel_size],
where offset_groups may be 1 or any divisor of in_channels.
By default each kernel tap is constrained to its own receptive field. Pass
unconstrained=True to let taps sample anywhere in the sequence.
PackedDeformConv1d
PackedDeformConv1d computes the offsets internally using a depthwise-separable
convolutional block, as detailed in the paper below.
import torch
from dc1d.nn import PackedDeformConv1d
model = PackedDeformConv1d(
in_channels=64,
out_channels=64,
kernel_size=3,
dilation=4,
groups=64,
offset_groups=64,
padding="same",
)
y = model(torch.rand(4, 64, 256))
# y, offsets = model(x, with_offsets=True) # if you want the offsets too
Modulation (DCNv2)
Both layers accept the modulation of Zhu et al. 2019,
which weights each sampled position by a learned scalar as well as moving it.
Pass mask alongside offsets, with the same shape:
from dc1d.nn import DeformConv1d
model = DeformConv1d(in_channels=64, out_channels=64, kernel_size=3, padding="same")
y = model(x, offsets, mask=mask.sigmoid())
The mask is applied as given, matching torchvision.ops.deform_conv2d: if
you want the [0, 1] modulation of the paper, apply the sigmoid yourself. An
all-ones mask is bit-for-bit identical to passing no mask at all.
PackedDeformConv1d(..., modulated=True) predicts the mask for you, from a
second pointwise branch off the same depthwise trunk that produces the offsets,
ending in a sigmoid. The final projection is zero-initialised, so every tap
starts at exactly 0.5 and the layer has to learn to gate; note this halves the
output scale at initialisation relative to modulated=False. With
with_offsets=True the layer then returns (y, (offsets, mask)) rather than
(y, offsets). The default is modulated=False, which is unchanged DCNv1 and
adds no parameters.
ONNX export
Both layers export to ONNX through the dynamo exporter with no custom operators, and the exported graph is length-agnostic:
torch.onnx.export(
model,
(x,),
"model.onnx",
dynamic_shapes={"input": {2: torch.export.Dim.AUTO}},
dynamo=True,
)
Export at one length and run at any other. tests/test_export.py checks this
against onnxruntime from 120 to 1600 samples for a model exported at 200, and
requires the export dependency group (uv sync --group export).
Going faster, and using less memory
Nothing below changes what the layer returns — the forward stays bit-for-bit
identical, including the invariant that zero offsets reproduce nn.Conv1d
exactly.
torch.compile first. The layer traces to a single graph with no breaks, so
torch.compile(model) works with fullgraph=True and is worth 2.3–9.1× on
the forward and 1.2–2.1× on forward+backward on an RTX 3090. Note that every
distinct sequence length is a separate compilation (~0.3 s), which matters for
variable-length audio.
Then, if you use depthwise offsets, trade a little backward latency for a lot
of memory. The default backward keeps ~3× the sampled tensor alive between the
forward and the backward, or ~8.5× when offset_groups == in_channels. An
alternative backward keeps ~1×:
import functools
from dc1d.nn import DeformConv1d
from dc1d.ops import efficient_linterpolate
model = DeformConv1d(
in_channels=512,
out_channels=512,
kernel_size=3,
padding="same",
interpolation_function=functools.partial(efficient_linterpolate, gather_lerp="recompute"),
)
How much this is worth depends on offset_groups, and the answer changed in
2026-08 when the default gather was rewritten for ONNX correctness and took most
of the saving with it:
offset_groups < in_channels |
offset_groups == in_channels |
|
|---|---|---|
| what the default already holds | ~3× the sampled tensor | ~8.5× |
recompute peak memory, eager |
~1.3× lower | 1.6–2.1× lower |
| cost, eager forward+backward | 10–23% slower | 10–23% slower |
So recompute is clearly worth it for depthwise offsets, which is the
configuration the DTCN paper uses, and marginal otherwise. Under
torch.compile it is both 0–12% faster and smaller, so if you compile
there is no reason not to use it. Full numbers, including why the hand-written
backward does not make eager faster, are in
benchmarks/BACKENDS.md §5.9 and §5.9.4a.
Examples and benchmarks
python playground/readme_example.py # the snippet above
python playground/param_example.py # large-dilation depthwise example
python benchmarks/benchmark.py # timings (add --device cuda for GPU)
The benchmarks use torch.utils.benchmark.Timer, which warms up both paths
equally and synchronises CUDA around the timed region. Timings printed by
earlier versions of this repo (a bare time.time() around an async CUDA launch,
with warmup for the deformable path only) were not meaningful.
Demo notebook
docs/demo.ipynb demonstrates, on tensors small enough to plot, that zero offsets
reproduce nn.Conv1d exactly, that an integer offset is an exact shift, that a
fractional offset interpolates linearly, that the offset gradient matches a finite
difference, that sampling positions survive float16, and that the offsets train. Every
exact property is asserted, so executing the notebook is itself a test. Figures are also
written to docs/demo/.
uv sync --group demo
uv run --group demo jupyter lab docs/demo.ipynb
uv run --group demo jupyter nbconvert --to notebook --execute --inplace docs/demo.ipynb
Papers
Please cite the following if you use this package:
@INPROCEEDINGS{dtcn23,
author={Ravenscroft, William and Goetze, Stefan and Hain, Thomas},
booktitle={ICASSP 2023 - 2023 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP)},
title={Deformable Temporal Convolutional Networks for Monaural Noisy Reverberant Speech Separation},
year={2023},
volume={},
number={},
pages={1-5},
doi={10.1109/ICASSP49357.2023.10095230}}
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 dc1d-0.2.0.tar.gz.
File metadata
- Download URL: dc1d-0.2.0.tar.gz
- Upload date:
- Size: 32.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
554698325826305f71d5058a9a3eb2c44d213e62bc7e62b241d943e1eefaf4cb
|
|
| MD5 |
140c16f769086b6a22c454d5a2b25035
|
|
| BLAKE2b-256 |
6e1ab337197556afdaf28609b4b7df75fbfb350712494ddfeb98afb4fa5ad6a6
|
Provenance
The following attestation bundles were made for dc1d-0.2.0.tar.gz:
Publisher:
release.yml on jwr1995/dc1d
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dc1d-0.2.0.tar.gz -
Subject digest:
554698325826305f71d5058a9a3eb2c44d213e62bc7e62b241d943e1eefaf4cb - Sigstore transparency entry: 2371203705
- Sigstore integration time:
-
Permalink:
jwr1995/dc1d@9efd91bd8bbd16063ca19dd846aa2823d40a0209 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jwr1995
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9efd91bd8bbd16063ca19dd846aa2823d40a0209 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file dc1d-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dc1d-0.2.0-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3b11c7fda8bcfa1650b6f4964c70d67a389c11782d990ebe14269f821f023d8
|
|
| MD5 |
2683c232dc8fd7968d9defadcbc83e0f
|
|
| BLAKE2b-256 |
89e55b6f02698714fb50840b31e8584c11bd099e693e4564409dc555f2938c7e
|
Provenance
The following attestation bundles were made for dc1d-0.2.0-py3-none-any.whl:
Publisher:
release.yml on jwr1995/dc1d
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dc1d-0.2.0-py3-none-any.whl -
Subject digest:
e3b11c7fda8bcfa1650b6f4964c70d67a389c11782d990ebe14269f821f023d8 - Sigstore transparency entry: 2371203715
- Sigstore integration time:
-
Permalink:
jwr1995/dc1d@9efd91bd8bbd16063ca19dd846aa2823d40a0209 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jwr1995
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9efd91bd8bbd16063ca19dd846aa2823d40a0209 -
Trigger Event:
workflow_dispatch
-
Statement type: