SAKSHAM KAPOOR / SELECTED WORKAPPLE COREMLTOOLS · #2752 + #2753 · CASE STUDY

When a working image model cannot cross the conversion bridge.

Two missing translations could stop a model on its way from PyTorch to Apple's tools. I implemented both. Choose an operation, then follow its math, translation, runtime challenge, and evidence one step at a time.

Read the implementation walkthrough and contributor guide ↓

Two contributions, two different calculations:

Chapter 1 of 6 · Deformable convolution

The model runs. The translation stops.

PyTorch model
Missing conversion ruledeform_conv2d
Conversion stops

CoreMLTools needs a rule for deformable convolution. The source operation has no supported translation yet.

Toy values explain the operation. They are not a full model or a device benchmark.

Imagine a developer preparing an image model for an Apple application. The model's calculations already work in PyTorch. But conversion is another task: every source operation needs a representation the target tools understand. One missing rule can stop the process.

Deformable convolution was one of those gaps, tracked in issue #1889. The task was to express its behavior with existing Core ML operations. That meant preserving where the model samples an image, how it interpolates between pixels, and how those samples are combined.

What to take from this stepA conversion failure is not necessarily a broken model. It can be a missing translation between two systems.

1 / 6 · Your pace

Scroll through the story, or choose a chapter.

01 / The roadblock

The model runs. The translation stops.

Imagine a developer preparing an image model for an Apple application. The model's calculations already work in PyTorch. But conversion is another task: every source operation needs a representation the target tools understand. One missing rule can stop the process.

Deformable convolution was one of those gaps, tracked in issue #1889. The task was to express its behavior with existing Core ML operations. That meant preserving where the model samples an image, how it interpolates between pixels, and how those samples are combined.

What to take from this stepA conversion failure is not necessarily a broken model. It can be a missing translation between two systems.

02 / Where to look

Let the model move its sampling points.

Begin with a tiny feature map: an array of values produced while processing an image. Our four neighboring values are 0, 10, 20, and 30. The coordinates tell us where to read; the values tell us what is stored there.

A regular convolution samples a fixed grid. A deformable convolution adds learned offsets, letting those positions move. In the diagram, an offset of half a pixel in each direction moves the point from the top-left value to the middle of its four neighbors. The full operation does this for many positions.

What to take from this stepBefore translating an image operation, make the coordinate system and sampling positions explicit.

03 / Between pixels

A fractional position needs a blended value.

There is no stored pixel exactly at every fractional coordinate. Bilinear interpolation fills that gap by blending the four surrounding values according to distance. A closer neighbor gets more weight. At the exact midpoint, all four weights are one quarter.

That gives our moved sample a value of 15. My translation uses Core ML's resample operation for this part of the calculation. Reusing a supported primitive is useful, but its coordinate and edge behavior must match what the PyTorch operation expects.

What to take from this stepSampling decides which values to read. Interpolation decides how to read a point that lies between stored values.

04 / Combine values

Now apply the mask and learned weights.

Sampling alone does not finish deformable convolution. With a modulation mask, each sampled value is scaled before the learned kernel weights combine the values. In our one-sample illustration, a mask of 0.8 and a weight of 2 turn the sample value 15 into a contribution of 24.

My lowering arranges the sampled channels and uses a grouped 1×1 convolution for the weighted combination, followed by optional bias. The ordering and grouping matter. An ordinary 1×1 convolution on the original image would not replace the offset sampling we just performed.

What to take from this stepSeparate the operation into sampling, optional modulation, and weighted combination before implementing it.

05 / The runtime

Correct math still needs the right coordinate convention.

The mathematical recipe is only part of a converter. Near an image edge, interpolation may need both valid neighbors and zeros outside the image. An explicit zero border makes those neighbors available to the sampling primitive. A graph can be constructed successfully while a native runtime still produces a mismatch if its coordinate convention is wrong.

The public review records a coordinate-mode correction. The merged code explicitly pads the input, shifts sample positions, and normalizes the padded pixel coordinates to the range −1 to 1 with align_corners=True. In execution, those steps happen before resample; we are examining them here as the runtime-debugging part of the story.

What to take from this stepConversion success, mathematical reasoning, and native numerical agreement answer different questions.

06 / The evidence

Test the translation, then put it upstream.

The useful result is a translation that fits the project's supported operations and has evidence for its behavior. Numerical tests compare converted outputs with PyTorch. Configuration coverage explores more than one friendly example, and error-path tests make the support boundary explicit.

PR #2752 merged into Apple's CoreMLTools repository on July 15, 2026. Its review and validation records distinguish graph/conversion checks from native Apple prediction checks. The contribution adds shared support for this operation; any complete model still needs its remaining operations and runtime requirements checked.

What to take from this stepA converter contribution brings together math, target primitives, numerical tests, platform behavior, and maintainer review.

Technical details for this chapter

Missing lowering: torchvision::deform_conv2d. The implementation uses MIL resample plus grouped 1x1 conv.

Both implementations lower Torch operations to Core ML MIL primitives for ML Program conversion. The walkthrough separates the mathematical idea from runtime representation and validation.

Inside the contribution

From the idea to the upstream implementation

These two contributions bridge the math a vision model uses and the operations Apple's tools can execute. Here is the background, the implementation, and a practical way to learn from the code.

1. A working model still needs a translation

Training and deployment use different software systems. A model written in PyTorch describes operations such as convolutions, sampling, and activation functions. Apple's CoreMLTools converter translates a supported model into a representation Core ML can use.

