Skip to content

Add configurable cache modes and fallback policies for non-isbits types#34

Merged
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:cr/cached-fallback-policy
Mar 30, 2026
Merged

Add configurable cache modes and fallback policies for non-isbits types#34
ChrisRackauckas merged 2 commits into
SciML:mainfrom
ChrisRackauckas-Claude:cr/cached-fallback-policy

Conversation

@ChrisRackauckas-Claude
Copy link
Copy Markdown
Contributor

Summary

Design

Two orthogonal configuration axes replace the boolean FB type parameter:

Cache modes (how fallback FunctionWrappers are cached):

Mode Behavior Allocs per call
NoCache Dynamic dispatch via obj[](arg...) every call 1
SingleCache Lazily cache a FunctionWrapper for last-seen arg types 0 after first
DictCache Cache per arg type in a Dict 0 after first per type

Fallback policies (when fallback is allowed):

Policy Behavior
Strict Always error on mismatch (= legacy Val{false})
AllowAll Always fall back (= legacy Val{true})
AllowNonIsBits Fall back for non-isbits args; error for isbits mismatches

The key mechanism: on first fallback hit, a FunctionWrapper{Any, ActualArgTypes} is created and cached. Subsequent calls use the cached wrapper's ccall path — zero extra allocations, no type parameter on the function (no unwanted specialization).

Benchmark results (from SciML/SciMLBase.jl#1301)

Type Problem Direct NoCache (+1 alloc) SingleCache (0 alloc)
BigFloat Lorenz N=3 565 ns, 8 allocs 627 ns, 9 allocs 607 ns, 8 allocs
GradientTracer Lorenz N=3 638 ns, 24 allocs 694 ns, 25 allocs 665 ns, 24 allocs
BigFloat Heat N=100 101.6 μs, 498 allocs 101.7 μs, 499 allocs 101.3 μs, 498 allocs

Float64 isbits match path: all configurations identical at ~19.5 ns, 0 allocs. No regression.

Backward compatibility

Legacy APIs are preserved and map to the new system:

  • FunctionWrappersWrapper(f, args, rets, Val{false}())Strict + NoCache
  • FunctionWrappersWrapper(f, args, rets, Val{true}())AllowAll + NoCache
  • FunctionWrappersWrapper{FW, false}(fwt)Strict + NoCache
  • FunctionWrappersWrapper{FW, true}(fwt)AllowAll + NoCache

Test plan

  • All existing tests pass (60 tests)
  • New tests for all 3 fallback policies (Strict, AllowAll, AllowNonIsBits)
  • New tests for all 3 cache modes (NoCache, SingleCache, DictCache)
  • Legacy API backward compatibility tests (Val{true}/Val{false}, FWW{FW,Bool})
  • AllowNonIsBits correctly allows BigFloat/tracers, rejects Float32 mismatches
  • SingleCache thrashing test (alternating types still works)
  • Runic formatting check passes

🤖 Generated with Claude Code

ChrisRackauckas and others added 2 commits March 30, 2026 05:34
Replaces the boolean `FB` type parameter with two orthogonal configuration axes:

Cache modes (how fallback FunctionWrappers are cached):
- NoCache: dynamic dispatch every call (1 alloc, legacy behavior)
- SingleCache: cache one FunctionWrapper for last arg types (0 alloc on hit)
- DictCache: cache per arg type in a Dict (0 alloc, multi-type)

Fallback policies (when fallback is allowed):
- Strict: always error on mismatch (legacy Val{false})
- AllowAll: always fall back (legacy Val{true})
- AllowNonIsBits: fall back only for non-isbits types, error for isbits mismatches

Default is SingleCache + AllowNonIsBits, which provides zero-allocation
fallback for BigFloat, SparseConnectivityTracer, and similar non-isbits types
while still catching isbits type mismatches as bugs.

The key mechanism is lazily creating and caching a FunctionWrapper for the
actual argument types on first fallback hit. Subsequent calls use the cached
wrapper's ccall path with no extra allocations.

Legacy API (Val{true}/Val{false} and FunctionWrappersWrapper{FW,Bool})
is preserved for backward compatibility, mapping to AllowAll/Strict + NoCache.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Breaking change: removes the boolean FB type parameter and Val{true}/Val{false}
API. The FunctionWrappersWrapper type now takes a fallback policy and cache
storage type as type parameters instead.

Cache modes (how fallback FunctionWrappers are cached):
- NoCache: dynamic dispatch every call (1 alloc per call)
- SingleCache: lazily cache a FunctionWrapper for last-seen arg types (0 alloc)
- DictCache: cache per arg type in a Dict (0 alloc, multi-type)

Fallback policies (when fallback is allowed):
- Strict: always error on type mismatch
- AllowAll: always fall back to original function
- AllowNonIsBits: fall back for non-isbits types, error for isbits mismatches

Default: SingleCache + AllowNonIsBits

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChrisRackauckas ChrisRackauckas merged commit 86cbba9 into SciML:main Mar 30, 2026
6 of 7 checks passed
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/SciMLBase.jl that referenced this pull request Mar 30, 2026
FunctionWrappersWrappers v1.0.0 (SciML/FunctionWrappersWrappers.jl#34)
adds configurable cache modes and fallback policies for non-isbits types.
This is non-breaking — it strictly allows more types through the wrapper.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChrisRackauckas ChrisRackauckas mentioned this pull request Mar 30, 2026
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