Skip to content

Convert to BFloat16 with the hardware instruction on the emulated path - #1122

Merged
maleadt merged 1 commit into
mainfrom
tb/bfloat16-conversion
Oct 28, 2026
Merged

maleadt merged 1 commit into
mainfrom
tb/bfloat16-conversion

Conversation

@maleadt

@maleadt maleadt commented Oct 28, 2026

Copy link
Copy Markdown
Member

Before Julia 1.13 there is no LLVM bfloat arithmetic on Apple silicon, and BFloat16s converts from Float32 by rounding the bits as an integer: isnan(x) ? 0x7fc0 : (bits + bias) >> 16. Apple's back-end drops the shift of that select when its result is used as 32 bits. The IR and AIR Metal.jl emits are correct. Minimal case, without BFloat16 (macOS 27, M1):

k(out, a) = (i = thread_position_in_threadgroup().x; f = a[i] + a[i]; b = reinterpret(UInt32, f);
             out[i] = isnan(f) ? UInt32(0x7fc0) : b >> 16; return)
# returns 0x40000000 for a = ones(Float32, 32), instead of 0x00004000

Only an fcmp ord/uno condition combined with a logical shift right by 16 triggers it. Normally the result is truncated to i16 right away and nothing goes wrong, but anything that widens the bits gets them wrong:

On the emulated path, the device conversion now emits the fptrunc float to bfloat that the native path uses. Metal.jl requires macOS 15, so the conversion is available on every supported system. The conversion matched BFloat16s bit for bit on 2M inputs, including subnormals, infinities, signed zeros and rounding ties. The only difference is that NaN payloads are kept rather than normalized to 0x7fc0, as on the native path.

Without LLVM `bfloat` arithmetic (before Julia 1.13), BFloat16s converts
from `Float32` by rounding the bits as an integer, selecting the NaN
pattern with `isnan(x) ? 0x7fc0 : (bits + bias) >> 16`. Apple's back-end
drops the shift of that select when its result is used as 32 bits, so
shuffling a `BFloat16` (which KernelInterface does as a `UInt32`) or a
`Complex{BFloat16}` returned garbage: e.g. `sum` of a `BFloat16` array is
NaN with GPUArrays' ordered reductions.

Emit the `fptrunc` to `bfloat` that the native path uses instead, which
every supported macOS has.
@maleadt
maleadt merged commit 45e603b into main Oct 28, 2026
11 of 17 checks passed
@maleadt
maleadt deleted the tb/bfloat16-conversion branch October 28, 2026 10:16
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark results

Compared with 0877ad5a3, the merge base.
The environment differs from the baseline's: GPUCompiler 2.18.0 → 2.19.0.

Times are the minimum time of every benchmark. A change is unusual when it exceeds both three times the benchmark's typical change between consecutive runs on the default branch and a minimum per category (performance 3%, latency 5%, applications 3%, other 3%). A benchmark that starts or stops allocating is listed however few the allocations. A single run can still be a fluke, so rerun before drawing conclusions.

