@@ -439,6 +439,74 @@ def fun():
439439
440440 self .assertFalse (mx .allclose (fun (), fun (), 1e-2 , 1e-2 ))
441441
442+ def test_compile_rng_across_threads (self ):
443+ # A function compiled with inputs/outputs=mx.random.state on one thread
444+ # must still use (and advance/seed) the calling thread's RNG state when
445+ # invoked from another thread, whether captured directly or nested.
446+
447+ # The state sentinel is a single global object shared across threads.
448+ state_from_thread = {}
449+
450+ def grab ():
451+ state_from_thread ["s" ] = mx .random .state
452+
453+ t = threading .Thread (target = grab )
454+ t .start ()
455+ t .join ()
456+ self .assertIs (mx .random .state , state_from_thread ["s" ])
457+
458+ direct = partial (mx .compile , inputs = mx .random .state , outputs = mx .random .state )(
459+ lambda : mx .random .uniform (shape = (10 , 10 ))
460+ )
461+
462+ nested_state = [{"unused" : mx .array (0.0 )}, mx .random .state ]
463+ nested = partial (mx .compile , inputs = nested_state , outputs = nested_state )(
464+ lambda : mx .random .uniform (shape = (10 , 10 ))
465+ )
466+
467+ for fun in (direct , nested ):
468+ results = {}
469+
470+ def worker ():
471+ with mx .stream (mx .cpu ):
472+ a = fun ()
473+ b = fun ()
474+ results ["advances" ] = not bool (mx .allclose (a , b , 1e-2 , 1e-2 ).item ())
475+ mx .random .seed (42 )
476+ c = fun ()
477+ mx .random .seed (42 )
478+ d = fun ()
479+ results ["seed_reproducible" ] = bool (mx .allclose (c , d ).item ())
480+ mx .random .seed (1234 )
481+ e = fun ()
482+ results ["seed_changes" ] = not bool (
483+ mx .allclose (c , e , 1e-2 , 1e-2 ).item ()
484+ )
485+
486+ t = threading .Thread (target = worker )
487+ t .start ()
488+ t .join ()
489+
490+ self .assertTrue (results ["advances" ])
491+ self .assertTrue (results ["seed_reproducible" ])
492+ self .assertTrue (results ["seed_changes" ])
493+
494+ def test_compile_state_capture_with_rng_updates_in_place (self ):
495+ # Capturing mx.random.state alongside other state via outputs= must not
496+ # break in-place updates of the other captured containers.
497+ counter = {"v" : mx .array (0.0 )}
498+ state = [counter , mx .random .state ]
499+
500+ @partial (mx .compile , inputs = state , outputs = state )
501+ def step ():
502+ counter ["v" ] = counter ["v" ] + 1.0
503+ return mx .random .uniform (shape = (2 ,))
504+
505+ for _ in range (3 ):
506+ step ()
507+ mx .eval (counter ["v" ])
508+ self .assertEqual (counter ["v" ].item (), 3.0 )
509+
442510 def test_compile_kwargs (self ):
443511 @mx .compile
444512 def fun (x , y , z ):
0 commit comments