Fix JSEP pooling output shape to honor ceil_mode#29627
Conversation
js/web PoolConvUtil.computePoolOutputShape was floor-only and silently ignored ceil_mode, allocating output tensors one element too small along each spatial axis for ceil_mode=1. Thread a ceilMode flag through computePoolOutputShape -> computeShapeHelper -> adjustPadAndReturnShape in both js/web/lib/wasm/jsep/util.ts and the legacy js/web/lib/onnxjs/util.ts, routing all branches through a new computeOutputSize helper that replicates the C++ pool_attributes.h ComputeOutputSize logic exactly, including the shrink-last-window-if-it-starts-in- padding rule. Pass attributes.ceilMode from pool.ts. Floor behavior (ceilMode=0, the default) is unchanged, so Conv and auto_pad adjustment are unaffected. Adds js/web/test/unittests/pool-output-shape.ts asserting output shapes for both implementations against the CPU reference ground-truth cases (maxpool_2d_ceil, AvgPool 1D/2D/3D ceil, dilation, VALID, a shrink-rule discriminator, and a floor-mode no-regression case). Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Core of the ORT-Web pooling ceil_mode shape fix: add the ceilMode parameter and the computeOutputSize helper (replicating C++ pool_attributes.h ComputeOutputSize, including the shrink-last-window rule) to js/web/lib/wasm/jsep/util.ts and js/web/lib/onnxjs/util.ts, and pass attributes.ceilMode from pool.ts. Floor-mode behavior is unchanged. Paired with the pool-output-shape unit tests. Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify at the ceil_mode throw sites in parseAveragePool/MaxPoolAttributes (and in the pool-output-shape unit test header) that the output-shape math now honors ceil_mode but end-to-end execution stays gated because the WebGPU kernel does not yet implement ceil_mode trailing-padding handling. Removing the throws + adding kernel support is the tracked follow-up; the shape path is exercised directly by the unit tests until then. Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add SAME_UPPER and SAME_LOWER + ceil_mode=1 shape test cases (the auto_pad branch was modified but previously untested); reviewer-verified in=5, stride=2, kernel=3 -> out 3 (M1). - Use `ceilMode === 1` instead of truthy `ceilMode` in computeOutputSize for exact C++ `ceil_mode == 1` fidelity, so out-of-spec values fall through to floor (M2). - Reword the computeOutputSize doc comment: it produces identical RESULTS to the C++ reference (the JS signature takes a pre-computed numerator, not the raw attributes) (M3). - Comment the shrink guard explaining why inSize/padHead are passed again (M4). - Add a 'Keep in sync with the onnxjs/jsep copy.' breadcrumb in both copies (M5). - Note that the onnxjs copy's ceilMode path exists for test parity only; the onnxjs WebGL caller intentionally does not pass ceilMode (legacy, still throws) (M6). Agent-signed-off: Developer (284885ab) [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 ORT-Web pooling output-shape computation to correctly honor ceil_mode (including the ONNX “shrink the last window if it starts entirely in trailing padding” rule) in both the JSEP and legacy onnxjs copies of PoolConvUtil.computePoolOutputShape. It also adds unit tests that exercise the pure shape helper directly (since end-to-end WebGPU pooling still throws on ceil_mode != 0 pending kernel support).
Changes:
- Thread
ceilModethroughcomputePoolOutputShape → computeShapeHelper → adjustPadAndReturnShapeand centralize per-dimension sizing in acomputeOutputSize()helper. - Pass
attributes.ceilModeinto the WebGPU pooling output-shape computation. - Add unit tests covering ceil-mode shapes (including the shrink rule) for both implementations (jsep + onnxjs).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| js/web/lib/wasm/jsep/util.ts | Adds ceilMode plumbing and computeOutputSize() for pooling/conv output shape calculation. |
| js/web/lib/onnxjs/util.ts | Mirrors the same ceilMode and computeOutputSize() logic for the legacy onnxjs copy. |
| js/web/lib/wasm/jsep/webgpu/ops/pool.ts | Passes ceilMode into shape computation and expands comments around current ceil_mode execution guards. |
| js/web/test/unittests/pool-output-shape.ts | New unit tests validating ceil-mode output shapes (and shrink rule) against CPU-reference expectations. |
| js/web/test/unittests/index.ts | Registers the new unit test module. |
Review summarySolid, well-tested, and correctly scoped (shape-only, execution still gated). The ceil-mode math matches the C++ reference ( 🔴 Major (1)
const legacyTargetSize = (inSize + stride - 1) / stride; // float; C++ uses int64 divisionReproduced with
Floor mode masks this (the extra pad is absorbed by Fix: use integer division to match C++, in both files: const legacyTargetSize = Math.floor((inSize + stride - 1) / stride);and add a non-divisible regression case, e.g. 🟡 Minor (2)
⚪ Nits
Follow-up (out of scope, already tracked)End-to-end Reviewed by a multi-model review team (readability / correctness / adversarial / spec-adherence / cross-module integration). |
…ity pass MAJOR: In both js/web/lib/wasm/jsep/util.ts and js/web/lib/onnxjs/util.ts the SAME_UPPER/SAME_LOWER legacyTargetSize used JS float division while C++ pool_attributes.h uses integer division. Wrap it in Math.floor so the auto_pad pad computation matches C++ ComputeSizePadDilations exactly. Pre-existing latent bug the new ceil_mode thread exposes: in=2,stride=2,kernel=3,SAME_UPPER,ceil=1 yielded [.,.,2] instead of the correct [.,.,1]. TEST: add non-divisible SAME_UPPER + SAME_LOWER + ceil_mode regression cases (input spatial [2] -> [1]) that fail without the Math.floor fix and pass with it. CLARITY (doc-only): reword the retained ceil_mode throw messages in pool.ts to point at KERNEL/EXECUTION (padding/divisor) rather than shape computation, and qualify the ceil_mode TODO banner to note the output shape is supported while kernel execution is pending. Agent-signed-off: Developer (284885ab) [claude-opus-4.8 via copilot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…icrosoft#29627) This fixup fixes SAME_UPPER/SAME_LOWER pad distribution for ALL ceil modes, not only ceil_mode=1: the earlier float-division legacyTargetSize mis-rounded the auto_pad pad split on the ceil_mode=0 (floor) path too, producing wrong (and sometimes fractional) pads even though the output SHAPE was unchanged there. The Math.floor(legacyTargetSize) fix aligns JSEP/onnxjs with the CPU/CUDA EP behavior; nothing previously-correct regresses (SAME_* ceil_mode=0 output shape is unchanged — only the pad split is corrected). - Readability: add a one-line WHY breadcrumb above the Math.floor line in both js/web/lib/wasm/jsep/util.ts and js/web/lib/onnxjs/util.ts. - Coverage: add ceilMode=0 SAME_UPPER and SAME_LOWER non-divisible regression cases (in=2, stride=2, kernel=3) that assert the corrected pad out-param distribution (SAME_UPPER -> [0,1], SAME_LOWER -> [1,0]), not just the output shape. Extended the test harness to capture and assert the mutated pads. These fail against the pre-fix float division ([1,1]) and pass with Math.floor. 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
The ORT-Web JSEP pooling output-shape helper (
PoolConvUtil.computePoolOutputShape→computeShapeHelper→adjustPadAndReturnShape) was floor-only: it had noceilModeparameter and silently ignoredceil_mode. Underceil_mode=1this allocated the output tensor one element too small along each affected spatial axis, producing a wrong output shape (and downstream shape mismatches) — independent of any pooling divisor concern.The same floor-only helper is duplicated in the legacy
js/web/lib/onnxjs/util.ts; both copies are fixed.Fix
ceilModeparameter (default0, i.e. floor) throughcomputePoolOutputShape→computeShapeHelper→adjustPadAndReturnShapein bothjs/web/lib/wasm/jsep/util.tsandjs/web/lib/onnxjs/util.ts.computeOutputSize()helper that produces results identical to the C++ referencePoolAttributes::ComputeOutputSize(onnxruntime/core/providers/cpu/nn/pool_attributes.h), including theceil_mode"shrink the last window if it starts entirely in the trailing padding" rule (ref: Fix corner case where output size need to reduce by one in MaxPool onnx/onnx#5741).ceilModedefault) is algebraically unchanged, so Conv andauto_padpad-adjustment are unaffected.js/web/lib/wasm/jsep/webgpu/ops/pool.tsnow passesattributes.ceilModeinto the shape computation.Scope: SHAPE-only
This PR fixes the output-shape computation only. End-to-end
ceil_modeexecution remains gated by the existingthrowguards inparseAveragePoolAttributes/parseMaxPoolAttributes, because the WebGPU pooling kernel does not yet implementceil_modetrailing-padding handling (and, for AveragePool, thecount_include_paddivisor). Removing those throws + adding kernel support is a tracked follow-up; the now-correct shape path is exercised directly by the added unit tests until then. Comments at the throw sites and in the test header document this.Tests
Adds
js/web/test/unittests/pool-output-shape.ts(registered inunittests/index.ts), asserting output shapes for both implementations (jsep + onnxjs) against the C++ CPU reference ground truth:test_maxpool_2d_ceil→[1,1,2,2],AveragePool_10_ceil1_2d→[1,1,2,3]ceil1D[1,2,4]/ 2D[1,1,3,3]/ 3D[1,1,2,2,2](matching the CPUpool_op_test.cccases)ceil_modewith dilation,VALID/SAME_UPPER/SAME_LOWERauto_padceil()would give 3; correct = 2)Related
avg_pool2dwithceil_mode=Trueandcount_include_pad=Truepytorch/pytorch#183528Post-review fixups
legacyTargetSize = (inSize + stride - 1) / stridecomputation now usesMath.floor(...)to match C++pool_attributes.hComputeSizePadDilations, which uses integer division. This fixes a latent float-division divergence that mis-rounded the auto_pad pad distribution. The correction applies to all ceil modes (not onlyceil_mode=1), aligning JSEP/onnxjs with the CPU/CUDA EPs. The floor-path (ceil_mode=0) output shape is unchanged — only the SAME_* pad split is corrected — so nothing previously-correct regresses. Addedceil_mode=0SAME_UPPER/SAME_LOWER non-divisible regression tests that assert the corrected pad out-param (SAME_UPPER →[0,1], SAME_LOWER →[1,0]).ceil_modethrowstatements and the pool op TODO banner were reworded to make explicit that the output shape is now computed correctly, whileceil_modekernel execution (padding/divisor) remains the pending WebGPU follow-up.