Skip to content

Add ConvNeXt for 1D, 2D and 3D inputs#8995

Open
VenkateswarluNagineni wants to merge 6 commits into
Project-MONAI:devfrom
VenkateswarluNagineni:feat/convnext
Open

Add ConvNeXt for 1D, 2D and 3D inputs#8995
VenkateswarluNagineni wants to merge 6 commits into
Project-MONAI:devfrom
VenkateswarluNagineni:feat/convnext

Conversation

@VenkateswarluNagineni

Copy link
Copy Markdown
Contributor

Fixes #4798.

Description

Adds ConvNeXt (A ConvNet for the 2020s) as a classification backbone. MONAI has no ConvNeXt today, even though MedNeXt derives from it — MedNeXt is a segmentation architecture, so the modern convolutional backbone is still missing alongside resnet/densenet/senet/efficientnet.

This follows the direction @wyli gave on the issue:

I think from monai's perspective we can add value to it by supporting both 2d and 3d, and making use of existing monai network layers/blocks whenever possible.

Supports 1D, 2D and 3D. The reference implementation is 2D-only; every layer here is built through MONAI's dimension-agnostic factories, so volumetric inputs work with the same code path. The reference applies the pointwise convolutions as Linear layers on a permuted channels-last tensor; using 1x1 convolutions instead is mathematically equivalent and keeps the block agnostic to the number of spatial dimensions.

Reuses existing MONAI layers rather than re-implementing them:

  • Conv / Pool factories for all dimension-agnostic ops
  • DropPath (monai.networks.layers) for stochastic depth — it is already shape-agnostic, and the DropPath(p) if p > 0 else nn.Identity() guard and torch.linspace schedule follow swin_unetr.py
  • get_act_layer for the act argument
  • trunc_normal_ for the reference's std=0.02 initialisation

The structure follows densenet.py (the features / class_layers split, spatial_dims, in_channels, out_channels ordering, OrderedDict module naming, real subclasses for variants, alias block).

LayerNormNd

ConvNeXt normalises over the channel dimension of a channels-first feature map. torch.nn.LayerNorm normalises over the trailing dimensions, so it expects channels-last and cannot be used directly, and MONAI has no channels-first equivalent (Norm.LAYER is a plain nn.LayerNorm alias). So this PR adds LayerNormNd, which normalises dimension 1 of a (batch, channel, *spatial) tensor for any number of spatial dimensions.

It computes the statistics explicitly over dim=1 instead of permuting to channels-last, which keeps it uniform across ranks and TorchScript-friendly. A test asserts it equals nn.LayerNorm applied to the equivalent permuted tensor.

I've kept it in convnext.py to keep this PR's surface small — happy to promote it to monai/networks/layers/ as a public layer if you'd prefer, since nothing else in MONAI currently offers this.

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • New feature (non-breaking change which adds functionality).
  • New tests added to cover the changes.
  • Integration tests passed locally by running ./runtests.sh -f -u --net --coverage.
  • Quick tests passed locally by running ./runtests.sh --quick --unittests --disttests.
  • In-line docstrings updated.
  • Documentation updated, tested make html command in the docs/ folder.

Docs are updated (docs/source/networks.rst, following the DenseNet entry pattern — base class with :members:, variants without), but I haven't run make html locally, and I'm on Windows so I ran the test files directly with unittest and the linters individually rather than through runtests.sh. Details below — happy to run anything else you'd like to see.

Testing

New file tests/networks/nets/test_convnext.py, 26 tests, all passing (python -m unittest tests.networks.nets.test_convnext):

  • Faithfulness to the reference. The default variants match the parameter counts published by facebookresearch/ConvNeXt exactly for ImageNet-1k (Tiny 28,589,128 / Small 50,223,688 / Base 88,591,464; Large 197,767,336 and XLarge 350,196,968 also verified locally). This checks the architecture is correct rather than merely shape-correct.
  • Shapes for 1D, 2D (non-cubic input) and 3D, over all five variants, plus a case with stochastic depth and layer scale disabled.
  • LayerNormNd equivalence to nn.LayerNorm on the permuted tensor, for 2D and 3D (max difference ~3e-07).
  • TorchScript via test_script_save for every case, including both the layer-scale and no-layer-scale paths.
  • Drop path schedule ascends linearly from 0 to drop_path_rate across the whole network.
  • Ill-args raise ValueError.

Also ran tests/networks/nets/test_densenet.py and test_mednext.py (56 tests) to confirm the nets/__init__.py addition doesn't regress anything.

Checks run on Windows with torch 2.13.0+cpu: ruff check, black --check, isort --check-only all clean on the changed files; mypy monai/networks/nets/convnext.py reports no errors for this file. (I ran the test files directly with unittest rather than the full ./runtests.sh, which expects a POSIX shell.)

