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

Rework of how transformations are handled #575

Merged
merged 198 commits into from
Aug 21, 2024
Merged

Conversation

torfjelde
Copy link
Member

@torfjelde torfjelde commented Jan 31, 2024

While working on #555, I ran into the pain that is our current way of handling transformations.

This is partially because we've been slowly adding support for things that wasn't support back in the day, while trying to preserve functionality.

I think it's now time rip out quite a bit of the historical artifacts and rework this.

In DynamicPPL.jl we effectively have a very annoying problem where we can have three different "representation" of a realization:

  1. The "internal representation", i.e. how a AbstractVarInfo stores the realization internally.
  2. The "linked internal representation", i.e. how a AbstractVarInfo stores a "linked" realization internally (this might differ from (1) in dimensionality, e.g. VarInfo, but also type, e.g. SimpleVarInfo).
  3. The "model representation", i.e. how a realization of a variable is expected to be represented when working with it in the model body; this is always decided by the Distribution from which the variable is sampled.

Back in the day when everything was easier, we could define the map from_linked_internal (needed in getindex, assume, etc.) as a simple composition of from_internal (aka reconstruct) and invlink, and similarly for to_linked_internal (needed in setindex!, push!, etc.). But this resulting in multiple bugs once we started working with less convenient distributions, e.g. LKJCholesky where the linked representation is a Vector but the model representation is a Cholesky.

So these days we can't really think about from_linked_internal as a simple composition, but instead has to think about it as an "independent" mapping that can sometimes be represented in the old way not always.

To handle this, we made reconstruct, a function which was meant to take us from "internal representation" to "model representation", also accept the linking transformation, and attempts to construct whatever representation is needed for the pair (invlink_transform(dist), dist). This is okay, but because we have different implementations of AbstractVarInfo, e.g. VarInfo and SimpleVarInfo, each of which have different internal representations, the reconstruct has this behavior where it sometimes does what you expect and sometimes doesn't do anything, and it's difficult to determine exactly when what happens.

This PR removes reconstruct completely, in addition to other related methods, and effectively boils the entire transformation handling down to two mappings:

  • from_internal_transform(varinfo, vn, dist): construct a transformation that takes us from the internal representation to the model representation.
  • from_internal_transformation(varinfo, vn, dist): construct a transformation that takes us from the internal representation to the model representation.
  • from_linked_internal_transform(varinfo, vn, dist): construct a transformation that takes us from the linked internal representation to the model representation. (correction by @yebai)

Notice that these methods construct a transformation, and we want these transformations to also define InverseFunctions.inverse and ChangesOfVariables.with_logabsdet_jacobian so that we can easily define the to_* mappings and obtain the log-abs-det-jacobian corrections for the transformations easily.

But that's really all we need + now everything is very explicit and clear.

There is a longer exposition on the topic in the docs accompanying this PR (see the docs preview under the "internals" tab) where I discuss why we need certain methods that might at a first glance now seem necessary.

Note that there is one drawback with this approach: we might run into type-instabilities since we're constructing a transform. One would hope that union splitting saves us; indeed that seems to work nicely for our test-suite on more recent Julia versions but not on 1.6. I'm not sure how much we should care about this though because once we move away from all the Selector stuff + old Gibbs sampler #573 , there's really no reason why anyone would every link only a subset of a varinfo, at which point we should always use the immutable link method and make whether or not a varinfo is linked available at compile-time, not runtime.

@devmotion @yebai

torfjelde and others added 30 commits October 31, 2023 23:10
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@torfjelde
Copy link
Member Author

Aight, so it seems we weren't really testing cases such as

product_distribution(fill(Dirichlet(ones(4)), 2, 3))

(which might be because we didn't have support for this in Bijectors.jl until recently), and so I just added a test for this and now things are failiing 🙃

It turns out there's a bug in Bijectors.logpdf_with_trans for these distributions, so we need to fix this in Bijecotrs.jl. Will open an PR.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
test/model.jl Outdated Show resolved Hide resolved
Co-authored-by: Markus Hauru <[email protected]>
@torfjelde
Copy link
Member Author

Thanks @mhauru

@torfjelde
Copy link
Member Author

Looks like everything is passing nicely:)

Copy link
Member

@mhauru mhauru left a comment

Choose a reason for hiding this comment

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

My only two unresolved comments are very minor, so I'm happy to approve. Likewise @yebai has a few unresolved comments, but he approved as well, so I assume he considers them non-blocking. @torfjelde, good to merge on your side?

@@ -2005,7 +2026,39 @@ end

function values_from_metadata(md::Metadata)
Copy link
Member

Choose a reason for hiding this comment

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

Having read the code more carefully, I think I agree with from_internal_transform being the right thing. Could still have a docstring, but optional.

@shravanngoswamii
Copy link
Member

I will open another PR for converting diagrams images to Mermaid!

@torfjelde torfjelde enabled auto-merge August 21, 2024 08:48
@torfjelde torfjelde added this pull request to the merge queue Aug 21, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 21, 2024
@mhauru
Copy link
Member

mhauru commented Aug 21, 2024

The API docs index.html had grown to exceed the threshold size allowed by Documenter by a few kilobytes. I doubled the threshold to 400 KiB. There doesn't seem to be anything wrong with the API page, it's just plain long.

@mhauru mhauru enabled auto-merge August 21, 2024 09:26
@torfjelde
Copy link
Member Author

Does this count the images? Or is it just the HTML?

@mhauru mhauru added this pull request to the merge queue Aug 21, 2024
@mhauru
Copy link
Member

mhauru commented Aug 21, 2024

I'm not sure if it would count the images, but they are small, and not on the page that exceeds the threshold.

Merged via the queue into master with commit 138bd40 Aug 21, 2024
11 of 12 checks passed
@mhauru mhauru deleted the torfjelde/transformations branch August 21, 2024 10:04
@mhauru
Copy link
Member

mhauru commented Aug 21, 2024

Hurray! Great stuff @torfjelde!

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.

7 participants