FlexiChains by default#44
Conversation
28047fa to
ebd73cc
Compare
penelopeysm
left a comment
There was a problem hiding this comment.
@rsenne Thought I'd get your opinions on a couple of points before going further.
|
Thanks for this! Here are my thoughts:
Also re your questions to ponder:
I agree
If you agree with my above thoughts, none! :) |
|
You're totally correct, there's no real need to support MCMCChains. That makes life a lot easier! |
|
Okay, I got all the tests to pass locally. Apart from switching over to FlexiChains, I made a couple of design calls (which I am hoping is positive 🙂), which I'll discuss in the comments |
There was a problem hiding this comment.
Pull request overview
This PR migrates ParallelMCMC’s chain output and tests from MCMCChains.Chains to FlexiChains, making FlexiChains chain types (e.g. VNChain / SymChain) the primary output target across samplers and extensions.
Changes:
- Replaced
MCMCChainswithFlexiChainsin core module dependencies and in the AbstractMCMC integration (bundle_samples,mcmcsamplepaths). - Updated Turing/DynamicPPL and interface tests to assert on FlexiChains APIs (
parameters,extras,niters,stack=trueindexing). - Updated package/test dependencies to drop
MCMCChainsand addFlexiChains.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/test-Turing-Integration.jl | Switches Turing integration expectations from MCMCChains names/indexing to FlexiChains parameter/extras APIs. |
| test/test-GPU-Performance.jl | Removes unused MCMCChains test dependency. |
| test/test-GPU-AD-HVP.jl | Removes unused MCMCChains test dependency. |
| test/test-DEER-Turing-Logistic.jl | Updates chain type + indexing to FlexiChains (VNChain/SymChain, stack=true). |
| test/test-DEER-Interface.jl | Migrates interface tests from names(..., :internals)/size to FlexiChains extras/niters/nchains. |
| test/test-Adaptive-MALA.jl | Migrates assertions to FlexiChains chain types and extras. |
| test/test-AbstractMCMC-Interface.jl | Expands test coverage for symbol vs varname keys and vector-valued params under FlexiChains. |
| test/Project.toml | Adds FlexiChains and removes MCMCChains from the test environment deps. |
| src/ParallelMCMC.jl | Replaces using MCMCChains with using FlexiChains and re-exports sample. |
| src/interface.jl | Implements FlexiChains-based chain construction for all samplers; changes default param naming to a single vector-valued :x. |
| Project.toml | Adds FlexiChains, removes MCMCChains, and updates extension trigger list. |
| ext/LogDensityProblemsExt.jl | Updates docs to mention FlexiChains (but still contains outdated default-name text). |
| ext/DynamicPPLExt.jl | Switches DynamicPPL integration to FlexiChains, adds VNChain-only bundling, and overrides construction for VarName chains. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. ❌ Your project check has failed because the head coverage (82.05%) is below the target coverage (90.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #44 +/- ##
==========================================
+ Coverage 80.06% 82.05% +1.98%
==========================================
Files 8 8
Lines 1164 1159 -5
==========================================
+ Hits 932 951 +19
+ Misses 232 208 -24 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| # Wrap parameter names in the format that FlexiChains expects. Ref: | ||
| # https://pysm.dev/FlexiChains.jl/stable/arrays/#api-fromarray | ||
| if param_names === nothing | ||
| param_names = model.param_names | ||
| end | ||
| wrapped_param_names = if param_names === nothing | ||
| # Wasn't defined either as `param_names` or `model.param_names`, so make some up | ||
| (to_parameter(:x) => (size(vals, 2),),) | ||
| else | ||
| map(param_names) do n | ||
| if n isa Pair | ||
| to_parameter(n.first) => n.second | ||
| elseif n isa TKey || n isa Symbol | ||
| to_parameter(n) | ||
| else | ||
| throw(ArgumentError("param_names must be a collection of Pairs or $TKey, got $(typeof(n))")) | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
The main change is to do with how param_names are handled. This is actually not really even a big change but I think it accounts for most of the real code changes in this PR (the rest is boilerplate) so I may as well explain
Previously you could pass a vector of Symbol, and then that gets stored in MCMCChains, nice and simple. Now I've 'upgraded' it to support grouping different parameters together. Let's say you have a model with dimension 3, but conceptually you really want the first parameter to be a scalar and the other two to be grouped together as a vector. FlexiChains has a constructor that lets you do that (https://pysm.dev/FlexiChains.jl/stable/arrays/#api-fromarray).
It's a faff for the user to write all that out, but this code block lets people strip away all the FlexiChains.Parameter stuff and just write something like param_names = (:x, :y => (2,)). On top of that, this also allows us to naturally 'upgrade' a Symbol chain to a VarName chain: if the user requests
chain_type=FlexiChain{VarName}, param_names=(:x, :y => (2,))
then the :x and :y will just be 'upgraded' to @varname(x) and @varname(y). (VarNames have some benefits over Symbols in that it's easier to index into part of them, say to get y[1] out.)
Finally, the default param name has been changed: instead of scalar-valued x[i] for i in 1:D, it's now just one vector-valued parameter. That makes it easier to access the entire thing as a 3D array, while still allowing access to individual elements.
Given all this, I'd suggest that the docs should probably push users towards using VNChain as the go-to chain type, and say something like 'SymChain also works if you prefer it'? I haven't yet updated the docs, but if you're in agreement then I'll do it in the final round of this PR!
There was a problem hiding this comment.
Yes this all LGTM and will be a big improvement imo
| ) | ||
| N = length(samples) | ||
| D = model.dim | ||
| for TKey in (Symbol, VarName) |
There was a problem hiding this comment.
The cost of supporting both Symbol and VarName chains is having to do this loop over both keys (to avoid ambiguities) but since it's self-contained in this file, I think it's fine. The problem I was worried about last time was more about having to have a separate but same implementation in an MCMCChainsExt.
There was a problem hiding this comment.
makes sense--loops are okay w/me but see below!
| @test all(a -> a == 0.0 || a == 1.0, acc) | ||
| end | ||
|
|
||
| @testset "sample() with custom param_names" begin |
There was a problem hiding this comment.
You can see some examples of the param_names change works in this testset.
| s = samples[i] | ||
| vals[i, :] .= s.x | ||
| internals[i, 1] = s.logp | ||
| internals[i, 2] = s.accepted ? 1.0 : 0.0 |
There was a problem hiding this comment.
Last question: Is there a need to store accepted as a F64, is it just so that internals is concrete? If it's the latter, maybe we could think about making internals a NamedTuple like (logp = [logp_matrix...], accepted = [accepted_matrix...])? That would let us store it as the natural Bool. Not one for this PR though, it's a separate thing!
There was a problem hiding this comment.
Good catch. Nope no reason. I like your suggestion. I'll open a new issue about it
|
Okay so I was reading over the code and I had a thought (I'm mildly sleep deprived and this idea is only half-baked so feel free to say I'm not making any sense). But i was looking at the functionality you put in And so I guess IIUC the # helper with same functionality as _construct_flexichain
function parameter_keys(spec, ::Type{TKey}) where {TKey}
to_parameter(vn::VarName) = Parameter(vn)
to_parameter(s::Symbol) = Parameter(TKey <: VarName ? VarName{s}() : s)
map(spec) do n
n isa Pair ? (to_parameter(n.first) => n.second) :
(n isa TKey || n isa Symbol) ? to_parameter(n) :
throw(ArgumentError("param_names must be Symbols, $TKey, or name=>shape Pairs"))
end
endand then add a function bundle_samples(transitions, m, s, state, ::Type{FlexiChain{Symbol}};
param_names=nothing, stats=missing, …)
if param_names === nothing
# unchanged
else
keys = parameter_keys(param_names, Symbol) # Piece 1
arr = stack_parameter_values(transitions) # iters × Σdims, from the hook values
extras = stack_stats(transitions)
return FlexiChain{Symbol}(hcat(arr, extras), (keys..., Extra.(...)...))
end
endConditioned on me making a semblance of sense, the PR as is can still proceed and this can be addressed later as these changes would need to be implemented themselves. Otherwise PR is looking good thus far! |
Yes, that and also the fact that you want to generate all the samples at once. I think the current AbstractMCMC interface kind of assumes that you're generating samples iteratively. The
I think maybe. I have a couple of mild objections:
Basically, let's not overengineer things yet. (In fact I feel like |
ah yes great point
a very reasonable assumption until this package :)
makes sense/sounds good to me |
| DifferentiationInterface = "0.7.13" | ||
| DynamicPPL = "0.40.6, 0.41" | ||
| Enzyme = "0.13.146" | ||
| ForwardDiff = "1" |
There was a problem hiding this comment.
We should still declare a compat v for this even if removed from the Ext
There was a problem hiding this comment.
Oh, is it known to not work with ForwardDiff v0?
There was a problem hiding this comment.
hmm i guess not known, but i haven't tested so maybe its fine
| """ | ||
| function ParallelMCMC.DensityModel( |
There was a problem hiding this comment.
So right now I have ForwardDiff as the dfault for this ext because of issue #25 and ForwardDiff worked as a workaround. I failed to document this so thats my b. though I'm not sure it makes total sense to have this as a default just given there is other backends i didn't try. So at the very least we need to either re-add in the ForwardDiff as a gate for this ext or just remove this
There was a problem hiding this comment.
No worries. So personally I don't think it's necessary to have ForwardDiff as an extension trigger -- for one, maybe people don't actually want to use ForwardDiff (in which case forcing them to load it is one extra dep), and secondly it reduces discoverability. After all the main package is also agnostic and doesn't load any AD backend as a hard dep so I think the extension can do the same thing.
There was a problem hiding this comment.
yes i agree let's remove as hard dep and remove the default
There was a problem hiding this comment.
Hmm I was thinking it's okay to keep AutoForwardDiff as the default (it would be a bit annoying as if you don't import ForwardDiff and run it it would error, but that's no different from the main package). However if I'm not mistaken, you are suggesting to leave backend unspecified in the DynamicPPL extension, and let the user pass it via the sampler instead? If that's the case I do actually think that that's better but maybe we can do it in a separate PR?
There was a problem hiding this comment.
yes that was my thinking to prevent the error, but yes thats fine we can push till next PR
There was a problem hiding this comment.
Okay so for now I just reverted this so that ForwardDiff is still an extension trigger (restoring the old behaviour) and we could look at it later. I haven't thought too much about it but it might be a bit of a faff as you have to re-wrap the DynamicPPL model with the adtype from the sampler hence why I thought it made sense to defer.
| function AbstractMCMC.bundle_samples( | ||
| ts::Vector{<:$Ttrans}, | ||
| model::DensityModelLDF, | ||
| spl::$Tspl, | ||
| state::$Tstate, | ||
| chain_type::Type{SymChain}; | ||
| kwargs..., | ||
| ) | ||
| throw(ArgumentError("FlexiChains.SymChain is not supported for DynamicPPL models; please use VNChain instead.")) |
There was a problem hiding this comment.
So I was going to add a test for this but before I do that, I realised that this might be kind of annoying behaviour, in that if you specify the wrong chain type it will do all the sampling but only error at the end. That's no worse than the current behaviour ofc (current behaviour is to do all the sampling and then have an inscrutable MethodError at the end). Just wanted to check you're okay with that first!
There was a problem hiding this comment.
hmm yeah a bit annoying, but as you said better than the earlier behavior with a legible error now. I say this is good w/ me and can be revisited later if necessary. Go ahead and add some tests!
|
I think I have addressed or deferred everything :) |
|
May I humbly request tests for the missed lines? Otherwise i'll get a review on this shortly |
… pr/penelopeysm/20
|
Everything looks good codewise -- i added a few suggested tests :) |
Co-authored-by: Ryan Senne <50930199+rsenne@users.noreply.github.com>
…into pr/penelopeysm/20
| @eval function AbstractMCMC.bundle_samples( | ||
| samples::Vector{<:ParallelMALATransition}, | ||
| model::DensityModel, | ||
| sampler::ParallelMALASampler, | ||
| state::ParallelMALAState, | ||
| ::Type{FlexiChains.FlexiChain{$TKey}}; | ||
| param_names=nothing, | ||
| kwargs..., | ||
| ) | ||
| N = length(samples) | ||
| D = model.dim | ||
|
|
||
| names = if param_names !== nothing | ||
| param_names | ||
| elseif model.param_names !== nothing | ||
| model.param_names | ||
| else | ||
| [Symbol("x[$i]") for i in 1:D] | ||
| end | ||
| internal_names = [:logp] | ||
|
|
||
| internal_names = [:logp] | ||
| vals = Matrix{Float64}(undef, N, D) | ||
| internals = Matrix{Float64}(undef, N, 1) | ||
|
|
||
| vals = Matrix{Float64}(undef, N, D) | ||
| internals = Matrix{Float64}(undef, N, 1) | ||
| for i in 1:N | ||
| vals[i, :] .= samples[i].x | ||
| internals[i, 1] = samples[i].logp | ||
| end | ||
|
|
||
| for i in 1:N | ||
| vals[i, :] .= samples[i].x | ||
| internals[i, 1] = samples[i].logp | ||
| return _construct_flexichain($TKey, vals, internals, param_names, internal_names, model) | ||
| end | ||
|
|
||
| return MCMCChains.Chains( | ||
| hcat(vals, internals), | ||
| vcat(names, internal_names), | ||
| Dict(:parameters => names, :internals => internal_names), | ||
| ) | ||
| end |
There was a problem hiding this comment.
Note to self: An issue for another PR became apparent to me when trying to figure out what conditions this path was exercised and I came to the realization that this is only use dwhen a non-default kwarg like thinning is used.
It took me a second to realize why but basically _sample_parallel_mala_chain needs to be retooled/extended so it supports discard_initial/thinning etc as otherwise we get kicked off the fast path. I think I just never wired this up properly. This would in turn stop routing through the generic mcmcsample path.
|
Thanks for all of the amazing work on this @penelopeysm! Super happy to have FlexiChains support |
Some questions worth pondering:
FlexiChains.FlexiChain{Symbol}is a massive faff. When I started writing FlexiChains it was very much with the Turing use case in mind, which is whyFlexiChain{VarName}has a convenient aliasVNChain. I think it probably makes sense to also make an alias forFlexiChain{Symbol}.Add SymChain alias penelopeysm/FlexiChains.jl#248