![【Bug已解决】[Web] using ceil() in shape computation is not yet supported for MaxPool 解决方案](http://pic.xiahunao.cn/yaotu/【Bug已解决】[Web] using ceil() in shape computation is not yet supported for MaxPool 解决方案)
【Bug已解决】[Web] using ceil() in shape computation is not yet supported for MaxPool 解决方案一、现象长什么样在 Web 端ONNX Runtime Web走 WebGPU / WebNN跑一个带MaxPool的模型并且MaxPool开了ceil_mode1输出尺寸向上取整。构造 session 或推理时报错提示形状计算里用了ceil()但不被支持const session await ort.InferenceSession.create(maxpool_ceil.onnx, { executionProviders: [webgpu], // 或 webnn }); // 报错using ceil() in shape computation is not yet supported for MaxPool最小信号MaxPool 带 ceil_mode1 - Web 后端报 ceil() 不支持 MaxPool 带 ceil_mode0 - 正常注意同样的模型在 CPU/CUDA EP 上ceil_mode1完全正常只在 WebWebGPU/WebNN后端报错。这是 Web 后端的形状计算对ceil_mode支持缺失。二、背景MaxPool的输出空间尺寸公式以一维为例是out floor((in 2*pad - dilation*(k-1) - 1) / stride) 1 # ceil_mode0 out ceil ((in 2*pad - dilation*(k-1) - 1) / stride) 1 # ceil_mode1ceil_mode控制结果向下还是向上取整。很多视觉模型尤其输入尺寸不被 stride 整除时依赖ceil_mode1来保证输出覆盖完整感受野。ONNX Runtime 的 CPU/CUDA EP 在图优化阶段会预先算好每个MaxPool的输出形状静态形状推断ceil_mode1时直接用ceil()算出整数尺寸没问题。但 Web 后端WebGPU/WebNN的形状计算路径用来决定 buffer 大小、调度 workgroup没有实现ceil()这个分支—— 它只处理了ceil_mode0的floor()路径遇到ceil_mode1时要么报错“不支持 ceil()”要么算出错误偏小的输出形状导致后续 buffer 分配或 kernel 调度错。三、根因根因是Web 后端的MaxPool形状计算只实现了ceil_mode0的floor()分支没有ceil_mode1的ceil()分支形状计算缺 ceil 分支Web 端的 shape inference / buffer sizing 代码在算MaxPool输出尺寸时写死了floor(...)1没判断ceil_mode属性遇到ceil_mode1直接走不支持路径或报错。与 CPU/CUDA 不一致CPU/CUDA 在静态形状推断里正确处理了ceil_mode所以那两个 EP 正常凸显这是 Web 后端特有的实现缺口。只在 Web 暴露WebGPU/WebNN 后端因为要在 GPU 上精确分配 buffer、算 dispatch 尺寸强依赖准确的输出形状形状算错偏小会导致越界或结果错所以直接报错而不是静默错。所以这不是模型算错而是Web 后端MaxPool形状计算对ceil_mode1的支持缺失。四、最小可运行复现下面用 Python 模拟“ceil_mode 形状计算floor 分支有、ceil 分支缺”的偏差import math def maxpool_out(in_size, k, pad, stride, dilation1, ceil_mode0): MaxPool 输出尺寸公式。 numerator in_size 2 * pad - dilation * (k - 1) - 1 if ceil_mode 1: return math.ceil(numerator / stride) 1 return math.floor(numerator / stride) 1 def web_shape_calc(in_size, k, pad, stride, ceil_mode): Web 后端有 bug只实现了 ceil_mode0 的 floor 分支。 numerator in_size 2 * pad - (k - 1) - 1 if ceil_mode 1: raise NotImplementedError(using ceil() in shape computation is not yet supported) return math.floor(numerator / stride) 1 if __name__ __main__: # in7, k3, pad0, stride2 - ceil_mode1 应输出 4, ceil_mode0 输出 3 correct_ceil maxpool_out(7, 3, 0, 2, ceil_mode1) correct_floor maxpool_out(7, 3, 0, 2, ceil_mode0) print(正确 ceil_mode1:, correct_ceil, ceil_mode0:, correct_floor) try: web_shape_calc(7, 3, 0, 2, ceil_mode1) except NotImplementedError as e: print(Web 后端报错:, e) # 复现 using ceil() not supported assert correct_ceil 4跑出来正确ceil_mode1得 4Web 后端对ceil_mode1抛NotImplementedError正好复现“using ceil() not yet supported”。五、解决方案第一层最小直接修复最小修复在 Web 后端的MaxPool形状计算里补上ceil_mode1的ceil()分支。对使用者临时规避有几种import onnx from onnx import helper # 方式 A导出时把 MaxPool 的 ceil_mode 设成 0若模型允许输出会略小 # 修改 onnx 图里 MaxPool 节点的 attribute ceil_mode 0 # 方式 B用 Pad ceil_mode0 手动模拟 ceil 效果补 padding 让 floor 等价于 ceil # 在 MaxPool 前插 Pad使 (in2pad) 恰好被 stride 整除 # 对 ORT 仓库侧修复 web_shape_calc 增加 ceil 分支见第四节公式对 ORT 仓库侧修复是改 Web 后端的MaxPoolshape inference读ceil_mode属性为 1 时走math.ceil分支算输出尺寸。这一层立刻让ceil_mode1在 Web 上可用。六、解决方案第二层结构性改进把“Web 后端各算子的形状计算必须覆盖所有 mode 属性”收口成唯一的配置对象OrtMaxPoolCeilShapePolicyWeb 形状计算读它from dataclasses import dataclass, field from typing import Tuple dataclass(frozenTrue) class OrtMaxPoolCeilShapePolicy: Web MaxPool 形状计算的单一事实来源。 # 必须支持的 ceil_mode 取值 supported_ceil_modes: Tuple[int, ...] (0, 1) # 输出尺寸公式用的取整函数映射 round_fn: Tuple[str, str] (floor, ceil) # (ceil_mode0, ceil_mode1) # 受影响的 Web 后端 web_backends: Tuple[str, ...] (webgpu, webnn) # 是否校验所有 mode 都被实现缺一个就 fail require_all_modes: bool True def out_size(self, in_size, k, pad, stride, dilation1, ceil_mode0) - int: num in_size 2 * pad - dilation * (k - 1) - 1 fn self.round_fn[ceil_mode] import math return (math.ceil if fn ceil else math.floor)(num / stride) 1 def describe(self) - str: return Web MaxPool 形状计算覆盖 ceil_mode0/1不再缺 ceil 分支 POLICY OrtMaxPoolCeilShapePolicy() def plan_maxpool_shape(in_size, k, pad, stride, ceil_mode, policy: OrtMaxPoolCeilShapePolicy POLICY) - int: return policy.out_size(in_size, k, pad, stride, ceil_modeceil_mode)所有 Web 形状计算读同一份POLICYceil_mode1不再缺分支新增 mode 必须实现否则 fail。七、解决方案第三层断言 / CI 守护把“MaxPool ceil_mode1 形状计算正确”做成断言。下面用 pytest 风格守护复用第四节公式import math import pytest def test_ceil_mode_one_supported(policy): assert 1 in policy.supported_ceil_modes def test_ceil_shape_correct(policy): # in7,k3,pad0,stride2,ceil_mode1 - 4 assert policy.out_size(7, 3, 0, 2, ceil_mode1) 4 assert policy.out_size(7, 3, 0, 2, ceil_mode0) 3 def test_web_backends_covered(policy): assert webgpu in policy.web_backends def test_require_all_modes(policy): assert policy.require_all_modes is True这四组断言锁住(1)ceil_mode1受支持(2) ceil/floor 形状数值正确(3) Web 后端已覆盖(4) 要求所有 mode 实现。CI 跑通即代表 Web MaxPool 形状计算无缺口。八、排查清单遇到 Web 上 MaxPool 报 ceil() not supported看 MaxPool 的 ceil_mode 属性是不是 1设 0 是否正常 - 锁定 ceil 分支缺失。对比 CPU/CUDA那两个 EP 正常 - 确认是 Web 后端特有缺口。查 Web 形状计算有没有ceil()分支还是只写了floor()。临时规避导出时设ceil_mode0或 Pad 模拟 ceil 效果。根本修复补ceil_mode1的ceil()分支。统一策略对象用OrtMaxPoolCeilShapePolicy固化。CI 守护断言 ceil_mode1 形状正确、要求所有 mode 实现。九、小结[Web] using ceil() in shape computation is not yet supported for MaxPool的根因是Web 后端WebGPU/WebNN的MaxPool形状计算只实现了ceil_mode0的floor()分支没有ceil_mode1的ceil()分支遇到向上取整的 MaxPool 直接报错CPU/CUDA EP 在静态形状推断里正确处理了两种 mode所以正常。最小修复是在 Web 后端MaxPool形状计算补上ceil_mode1的ceil()分支或导出时降级ceil_mode0结构性改进是用唯一的OrtMaxPoolCeilShapePolicy固化所有 mode 必须实现CI 用四组断言守护“ceil_mode1 支持、形状数值正确、Web 后端覆盖、要求全 mode”。记住算子的形状计算要覆盖所有 mode 属性少一个分支就会在某个 EP 上崩。