Skip to content

Commit 988c912

Browse files
Merge branch 'master' into sync_msft_10_7_2026
2 parents 40cb0bf + f4aa2b4 commit 988c912

32 files changed

Lines changed: 1685 additions & 246 deletions

js/web/lib/onnxjs/util.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,8 @@ export class PoolConvUtil {
12601260
* @param pads Padding for the beginning and ending along each axis.
12611261
* @param autoPad DEPRECATED attribute supported for legacy models. Specifies how to implicitly calculate pads in each
12621262
* dimension. Can take values NOTSET, SAME_UPPER, SAME_LOWER, or VALID.
1263+
* @param ceilMode When set to 1, use ceil() instead of floor() to compute the output spatial size (and apply the
1264+
* "shrink the last window if it starts entirely in padding" rule). Defaults to 0 (floor).
12631265
*/
12641266
static computePoolOutputShape(
12651267
isGlobalOperator: boolean,
@@ -1269,6 +1271,7 @@ export class PoolConvUtil {
12691271
kernelShape: number[],
12701272
pads: number[],
12711273
autoPad?: string,
1274+
ceilMode = 0,
12721275
): number[] {
12731276
if (inputDims.length <= 0) {
12741277
throw new Error('input shape must be of size greater than 0');
@@ -1286,6 +1289,7 @@ export class PoolConvUtil {
12861289
kernelShape,
12871290
pads,
12881291
autoPad,
1292+
ceilMode,
12891293
);
12901294
return outputDims;
12911295
}
@@ -1332,6 +1336,7 @@ export class PoolConvUtil {
13321336
kernelShape: readonly number[],
13331337
pads: number[],
13341338
autoPad?: string,
1339+
ceilMode = 0,
13351340
) {
13361341
if (isGlobalOperator) {
13371342
for (let dim = 0; dim < inputDims.length - 2; dim++) {
@@ -1349,12 +1354,44 @@ export class PoolConvUtil {
13491354
dim,
13501355
dim + inputDims.length - 2,
13511356
autoPad,
1357+
ceilMode,
13521358
),
13531359
);
13541360
}
13551361
}
13561362
}
13571363

1364+
// Computes the output spatial size for a single dimension.
1365+
// Produces results identical to the C++ PoolAttributes::ComputeOutputSize
1366+
// (onnxruntime/core/providers/cpu/nn/pool_attributes.h), including the ceil_mode
1367+
// "shrink the last window if it starts entirely in the trailing padding" rule. The JS
1368+
// signature takes a pre-computed `numerator` (equal to `inSize + padHead + padTail - dkernel`,
1369+
// matching the C++ `in_size + pad_head + pad_tail - dilation * (kernel - 1) - 1`) instead of
1370+
// the raw pooling attributes, but the computed output size is the same.
1371+
// Keep in sync with the onnxjs/jsep copy.
1372+
// NOTE: In this onnxjs copy the ceilMode path exists for shape-test parity with the jsep copy;
1373+
// the onnxjs WebGL pooling caller (backends/webgl/ops/pool.ts) intentionally does NOT pass
1374+
// ceilMode (legacy path still throws on ceil_mode != 0), so it always uses the floor default.
1375+
private static computeOutputSize(
1376+
numerator: number,
1377+
stride: number,
1378+
inSize: number,
1379+
padHead: number,
1380+
ceilMode: number,
1381+
): number {
1382+
let outSize = Math.floor(numerator / stride) + 1;
1383+
// Match C++ `ceil_mode == 1` exactly so out-of-spec ceil_mode values do not diverge.
1384+
if (ceilMode === 1) {
1385+
outSize = Math.ceil(numerator / stride) + 1;
1386+
// Ensure the last pooling window starts inside the image (ref: https://github.com/onnx/onnx/pull/5741).
1387+
// inSize and padHead are needed here to reconstruct the last window's start position.
1388+
if ((outSize - 1) * stride >= inSize + padHead) {
1389+
outSize -= 1;
1390+
}
1391+
}
1392+
return outSize;
1393+
}
1394+
13581395
// helper for computeShapeHelper() and adjustPadsBasedOnAutoPad()
13591396
// adjusts pad value for given 'autoPad' string and computes output shape along a particular dimension
13601397
private static adjustPadAndReturnShape(
@@ -1366,30 +1403,44 @@ export class PoolConvUtil {
13661403
padHeadIndex: number,
13671404
padTailIndex: number,
13681405
autoPad?: string,
1406+
ceilMode = 0,
13691407
): number {
13701408
const dkernel = dilation * (kernel - 1) + 1;
13711409
if (autoPad && autoPad !== 'NOTSET') {
13721410
switch (autoPad) {
13731411
case 'VALID':
13741412
pads[padHeadIndex] = 0;
13751413
pads[padTailIndex] = 0;
1376-
return Math.floor((inSize - dkernel) / stride + 1);
1414+
return PoolConvUtil.computeOutputSize(inSize - dkernel, stride, inSize, 0, ceilMode);
13771415
case 'SAME_LOWER':
13781416
case 'SAME_UPPER':
13791417
if (dilation !== 1) {
13801418
throw new Error('Dilation not supported for SAME_UPPER or SAME_LOWER');
13811419
} else {
1382-
const legacyTargetSize = (inSize + stride - 1) / stride;
1420+
// Integer division to match C++ pool_attributes.h ComputeSizePadDilations; float division mis-rounds SAME_* pads.
1421+
const legacyTargetSize = Math.floor((inSize + stride - 1) / stride);
13831422
const padNeeded = (legacyTargetSize - 1) * stride + kernel - inSize;
13841423
pads[padHeadIndex] = autoPad === 'SAME_LOWER' ? Math.floor((padNeeded + 1) / 2) : Math.floor(padNeeded / 2);
13851424
pads[padTailIndex] = padNeeded - pads[padHeadIndex];
1386-
return Math.floor((inSize + padNeeded - kernel) / stride + 1);
1425+
return PoolConvUtil.computeOutputSize(
1426+
inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel,
1427+
stride,
1428+
inSize,
1429+
pads[padHeadIndex],
1430+
ceilMode,
1431+
);
13871432
}
13881433
default:
13891434
throw new Error('Unsupported AutoPad type');
13901435
}
13911436
} else {
1392-
return Math.floor((inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel) / stride + 1);
1437+
return PoolConvUtil.computeOutputSize(
1438+
inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel,
1439+
stride,
1440+
inSize,
1441+
pads[padHeadIndex],
1442+
ceilMode,
1443+
);
13931444
}
13941445
}
13951446
}

js/web/lib/wasm/jsep/util.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ export class PoolConvUtil {
366366
* @param pads Padding for the beginning and ending along each axis.
367367
* @param autoPad DEPRECATED attribute supported for legacy models. Specifies how to implicitly calculate pads in each
368368
* dimension. Can take values NOTSET, SAME_UPPER, SAME_LOWER, or VALID.
369+
* @param ceilMode When set to 1, use ceil() instead of floor() to compute the output spatial size (and apply the
370+
* "shrink the last window if it starts entirely in padding" rule). Defaults to 0 (floor).
369371
*/
370372
static computePoolOutputShape(
371373
isGlobalOperator: boolean,
@@ -375,6 +377,7 @@ export class PoolConvUtil {
375377
kernelShape: number[],
376378
pads: number[],
377379
autoPad?: string,
380+
ceilMode = 0,
378381
): number[] {
379382
if (inputDims.length <= 0) {
380383
throw new Error('input shape must be of size greater than 0');
@@ -392,6 +395,7 @@ export class PoolConvUtil {
392395
kernelShape,
393396
pads,
394397
autoPad,
398+
ceilMode,
395399
);
396400
return outputDims;
397401
}
@@ -438,6 +442,7 @@ export class PoolConvUtil {
438442
kernelShape: readonly number[],
439443
pads: number[],
440444
autoPad?: string,
445+
ceilMode = 0,
441446
) {
442447
if (isGlobalOperator) {
443448
for (let dim = 0; dim < inputDims.length - 2; dim++) {
@@ -455,12 +460,41 @@ export class PoolConvUtil {
455460
dim,
456461
dim + inputDims.length - 2,
457462
autoPad,
463+
ceilMode,
458464
),
459465
);
460466
}
461467
}
462468
}
463469

470+
// Computes the output spatial size for a single dimension.
471+
// Produces results identical to the C++ PoolAttributes::ComputeOutputSize
472+
// (onnxruntime/core/providers/cpu/nn/pool_attributes.h), including the ceil_mode
473+
// "shrink the last window if it starts entirely in the trailing padding" rule. The JS
474+
// signature takes a pre-computed `numerator` (equal to `inSize + padHead + padTail - dkernel`,
475+
// matching the C++ `in_size + pad_head + pad_tail - dilation * (kernel - 1) - 1`) instead of
476+
// the raw pooling attributes, but the computed output size is the same.
477+
// Keep in sync with the onnxjs/jsep copy.
478+
private static computeOutputSize(
479+
numerator: number,
480+
stride: number,
481+
inSize: number,
482+
padHead: number,
483+
ceilMode: number,
484+
): number {
485+
let outSize = Math.floor(numerator / stride) + 1;
486+
// Match C++ `ceil_mode == 1` exactly so out-of-spec ceil_mode values do not diverge.
487+
if (ceilMode === 1) {
488+
outSize = Math.ceil(numerator / stride) + 1;
489+
// Ensure the last pooling window starts inside the image (ref: https://github.com/onnx/onnx/pull/5741).
490+
// inSize and padHead are needed here to reconstruct the last window's start position.
491+
if ((outSize - 1) * stride >= inSize + padHead) {
492+
outSize -= 1;
493+
}
494+
}
495+
return outSize;
496+
}
497+
464498
// helper for computeShapeHelper() and adjustPadsBasedOnAutoPad()
465499
// adjusts pad value for given 'autoPad' string and computes output shape along a particular dimension
466500
private static adjustPadAndReturnShape(
@@ -472,30 +506,44 @@ export class PoolConvUtil {
472506
padHeadIndex: number,
473507
padTailIndex: number,
474508
autoPad?: string,
509+
ceilMode = 0,
475510
): number {
476511
const dkernel = dilation * (kernel - 1) + 1;
477512
if (autoPad && autoPad !== 'NOTSET') {
478513
switch (autoPad) {
479514
case 'VALID':
480515
pads[padHeadIndex] = 0;
481516
pads[padTailIndex] = 0;
482-
return Math.floor((inSize - dkernel) / stride + 1);
517+
return PoolConvUtil.computeOutputSize(inSize - dkernel, stride, inSize, 0, ceilMode);
483518
case 'SAME_LOWER':
484519
case 'SAME_UPPER':
485520
if (dilation !== 1) {
486521
throw new Error('Dilation not supported for SAME_UPPER or SAME_LOWER');
487522
} else {
488-
const legacyTargetSize = (inSize + stride - 1) / stride;
523+
// Integer division to match C++ pool_attributes.h ComputeSizePadDilations; float division mis-rounds SAME_* pads.
524+
const legacyTargetSize = Math.floor((inSize + stride - 1) / stride);
489525
const padNeeded = (legacyTargetSize - 1) * stride + kernel - inSize;
490526
pads[padHeadIndex] = autoPad === 'SAME_LOWER' ? Math.floor((padNeeded + 1) / 2) : Math.floor(padNeeded / 2);
491527
pads[padTailIndex] = padNeeded - pads[padHeadIndex];
492-
return Math.floor((inSize + padNeeded - kernel) / stride + 1);
528+
return PoolConvUtil.computeOutputSize(
529+
inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel,
530+
stride,
531+
inSize,
532+
pads[padHeadIndex],
533+
ceilMode,
534+
);
493535
}
494536
default:
495537
throw new Error('Unsupported AutoPad type');
496538
}
497539
} else {
498-
return Math.floor((inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel) / stride + 1);
540+
return PoolConvUtil.computeOutputSize(
541+
inSize + pads[padHeadIndex] + pads[padTailIndex] - dkernel,
542+
stride,
543+
inSize,
544+
pads[padHeadIndex],
545+
ceilMode,
546+
);
499547
}
500548
}
501549
}

js/web/lib/wasm/jsep/webgpu/ops/pool.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
} from './common';
2121

2222
// TODO: support:
23-
// - ceil_mode "test_maxpool_2d_ceil"
23+
// - ceil_mode kernel execution "test_maxpool_2d_ceil" (output SHAPE already honors ceil_mode
24+
// via PoolConvUtil.computePoolOutputShape; the WebGPU kernel padding/divisor handling is pending)
2425
// - storage_order "test_maxpool_with_argmax_2d_precomputed_strides"
2526
// - [MaxPool] dilations "test_maxpool_2d_dilations"
2627
// - [MaxPool] output[1] "test_maxpool_with_argmax_2d_precomputed_pads"
@@ -56,6 +57,7 @@ const getAdjustedPoolAttributesAndOutputShape = <AttributeType extends AveragePo
5657
kernelShape,
5758
pads,
5859
attributes.autoPad,
60+
attributes.ceilMode,
5961
);
6062

6163
const newAttributes = Object.assign({}, attributes);
@@ -391,9 +393,15 @@ export const parseAveragePoolAttributes = (attributes: Record<string, unknown>):
391393
const countIncludePad = (attributes.count_include_pad as number) === 0 ? false : true;
392394

393395
const attr = parsePoolCommonAttributes(attributes);
394-
// TODO: support attribute 'ceil_mode'
396+
// ceil_mode is honored by the output-shape math (PoolConvUtil.computePoolOutputShape now
397+
// threads ceilMode and matches the C++ reference), but end-to-end execution stays gated
398+
// here: the WebGPU pooling kernel must also apply the ceil_mode trailing-padding handling
399+
// (and, for AveragePool, the count_include_pad divisor) before this throw can be removed.
400+
// Tracked follow-up: remove this guard + add kernel ceil_mode padding support.
395401
if (attr.ceilMode !== 0) {
396-
throw new Error('using ceil() in shape computation is not yet supported for AveragePool');
402+
throw new Error(
403+
'ceil_mode output-shape is computed, but ceil_mode kernel execution (padding/divisor) is not yet implemented in the WebGPU AveragePool kernel',
404+
);
397405
}
398406
const averagePoolAttributes = { countIncludePad, ...attr, cacheKey: '' };
399407
return { ...averagePoolAttributes, cacheKey: createAveragePoolShaderKeyFromAttributes(averagePoolAttributes) };
@@ -491,12 +499,19 @@ export const parseMaxPoolAttributes = (attributes: Record<string, unknown>): Max
491499
const dilations = attributes.dilations as [number, number];
492500

493501
const attr = parsePoolCommonAttributes(attributes);
494-
// TODO: support attribute 'ceil_mode' and 'storage_order'
502+
// TODO: support attribute 'storage_order'
495503
if (storageOrder !== 0) {
496504
throw new Error('column major storage order is not yet supported for MaxPool');
497505
}
506+
// ceil_mode is honored by the output-shape math (PoolConvUtil.computePoolOutputShape now
507+
// threads ceilMode and matches the C++ reference), but end-to-end execution stays gated
508+
// here: the WebGPU pooling kernel must also apply the ceil_mode trailing-padding handling
509+
// before this throw can be removed. Tracked follow-up: remove this guard + add kernel
510+
// ceil_mode padding support.
498511
if (attr.ceilMode !== 0) {
499-
throw new Error('using ceil() in shape computation is not yet supported for MaxPool');
512+
throw new Error(
513+
'ceil_mode output-shape is computed, but ceil_mode kernel execution (padding) is not yet implemented in the WebGPU MaxPool kernel',
514+
);
500515
}
501516
const maxPoolAttributes = { storageOrder, dilations, ...attr, cacheKey: '' };
502517
return { ...maxPoolAttributes, cacheKey: createMaxPoolShaderKeyFromAttributes(maxPoolAttributes) };

js/web/test/unittests/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ if (typeof window !== 'undefined') {
1313

1414
require('./backends/wasm/test-model-metadata');
1515

16+
require('./pool-output-shape');
17+
1618
require('./opset');

0 commit comments

Comments
 (0)