Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Use separable SPPF pooling in PyTorch, preserving square pooling for export - #26082

Open
glenn-jocher wants to merge 1 commit into
mainfrom
sppf-separable-pooling
Open

Use separable SPPF pooling in PyTorch, preserving square pooling for export#26082
glenn-jocher wants to merge 1 commit into
mainfrom
sppf-separable-pooling

Conversation

@glenn-jocher

@glenn-jocher glenn-jocher commented Sep 7, 2026

Copy link
Copy Markdown
Member

Replace SPPF's 5×5 max pooling with sequential 1×5 and 5×1 pooling during PyTorch training, validation, and prediction. Width-first pooling preserves gradient tie-breaking. Exports retain the original square pooling.

Performance

Faster on CPU; slower for the SPPF block on CUDA, with little change to full-model CUDA inference.

Workload CPU latency CUDA latency
SPPF inference 22–45% lower 8–15% higher
SPPF forward + backward 13–28% lower 11–19% higher
YOLO26n / YOLO26s inference 0.2–4.6% lower Approximately unchanged (−1.24% to +0.17%)

Ranges cover the measured configurations on ultra5: AMD EPYC 9655 (FP32, 1/8 threads) and RTX PRO 6000 Blackwell (CUDA 7, FP32/FP16), using PyTorch 2.7.0 / CUDA 12.8. Full-model inference uses 640×640 inputs and batches 1/8, excluding preprocessing and postprocessing. Results are medians of three trials on a shared server. Full-model training throughput was not measured.

Validation

  • 63 output/gradient comparisons and all 5 existing model-forward tests passed.
  • YOLO26n/YOLO11n real-image prediction and one epoch of CUDA training/validation on COCO8 passed.
  • ONNX and TorchScript runtime comparisons passed; both exports retain exactly three square pooling operations.
Full benchmark data and reproduction scripts

Setup

  • Host: ultra5, dual AMD EPYC 9655 96-Core processors; CPU FP32 with 1 and 8 intra-op threads.
  • GPU: NVIDIA RTX PRO 6000 Blackwell Server Edition, physical CUDA 7, selected after CUDA 0 became 99–100% utilized while CUDA 7 reported 0%. CUDA 7 had an existing ~43 GB allocation; other GPUs were running workloads. These are shared-server measurements, not an exclusive-host guarantee.
  • Python 3.10.12, PyTorch 2.7.0, CUDA 12.8; GPU FP32 and FP16. Contiguous NCHW, eager PyTorch, no compilation. GPU tests use 8 CPU threads.
  • Compare the original SPPF forward from base b5f6c7024 against this implementation, using identical weights/inputs. Pool-only rows compare three sequential square pools against three pairs of separable pools. Complete SPPF rows use SPPF(256,256,k=5,n=3) (128 hidden pooling channels); pooling-only rows have 256 channels.
  • Full-model rows use fused, pretrained YOLO26n/YOLO26s at 640×640, batches 1/8; timings exclude preprocessing, data transfers, and postprocessing. Full-model timing measures inference only. Training rows measure complete SPPF or pooling forward + sum reduction + backward, including gradient clearing, without an optimizer step.
  • 10 warmup calls per variant, followed by three alternating-order torch.utils.benchmark.Timer.blocked_autorange(min_run_time=0.3) trials per variant. Timer synchronizes accelerator work. Report the median of the three trial medians. All timings are milliseconds per batch; speedup = old/new (>1 is faster). Include every trial median below to expose variation.

Full results

CPU float32, 1 CPU thread(s)

