Skip to main content

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

dc1d-0.2.0.tar.gz (32.2 kB view details)

Uploaded Source

Built Distribution

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

dc1d-0.2.0-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

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

Hashes for dc1d-0.2.0.tar.gz
Algorithm Hash digest
SHA256 554698325826305f71d5058a9a3eb2c44d213e62bc7e62b241d943e1eefaf4cb
MD5 140c16f769086b6a22c454d5a2b25035
BLAKE2b-256 6e1ab337197556afdaf28609b4b7df75fbfb350712494ddfeb98afb4fa5ad6a6

See more details on using hashes here.

Provenance

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

Publisher: release.yml on jwr1995/dc1d

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

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

Hashes for dc1d-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e3b11c7fda8bcfa1650b6f4964c70d67a389c11782d990ebe14269f821f023d8
MD5 2683c232dc8fd7968d9defadcbc83e0f
BLAKE2b-256 89e55b6f02698714fb50840b31e8584c11bd099e693e4564409dc555f2938c7e

See more details on using hashes here.

Provenance

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

Publisher: release.yml on jwr1995/dc1d

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 Sentry Error logging StatusPage Status page