Skip to content

perf(linear): pin the two-FMA value blend with hardware-gated fma - #215

Closed
mgyoo86 wants to merge 2 commits into
masterfrom
perf/blend-hardware-fma
Closed

mgyoo86 wants to merge 2 commits into
masterfrom
perf/blend-hardware-fma

Conversation

@mgyoo86

@mgyoo86 mgyoo86 commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

On hardware with FMA, linear and cubic value evaluation now gives the same bits regardless of which search policy or --check-bounds mode compiled it, and runs slightly faster. Nothing changes on hardware without FMA or for non-float carriers.

Why. The shared linear blend α·yR + (1−α)·yL is written as two muladds, but muladd only allows fusion. Depending on the code it gets inlined into, LLVM sometimes fused both operations and sometimes neither, so two search policies could return results 1-2 ULP apart (the flake fixed in #212) and the hot path paid an extra multiply and add.

What. When the blend eltype is a real IEEEFloat and the compilation target has hardware FMA, the blend uses fma, which pins both fused operations. The check is Core.Intrinsics.have_fma(T), the same codegen-time constant Base.fma dispatches on, so there is no runtime branch and no software-emulated fma anywhere. The intrinsic is not public API, so the method sits under @static if isdefined(...) and falls back to today's muladd if it ever disappears.

before after
search policies agree bit-for-bit (linear, cubic) no, 1-2 ULP on ~17 % of queries yes
cubic 1D, 10k queries 19.2 µs 17.8 µs
linear 1D, 10k queries 13.8 µs 13.5 µs
hardware without FMA, Complex, Dual, Unitful, colorants unchanged unchanged

This is a same-machine consistency and performance change, not a cross-machine reproducibility guarantee; other kernels still use muladd, so cross-path tests keep their ULP budget.

Changes

  • src/linear/linear_kernels.jl: _linear_value_blend(::_LinearBlendFMA) → _fma2_blend, with a hardware-gated fma method for same-width IEEEFloat and the previous muladd form for everything else.

Not covered

  • Results move by at most 2 ULP toward the exact value; no existing test needed a tolerance change.
  • The Hermite-family value kernel has a similar, larger opportunity (16 → 12 ops) and is left for its own PR.

Tests

  • Linear, cubic, search, node-exactness, colorant, complex, mixed-precision and ND linear test files pass on Julia 1.13; the blend arms and bit-identity were also checked on LTS 1.10 with --check-bounds=yes.

`_linear_value_blend(::_LinearBlendFMA)` is documented as the verbatim 2-FMA
form, but `muladd` only permits fusion. Inlined next to search code, LLVM's SLP
vectorizer packs the two products into a 2-lane vector plus a horizontal add,
which cannot fuse: every Vector-grid 1D path paid one extra multiply and add
(linear fma=1 instead of 2, cubic fma=5 instead of the documented 6), and under
--check-bounds=yes only the LinearBinarySearch specialization was packed, which
is the 1-2 ULP cross-path drift behind #212.

For same-width IEEEFloat blends on a target with hardware FMA the blend now uses
`fma`, pinning both fused operations at the same cost. The check is
`Core.Intrinsics.have_fma(T)`, the codegen-time constant Base.fma dispatches on;
without hardware FMA the blend keeps `muladd` (no fma_emulated). The intrinsic is
not public API, so the method is defined under `@static if isdefined(...)` and
falls back to `muladd` if it ever disappears. Complex, mixed-width, Dual, Unitful
and colorant carriers keep their existing arms.

Measured (aarch64, 10k sorted queries, 0 allocs): linear 1D 13.8→13.5 µs
(Binary), cubic 1D 19.2→17.8 µs; all search policies bit-identical on Julia
1.10 and 1.13; cubic value error vs exact 0.069→0.062 eps, 66.8→71.6 % exact.
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

FastInterpolations.jl Benchmarks

All benchmarks (71 total, click to expand)
Benchmark Current: ea58668 Previous Imm. Ratio Grad. Ratio
10_nd_construct/bicubic_2d 36848 ns 34525.0 ns 1.067 0.969
10_nd_construct/bilinear_2d 590.52 ns 568.7 ns 1.038 0.949
10_nd_construct/phs_2d 1080527.6 ns 1071600.1 ns 1.008 -
10_nd_construct/phs_3d 17936438.6 ns 17321717.9 ns 1.035 -
10_nd_construct/tricubic_3d 309629 ns 313178.0 ns 0.989 0.865
10_nd_construct/trilinear_3d 1832.62 ns 1648.1 ns 1.112 1.039
11_nd_eval/bicubic_2d_batch 1418.6 ns 1429.7 ns 0.992 1.018
11_nd_eval/bicubic_2d_scalar 15.82 ns 15.8 ns 1.0 0.883
11_nd_eval/bilinear_2d_scalar 8.51 ns 8.5 ns 1.0 0.993
11_nd_eval/phs_2d_batch 38513.1 ns 38945.0 ns 0.989 -
11_nd_eval/phs_2d_scalar 300.36 ns 303.8 ns 0.989 -
11_nd_eval/phs_3d_batch 234966.7 ns 234531.0 ns 1.002 -
11_nd_eval/phs_3d_scalar 2229.18 ns 2224.8 ns 1.002 -
11_nd_eval/tricubic_3d_batch 3066.7 ns 3057.7 ns 1.003 0.939
11_nd_eval/tricubic_3d_scalar 34.76 ns 34.3 ns 1.015 1.019
11_nd_eval/trilinear_3d_scalar 14.02 ns 13.0 ns 1.077 1.004
12_cubic_eval_gridquery/range_random 2853.14 ns 2884.6 ns 0.989 0.673
12_cubic_eval_gridquery/range_sorted 2852.76 ns 2878.0 ns 0.991 0.675
12_cubic_eval_gridquery/vec_random 9848.44 ns 8802.7 ns 1.119 1.068
12_cubic_eval_gridquery/vec_sorted 2381.46 ns 2637.1 ns 0.903 0.738
13_nd_oneshot_gridquery/bicubic_2d_rand_rand 62007 ns 61417.2 ns 1.01 0.952
13_nd_oneshot_gridquery/bicubic_2d_sort_rand 58059.6 ns 57310.5 ns 1.013 0.936
13_nd_oneshot_gridquery/bicubic_2d_sort_sort 56309.3 ns 55603.4 ns 1.013 0.935
13_nd_oneshot_gridquery/bilinear_2d_rand_rand 16800.64 ns 16337.9 ns 1.028 1.012
13_nd_oneshot_gridquery/bilinear_2d_sort_rand 9900.12 ns 9360.0 ns 1.058 1.073
13_nd_oneshot_gridquery/bilinear_2d_sort_sort 5118.96 ns 5596.5 ns 0.915 1.001
14_series_oneshot_batch/constant_inplace_vec_k8_q1000_rand 13572.4 ns 13383.1 ns 1.014 0.937
14_series_oneshot_batch/linear_inplace_vec_k8_q1000_rand 13843.9 ns 14036.3 ns 0.986 0.963
15_gridded_query/cubic_oneshot_2d_40x32 41295.2 ns 41465.7 ns 0.996 0.93
15_gridded_query/cubic_persistent_2d_40x32 8326.5 ns 8331.7 ns 0.999 0.977
15_gridded_query/linear_oneshot_2d_40x32 1661.72 ns 1538.3 ns 1.08 1.077
15_gridded_query/linear_oneshot_3d_12x10x8 2917.06 ns 1932.6 ns 1.509 1.45
15_gridded_query/linear_persistent_2d_40x32 1627.24 ns 1516.6 ns 1.073 1.082
15_gridded_query/linear_persistent_3d_12x10x8 2781.4 ns 1887.5 ns 1.474 1.391
16_phs/construct_g0100 35826.44 ns 34416.5 ns 1.041 -
16_phs/construct_g1000 36138.6 ns 34249.2 ns 1.055 -
16_phs/eval_q00001 112.7 ns 111.6 ns 1.01 -
16_phs/eval_q00100 11879.64 ns 11914.7 ns 0.997 -
16_phs/eval_q10000 1173327.3 ns 1178071.9 ns 0.996 -
16_phs/oneshot_q00001 37321.44 ns 35355.5 ns 1.056 -
16_phs/oneshot_q10000 1770210.3 ns 1763848.5 ns 1.004 -
1_cubic_oneshot/q00001 455.24 ns 457.0 ns 0.996 1.0
1_cubic_oneshot/q10000 29710.6 ns 29532.3 ns 1.006 0.685
2_cubic_construct/g0100 1329.1 ns 1302.4 ns 1.02 1.016
2_cubic_construct/g1000 13555.4 ns 12559.6 ns 1.079 1.075
3_cubic_eval/q00001 9.91 ns 9.9 ns 1.0 0.482
3_cubic_eval/q00100 301.96 ns 304.8 ns 0.991 0.683
3_cubic_eval/q10000 28931.1 ns 29059.5 ns 0.996 0.679
4_linear_oneshot/q00001 27.14 ns 26.7 ns 1.015 1.023
4_linear_oneshot/q10000 18442.5 ns 18657.0 ns 0.989 1.0
5_linear_construct/g0100 40.77 ns 35.5 ns 1.149 1.1
5_linear_construct/g1000 345.94 ns 263.0 ns 1.315 1.263
6_linear_eval/q00001 8.72 ns 9.0 ns 0.968 0.834
6_linear_eval/q00100 197.96 ns 198.4 ns 0.998 1.008
6_linear_eval/q10000 18317.3 ns 18318.3 ns 1.0 1.006
7_cubic_range/scalar_query 5.7 ns 5.7 ns 1.0 0.708
7_cubic_vec/scalar_query 9.11 ns 8.5 ns 1.071 0.832
8_cubic_multi/construct_s001_q100 515.56 ns 511.1 ns 1.009 1.01
8_cubic_multi/construct_s010_q100 4343.74 ns 4240.0 ns 1.024 1.012
8_cubic_multi/construct_s100_q100 40698.1 ns 39579.2 ns 1.028 1.029
8_cubic_multi/eval_s001_q100 428.58 ns 430.4 ns 0.996 0.735
8_cubic_multi/eval_s010_q100 1306.04 ns 1337.3 ns 0.977 0.894
8_cubic_multi/eval_s010_q100_scalar_loop 2210.34 ns 2195.3 ns 1.007 0.951
8_cubic_multi/eval_s100_q100 10030.8 ns 10040.8 ns 0.999 0.982
8_cubic_multi/eval_s100_q100_scalar_loop 3270.1 ns 3338.2 ns 0.98 0.961
9_nd_oneshot/bicubic_2d 34735 ns 34965.6 ns 0.993 0.921
9_nd_oneshot/bilinear_2d 453.46 ns 451.1 ns 1.005 1.004
9_nd_oneshot/phs_2d 1349818.8 ns 1331365.8 ns 1.014 -
9_nd_oneshot/phs_3d 20025410 ns 19856472.4 ns 1.009 -
9_nd_oneshot/tricubic_3d 322088.7 ns 315974.6 ns 1.019 0.874
9_nd_oneshot/trilinear_3d 853.6 ns 837.5 ns 1.019 0.935

⚠️ Performance Regression Confirmed ⚠️

After re-running 6 flagged benchmark(s) 10 time(s), 6 regression(s) confirmed.

Benchmark Current Previous Imm. Ratio Grad. Ratio Tier
10_nd_construct/trilinear_3d 1832.62 ns 1648.1 ns 1.112 1.039 immediate
12_cubic_eval_gridquery/vec_random 9848.44 ns 8802.7 ns 1.119 1.068 immediate
15_gridded_query/linear_oneshot_3d_12x10x8 2917.06 ns 1932.6 ns 1.509 1.45 both
15_gridded_query/linear_persistent_3d_12x10x8 2781.4 ns 1887.5 ns 1.474 1.391 both
5_linear_construct/g0100 40.77 ns 35.5 ns 1.149 1.1 immediate
5_linear_construct/g1000 345.94 ns 263.0 ns 1.315 1.263 both

Thresholds: immediate > 1.1x (vs latest master), gradual > 1.1x (vs sliding window)

Runner: znver3|4c — AMD EPYC 7763 64-Core Processor, julia 1.13.1. Times are min-merged and compared only against this same machine's history.

This comment was automatically generated by Benchmark workflow.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused implementation correctly preserves fallback behavior while enforcing deterministic fused evaluation where supported.

0 open findings

What changed in this PR

Pins real IEEE-float interpolation blends to hardware FMA while preserving existing fallbacks.

Changes:

  • Adds hardware-gated two-FMA evaluation.
  • Retains muladd for unsupported hardware and other carriers.
File Description
src/​linear/​linear_kernels.jl Introduces the guarded _fma2_blend implementation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.26%. Comparing base (5901fbe) to head (ea58668).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #215   +/-   ##
=======================================
  Coverage   96.26%   96.26%           
=======================================
  Files         171      171           
  Lines       15417    15420    +3     
=======================================
+ Hits        14841    14844    +3     
  Misses        576      576           
Files with missing lines Coverage Δ
src/linear/linear_kernels.jl 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mgyoo86

mgyoo86 commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

Closing: premature optimization.

The CI benchmark regressions on linear ND / gridded paths (+4…+19 %) are real and reproduce locally. Pinning fma removes the freedom LLVM used to pack the independent blends of an ND cell into vector instructions, so forcing fusion loses more than it gains there. Even on cubic 1D the effect is a wash (sorted batches −3 %, random queries with LinearBinarySearch +8 %).

The cross-path consistency issue that motivated this is already handled on the test side by #212. Keeping muladd as the repo-wide default stands.

@mgyoo86 mgyoo86 closed this Oct 8, 2026
@mgyoo86
mgyoo86 deleted the perf/blend-hardware-fma branch October 8, 2026 18:37
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.

2 participants