docs: add "Understanding PyTensor compilation" guide for PyMC users [first draft / RFC]#8326
docs: add "Understanding PyTensor compilation" guide for PyMC users [first draft / RFC]#8326drbenvincent wants to merge 7 commits into
Conversation
Add an explanation-type page to the core notebooks that describes how PyMC hands a model's math to PyTensor and what PyTensor does with it: the two-stage compilation pipeline (graph rewriting then backend linking), the available backends (Numba/C/JAX/PyTorch/MLX/VM), and the boundary between PyTensor compilation and the MCMC sampler engine (pymc/nutpie/numpyro/blackjax). Complements the existing "PyMC and PyTensor" notebook (which covers how models become graphs). Includes a pipeline diagram (committed as SVG, with the Mermaid source alongside) and links it into the core-notebooks toctree. Co-authored-by: Cursor <cursoragent@cursor.com>
Documentation build overview
216 files changed ·
|
|
First skim, it's a tad too much |
I'll trim it down |
|
Thanks for taking the time. Revisions incoming... |
Reposition the page as a high-level overview per review feedback: broaden the mental model beyond logp+gradient, remove all Under the hood dropdowns, fix factual errors (useless/canonicalize, pm.Data), and soften the Stage 1 caching claim. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Second pass addressing review feedback — repositioned as a pure overview rather than a gateway with deep dives. Summary of changes (274 → 225 lines):
Replies on each inline comment below with specifics. |
Expand the data handling paragraph in the Runtime section to explain why pm.Data avoids recompilation (runtime input vs compile-time constant) and when the trade-off matters. Co-authored-by: Cursor <cursoragent@cursor.com>
Enumerate more of what the guide explains beyond compilation/caching: pm.Data mutability, numerical stability, backend choice, and why vectorized code helps. Co-authored-by: Cursor <cursoragent@cursor.com>
| @@ -0,0 +1,235 @@ | |||
| (pytensor_compilation)= | |||
| # PyTensor, WTF? | |||
There was a problem hiding this comment.
Done — title is now PyTensor: a high level introduction.
There was a problem hiding this comment.
No. And that was an over eager agent commenting when I didn't ask it to.
I have revisions. Need to perfect. Plan to push revisions over the weekend at some point
There was a problem hiding this comment.
Maybe don't give it gh permissions to comment ;)
| (pytensor_compilation)= | ||
| # PyTensor, WTF? | ||
|
|
||
| **Author:** [Benjamin Vincent](https://github.com/drbenvincent) |
There was a problem hiding this comment.
do we put authors of core notebooks at all / at the top?
There was a problem hiding this comment.
There are author tags on top and an authors section at the bottom
There was a problem hiding this comment.
Switched to the jupyter_style convention: :::{post} with :author: at the top and an ## Authors section at the bottom. Removed the standalone **Author:** line.
|
|
||
| ## The pipeline at a glance | ||
|
|
||
| :::{figure} ../../images/pytensor_compilation.svg |
There was a problem hiding this comment.
I think this diagram is too much (also outdated from our iterations?)
There was a problem hiding this comment.
Diagram replaced with a linear 7-node flow (model → graph → prep → Stage 1 → Stage 2 → callable → sampler). Removed outdated internals (FunctionGraph, useless, etc.). Backend/sampler choices are prose-only now.
|
|
||
| The first edit is an easy win. **Merge** spots when the same calculation appears more than once and makes the model compute it a single time, then reuse the result — like noticing you've added up the same column of numbers twice and deciding to do it once. | ||
|
|
||
| #### Speaking with one voice: `canonicalize` |
There was a problem hiding this comment.
I don't think we need this anthropomorphism of pytensor stages (in the headers)
There was a problem hiding this comment.
Strong agree. The entire "editor" metaphor is tortured and borderline patronizing imo.
There was a problem hiding this comment.
Removed anthropomorphic headers and editor/recipe metaphors throughout. Section headers now use pass names and technical descriptions.
|
we don't have reviewnb on here :( |
jessegrabowski
left a comment
There was a problem hiding this comment.
went through the first half, will leave more comments on 2nd half later
| @@ -0,0 +1,91 @@ | |||
| flowchart TD | |||
There was a problem hiding this comment.
my preference would be for these to be generated when we deploy the docs via sphinx hook, like what we do with the example gallery. That way we don't need to check in the diagram or image.
There was a problem hiding this comment.
Actually why isn't this whole thing a notebook and let sphinx handle the conversion to markdown?
There was a problem hiding this comment.
Keeping checked-in .mmd/.svg for now (simplified source). Staying as .md rather than converting to .ipynb — happy to revisit Sphinx-time generation in a follow-up if we add mermaid support to the docs build.
| (pytensor_compilation)= | ||
| # PyTensor, WTF? | ||
|
|
||
| **Author:** [Benjamin Vincent](https://github.com/drbenvincent) |
There was a problem hiding this comment.
There are author tags on top and an authors section at the bottom
|
|
||
| The one coupling to be aware of: the JAX-based engines (`numpyro`, `blackjax`, and `nutpie` in JAX mode) need PyTensor to have compiled the callable to JAX. PyMC handles wiring this up for you. | ||
|
|
||
| #### Numba (the default) |
There was a problem hiding this comment.
It's nice to list out the backends but there's zero percent chance we keep this up to date. Maybe it's not a reason to not do it, but flagging it as high entropy.
There was a problem hiding this comment.
Collapsed the six per-backend subsections into the summary table only. Backend details live in one place now.
| @@ -0,0 +1,235 @@ | |||
| (pytensor_compilation)= | |||
| # PyTensor, WTF? | |||
|
|
||
| #### Keeping the numbers safe: `stabilize` | ||
|
|
||
| This is the pass PyMC users benefit from the most, even though they almost never see it work. Here is the problem it solves. Computers store numbers with only so many digits of precision, and some formulas that are perfectly correct on paper fall apart on a real computer. |
There was a problem hiding this comment.
important vocab: floating point precision. Consider mentioning half-precision calculation since everyone has a bee in their bonnet about GPUs.
There was a problem hiding this comment.
Added floating-point precision language in stabilize; noted reduced precision on GPU backends.
|
|
||
| #### Shortcuts and fewer passes over the data: `specialize` and `fusion` | ||
|
|
||
| Two more speed-ups, both about doing less work. **Specialize** swaps a general-purpose operation for a faster special-case version whenever one exists — the way you'd compute `x²` as `x * x` instead of starting up a general "raise to any power" routine. |
There was a problem hiding this comment.
nit: prefer mathjax to unicode
There was a problem hiding this comment.
Switched to $x^2$ MathJax.
|
|
||
| Two more speed-ups, both about doing less work. **Specialize** swaps a general-purpose operation for a faster special-case version whenever one exists — the way you'd compute `x²` as `x * x` instead of starting up a general "raise to any power" routine. | ||
|
|
||
| **Fusion** fixes a subtler waste. Imagine your model computes `a * b + c` element by element across big arrays. Done naively, the computer builds one whole temporary array to hold `a * b`, then another to hold `+ c` — a lot of memory shuffled around for nothing. Fusion merges these element-wise steps so each element gets *all* of its operations in a single pass, with no temporaries in between. Picture an assembly line where each item is fully finished at one station, instead of the entire batch being carried back and forth between stations. |
There was a problem hiding this comment.
Again i think the desire to be "accessible" is really holding you back here. You should really be talking about loops, and how many times you have to go over an array to do a computation. If your target audience really is people with zero idea about how computers work, it's not obvious at all where the loops even exist in an expression like a * b + c.
There was a problem hiding this comment.
Fusion section now describes array traversals and temporary allocations explicitly.
|
|
||
| #### Last-minute, backend-aware edits: backend-specific rewrites and in-place ops | ||
|
|
||
| The final edits depend on *where* your model is headed and on squeezing out memory. **Backend-specific** edits are small tweaks tailored to the backend you chose — a few extra tricks that only make sense for Numba, or only for JAX, and so on. Tricks that only help the C backend are dropped when you're not using it. |
There was a problem hiding this comment.
This isn't true in general, backend-specific rewrites can happen anywhere. For example solve_discrete_lyapunov has a jax version specific in specialize phase. I get that you're trying to tell this as a linear story but it's just not.
There was a problem hiding this comment.
Noted backend-specific rewrites can appear in multiple phases (e.g. solve_discrete_lyapunov in specialize).
|
|
||
| The final edits depend on *where* your model is headed and on squeezing out memory. **Backend-specific** edits are small tweaks tailored to the backend you chose — a few extra tricks that only make sense for Numba, or only for JAX, and so on. Tricks that only help the C backend are dropped when you're not using it. | ||
|
|
||
| **In-place** edits save memory by letting an operation write its result directly over its input's memory, instead of allocating a brand-new array — like reusing the same sheet of scratch paper rather than grabbing a fresh one every time. Overwriting is risky, though: what if some other part of the graph still needs that data? So before any in-place edit runs, a "destroy handler" acts as a safety inspector, tracking exactly which chunks of memory are safe to reuse. (JAX skips in-place edits altogether — it manages memory in its own way.) |
There was a problem hiding this comment.
talking about the destroy handler is too deep in the weeds for this tone/presentation. I suggest something like "because we see the whole computation from start to finish, Pytensor is able to reason about when it would be safe to reuse memory. If it spots that some intermediate result is never going to be used again, it can re-use it as a scratch-pad or memory buffer in a later computation, saving expensive memory allocations"
There was a problem hiding this comment.
Removed destroy-handler name; explained whole-graph memory reuse in plain terms.
Co-authored-by: Jesse Grabowski <48652735+jessegrabowski@users.noreply.github.com>
|
Third pass addressing review feedback. Summary:
Kept as |
Bear in mind I am a pytensor beginner - but I think that means I'm a good person to write a guide like this. Corrections welcome.
This was originally opened against pytensor (pymc-devs/pytensor#2220), but @ricardoV94 pointed out it's really PyMC documentation and that the existing {ref}
pymc_pytensornotebook already covers the "graph" part. So I've moved it here and framed it explicitly as a companion to that notebook, placed right after it in the core-notebooks toctree.Summary
A new core-notebooks page, "Understanding PyTensor compilation", for people who reach PyTensor through PyMC rather than using it directly. Where
pymc_pytensorexplains how a model becomes a PyTensor graph, this page picks up where that leaves off and explains what PyTensor does with the graph. It answers two recurring PyMC questions: "why does sampling take a while to start?" (compilation) and "why is the first run slow but later runs faster?" (caching).What's included
backend=/compile_kwargs/config.linker) vs. how the sampler is chosen (nuts_sampler=: pymc/nutpie/numpyro/blackjax), and why they're mostly-independent choices.pytensorintersphinx mapping) for the API-level detail.Implementation notes
.mdpage (the content is explanation, not executable;nb_execution_modeisoffand the core-notebooks tree already contains a non-notebook page).docs/source/images/pytensor_compilation.svg) with the Mermaid source alongside (.mmd), to avoid adding a Mermaid build dependency. Happy to switch to a livesphinxcontrib-mermaiddirective instead if preferred — let me know.pytensorintersphinx inventory); the diagram, dropdowns, and all PyTensor cross-references render/resolve. I couldn't run the fulldocs/sourcebuild locally due to an unrelated pytensor/pymc version mismatch in my env (No module named 'pytensor.graph.traversal'), so a CI/RTD docs build is the real check.Open questions for reviewers
pymc_pytensor, the right home — or should this live elsewhere (e.g. a how-to / explanation section)?Made with Cursor
Made with Cursor