MLX: add native_group_norm and upsample_nearest2d handlers - #22050
MLX: add native_group_norm and upsample_nearest2d handlers#22050msluszniak wants to merge 1 commit into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22050
Note: Links to docs will display an error until the docs builds have been completed. ❌ 5 Pending, 1 Unclassified FailureAs of commit bb67233 with merge base fbd4bbf ( UNCLASSIFIED FAILURE - DrCI could not classify the following job because the workflow did not run on the merge base. The failure may be pre-existing on trunk or introduced by this PR:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
|
This PR needs a
|
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
9d8cb4a to
bb67233
Compare
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).