Geometric mean Unusual: slower Unusual: faster Compared
Performance −0.1% 1 1 74
Latency −0.3% 0 0 4
Applications +1.2% 1 0 5
Benchmark Baseline This PR Change
Performance
array/random/randn!/Float32 357 µs 294 µs −17.7% 🟢 faster
array/private/iteration/findfirst/bool 255 µs 283 µs +11.2% 🔴 slower
Applications
metaldevrt 370 µs 393 µs +6.1% 🔴 slower
All benchmarks
Benchmark Baseline This PR Change
Performance
array/accumulate/Float32/1d 393 µs 392 µs −0.3%
array/accumulate/Float32/dims=1 373 µs 364 µs −2.5%
array/accumulate/Float32/dims=1L 8.87 ms 8.86 ms −0.1%
array/accumulate/Float32/dims=2 436 µs 431 µs −1.2%
array/accumulate/Float32/dims=2L 2.86 ms 2.69 ms −6.0%
array/accumulate/Int64/1d 520 µs 526 µs +1.1%
array/accumulate/Int64/dims=1 518 µs 521 µs +0.5%
array/accumulate/Int64/dims=1L 1.64 ms 1.64 ms +0.3%
array/accumulate/Int64/dims=2 540 µs 538 µs −0.3%
array/accumulate/Int64/dims=2L 5.45 ms 5.43 ms −0.3%
array/broadcast 238 µs 237 µs −0.1%
array/construct 2.25 µs 2.21 µs −1.9%
array/permutedims/2d 340 µs 338 µs −0.6%
array/permutedims/3d 465 µs 461 µs −0.8%
array/permutedims/4d 423 µs 420 µs −0.6%
array/private/copy 226 µs 228 µs +0.8%
array/private/copyto!/cpu_to_gpu 219 µs 224 µs +2.3%
array/private/copyto!/gpu_to_cpu 224 µs 220 µs −1.8%
array/private/copyto!/gpu_to_gpu 217 µs 219 µs +0.6%
array/private/iteration/findall/bool 354 µs 356 µs +0.4%
array/private/iteration/findall/int 428 µs 421 µs −1.7%
array/private/iteration/findfirst/bool 255 µs 283 µs +11.2% 🔴 slower
array/private/iteration/findfirst/int 283 µs 279 µs −1.4%
array/private/iteration/findmin/1d 348 µs 347 µs −0.2%
array/private/iteration/findmin/2d 389 µs 386 µs −0.9%
array/private/iteration/logical 365 µs 364 µs −0.2%
array/private/iteration/scalar 1.40 ms 1.34 ms −3.9%
array/random/rand!/Float32 426 µs 427 µs +0.2%
array/random/rand!/Int64 451 µs 447 µs −1.1%
array/random/rand/Float32 444 µs 439 µs −1.2%
array/random/rand/Int64 515 µs 514 µs −0.2%
array/random/randn!/Float32 357 µs 294 µs −17.7% 🟢 faster
array/random/randn/Float32 368 µs 364 µs −0.9%
array/reductions/mapreduce/Float32/1d 214 µs 218 µs +2.0%
array/reductions/mapreduce/Float32/dims=1 216 µs 213 µs −1.0%
array/reductions/mapreduce/Float32/dims=1L 581 µs 579 µs −0.5%
array/reductions/mapreduce/Float32/dims=2 256 µs 253 µs −1.4%
array/reductions/mapreduce/Float32/dims=2L 423 µs 408 µs −3.6%
array/reductions/mapreduce/Int64/1d 210 µs 230 µs +9.3%
array/reductions/mapreduce/Int64/dims=1 218 µs 220 µs +1.1%
array/reductions/mapreduce/Int64/dims=1L 998 µs 994 µs −0.4%
array/reductions/mapreduce/Int64/dims=2 264 µs 259 µs −2.0%
array/reductions/mapreduce/Int64/dims=2L 600 µs 599 µs −0.0%
array/reductions/reduce/Float32/1d 219 µs 216 µs −1.0%
array/reductions/reduce/Float32/dims=1 212 µs 212 µs −0.1%
array/reductions/reduce/Float32/dims=1L 580 µs 576 µs −0.7%
array/reductions/reduce/Float32/dims=2 253 µs 255 µs +0.7%
array/reductions/reduce/Float32/dims=2L 422 µs 413 µs −2.1%
array/reductions/reduce/Int64/1d 233 µs 230 µs −1.7%
array/reductions/reduce/Int64/dims=1 224 µs 220 µs −2.0%
array/reductions/reduce/Int64/dims=1L 1.00 ms 999 µs −0.2%
array/reductions/reduce/Int64/dims=2 262 µs 264 µs +0.6%
array/reductions/reduce/Int64/dims=2L 590 µs 606 µs +2.7%
array/shared/copy 128 µs 129 µs +1.0%
array/shared/copyto!/cpu_to_gpu 36.8 µs 37.5 µs +1.8%
array/shared/copyto!/gpu_to_cpu 37.3 µs 37.8 µs +1.5%
array/shared/copyto!/gpu_to_gpu 37.3 µs 37.9 µs +1.7%
array/shared/iteration/findall/bool 359 µs 359 µs −0.1%
array/shared/iteration/findall/int 429 µs 429 µs +0.1%
array/shared/iteration/findfirst/bool 259 µs 278 µs +7.2%
array/shared/iteration/findfirst/int 286 µs 282 µs −1.3%
array/shared/iteration/findmin/1d 347 µs 347 µs −0.0%
array/shared/iteration/findmin/2d 391 µs 396 µs +1.4%
array/shared/iteration/logical 367 µs 366 µs −0.2%
array/shared/iteration/scalar 75.7 ns 84.5 ns +11.6%
array/sorting/1d 1.91 ms 1.91 ms +0.2%
array/sorting/2d 8.32 ms 8.37 ms +0.6%
kernel/indexing 216 µs 210 µs −2.9%
kernel/indexing_checked 393 µs 391 µs −0.4%
kernel/launch 775 ns 779 ns +0.5% noise unknown
kernel/metalcall 821 ns 833 ns +1.5% noise unknown
kernel/rand 410 µs 406 µs −0.8%
metal/synchronization/context 556 ns 572 ns +2.9%
metal/synchronization/stream 362 ns 369 ns +2.1%
Latency
compilation 1.78 s 1.77 s −0.8%
import 1.47 s 1.47 s +0.4%
launch 1.61 s 1.59 s −1.0% noise unknown
precompile 35.3 s 35.4 s +0.3%
Applications
byval/reference 1.12 ms 1.12 ms −0.1%
byval/slices=1 1.11 ms 1.12 ms +0.1%
byval/slices=2 2.03 ms 2.02 ms −0.4%
byval/slices=3 6.78 ms 6.80 ms +0.3%
metaldevrt 370 µs 393 µs +6.1% 🔴 slower

History: https://juliagpu.org/perf/?pkg=metal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant