I have been using a small macOS research project to exercise Metal with a workload that differs from rendering and dense machine-learning kernels. Autolykos v2 is useful for this because it combines a large, height-dependent working set with pseudo-random reads, integer-heavy hashing, sustained execution, and periodic replacement of the dataset.
The project happens to be a miner, but my question here is strictly about Metal compute behaviour.
On the Apple M4 system used for these measurements, the full dataset contained 216,430,305 elements of 32 bytes each: 6.93 GB, or about 6.45 GiB, held in a .storageModePrivate buffer. For every nonce, the search kernel:
- computes an index seed,
- performs 32 pseudo-random 32-byte dataset reads,
- accumulates the eight 32-bit limbs into a 256-bit sum, and
- applies a final BLAKE2b compression and target comparison.
The normal dispatch uses 128 threads per threadgroup. The wider pipeline also builds the next height's dataset in chunks on a separate command queue while search continues, and keeps two search command buffers in flight. I record command-buffer wall time, gpuStartTime/gpuEndTime, unions of overlapping intervals, and thermal state.
To estimate the ceiling imposed by the random gathers, I added a non-consensus microbenchmark. It retains the normal seed calculation, index distribution, all 32 dataset reads, and the complete accumulation, but omits the final BLAKE2b compression. The accumulated result remains observable through a comparison, so the gather loop cannot simply disappear.
I expected this stripped kernel to be at least slightly faster. Instead, an order-balanced campaign on an M4 produced:
- complete search kernel: 3.108 million nonces/s median active throughput
- gather-only kernel: 2.952 million nonces/s
- ratio: 105.3%
All four same-round ratios were between 103.18% and 105.74%. Each measured run used the full dataset, a 30-second search interval, an excluded warm-up, and a start-temperature gate below 50 °C. Both compute pipeline states reported maxTotalThreadsPerThreadgroup == 1024.
My conservative conclusion was not to pursue speculative register-pressure or manual memory-level-parallelism rewrites. The access pattern appears sufficiently dominant, while the supposedly simpler microbenchmark may have changed the compiled pipeline in a way that makes it a poor upper-bound model.
My questions are:
Can removing the trailing arithmetic legitimately make a memory-latency-heavy Metal kernel slower by changing register allocation, instruction scheduling, or the amount of useful latency hiding? Or would you first suspect a flaw in this kind of gather-only benchmark construction?
Also, which Metal GPU counters are the most reliable way to distinguish memory-latency saturation from register-limited occupancy in a long-running compute kernel? I am looking at compute occupancy, buffer and ALU limiters, bandwidth, and cache behaviour, but maxTotalThreadsPerThreadgroup alone is clearly too coarse to explain the result.
This is one hardware-specific observation rather than a general claim about Apple GPUs. If useful, I can reduce the workload to a smaller standalone reproducer.
The source code, benchmark driver, and complete campaign report are available here: https://github.com/giffeler/ergometal
The detailed measurements and validation procedure for this comparison are documented here: https://github.com/giffeler/ergometal/blob/main/Benchmarks/2026-08-15-search-gather-ceiling-ab.md