Scope Input BCHW Mode Square ms Separable ms Speedup Square trial medians ms Separable trial medians ms
3 pooling iterations 1×256×20×20 inference 11.4308 5.5673 2.053× 11.4317 / 11.4187 / 11.4308 5.5673 / 5.5651 / 5.5730
SPPF 1×256×20×20 inference 8.0908 5.1338 1.576× 8.0908 / 8.0897 / 8.0923 5.1319 / 5.1340 / 5.1338
3 pooling iterations 1×256×20×20 forward+backward 11.7851 6.0842 1.937× 11.7851 / 11.7823 / 11.8147 6.1745 / 6.0788 / 6.0842
SPPF 1×256×20×20 forward+backward 13.1918 10.2719 1.284× 13.1918 / 13.1934 / 13.1724 10.2719 / 10.2683 / 10.2726
3 pooling iterations 8×256×20×20 inference 91.4493 45.1974 2.023× 91.6849 / 91.4493 / 91.2699 45.1974 / 46.2535 / 45.0808
SPPF 8×256×20×20 inference 64.4826 41.0195 1.572× 64.4453 / 64.7577 / 64.4826 41.0195 / 41.0891 / 40.9757
3 pooling iterations 8×256×20×20 forward+backward 93.9537 48.2436 1.947× 94.7515 / 93.9537 / 93.4095 48.2024 / 48.2436 / 48.6485
SPPF 8×256×20×20 forward+backward 104.5763 83.9606 1.246× 104.7687 / 104.5763 / 104.4848 82.4869 / 83.9606 / 84.0987
3 pooling iterations 1×256×40×40 inference 47.5920 22.3446 2.130× 47.5920 / 47.5705 / 47.5947 22.3430 / 22.3446 / 22.4661
SPPF 1×256×40×40 inference 33.1378 20.4291 1.622× 33.1381 / 33.1196 / 33.1378 20.4291 / 20.4152 / 20.4630
3 pooling iterations 1×256×40×40 forward+backward 48.6025 24.0478 2.021× 48.6610 / 48.6025 / 48.5586 24.0478 / 24.0468 / 24.0506
SPPF 1×256×40×40 forward+backward 53.2125 41.0157 1.297× 53.4645 / 53.2122 / 53.2125 41.1350 / 40.9623 / 41.0157
yolo26n 1×3×640×640 inference 83.5561 79.6753 1.049× 82.6036 / 83.5561 / 84.9507 80.9145 / 79.6164 / 79.6753
yolo26n 8×3×640×640 inference 707.6871 685.2524 1.033× 699.9923 / 707.6871 / 713.4964 685.2622 / 682.0066 / 685.2524
yolo26s 1×3×640×640 inference 255.4818 247.4461 1.032× 255.4818 / 254.3145 / 257.9481 248.2486 / 245.9117 / 247.4461
yolo26s 8×3×640×640 inference 2273.7567 2214.8452 1.027× 2283.8146 / 2273.7567 / 2252.0174 2214.8452 / 2206.2944 / 2215.8326

CPU float32, 8 CPU thread(s)

Scope Input BCHW Mode Square ms Separable ms Speedup Square trial medians ms Separable trial medians ms
3 pooling iterations 1×256×20×20 inference 1.4821 0.7066 2.098× 1.4821 / 1.5094 / 1.4802 0.7029 / 0.7082 / 0.7066
SPPF 1×256×20×20 inference 1.1032 0.8621 1.280× 1.1181 / 1.0999 / 1.1032 0.8570 / 0.8763 / 0.8621
3 pooling iterations 1×256×20×20 forward+backward 1.6094 0.9397 1.713× 1.6119 / 1.5949 / 1.6094 0.9485 / 0.9367 / 0.9397
SPPF 1×256×20×20 forward+backward 1.9476 1.6987 1.147× 1.9476 / 1.9452 / 1.9754 1.6987 / 1.6755 / 1.7584
3 pooling iterations 8×256×20×20 inference 11.6967 5.5909 2.092× 11.6967 / 11.8365 / 11.6954 5.5909 / 5.5808 / 5.6181
SPPF 8×256×20×20 inference 6.7470 3.6775 1.835× 6.7783 / 6.7470 / 6.7363 3.6753 / 4.0173 / 3.6775
3 pooling iterations 8×256×20×20 forward+backward 12.0446 6.0805 1.981× 12.0618 / 12.0446 / 12.0352 6.0805 / 6.0653 / 6.3355
SPPF 8×256×20×20 forward+backward 9.3903 6.7623 1.389× 9.3841 / 9.4915 / 9.3903 6.7321 / 6.7740 / 6.7623
3 pooling iterations 1×256×40×40 inference 6.1162 2.8382 2.155× 6.1511 / 6.1155 / 6.1162 2.8379 / 2.8382 / 2.8425
SPPF 1×256×40×40 inference 3.8172 2.4867 1.535× 3.8167 / 3.8210 / 3.8172 2.4910 / 2.4775 / 2.4867
3 pooling iterations 1×256×40×40 forward+backward 6.3412 3.4410 1.843× 6.3519 / 6.3412 / 6.3312 3.3671 / 3.4874 / 3.4410
SPPF 1×256×40×40 forward+backward 5.3798 4.1814 1.287× 5.4081 / 5.3798 / 5.3785 4.1814 / 4.2304 / 4.0611
yolo26n 1×3×640×640 inference 20.4201 20.1547 1.013× 20.4201 / 20.4711 / 20.3928 20.1547 / 20.0209 / 21.0529
yolo26n 8×3×640×640 inference 135.4630 132.7973 1.020× 138.5505 / 135.4630 / 134.6678 132.7973 / 135.0963 / 108.1472
yolo26s 1×3×640×640 inference 37.7059 37.6345 1.002× 39.7874 / 37.7059 / 37.2793 39.0975 / 37.5970 / 37.6345
yolo26s 8×3×640×640 inference 362.6495 358.1403 1.013× 367.7901 / 361.2911 / 362.6495 360.6236 / 358.1403 / 357.6469

