Skip to content

fix(store): devirtualize ND factory axis maps — zero-alloc Range builds#196

Merged
mgyoo86 merged 4 commits into
masterfrom
fix/nd-range-build-alloc
Jul 20, 2026
Merged

fix(store): devirtualize ND factory axis maps — zero-alloc Range builds#196
mgyoo86 merged 4 commits into
masterfrom
fix/nd-range-build-alloc

Conversation

@mgyoo86

@mgyoo86 mgyoo86 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

The cache_axis work (#194) routed per-axis wrap calls through closure-maps (map((g, bc) -> _policy_axis(g, bc, store), …) and the inner-ctor _store_axis form). On Julia 1.12, codegen leaves those per-axis calls dynamic for Range axes: runtime sparam computation plus a heap box of the isbits _CachedRange return — 416 B per 2-D build, paid by every ND forward and one-shot-integrate build with Range axes since #194. Vector axes return heap pointers and stay clean, which is why the leak was Range-only. Eval was never affected — only the build.

Fix

Replace the closure-maps with @generated unrolled helpers — _policy_axes, _store_axes (core + method-aware hetero arm), _convert_cache_axes — following the existing _cache_axes_pooled / _resolve_axes pattern. All 11 call sites converted: the 5 store-policy maps from #194 plus 6 legacy _convert_copy(_cache_axis(…)) inner ctors carrying the same shape.

Build allocation, copy=false (Julia 1.12, function-barrier measured)

build path Vector Range (before) Range (after)
1D forward / one-shot integrate 0 B 0 B 0 B
ND forward build+eval 0 B 416 B 0 B
ND one-shot integrate 0 B 416 B 0 B

Tests — three-layer pin (TDD, red-first)

  1. Zero-alloc build contract (committed red first): forward and one-shot integrate, 1D/ND × Vector/Range, 0 B on Julia ≥ 1.12.
  2. Unit pins for the @generated helpers on Range axes — covers the hetero arm, whose end-to-end build has inherent allocations and cannot be pinned at zero.
  3. Source lint: closure-maps over the axis-wrap family are banned in src/ (bare-function maps stay legal), so the pattern cannot silently return at any current or future call site. Red at introduction against the 6 legacy ctors, green after conversion.

Also carries a small docs rider: a canonical @docs block for integrate / cumulative_integrate so makedocs checkdocs = :exports passes.

mgyoo86 added 4 commits July 20, 2026 12:12
…ange)

With store=StorePolicy(copy=false, cache_axis=false) a trivial build must
allocate nothing on Julia >= 1.12, measured through function barriers for
1D/ND x Vector/Range x forward-eval/one-shot-integrate. Every cell holds
except ND Range axes, which leak a constant 416 B of build-time boxing —
pinned RED ahead of the fix. Existing suites only asserted the relative
raw < copying-build ordering, which let this absolute leak slip through.
…grate

PR #194/#195 added docstrings for integrate, cumulative_integrate, and
cumulative_integrate! but no canonical @docs block, so makedocs with
checkdocs = :exports failed with :missing_docs. Collect all three in an
API Reference section on the 1D integration page.
…+ unit pins

Lint testitem scans src/ and bans `map((…) -> _policy_axis/_store_axis/
_cache_axis(…), …)` closure-maps — the recurring alloc trap; RED at this
commit against six legacy `_convert_copy(_cache_axis(…))` inner ctors.
Also unit-pins the @generated axis-map helpers (including the hetero arm,
whose end-to-end build has inherent allocations and cannot be pinned at
zero) as zero-alloc on Range axes.
…zero

