Skip to content

perf(hetero): fold all-Cardinal ND evaluation without per-fiber setup - #229

Open
Beforerr wants to merge 1 commit into
ProjectTorreyPines:masterfrom
Beforerr:perf/cardinal-nd-cell-fold
Open

Beforerr wants to merge 1 commit into
ProjectTorreyPines:masterfrom
Beforerr:perf/cardinal-nd-cell-fold

Conversation

@Beforerr

@Beforerr Beforerr commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Evaluating an all-Cardinal, non-periodic HeteroInterpolantND is much slower than its stencil requires. On a 64³ grid, a 3D evaluation takes about 76 ns for
SVector{3,Float64} data and 52 ns for Float64. A hand-written Catmull-Rom takes about 25 ns for SVector{3,Float64}.

Root cause

_collapse_dims runs a full 1D one-shot for each of the 21 fibers in 3D, repeating the search and setup, and acquires pool buffers at each level. A Cardinal fiber is also linear in its stencil values, with coefficients that depend only on the axis and query.

Fix

Add a more specific _collapse_dims method for tuples of two or more CardinalInterp{<:Any, NoBC} axes:

  • Resolve the search and the four stencil weights once per axis, for the requested eval op.
  • Fold the stencil values recursively as weighted sums, with no pool buffers.

Existing callers need no changes. That includes derivative ops and periodic axes that the wrap-aware path has already stripped to NoBC. PCHIP, Akima, global-solve methods and mixed tuples stay on the existing fiber path, though an all-Cardinal tail of a mixed tuple does use the new method.

Tuples of four or more axes go through a @noinline boundary to limit generated code size.

Extrapolation is not re-applied per fiber. The method relies on callers having already applied it to q_eval, and uses a two-sided clamped search so indices stay in range.

How verified

Benchmarks on an 64³ grid, 3D evaluation:

Data Before After
SVector{3,Float64} 76 ns 29 ns
Float64 52 ns 21 ns

Native code for the SVector call drops from about 18.8k to 1.1k instructions.

Results change by rounding only, because weighted sums replace the kernel's secant differences. For example, a constant field of 1.1 now evaluates with an error of about 2e-16 at one test point, where it was exact.

The new test/test_cardinal_cell_fold.jl compares against nested 1D cardinal_interp calls along whole fibers. It covers n = 2, 3, 4, 5, 8, uniform and non-uniform grids, tension, derivatives, SVector data, knots and extrapolated queries. It also checks that a NaN inside the fixed window but outside the end cell's stencil stays out of the result, and that evaluation doesn't allocate.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

FastInterpolations.jl Benchmarks

🚨 Regression baseline is a different CPU

CPU
This PR run `znver4
Master baseline (latest commit) `znver3

The Previous / Imm. Ratio / Grad. Ratio columns compare against master history, but master's most recent commit was benchmarked on a different CPU — so a naive comparison is cross-CPU and unreliable. The ratios below use only same-znver4|4c history and are blank if master has none on this CPU yet.

