Skip to content

Cache compiled model functions#8330

Open
velochy wants to merge 7 commits into
pymc-devs:mainfrom
velochy:memoize-forward
Open

Cache compiled model functions#8330
velochy wants to merge 7 commits into
pymc-devs:mainfrom
velochy:memoize-forward

Conversation

@velochy

@velochy velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Cache compiled model functions on frozen models

Description

pm.sample_posterior_predictive / sample_prior_predictive, logp_dlogp_function, and
the initial-point function recompile on every call, even when the model graph is fixed and
only data values change. This dominates wall time in iterative workflows — projecting a
posterior onto a population in many batches with changing pm.set_data, or repeatedly
calling pm.sample on one model.

Caching is opt-in through a new model transform (as proposed in review):

from pymc.model.transform.optimization import freeze_model

with freeze_model(m) as frozen_m:
    for batch in batches:
        pm.set_data({"x": batch})
        pm.sample_posterior_predictive(idata, predictions=True)  # compiles once
  • freeze_model(model) returns a frozen copy that memoizes the graphs and compiled
    functions it builds (logp/dlogp/d2logp, compile_fn, logp_dlogp_function,
    initial_point, and the forward-sampling function). Because a frozen model cannot be
    mutated, no cache invalidation is needed — graph-mutating methods (register_rv,
    add_coord, set_initval, ...) raise, and the dims/data that any free variable depends
    on are frozen to constants via freeze_dims_and_data, so nothing that initial_point or
    logp_dlogp_function bake in can change.
  • Data (and dims) that only Deterministics and observed variables depend on stay shared:
    pm.set_data value updates and resizes are runtime inputs of the cached functions and
    take effect without recompiling.
  • Custom initial values are transplanted onto the frozen model (the original is untouched).
  • Mutable models are unchanged: no caching, no invalidation — set_data/set_dim and all
    mutation behave exactly as on main.
  • Seeding is decoupled from compilation: cached functions compile seed-independently
    (compile(random_seed=False)) and are reseeded on every compile_fn call, in the same
    order compile itself uses, so a cached function yields the same RNG stream as a fresh
    compile. Functions with RNGs compiled to linkers that detach RNG shared variables at
    compile time (JAX/MLX/PyTorch) cannot be reseeded and are compiled fresh each call;
    RNG-free functions are cached on all backends.
  • compile_forward_sampling_function gains a model= argument so forward sampling routes
    through the cache.

Reusing cached RNG functions on JAX-family backends would need a pytensor-level way to
update a compiled function's detached RNG variables (see pymc-devs/pytensor#2271 for a first
attempt); until then those are compiled per call.

Related Issue

Checklist

  • Pre-commit linting/style checks pass
  • Included tests (freeze partitioning, initval preservation, cache hits, forbidden
    mutation, set_data value/resize reuse, reseeding, JAX bypass, sppc reuse across
    set_data, same-seed reproducibility)
  • Added docstrings and an API docs entry

Type of change

  • New feature / enhancement

@velochy velochy marked this pull request as draft June 16, 2026 07:15
@read-the-docs-community

read-the-docs-community Bot commented Jun 16, 2026

Copy link
Copy Markdown

@velochy velochy marked this pull request as ready for review June 16, 2026 07:19
@velochy

velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@ricardoV94 so this is what Opus came up with. Reviewed the code and it looks quite clean and sensible.

Worth noting a call it made and I agree with is only caching compiled code, not graph construction. Initial graph construction tends to be fast and I don't see a real reason to cache it. LMK if you disagree.

But as you said - with caching, the hard part is being sure the invalidations are all there, and this is really hard to assess without being very intimate with the codebase - so this part definitely needs your review.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.96644% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.73%. Comparing base (c870b5c) to head (dce765a).

Files with missing lines Patch % Lines
pymc/model/core.py 95.06% 12 Missing ⚠️
pymc/data.py 50.00% 2 Missing ⚠️
pymc/dims/model.py 66.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #8330      +/-   ##
==========================================
- Coverage   91.73%   91.73%   -0.01%     
==========================================
  Files         128      128              
  Lines       20697    20775      +78     