CUDA float32, 8 CPU thread(s)

Scope Input BCHW Mode Square ms Separable ms Speedup Square trial medians ms Separable trial medians ms
3 pooling iterations 1×256×20×20 inference 0.0184 0.0277 0.666× 0.0184 / 0.0184 / 0.0184 0.0277 / 0.0277 / 0.0277
SPPF 1×256×20×20 inference 0.0701 0.0806 0.869× 0.0699 / 0.0701 / 0.0701 0.0805 / 0.0810 / 0.0806
3 pooling iterations 1×256×20×20 forward+backward 0.1054 0.1644 0.642× 0.1353 / 0.1047 / 0.1054 0.1642 / 0.1644 / 0.1651
SPPF 1×256×20×20 forward+backward 0.3336 0.3819 0.874× 0.3329 / 0.3337 / 0.3336 0.3819 / 0.3832 / 0.3817
3 pooling iterations 8×256×20×20 inference 0.0488 0.0532 0.917× 0.0487 / 0.0488 / 0.0488 0.0532 / 0.0531 / 0.0534
SPPF 8×256×20×20 inference 0.0737 0.0800 0.922× 0.0737 / 0.0737 / 0.0737 0.0799 / 0.0800 / 0.0800
3 pooling iterations 8×256×20×20 forward+backward 0.1338 0.2112 0.633× 0.1338 / 0.1314 / 0.1372 0.2112 / 0.2147 / 0.2101
SPPF 8×256×20×20 forward+backward 0.4684 0.5220 0.897× 0.4684 / 0.4694 / 0.4525 0.5220 / 0.5219 / 0.5348
3 pooling iterations 1×256×40×40 inference 0.0307 0.0317 0.969× 0.0307 / 0.0307 / 0.0307 0.0317 / 0.0317 / 0.0317
SPPF 1×256×40×40 inference 0.0701 0.0806 0.869× 0.0698 / 0.0701 / 0.0701 0.0806 / 0.0808 / 0.0805
3 pooling iterations 1×256×40×40 forward+backward 0.1344 0.2151 0.625× 0.1518 / 0.1344 / 0.1316 0.2152 / 0.2072 / 0.2151
SPPF 1×256×40×40 forward+backward 0.4489 0.5361 0.837× 0.4489 / 0.4461 / 0.4619 0.5375 / 0.5361 / 0.5338
yolo26n 1×3×640×640 inference 2.3754 2.3793 0.998× 2.3754 / 2.3845 / 2.3696 2.3793 / 2.3689 / 2.3800
yolo26n 8×3×640×640 inference 3.5324 3.5370 0.999× 3.5655 / 3.5288 / 3.5324 3.5337 / 3.5383 / 3.5370
yolo26s 1×3×640×640 inference 2.4752 2.4768 0.999× 2.5080 / 2.4752 / 2.4494 2.4768 / 2.4493 / 2.6356
yolo26s 8×3×640×640 inference 5.9888 5.9143 1.013× 5.9131 / 6.2088 / 5.9888 5.9149 / 5.9143 / 5.9108

CUDA float16, 8 CPU thread(s)