Notes

  • No pretrained support in this PR. The reference 2D weights could be mapped onto this implementation in a follow-up (the 1x1-conv vs Linear pointwise weights differ only by a reshape), but that felt like a separate change; MedNeXt likewise ships without pretrained weights.
  • Every spatial dimension of the input should be divisible by 32 (patchify stem of 4 plus three downsamplings), which is documented in the class docstring.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds LayerNormNd and a configurable ConvNeXt implementation supporting 1D, 2D, and 3D inputs, staged blocks, stochastic depth, layer scaling, preset variants, package exports, documentation, and tests for behavior, scripting, parameter counts, and validation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding ConvNeXt support for 1D/2D/3D inputs.
Description check ✅ Passed The PR description mostly follows the template and includes the required summary, change types, and testing notes.
Linked Issues check ✅ Passed The changes implement the requested ConvNeXt architecture from issue #4798 and add the needed docs, exports, and tests.
Out of Scope Changes check ✅ Passed The added LayerNormNd support is directly tied to ConvNeXt, and no unrelated changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
monai/networks/nets/convnext.py (1)

70-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete Google-style documentation for every new definition.

  • monai/networks/nets/convnext.py#L70-L74: document the input and returned tensor.
  • monai/networks/nets/convnext.py#L120-L131: document the block input and residual output.
  • monai/networks/nets/convnext.py#L250-L253: document classifier input and output.
  • monai/networks/nets/convnext.py#L256-L363: document variant constructor parameters and exceptions.
  • tests/networks/nets/test_convnext.py#L82-L92: document parameterized test arguments.
  • tests/networks/nets/test_convnext.py#L147-L151: add a concise test docstring.