All benchmarks (71 total, click to expand)
Benchmark Current: 009ef6c Previous Imm. Ratio Grad. Ratio
10_nd_construct/bicubic_2d 34011 ns 35262.0 ns 0.965 0.87
10_nd_construct/bilinear_2d 579.28 ns 691.0 ns 0.838 0.79
10_nd_construct/phs_2d 1051949.8 ns 1164076.4 ns 0.904 -
10_nd_construct/phs_3d 15605470.5 ns 18671391.9 ns 0.836 -
10_nd_construct/tricubic_3d 269366 ns 289847.0 ns 0.929 0.817
10_nd_construct/trilinear_3d 1559.74 ns 1955.9 ns 0.797 0.764
11_nd_eval/bicubic_2d_batch 1256.9 ns 1500.2 ns 0.838 0.815
11_nd_eval/bicubic_2d_scalar 15.12 ns 16.5 ns 0.915 0.851
11_nd_eval/bilinear_2d_scalar 7.31 ns 7.6 ns 0.961 0.948
11_nd_eval/phs_2d_batch 26664.1 ns 35060.6 ns 0.761 -
11_nd_eval/phs_2d_scalar 220.72 ns 291.4 ns 0.757 -
11_nd_eval/phs_3d_batch 157316.9 ns 206960.8 ns 0.76 -
11_nd_eval/phs_3d_scalar 1484.82 ns 2001.0 ns 0.742 -
11_nd_eval/tricubic_3d_batch 2557.8 ns 3268.8 ns 0.782 0.724
11_nd_eval/tricubic_3d_scalar 25.73 ns 36.6 ns 0.704 0.686
11_nd_eval/trilinear_3d_scalar 11.41 ns 12.1 ns 0.942 0.841
12_cubic_eval_gridquery/range_random 2422.06 ns 3002.0 ns 0.807 0.521
12_cubic_eval_gridquery/range_sorted 2416.24 ns 2993.0 ns 0.807 0.52
12_cubic_eval_gridquery/vec_random 7491.08 ns 9557.3 ns 0.784 0.749
12_cubic_eval_gridquery/vec_sorted 2022.66 ns 2538.5 ns 0.797 0.636
13_nd_oneshot_gridquery/bicubic_2d_rand_rand 53997.3 ns 62970.7 ns 0.857 0.835
13_nd_oneshot_gridquery/bicubic_2d_sort_rand 50815.5 ns 57671.9 ns 0.881 0.833
13_nd_oneshot_gridquery/bicubic_2d_sort_sort 49679.8 ns 55947.3 ns 0.888 0.841
13_nd_oneshot_gridquery/bilinear_2d_rand_rand 13950 ns 16910.3 ns 0.825 0.812
13_nd_oneshot_gridquery/bilinear_2d_sort_rand 8414.06 ns 9601.5 ns 0.876 0.89
13_nd_oneshot_gridquery/bilinear_2d_sort_sort 3891.24 ns 4748.6 ns 0.819 0.844
14_series_oneshot_batch/constant_inplace_vec_k8_q1000_rand 10690.1 ns 13677.2 ns 0.782 0.59
14_series_oneshot_batch/linear_inplace_vec_k8_q1000_rand 12517.8 ns 16030.6 ns 0.781 0.732
15_gridded_query/cubic_oneshot_2d_40x32 39366.3 ns 41303.9 ns 0.953 0.881
15_gridded_query/cubic_persistent_2d_40x32 6543.9 ns 7886.6 ns 0.83 0.805
15_gridded_query/linear_oneshot_2d_40x32 1253.28 ns 1558.5 ns 0.804 0.789
15_gridded_query/linear_oneshot_3d_12x10x8 1449.78 ns 1833.5 ns 0.791 0.719
15_gridded_query/linear_persistent_2d_40x32 1213.62 ns 1526.8 ns 0.795 0.78
15_gridded_query/linear_persistent_3d_12x10x8 1447.58 ns 1800.1 ns 0.804 0.741
16_phs/construct_g0100 30298.4 ns 37382.7 ns 0.81 -
16_phs/construct_g1000 30173.4 ns 37214.8 ns 0.811 -
16_phs/eval_q00001 77.41 ns 98.9 ns 0.782 -
16_phs/eval_q00100 8068.74 ns 10330.6 ns 0.781 -
16_phs/eval_q10000 807106.1 ns 1034718.9 ns 0.78 -
16_phs/oneshot_q00001 31040.54 ns 38354.5 ns 0.809 -
16_phs/oneshot_q10000 1274782.2 ns 1629214.3 ns 0.782 -
1_cubic_oneshot/q00001 414.82 ns 499.5 ns 0.83 0.776
1_cubic_oneshot/q10000 25343.1 ns 31012.7 ns 0.817 0.53
2_cubic_construct/g0100 1088.64 ns 1366.4 ns 0.797 0.729
2_cubic_construct/g1000 10364.6 ns 13238.5 ns 0.783 0.734
3_cubic_eval/q00001 6.91 ns 9.0 ns 0.767 0.289
3_cubic_eval/q00100 275.4 ns 314.9 ns 0.875 0.569
3_cubic_eval/q10000 24744.3 ns 30292.6 ns 0.817 0.526
4_linear_oneshot/q00001 22.63 ns 29.1 ns 0.777 0.789
4_linear_oneshot/q10000 16176.4 ns 19335.5 ns 0.837 0.829
5_linear_construct/g0100 27.24 ns 33.4 ns 0.814 0.718
5_linear_construct/g1000 216.03 ns 254.6 ns 0.849 0.791
6_linear_eval/q00001 6.1 ns 7.9 ns 0.771 0.599
6_linear_eval/q00100 186.88 ns 200.7 ns 0.931 0.939
6_linear_eval/q10000 15857.9 ns 19192.3 ns 0.826 0.822
7_cubic_range/scalar_query 4.6 ns 5.4 ns 0.852 0.494
7_cubic_vec/scalar_query 7.01 ns 8.6 ns 0.814 0.67
8_cubic_multi/construct_s001_q100 450.88 ns 553.4 ns 0.815 0.716
8_cubic_multi/construct_s010_q100 3663.72 ns 4675.7 ns 0.784 0.755
8_cubic_multi/construct_s100_q100 33726.8 ns 43932.7 ns 0.768 0.759
8_cubic_multi/eval_s001_q100 361.54 ns 461.1 ns 0.784 0.499
8_cubic_multi/eval_s010_q100 1147.12 ns 1384.2 ns 0.829 0.678
8_cubic_multi/eval_s010_q100_scalar_loop 1978.18 ns 2366.5 ns 0.836 0.785
8_cubic_multi/eval_s100_q100 8414.6 ns 10703.8 ns 0.786 0.748
8_cubic_multi/eval_s100_q100_scalar_loop 3215.9 ns 3680.4 ns 0.874 0.886
9_nd_oneshot/bicubic_2d 34091.3 ns 35423.2 ns 0.962 0.792
9_nd_oneshot/bilinear_2d 430.86 ns 459.7 ns 0.937 0.921
9_nd_oneshot/phs_2d 1285733.5 ns 1409314.6 ns 0.912 -
9_nd_oneshot/phs_3d 18596539.4 ns 21174592.9 ns 0.878 -
9_nd_oneshot/tricubic_3d 272859.8 ns 301679.3 ns 0.904 0.746
9_nd_oneshot/trilinear_3d 731.1 ns 802.2 ns 0.911 0.817

✅ 2 benchmark(s) were flagged but verified as CI noise after 10 re-run(s)

Runner: znver4|4c — AMD EPYC 9V74 80-Core Processor, julia 1.13.3. Times are min-merged and compared only against this same machine's history.

This comment was automatically generated by Benchmark workflow.

Evaluating an all-Cardinal, non-periodic `HeteroInterpolantND` ran a full 1D
one-shot for each fiber, plus pool buffers per level. A cardinal fiber is linear
in its stencil values, with coefficients that depend only on the axis and query,
so resolve four weights once per axis for the requested eval op and fold the
stencil as weighted sums.

On a 64^3 grid (Apple M5, Julia 1.14.0-alpha2), 3D evaluation drops from 76 to
29 ns for `SVector{3,Float64}` data and from 52 to 21 ns for `Float64`. Results
change by rounding only. The `@noinline` boundary at 4+ axes is what keeps
compile time bounded: without it the first 4D call took 37 s rather than 0.65 s.

This branch has not been deployed

No deployments
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