==========================================
+ Hits        18987    19057      +70     
- Misses       1710     1718       +8     
Files with missing lines Coverage Δ
pymc/backends/arviz.py 96.39% <100.00%> (ø)
pymc/backends/zarr.py 93.87% <100.00%> (ø)
pymc/model/transform/__init__.py 100.00% <100.00%> (ø)
pymc/model/transform/conditioning.py 95.83% <100.00%> (ø)
pymc/model/transform/optimization.py 100.00% <100.00%> (ø)
pymc/model/transform_values.py 95.65% <100.00%> (ø)
pymc/pytensorf.py 88.94% <100.00%> (ø)
pymc/sampling/deterministic.py 95.65% <100.00%> (ø)
pymc/sampling/forward.py 96.78% <100.00%> (+0.02%) ⬆️
pymc/stats/log_density.py 97.61% <100.00%> (ø)
... and 4 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.

@velochy velochy force-pushed the memoize-forward branch 2 times, most recently from ef13a55 to 42b0dbf Compare June 16, 2026 09:10
@ricardoV94

Copy link
Copy Markdown
Member

It's a bit annoying that set_data must invalidate cache. Many methods don't care about, but then stuff like initial_point and logp_dlopg_function bake the last shape when built. Maybe we want two levels of cache invalidation? Those affected by static shape changes and those not. But it may open more issue than it solves, so I'm leaning towards the more conservative approach you took

@velochy velochy force-pushed the memoize-forward branch 3 times, most recently from 0475a4d to 137aa98 Compare June 16, 2026 14:53
@velochy

velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

It's a bit annoying that set_data must invalidate cache. Many methods don't care about, but then stuff like initial_point and logp_dlopg_function bake the last shape when built. Maybe we want two levels of cache invalidation? Those affected by static shape changes and those not. But it may open more issue than it solves, so I'm leaning towards the more conservative approach you took

Honestly two level caching feels like overkill here. I think fixed shape is likely the most common use case anyway, and it can be used for non-fixed sizes as well by using max shape and using a mask in many other cases.

And, if there is ever a lot of demand, it can be a follow-up PR.

@velochy

velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

In fixing the tests, it seems setting rvs_to_initial_values is a pattern in some tests. I currently solved it by subclassing the dict and giving it a custom setter that invalidates cache (a). The other two options are:
(b) Change the tests and hope no-one else directly edits this dict
(c) Don't cache initvals compilation anyway

I don't think (b) is a good option, but (c) is worth considering as initvals graph is likely to be tiny under most use cases I can think of (including the ones we have, to my knowledge) and it would reduce change surface a fair bit.

@ricardoV94 your thoughts?

@ricardoV94

Copy link
Copy Markdown
Member

We should change the tests, there's one API for updating initvals and that's the one that should be used. Similar for all dicts/list mutation. Some codepaths also used to override rvs_to_transforms, but we have a model transform for that, which is the right API

@ricardoV94

Copy link
Copy Markdown
Member

There are other dicts/lists in a Model that people may use, it's not unique to initvals imo

@velochy

velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

So you want to go down that (b) route and have the thing break if users ever try to directly modify the internal dicts instead of using the appropriate setters? You sure?

@ricardoV94

Copy link
Copy Markdown
Member

I want to see how that turns out. Not sure at all :D

@velochy

velochy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Nice to know you like living on the edge :D
It is definitely the cleanest approach. But both (a) and (c) were definitely safer.

Then again - we are talking about bugs that only come up if someone samples the same model twice, which is something that does not come up all that often, and usually with power users who can likely figure it out. So it's most likely fine.

@velochy

velochy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@ricardoV94 this is ready from my side and tests are green. Let me know if you want any changes

@velochy

velochy commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@ricardoV94 gentle reminder this is still waiting. It solved a pretty big performance issue for us, so it would be nice if it got merged somewhere in the next few weeks :)

@ricardoV94

Copy link
Copy Markdown
Member

Bumped closer to the top of my stack ;)

Comment thread pymc/model/core.py
return fn