As per path instructions, docstrings must be present for all definitions using appropriate Google-style sections.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/networks/nets/convnext.py` around lines 70 - 74, Complete the
Google-style docstrings for all listed definitions: in
monai/networks/nets/convnext.py ranges 70-74, 120-131, and 250-253, document
tensor inputs and outputs; in the variant constructors at 256-363, document
parameters and raised exceptions; in tests/networks/nets/test_convnext.py ranges
82-92 and 147-151, document parameterized arguments and add a concise test
description. Ensure every Args, Returns, and Raises section accurately reflects
the existing signatures and behavior.

Source: Path instructions

tests/networks/nets/test_convnext.py (1)

66-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the aliases claimed by this test matrix.

TEST_VARIANT_CASES contains only canonical ConvNeXt* classes, despite the comment claiming alias coverage. Import and include the exported aliases so broken package exports are detected.

As per path instructions, ensure new or modified definitions are covered by unit tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/networks/nets/test_convnext.py` around lines 66 - 70, Update
TEST_VARIANT_CASES to include the exported ConvNeXt aliases alongside the
canonical classes, preserving coverage across both TEST_CASE_1 and TEST_CASE_2.
Import the aliases from the same package source and ensure each alias is
exercised by the existing matrix so package export regressions are tested.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@monai/networks/nets/convnext.py`:
- Around line 174-188: Update ConvNeXt.__init__ validation to reject empty
depths/features, any nonpositive depth or feature value, nonpositive or even
kernel_size values that would break residual shape matching, and drop_path_rate
outside the inclusive [0, 1] range. Preserve the existing spatial_dims and
equal-length checks, and add unit tests covering each invalid-argument case.

---

Nitpick comments:
In `@monai/networks/nets/convnext.py`:
- Around line 70-74: Complete the Google-style docstrings for all listed
definitions: in monai/networks/nets/convnext.py ranges 70-74, 120-131, and
250-253, document tensor inputs and outputs; in the variant constructors at
256-363, document parameters and raised exceptions; in
tests/networks/nets/test_convnext.py ranges 82-92 and 147-151, document
parameterized arguments and add a concise test description. Ensure every Args,
Returns, and Raises section accurately reflects the existing signatures and
behavior.

In `@tests/networks/nets/test_convnext.py`:
- Around line 66-70: Update TEST_VARIANT_CASES to include the exported ConvNeXt
aliases alongside the canonical classes, preserving coverage across both
TEST_CASE_1 and TEST_CASE_2. Import the aliases from the same package source and
ensure each alias is exercised by the existing matrix so package export
regressions are tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dfedbdb4-57cf-4da5-8fb8-d068ea0cbb0d

📥 Commits

Reviewing files that changed from the base of the PR and between 3a458fe and c4d4ecf.

📒 Files selected for processing (4)
  • docs/source/networks.rst
  • monai/networks/nets/__init__.py
  • monai/networks/nets/convnext.py
  • tests/networks/nets/test_convnext.py

Comment thread monai/networks/nets/convnext.py
@VenkateswarluNagineni

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed in the latest commit:

  • Input validation — added. I confirmed the kernel_size case is a real trap: an even kernel doesn't preserve the spatial size at stride 1, so the residual add fails with an opaque RuntimeError: The size of tensor a (8) must match tensor b (9) deep in forward. It's now rejected in __init__ with a clear message, along with empty/mismatched/non-positive depths/features and an out-of-range drop_path_rate. All are covered by test_ill_arg and documented in the class Raises.
  • Alias coverage — fixed. TEST_VARIANT_CASES now mixes the exported lowercase/mixed-case aliases (convnext_tiny, Convnext_base, …) with the canonical classes, so a broken package export is caught too.

I've left the per-forward docstrings out on purpose: the peer classification nets in this package (densenet.py, senet.py) document behavior at the class level and don't docstring forward/private helpers, and the ConvNeXt/_ConvNeXtBlock/LayerNormNd class docstrings already cover the tensor in/out contract. Following the existing house style there rather than adding docstrings the neighbouring modules don't have.

@aymuos15

Copy link
Copy Markdown
Contributor

happy to promote it to monai/networks/layers/ as a public layer if you'd prefer,

Would definitely do this

@aymuos15

Copy link
Copy Markdown
Contributor

Out of curiosity why not Port the V2 version instead? (Cool contribution!)

@VenkateswarluNagineni

Copy link
Copy Markdown
Contributor Author

Thanks @aymuos15!

Public LayerNormNd — done, just pushed. It now lives in monai/networks/layers/layer_norm_nd.py (mirroring drop_path.py), is exported from monai.networks.layers, has a Layers docs entry and its own equivalence-vs-nn.LayerNorm test, and convnext.py imports it from there — so any channels-first backbone can reuse it instead of it being buried in this net.

ConvNeXt V2 — good call, and I'd like to add it, but as a focused follow-up rather than swapping it in here:

  • Add ConvNeXt model architecture #4798 asked for ConvNeXt, and V1 is still the most widely used variant with the broadest set of released weights, so it's the natural first backbone to land.
  • This PR pins architectural fidelity by asserting exact parameter-count parity with the official V1 release (Tiny 28,589,128 / Small 50,223,688 / Base 88,591,464).
  • V2's changes sit cleanly on exactly this scaffolding: it drops LayerScale and adds a Global Response Normalization (GRN) layer per block (the FCMAE pretraining recipe is orthogonal to the modules). I can add a GRN layer + V2 variant presets in a follow-up that reuses this block/stage code directly.

Happy to fold V1 + V2 into this single PR instead if you'd prefer — just say the word.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
monai/networks/layers/layer_norm_nd.py (1)

42-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document forward’s tensor contract.

Add a Google-style docstring for x and the normalized return tensor.

As per path instructions, “Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/networks/layers/layer_norm_nd.py` around lines 42 - 46, Add a
Google-style docstring to LayerNormND.forward documenting the x input tensor and
the normalized tensor returned, including their expected shape or dimensionality
where established by the class contract. Keep the normalization implementation
unchanged and document any raised exceptions only if this method explicitly
raises them.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@monai/networks/layers/layer_norm_nd.py`:
- Around line 33-40: Validate num_channels and spatial_dims at the start of
LayerNormND.__init__, rejecting zero or negative channel counts and negative
spatial dimensions before creating parameters or param_shape; preserve
valid-dimension behavior and add unit tests covering each invalid argument.

---

Nitpick comments:
In `@monai/networks/layers/layer_norm_nd.py`:
- Around line 42-46: Add a Google-style docstring to LayerNormND.forward
documenting the x input tensor and the normalized tensor returned, including
their expected shape or dimensionality where established by the class contract.
Keep the normalization implementation unchanged and document any raised
exceptions only if this method explicitly raises them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47c89d08-0c0f-492d-9b73-fbbca5094a30

📥 Commits

Reviewing files that changed from the base of the PR and between a66704b and d0ca6bd.

📒 Files selected for processing (6)
  • docs/source/networks.rst
  • monai/networks/layers/__init__.py
  • monai/networks/layers/layer_norm_nd.py
  • monai/networks/nets/convnext.py
  • tests/networks/layers/test_layer_norm_nd.py
  • tests/networks/nets/test_convnext.py
💤 Files with no reviewable changes (1)
  • tests/networks/nets/test_convnext.py

Comment on lines +33 to +40
def __init__(self, num_channels: int, spatial_dims: int, eps: float = 1e-6) -> None:
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(num_channels))
self.bias = nn.Parameter(torch.zeros(num_channels))
# broadcast the affine parameters against (batch, channel, *spatial); precomputed so that the
# module is scriptable without inspecting the rank of the input at runtime.
self.param_shape = [1, num_channels] + [1] * spatial_dims

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid layer dimensions.

Negative spatial_dims produces an affine shape that can broadcast over the wrong axis; num_channels=0 also creates an unusable layer. Validate both and add invalid-argument tests.

Proposed fix
     def __init__(self, num_channels: int, spatial_dims: int, eps: float = 1e-6) -> None:
         super().__init__()
+        if num_channels <= 0:
+            raise ValueError("num_channels must be positive.")
+        if spatial_dims < 0:
+            raise ValueError("spatial_dims must be non-negative.")
         self.eps = eps

As per path instructions, “Examine code for logical error or inconsistencies” and ensure modified definitions have unit tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __init__(self, num_channels: int, spatial_dims: int, eps: float = 1e-6) -> None:
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(num_channels))
self.bias = nn.Parameter(torch.zeros(num_channels))
# broadcast the affine parameters against (batch, channel, *spatial); precomputed so that the
# module is scriptable without inspecting the rank of the input at runtime.
self.param_shape = [1, num_channels] + [1] * spatial_dims
def __init__(self, num_channels: int, spatial_dims: int, eps: float = 1e-6) -> None:
super().__init__()
if num_channels <= 0:
raise ValueError("num_channels must be positive.")
if spatial_dims < 0:
raise ValueError("spatial_dims must be non-negative.")
self.eps = eps
self.weight = nn.Parameter(torch.ones(num_channels))
self.bias = nn.Parameter(torch.zeros(num_channels))
# broadcast the affine parameters against (batch, channel, *spatial); precomputed so that the
# module is scriptable without inspecting the rank of the input at runtime.
self.param_shape = [1, num_channels] + [1] * spatial_dims
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/networks/layers/layer_norm_nd.py` around lines 33 - 40, Validate
num_channels and spatial_dims at the start of LayerNormND.__init__, rejecting
zero or negative channel counts and negative spatial dimensions before creating
parameters or param_shape; preserve valid-dimension behavior and add unit tests
covering each invalid argument.

