apple / apple/coreai-torch

Depthwise conv_transpose aborts in MPSGraph (explicit_padding = -1) when output_padding > padding

Open
#58 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
152
Forks
45
Avg merge
1d 7m
Merged PRs (30d)
12

Description

## Summary

`ConvTranspose1d` and `ConvTranspose2d` abort the process at execute time when the layer has one channel per group and `output_padding` is greater than `padding`. MPSGraph asserts during `MPSRuntimeCanonicalization` with

```
'mps.depthwise_conv_3d_data_gradient' op Invalid `explicit_padding` value -1, all values should be non-negative
MPSGraphExecutable.mm:6377: failed assertion `Error: MLIR pass manager failed'
```

That is a native assertion rather than a Python exception, so it cannot be caught and the process exits with 134.

## Environment

- macOS 27.0 (26A5388g), arm64, Apple M5 Pro
- coreai-torch at main `8ab36bb` (the in-repo `__version__` still reads 0.4.1)
- coreai-core 1.0.0b2, torch 2.13.0, numpy 2.4.6, Python 3.11.15

## Minimal repro

```python
import asyncio, tempfile
from pathlib import Path
import torch, torch.nn as nn
import coreai.runtime as rt
from coreai_torch import TorchConverter

class M(nn.Module):
def __init__(self, groups, padding, output_padding):
super().__init__()
self.c = nn.ConvTranspose1d(4, 4, 3, stride=2, padding=padding,
output_padding=output_padding, groups=groups)
def forward(self, x):
return self.c(x)

async def run(groups, padding, output_padding, cpu_only=False):
torch.manual_seed(0)
m = M(groups, padding, output_padding).eval()
x = torch.randn(1, 4, 8)
ep = torch.export.export(m, args=(), kwargs={"x": x})
ep = ep.run_decompositions(torch.export.default_decompositions())
conv = TorchConverter()
conv.add_exported_program(ep)
prog = conv.to_coreai()
with tempfile.TemporaryDirectory() as td:
asset = prog.save_asset(Path(td) / "m.aimodel")
opts = rt.SpecializationOptions.cpu_only() if cpu_only else None
async with asset.executable(specialization_options=opts) as model:
fn = model.load_function("main")
out = await fn({fn.desc.input_names[0]: rt.NDArray(x.numpy())})
return list(out.values())[0].numpy().shape

for groups, padding, op, cpu in [
(1, 0, 1, False), # not depthwise
(4, 1, 1, False), # depthwise, output_padding == padding
(4, 0, 1, True), # depthwise, output_padding > padding, cpu_only
(4, 0, 1, False), # depthwise, output_padding > padding, default
]:
print(f"groups={groups} padding={padding} output_padding={op} cpu_only={cpu}", flush=True)
print(f" -> {asyncio.run(run(groups, padding, op, cpu))}", flush=True)
```

## Expected vs actual

The first three lines are controls and print a shape. The fourth ends the process.

```
groups=1 padding=0 output_padding=1 cpu_only=False
-> (1, 4, 18)
groups=4 padding=1 output_padding=1 cpu_only=False
-> (1, 4, 16)
groups=4 padding=0 output_padding=1 cpu_only=True
-> (1, 4, 18)
groups=4 padding=0 output_padding=1 cpu_only=False
-> assertion, exit 134
```

## Scope

I ran 332 conv_transpose configurations against torch eager on main, covering 1D and 2D, stride 1 to 4, padding 0 to 2, dilation 1 and 2, arguments that differ between axes, dynamic shapes, and a squeeze after the conv. 320 match torch. The 12 that abort share one shape.

- The trigger follows channels per group rather than `groups` itself. With 8 channels, `groups` of 1, 2 and 4 all pass and `groups=8` aborts. The same holds at 4/4, 6/6 and 12/12.
- The boundary is `output_padding > padding`, with no exceptions across 36 depthwise configurations. 20 pass at `output_padding <= padding` and 16 abort above it. `stride=3, padding=1, output_padding=1` passes while `output_padding=2` aborts.
- `cpu_only` returns the correct shape and values for every configuration that aborts on the default delegate.
- A non-transposed depthwise `Conv1d` of the same shape is fine.

## Notes

`output_padding > padding` is the case whose pre-padding branch #40 removed. On the commit before it (`ef1181b`) these same configurations return an over-long tensor instead of aborting, so depthwise moved from a wrong result to a crash rather than being fixed.

`tests/ops/test_ops.py::test_conv_transpose` does have a `groups` axis, but its only grouped entry is 4 channels at `groups=2`, which is 2 channels per group with `output_padding=0`, so this corner has no coverage.

I have not established whether the wrong `explicit_padding` originates in the converter or below it. The converter passes `padding` and `output_pad` straight through, and the CPU path handles the same pair correctly.

Contributor guide

Open the contributing guide

Research direction

Start with the minimal reproduction and compare the default delegate with the cpu_only control for depthwise ConvTranspose1d and ConvTranspose2d cases where output_padding exceeds padding. Inspect the converter path that passes padding and output_pad, then review tests/ops/test_ops.py::test_conv_transpose. Done means the triggering configurations no longer abort and match torch eager results, with regression coverage for this boundary.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
backend, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.