Scope Input BCHW Mode Square ms Separable ms Speedup Square trial medians ms Separable trial medians ms
3 pooling iterations 1×256×20×20 inference 0.0188 0.0289 0.649× 0.0186 / 0.0188 / 0.0188 0.0289 / 0.0308 / 0.0288
SPPF 1×256×20×20 inference 0.0791 0.0894 0.884× 0.0795 / 0.0790 / 0.0791 0.0894 / 0.0895 / 0.0892
3 pooling iterations 1×256×20×20 forward+backward 0.1320 0.2081 0.634× 0.1436 / 0.1296 / 0.1320 0.2082 / 0.2081 / 0.2046
SPPF 1×256×20×20 forward+backward 0.3332 0.3843 0.867× 0.3349 / 0.3332 / 0.3321 0.3844 / 0.3843 / 0.3831
3 pooling iterations 8×256×20×20 inference 0.0492 0.0553 0.889× 0.0492 / 0.0492 / 0.0492 0.0553 / 0.0553 / 0.0554
SPPF 8×256×20×20 inference 0.0835 0.0940 0.888× 0.0835 / 0.0835 / 0.0840 0.0938 / 0.0940 / 0.0941
3 pooling iterations 8×256×20×20 forward+backward 0.1549 0.2110 0.734× 0.1562 / 0.1347 / 0.1549 0.2105 / 0.2110 / 0.2762
SPPF 8×256×20×20 forward+backward 0.7411 0.8334 0.889× 0.7413 / 0.7395 / 0.7411 0.8345 / 0.8334 / 0.8235
3 pooling iterations 1×256×40×40 inference 0.0311 0.0369 0.842× 0.0310 / 0.0344 / 0.0311 0.0369 / 0.0369 / 0.0372
SPPF 1×256×40×40 inference 0.0785 0.0889 0.884× 0.0784 / 0.0785 / 0.0789 0.0889 / 0.0889 / 0.0888
3 pooling iterations 1×256×40×40 forward+backward 0.0838 0.1143 0.733× 0.0977 / 0.0838 / 0.0837 0.1081 / 0.1165 / 0.1143
SPPF 1×256×40×40 forward+backward 0.2602 0.2969 0.877× 0.2629 / 0.2602 / 0.2599 0.2969 / 0.2962 / 0.2972
yolo26n 1×3×640×640 inference 2.4551 2.4562 1.000× 2.4577 / 2.4551 / 2.4479 2.4546 / 2.5026 / 2.4562
yolo26n 8×3×640×640 inference 2.9250 2.8985 1.009× 2.9086 / 2.9250 / 2.9301 2.8985 / 2.8985 / 2.9182
yolo26s 1×3×640×640 inference 2.5169 2.5173 1.000× 2.5187 / 2.5148 / 2.5169 2.5110 / 2.5250 / 2.5173
yolo26s 8×3×640×640 inference 4.3316 4.3365 0.999× 4.3328 / 4.3307 / 4.3316 4.3365 / 4.3354 / 4.3375

Reproduce

Run from this PR checkout in an environment with its dependencies, using the selected physical GPU:

CUDA_VISIBLE_DEVICES=7 OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=8 python benchmark_sppf.py
Complete benchmark script
import copy
import json
import platform
import statistics
import types
import torch
from torch.utils.benchmark import Timer
from ultralytics import YOLO
from ultralytics.nn.modules import SPPF

torch.manual_seed(0)
torch.set_num_threads(8)
print(json.dumps({'torch': torch.__version__, 'cuda': torch.version.cuda, 'python':platform.python_version(), 'gpu':torch.cuda.get_device_name(0)}),flush=True)
results=[]
def original_forward(self,x):
    y=[self.cv1(x)]
    y.extend(self.m(y[-1]) for _ in range(getattr(self,"n",3)))
    y=self.cv2(torch.cat(y,1))
    return y+x if getattr(self,"add",False) else y

def bench(label, shape, dtype, device, threads, old, new, training=False):
    def run(m):
        if training:
            m.zero_grad(set_to_none=True)
            m(x).sum().backward()
            x.grad=None
        else:
            with torch.inference_mode():
                m(x)
    x=torch.randn(shape,device=device,dtype=dtype,requires_grad=training)
    funcs=[lambda:run(old),lambda:run(new)]
    for f in funcs:
        for _ in range(10): f()
    samples=[[],[]]
    for rep in range(3):
        for i in ([0,1] if rep%2==0 else [1,0]):
            samples[i].append(Timer(stmt='f()',globals={'f':funcs[i]},num_threads=threads).blocked_autorange(min_run_time=0.3).median*1000)
    a,b=map(statistics.median,samples)
    row=dict(label=label,shape=list(shape),dtype=str(dtype).split('.')[-1],device=device,threads=threads,mode='forward+backward' if training else 'inference',old_ms=a,new_ms=b,speedup=a/b,samples_ms=samples)
    results.append(row)
    print(json.dumps(row),flush=True)
    with open('/tmp/sppf-results.json','w') as f:json.dump(results,f,indent=2)

