Summary
The latest native macOS CPU benchmark run shows several repeatable performance gaps for tenferro-rs on an Apple M4. This is a consolidated report so the individual regressions can be triaged together.
Benchmark reports:
Environment
- Hardware: Apple M4, macOS 26.5.2, 1 or 4 threads
- tenferro-rs commit:
11a5b5a3c30b6258919557b4b69b429b6e686d75
- CPU backend:
system-accelerate, Apple Accelerate
- PyTorch: 2.12.0, Accelerate
- JAX: 0.10.1, XLA CPU
- Timings are medians in milliseconds; inputs/setup are outside the timed region where documented.
Collection command:
PERMUTATION_EXTRA_FEATURES=hptt BENCHMARK_TARGET_PROFILE=mac-cpu ./scripts/run_all.sh 1 4
Observed gaps
1. Small matrix and AD overhead
At 4 threads, tenferro-rs eager mode is substantially slower than PyTorch for small shapes:
| benchmark |
shape |
tenferro eager |
PyTorch |
ratio |
einsum_ij_jk_ik |
2x2 |
0.425 ms |
0.029 ms |
14.7x |
matmul |
2x2 |
0.296 ms |
0.010 ms |
29.6x |
svd |
2x2 |
0.276 ms |
0.010 ms |
27.6x |
solve |
2x2, rhs=1 |
0.332 ms |
0.018 ms |
18.4x |
eigh |
2x2 |
0.231 ms |
0.011 ms |
21.0x |
The traced path reduces the overhead but remains slower for several rows, for example 0.038 ms vs 0.010 ms for 2x2 matmul.
Small reverse-mode AD also shows a large fixed cost:
| benchmark |
shape |
tenferro trace |
PyTorch |
ratio |
grad_sum_qr_vjp |
2x2 |
0.335 ms |
0.075 ms |
4.5x |
grad_sum_qr_jvp |
4x4 |
0.275 ms |
0.068 ms |
4.0x |
grad_sum_eigh_vjp |
2x2 |
0.152 ms |
0.041 ms |
3.7x |
2. Indexing and metadata-only views
The public API report has unusually large gaps:
gather, f64, 262144 elements, 1 thread: tenferro direct 3.290 ms vs PyTorch 0.180 ms and JAX 0.066 ms.
gather, 4 threads: tenferro direct 1.060 ms vs PyTorch 0.180 ms and JAX 0.079 ms.
reshape_view, 33554432 -> 8192x4096: tenferro direct 9.089 ms (1 thread) / 7.928 ms (4 threads), while PyTorch reports 0.001 ms.
slice_view, 4194304 -> 2096128: tenferro 0.741 / 0.693 ms vs PyTorch 0.001 ms.
transpose_view, 4096x4096: tenferro 3.283 / 2.944 ms vs PyTorch 0.001 ms.
The view rows are intended to be metadata-only operations. Please verify whether the tenferro public API is materializing or otherwise touching the full storage.
3. Permutation materialization
The largest end-to-end materialization gaps are:
rotation_6d_32_32_32_32_16_16, 1 thread: tenferro 339.853 ms vs HPTT 217.848 ms and Strided.jl 206.587 ms.
- The same row, 4 threads: tenferro 253.926 ms vs HPTT 117.239 ms.
reverse_23d_2, 1 thread: tenferro 45.276 ms vs strided-rs 13.166 ms.
reverse_23d_2, 4 threads: tenferro 21.777 ms vs strided-rs 7.316 ms.
transpose_3d_256_201, 1 thread: tenferro 25.293 ms vs strided-rs 14.558 ms.
transpose_3d_256_201, 4 threads: tenferro 8.601 ms vs HPTT 7.572 ms.
These rows measure transpose_view followed by to_contiguous, with destination allocation included and the view construction outside timing.
4. Real FFT
The cached FftExecutor path, which is the primary fair comparison against warmed torch.fft, is slower for real transforms:
| operation |
dtype |
threads |
tenferro cached |
PyTorch |
ratio |
rfft |
f64 |
1 |
7.566 ms |
4.885 ms |
1.55x |
rfft |
f64 |
4 |
8.070 ms |
4.864 ms |
1.66x |
irfft |
c64 |
1 |
8.508 ms |
5.014 ms |
1.70x |
irfft |
c64 |
4 |
7.481 ms |
4.965 ms |
1.51x |
Complex fft/ifft rows do not show the same pattern consistently, so this may be specific to the real-transform implementation or Accelerate dispatch.
5. Eager einsum path overhead
For larger contraction workloads, trace mode is often close to PyTorch, but eager mode adds a measurable overhead:
str_mps_varying_inner_product_200, 4 threads: eager 19.314 ms vs PyTorch 6.399 ms; trace 8.211 ms.
str_matrix_chain_multiplication_100, 4 threads: eager 6.777 ms vs PyTorch 3.147 ms; trace 3.397 ms.
lm_batch_likelihood_sentence_3_12d, 4 threads: eager 40.695 ms vs PyTorch 26.058 ms; trace 16.057 ms.
lm_batch_likelihood_sentence_4_4d, 4 threads: eager 20.187 ms vs PyTorch 8.418 ms; trace 9.405 ms.
Questions
- Can the small-shape overhead be separated into tensor allocation, runtime dispatch, layout conversion, and backend call overhead?
- Do the view APIs currently materialize data on the Accelerate backend?
- Is the permutation kernel expected to use a specialized blocked/parallel copy path for these stride patterns?
- Are
rfft/irfft using the optimal Accelerate/vDSP path, and is the cached plan actually reused?
- Could the eager einsum path cache intermediate metadata/plans similarly to the traced path?
The benchmark report contains the raw-run metadata and per-backend logs for follow-up profiling.
Summary
The latest native macOS CPU benchmark run shows several repeatable performance gaps for tenferro-rs on an Apple M4. This is a consolidated report so the individual regressions can be triaged together.
Benchmark reports:
Environment
11a5b5a3c30b6258919557b4b69b429b6e686d75system-accelerate, Apple AccelerateCollection command:
Observed gaps
1. Small matrix and AD overhead
At 4 threads, tenferro-rs eager mode is substantially slower than PyTorch for small shapes:
einsum_ij_jk_ikmatmulsvdsolveeighThe traced path reduces the overhead but remains slower for several rows, for example 0.038 ms vs 0.010 ms for 2x2 matmul.
Small reverse-mode AD also shows a large fixed cost:
grad_sum_qr_vjpgrad_sum_qr_jvpgrad_sum_eigh_vjp2. Indexing and metadata-only views
The public API report has unusually large gaps:
gather, f64, 262144 elements, 1 thread: tenferro direct 3.290 ms vs PyTorch 0.180 ms and JAX 0.066 ms.gather, 4 threads: tenferro direct 1.060 ms vs PyTorch 0.180 ms and JAX 0.079 ms.reshape_view, 33554432 -> 8192x4096: tenferro direct 9.089 ms (1 thread) / 7.928 ms (4 threads), while PyTorch reports 0.001 ms.slice_view, 4194304 -> 2096128: tenferro 0.741 / 0.693 ms vs PyTorch 0.001 ms.transpose_view, 4096x4096: tenferro 3.283 / 2.944 ms vs PyTorch 0.001 ms.The view rows are intended to be metadata-only operations. Please verify whether the tenferro public API is materializing or otherwise touching the full storage.
3. Permutation materialization
The largest end-to-end materialization gaps are:
rotation_6d_32_32_32_32_16_16, 1 thread: tenferro 339.853 ms vs HPTT 217.848 ms and Strided.jl 206.587 ms.reverse_23d_2, 1 thread: tenferro 45.276 ms vs strided-rs 13.166 ms.reverse_23d_2, 4 threads: tenferro 21.777 ms vs strided-rs 7.316 ms.transpose_3d_256_201, 1 thread: tenferro 25.293 ms vs strided-rs 14.558 ms.transpose_3d_256_201, 4 threads: tenferro 8.601 ms vs HPTT 7.572 ms.These rows measure
transpose_viewfollowed byto_contiguous, with destination allocation included and the view construction outside timing.4. Real FFT
The cached
FftExecutorpath, which is the primary fair comparison against warmedtorch.fft, is slower for real transforms:rfftrfftirfftirfftComplex
fft/ifftrows do not show the same pattern consistently, so this may be specific to the real-transform implementation or Accelerate dispatch.5. Eager einsum path overhead
For larger contraction workloads, trace mode is often close to PyTorch, but eager mode adds a measurable overhead:
str_mps_varying_inner_product_200, 4 threads: eager 19.314 ms vs PyTorch 6.399 ms; trace 8.211 ms.str_matrix_chain_multiplication_100, 4 threads: eager 6.777 ms vs PyTorch 3.147 ms; trace 3.397 ms.lm_batch_likelihood_sentence_3_12d, 4 threads: eager 40.695 ms vs PyTorch 26.058 ms; trace 16.057 ms.lm_batch_likelihood_sentence_4_4d, 4 threads: eager 20.187 ms vs PyTorch 8.418 ms; trace 9.405 ms.Questions
rfft/irfftusing the optimal Accelerate/vDSP path, and is the cached plan actually reused?The benchmark report contains the raw-run metadata and per-backend logs for follow-up profiling.