Ns/benchmark - #13
Draft
nsiccha wants to merge 8 commits into
Draft
Conversation
…than brms for the primal computation and 20 times (Enzyme) or 30 times (Mooncake) slower for the gradient computation
…and Turing with gradient performance analysis Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
See the readme.
I think claude claims a few silly things, but also a few accurate ones. Which are which should be obvious 😇 .
Claude content below.
Bayesian Regression Model Benchmarks
Benchmarking log-density and gradient evaluation for a Bayesian linear regression across multiple backends and AD systems.
Problem:
drugs ~ o + c + e + a + n(ESCS dataset, N=604, K=5, 7 unconstrained parameters).Running
cd scripts/Benchmarking julia run_benchmark.jlGradient correctness is verified against brms/Stan as the reference.
Results
Primal (log-density evaluation)
Gradient (log-density + gradient, correct results only)
Implementations
Hand-written Julia variants
All implement the same log-density as the brms-generated Stan model: centered predictors, student-t priors, half-student-t on sigma.
Xc * ballocating,dot(r, r)for sum of squaresBase.broadcastedwith per-row dot products, custommysumwithpreprocess/instantiatemul!(BLAS gemv) + SIMD residual loop.=into pre-allocated buffer (same speed as lazy)mul!(r = Y - Xc*bin one BLAS call) + SIMD loopℓ_inner(mu, q)for fine-grained Enzyme annotationsStan variants
normal_id_glm_lpdf— hand-optimized fused likelihood with custom adjointXc * b+ vectorizednormal_lpdfmu+ vectorizednormal_lpdfTuring/DynamicPPL variants
@modelwith per-observationY[i] ~ Normal(mu[i], sigma)loopTuring.@addlogprob!, bypassing tilde processingAD Backend Notes
Enzyme
function_annotation=Enzyme.Const: Produces wrong gradients on julia3/5/6/7.Constprevents shadow memory allocation for captured mutable buffers (mu,r_buf), so adjoints can't propagate throughmul!.function_annotation=Enzyme.Duplicated: Correct gradients but creates shadows for ALL captured variables including constant data arrays (Y: 4.8KB, Xc: 24KB), adding ~1 μs overhead from zeroing ~30KB per call.ℓ_inner(mu, q)takes the buffer as an explicit argument. Called withConst(ℓ_inner)+Duplicated(mu, dmu)+Duplicated(q, grad). Only ~5KB shadow for the buffer. Matches Stan performance.Mooncake
NoRDatafor Y/Xc) but has higher per-call overhead than Enzyme's LLVM-level codegen.ForwardDiff
Fails on julia3/5/6/7 because
mul!writes into pre-allocatedFloat64buffers that can't holdDualnumbers. Works on julia1 (allocating version).Key Insights
Why hand-written Julia primals beat Stan
Stan evaluates with autodiff-ready
vartypes even for primal-only calls (propto=false). Julia operates on plainFloat64with zero-allocation BLAS + SIMD loops.Why Stan's gradient is hard to beat
Stan's
normal_id_glm_lpdfhas a hand-written adjoint that fuses primal and gradient into one pass, reusing intermediates. Gradient/primal ratio is 1.7x. Julia + Enzyme achieves 5.8x (whole-closureDuplicated) and comes close at 4.6x with fine-grainedConst/Duplicatedannotations.Why broadcasted row-dots are slow
eachrow(Xc)+ per-rowdot: 604 dot products of length 5, each too short for SIMD. BLASgemvprocesses column-by-column — each axpy touches 604 contiguous elements, perfect for vectorization.DynamicPPL overhead breakdown
@addlogprob!)~tilde statementsDynamicPPL v0.40 is already type-stable (
@code_warntypeshows concrete types). The overhead is inherent to its generality: parameter unpacking, bijector transforms, accumulator bookkeeping.DynamicPPL main branch
The main branch of DynamicPPL adds an
adtypekeyword toLogDensityFunctionfor integrated gradient computation. This passes model internals asDI.Constantcontexts — conceptually similar to our manual EnzymeConst/Duplicatedsplit. However, the gradient path doesn't work yet with current Mooncake/DI versions (fails atprepare_gradientwith multi-argumentConstantcontexts).