The MLX backend had no handler for aten.native_group_norm or
aten.upsample_nearest2d. GroupNorm sits in every ResBlock of a
Stable-Diffusion-style UNet and nearest upsampling sits in every decoder
stage, so the two gaps together shatter a diffusion model instead of
merely slowing it down: the SDXS-512-DreamShaper UNet partitions into 28
delegate subgraphs, leaving 25 native_group_norm and 2
upsample_nearest2d nodes on the CPU, and each boundary crossing leaves
and re-enters the MLX runtime.
Both ops lower to primitives the backend already has, so this needs no
schema or runtime change.
native_group_norm normalizes each group of C / group channels together
with all of their spatial positions, so reshaping the input to
(N * group, (C / group) * HxW) puts exactly that set on the last axis and
fast::layer_norm computes it as one fused kernel. The affine parameters
are applied afterwards on the original shape rather than being handed to
layer_norm, because group norm's weight and bias are per channel while
layer_norm's are per normalized element; the two only coincide when every
group holds a single channel.
upsample_nearest2d becomes take(take(x, idx_h, -2), idx_w, -1). The
source index for an output position is
min(floor(dst * scale), in_size - 1), which depends only on the static
input and output sizes, so both index vectors are constants. Expressing
it as a gather rather than a repeat also covers non-integer scale factors
and downsampling.
With the handlers registered the same UNet lowers to a single delegate
subgraph with nothing left on the CPU.
Adds op tests for both: group norm over affine and non-affine, one
channel per group and one group for all channels, non-square spatial
extents and a 3D (N, C, L) input; upsampling over integer, anisotropic
and fractional scale factors, explicit output sizes and downsampling.
All 11 configurations match eager through the MLX runtime, the upsample
ones bit-exactly.
Fixes pytorch#22017
Summary
Fixes #22017.
The MLX backend has no handler for
aten.native_group_normoraten.upsample_nearest2d. GroupNorm sits in every ResBlock of a Stable-Diffusion-style UNet and nearest upsampling sits in every decoder stage, so the two gaps together shatter a diffusion model rather than merely slowing it down.Measured on the SDXS-512-DreamShaper UNet (SD-1.5 architecture, 4x64x64 latents):
native_group_norm, 2xupsample_nearest2dEach of those 27 boundaries was a delegate handoff per
denoisecall, leaving and re-entering the MLX runtime.Both ops lower to primitives the backend already has, so this needs no schema or runtime change.
Approach
native_group_normnormalizes each group ofC / groupchannels together with all of their spatial positions. Reshaping the input to(N * group, (C / group) * HxW)puts exactly that set on the last axis, sofast::layer_normcomputes the normalization as a single fused kernel. The affine parameters are applied afterwards on the original shape rather than being passed tolayer_norm, because group norm's weight and bias are per channel while layer_norm's are per normalized element; the two only coincide when every group holds a single channel. Only the normalized output is produced, matching the existingnative_layer_normhandler's treatment of mean/rstd.upsample_nearest2dbecomestake(take(x, idx_h, -2), idx_w, -1). The source index for an output position ismin(floor(dst * scale), in_size - 1)(aten'snearest_neighbor_compute_source_index), which depends only on the static input and output sizes, so both index vectors are constants. Expressing it as a gather rather than a repeat also covers non-integer scale factors and downsampling. Both the.vecand.defaultoverloads are registered.Test plan
Adds
group_normandupsample_nearest2dtobackends/mlx/test/test_ops.py, 11 configurations in total:(N, C, L)inputAll 11 match eager through the MLX runtime; the upsample ones are bit-exact (
rtol = atol = 0).