Add model_to_float32 / model_to_float64 model transforms - #8402
Conversation
Graph-wide float dtype conversion: data (constants and shared variables), RandomVariables, inner graphs of SymbolicRandomVariables, value variables and explicit casts. Useful to sample models in single precision, where pytensor's floatX alone cannot prevent float64 upcasting from variables already baked into the model graph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o_float32
Ops can store dtype aliases (AllocEmpty{dtype='float'}); normalize before
comparing. Value transforms travel as op metadata and may bake float64
constants into logp/initial-point graphs; probe them under the target floatX
and wrap in a casting proxy when they leak the wrong dtype.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A transform embedding float64 constants (e.g. precomputed basis matrices) left a float64 island in logp/dlogp: its matmuls and their gradients ran in double precision behind casts. Run the wrapped transform's graphs through the same float-casting conversion so embedded constants convert too; the output cast remains as a last resort. On a production model this cut float64-touching nodes in the compiled logp+dlogp graph from 354/1640 to 4/1638. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
To be very clear - I do not think this is a great idea as it stands. It's a proof of concept to what @jessegrabowski proposed off-hand in discord. For one, you probably want this as a setting for freeze_dims_and_data or as a standard pre-processing step before sampling all models (to unify inputs to either F32 or F64 world) instead of just stand-alone userspace functions. Also, it is worth pointing out that the functions cannot actually convert all inputs - in quite a few cases, supposedly. Quoting my AI friend:
But this should serve as a proof-of-concept in terms of what it would take, as a discussion starter. |
Documentation build overview
9 files changed ·
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8402 +/- ##
==========================================
+ Coverage 91.87% 91.90% +0.02%
==========================================
Files 128 128
Lines 21256 21363 +107
==========================================
+ Hits 19530 19633 +103
- Misses 1726 1730 +4
🚀 New features to boost your workflow:
|
Add
model_to_float32/model_to_float64model transformsWith
floatX="float32", pytensor still permits mixed precision — unlike JAX, which refuses to upcast without x64 enabled. Any float64 constant,pm.Data, or RV already baked into the model graph silently upcasts everything downstream, so models "built float64" never actually sample in single precision — in factpm.sampleunder float32 flags fails outright on such models, because the init jitter builds float32 RVs against the float64 graph. This PR makes precision a one-call property of the model instead of something threaded through every input:model_to_float32(model)recreates the model via the fgraph round-trip with every float64 variable cast to float32: data (constants andpm.Datashareds), RandomVariables and other ops with a baked output dtype (dtype aliases likeAllocEmpty{dtype='float'}are normalized), the inner graphs of SymbolicRandomVariables (rebuilt recursively, with frozen static shapes restored viaspecify_shape), value variables, and explicit.astype("float64")casts. Value transforms are probed under the targetfloatX, and any transform that embeds foreign-dtype constants (e.g. precomputed float64 basis matrices) is wrapped so its forward/backward graphs are converted too. Integer, boolean, RNG and dim-length variables are untouched; constant and strategy-string initial values are preserved.model_to_float64is the inverse, sharing the same machinery.Validated end-to-end on a production survey-modeling pipeline sampled with nutpie: logp matches float64 to ~1e-8 relative, posteriors agree within MC error, and the optimized logp+dlogp graphs come out fully float32. Speed is model- and backend-dependent: on a CPU/numba backend these (gather/indexing-bound) models gain only a few percent per gradient, measured with interleaved A/B timing; the larger wins are expected on GPU backends and for compute-bound models.
Limitations: the converted model should be compiled under
floatX="float32", since logp graphs are built lazily and Python-float constants introduced there followfloatX(documented in the docstring).pm.set_dataon the converted model expects float32 arrays. The OpFromGraph rebuild currently reaches into private attrs (fgraph,input_types,output_types, gradient caches); happy to move that behind a proper pytensor helper as a follow-up if preferred.Tests cover dtype conversion across RV kinds (incl.
ZeroSumNormal, Deterministics, observed data), logp/dlogp equality vs float64, sampling smoke incl. nutpie, static-shape and transform preservation, foreign-dtype transform wrapping, initval preservation, explicit-cast redirection, and the float32→float64 round trip. Docs: added todocs/source/api/model/optimization.rstwith a usage example.