fix(nd): value-matched grid promotion#182
Conversation
- test_nd_store_copyfalse_narrow_float.jl: StorePolicy(copy=false) must alias narrow-float ND data (Float32/Float16/ComplexF32) — itp.data === A, no O(n^2) widening copy at build; Float64/ComplexF64 aliasing pinned as guards. - test_nd_output_type_promotion.jl: persistent ND eval output follows promote_type(grid, data, query) with @inferred stability across linear/cubic/quadratic/constant x grid containers x Float32/Float64. Red at this commit: the ND builders derive the grid float from the grid eltype alone (Int/OneTo -> Float64) and widen the data to match before the store policy is consulted.
…rids Tg = float(_promote_grid_eltype(grids)) floated an Int/OneTo grid to Float64 regardless of the data eltype, dragging Tv — and the stored array, via Tv.(data) — up with it before StorePolicy was consulted: copy=false was silently violated with a full O(n^N) widening copy for Float32/ Float16/ComplexF32 data, and copy=true paid a double copy (widen + own). Use the 1D rule _promote_grid_float(_promote_grid_eltype(grids), Tv_raw): Float32 data keeps a Float32 grid so the data aliases under copy=false; Int data still floats to Float64; duck/colorant types unaffected. fast_imresize (ImageTransformations) bare-Float32 1024^2 -> 256^2 downsample: 2354us / 8.4MB -> 139us / 256KB, values within Float32 round-off.
- test_nd_output_type_promotion.jl: one-shot testitem — scalar one-shot output follows promote_type(grid, data, query) with @inferred stability (linear/cubic/quadratic/constant x grid containers x Float32/Float64). - test_nd_raw_grid_oneshot.jl: quadratic scalar strategy contract — both AutoCoeffs (-> OnTheFly) and explicit PreCompute zero-alloc on Int Vector grids; bit-exact persistent parity (===) is opt-in via coeffs=PreCompute(), the default pins ~ rtol=1e-12. - test_nd_oneshot_onthefly.jl: quadratic seed identity — default == explicit OnTheFly bit-exactly, ~ PreCompute within FP-reordering noise. Red at this commit: scalar one-shots float Int grids to Float64 regardless of the data eltype, and quadratic hardcodes AutoCoeffs -> PreCompute.
…Coeffs per policy
Scalar one-shots derived the grid float from the grid eltype alone, so an
Int/OneTo grid beside Float32 data evaluated in Float64 — throwing the ::Tr
witness assert (linear/cubic/quadratic) or silently returning Float64
(constant). Value-match via _promote_grid_float(grid eltype, Tv):
- linear/cubic/quadratic/constant scalar witnesses + 3-arg Tg-aware
_resolve_axis (Range converts to _CachedRange{Tg}; raw Vector stays raw —
the search promote-compares it).
- OnTheFly hetero core: value-matched Tg for pool buffers. The global-solve
path keeps RAW-form grids (inner 1D one-shots wrap :exclusive axes
themselves; the wrapped virtual n+1 vs data n asserts) and converts only
float-mismatched axes — Int+Float64 stays raw, preserving the 1D cubic
cache memoization.
- quadratic scalar AutoCoeffs -> _resolve_coeffs_nd_oneshot (same policy as
cubic): scalar -> OnTheFly (1.5-3.5x faster, 0-5 ULP vs PreCompute),
batch -> PreCompute. Bit-exact persistent parity (=== itp(q)) is opt-in
via coeffs=PreCompute().
- _cache_axis_pooled 3-arg Vector overloads: converting copyto! into a
POOLED buffer (zero heap; acquire! returns a plain Vector so the 2-arg
wrap aliases it). The Range 3-arg overloads are load-bearing dispatch
shields: AbstractRange <: AbstractVector would otherwise send Ranges into
the pooled-Vector overload and lose the stack _CachedRange form.
Hermite testitem: output follows promote_type(grid, data-and-partials, query) with @inferred stability across grid containers x Float32/Float64. Red at this commit: the one-shot prepare floats an Int/OneTo grid to Float64 regardless of the data/partials eltype (inv(Int) in _coeff_op), silently returning Float64 and copying data + K partials every call.
…cision
_hermite_oneshot_prepare derived Tg from the grid eltype alone, so an Int/
OneTo grid beside Float32 data + partials promoted Tv to Float64 via
_coeff_op's inv(Int) — coercing (copying) the data and all K partials on
every one-shot call and silently returning Float64. Two independent Float64
sources fixed:
- Tg value-matched to promote_type(eltype(data), Tv_part): Tv stays narrow,
the _coerce_*_eltype calls pass through (no per-call copies).
- Float-mismatched axes convert to Tg (Int grid + Float32 -> _CachedRange{F32})
so the cell widths (inv(h)) don't reintroduce Float64; matching axes stay
raw (Int + Float64 unchanged; Dual axes never convert).
- Batch witness Tr uses the same value-matched Tg, agreeing with the eval.
Persistent hermite was already covered by the central _nd_promote_grids fix.
coeffs=PreCompute() routes through the 1D _get_cubic_cache machinery, which is still data-unaware (floats an Int grid to Float64); the value-matched witness then rejects the Float64 eval with a TypeError. Default (OnTheFly) is green. The test_broken flips when the 1D one-shot value-match phase lands, forcing the pin update.
…sion bridge
Type the per-query cell widths instead of converting whole axes: a new
data-aware _compute_all_local_params(..., Tg) overload converts hs/inv_hs
to the caller-supplied value-matched Tg (Int grid + Float32 data → Float32),
so raw Int axes no longer widen the eval to Float64 via inv(Int). dLs keep
their natural q - L promotion — converting them would strip Dual-query
partials. The 4-arg form delegates with its previous grid-eltype derivation
(bit-identical for existing callers).
Hermite cores compute Tg once and pass it to the params call and to
_pack_and_extend_nodal_derivs_pooled (new ::Type{Tg} parameter with the old
data-blind default), so an :exclusive extension can no longer reintroduce
Float64 into dL. The prepare-level grids_eff conversion bridge is removed —
grids flow raw end-to-end; widths cost O(N) scalars per query instead of an
O(n) axis conversion (and no per-call allocation for Int Vector grids).
The hetero global-solve bridge stays: inner 1D one-shots re-derive their
types from raw grid eltypes internally, which width-typing at the ND level
cannot reach (resolved by the 1D value-match phase).
The matrix covered Int ranges (OneTo) but not Int Vectors — which is exactly where the last hole hid: 322 rows, one red (linear one-shot IntVec F32xF32).
The raw-Vector alpha leaf ((q - L) / float(R - L)) and the raw-Vector _get_inv_h fallback (inv(xR - xL)) are data-blind: an Int Vector axis beside Float32 data leaked Float64 into the eval, throwing the value-matched ::Tr witness. Convert inv_h to the value-matched Tg at the read and compute alpha = (q - L) * inv_h from it (the persistent path's 3-arg form): - _CachedRange axes: bit-identical (the 4-arg alpha already multiplied by the cached inv_h; convert folds). - Raw-Vector / seam cells: <= 1 ULP (division -> reciprocal multiply); exact for power-of-two widths, so the unit-spacing === pins hold. - alpha keeps query-blood promotion (Dual query => Dual alpha); the seam cell now shares the deriv path's denominator by construction.
…ion stays natural Arithmetic kernels (linear/cubic/quadratic) divide, so Int data floats: Tg = float(promote(grid, Int)) — Int grid + Int data -> Float64 for every query type, while a Float32 grid keeps Float32 (Int cannot widen it). The selection kernel (constant) has no x*y arithmetic: output follows the pure promote_type(grid, data, query) with NO float forcing — all-Int in, Int out — and one-shot must match persistent. Red at this commit: the constant scalar one-shot applied the arithmetic value-matched (float-forcing) Tg, so an IntOneTo axis converted to Float64 and diverged from the persistent/batch selection contract.
…ernel rule) The scalar one-shot applied the arithmetic float-forcing value-match to a selection kernel, converting an IntOneTo axis to Float64 — diverging from the persistent/batch paths (_nd_promote_grids_raw) and from the IntVec container. Constant has no x*y arithmetic: use the raw promoted grid eltype so all-Int inputs stay Int and the output follows the natural promote_type(grid, data, query), identical across one-shot/persistent and grid containers.
… the Tg axis
- `_Cached{Range,Vector}` x `PeriodicBC{:exclusive}` x Tg diagonal methods:
the specific-container and specific-BC arms cross-ambiguated without them
(Aqua RED + MethodError in the hetero one-shot exclusive path).
- Convert-first in the Range exclusive arms (`_resolve_axis` raw/diagonal,
`_cache_axis` raw): `_resolve_bc_period` types the period from the axis
eltype, so resolving against the pre-conversion axis let a Float64 period
re-widen a value-matched Float32 axis through the EPA seam promote.
- Pin the full 3-arg dispatch table + the period-follows-Tg contract in
test_axis_data_resolvers.jl.
FastInterpolations.jl Benchmarks
All benchmarks (50 total, click to expand)
|
| Benchmark | Current | Previous | Imm. Ratio | Grad. Ratio | Tier |
|---|---|---|---|---|---|
10_nd_construct/bilinear_2d |
638.38 ns |
577.1 ns |
1.106 |
1.039 |
immediate |
Thresholds: immediate > 1.1x (vs latest master), gradual > 1.1x (vs sliding window)
Runner:
znver3|4c— AMD EPYC 7763 64-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 Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #182 +/- ##
==========================================
- Coverage 96.85% 96.39% -0.47%
==========================================
Files 151 151
Lines 12609 12848 +239
==========================================
+ Hits 12213 12385 +172
- Misses 396 463 +67
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes ND interpolation type promotion so that integer grids are floated in a data-aware (value-matched) way instead of always becoming Float64, preventing unwanted widening/copies for narrow-float (e.g. Float32 / Float16 / ComplexF32) inputs and aligning persistent vs one-shot ND output eltypes with the natural promote_type(grid, data, query) contract.
Changes:
- Introduces value-matched ND grid float promotion (
Intgrid +Float32data ⇒Float32grid) across ND builders and one-shot paths. - Refactors ND one-shot axis resolution and local-parameter computation to preserve narrow types (and avoid
inv(Int)-drivenFloat64leakage). - Adds/updates targeted tests to pin zero-copy
StorePolicy(copy=false), output-type matrices (@inferred), and key axis resolver dispatch behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/test_nd_store_copyfalse_narrow_float.jl | New tests pin zero-copy behavior and prevent narrow-float widening under StorePolicy(copy=false) for ND builds. |
| test/test_nd_raw_grid_oneshot.jl | Updates quadratic ND one-shot tests for new coeffs policy (scalar AutoCoeffs ⇒ OnTheFly) and raw-grid allocation/type parity. |
| test/test_nd_output_type_promotion.jl | New tests pin the ND output-type contract (promote_type(grid, data, query)) for persistent + one-shot across methods and grid kinds. |
| test/test_nd_oneshot_onthefly.jl | Adjusts quadratic AutoCoeffs expectations to reflect scalar OnTheFly default and PreCompute ≈ parity. |
| test/test_axis_data_resolvers.jl | Adds dispatch-table tests to pin Tg-aware _resolve_axis behavior (including ambiguity-preventing diagonals). |
| src/quadratic/nd/quadratic_nd_oneshot.jl | Applies value-matched grid float type in pooled axis caching and adopts shared ND coeffs resolution policy for one-shot. |
| src/linear/nd/linear_nd_oneshot.jl | Resolves axes with Tg-aware _resolve_axis, and types inv_h/α consistently to prevent Float64 leakage from Int grids. |
| src/hetero/hetero_oneshot.jl | Uses value-matched Tg for axis resolution and carefully converts only float-mismatched raw grids for global-solve collapse. |
| src/hermite/nd/hermite_nd_oneshot.jl | Value-matches Tg against data∪partials, threads Tg into pooled pack/extend and local-param computation. |
| src/hermite/nd/hermite_nd_build.jl | Extends pooled pack/extend helper to accept caller-supplied Tg for exclusive periodic extension typing. |
| src/cubic/nd/cubic_nd_oneshot.jl | Uses value-matched Tg for ND cubic scalar one-shot type behavior to align witness/eval. |
| src/core/nd_utils.jl | Adds Tg-supplied _compute_all_local_params overload and switches ND grid promotion to value-matched _promote_grid_float. |
| src/core/cached_vector.jl | Improves pooled axis caching for mismatched vector eltypes and adds Tg-aware _resolve_axis overloads for vectors. |
| src/core/cached_range.jl | Adds Tg-aware _resolve_axis/_cache_axis_pooled for ranges and convert-first exclusive-period handling to preserve Tg. |
| src/constant/nd/constant_nd_oneshot.jl | Ensures constant ND one-shot keeps natural promotion (no arithmetic float-forcing) via raw grid-eltype Tg. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…partials Two review findings on the value-match arc: - cubic scalar `coeffs=PreCompute()`: the value-matched wrapper witness (F32) vs the legacy-width backend (F64, identity-memoised data-unaware `_get_cubic_cache`) made a documented public call throw TypeError for Int grids + Float32 data. The PreCompute branch now asserts the backend's own width (returns Float64, master parity) until the 1D cubic-cache value-match phase lands; the @test_broken type pin flips then. - hermite ND persistent build value-matched grids against data only, while the one-shot promotes against data ∪ partials: Float32 data + Float64 partials stored Int coordinates as Float32 (quantized above 2^24, persistent drifted 9.5e-5 from one-shot). New 3-arg `_nd_promote_grids(grids, data, Tv_extra)` folds the partial eltype into the value-match; persistent is now bit-identical to one-shot. Pins: hard no-throw + OnTheFly-parity regression guard (plain @test — the @test_broken had swallowed the throw), mixed-width parity testitem, and an all-Float32 no-over-widen guard.
…ver Tg divergence RED: on a raw Vector grid under an :exclusive PeriodicBC with narrow-float data, the value-matched linear ND one-shot threw (Float64 axis vs Float32 witness) while persistent returned Float32, and _resolve_axis diverged from _cache_axis (Float64 vs Float32 axis). Also flips the _CachedVector x :exclusive diagonal test to the convert-to-Tg contract, matching the _CachedRange diagonal.
The one-shot _resolve_axis :exclusive Vector arms resolved the period against the RAW grid (float(Int)=Float64), so a value-matched Tg=Float32 axis re-widened to Float64 past the Float32 witness -> one-shot crash / type divergence from the persistent path. Add a shared _wrap_exclusive(inner, bc) that always resolves the period against the already-converted inner, and route every :exclusive arm (Range+Vector, one-shot+persistent, 2-/3-arg) through it. The Vector 3-arg arms now convert-first to Tg; the 12 other arms are behavior-preserving. Closes the 2-arg/3-arg period-timing split.
… Int->Float64 consistency Worktree ablation at the pre-fix commit confirmed both linear AND cubic one-shot crashed (Float64 axis vs Float32 witness) on an Int Vector + :exclusive PeriodicBC + Float32 data; the pin was previously linear-only. Also locks the selection-kernel contract: constant's exclusive axis floats Int->Float64 for both one-shot and persistent (consistent, not the arithmetic value-match) so a future change can't silently split them. Hermite uses a separate _pack_and_extend path, out of scope here.
… _resolve_axis Copilot review on PR #182: the 3-arg (and 2-arg) :exclusive Vector arms match _ExclusivePeriodicAxis (it is <: AbstractVector) and re-wrap it, nesting toward length (n+1)+1 which throws in the ctor. _cache_axis already defends this; _resolve_axis only had the 1-arg passthrough.
… (no double-wrap) Add the 2-arg and 3-arg :exclusive _ExclusivePeriodicAxis passthroughs to _resolve_axis, mirroring the _cache_axis set and the documented dispatch-table contract (_ExclusivePeriodicAxis + any -> passthrough). Non-exclusive 2-/3-arg forms already fall to the raw = x passthrough, so only the :exclusive arms needed the guard.
…ion) Julia 1.10/1.11 elide type-stable OnTheFly ND intermediates unreliably: the same warm path stack-allocates in isolation but heap-allocates (~1-2 KB) in the full-suite compilation context, so @allocated on LTS measures Julia's elision rather than FI's zero-alloc contract. All three LTS jobs (ubuntu/windows/macos) fail while every 1.12 job passes. A 100x LTS budget absorbs the elision noise while still catching a catastrophic regression; 1.12+ stays strict (0). Not AAP auto-manage: compact! is a no-op on <1.12, and a 30s wall-clock timer would fail 1.12 too.
…ange axes
Re-lands the 1D value-match phase (dropped in a branch rework) and extends it.
1D one-shot axes were resolved data-blind (`float(eltype(x))` → Float64 for Int
grids) while the persistent path was already value-matched:
- New Tg-typed 2-arg `_resolve_axis(x, ::Type{Tg})` (Ranges → `_to_float(x, Tg)`,
vectors pass through) for the no-BC normalize points; linear/constant thread
the BC 3-arg form (constant keeps the raw natural selection rule); quadratic
uses the pooled 3-arg (Int vectors convert into pool buffers); hermite
promotes against y ∪ dy via the extracted `_hermite_grid_float`.
- Cubic pre-normalizes to `_CachedRange{Tg}` — isbits axes have value-
deterministic objectid, so the spline-cache memoisation survives.
ND scalar `coeffs=PreCompute()` now value-matches Range axes the same way; the
witness follows the per-axis width (`_cubic_pc_grid_float`), so all-Range grids
return Float32 (the `@test_broken` flipped to hard tests) and raw-Vector axes
stay well-typed at the legacy cache width.
Int-VECTOR axes for linear/hermite close via canonical compile-time cell-
geometry types: a 1-arg witness op `_inv_op` with `_promote_eltype` computes the
reciprocal-spacing type at the function surface (`Tinv = _promote_eltype(
_inv_op, _promote_grid_float(Tg, Tv))`), and use sites just `convert` — a no-op
for float/duck/Unitful axes (inverse units convert to themselves), a real
Float64→value-width float only for Int axes. α is now uniformly (q−L)·inv_h,
matching the persistent and ND `_locate_cell` forms.
Pins: 1D output-type matrix (scalar `@inferred` × 5 gridspecs, persistent
parity, batch eltypes, constant natural, hermite y∪dy, Int-data, Series guard).
Remaining `@test_broken` ×5 (all pre-existing behavior): 1D Int-vector
cubic/pchip/akima/cardinal + ND cubic PreCompute Int-vector axis.
7909b54 to
1d965a3
Compare
…r backend
Replace the per-axis witness helper (`_cubic_pc_grid_float`) with the same
pooled 3-arg wrap the quadratic scalar backend uses: Ranges → isbits
`_CachedRange{Tg}` (spline caches memoise via value-deterministic objectid),
mismatched Vectors convert into pool buffers (warm one-shots stay zero-alloc).
The whole solve pipeline then runs at the value-matched width, so the wrapper
asserts plain `Tr` and Int-VECTOR axes now return Float32 too — the last ND
cubic PreCompute `@test_broken` becomes a hard test.
The value-match arc resolved grid floats via `map(g -> f(g, Tg), grids)` closures capturing the type witness Tg. A closure field of type `Type` de-optimizes on Julia LTS (weak const-prop), heap-allocating downstream per-fiber/per-query eval (60-90 KB windowed OnTheFly, 0.4-8 KB scalar PreCompute) while 1.12 elides it entirely. Thread Tg as a plain map argument via `ntuple(_ -> Tg, Val(N))` across the 6 one-shot sites (hetero OnTheFly + raw-form bridge, cubic/quadratic PreCompute grid wrap, linear resolve+inv_h, constant resolve). Behavior-identical: Int-Range/OneTo + Float32 data still returns Float32; verified LTS warm-path alloc returns to 0 (master parity). Restore ND_ALLOC_THRESHOLD to the master 240-byte margin (the earlier 100x inflation was masking these real regressions).
Consistency follow-up to 71cff11: the same `map((g,bc) -> f(g,bc,Tg), ...)` Tg-capturing-closure pattern lived in 10 persistent constructors (*_nd_types.jl, *_adjoint_types.jl, quadratic_nd_interpolant.jl). These are build-once paths and the Interpolant struct re-concretizes the grid type at its boundary, so they were NOT a zero-alloc-contract violation (unlike the one-shot backends) — but uniform closure-free threading via ntuple(_ -> Tg, Val(N)) removes the anti-pattern everywhere. Behavior-identical (T === Tg): persistent build+eval matches one-shot across all 7 families + hetero + store paths.
…t to cubic Unit-pin the width-first `_get_h`/`_get_inv_h`/`_get_inv_2cell`/secant forms (span-then-convert-then-inv: a 2^24-coordinate Int axis must not cancel under endpoint conversion) and drop pchip/akima/cardinal from the IntVec known-red carve-out — only cubic (eltype-aware spline-cache key) remains deferred.
…rdinal one-shots Raw Int-Vector axes minted `inv(Int)::Float64` inside secants and cell geometry, silently widening Float32 one-shots (IntVec broken 4 -> 1; only cubic's eltype-aware spline-cache key remains deferred). - Leaf width-first forms `_get_h`/`_get_inv_h`/`_get_inv_2cell(Tw, x, i)`: raw axes difference in their OWN eltype, convert the SPAN, then divide — the reciprocal is born at `Tw` (single rounding; large-Int coordinates cannot cancel). Wrapped axes reuse the cached reciprocal via dispatch shields (`_CachedVector`/`_CachedRange`/`_ExclusivePeriodicAxis`). - Width-first secants + periodic primitives; the `:exclusive` seam resolves its period AT `Tw`, so a Float64 period cannot re-widen the weights. - hermite sm eval arms declare `Tw = _promote_grid_float(Tg, Tv)` and thread it through `_local_slope(Tw, ...)` and the kernel h/inv_h. Width-less `_local_slope` bodies are preserved verbatim (integrate/adjoint callers): eltype delegation would let an Int `Tw` reach the seam-period convert. - PreCompute backends type `Tdy` from the value-matched width and pass `Tw` into the bulk slope fillers (old kwarg forms delegate with `eltype(x)` — bit-identical; secant-fastpath suite unchanged). - cardinal scale typed `typeof(float(one(Tw)))` — dimensionless slot, Unitful-safe, and a Float64 default tension stops widening F32 axes.
…losures `ntuple(_ -> Tg, Val(N))` is itself a Tg-capturing closure: in a degraded- inference context (a CI worker after ~600 testitems) the tuple elements decay to `DataType` and every per-axis call goes through dynamic dispatch — the Julia 1.12 CI warm-alloc failures at exactly 32 B/axis (cubic/quad PreCompute 64/96 B), unreproducible in a fresh local process where Tg const-folds. The quadratic OnTheFly control (same pooled map, no Tg argument) stayed at 0 B on the same CI worker, isolating the Tg value flow as the cause. Replace the one-shot hot sites with @generated tuple helpers whose `Tg` is a STATIC signature parameter (`_convert_grids_typed` precedent): no closure and no runtime `Type` tuple on any Julia version, and a degraded context is bounded to one dispatch per call instead of per axis. - `_cache_axes_pooled(pool, grids, Tg)` — cubic/quadratic PreCompute scalar - `_resolve_axes(grids, bcs, Tg)` — linear/constant/hetero OnTheFly - `_bridge_axes_raw(grids, Tg)` — hetero global-solve bridge (the raw-vs-convert branch now decided per axis in the generator, from types) - `_convert_inv_hs(grids, idxs, Ls, Rs, Tg)` — linear ND scalar (keeps the convert-after form bit-for-bit) Persistent-constructor sites keep the ntuple form (build-time, once — benign). Verified: 1.12 raw-grid/promotion/hetero/OTF/allocation suites green; LTS temp-env probe improves PreCompute 112 B -> 48 B (residual 48 B floor is pre-existing and identical on untouched paths).
Harmonize the linear ND scalar one-shot onto the width-first geometry family and close the last narrow-float follow-ups. - Add a width-first search-result row `_get_inv_h(Tw, g, idx, xL, xR)` across the Vector / _CachedVector / _CachedRange / _ExclusivePeriodicAxis hierarchy: a raw axis spans `xR - xL` in its own eltype and divides at the value-matched `Tw` (span-first — an Int axis no longer mints Float64 via `inv(Int)`), while wrapped axes reuse their cached reciprocal. Rename the linear one-shot helper `_convert_inv_hs` -> `_typed_inv_hs`. A brute-force sweep over Int spans 2..2^24 shows convert-after and span-first agree bit-for-bit, so no value pin needs re-baselining. - Make the `_bridge_axes_raw` @generated helper extension-safe: its generator body ran `float(eltype(...))`, which on a `ForwardDiff.Dual` axis reaches a constructor the extension defines later -> a "method too new" world-age MethodError that broke every AD extension test. Decide keep/convert from a pure `Ti === Tg` identity first and only call `float()` on Base numeric eltypes. Generators must assemble expressions from Base-only operations. - Pin the Int-Vector-grid + Float32-data arms (Tg = Float32): the pooled PreCompute / linear wraps stay zero-alloc, and the OnTheFly collapse bridge's per-call heap convert is marked `@test_broken` (>= 1.12) until it acquires from the pool. Refresh the stale raw-grid comment (pooled wrap + autocache content-match, not objectid memoisation).
…Float32
A cubic one-shot on a raw `Vector{Int}` grid with `Float32` data returned
`Float64` — the cache is keyed by grid alone and an integer grid was hardcoded
to the `Float64` bank, so `cache.x` (and the solve) widened to Float64. The
persistent interpolant already value-matches (it converts the grid at build),
so scalar and batch one-shots were the inconsistent paths.
Route the cache by a value-matched float type instead. `_get_cubic_cache` gains
an optional `::Type{Tg}` argument (default reproduces the previous `float(T)`
behavior); when supplied, the Integer/Rational arms of the derivative and
periodic cache lookups land in the `Vector{Tg}` bank. The scalar, batch, and
periodic one-shot cores pass `Tg_eff = _promote_grid_float(grid, data)`, so an
Int grid beside Float32 data is cached and solved at Float32 — scalar, batch,
and persistent now agree bit-for-bit. Float / Range / Dual grids ignore `Tg`
(already value-typed or ephemeral). Series and adjoint keep the default float;
threading `Tg` there later is pure call-site wiring.
An Int grid used with both Float32 and Float64 data now occupies two banks
(type-separated, ring-bounded) — the inherent cost of a real Float32
factorization; the common single-precision case is unchanged.
Also delete the now-redundant `_bridge_axes_raw` helper: the hetero OnTheFly
caller already promotes every axis via `_nd_promote_grids`, so the per-axis
raw/convert split was a no-op. (The mixed-method hetero fallback's own eager
grid convert remains, pinned `@test_broken` as a follow-up.)
…ger convert)
The mixed-method hetero one-shot fallback promoted grids via `_nd_promote_grids`,
which eagerly converted every axis (`_convert_grids_typed` → per-axis `Tg.(x)`,
~176 B on an Int Vector axis) even though the result was already value-matched.
Split promotion into a type-only path. New `_nd_promote_types` returns
`(Tg, Tv, Tz)` from the grid/data types alone (no `_convert_grids_typed`). The
scalar and batch hetero dispatches use it and hand the OnTheFly branch the raw
grids — each inner 1D one-shot value-matches its own axis via the data-aware
cubic cache / width-first geometry. The PreCompute branch still gets converted
grids (`_interp_nd_hetero_oneshot` takes a homogeneous-eltype `Tg` tuple and
uses it directly). Grid validation runs on the raw grids (domain checks are
eltype-independent).
Every mixed combination (Cubic×{Linear,Cubic,Constant,Quadratic,Pchip,Cardinal,
Akima}, Linear×Pchip) now matches the all-Float32-grid result bit-for-bit with
the eager convert removed (~176 B → the shared ~48 B pool residual, zero on
1.12). PreCompute, batch, and Dual-grid paths are unchanged.
Summary
ND builders promoted grids data-blind (
float(eltype(grid))→ alwaysFloat64), which dragged narrow data (Float32/Float16/ComplexF32) up with them: a fullTv.(data)copy even understore=StorePolicy(copy=false), a double copy undercopy=true, andFloat64outputs whereFloat32was expected. Reported downstream by ImageTransformations'fast_imresize.This PR makes the grid float type value-matched to the data (Int grid + Float32 data → Float32 grid), applied uniformly across all ND methods on both persistent and one-shot paths.
copy=falsenow aliases the user's array (zero copies), andfast_imresizeFloat32 e2e goes 2354 µs / 8.4 MB → 139 µs / 256 KB.The rules (now pinned as tests)
promote_type(grid, data, query), with the grid entering value-matched. F32 grid + F32 data + F32 query → F32; F64 query → F64.Tests & notes
Output-type matrix (methods × grid kinds × data × query types, ~400 assertions, all
@inferred), zero-copy store pins, and axis-resolver dispatch-table pins. One@test_brokendocuments the known remaining gap (cubic explicit-PreComputewith Int grid + Float32 data), owned by the follow-up 1D one-shot value-match phase — the 1D methods still default to Float64 there and will get the same treatment in a separate PR. 13 commits (tests-first TDD history), 15 files, +480/−97.