@memoize
def _compile_fn(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one gotcha because you decided not to cache logp/dlogp outputs is that calling model.compile_fn(model.value_vars, model.logp() will not cache because the logp variables are fresh every time

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument not to do it wasn't super convincing to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI agrees (as it often does). Had it implement caching logp too, and check for any mutation risks. It flagged rewrite_pregrad but claimed it tested it and found it not to be an issue in practice.

Comment thread tests/model/test_core.py Outdated

f1 = m.compile_fn(mu, inputs=[], point_fn=False)
with m:
m.set_data("x", np.ones(3))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one foot gun now is if users do x.set_value() directly. I'm really bummed that updating shared variable values has to invalidate cache... and I'm thinking whether we should have one layer of work on the user. an explicit cached_model = model.cache() so we have a place to add docstrings telling users about how cache works and cache invalidation?

Technically setting data without changing size can also invalidate initial point as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we don't compile initval, this seems to not be an issue.
As for documentation, added it to the "Model" right now. Seems like a decently right place, but maybe a bit too overloaded already. LMK if this works for you or if you want it separated somehow

Comment thread tests/model/test_core.py Outdated
with pm.Model() as m:
pm.Normal("x", 0, 1, size=3, initval="prior") # random initval

ip1 = m.initial_point(1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does initial_point seed only accept integer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

integer or a list or ndarray of ints. Generator not supported, but supposedly this is the present state and not a change in this PR.

Comment thread tests/sampling/test_forward.py
Comment thread pymc/model/core.py Outdated

The cache is cleared automatically whenever the model graph is mutated through its
public API (``register_rv``, ``add_coord(s)``, ``set_dim``, ``set_initval``,
``register_data_var``, ...). A value-only ``set_data`` keeps the cache (data is a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(data is a runtime input), makes it sound like it is safe. But set_data can influence things that are cached like initial_point and even the shape of variables, without its shape also changing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I also managed to forget that size can be determined at run-time based on data values provided, so resize detection is not foolproof.

AI recommended clearing the size-baking caches (_logp_dlogp_function) on every set_data, but keeping _compile_fn (the forward-sampling/headline cache) when the data's own shape is unchanged. Sounds like it makes sense, but I am far too much out of my depth to be sure here. How does it sound to you?

@velochy

velochy commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

The test failures do look like they are main branch flakes, as they replicate on current main as well.

Anything here left for me to do?

@velochy

velochy commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@ricardoV94 any chance youre ok just bypassing caching for jax and calling it good enough?

@ricardoV94

Copy link
Copy Markdown
Member

@ricardoV94 any chance youre ok just bypassing caching for jax and calling it good enough?

I have two concerns:

  1. Re-seeding JAX/MLX/PyTorch functions (supporting JAX now is enough)
  2. Changing a variable dim that affects a variable in initial_point

I am leaning to having a variant of freeze_dims_and_data (freeze_model?), that returns a frozen/cacheable model, with the constraint that you can only set_data that's upstream of Deterministic/ObservedRVs (all other shared/dims get frozen). All these calls that "invalidate" caching are simply not permitted in that subclass.

For now, whenever we compile a function that has Shared RNGs and JAX/MLX/PyTorch linker we do not cache it. I think the Linker should be the one with the method that allows updating these variables from fresh regular variable values (and call tipyfy or whatever) as it already has the logic to sever/handle these variables during the first compilation

@velochy

velochy commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Making cacheability of a model a property set similar to freezing instead of caching behind the scenes makes sense to me.

Do you want it to just be a flag that allows caching and still compiles on first use, or should freeze_model already compile as well? Case can be made for both (and for my use case I have no preference) but for on-line usage i imagine it might be slightly better if compile would happen before putting into production use. Then again, code likely cleaner the other way, so your call.

Allowing resizing only on post-observation dimensions seems straightforward enough as well.

Still not sure I understand the reseeding idea, but i understand you would be ok not caching on the complex backends, at least in the first pass?

I'll cook something up w AI help later today.

@ricardoV94

ricardoV94 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Do you want it to just be a flag that allows caching and still compiles on first use, or should freeze_model already compile as well?

On first use, you don't know which functions the user will need to compile.

Still not sure I understand the reseeding idea, but i understand you would be ok not caching on the complex backends, at least in the first pass?

I think we just need fn.get_shared to respect the shared variables from the linker, so you can find them, and then when setting them you need to call tipyfy (on the respective linker) backend. I dunno the most ergonomic way to do that but shouldn't be too hard. What you were suggesting wasn't super off, it was just too RNG specific and was doing one seed -> multiple generator logic that's beyond PyTensor

@velochy velochy force-pushed the memoize-forward branch 2 times, most recently from b82c4cf to e10e5ad Compare July 9, 2026 20:08
@velochy

velochy commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

I am leaning to having a variant of freeze_dims_and_data (freeze_model?), that returns a frozen/cacheable model,

This is now implemented (and I had AI re-write the PR description to match). Let me know your thoughts @ricardoV94

Comment thread pymc/model/transform/optimization.py Outdated
Comment thread pymc/model/core.py Outdated
linker = get_mode(mode).linker
except Exception:
return False
return type(linker).__name__ in ("JAXLinker", "MLXLinker", "PytorchLinker")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not isinstance?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Built a solution with isinstance, but it might genuinely not be the right call here, as it relies on importing the linker modules that might not be present in all systems so it creates this ugliness:

return any(
    (module := sys.modules.get(module_path)) is not None
    and isinstance(linker, getattr(module, class_name))
    for module_path, class_name in _RNG_DETACHING_LINKERS
)

Both options seem bad so your call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the linkers are always available

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(reply drafted by AI)

You're right — verified the linker classes import fine on a machine without mlx/torch installed. Simplified to a plain isinstance(linker, JAXLinker | MLXLinker | PytorchLinker).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing if we can have an okay compromise for multi-backend types in the meantime

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pymc-devs/pytensor#2285

Do I adjust the PR with the assumption that functionality will get merged, or will that be a follow-up fix once it is in?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the adjustment is just to be more permissive yeah go ahead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(reply drafted by AI)

Kept the current isinstance check for now, since a released pymc can't import the new is_backend_divergent API until pytensor#2285 lands in a release. Once it does, this whole bypass can go away and RNG functions can be cached on JAX/MLX/PyTorch too — happy to do that as the follow-up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no is_backend_divergent api need, the PR makes seeding work across the different backends

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(reply drafted by AI)

Ah, got it — thanks for the correction. So #2285 makes reseeding work directly on JAX/MLX/PyTorch (no is_backend_divergent needed), meaning once it's in a pytensor release we can drop this bypass entirely and cache RNG functions on every backend. Kept the isinstance bypass for now since it can't import unreleased pytensor; removing it is the clean follow-up.

Comment thread pymc/model/core.py Outdated
velochy and others added 2 commits July 10, 2026 12:09
`pm.sample_posterior_predictive` / `sample_prior_predictive`,
`logp_dlogp_function`, and the initial-point function recompile on every
call, even when the model graph is fixed and only data values change.
This dominates wall time in iterative workflows — projecting a posterior
onto a population in many batches with changing `pm.set_data`, or
repeatedly calling `pm.sample` on one model.

Caching is opt-in through a new `freeze_model` transform:

    from pymc.model.transform.optimization import freeze_model

    with freeze_model(m) as frozen_m:
        for batch in batches:
            pm.set_data({"x": batch})
            pm.sample_posterior_predictive(idata, predictions=True)  # compiles once

`freeze_model` returns a frozen copy whose compiled graphs and functions
(`logp`/`dlogp`/`d2logp`, `compile_fn`, `logp_dlogp_function`,
`initial_point`, and the forward-sampling function) are memoized. Because
a frozen model cannot be mutated, no cache invalidation is needed:
graph-mutating methods raise, and the dims and data that any free
variable depends on are frozen to constants via `freeze_dims_and_data`,
so nothing that `initial_point` or `logp_dlogp_function` bake in can
change. Data (and dims) that only Deterministics and observed variables
depend on stay shared — `pm.set_data` value updates and resizes are
runtime inputs of the cached functions and take effect without
recompiling. Custom initial values are transplanted onto the frozen
model. Mutable models are unchanged: no caching, no invalidation.

Seeding is decoupled from compilation: cached functions compile
seed-independently (`compile(random_seed=False)`) and `compile_fn`
reseeds afterwards in the same order `compile` uses, so a cached function
yields the same RNG stream as a fresh compile. Functions with RNGs
compiled to linkers that detach RNG shared variables (JAX/MLX/PyTorch)
cannot be reseeded and are compiled fresh each call; RNG-free functions
are cached on every backend. `compile_forward_sampling_function` gains a
`model=` argument so forward sampling routes through the cache.

Closes pymc-devs#7815. Related to pymc-devs#7816 (derived from @ricardoV94's draft).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r check

Introduce a FrozenModel subclass instead of a `_frozen` flag on Model.
Model carries no caching or freezing machinery at all: FrozenModel
overrides the cacheable methods with locally_cachedmethod-wrapped
versions and the graph-mutating methods with stubs that raise.
freeze_model rebinds the reconstructed model to this class. A full
abstract-base split (mutation methods absent rather than raising) would
break isinstance(model, Model) for downstream code, so the subclass
shares Model's interface and type.

Reject symbolic initial values in freeze_model: they reference variables
of the original graph and cannot be transplanted onto the frozen copy.
None, strategy strings, and constants remain supported.

Identify RNG-detaching linkers by isinstance instead of class-name
strings, looking the classes up in sys.modules so optional backends are
never imported by the check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
velochy and others added 2 commits July 10, 2026 13:05
… isinstance

Split the model hierarchy per the abstract/final pattern: BaseModel
(abstract) holds everything shared, Model adds the graph-mutating
methods, and FrozenModel adds caching. Mutation methods do not exist on
FrozenModel at all, and neither abstract instantiation nor direct
FrozenModel construction is possible. FrozenModel is no longer a Model
subclass; runtime code paths carry no isinstance(Model) checks, so
sampling and forward functions work with both finals.

Also simplify the RNG-detaching-linker check to a direct isinstance:
the JAX/MLX/PyTorch linker classes are importable without their
frameworks installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
modelcontext now returns BaseModel, so functions that only read the
model (arviz/zarr converters, forward sampling, compute_deterministics,
value-transform evaluation, log-density stats, remove_value_transforms)
annotate their model arguments as BaseModel — they work with both
finals. Functions that register new variables (pm.Data and its dims
variant) accept BaseModel but narrow to Model at runtime, raising an
informative TypeError on a FrozenModel instead of an AttributeError.
The __class__ rebinding in freeze_model gets a targeted ignore plus a
cast for the return type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ricardoV94

Copy link
Copy Markdown
Member

I'm seeing that jax crash elsewhere, we may need to skip that test

for name, initval in initial_values.items():
frozen_model.set_initval(frozen_model[name], initval)

frozen_model.__class__ = FrozenModel # type: ignore[assignment]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works xD?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(reply drafted by AI)

It does — it's the standard idiom for retyping an instance in place. Model and FrozenModel are both pure-Python subclasses of BaseModel with the same instance layout (no __slots__), so reassigning __class__ only changes method resolution: the mutators become unavailable and the compiled functions become the cached overrides. The instance __dict__ (all the model state built by the round-trip) is untouched, and FrozenModel.__init__ is never called (it exists only to block direct construction). Added a comment to that effect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Real Margus here)
Yeah I'm surprised too, but that's actually pretty useful to know in the future :D

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a test already that isinstance(FrozenModel) and not isinstance(Model). I know there is for trying to use methods that are not available after freezing (right?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(reply drafted by AI)

Yes — test_mutation_forbidden_on_frozen now asserts isinstance(fm, FrozenModel), isinstance(fm, BaseModel), and not isinstance(fm, pm.Model) explicitly, right next to the AttributeError checks for the removed mutators. test_abstract_and_final_classes covers that neither BaseModel nor FrozenModel can be constructed directly.

Move the initial-value preservation from freeze_model into
freeze_dims_and_data so all callers of the transform benefit (requested
in review). fgraph_from_model drops and rejects initial values; the
transform now clears them for the round-trip and transplants the
constant and strategy-string ones back onto the rebuilt model, rejecting
symbolic ones (which reference the original graph). freeze_model becomes
a thin wrapper that retypes the result to FrozenModel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@velochy

velochy commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

As general context: I'm burning my free Fable credits here, and having AI answer directly because - well, Fable is really surprisingly good. I'm still trying to keep up with what is going on myself, but it makes sense it writes the answers to you directly rather than me just rewording everything (and potentially misunderstanding something).

@ricardoV94

Copy link
Copy Markdown
Member

As general context: I'm burning my free Fable credits here, and having AI answer directly because - well, Fable is really surprisingly good. I'm still trying to keep up with what is going on myself, but it makes sense it writes the answers to you directly rather than me just rewording everything (and potentially misunderstanding something).

Can you prompt it to add a good joke at the end of each reply. If I don't get human interaction at least a giggle?

@ricardoV94

Copy link
Copy Markdown
Member

Cherry pick fc3f3c3 to fix the CI

ricardoV94 and others added 2 commits July 10, 2026 22:07
@velochy

velochy commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

(comment drafted by AI)

Cherry-picked fc3f3c3 (Install jax from pip) — thanks, that matches what I'd traced: conda-forge's jaxlib 0.10.x CPU build segfaults in XLA codegen on the runners while the PyPI wheels compile the same graphs fine.

Why did the FrozenModel make such a great teammate? It never gave anyone the cold shoulder about recompiling — it just learned to let it go. ❄️

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.

Cache model functions for iterative workflows

2 participants