Skip to content

Conversation

@jrevels
Copy link
Member

@jrevels jrevels commented Dec 15, 2016

This aims to fix #178, and any related issues with other functions.

I am not totally sure this does the right thing for nested Dual numbers; perhaps the perturbation check should be recursive?

There is a slight performance penalty here which is more or less significant depending on the function. I'm also interested to see what it does to SIMD performance.

EDIT:Removed a snippet of an alternate idea that I've now decided against.

@jrevels
Copy link
Member Author

jrevels commented Dec 22, 2016

The approach I ended up going with here is to make Partials "NaN/Inf-safe".

Unfortunately, I'm seeing about performance regressions of ~10% on this branch vs. master when benchmarking some DiffBase functions, and that's probably going to be near the lower bound on such regressions for this change - I'm only benchmarking cases where it only needs to do the isnan/isinf/zero checks, i.e. not even checking the whole Partials.

Maybe it would be better to create a module-level flag for enabling this change so that users can opt-in only if they need it, preserving performance for users who don't need it. cc @mlubin @KristofferC for your opinions.

src/partials.jl Outdated
#----------------------#

@inline function @compat(Base.:*)(partials::Partials, x::Real)
x = ifelse((isnan(x) || isinf(x)) && iszero(partials), one(x), x)
Copy link
Contributor

Choose a reason for hiding this comment

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

!isfinite could be better than isnan || isinf

@KristofferC
Copy link
Collaborator

KristofferC commented Dec 22, 2016

10% seems quite a hefty penalty in my opinion.

One way could be to define most operations for an AbstractDual and then have two concrete types where the NaN behaviour is dispatched on. I don't really like this way though. Feels a bit too intrusive.

@jrevels
Copy link
Member Author

jrevels commented Dec 23, 2016

I don't really like this way though.

"This way" referring to this PR (and the proposed "users switch this on/off via a module-level constant") or "this way" referring to your NaN-safe type idea?

I do think the performance hit is too great here to make this the default behavior. The most convenient API for this, IMO, would be adding something like a nansafe flag to the config types so that users could easily enable this behavior when they need it. Having this be a runtime switchable thing would probably mean implementing something close to what you mentioned (though I might just go with adding a Bool type parameter to Partials to flip it on/off). That could be done in a separate PR, though, and we could just provide the module-level option for now.

@jrevels
Copy link
Member Author

jrevels commented Dec 23, 2016

Any performance benefit?

From switching to !isfinite(x)? Yeah, thanks! It seems to improve performance by around ~3%-5% on the DiffBase functions I'm looking at versus isnan(x) || isinf(x).

@mlubin
Copy link
Contributor

mlubin commented Dec 23, 2016

The module-level option is problematic for packages that use ForwardDiff under the hood. What is JuMP supposed to do?

@KristofferC
Copy link
Collaborator

I thought that introducing a second concrete Dual number type is perhaps a quite large change to solve this problem (at least compared to the solution you have now). A 5% performance penalty might be acceptable to skip the headache of two concrete types? I haven't really run into this problem myself so I feel I can't really weigh options against eachother in an informed way.

@jrevels
Copy link
Member Author

jrevels commented Dec 23, 2016

The module-level option is problematic for packages that use ForwardDiff under the hood. What is JuMP supposed to do?

Really good point. It's definitely bothersome that whatever module-level option the user sets would apply to all downstream module usage, when they might instead want to only apply it selectively (e.g. have NaN-safe mode enabled for their JuMP model but disabled for usage in Optim). For now, however, it'd still an improvement over not having this option at all.

