Skip to content

Better handling of multiple output nodes in logprob inference - #8395

Draft
ricardoV94 wants to merge 3 commits into
pymc-devs:mainfrom
ricardoV94:measurable_multi_output
Draft

Better handling of multiple output nodes in logprob inference#8395
ricardoV94 wants to merge 3 commits into
pymc-devs:mainfrom
ricardoV94:measurable_multi_output

Conversation

@ricardoV94

Copy link
Copy Markdown
Member
  • Compatibility with pytensor 3.2
  • Drop two stale pieces of logprob documentation
  • Derive a joint density in a single call, however its values are reached
  • Let a dims variable take part in a joint density

@read-the-docs-community

read-the-docs-community Bot commented Aug 9, 2026

Copy link
Copy Markdown

get_related_valued_nodes documents a `Returns` section listing an rv_node it does
not return, and logprob_make_vector carries a TODO about a circular dependency
that was resolved when the import it referred to moved to the module level; its
twin in logprob_join was removed at the time and this one was left behind.
@ricardoV94
ricardoV94 force-pushed the measurable_multi_output branch from bd38679 to d14750d Compare August 10, 2026 07:16
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.24324% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.83%. Comparing base (98be75a) to head (018c0a2).

Files with missing lines Patch % Lines
pymc/logprob/abstract.py 88.88% 4 Missing ⚠️
pymc/logprob/basic.py 93.10% 4 Missing ⚠️
pymc/dims/distributions/core.py 90.00% 1 Missing ⚠️
pymc/logprob/transform_value.py 83.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #8395      +/-   ##
==========================================
- Coverage   91.85%   91.83%   -0.02%     
==========================================
  Files         128      128              
  Lines       21259    21342      +83     
==========================================
+ Hits        19527    19599      +72     
- Misses       1732     1743      +11     
Files with missing lines Coverage Δ
pymc/distributions/continuous.py 98.05% <100.00%> (ø)
pymc/logprob/binary.py 96.34% <100.00%> (ø)
pymc/logprob/censoring.py 98.60% <100.00%> (ø)
pymc/logprob/checks.py 97.72% <100.00%> (ø)
pymc/logprob/cumsum.py 93.33% <100.00%> (ø)
pymc/logprob/linalg.py 93.61% <100.00%> (ø)
pymc/logprob/mixture.py 96.98% <100.00%> (ø)
pymc/logprob/order.py 92.39% <100.00%> (ø)
pymc/logprob/scan.py 94.54% <100.00%> (+0.15%) ⬆️
pymc/logprob/switch.py 80.68% <100.00%> (ø)
... and 7 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The logp helper bottomed out on one variable at a time, so a node whose density
is joint over several outputs had it derived from a subset whenever one of the
values arrived through a measurable chain -- exp, +1, a clip, anything.

Such a target now leaves a DensityQuery behind, and a resolution pass answers
every query about a node in one _logprob call. Answering one can only raise new
queries about that node's ancestors, so a sinks-first pass resolves them all.
Typing a query before its term exists needs to know which axes the density
consumes, so MeasurableOp gains `supp_axes` -- the smallest piece of pymc-devs#6360 --
set by the rewrite that built the op; a query refuses rather than guess.

Recursion into a measurable input now goes through request_logprob (renamed
from _logprob_helper, which warns and forwards), which decides whether _logprob
can be dispatched at all; it is dispatched directly only with every value of
the node in hand. conditional_logp conditions by rewiring the graph it owns
rather than substituting into clones, keeping node identity so that queries can
be matched to their node.
The cast into xtensor space dispatched _logprob on the variable underneath rather
than going through the helper, so a dims value could never join the values of a
node whose density is joint over several of them.

Once it does, the term that comes back has to be labelled. Its dims were read off
positionally, on the assumption that a density is over the rightmost dims, so one
taken over a dim in the middle came back labelled by the dim it had just consumed
-- ("obs", "trial") reduced over "obs" was labelled ("obs",) rather than
("trial",). The cast op already carries the dims a density consumes as core_dims,
and the rewrite that builds it can now name them whenever the variable it wraps
says which axes its measure is over: they are counted from the right, and the op's
dims are laid out in the same order, so they index them directly. A variable that
says nothing leaves core_dims unset and keeps the positional guess, which is right
whenever the density really is over the rightmost dims.
@ricardoV94
ricardoV94 force-pushed the measurable_multi_output branch from d14750d to 018c0a2 Compare August 10, 2026 11:49
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.

1 participant