A lowering is one piece of that translation: replace a source operation with target operations that express its behavior. MIL, Core ML's Model Intermediate Language, provides those building blocks. An operator-level gap can stop the entire conversion before an app ever runs the model.

I added two Torch frontend lowerings. Deformable convolution issue #1889 and ROIAlign issue #1793 document the missing support. The ROIAlign report dates to March 2023 and shows a detector running in PyTorch before failing at conversion. This is a concrete integration barrier, even when the model itself is valid.

2. Deformable convolution: reconstructing a missing operation

A normal convolution reads an image or feature map at regularly spaced positions and combines the values with learned weights. Deformable convolution adds learned offsets to those positions. With a modulation mask, the model can also scale the contribution from each sample.

The sample locations can fall between pixels. Bilinear interpolation estimates a value by blending four neighboring pixels according to distance. That sampling behavior has to survive conversion, including what happens at the image boundary.

Sampling between pixels. Put values 0 and 10 on the top row of a 2×2 grid, and 20 and 30 on the bottom row. Exactly halfway between the four pixels, each contributes one quarter: (0 + 10 + 20 + 30) / 4 = 15.

A toy example of interpolation. A real deformable convolution performs many such samples, then applies its weights and optional mask.

My implementation builds that operation from existing MIL primitives:

  1. Construct the regular sampling grid from kernel position, stride, padding, and dilation. Add the learned offsets.
  2. Add an explicit zero border and adjust coordinates so interpolation at the boundary matches the intended sampling behavior.
  3. Normalize the coordinates and use resample for bilinear sampling. Apply the modulation mask when present.
  4. Arrange the sampled values and use a grouped 1×1 convolution to perform the weighted combination. Add the bias when present.

The final 1×1 convolution is doing the weighted combination after sampling; it is not a claim that an ordinary 1×1 convolution alone replaces deformable convolution. Grouping and the order of sampled channels matter to the result.

3. ROIAlign: sampling a selected region consistently

An object detector often needs to extract information from a proposed region of a feature map. ROIAlign divides that region into output bins, samples within them, and averages the samples. Interpolation lets it work with fractional positions instead of rounding every coordinate to a pixel.

My fixed-sampling lowering selects the correct batch image for each box, applies the spatial scale and alignment offset, and constructs sample positions for each bin. It uses resample to obtain the values, accumulates them, and divides by the number of samples.

For each output bin:
  place a fixed grid of sample points
  interpolate the value at each point
  output the average of those values

For example, four sample values of 5, 10, 20, and 25 produce an output-bin value of 15. Unlike deformable convolution's learned weighted combination, this step is an average.

The implementation supports tensor-form boxes with a batch index and four coordinates, multiple regions, and both alignment settings. It requires a positive constant sampling ratio. Adaptive sampling is rejected explicitly, so an unsupported setting cannot silently become a different calculation.

4. The difficult part: agreement across representations and runtimes

A graph can convert successfully yet produce the wrong values on the target runtime. In the deformable-convolution review, initial native prediction failures led to a coordinate-mode correction. The merged implementation uses normalized coordinates in the range −1 to 1 with align_corners=True.

The ROIAlign discussion records the same coordinate-mode adjustment and precision-specific test tolerance. It also shows test revisions requested by the maintainer. A useful contributor lesson is to interpret feedback against the actual failing behavior and the project's testing conventions.

Different checks answer different questions
CheckWhat it establishes
Conversion / graph constructionCan the frontend express the operation using supported MIL operations?
Numerical comparisonDo supported inputs produce results close to the PyTorch reference at the tested precision?
Native Apple executionDoes the converted representation behave correctly on the actual runtime? Linux conversion alone cannot establish this.
Rejection testsDoes an unsupported setting, such as adaptive ROIAlign sampling, fail clearly?

The merged TestDeformConv2d tests vary masks, bias, grouping, kernel shapes, stride, padding, and dilation. The TestRoiAlign tests cover multiple regions and batch indices, scale and alignment choices, and the adaptive-sampling error path. Frontend and precision coverage help reveal differences a single example would miss.

5. How to approach a first converter contribution

  1. Reduce the conversion failure. Start with one unsupported operation and a tiny input. A full detector introduces too many unrelated places to fail.
  2. Write down the mathematical contract. Track input shapes, coordinate order, padding, alignment, and aggregation. Decide which options the first implementation will support.
  3. Find target primitives. Read the registered lowering in ops.py. Follow how inputs become sample coordinates, then trace those samples to the output.
  4. Build a test matrix around boundaries. Try a non-square input, a fractional coordinate, a boundary sample, a different batch index, and one deliberately unsupported setting.
  5. Separate conversion from runtime validation. Follow the CoreMLTools contributing guide and identify which checks your environment can actually perform. Report each result with its platform and precision.

Try it yourself: calculate the midpoint sample in the 2×2 example. Then move the sample onto a corner, and finally just outside the image. Before reading the padding code, write down which neighboring pixels should contribute in each case.

This turns an abstract conversion task into a small correctness problem. Then compare your reasoning with the explicit border and coordinate normalization in the merged implementation.

6. What this contribution demonstrates

I implemented two mathematical operations across a framework boundary, using existing Core ML primitives and tests against PyTorch. The work required sampling geometry, tensor layout, grouping, numerical precision, and platform-aware debugging.

PR #2752 merged July 15, 2026, and PR #2753 merged July 20, 2026. Those merges resolve two reported conversion gaps and put the implementations in Apple's upstream tooling, where other developers can build on them.

Press ⌘+K to search