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

fix: fix remake for parameters dependent on observed variables #832

Merged
merged 1 commit into from
Oct 28, 2024

Conversation

AayushSabharwal
Copy link
Member

@isaacsas @TorkelE this should fix the remake issue you were having with conservation laws.

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.

Comment on lines -680 to +685
p = anydict(k => symbolic_type(v) === NotSymbolic() ? v : symbolic_evaluate(v, p)
for (k, v) in p)
for (k, v) in p
p[k] = symbolic_type(v) === NotSymbolic() ? v : symbolic_evaluate(v, p)
end
Copy link
Member

Choose a reason for hiding this comment

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

why is this preferred? This would be a slower construction.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it? I thought it would be faster since it doesn't create a new dictionary

Copy link
Member

Choose a reason for hiding this comment

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

oh I didn't see that it was extending an old one.

It's still adding elements one by one. That might amortize?

Copy link
Member Author

Choose a reason for hiding this comment

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

julia> function oop(dict)
           return Dict{Any, Any}(k => 2v for (k, v) in dict)
       end
julia> function iip(dict)
           for (k, v) in dict
               dict[k] = 2v
           end
       end
julia> dict = Dict{Any, Any}(rand() => rand() for _ in 1:10000);
julia> oop(dict); iip(dict);
julia> @be oop(dict)
Benchmark: 25 samples with 1 evaluation
 min    2.599 ms (50092 allocs: 1.183 MiB)
 median 2.696 ms (50092 allocs: 1.183 MiB)
 mean   3.925 ms (50092 allocs: 1.183 MiB, 3.60% gc time)
 max    29.143 ms (50092 allocs: 1.183 MiB, 90.08% gc time)

julia> @be iip(dict)
Benchmark: 86 samples with 1 evaluation
 min    1.004 ms (20085 allocs: 313.828 KiB)
 median 1.052 ms (20085 allocs: 313.828 KiB)
 mean   1.140 ms (20085 allocs: 313.828 KiB, 1.77% gc time)
 max    5.962 ms (20085 allocs: 313.828 KiB, 81.95% gc time)

In-place seems faster

Copy link
Member

Choose a reason for hiding this comment

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

Okay, that's mainly because the generator isn't optimizing there 😅 . Both are slow, but it's fine.

@ChrisRackauckas ChrisRackauckas merged commit bf913e7 into SciML:master Oct 28, 2024
33 of 51 checks passed
@isaacsas
Copy link
Member

Thanks for the quick fix!

I still think this is likely to lead to silent errors for many users if

remake(prob; u0 = [...])

and

remake(prob; u0 = [...], p = Dict())

are not equivalent. I strongly suspect most people will just use the former and not even realize they might need to use the latter. (Which as we see currently leads to silent correctness issues.)

Is there no way to make the latter the default behavior, or alternatively have a system opt into it? If it is made the default there could be a kwarg to turn it off for more advanced users who know their system doesn't need such behavior (and hence can get better performance avoiding it)? That feels like a safer convention from a correctness perspective.

@ChrisRackauckas
Copy link
Member

Well not quite. It's specifically a non-symbolic remake, remake(prob, u0 = [x => 5]), is fine. remake(prob, u0 = [2.0,3.0], p = [3.0,2.0]) reverts back to the normal non-symbolic dispatch though, which then just makes those arrays prob.u0 and prob.p. That of course means it doesn't hit any of the initialization stuff etc... and I hate it. Indeed it's a bit of a footgun. I do think that if you have a symbolic system and you try to do that, you need to do some override boolean and we should error by default. I have seen too many people do non-symbolic things assuming an order etc. and it's just wrong.

There is a use case with optimization where, if an optimizer is choosing new parameters, you want to just remake(prob, p=newp) (and indeed with MTKParameters it's doing something a little special only replacing the tunables, but still), but we should definitely guard that with safety because I see too many people accidentally hit the non-symbolic dispatch thinking it means something that it doesn't. Basically, you should have to say "yes, I'm very sure that I understand the risks here, I know my parameter ordering, I know what SymbolicIndexingInterface, getsym, setsym etc. is, but I want to just put this array here" and we can go "sure, it sounds like you read and know the error with this, good luck".

@isaacsas
Copy link
Member

Can one not detect if they are in the symbolic remake context, and in that case enforce that the two forms are equivalent (modulo an explict kwarg to opt out)?

@ChrisRackauckas
Copy link
Member

That's what we do right now, without the keyword argument to make it explicit. Dict() just means it's an empty symbol map.

@isaacsas
Copy link
Member

Yes, but I’m saying to make passing an empty map equivalent to not passing the kwarg at all. These are different now in the symbolic case.

@ChrisRackauckas
Copy link
Member

That's what we do. That's why remake(prob; u0 = [2.0,3.0], p = Dict()) hits the symbolic dispatch while remake(prob; u0 = [2.0,3.0]) does not.

@AayushSabharwal AayushSabharwal deleted the as/remake-fix branch October 28, 2024 15:59
@AayushSabharwal
Copy link
Member Author

I think @isaacsas means remake(prob; u0 = [x => 1.0]) is not equivalent to remake(prob; u0 = [x => 1.0], p = Dict()).

@isaacsas
Copy link
Member

Yes, that is what I am saying should be changed.

@ChrisRackauckas
Copy link
Member

Yes that should definitely by changed.

@AayushSabharwal
Copy link
Member Author

Isn't it breaking, though?

@AayushSabharwal
Copy link
Member Author

The reason it was implemented this way was (iirc) for the limited cases where symbolic remake worked prior to SII, that's what it did.

@AayushSabharwal
Copy link
Member Author

@isaacsas
Copy link
Member

This feels like it is a worthwhile reason for a breaking change.

@ChrisRackauckas
Copy link
Member

Yeah but what I mean is, is there even any ODE that is solvable where u0 = [x => 2.0]? I would think that would just error in the solver since zero(u0) isn't defined or something. I can't think of a case you'd actual break.

@AayushSabharwal
Copy link
Member Author

u0 = [x => 2.0] hits the symbolic path, which updates the appropriate index of the u0 vector and creates the initialization problem. However, it doesn't update any parameter values which have a default dependent on the new u0.

@ChrisRackauckas
Copy link
Member

oh... that's a bug. It's got to be interpreted as a bug. I can't see someone actually relying on that behavior?

@AayushSabharwal
Copy link
Member Author

I mean, it allows you to avoid the overhead of creating the symbolic map, substituting and calling remake_buffer while getting the same solve result because initialization.

@isaacsas
Copy link
Member

I mean, it allows you to avoid the overhead of creating the symbolic map, substituting and calling remake_buffer while getting the same solve result because initialization.

But only for ODEs/DAEs. For all other problem types it is a potential bug no?

@isaacsas
Copy link
Member

isaacsas commented Oct 28, 2024

Is there no notion of an underlying dependency map that could be tested to decide if " creating the symbolic map, substituting and calling remake_buffer while getting the same solve result because initialization" could be bypassed?

@isaacsas
Copy link
Member

Again though, it feels like the default here should be the safest option, with ways to opt out for users who know what they are doing and need better performance.

@ChrisRackauckas
Copy link
Member

Yeah this seems to be trying to hard to save a little bit of performance. The setsym interface is required for full performance anyways, so this isn't necessarily fast, just a little not slower.

@isaacsas
Copy link
Member

Why was this merged and released? The Catalyst downstream test failures that appear here are pretty clearly remake related. Several Catalyst test sets are now failing too, see everything I'm having to disable in: SciML/Catalyst.jl#1098

@ChrisRackauckas
Copy link
Member

That wasn't clear...

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.

3 participants