My MacBook can't divide by 20: an Apple Metal compiler bug

0 / 20 = 25

That's what a Metal shader on my MacBook returned. Some tinkering showed I had stumbled upon a bug in Apple's Metal compiler.

For uint inputs, Metal's built-in mulhi(a, b) returns the high 32 bits of the 64-bit product. Two 32-bit numbers can multiply into a 64-bit result, and whatever doesn't fit in the low 32 bits spills into the high 32.

Example: 25,769,804 × 500 = 12,884,902,000 = 3 × 2³² + 112, so the high half holds 3, the low half 112, and mulhi returns the 3. In the cases I tested, dividing mulhi's result by a non-power-of-two constant produced incorrect results: minimal_repro.swift.

Feed x = 1 to mulhi(x, 500u) / 20u: the product is 500, nowhere near 2³², so mulhi(1, 500) is 0, and zero divided by 20 is zero.

The compiled kernel wrote 25.

25 is not random though: it is 500 / 20. The compiled code had divided the low 32 bits of the product (500) instead of the high 32 bits (0). Every input in the linked reproducer matched that pattern. In separate tests, every non-power-of-two divisor I tried (3, 5, 10, 100, and 499) misbehaved the same way, while the power-of-two divisors I tried (2, 4, 8, 16, and 256) were all correct.

Across the cases I tested, I reproduced the bug only when all three conditions held:

My Mac was on macOS 26.5.1 when I first found the bug. I also reproduced it on 15.3.2, released more than a year earlier, and confirmed that 26.6.1 no longer has it. I disassembled both binaries with applegpu. Here is mulhi(x, 500u) / 20u on each:

broken (macOS 15.3.2):
e204 f401               mov_imm  r1l, 500
9e01 c022 0c00 0000     imadd    r0, r0, r1l        ← computes low32(x × 500)
e205 cdcc cccc          mov_imm  r1, 3435973837     ← multiplier used in the compiler's constant-division sequence for / 20
9e03 c022 2c00 0000     imadd    r0_r1, r0, r1      ← / 20 sequence consumes that low half
2e81 0020 2c04 0000     bfeil    r0, 0, r1, 4

fixed (macOS 26.6.1):
e204 f401               mov_imm  r1l, 500
9e03 c022 0c00 0000     imadd    r0_r1, r0, r1l     ← computes both halves of x × 500: low in r0, high in r1
e201 cdcc cccc          mov_imm  r0, 3435973837     ← same constant
9e03 c202 2c00 0000     imadd    r0_r1, r1, r0      ← / 20 sequence consumes the high half
2e81 0020 2c04 0000     bfeil    r0, 0, r1, 4       ← byte-identical

Three instructions differ:

The broken binary uses r0, containing the low half, while the fixed binary uses r1, containing the high half. The surrounding instruction and register-allocation differences follow from that choice. In the broken sequence, the high half of x × 500 is never materialized at all.

I filed FB24193979 with Apple on August 6, 2026. Apple released macOS 26.6.1 that same day, and the reproducer already produced correct results there.

macOS 26.5.1 was broken. I did not test 26.5.2 or 26.6, so I can only place the fix somewhere after 26.5.1 and no later than 26.6.1. I found no public release-note mention of this compiler miscompilation in Apple's release notes. Whatever changed was already in the 26.6.1 build before my report could have influenced it.