1212from collections .abc import Callable , Mapping
1313from dataclasses import dataclass
1414from pathlib import Path
15+ from time import perf_counter
1516from types import MappingProxyType
1617from typing import Any , get_args , get_origin , get_type_hints
1718
2728 flatten_kernel_data ,
2829 is_kernel_data ,
2930 iter_layout_leaves ,
31+ kernel_data ,
3032 lane_expression ,
3133 map_proxy ,
3234 proxy_symbol ,
@@ -79,6 +81,20 @@ def _invalidate_term_cache() -> None:
7981 _TERM_CACHE .clear ()
8082
8183
84+ @kernel_data
85+ class TermScalarBuffer :
86+ """Runtime buffer slot for one numeric term argument.
87+
88+ Numeric tuning arguments (thresholds, scales, gains, ...) are lowered into
89+ a fixed-dtype shared input slot: only the type/layout fingerprint enters
90+ the plan key, and the value itself is read at runtime. This keeps
91+ tuning-variant manager configs on one compiled plan instead of triggering
92+ a full recompile per value.
93+ """
94+
95+ value : np .float32
96+
97+
8298@dataclass (frozen = True )
8399class ManagerSimInput :
84100 """One compiled simulator input exposed through ``ManagerContext.sim``."""
@@ -111,6 +127,7 @@ def __init__(self, env: ManagerEnv):
111127 self ._plan_parts .append (("sim_inputs" , sim_input_fingerprint ))
112128
113129 def build (self ) -> CompiledManagerProgram :
130+ build_started = perf_counter ()
114131 self ._resolve_runtime_terms ()
115132 observation_groups = self ._env .observation_groups
116133 reward_terms = self ._env ._reward_terms
@@ -130,9 +147,17 @@ def build(self) -> CompiledManagerProgram:
130147 )
131148 self ._validate_observation_spaces (observation_groups )
132149
133- kernel_reward_weights = np .asarray (reward_weights , dtype = np .float32 ) * np .float32 (self ._env .cfg .ctrl_dt )
134- self ._plan_parts .append (("manager_context_dt" , np .float32 (self ._env .cfg .ctrl_dt )))
150+ # Reward weights stay unscaled in the runtime buffer; the dt scaling
151+ # happens inside the kernel via ctx.dt, so ctrl_dt does not need to
152+ # participate in the plan key.
153+ kernel_reward_weights = np .asarray (reward_weights , dtype = np .float32 )
135154 plan_key = hashlib .sha256 (repr (tuple (self ._plan_parts )).encode ()).hexdigest ()
155+ logger .info (
156+ "Manager startup %s: build plan finished in %.3fs (plan_key=%.12s)" ,
157+ self ._env_name (),
158+ perf_counter () - build_started ,
159+ plan_key ,
160+ )
136161 source = KernelSourceGenerator (self ._flat_input_count ).generate (
137162 observation_terms ,
138163 observation_layout ,
@@ -146,8 +171,28 @@ def build(self) -> CompiledManagerProgram:
146171 )
147172 generated_filename = self ._materialize_source (source , plan_key )
148173 kernels = _KERNEL_CACHE .get (plan_key )
149- if kernels is None :
150- logger .debug ("Manager kernel cache miss: plan_key=%s" , plan_key )
174+ if kernels is not None :
175+ logger .info (
176+ "Manager startup %s: kernels reused (cache=in-process, plan_key=%.12s)" ,
177+ self ._env_name (),
178+ plan_key ,
179+ )
180+ else :
181+ disk_cache = self ._has_generated_kernel_cache (plan_key )
182+ if disk_cache :
183+ logger .info (
184+ "Manager startup %s: compiled kernel cache found, loading (cache=disk, plan_key=%.12s)" ,
185+ self ._env_name (),
186+ plan_key ,
187+ )
188+ else :
189+ logger .info (
190+ "Manager startup %s: no cache for this plan, first compile may take a while "
191+ "(cache=miss, plan_key=%.12s)" ,
192+ self ._env_name (),
193+ plan_key ,
194+ )
195+ compile_started = perf_counter ()
151196 try :
152197 kernels = self ._compile_kernels (source , generated_filename )
153198 except (ImportError , OSError , RuntimeError , TypeError , ValueError ) as error :
@@ -156,8 +201,12 @@ def build(self) -> CompiledManagerProgram:
156201 generated_filename = self ._materialize_source (source , plan_key )
157202 kernels = self ._compile_kernels (source , generated_filename )
158203 _KERNEL_CACHE [plan_key ] = kernels
159- else :
160- logger .debug ("Manager kernel in-process cache hit: plan_key=%s" , plan_key )
204+ logger .info (
205+ "Manager startup %s: compile kernels finished in %.3fs (cache=%s)" ,
206+ self ._env_name (),
207+ perf_counter () - compile_started ,
208+ "disk" if disk_cache else "miss" ,
209+ )
161210 evaluate_kernel , observe_kernel , reset_kernel = kernels
162211
163212 layout = ManagerLayout (
@@ -271,6 +320,7 @@ def _resolve_reset(
271320 symbol = f"term_reset_{ term_index } "
272321 self ._term_functions [symbol ] = self ._generated_term (function , dispatcher )
273322 expressions = []
323+ plan_expressions = []
274324 prepared_indices = []
275325 for index , value in enumerate (term .args ):
276326 if is_kernel_data (value ):
@@ -285,15 +335,20 @@ def _resolve_reset(
285335 )
286336 prepared_indices .append (prepared_index )
287337 expressions .append (expression )
338+ plan_expressions .append (expression )
288339 else :
289- prepared_indices .append (None )
290- expressions .append (repr (value ))
340+ expression , _ , plan_part , prepared_index = self ._resolve_plain_arg (
341+ f"sim_reset.{ name } .args[{ index } ]" , value
342+ )
343+ prepared_indices .append (prepared_index )
344+ expressions .append (expression )
345+ plan_expressions .append (plan_part )
291346 self ._plan_parts .append (
292347 (
293348 "sim_reset" ,
294349 name ,
295350 self ._function_fingerprint (function ),
296- tuple ( expressions ) ,
351+ plan_expressions ,
297352 repr (descriptors ),
298353 fields ,
299354 )
@@ -473,7 +528,9 @@ def _resolve_observation_dispatch(
473528 f"ManagerContext and np.ndarray (conventionally named ctx and out)."
474529 )
475530 expressions = []
531+ plan_expressions = []
476532 prepared_indices = []
533+ warmup_values = []
477534 for index , value in enumerate (term .args ):
478535 if is_kernel_data (value ):
479536 _ , tree_def = flatten_kernel_data (value )
@@ -485,23 +542,30 @@ def _resolve_observation_dispatch(
485542 )
486543 prepared_indices .append (prepared_index )
487544 expressions .append (expression )
545+ plan_expressions .append (expression )
546+ warmup_values .append (value )
488547 else :
489- prepared_indices .append (None )
490- expressions .append (repr (value ))
548+ expression , warmup , plan_part , prepared_index = self ._resolve_plain_arg (
549+ f"observation.{ group } .{ name } .args[{ index } ]" , value
550+ )
551+ prepared_indices .append (prepared_index )
552+ expressions .append (expression )
553+ plan_expressions .append (plan_part )
554+ warmup_values .append (warmup )
491555 expressions = tuple (expressions )
492556 dispatcher = self ._compile_term (function )
493557 symbol = f"term_observation_{ term_index } "
494558 self ._term_functions [symbol ] = self ._generated_term (function , dispatcher )
495559 self ._plan_parts .append (
496- (group , name , "observation_dispatch" , self ._function_fingerprint (function ), output_size , expressions )
560+ (group , name , "observation_dispatch" , self ._function_fingerprint (function ), output_size , plan_expressions )
497561 )
498562 return PreparedInvocation (
499563 f"{ group } .{ name } " ,
500564 "observation" ,
501565 dispatcher ,
502566 None ,
503567 args_expressions = expressions ,
504- args_values = term . args ,
568+ args_values = tuple ( warmup_values ) ,
505569 args_prepared_indices = tuple (prepared_indices ),
506570 output_size = output_size ,
507571 )
@@ -574,7 +638,9 @@ def _resolve_dispatch_term(
574638 f"(conventionally named ctx)."
575639 )
576640 expressions = []
641+ plan_expressions = []
577642 prepared_indices = []
643+ warmup_values = []
578644 for index , value in enumerate (term .args ):
579645 if is_kernel_data (value ):
580646 _ , tree_def = flatten_kernel_data (value )
@@ -586,20 +652,27 @@ def _resolve_dispatch_term(
586652 )
587653 prepared_indices .append (prepared_index )
588654 expressions .append (expression )
655+ plan_expressions .append (expression )
656+ warmup_values .append (value )
589657 else :
590- prepared_indices .append (None )
591- expressions .append (repr (value ))
658+ expression , warmup , plan_part , prepared_index = self ._resolve_plain_arg (
659+ f"{ group } .{ name } .args[{ index } ]" , value
660+ )
661+ prepared_indices .append (prepared_index )
662+ expressions .append (expression )
663+ plan_expressions .append (plan_part )
664+ warmup_values .append (warmup )
592665 dispatcher = self ._compile_term (function )
593666 symbol = f"term_{ group } _{ term_index } "
594667 self ._term_functions [symbol ] = self ._generated_term (function , dispatcher )
595- self ._plan_parts .append ((group , name , "dispatch" , self ._function_fingerprint (function ), expressions ))
668+ self ._plan_parts .append ((group , name , "dispatch" , self ._function_fingerprint (function ), plan_expressions ))
596669 return PreparedInvocation (
597670 f"{ group } .{ name } " ,
598671 group ,
599672 dispatcher ,
600673 None ,
601674 args_expressions = tuple (expressions ),
602- args_values = term . args ,
675+ args_values = tuple ( warmup_values ) ,
603676 args_prepared_indices = tuple (prepared_indices ),
604677 )
605678
@@ -681,6 +754,38 @@ def _register_prepared(
681754 self ._prepared_types [proxy_symbol (proxy )] = proxy
682755 return prepared_index , lane_expression (layout , input_offset )
683756
757+ def _scalar_buffer_arg (self , source_name : str , value : float ) -> tuple [str , np .float32 ]:
758+ """Lower one numeric term argument into a runtime scalar buffer slot.
759+
760+ Returns the kernel-lane argument expression and the normalized warmup
761+ value. Only the fixed float32 layout enters the plan; the value itself
762+ is read from the input slot at runtime.
763+ """
764+ scalar = np .float32 (value )
765+ wrapped = TermScalarBuffer (scalar )
766+ _ , tree_def = flatten_kernel_data (wrapped )
767+ layout = self ._kernel_data_lowering .lower (
768+ tree_def ,
769+ context = source_name ,
770+ force_shared = True ,
771+ )
772+ prepared_index , _ = self ._register_prepared (source_name , wrapped , TermScalarBuffer , layout )
773+ leaf = iter_layout_leaves (layout )[0 ]
774+ return f"input_{ self ._input_offsets [prepared_index ] + leaf .slot_index } " , scalar
775+
776+ def _resolve_plain_arg (self , source_name : str , value : Any ) -> tuple [str | None , Any , str , int | None ]:
777+ """Resolve one non-kernel-data term argument.
778+
779+ Float-like scalars become runtime scalar buffers; every
780+ other scalar stays a compile-time constant. Returns the kernel-lane
781+ argument expression, the warmup value, the plan fingerprint part, and
782+ the prepared index (``None`` for both plain scalars and buffers).
783+ """
784+ if isinstance (value , (float , np .floating )):
785+ expression , warmup = self ._scalar_buffer_arg (source_name , value )
786+ return expression , warmup , "scalar_buffer(np.float32)" , None
787+ return repr (value ), value , repr (value ), None
788+
684789 def _validate_observation_spaces (self , groups : dict [str , ObservationGroupEntry ]) -> None :
685790 expected_policy = self ._env .policy_observation_space .shape
686791 if expected_policy != (groups ["policy" ].size ,):
@@ -704,9 +809,7 @@ def _validate_observation_spaces(self, groups: dict[str, ObservationGroupEntry])
704809
705810 @staticmethod
706811 def _materialize_source (source : str , plan_key : str ) -> str :
707- cache_dir = os .environ .get ("NUMBA_CACHE_DIR" )
708- if not cache_dir :
709- cache_dir = os .path .join (os .path .expanduser ("~" ), ".cache" , "motrixlab" , "numba-manager" )
812+ cache_dir = NumbaKernelCompiler ._generated_cache_dir ()
710813 path = Path (cache_dir ) / "generated" / f"{ plan_key } .py"
711814 path .parent .mkdir (parents = True , exist_ok = True )
712815 if not path .exists () or path .read_text (encoding = "utf-8" ) != source :
@@ -724,23 +827,53 @@ def _materialize_source(source: str, plan_key: str) -> str:
724827 return str (path )
725828
726829 @staticmethod
727- def _invalidate_generated_cache ( plan_key : str ) -> None :
830+ def _generated_cache_dir ( ) -> str :
728831 cache_dir = os .environ .get ("NUMBA_CACHE_DIR" )
729832 if not cache_dir :
730833 cache_dir = os .path .join (os .path .expanduser ("~" ), ".cache" , "motrixlab" , "numba-manager" )
834+ return cache_dir
835+
836+ @staticmethod
837+ def _generated_cache_paths (plan_key : str ) -> tuple [Path , ...]:
838+ """Return compiled-kernel cache files for one plan key.
839+
840+ Numba stores the cached specializations of the generated module next to
841+ its cache locator: under ``generated_<hash>/`` when ``NUMBA_CACHE_DIR``
842+ is set, and under ``generated/__pycache__/`` (beside the generated
843+ source) when it is not. Both layouts are covered so probe and
844+ invalidation agree with where the files actually live.
845+ """
846+ cache_dir = Path (NumbaKernelCompiler ._generated_cache_dir ())
847+ paths : list [Path ] = []
848+ for pattern in (f"generated_*/*{ plan_key } *" , f"generated/__pycache__/*{ plan_key } *" ):
849+ try :
850+ paths .extend (cache_dir .glob (pattern ))
851+ except OSError :
852+ logger .debug ("Unable to scan generated kernel cache: %s" , cache_dir , exc_info = True )
853+ return tuple (paths )
854+
855+ @staticmethod
856+ def _has_generated_kernel_cache (plan_key : str ) -> bool :
857+ """Return whether compiled-kernel cache files exist for one plan key."""
858+ return any (path .suffix in {".nbi" , ".nbc" } for path in NumbaKernelCompiler ._generated_cache_paths (plan_key ))
859+
860+ @staticmethod
861+ def _invalidate_generated_cache (plan_key : str ) -> None :
862+ cache_dir = NumbaKernelCompiler ._generated_cache_dir ()
731863 source_path = Path (cache_dir ) / "generated" / f"{ plan_key } .py"
732864 try :
733865 source_path .unlink (missing_ok = True )
734866 except OSError :
735867 logger .debug ("Unable to remove invalid generated source: %s" , source_path , exc_info = True )
736- for cache_path in Path ( cache_dir ). glob ( f"generated_*/* { plan_key } *" ):
868+ for cache_path in NumbaKernelCompiler . _generated_cache_paths ( plan_key ):
737869 try :
738870 cache_path .unlink (missing_ok = True )
739871 except OSError :
740872 logger .debug ("Unable to remove invalid generated cache: %s" , cache_path , exc_info = True )
741873
742874 def compile_specializations (self , inputs : tuple [Any , ...]) -> None :
743875 """Compile all term and fused-kernel specializations for the environment."""
876+ started = perf_counter ()
744877 try :
745878 self ._compile_specializations_once (inputs )
746879 except (
@@ -760,6 +893,14 @@ def compile_specializations(self, inputs: tuple[Any, ...]) -> None:
760893 _KERNEL_CACHE .pop (plan_key , None )
761894 self ._env ._task_program = self .build ().task
762895 self ._compile_specializations_once (inputs )
896+ logger .info (
897+ "Manager startup %s: term specializations finished in %.3fs" ,
898+ self ._env_name (),
899+ perf_counter () - started ,
900+ )
901+
902+ def _env_name (self ) -> str :
903+ return type (self ._env ).__name__
763904
764905 def _compile_specializations_once (self , inputs : tuple [Any , ...]) -> None :
765906 program = self ._env ._compiled_manager_program
0 commit comments