Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache parameters #274

Merged
merged 28 commits into from
Jul 6, 2024
Merged

Conversation

SouthEndMusic
Copy link
Member

@SouthEndMusic SouthEndMusic commented Jun 26, 2024

Fixes #271 (the other half)

Checklist

  • Appropriate tests were added
  • Any code changes were done in a way that does not break public API
  • All documentation related to code changes were updated
  • The new code follows the
    contributor guidelines, in particular the SciML Style Guide and
    COLPRAC.
  • Any new documentation only uses public API

Additional context

Add any other context about the problem here.

@SouthEndMusic SouthEndMusic marked this pull request as draft June 26, 2024 13:29
@SouthEndMusic
Copy link
Member Author

SouthEndMusic commented Jun 26, 2024

That was a bit fiddly to get right (or at least pass the LinearInterpolation tests). Quick benchmark:

using DataInterpolations
using BenchmarkTools

N = 1000
u = rand(N)
t = cumsum(rand(N))

itp = LinearInterpolation(u, t)

t_eval = range(first(t), last(t), length = 10000)

@btime itp($t_eval)

# old: 162.200 μs (3 allocations: 78.23 KiB)
# new: 119.900 μs (3 allocations: 78.23 KiB)

@SouthEndMusic
Copy link
Member Author

Another benchmark:

using DataInterpolations
using BenchmarkTools

N = 1000
u = rand(N)
t = cumsum(rand(N))

itp = QuadraticInterpolation(u, t)

@btime DataInterpolations.integral(itp, t[end])

# old: 18.400 μs (3 allocations: 48 bytes)
# new:  9.000 μs (3 allocations: 48 bytes)

@SouthEndMusic
Copy link
Member Author

SouthEndMusic commented Jun 28, 2024

I'll leave it at caching parameters for LinearInterpolation, QuadraticInterpolation, QuadraticSplineInterpolation and CubicSplineInterpolation for this PR.

Interesting ones to cache parameters for next are BsplineInterpolation and BsplineApprox, as they now allocate at every call in spline_coefficients:

using DataInterpolations
using BenchmarkTools

N = 1000
u = rand(N)
t = cumsum(rand(N))

t = [0, 62.25, 109.66, 162.66, 205.8, 252.3]
u = [14.7, 11.51, 10.41, 14.95, 12.24, 11.22]
A1 = QuadraticInterpolation(u,t)
A2 = BSplineInterpolation(u, t, 2, :Uniform, :Uniform)
A3 = BSplineApprox(u,t, 2, 4, :Uniform, :Uniform)

ts = collect(range(first(t), last(t), length = 10000))

@btime A1.(ts)
# 91.100 μs (3 allocations: 78.25 KiB)

@btime A2.(ts)
# 394.500 μs (10003 allocations: 1.14 MiB)

@btime A3.(ts)
# 387.400 μs (10003 allocations: 1015.77 KiB)

Edit: I made the non-ForwardDiff calls of BSplineInterpolation and BSplineApprox non-allocating by computing the B-spline coefficients in preallocated memory. This gives problems with ForwardDiff as expected, which could be solved using PreallocationTools, but I'm not sure that's desirable.

@btime A2.(ts)
# 250.600 μs (3 allocations: 78.28 KiB)

@btime A3.(ts)
# 244.500 μs (3 allocations: 78.28 KiB)

@SouthEndMusic SouthEndMusic marked this pull request as ready for review June 28, 2024 18:28
@SouthEndMusic
Copy link
Member Author

Refactor of LagrangeInterpolation to be non-allocating and using idx_prev:

using DataInterpolations
using BenchmarkTools

N = 10
t = cumsum(rand(N))
u = rand(N)
itp = LagrangeInterpolation(u, t, N-1)

ts = range(first(t), last(t), length = 10000)

@btime itp($ts)
# old: 1.806 ms (20003 allocations: 2.82 MiB)
# new: 465.300 μs (3 allocations: 78.23 KiB)

@SouthEndMusic
Copy link
Member Author

Using knowledge of which spline coefficients are non-zero:

using DataInterpolations
using BenchmarkTools

t = cumsum(rand(100))
u = rand(100)
A = BSplineInterpolation(u, t, 2, :Uniform, :Uniform)

ts = collect(range(first(t), last(t), length = 10000))

@btime A($ts)
# old: 1.161 ms (2 allocations: 78.17 KiB)
# new: 663.900 μs (2 allocations: 78.17 KiB)

Copy link
Member

@sathvikbhagavan sathvikbhagavan left a comment

Choose a reason for hiding this comment

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

Looks good! Can you also add a few tests to ensure all cache parameters are set properly?

