Skip to content

learning: fused group norm + scale/shift + SiLU, and 3D neighbourhood attention, as CUDA passes - #167

Merged
mcencini merged 8 commits into
mainfrom
claude/project-thread-utke60
Nov 3, 2026
Merged

mcencini merged 8 commits into
mainfrom
claude/project-thread-utke60

Conversation

@mcencini

@mcencini mcencini commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • learning.group_norm_silu(x, groups, weight, bias, scale, shift, eps, silu): a diffusion UNet's block in one pass over memory.
    • src/csrc/ops/groupnorm.cu: Welford fp32 statistics spread over several blocks, then one apply pass in the input's own dtype (fp32, fp16 or bf16).
    • Inference only. Under autograd, on the CPU or without CUDA in the library, plain torch computes the same.
    • ABI: bartorch_gn_built, bartorch_gn_workspace and bartorch_gn_silu, with stubs in groupnorm.c.
  • learning.NeighborhoodAttention3d / neighborhood_attention3d: each voxel attends to a shifted window^3 around it, through three CUDA passes (attention.cu) with their gradients, and a plain-torch fallback.
  • scripts/check_groupnorm.py and scripts/check_attention.py: --standalone builds the one .cu with nvcc and checks and times it on a card, without building BART.

Why

MRI2Qmap's teacher denoiser spent about three quarters of its time outside its convolutions: fp32 group norms, casts and unfused attention. Prebuilt kernels in the wheel are allowed on the targets; compiling at run time is not. Together with the mri2qmap-bartorch changes, the teacher's recon goes from 181 s to 140 s on an A40, with identical maps.

Validation

  • ./scripts/run_tests.sh (CPU build, clang): 2550 passed, 177 skipped. The CUDA tests skip without a card.
  • check_groupnorm.py --standalone --arch 86 on an A40 (nvcc 12.8): max relative error against float64 1.7e-7 (fp32), 3.0e-4 (fp16), 3.5e-3 (bf16). Fused vs eager fp32 norm: (128, 64³) 0.58 vs 5.21 ms, (256, 32³) 0.38 vs 1.39 ms.
  • check_attention.py --standalone on an RTX 4060: max rel error fp32 1.4e-6, fp16 1.5e-3, bf16 1.2e-2. 64³, 2 heads, window 5: 23 ms forward against 0.91 s for plain torch.
  • The full CUDA library build with these files has not been run here (no nvcc in this environment). Both .cu files were compiled standalone with nvcc on the cards above.

Documentation impact

New entries in docs/api/learning.md for group_norm_silu, NeighborhoodAttention3d and neighborhood_attention3d. A release is needed for mri2qmap-bartorch to use the fused norm.


Generated by Claude Code

NeighborhoodAttention3d lets each voxel of a 3D feature map attend to the
window^3 voxels around it (the window shifted inwards at an edge), with a
learned bias per relative position. Its cost is linear in the volume, so it
fits a denoiser at 64^3 where full attention would need over 100 GB.

On a card the operation and its gradient run as three passes compiled into
the library (src/csrc/ops/attention.cu): gather_dot, gather_sum and a
scatter_sum written as a gather, so no atomics and repeatable sums. Float32,
float16 and bfloat16, 8 to 64 channels per head, windows up to 9, sums in
float32. A build without CUDA exports the entry points and reports them absent;
anywhere else a plain-torch path serves.

scripts/check_attention.py compares the passes and their gradients with the
plain-torch path and times them; --standalone builds attention.cu alone with
nvcc, so the kernels can be checked without building BART.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPF5rgL8sKizNYzB8D
…hout importing bartorch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPF5rgL8sKizNYzB8D
…al; documented; exact reference

The plain-torch path computes in float64 for float64 inputs, so the tests hold
it to the voxel-by-voxel attention to round-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPF5rgL8sKizNYzB8D
…hift and SiLU in one CUDA pass

Two short passes take a group's float32 statistics (Welford, a group
spread over several blocks and merged), and one more reads the
activations once more and writes them once, in their own element type,
with the norm's affine, the per-item scale and shift and SiLU folded
into one multiply-add per row. Eager torch reads and writes them four
or five times, with casts around a float32 norm, which in MRI2Qmap's
teacher costs more than its convolutions.

Inference only: under autograd, without a card or without CUDA in the
library, plain torch computes the same. scripts/check_groupnorm.py
--standalone builds groupnorm.cu alone with nvcc to check and time it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPoLKTK5gZEQeb1W8D
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.32710% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bartorch/learning/_neighborhood.py 94.57% 7 Missing ⚠️
src/bartorch/learning/_groupnorm.py 95.31% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

… the host

_neighborhood._on gives the card's stream with that card current, and no
stream off a card; the group norm's call moves into _launch. A stand-in
library doing the passes' arithmetic in torch on the host memory it is
handed then checks everything on the Python side of the ABI without a
card: pointers, shapes, workspace, the error report, and the attention's
autograd function with its gradients against the plain-torch path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPoLKTK5gZEQeb1W8D
The standalone checks run nvcc with a fixed argument list on a file of
this checkout; nothing reaches it from outside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPoLKTK5gZEQeb1W8D

mcencini commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

CodeFactor reports 8 minor issues on this PR. I can't see which ones, because its issue list only renders in a signed-in browser.

My guess was bandit's subprocess warnings in the two --standalone check scripts. I marked those with # nosec in c14fd3d, but the count stayed at 8, so that guess was wrong. I'm leaving the rest as they are, since they're minor. If someone can see the list, I'll fix whichever ones are real.

Coverage: the CUDA calls are now exercised on the host through a stand-in library in 371f622 (tests/test_learning_kernel_calls.py). That brings _groupnorm.py and _neighborhood.py to about 95 % line coverage locally.


Generated by Claude Code

cuDNN's fastest 3-D convolutions run channels last; a norm that only reads
NCDHW forces a transpose on either side. The pass now takes channels-last
input as it lies (statistics per channel strip, groups merged after) and
writes channels-last output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPF5rgL8sKizNYzB8D
The statistics pass split each item into as many runs as a single group's
size called for, so a deep level (512 channels at 8^3, three items) ran on
three blocks; the A40 showed it 3-12x slower than the NCDHW pass below 64^3
and 2.5-4x at 64^3. Runs are now sized by the item's whole extent, at least
4096 elements each and at most 256 per item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MFB6QPF5rgL8sKizNYzB8D
@mcencini
mcencini merged commit a378f70 into main Nov 3, 2026
58 of 61 checks passed
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.

2 participants