Fix CUDA AveragePool wrong results with asymmetric padding and dilation#29631
Fix CUDA AveragePool wrong results with asymmetric padding and dilation#29631titaiwangms wants to merge 6 commits into
Conversation
cuDNN's pooling descriptor (CudnnPoolingDescriptor::Set) stores a single symmetric pad value per axis and applies it to both sides, so it silently discards the ONNX end pad whenever pad_begin != pad_end. All asymmetric-pad AveragePool on CUDA was therefore wrong (explicit asymmetric pads, auto_pad=SAME_UPPER/SAME_LOWER, and ceil_mode boundaries) -- confirmed by GPU probe (1D pad(0,3) CUDA=[4,6.5,8] vs CPU=[4,5.571,4]; 2D pad(0,0,3,3) max_abs_diff 53.25). Add a custom AveragePoolWithPad CUDA kernel (avg_pool_impl.cu/.h, modeled on max_pool_with_index.cu) that honors per-side pads and matches the CPU reference divisor exactly: start=idx*stride-pad_begin, end=min(start+kernel*dilation, in+pad_end); include-pad divisor = product of (1+(end-start-1)/dilation), exclude-pad divisor = in-bounds cell count. Covers fp32/fp64/fp16/bf16 (half and bf16 accumulate in float). Dispatch guard in Pool<T,AveragePool>::ComputeInternal routes only asymmetric-pad AveragePool to the kernel; symmetric pads (the common case, including all global pooling) keep the unchanged fast cuDNN path, so there is zero perf regression. MaxPool asymmetric pads were probe-verified already correct on cuDNN, so MaxPool is left unchanged. Agent-signed-off: Developer (118f158e) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xcluded) Add PoolTest cases covering the cuDNN asymmetric-end-pad AveragePool fix, with the CUDA EP un-excluded so the CUDA leg actually runs against the CPU reference oracle: 1D/2D explicit asymmetric tail pads (include- and exclude-pad divisor branches), auto_pad=SAME_UPPER (naturally asymmetric), an fp16 case exercising the half accumulate-in-float path, a symmetric-pad regression guard (must stay on cuDNN), and a MaxPool asymmetric probe/regression. Expected values are the CPU reference outputs, cross-checked against the GPU probe numbers (1D=[4,5.571,4], 2D last=17.75). Agent-signed-off: Developer (118f158e) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cuDNN's pooling descriptor has no dilation parameter, so symmetric-pad AveragePool with dilation>1 silently dropped the dilation on the cuDNN fast path. Extend the ComputeInternal dispatch guard from asymmetric-pads only to also catch !default_dilations (mirroring MaxPool<8>), routing those cases to the custom AveragePoolWithPad kernel which honors dilation. Exclude global pooling explicitly: PoolAttributes returns early for it and leaves kernel_shape/strides/dilations unpopulated (default_dilations stays false), so it must stay on cuDNN. Also rename the begin-pad kernel params pad_h/w/d -> pad_*_head for symmetry with the existing pad_*_tail. Agent-signed-off: Developer (118f158e) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lock in the extended dispatch guard: a symmetric-pad AvgPool with dilation>1 (opset 19) that would be wrong on cuDNN but matches the CPU AveragePoolV19 reference via the custom kernel, plus a SAME_LOWER asymmetric-pad case complementing the existing SAME_UPPER test. Both run with the CUDA leg un-excluded. Documents why bf16 is not tested (ONNX AveragePool schema type constraint excludes bfloat16, so OpTester rejects the graph at load). Agent-signed-off: Developer (118f158e) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes CUDA EP AveragePool correctness when cuDNN cannot faithfully represent the ONNX pooling request—specifically for asymmetric per-side padding and dilation > 1—by adding a CUDA fallback kernel and routing only the unsupported cases to it, while keeping the cuDNN fast path for the common symmetric/non-dilated cases.
Changes:
- Add a custom CUDA kernel (
AveragePoolWithPad) that implements ONNX per-side pads, dilation, andcount_include_paddivisor behavior consistent with the CPU reference functors. - Add a targeted dispatch guard in the CUDA pooling kernel to use the custom implementation only for non-global AveragePool with asymmetric pads or non-default dilations.
- Add CUDA parity tests (kept CUDA un-excluded) to validate asymmetric padding, SAME_* auto_pad asymmetry, dilation>1, and fp16 behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| onnxruntime/test/providers/cpu/nn/pool_op_test.cc | Adds CUDA parity tests covering asymmetric pads, SAME_* auto_pad, dilation>1, fp16, and a MaxPool unaffected probe. |
| onnxruntime/core/providers/cuda/nn/pool.cc | Routes AveragePool to a custom CUDA kernel when pads are asymmetric or dilation is non-default (excluding global pooling). |
| onnxruntime/core/providers/cuda/nn/avg_pool_impl.h | Declares the new AveragePoolWithPad CUDA fallback API. |
| onnxruntime/core/providers/cuda/nn/avg_pool_impl.cu | Implements the CUDA kernel and instantiations for fp32/fp64/fp16/bf16 (NHWC guarded). |
Review summaryStrong PR. The custom kernel is a faithful, verified port of the CPU v19 reference ( Verified a potential concern and cleared it: symmetric explicit pad + 🟠 Major — test coverageNew CUDA tests cover only NCHW 1D/2D. The kernel's highest-risk logic is the layout/rank-dependent indexing, but the NHWC decode branch and the 3D traversal branch are never exercised on CUDA. Recommend adding:
Also, the pre-existing 🟡 Minor
⚪ Nit
Notes / follow-ups (out of scope)
Reviewed by a multi-model review team (readability / correctness / adversarial / spec-adherence+numerical / cross-module integration). All nine new expected-value arrays were independently re-derived from the CPU v19 reference; the symmetric-ceil coverage question was resolved against a pre-existing CUDA-enabled test. |
…mment tidy (microsoft#29631) Review-feedback fixups for PR microsoft#29631 (no correctness changes): - Coverage: un-exclude the CUDA (NCHW) leg on the two pre-existing asymmetric-pad parity tests AveragePool_CountIncludePad_AsymmetricPads and AveragePool3D_CountIncludePad_AsymmetricPads. This PR fixes exactly that path; the existing expected values are CPU-reference values and the custom kernel mirrors CPU, so they apply directly. Closes the missing 3D-CUDA coverage gap. Other EP exclusions (cuDNN NHWC, TensorRT, CoreML, etc.) are kept as-is. - Comment accuracy: the cuDNN dispatch-guard comment in cuda/nn/pool.cc listed 'ceil_mode boundaries' as a cause requiring the custom kernel. Symmetric ceil_mode is v19-correct on cuDNN, so this was inaccurate. Reworded to the real causes of asymmetric padding: explicit asymmetric pads and SAME_UPPER/SAME_LOWER. - Readability: de-jargon the new CUDA test comments ('Target (b)', 'QA probe') to describe what the tests verify for external readers. CUDA Debug build; full PoolTest suite passes (53 passed, 2 DML-skipped). The two un-excluded tests now run on the CUDA EP and pass. Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eview) Two doc/comment-only Minor fixups (no logic change): - avg_pool_impl.h: apply the same 'ceil_mode boundaries' correction already made in pool.cc to this sibling header so the two agree. Asymmetric padding arises from explicit asymmetric pads or SAME_UPPER/SAME_LOWER resolving to asymmetric pads — not from ceil_mode (symmetric ceil_mode is v19-correct on cuDNN). - pool_op_test.cc: correct the kCudaNHWCExecutionProvider-exclusion comment on the two un-excluded asymmetric tests. NHWC is excluded because the custom kernel's NHWC decode branch is not yet covered by these tests (asymmetric NHWC routes to that branch, NOT to cuDNN) — tracked as a follow-up. The exclusion itself is unchanged. Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review fold-in (post-review fixups pushed:
|
Summary
Fixes wrong
AveragePooloutput on the CUDA EP whenever cuDNN's pooling descriptor cannot represent the requested pooling — i.e. asymmetric padding or dilation > 1. This is the CUDA-EP counterpart of the CPU fix in #29629 and, together with it, the JSEP shape fix in #29627, closes out the CUDA leg of pytorch/pytorch#183528.Root cause
CudnnPoolingDescriptor::Setcopies only the begin pads (pads[0..rank)) into the cuDNN pooling descriptor, which stores a single symmetric pad value per axis and applies it to both sides. The ONNX end pads (pads[rank..2*rank)) are silently dropped. As a result, all asymmetric-pad AveragePool on CUDA is wrong:pads,auto_pad = SAME_UPPER/SAME_LOWER(which produce naturally asymmetric pads),ceil_mode = 1boundary windows.Separately, the cuDNN pooling descriptor has no dilation parameter at all, so any
dilations > 1is also silently ignored — even with symmetric pads.Example divergence from the CPU reference (QA probe): 1D
pad(0,3), k7, s3,ceil_mode=1,count_include_pad=1gave CUDA[4, 6.5, 8]vs. correct[4, 5.571, 4]; 2Dpad(0,0,3,3)gave71.0vs. correct17.75.Fix
Add a custom CUDA average-pool kernel (
avg_pool_impl.cu/.h, modeled onmax_pool_with_index.cu) that honors per-side pads and dilation, and computes thecount_include_paddivisor exactly like the CPU v19 reference functor (AveragePool{1,2,3}DTask):input + pad_tail(drops the ceil-mode phantom cells), dividing by∏ (1 + (end - start - 1)/dilation);In
Pool<T, AveragePool, Layout>::ComputeInternal, a cheap dispatch guard routes to the custom kernel only when the pooling is non-global and (asymmetric padsor!default_dilations). Every symmetric, non-dilated case — the overwhelmingly common path, including allGlobalAveragePool— stays on the existing cuDNN fast path, so there is zero perf regression on the common path.GlobalAveragePoolis excluded explicitly becausePoolAttributesleaves itskernel_shape/strides/dilationsunpopulated.MaxPoolis unaffected (it ignores pad cells;MaxPool<8>already routes dilation to its own custom kernel via the same!default_dilationscheck we mirror here).Covers fp32, fp64, fp16, and bf16 (fp16/bf16 accumulate in float).
Tests
Added CUDA-un-excluded parity tests in
pool_op_test.cc— the CUDA leg actually runs and must match the CPU reference oracle:auto_pad=SAME_UPPERandSAME_LOWER(naturally asymmetric);MaxPoolasymmetric-pad case confirming MaxPool is unaffected.The
ceil_mode + count_include_padcases use opset 19 so the CPU leg runs the already-correct v19 reference functor and validates the CUDA kernel independently of the separate opset-7..18 CPU MLAS fix (#29629); CUDA routing is opset-independent. FullPoolTestsuite passes on an A100 (53 passed / 2 DML-skipped / 0 failures).Related
ceil_mode + count_include_paddivisor fix.ceil_mode.avg_pool2dwithceil_mode=Trueandcount_include_pad=Truepytorch/pytorch#183528.