@@ -35,6 +35,23 @@ A2(300.0)

The values computed beyond the range of the time points provided during interpolation will not be reliable, as these methods only perform well within the range and the first/last piece polynomial fit is extrapolated on either side which might not reflect the true nature of the data.

The keyword `safetycopy=false` can be passed to make sure no copies of `u` and `t` are made when initializing the interpolation object.
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better to explain why we need safetycopy

push!(di.u, u)
push!(di.t, t)
di
function push!(A::LinearInterpolation{U, T}, u::eltype(U), t::eltype(T)) where {U, T}
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be allowed only if safetycopy is false?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think I fully understand the reasoning here. If safetycopy is true, then the u, t vectors within the interpolation object can safely be assumed to be only used within the context of this package. Surely then it is safe to add data to it? Only if u, t are also used in code outside the DataInterpolations context do you run the risk of breaking that code by modifying u, t.

Copy link
Member

Choose a reason for hiding this comment

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

I think I understand your point. It comes down what should be the semantics of push! for interpolation objects. Currently it mutates the original array. Maybe we should handle both cases? If it is true, it would push to the copy, if it is false it would mutate the original array. For both we have to compute the parameters again.
@ChrisRackauckas, your thoughts?

test/interpolation_tests.jl Show resolved Hide resolved
@SouthEndMusic
Copy link
Member Author

@sathvikbhagavan I made it so that push! and append! throw an error when safetycopy = true, but the error I now get in online_test.jl I think illustrates my point from earlier. u1, t1 are modified in the first iteration of the loop, which causes problems in the later iterations. Or would you say that this is bad use of the interpolation objects?

If this is indeed the behavior you want, I still need some help explaining the use of safetycopy in the docs.

docs/src/interface.md Outdated Show resolved Hide resolved
u === A3.u.parent
```

Note that this does not prevent allocation in every interpolation constructor call, because parameter values are cached for some interpolation types.
Copy link
Member

Choose a reason for hiding this comment

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

Can you also add a line describing which interpolations support caching?

src/online.jl Outdated
push!(di.t, t)
di
function push!(A::LinearInterpolation{U, T}, u::eltype(U), t::eltype(T)) where {U, T}
!A.safetycopy && throw(UnsafeAddDataError())
Copy link
Member

Choose a reason for hiding this comment

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

Is this needed? Shouldn't we allow for both?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is fine by me, I think it is clear enough that this leads to mutation of the input u, t if someone explicitly sets safetycopy = false.

@@ -83,7 +83,7 @@ end

@testset "LagrangeInterpolation" begin
u = [1.0, 4.0, 9.0]
t = [1.0, 2.0, 3.0]
t = [1.0, 2.0, 6.0]
Copy link
Member

Choose a reason for hiding this comment

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

Why was this changed?

Copy link
Member Author

Choose a reason for hiding this comment

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

I added this function to conveniently test whether the implementation of a new interpolation type is complete: https://github.com/SciML/DataInterpolations.jl/pull/274/files#diff-f66955aae2aee262495ff068999fe35f8501cd1cbef7b1057aa26d2f1099a4f3R6-R17

To do this, I moved throwing an error when integration is not defined from specialised methods of integral to methods of _integral. However, that now means that the extrapolation error is thrown before the 'integral not found' error. So, to still test the integral not found error, I had to make sure that the test for this does not try to extrapolate.

Copy link
Member

Choose a reason for hiding this comment

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

ok, that should be fine.

test/online_tests.jl Outdated Show resolved Hide resolved
test/online_tests.jl Outdated Show resolved Hide resolved
Copy link
Member

@sathvikbhagavan sathvikbhagavan left a comment

Choose a reason for hiding this comment

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

Almost there. Just a few cleanups.

@@ -83,7 +83,7 @@ end

@testset "LagrangeInterpolation" begin
u = [1.0, 4.0, 9.0]
t = [1.0, 2.0, 3.0]
t = [1.0, 2.0, 6.0]
Copy link
Member

Choose a reason for hiding this comment

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

ok, that should be fine.

docs/src/interface.md Outdated Show resolved Hide resolved
test/online_tests.jl Outdated Show resolved Hide resolved
src/interpolation_utils.jl Outdated Show resolved Hide resolved
docs/src/interface.md Outdated Show resolved Hide resolved
Copy link
Member

@sathvikbhagavan sathvikbhagavan left a comment

Choose a reason for hiding this comment

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

LGTM! Nice work! Thanks!

@ChrisRackauckas ChrisRackauckas merged commit dbcc4ca into SciML:master Jul 6, 2024
9 of 10 checks passed
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.

Increasing performance by caching
3 participants