Depthwise conv_transpose aborts in MPSGraph (explicit_padding = -1) when output_padding > padding
- 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
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