In a future PR, we could add AbstractConfig constructors like GradientConfig(x, Chunk{N}, NaNSafe{true}) (OT: I really wish something like JuliaLang/julia#16580 would land in v0.6, but that's really unlikely...fingers crossed for v1.0). The generated dual numbers would then contain partials of type Partials{N,T,true}, which would dispatch to the NaN-safe versions of methods. Downstream modules could then support passing that option through their APIs (or simply setting it to a reasonable default for themselves internally).

@jrevels jrevels merged commit 253d305 into master Dec 26, 2016
@jrevels jrevels deleted the jr/logfix branch December 26, 2016 17:23
- if (julia -e 'VERSION < v"0.5" && exit(1)'); then
julia -e 'include(joinpath(JULIA_HOME, Base.DATAROOTDIR, "julia", "build_sysimg.jl")); build_sysimg(force=true)';
julia -e 'Pkg.clone(pwd()); Pkg.build("ForwardDiff"); Pkg.test("ForwardDiff"; coverage=true)';
julia -e 'Pkg.clone(pwd()); Pkg.build("ForwardDiff"); Pkg.checkout("DiffBase"); Pkg.test("ForwardDiff"; coverage=true)';
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you still need to do this? These kind of things should be very temporary, otherwise you're not testing against the same versions of things your users will have installed.

Copy link
Member Author

Choose a reason for hiding this comment

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

DiffBase serves as a repository of test functions. If I didn't do this, I'd have to re-tag DiffBase every time I wanted to add a new shared test function, which would get really annoying.

I share your concern though. I've been meaning to factor the shared test suite out of DiffBase and into an unregistered package (DiffTestSuite or something). Then DiffTestSuite could be a test-only dependency that I Pkg.checkout every time, and I could go back to CI-testing DiffBase's functionality with the proper versions. That's what I probably should've done this in the first place...I'll set it up tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

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

Pkg.add("ForwardDiff"); Pkg.test("ForwardDiff") should pass. If that requires tagging more often, tag more often. Pkg.test shouldn't depend on unregistered packages.

Copy link
Contributor

@tkelman tkelman Jan 25, 2017

Choose a reason for hiding this comment

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

Same dependency versioning issue applies as everywhere else - if it's needed to pass tests and doesn't exist in every version of DiffBase, then at least test/REQUIRE needs to have a corresponding minimum version requirement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough - I'll fix this to use tagged DiffBase versions and start tagging DiffBase more often, then.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't mind if you want to run additional tests on CI-only, or have things temporarily checked out to branches on CI if that's necessary while things are being developed -- as long as you keep in mind how that configuration will differ from what users will have installed, and that Pkg.test should work for them on releases too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, no worries! My future workflow will be to keep master branch's CI running against tagged dependency versions for the reasons you mentioned. I can always toggle a PR's CI config to use whatever DiffBase branch I want during development, and then before merging, I can tag the new DiffBase version (if necessary) and switch the config back to using only tagged versions.

@andreasnoack
Copy link
Member

This is what I expect to see after flipping the NANSAFE_MODE_ENABLED flag

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0)
Dual{Float64}(Inf,0.0)

but should I expect to see

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0,1.0)
Dual{Float64}(Inf,NaN,Inf)

?

@jrevels
Copy link
Member Author

jrevels commented Feb 12, 2019

Hmm. Yeah, it would probably be consistent for us to check/propagate all partials individually when NANSAFE_MODE_ENABLED is set to true. Might cause a bigger performance hit for this NANSAFE mode but maybe not, and it'd probably be worthwhile either way.

@ajwheeler ajwheeler mentioned this pull request Nov 13, 2024
@devmotion
Copy link
Member

This is what I expect to see after flipping the NANSAFE_MODE_ENABLED flag

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0)
Dual{Float64}(Inf,0.0)

but should I expect to see

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0,1.0)
Dual{Float64}(Inf,NaN,Inf)

?

For anyone coming across this PR - this was fixed by #777 in ForwardDiff >= 1.2.2. With the latest release of ForwardDiff (and NaN-safe mode enabled):

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0)
Dual{Float64}(Inf,0.0)

julia> Inf*ForwardDiff.Dual{Float64}(1.0,0.0,1.0)
Dual{Float64}(Inf,0.0,Inf)

Might cause a bigger performance hit for this NANSAFE mode but maybe not,

It actually improved performance of NaN-safe mode 🙂

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.

log(Dual(0.0,0.0)) is Dual(-Inf,NaN)

6 participants