class Pool(torch.nn.Module):
    def __init__(self,separable):
        super().__init__();self.separable=separable
    def forward(self,x):
        for _ in range(3):
            x=(torch.nn.functional.max_pool2d(torch.nn.functional.max_pool2d(x,(1,5),1,(0,2)),(5,1),1,(2,0)) if self.separable else torch.nn.functional.max_pool2d(x,5,1,2))
        return x

for device,dtype,threads in [('cpu',torch.float32,1),('cpu',torch.float32,8),('cuda',torch.float32,8),('cuda',torch.float16,8)]:
    for shape in [(1,256,20,20),(8,256,20,20),(1,256,40,40)]:
        for training in [False,True]:
            old=Pool(False).to(device);new=Pool(True).to(device)
            bench('3 pooling iterations',shape,dtype,device,threads,old,new,training)
            old=SPPF(shape[1],shape[1]).to(device=device,dtype=dtype);old.export=True
            new=copy.deepcopy(old);new.export=False
            old.forward=types.MethodType(original_forward,old)
            old.train(training);new.train(training)
            bench('SPPF',shape,dtype,device,threads,old,new,training)
    for name in ['yolo26n','yolo26s']:
        old=YOLO(name+'.pt').model.fuse(verbose=False).eval().to(device=device,dtype=dtype)
        for m in old.modules():
            if isinstance(m,SPPF):m.export=True
        new=copy.deepcopy(old)
        for m in old.modules():
            if isinstance(m,SPPF):m.forward=types.MethodType(original_forward,m)
        for m in new.modules():
            if isinstance(m,SPPF):m.export=False
        for batch in [1,8]:
            bench(name,(batch,3,640,640),dtype,device,threads,old,new)
        del old,new
Focused validation script
import copy
import json
import torch
import onnx
import onnxruntime as ort
import numpy as np
from ultralytics import YOLO
from ultralytics.nn.modules import SPPF

