From 9028a6ce764b9c0f0b8ff7770bec867e1491d30c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:07:45 +0000 Subject: [PATCH 1/4] Initial plan From c16abcc9d4a0762cfa942955fcc2bf22ce10aa76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:13:37 +0000 Subject: [PATCH 2/4] Add global lock to build_callable for thread safety on Julia 1.10 Serialise build_callable under a ReentrantLock so that concurrent threads cannot race during first-time callable IR generation and cache population, which caused segfaults in threaded Particle Gibbs sampling on Julia 1.10. --- src/copyable_task.jl | 57 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index febf294e..fc86704c 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -157,6 +157,8 @@ Returns a `MistyClosure` which is used by `TapedTask` to implement the the current world age, will make a copy of an existing `MistyClosure`. If not, will derive it from scratch (derive the IR + compile it etc). """ +const build_callable_lock = ReentrantLock() + function build_callable(sig::Type{<:Tuple}) if sig <: Tuple{typeof(produce),Any} msg = """ @@ -165,32 +167,37 @@ function build_callable(sig::Type{<:Tuple}) TapedTask from that function.""" throw(ArgumentError(msg)) end - world_age = Base.get_world_counter() - key = CacheKey(world_age, sig) - if haskey(mc_cache, key) - return fresh_copy(mc_cache[key]) - else - ir_results = Base.code_ircode_by_type(sig) - if isempty(ir_results) - _throw_ir_error(sig) - end - ir = ir_results[1][1] - # Check whether this is a varargs call. - isva = which(sig).isva - bb, refs, types = derive_copyable_task_ir(BBCode(ir)) - bb, refs = eliminate_refs(bb, refs) - unoptimised_ir = IRCode(bb) - @static if VERSION > v"1.12-" - # This is a performance optimisation, copied over from Mooncake, where setting - # the valid world age to be very strictly just the current age allows the - # compiler to do more inlining and other optimisation. - unoptimised_ir = set_valid_world!(unoptimised_ir, world_age) + lock(build_callable_lock) + try + world_age = Base.get_world_counter() + key = CacheKey(world_age, sig) + if haskey(mc_cache, key) + return fresh_copy(mc_cache[key]) + else + ir_results = Base.code_ircode_by_type(sig) + if isempty(ir_results) + _throw_ir_error(sig) + end + ir = ir_results[1][1] + # Check whether this is a varargs call. + isva = which(sig).isva + bb, refs, types = derive_copyable_task_ir(BBCode(ir)) + bb, refs = eliminate_refs(bb, refs) + unoptimised_ir = IRCode(bb) + @static if VERSION > v"1.12-" + # This is a performance optimisation, copied over from Mooncake, where setting + # the valid world age to be very strictly just the current age allows the + # compiler to do more inlining and other optimisation. + unoptimised_ir = set_valid_world!(unoptimised_ir, world_age) + end + optimised_ir = optimise_ir!(unoptimised_ir) + mc_ret_type = callable_ret_type(sig, types) + mc = misty_closure(mc_ret_type, optimised_ir, refs...; isva=isva, do_compile=true) + mc_cache[key] = mc + return mc, refs[end] end - optimised_ir = optimise_ir!(unoptimised_ir) - mc_ret_type = callable_ret_type(sig, types) - mc = misty_closure(mc_ret_type, optimised_ir, refs...; isva=isva, do_compile=true) - mc_cache[key] = mc - return mc, refs[end] + finally + unlock(build_callable_lock) end end From 8acd5deeefc872dddc3a27687c3f43b10c5fd5bc Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Fri, 5 Jun 2026 12:53:18 +0100 Subject: [PATCH 3/4] Fix docs, format, and Turing integration CI failures - Move `build_callable_lock` above the docstring so the docstring attaches to `build_callable` again (fixes Documenter `docs_block`/`missing_docs`) - Wrap the `misty_closure` call to stay within blue style's margin at its new indentation (fixes JuliaFormatter check) - Update the SMC test to FlexiChains' `(niters, nchains)` layout: the chain count is `size(chain, 2)`, not `size(chain, 3)` Co-Authored-By: Claude Opus 4.8 (1M context) --- src/copyable_task.jl | 8 +++++--- test/integration/turing/main.jl | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index fc86704c..17c3f022 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -149,6 +149,8 @@ function generate_ir(stage::Symbol, fargs...; kwargs...) throw(ArgumentError("unknown stage $stage")) end +const build_callable_lock = ReentrantLock() + """ build_callable(sig::Type{<:Tuple}) @@ -157,8 +159,6 @@ Returns a `MistyClosure` which is used by `TapedTask` to implement the the current world age, will make a copy of an existing `MistyClosure`. If not, will derive it from scratch (derive the IR + compile it etc). """ -const build_callable_lock = ReentrantLock() - function build_callable(sig::Type{<:Tuple}) if sig <: Tuple{typeof(produce),Any} msg = """ @@ -192,7 +192,9 @@ function build_callable(sig::Type{<:Tuple}) end optimised_ir = optimise_ir!(unoptimised_ir) mc_ret_type = callable_ret_type(sig, types) - mc = misty_closure(mc_ret_type, optimised_ir, refs...; isva=isva, do_compile=true) + mc = misty_closure( + mc_ret_type, optimised_ir, refs...; isva=isva, do_compile=true + ) mc_cache[key] = mc return mc, refs[end] end diff --git a/test/integration/turing/main.jl b/test/integration/turing/main.jl index 20ff1671..468f6f83 100644 --- a/test/integration/turing/main.jl +++ b/test/integration/turing/main.jl @@ -15,7 +15,7 @@ model = f() @testset "SMC" begin chain = sample(StableRNG(468), model, SMC(), 100; progress=false) @test size(chain, 1) == 100 - @test size(chain, 3) == 1 + @test size(chain, 2) == 1 end @testset "PG" begin From d28c64d3589d265d0d2249f6ec9040383435b2b8 Mon Sep 17 00:00:00 2001 From: Hong Ge Date: Mon, 15 Jun 2026 11:05:32 +0100 Subject: [PATCH 4/4] Move seed_id! under build_callable lock seed_id! mutates the shared global _id_count, the same structure the ID() constructor mutates during IR derivation. Calling it from TapedTask ran it outside build_callable_lock, so it could race with ID generation on another thread and crash. Move it into the cache-miss branch of build_callable, under the lock, alongside the ID() calls it pairs with. Addresses review feedback from @sunxd3. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/copyable_task.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/copyable_task.jl b/src/copyable_task.jl index 17c3f022..dfc878b4 100644 --- a/src/copyable_task.jl +++ b/src/copyable_task.jl @@ -174,6 +174,9 @@ function build_callable(sig::Type{<:Tuple}) if haskey(mc_cache, key) return fresh_copy(mc_cache[key]) else + # Reset the ID counter under the lock: only this (cache-miss) branch generates + # `ID`s, and both `seed_id!` and `ID()` mutate the shared `_id_count`. + seed_id!() ir_results = Base.code_ircode_by_type(sig) if isempty(ir_results) _throw_ir_error(sig) @@ -416,7 +419,6 @@ function TapedTask(taped_globals::Any, fargs...; kwargs...) """ end all_args = isempty(kwargs) ? fargs : (Core.kwcall, getfield(kwargs, :data), fargs...) - seed_id!() # a BBCode thing. mc, count_ref = build_callable(typeof(all_args)) return TapedTask(taped_globals, all_args, mc, count_ref) end