From 5ee68d9ee18d025ddb9199eb7e474c5c657b1916 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 18 Jun 2026 11:26:01 +0530 Subject: [PATCH] fix: avoid world-age/serialization issues in distributed workflows Co-authored-by: Claude --- .../src/systems/codegen_utils.jl | 37 ++++++++++++++++++- lib/ModelingToolkitBase/test/odesystem.jl | 16 ++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/ModelingToolkitBase/src/systems/codegen_utils.jl b/lib/ModelingToolkitBase/src/systems/codegen_utils.jl index 602f0f8ccc..1e27c7e9b9 100644 --- a/lib/ModelingToolkitBase/src/systems/codegen_utils.jl +++ b/lib/ModelingToolkitBase/src/systems/codegen_utils.jl @@ -137,7 +137,16 @@ function eval_or_rgf(expr::Expr; eval_expression = false, eval_module = @__MODUL if eval_expression return eval_module.eval(expr) else - return drop_expr(RuntimeGeneratedFunction(eval_module, eval_module, expr)) + # Only function-definition expressions benefit from RuntimeGeneratedFunction (avoids + # world-age issues with new methods). Module-level references such as + # `:(ModelingToolkitBase._oop_unimplemented)` are not function definitions; wrapping + # them in an RGF would fail. Evaluate them directly instead — no new method is + # introduced so there is no world-age concern. + if Meta.isexpr(expr, :function) || Meta.isexpr(expr, :->) + return drop_expr(RuntimeGeneratedFunction(eval_module, eval_module, expr)) + else + return eval_module.eval(expr) + end end end @@ -595,12 +604,36 @@ Base.@nospecializeinfer function build_function_wrapper( end optimize = resolve_optimize_option(optimize) - return Symbolics.codegen_function(ir, expr, args; wrap_code, similarto, cse, optimize, kwargs...) + result = Symbolics.codegen_function(ir, expr, args; wrap_code, similarto, cse, optimize, kwargs...) + # When iip_config disables one side, Symbolics generates an anonymous `unimplemented` + # function expression. Replace it here — where we know which side is disabled — with a + # reference to a named module-level function so all downstream paths (expression=Val{true}, + # eval_expression=true, distributed serialization) get a stable, serializable callable. + iip_config = get(kwargs, :iip_config, (true, true)) + if result isa NTuple{2, Expr} + oop_expr, iip_expr = result + if !iip_config[1] + oop_expr = OOP_UNIMPLEMENTED_EXPR + end + if !iip_config[2] + iip_expr = IIP_UNIMPLEMENTED_EXPR + end + result = (oop_expr, iip_expr) + end + return result end resolve_optimize_option(x) = x resolve_optimize_option(::Nothing) = nothing +# Module-level fallback functions for the disabled side of an `iip_config` pair. +# Using named module-level functions ensures correct serialization across all codegen paths +# (expression=Val{true} evaluated by the user, eval_expression=true, distributed workers). +_oop_unimplemented(args...) = throw(Symbolics.FunctionUnimplementedError("out-of-place")) +_iip_unimplemented(args...) = throw(Symbolics.FunctionUnimplementedError("in-place")) +const OOP_UNIMPLEMENTED_EXPR = :($ModelingToolkitBase._oop_unimplemented) +const IIP_UNIMPLEMENTED_EXPR = :($ModelingToolkitBase._iip_unimplemented) + """ $(TYPEDEF) diff --git a/lib/ModelingToolkitBase/test/odesystem.jl b/lib/ModelingToolkitBase/test/odesystem.jl index fad913c820..3f81ddd51e 100644 --- a/lib/ModelingToolkitBase/test/odesystem.jl +++ b/lib/ModelingToolkitBase/test/odesystem.jl @@ -110,6 +110,22 @@ f.f(du, u, p, 0.1) @test du == [4, 0, -16] @test_throws Symbolics.FunctionUnimplementedError f.f(u, p, 0.1) +# check that iip_config with expression = Val{true} produces serializable functions +# (regression test for https://github.com/SciML/ModelingToolkit.jl/issues/4464) +f_iip_expr = ODEFunction(de; iip_config = (false, true), expression = Val{true}) +f_iip_from_expr = eval(f_iip_expr) +# verify the OOP stub is the stable module-level function (not a Main closure) +@test f_iip_from_expr.f.f_oop === ModelingToolkitBase._oop_unimplemented +# verify round-trip serialization works (simulates distributed usage) +@testset "Issue#4464" begin + using Serialization + buf = IOBuffer() + serialize(buf, f_iip_from_expr) + seekstart(buf) + f2 = deserialize(buf) + @test f2.f.f_oop === ModelingToolkitBase._oop_unimplemented +end + #check iip f = eval(ODEFunction(de; expression = Val{true})) f2 = ODEFunction(de)