The cache_axis work (#194) mapped per-axis wrap calls through closures
capturing `store` (`map((g, bc) -> _policy_axis(g, bc, store), …)` and the
inner-ctor `_store_axis` form with an `ntuple(_ -> Tg)` witness). On Julia
1.12 codegen leaves those per-axis calls dynamic for Range axes: runtime
sparam computation plus a heap box of the isbits `_CachedRange` return —
416 B per 2-D build, paid by every forward and one-shot-integrate build
with Range axes (Vector axes return heap pointers and stay clean, which
is why the leak was Range-only).

Replace the closure-maps with @generated unrolled helpers — `_policy_axes`,
`_store_axes` (core + method-aware hetero arm), `_convert_cache_axes` —
following the `_cache_axes_pooled` / `_resolve_axes` pattern, and convert
all 11 call sites (5 store-policy maps + 6 legacy `_convert_copy` ctors),
turning the source lint green. Zero-alloc build contract now holds across
1D/ND x Vector/Range x forward/one-shot integrate on Julia >= 1.12.
@github-actions

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 (56 total, click to expand)
Benchmark Current: b036be3 Previous Imm. Ratio Grad. Ratio
10_nd_construct/bicubic_2d 38327 ns 37855.0 ns 1.012 0.975
10_nd_construct/bilinear_2d 746.7 ns 736.5 ns 1.014 1.034
10_nd_construct/tricubic_3d 327204 ns 329174.0 ns 0.994 0.991
10_nd_construct/trilinear_3d 2124.36 ns 2058.6 ns 1.032 1.058
11_nd_eval/bicubic_2d_batch 1542.2 ns 1548.2 ns 0.996 0.999
11_nd_eval/bicubic_2d_scalar 17.72 ns 17.7 ns 1.0 1.0
11_nd_eval/bilinear_2d_scalar 7.71 ns 7.7 ns 1.0 1.0
11_nd_eval/tricubic_3d_batch 3524.2 ns 3538.2 ns 0.996 0.998
11_nd_eval/tricubic_3d_scalar 37.66 ns 37.6 ns 1.0 1.005
11_nd_eval/trilinear_3d_scalar 13.52 ns 13.5 ns 1.0 0.998
12_cubic_eval_gridquery/range_random 4643.66 ns 4642.6 ns 1.0 1.0
12_cubic_eval_gridquery/range_sorted 4657.48 ns 4641.6 ns 1.003 1.003
12_cubic_eval_gridquery/vec_random 10089.52 ns 10029.6 ns 1.006 1.005
12_cubic_eval_gridquery/vec_sorted 3190.32 ns 3185.9 ns 1.001 1.005
13_nd_oneshot_gridquery/bicubic_2d_rand_rand 66715.7 ns 66951.6 ns 0.996 1.027
13_nd_oneshot_gridquery/bicubic_2d_sort_rand 62487.5 ns 62708.3 ns 0.996 1.023
13_nd_oneshot_gridquery/bicubic_2d_sort_sort 60668.7 ns 60510.1 ns 1.003 1.028
13_nd_oneshot_gridquery/bilinear_2d_rand_rand 17558.78 ns 17687.7 ns 0.993 1.031
13_nd_oneshot_gridquery/bilinear_2d_sort_rand 8947.62 ns 10174.6 ns 0.879 0.93
13_nd_oneshot_gridquery/bilinear_2d_sort_sort 4523.1 ns 4690.1 ns 0.964 0.986
14_series_oneshot_batch/constant_inplace_vec_k8_q1000_rand 16041.7 ns 15827.3 ns 1.014 0.851
14_series_oneshot_batch/linear_inplace_vec_k8_q1000_rand 14495.5 ns 16462.2 ns 0.881 0.8
15_gridded_query/cubic_oneshot_2d_40x32 45563.4 ns 44201.1 ns 1.031 1.009
15_gridded_query/cubic_persistent_2d_40x32 8106.9 ns 8122.9 ns 0.998 0.996
15_gridded_query/linear_oneshot_2d_40x32 1569.52 ns 1576.1 ns 0.996 0.997
15_gridded_query/linear_oneshot_3d_12x10x8 1988.34 ns 2009.4 ns 0.99 0.976
15_gridded_query/linear_persistent_2d_40x32 1535.28 ns 1540.9 ns 0.996 0.975
15_gridded_query/linear_persistent_3d_12x10x8 1953.68 ns 2009.8 ns 0.972 0.998
1_cubic_oneshot/q00001 502.74 ns 498.3 ns 1.009 0.921
1_cubic_oneshot/q10000 47779.7 ns 47820.4 ns 0.999 0.998
2_cubic_construct/g0100 1469.38 ns 1447.5 ns 1.015 0.975
2_cubic_construct/g1000 14053.8 ns 14149.9 ns 0.993 0.995
3_cubic_eval/q00001 23.73 ns 23.8 ns 0.996 0.989
3_cubic_eval/q00100 483.12 ns 481.9 ns 1.003 1.0
3_cubic_eval/q10000 47072.6 ns 47035.2 ns 1.001 1.001
4_linear_oneshot/q00001 28.64 ns 28.9 ns 0.99 1.001
4_linear_oneshot/q10000 19490.8 ns 19526.7 ns 0.998 1.0
5_linear_construct/g0100 35.76 ns 36.0 ns 0.992 0.96
5_linear_construct/g1000 274.31 ns 303.0 ns 0.905 1.019
6_linear_eval/q00001 10.02 ns 10.6 ns 0.944 0.994
6_linear_eval/q00100 198.3 ns 198.9 ns 0.997 0.994
6_linear_eval/q10000 19280.5 ns 19285.4 ns 1.0 1.0
7_cubic_range/scalar_query 9.31 ns 9.3 ns 1.0 1.0
7_cubic_vec/scalar_query 10.41 ns 10.5 ns 0.99 1.0
8_cubic_multi/construct_s001_q100 563.22 ns 574.6 ns 0.98 0.865
8_cubic_multi/construct_s010_q100 4718.78 ns 4840.9 ns 0.975 0.965
8_cubic_multi/construct_s100_q100 44142.3 ns 45026.3 ns 0.98 0.993
8_cubic_multi/eval_s001_q100 632.34 ns 626.1 ns 1.01 0.836
8_cubic_multi/eval_s010_q100 1533.48 ns 1537.7 ns 0.997 0.88
8_cubic_multi/eval_s010_q100_scalar_loop 2515.32 ns 2517.7 ns 0.999 0.998
8_cubic_multi/eval_s100_q100 10681.8 ns 10698.8 ns 0.998 0.936
8_cubic_multi/eval_s100_q100_scalar_loop 3738.5 ns 3935.8 ns 0.95 1.04
9_nd_oneshot/bicubic_2d 38843.5 ns 38954.4 ns 0.997 0.867
9_nd_oneshot/bilinear_2d 458.86 ns 459.5 ns 0.999 0.987
9_nd_oneshot/tricubic_3d 332390.7 ns 333636.4 ns 0.996 0.887
9_nd_oneshot/trilinear_3d 886.3 ns 895.3 ns 0.99 0.991

✅ No regressions detected

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

This comment was automatically generated by Benchmark workflow.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.33%. Comparing base (4a95d61) to head (b036be3).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #196      +/-   ##
==========================================
+ Coverage   96.30%   96.33%   +0.02%     
==========================================
  Files         164      164              
  Lines       13542    13554      +12     
==========================================
+ Hits        13042    13057      +15     
+ Misses        500      497       -3     
Files with missing lines Coverage Δ
src/constant/nd/constant_nd_adjoint_types.jl 91.66% <100.00%> (ø)
src/constant/nd/constant_nd_interpolant.jl 100.00% <100.00%> (ø)
src/constant/nd/constant_nd_types.jl 100.00% <100.00%> (ø)
src/core/nd_utils.jl 89.81% <100.00%> (+0.08%) ⬆️
src/core/store_policy.jl 92.10% <100.00%> (+1.48%) ⬆️
src/cubic/nd/cubic_nd_types.jl 100.00% <100.00%> (ø)
src/hermite/nd/hermite_nd_types.jl 100.00% <100.00%> (ø)
src/hetero/hetero_types.jl 100.00% <100.00%> (ø)
src/linear/nd/linear_nd_adjoint_types.jl 92.30% <100.00%> (ø)
src/linear/nd/linear_nd_interpolant.jl 100.00% <100.00%> (ø)
... and 3 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mgyoo86
mgyoo86 merged commit 0f6c4e8 into master Jul 20, 2026
25 checks passed
@mgyoo86
mgyoo86 deleted the fix/nd-range-build-alloc branch July 20, 2026 21:11
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