Skip to content

Make PassthroughRNG dispatch survive overlay method-table shadowing#72

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-passthroughrng-overlay-shadowing
May 11, 2026
Merged

Make PassthroughRNG dispatch survive overlay method-table shadowing#72
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-passthroughrng-overlay-shadowing

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor

Closes the root cause behind SciML/JumpProcesses.jl#588.

Summary

PassthroughRNG previously defined only the three no-second-arg methods (rand, randexp, randn). On Julia 1.12+, GPU back ends like CUDA.jl install device-side overlay tables via Base.Experimental.@consistent_overlay. Julia's OverlayMethodTable.findall (Compiler/src/methodtable.jl:73–92) returns overlay matches without consulting the base method table whenever the overlay fully covers the signature:

if nr  1 && result[nr].fully_covers
    # no need to fall back to the internal method table
    return result
end

CUDA.jl's @device_override Random.randexp(rng::AbstractRNG) therefore shadows our specific Random.randexp(::PassthroughRNG) on the device — the more specific base-table method is never seen by inference. The override's body then runs with rng::PassthroughRNG and calls Random.rand(rng, UInt52Raw()). The stdlib Sampler chain for that bottoms out at _rand52(r, rng_native_52(r)) → rand(r, UInt64); PassthroughRNG had no rng_native_52 and no typed-arg rand, so the chain statically reached throw(MethodError, ...) and GPUCompiler refused to lower the kernel:

InvalidIRError: ... resulted in invalid LLVM IR
Reason: unsupported call to an unknown function (call to jl_f_throw_methoderror)
Stacktrace:
 [1] rand        @ Random/src/generation.jl:114
 [2] rand        @ Random/src/Random.jl:255
 [3] randexp     @ CUDACore/src/device/random.jl:339   (device_override)

Confirmed on Julia 1.12.6 by directly calling Random.rand(PassthroughRNG(), Random.UInt52Raw()) on the CPU — the top two frames match the GPU InvalidIRError frames exactly.

Fix

Two forwarding methods so the Sampler chain bottoms out at bare rand(T):

Random.rng_native_52(::PassthroughRNG) = UInt64
Random.rand(rng::PassthroughRNG, ::Type{T}) where {T} = rand(T)

These preserve PassthroughRNG's ''use whatever default_rng() returns here'' semantics — bare rand(T) goes through default_rng(), which GPU back ends device-override to their device RNG (Philox2x32 in CUDA.jl). Verified locally on Julia 1.12.6:

rng_native_52(prng):  UInt64
rand(prng, UInt52Raw()):  2033734290634309481
rand(prng, UInt64):  11777094854860645014
rand(prng, Float32):  0.98086137
rand(prng, Float64):  0.9338644122927825
pois_rand(prng, 3.0):  3
pois_rand(prng, 50.0):  52

Added a regression test (@testset "PassthroughRNG dispatch") covering each of these calls. Bumped version 0.4.7 → 0.4.8.

Test plan

  • Full Pkg.test() green on Julia 1.12.6 — Aqua (10/10), JET static analysis (6/6), ExplicitImports (2/2), BigFloat (401/401), PassthroughRNG dispatch (5/5), Allocation Tests (10/10), and the count/ad/mixed statistical samplers.
  • CI green.

Notes

  • Please ignore until reviewed by @ChrisRackauckas.
  • This doesn't pull in a dependency on RandomNumbers.jl (alternative would have been to make PassthroughRNG <: RandomNumbers.AbstractRNG{UInt64} and inherit the rng_native_52 fallback — heavier and changes the package's dep graph).
  • There is a separate, downstream CUDA.jl issue exposed once this PR unblocks the dispatch chain: @noinline randexp_unlikely in the device randexp override (line 345) triggers julia.get_pgcstack on Julia 1.12 — that needs an upstream fix in CUDA.jl and is not in scope here.

🤖 Generated with Claude Code

PassthroughRNG previously defined only the three no-second-arg methods
(rand, randexp, randn). On Julia 1.12+, GPU back ends like CUDA.jl install
device-side overlay tables via Base.Experimental.@consistent_overlay. Julia's
OverlayMethodTable.findall returns overlay matches *without consulting the
base method table* whenever the overlay fully covers the signature, so an
overlay method like CUDA.jl's `@device_override Random.randexp(rng::AbstractRNG)`
shadows our specific `Random.randexp(::PassthroughRNG)` on the device. The
override's body then runs with rng::PassthroughRNG and calls
`Random.rand(rng, UInt52Raw())`. The stdlib Sampler chain for that bottoms out
at `_rand52(r, rng_native_52(r)) → rand(r, UInt64)`; PassthroughRNG had no
`rng_native_52` and no typed-arg rand, so the chain statically reached
`throw(MethodError, ...)`, which GPUCompiler refuses to lower (see
SciML/JumpProcesses.jl#588 for the original repro).

Add minimal forwarding methods so the chain still reaches bare rand(T):

    Random.rng_native_52(::PassthroughRNG) = UInt64
    Random.rand(rng::PassthroughRNG, ::Type{T}) where {T} = rand(T)

These keep PassthroughRNG's "use whatever default_rng() returns here"
semantics — bare rand(T) goes through default_rng(), which GPU back ends
device-override to their device RNG (Philox2x32 in CUDA.jl). Verified on
Julia 1.12.6 that rand(PassthroughRNG(), UInt52Raw()), rand(PassthroughRNG(),
UInt64), etc. all resolve cleanly after this change. Bump to 0.4.8.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas ChrisRackauckas marked this pull request as ready for review May 11, 2026 00:18
@ChrisRackauckas ChrisRackauckas merged commit 98aee58 into SciML:master May 11, 2026
10 of 13 checks passed
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.

2 participants