Source: Path instructions

VenkateswarluNagineni and others added 6 commits July 23, 2026 08:51
ConvNeXt is a modern convolutional classification backbone, and MONAI has
no implementation of it although MedNeXt derives from it. Add one that
supports volumetric inputs rather than only the 2D images of the reference
implementation, so that it can be used as a backbone for medical images.

The block is built from the existing dimension agnostic MONAI layers: the
`Conv` and `Pool` factories, `DropPath` for stochastic depth, `get_act_layer`
for the activation, and `trunc_normal_` for the reference initialisation.

Channel normalisation needs a channels-first `LayerNorm`, which MONAI does
not have: `torch.nn.LayerNorm` normalises over the trailing dimensions and
so expects a channels-last layout. `LayerNormNd` normalises the channel
dimension of a (batch, channel, *spatial) tensor for any number of spatial
dimensions, and is verified against `torch.nn.LayerNorm` applied to the
equivalent permuted tensor.

The default variants match the parameter counts published for the reference
2D models, which is asserted by the tests.

Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
…ests

Reject argument combinations that would otherwise fail deep in the forward pass
with an opaque error: an even `kernel_size` cannot preserve the spatial size at
stride 1 and so breaks the residual connection, and empty, mismatched or
non-positive `depths`/`features` and an out-of-range `drop_path_rate` are now
caught in `__init__` with a clear message. Document these in the class `Raises`.

Also mix the exported lowercase aliases into the variant test matrix so that a
broken package export is caught as well as a broken variant.

Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
Move the channels-first LayerNormNd out of the ConvNeXt net into
monai/networks/layers so any channels-first backbone can reuse it,
mirroring the DropPath layer. Export it from monai.networks.layers,
document it in the Layers docs, and add a dedicated layer test that
checks equivalence to nn.LayerNorm on the permuted channels-last tensor.

Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
Cast the bias to a tensor before `nn.init.constant_` (matching the
densenet/resnet/hovernet weight-init idiom) so mypy no longer flags the
`Tensor | Module` type, and document `LayerNormNd` under the
`monai.networks.layers` current module so autodoc can import it.

Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
The channels-first and channels-last shapes in the class docstring were
written as bare tuples, so docutils read the leading asterisk of
`*spatial` as the start of inline emphasis and the docs build failed on
the resulting warning. Mark them up as inline literals instead.

Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
@aymuos15

Copy link
Copy Markdown
Contributor

and V1 is still the most widely used variant with the broadest set of released weights

Wasnt aware pytorch doesnt have the official weights for it. My bad. This LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ConvNeXt model architecture

2 participants