torch.set_num_threads(8)
torch.manual_seed(42)
count=0
for device,dtype in [('cpu',torch.float32),('cuda',torch.float32),('cuda',torch.float16)]:
    for k in [3,5,7]:
        for shape in [(2,8,1,1),(2,8,3,7),(2,8,20,20)]:
            for tied in [False,True]:
                x=(torch.randint(-2,3,shape,device=device).to(dtype) if tied else torch.randn(shape,device=device,dtype=dtype)).requires_grad_()
                z=x.detach().clone().requires_grad_()
                a=torch.nn.functional.max_pool2d(x,k,1,k//2)
                b=torch.nn.functional.max_pool2d(torch.nn.functional.max_pool2d(z,(1,k),1,(0,k//2)),(k,1),1,(k//2,0))
                torch.testing.assert_close(a,b,rtol=0,atol=0)
                grad=torch.ones_like(a)
                a.backward(grad);b.backward(grad)
                torch.testing.assert_close(x.grad,z.grad,rtol=0,atol=0)
                count+=1
    for k,n,shortcut in [(3,1,False),(5,3,False),(7,4,True)]:
        a=SPPF(16,16,k,n,shortcut).to(device=device,dtype=dtype).train();a.export=True
        b=copy.deepcopy(a);b.export=False
        x=torch.randn(2,16,11,13,device=device,dtype=dtype,requires_grad=True)
        z=x.detach().clone().requires_grad_()
        out=a(x);other=b(z)
        torch.testing.assert_close(out,other,rtol=0,atol=0)
        out.float().square().mean().backward();other.float().square().mean().backward()
        tolerance=dict(rtol=0.02,atol=0.0001) if dtype==torch.float16 else dict(rtol=0.0001,atol=1e-7)
        torch.testing.assert_close(x.grad,z.grad,**tolerance)
        for p,q in zip(a.parameters(),b.parameters()):torch.testing.assert_close(p.grad,q.grad,**tolerance)
        count+=1
print('POOL_AND_SPPF_PARITY_PASS',count,flush=True)
for name in ['yolo26n.pt','yolo11n.pt']:
    model=YOLO(name)
    original=copy.deepcopy(model.model).eval()
    for m in original.modules():
        if isinstance(m,SPPF):m.export=True
    model.model.eval()
    x=torch.randn(1,3,160,160)
    with torch.inference_mode():
        torch.testing.assert_close(original(x),model.model(x),rtol=0,atol=0)
    model.predict('ultralytics/assets/bus.jpg',imgsz=160,device=0,verbose=False,save=False)
    print('CHECKPOINT_AND_REAL_IMAGE_PASS',name,flush=True)
model=YOLO('yolo26n.pt')
p=model.export(format='onnx',imgsz=160,device='cpu',simplify=False,opset=17)
graph=onnx.load(p)
pools=[node for node in graph.graph.node if node.op_type=='MaxPool']
kernels=[[list(a.ints) for a in node.attribute if a.name=='kernel_shape'][0] for node in pools]
assert kernels==[[5,5]]*3,kernels
assert all(not m.export for m in model.model.modules() if isinstance(m,SPPF))
session=ort.InferenceSession(p,providers=['CPUExecutionProvider'],sess_options=(lambda o:(setattr(o,'intra_op_num_threads',8),o)[1])(ort.SessionOptions()))
x=np.random.default_rng(42).normal(size=(1,3,160,160)).astype(np.float32)
output=session.run(None,{'images':x})[0]
reference=copy.deepcopy(model.model).cpu().eval().fuse(verbose=False)
from ultralytics.nn.modules import Detect
for m in reference.modules():
    if isinstance(m,Detect):m.export=True;m.format='onnx';m.max_det=300
with torch.inference_mode():expected=reference(torch.from_numpy(x)).numpy()
np.testing.assert_allclose(output,expected,rtol=1e-3,atol=1e-3)
print('ONNX_3_SQUARE_POOLS_AND_RUNTIME_PARITY_PASS',flush=True)
p=model.export(format='torchscript',imgsz=160,device='cpu')
script=torch.jit.load(p)
graph=str(script.inlined_graph)
assert graph.count('aten::max_pool2d(')==3
with torch.inference_mode():
    np.testing.assert_allclose(script(torch.from_numpy(x)).numpy(),expected,rtol=1e-3,atol=1e-3)
print('TORCHSCRIPT_3_POOLS_AND_RUNTIME_PARITY_PASS',flush=True)

@UltralyticsAssistant UltralyticsAssistant added enhancement New feature or request exports Model exports (ONNX, TensorRT, TFLite, etc.) python Pull requests that update python code labels Sep 7, 2026
@UltralyticsAssistant

Copy link
Copy Markdown
Member

👋 Hello @glenn-jocher, thank you for submitting a ultralytics/ultralytics 🚀 PR! This automated message confirms your contribution was received, and an Ultralytics engineer will assist with the review. To ensure a seamless integration of your work, please review the following checklist:

  • Define a Purpose: Clearly explain the purpose of your fix or feature in your PR description, and link to any relevant issues. Ensure your commit messages are clear, concise, and adhere to the project's conventions.
  • Synchronize with Source: Confirm your PR is synchronized with the ultralytics/ultralytics main branch. If it's behind, update it by clicking the 'Update branch' button or by running git pull and git merge main locally.
  • Ensure CI Checks Pass: Verify all Ultralytics Continuous Integration (CI) checks are passing. If any checks fail, please address the issues.
  • Update Documentation: Update the relevant documentation for any new or modified features.
  • Add Tests: If applicable, include or update tests to cover your changes, and confirm that all tests are passing.
  • Sign the CLA: Please ensure you have signed our Contributor License Agreement if this is your first Ultralytics PR by writing "I have read the CLA Document and I sign the CLA" in a new message.
  • Minimize Changes: Limit your changes to the minimum necessary for your bug fix or feature addition. "It is not daily increase but daily decrease, hack away the unessential. The closer to the source, the less wastage there is." — Bruce Lee

For more guidance, please refer to our Contributing Guide. Don't hesitate to leave a comment if you have any questions. Thank you for contributing to Ultralytics! 🚀

@UltralyticsAssistant UltralyticsAssistant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 PR Review

Made with ❤️ by Ultralytics Actions

LGTM. Reviewed the complete 14-line change in ultralytics/engine/exporter.py and ultralytics/nn/modules/block.py; the separable eager-PyTorch path and export-only square-pooling override are consistent with existing model and exporter behavior. No concrete correctness, compatibility, security, or performance issue was found.

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request exports Model exports (ONNX, TensorRT, TFLite, etc.) python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants