The same fp16 mlprogram, the same weights, the same input bytes. Correct on iOS 26 and macOS 26. Deterministic garbage on the iOS 17.5 and 18.5 runtimes, on Apple Silicon and on a real A12 phone alike. This is the bisection that found it, the smallest thing that still reproduces it, and the workaround that shipped. Written up because after a week of looking I could not find it described anywhere.
Homeoke pulls the lead vocal out of a song on the phone, using HTDemucs exported to Core ML. On an iPhone 14 Pro the backing comes out clean. On an iPhone XS it came out loud and noisy, described by ear as distortion.
The first suspect was the chip. The XS is an A12 from 2018, the package is fp16, and the upstream conversion script carries a warning that fp16 can overflow in the frequency branch. Three facts pointing the same way, and all three were a coincidence. That guess cost a day, which is the reason this post leads with where it actually went.
The test that settled it was running the identical model in an iOS 17.5 simulator on Apple Silicon, where there is no A12 within a hundred miles. Full song instrumental separations of one file, each compared against the macOS 26 output, which had already been verified by ear on a 14 Pro:
Two runtimes, two chip families, identical wrong numbers. And fp32 is not the way out: on the same runtimes an fp32 export is broken differently and worse. Every pre-26 Core ML runtime mis-executes this program, in both precisions.
Every step measured against both a torch reference and the macOS runtime.
Three chained transformer encoder layers. LayerNorm, multi-head attention and a feed forward block, sequence length 2688, model dimension 512, exported as an fp16 mlprogram, run with compute units set to cpuOnly. Two chained layers are clean. Three are not, deterministically, on any runtime before 26, including a simulator on a Mac. No device and no unusual hardware needed to see it.
The first read of the wrong output was that it is a mis-wiring rather than a miscalculation, because it correlates +0.75 with the negated input of the third layer at collapsed amplitude, which looks like a buffer planner reading the wrong tensor rather than a kernel computing the wrong number.
The refinement took it one step further, and it is not the attention math: a hand written stable attention graph produced byte identical garbage. It is the normalization chains at fp16 when the activations run hot, maximum around 43. Forcing six operations to fp32, layer_norm, reduce_mean, square, sqrt, real_div and sub, lifts the poisoned layer from correlation -0.28 to 0.98 and leaves everything else alone.
All of these produced the identical wrong bytes: coremltools 8.3 against 9.0; fp32, which is worse; compiling the model on the device instead of in Xcode; the experimentalMLE5EngineUsage settings; conv1d and ConvTranspose1d expressed as 2D; a hand written variance instead of the standard deviation operation; softmax in fp32; matmul in fp32; reordering the cross layer pattern; a pad and slice barrier between layers.
The byte stability across that list is the useful part of it. A numeric edge case drifts between builds and between runs. This does not move at all, which is what turned the search from tuning towards structure.
Mixed precision looks like the obvious fix and is a minefield on these runtimes. Adding almost any operation beyond that narrow set to the fp32 list produces NaN, and the same set that is clean on an isolated layer produces NaN at full model scale, because the cast paths are themselves unreliable.
What works is splitting the model into three Core ML programs, so that no single program contains the broken pattern:
Chained end to end on a physical iPhone XS on 18.7, a whole song instrumental comes back at correlation 0.9995 against the clean runtime reference, 0.01 dB off. It costs about 0.1 times real time, because roughly 40 MB of skip tensors have to be handed across the seams for every chunk. On an eight year old phone that is the difference between 1.4 and 1.3 times real time, which is a margin thin enough to matter.
Worth separating out, because from the outside it looks the same and it is not. Some songs failed at around 90 percent, repeatedly, always in the same place.
The cause is a chunk quiet enough that the fp16 normalization standard deviation underflows to exactly zero. The last breath of a fade out, mix rms around 2e-4. That chunk emits Inf and NaN on every attempt on 17.5 and 18.5, while macOS 26 handles the same bytes cleanly. Pure digital silence is safe, because zeros propagate. Audible audio is safe, because the deviation is representable. The kill zone is the narrow band between the two, which is precisely where pop songs fade out, and that is why the reports clustered at "about 90 percent" rather than anywhere else.
The proof of mechanism is the part that makes this certain rather than plausible: the failing chunk multiplied by 32 goes clean, and a healthy chunk divided by 64 goes NaN.
The fix is not clever. Below a measured rms floor, skip the model and pass the mix straight through. A chunk that quiet has no voice in it to separate.
Two tests are worth running before anything else, and both are cheap.
Reproduce it in a simulator. The bug is in the runtime, so an old simulator runtime on a current Mac shows it, and you can stop hunting for an old device. Download the runtime, boot any device on it, and run your model there against the same input you run on the host.
Build a depth ladder. Take the repeating part of your model and export it at one layer, two, three, four, five, comparing each against torch. If there is a cliff, you will find it in an hour, and where the cliff sits tells you far more than the full model's correlation ever will.
On iOS 26 and later none of this happens, so the fix for everybody eventually arrives as an OS update. Until then the split is what makes a 2018 phone produce correct audio, and it ships to every device rather than branching on the OS version, because one path that is tested everywhere beats two paths where only one of them ever gets exercised.
Homeoke separates a song on the phone in seconds and never uploads it. On the App Store, for any iPhone running iOS 17 or later.
Get it on the App Store