@@ -426,31 +426,62 @@ class SimpleJitFn:
426426 donate_argnames : frozenset [str ]
427427 graph : bool
428428 update_shardings : tuple [tp .Any , ...]
429+ captured_info : tp .Optional [list [tp .Any ]]
429430
430431 def __post_init__ (self ):
431432 functools .update_wrapper (self , self .f , updated = ())
433+ # When captures are prepended as an extra argument, the original
434+ # function's signature no longer matches the physical call signature.
435+ # Remove __wrapped__ so inspect.signature (used by JAX for validation
436+ # of static_argnums / donate_argnums) sees *args instead.
437+ if self .captured_info :
438+ delattr (self , '__wrapped__' )
432439
433440 @extract .treemap_copy_args
434441 def __call__ (self , * args , ** kwargs ):
435- current , snapshot = extract .snapshot (
436- labeled (args = args , kwargs = kwargs )
437- )
442+ if self .captured_info :
443+ captured_args = args [0 ]
444+ user_args = args [1 :]
445+ captured_shardings = self .in_shardings [0 ] if isinstance (self .in_shardings , tuple ) else None
446+ user_shardings = self .in_shardings [1 :] if isinstance (self .in_shardings , tuple ) else self .in_shardings
447+ current , snapshot = extract .snapshot (
448+ labeled (captured_args = captured_args , args = user_args , kwargs = kwargs )
449+ )
450+ else :
451+ user_args = args
452+ current , snapshot = extract .snapshot (
453+ labeled (args = user_args , kwargs = kwargs )
454+ )
438455 if self .graph :
439456 args , kwargs = extract .from_tree2 ((args , kwargs ))
440- out = self .f (* args , ** kwargs )
457+ if self .captured_info :
458+ captured_args = args [0 ]
459+ user_args = args [1 :]
460+ f = extract .replace_closure_cells (
461+ self .f , self .captured_info , captured_args )
462+ else :
463+ user_args = args
464+ f = self .f
465+ out = f (* user_args , ** kwargs )
441466 if self .graph :
442467 out = extract .to_tree2 (out , prefix = self .out_shardings )
443468 extract .check_no_aliases ('jit' , ** current , out = out , check = ['out' ])
444469 def keep_fn (jax_path , prefix , c , s ):
445470 if extract .variable_changed (c , s ):
446471 return True
447472 arg_type , arg_key , * _ = graphlib .jax_to_nnx_path (jax_path )
473+ if arg_type == 'captured_args' :
474+ return True
448475 if arg_type == 'args' :
449476 return arg_key in self .donate_argnums
450477 else : # arg_type == 'kwargs':
451478 return arg_key in self .donate_argnames
479+ if self .captured_info :
480+ prefix = labeled (captured_args = captured_shardings , args = user_shardings , kwargs = None )
481+ else :
482+ prefix = labeled (args = self .in_shardings , kwargs = None )
452483 updates = extract .get_updates (
453- current , snapshot , prefix = labeled ( args = self . in_shardings , kwargs = None ) ,
484+ current , snapshot , prefix = prefix ,
454485 known_prefixes = self .update_shardings , keep_fn = keep_fn
455486 )
456487 return out , updates
@@ -533,6 +564,9 @@ def __init__(
533564 self .partial_args = partial_args
534565 self .graph = graph
535566
567+ # Capture closure nodes once at construction time
568+ self ._captured_info = extract .find_captured_nodes (fun )
569+
536570 resolved = _resolve_argnums (fun , static_argnums , static_argnames )
537571 if isinstance (in_shardings , (tuple , list )) and resolved :
538572 expanded = list (in_shardings )
@@ -542,6 +576,25 @@ def __init__(
542576 else :
543577 self .in_shardings = in_shardings
544578
579+ # Prepend None shardings for captured nodes (passed as a list)
580+ n_captured = len (self ._captured_info )
581+ jit_in_shardings : tp .Any
582+ if n_captured > 0 :
583+ if isinstance (in_shardings , (tuple , list )):
584+ jit_in_shardings = ([None ] * n_captured ,) + tuple (in_shardings )
585+ elif in_shardings is not None :
586+ # Expand scalar in_shardings to match the number of user parameters
587+ # so it doesn't broadcast over the captures.
588+ sig = _fun_signature (fun )
589+ n_params = len (sig .parameters ) if sig is not None else 1
590+ jit_in_shardings = ([None ] * n_captured ,) + tuple (
591+ in_shardings for _ in range (n_params )
592+ )
593+ else :
594+ jit_in_shardings = in_shardings
595+ else :
596+ jit_in_shardings = in_shardings
597+
545598 donate_argnums_set = frozenset (
546599 (donate_argnums ,) if isinstance (donate_argnums , int )
547600 else donate_argnums or ()
@@ -550,21 +603,33 @@ def __init__(
550603 (donate_argnames ,) if isinstance (donate_argnames , str )
551604 else donate_argnames or ()
552605 )
606+
607+ # When captures are prepended as an extra first argument, offset
608+ # index-based jax.jit parameters so they still refer to the correct
609+ # physical arguments.
610+ if n_captured > 0 :
611+ jit_static_argnums : int | tp .Sequence [int ] | None = _offset_argnums (static_argnums , 1 )
612+ jit_donate_argnums : int | tp .Sequence [int ] | None = _offset_argnums (donate_argnums , 1 )
613+ else :
614+ jit_static_argnums = static_argnums
615+ jit_donate_argnums = donate_argnums
616+
553617 self .jitted_fn = jax .jit (
554618 SimpleJitFn (
555619 fun ,
556- self .in_shardings ,
620+ jit_in_shardings if n_captured > 0 else self .in_shardings ,
557621 out_shardings ,
558622 donate_argnums_set ,
559623 donate_argnames_set ,
560624 graph ,
561625 tuple (update_shardings ),
626+ self ._captured_info ,
562627 ),
563- in_shardings = in_shardings ,
628+ in_shardings = jit_in_shardings ,
564629 out_shardings = (out_shardings , update_shardings ),
565- static_argnums = static_argnums ,
630+ static_argnums = jit_static_argnums ,
566631 static_argnames = static_argnames ,
567- donate_argnums = donate_argnums ,
632+ donate_argnums = jit_donate_argnums ,
568633 donate_argnames = donate_argnames ,
569634 keep_unused = keep_unused ,
570635 device = device ,
@@ -594,10 +659,16 @@ def _maybe_from_tree(self, out):
594659 return out
595660
596661 def __call__ (self , * args : P .args , ** kwargs : P .kwargs ) -> R :
597- args = (* self .partial_args , * args ) # type: ignore[assignment]
598- args , kwargs = self ._maybe_to_tree (args , kwargs )
599- variables = extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
600- out , updates = self .jitted_fn (* args , ** kwargs )
662+ all_args : tuple [tp .Any , ...] = (* self .partial_args , * args )
663+ if self ._captured_info :
664+ all_args = (self ._captured_info , * all_args )
665+ all_args , kwargs = self ._maybe_to_tree (all_args , kwargs )
666+ if self ._captured_info :
667+ variables = extract .check_no_aliases (
668+ 'jit' , captured_args = all_args [0 ], args = all_args [1 :], kwargs = kwargs )
669+ else :
670+ variables = extract .check_no_aliases ('jit' , args = all_args , kwargs = kwargs )
671+ out , updates = self .jitted_fn (* all_args , ** kwargs )
601672 extract .apply_updates (variables , updates )
602673 return self ._maybe_from_tree (out )
603674
@@ -608,22 +679,33 @@ def __get__(self, obj, objtype=None):
608679
609680 def eval_shape (self , * args , ** kwargs ):
610681 args = (* self .partial_args , * args )
682+ args = (self ._captured_info , * args ) if self ._captured_info else args
611683 args , kwargs = self ._maybe_to_tree (args , kwargs )
612684 if not self .graph :
613- extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
685+ if self ._captured_info :
686+ extract .check_no_aliases (
687+ 'jit' , captured_args = args [0 ], args = args [1 :], kwargs = kwargs )
688+ else :
689+ extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
614690 out , _ = self .jitted_fn .eval_shape (* args , ** kwargs )
615691 return self ._maybe_from_tree (out )
616692
617693 def trace (self , * args , ** kwargs ):
618694 args = (* self .partial_args , * args )
695+ args = (self ._captured_info , * args ) if self ._captured_info else args
619696 args , kwargs = self ._maybe_to_tree (args , kwargs )
620697 if not self .graph :
621- extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
698+ if self ._captured_info :
699+ extract .check_no_aliases (
700+ 'jit' , captured_args = args [0 ], args = args [1 :], kwargs = kwargs )
701+ else :
702+ extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
622703 traced = self .jitted_fn .trace (* args , ** kwargs )
623704 return SimpleTraced (traced , self )
624705
625706 def lower (self , * args , ** kwargs ):
626707 args = (* self .partial_args , * args )
708+ args = (self ._captured_info , * args ) if self ._captured_info else args
627709 args , kwargs = self ._maybe_to_tree (args , kwargs )
628710 if not self .graph :
629711 extract .check_no_aliases ('jit' , args = args , kwargs = kwargs )
@@ -1916,6 +1998,18 @@ def shard_map_wrapper(*args, **kwargs):
19161998 return shard_map_wrapper # type: ignore
19171999
19182000
2001+ def _offset_argnums (
2002+ argnums : int | tp .Sequence [int ] | None ,
2003+ offset : int ,
2004+ ) -> int | tuple [int , ...] | None :
2005+ """Shift argnum indices by ``offset`` (e.g. when prepending captures)."""
2006+ if argnums is None :
2007+ return None
2008+ if isinstance (argnums , int ):
2009+ return argnums + offset
2010+ return tuple (i + offset for i in argnums )
2011+
2012+
19192013# We can't use private methods from jax._src.api_util
19202014# We copy the function: api_util.fun_signature
19212015def _fun_signature (fun : tp .Callable ) -> inspect .Signature | None :
0 commit comments