Repository navigation
perf(engine): convert a NamedTuple-record batch once at FitLogDensity construction - #132
Merged
Merged
Conversation
… construction
Batch record scoring allocated a fresh Missing-admitting vector per
record per evaluation, via ComposedDistributions' _named_value_vector
inside logpdf(d, ::NamedTuple) -- directly in the ForwardDiff gradient
loop for the shape rand(d, n) produces.
Add a scored_data field to FitLogDensity, prepared once at
distribution_to_logdensity construction via a new
_prepare_scored_data/_prepare_default_loglik_data hook (a no-op unless
overridden). The ComposedDistributions extension overrides it for
Sequential/Parallel composers scoring an AbstractVector{<:NamedTuple}
batch under the default loglik, converting each record once and
holding the converted form for the life of the problem.
observations(prob) still returns exactly what the caller passed in;
the converted batch is held separately and only reaches loglik when
loglik is the default reducer, so a caller-supplied loglik always
sees the untouched data.
Closes #115.
Contributor
Try this Pull Request!Option 1: Julia Package ManagerOpen Julia and type: import Pkg
Pkg.activate(temp=true)
Pkg.add(url="https://github.com/EpiAware/DistributionsInference.jl", rev="perf/batch-record-conversion-115")
using DistributionsInferenceOption 2: Local CheckoutIf you have the repo locally: git checkout perf/batch-record-conversion-115
julia --project=. -e "using Pkg; Pkg.instantiate()" |
Codecov Report✅ All modified and coverable lines are covered by tests.
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Contributor
|
📖 Documentation preview is ready! View the docs for this PR at: https://EpiAware.github.io/DistributionsInference.jl/previews/PR132/ This preview will be updated automatically when you push new commits. |
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #115: batch record scoring allocated per record, in the ForwardDiff
gradient loop.
FitLogDensitygains ascored_datafield, prepared once atdistribution_to_logdensityconstruction via a new_prepare_scored_data/_prepare_default_loglik_datahook (generic no-opunless an extension overrides it).
logdensitycallsloglik(obj, scored_data)instead ofloglik(obj, data).The
ComposedDistributionsextension overrides the hook forSequential/Parallelcomposers scoring anAbstractVector{<:NamedTuple}batch under the default
loglik: each record is converted once, via CD'sown
_named_value_vector, to itsMissing-admitting vector form and heldfor the life of the problem, instead of being re-converted on every
logdensityevaluation.A caller-supplied
loglikalways sees the untoucheddatait was handed —only the default reducer's batch is ever swapped, so a custom
loglikreading
datain some other shape cannot be silently broken by this.Public-surface note
observations(prob)is unchanged: it still returns exactly what the callerpassed to
distribution_to_logdensity, by identity (===). The convertedbatch lives on a new internal
scored_datafield with no public accessor,consistent with the existing internal-only
extra_state/concrete_fieldsfields. No caller of
template/observations/flat_priorsneeds to change(checked
ext/DistributionsInferenceBijectorsExt.jl,ext/DistributionsInferenceDynamicPPLExt.jl,ext/DistributionsInferenceAdvancedMHExt.jl— none touch.datadirectly).Measurements
Benchmarked on a two-leaf
Parallelcomposed distribution(
onset_admituncertain-Gamma,admit_deathfixed-LogNormal), 200NamedTuplerecords, matching therand(d, n)row shape the issuedescribes.
Before (current main):
logdensity, NamedTuple batchlogdensity, plain-vector batchForwardDiff.gradient, NamedTuple batchForwardDiff.gradient, plain-vector batchAfter (this PR):
logdensity, NamedTuple batchForwardDiff.gradient, NamedTuple batchThe
NamedTuple-record batch now matches the plain-vector batch almostexactly — a ~7.6x speedup and ~7.5x allocation reduction for
logdensity,and a ~5.6x speedup and ~6.8x allocation reduction under
ForwardDiff.gradient, consistent with (slightly exceeding) the issue'sreported ~5x.
The single-record
logpdf(obj, record)cost itself is unchanged (235ns/688BNamedTuple vs 32ns/96B vector) — that path isn't touched, only the batch
held by
FitLogDensityfor the life of a fit.Correctness: the
MissingdispatchThe
Missing-admitting element type is load-bearing (keeps the censoredlogpdf(d, ::AbstractVector{>:Missing})specialisation selected). Verified,not just reasoned about:
test/composed_ext.jl) builds a batch with amissingfield,confirms the cached
scored_datamatches CD's own_named_value_vectorconversion exactly, confirmswhich(logpdf, ...)resolves to the
AbstractVector{>:Missing}method (not the plain one),and confirms
logdensitythrough the cached batch matches a hand-rolleduncached
NamedTuplewalk tortol=1e-12.test/engine.jl) confirms a caller-suppliedloglikalwaysreceives the untouched
databy identity, and thatscored_datadefaultsto
data(by identity) when no extension hook applies.Type stability
scored_data's type is a fresh struct type parameter (SD), inferredconcretely in both cases checked (
Vector{Vector{Union{Missing,Float64}}}for the CD batch, identity for the generic no-op).
@code_warntypeonFitLogDensity(...)andlogdensity(...)shows no instability introducedby this change.
logdensity's body isAny-typed for this fixture, butthat traces to
ComposedDistributions.reconstructitself returning aUnion-typed tree (pre-existing onmain, unrelated to this PR — confirmedby
@code_warntypeingreconstructdirectly, unchanged by this diff).Testing
task test-ad(all AD backends): 1386/1386 pass.task test(full suite incl. quality): 627/627 pass.task format/task test-formatting: clean.This was opened by a bot. Please ping @seabbs for any questions.