1919 async_max_workers = 8 ,
2020)
2121
22- # Global base configuration
22+ # Global base configuration and owner tracking
2323main_thread_config = copy .deepcopy (DEFAULT_CONFIG )
24+ config_owner_thread_id = None
2425
26+ # Global lock for settings configuration
27+ global_lock = threading .Lock ()
2528
2629class ThreadLocalOverrides (threading .local ):
2730 def __init__ (self ):
28- self .overrides = dotdict () # Initialize thread-local overrides
31+ self .overrides = dotdict ()
2932
30-
31- # Create the thread-local storage
3233thread_local_overrides = ThreadLocalOverrides ()
3334
3435
3536class Settings :
3637 """
3738 A singleton class for DSPy configuration settings.
38-
39- This is thread-safe. User threads are supported both through ParallelExecutor and native threading.
40- - If native threading is used, the thread inherits the initial config from the main thread.
41- - If ParallelExecutor is used, the thread inherits the initial config from its parent thread.
39+ Thread-safe global configuration.
40+ - 'configure' can be called by only one 'owner' thread (the first thread that calls it).
41+ - Other threads see the configured global values from 'main_thread_config'.
42+ - 'context' sets thread-local overrides. These overrides propagate to threads spawned
43+ inside that context block, when (and only when!) using a ParallelExecutor that copies overrides.
44+
45+ 1. Only one unique thread (which can be any thread!) can call dspy.configure.
46+ 2. It affects a global state, visible to all. As a result, user threads work, but they shouldn't be
47+ mixed with concurrent changes to dspy.configure from the "main" thread.
48+ (TODO: In the future, add warnings: if there are near-in-time user-thread reads followed by .configure calls.)
49+ 3. Any thread can use dspy.context. It propagates to child threads created with DSPy primitives: Parallel, asyncify, etc.
4250 """
4351
4452 _instance = None
4553
4654 def __new__ (cls ):
4755 if cls ._instance is None :
4856 cls ._instance = super ().__new__ (cls )
49- cls ._instance .lock = threading .Lock () # maintained here for DSPy assertions.py
5057 return cls ._instance
5158
59+ @property
60+ def lock (self ):
61+ return global_lock
62+
5263 def __getattr__ (self , name ):
5364 overrides = getattr (thread_local_overrides , 'overrides' , dotdict ())
5465 if name in overrides :
@@ -64,8 +75,6 @@ def __setattr__(self, name, value):
6475 else :
6576 self .configure (** {name : value })
6677
67- # Dictionary-like access
68-
6978 def __getitem__ (self , key ):
7079 return self .__getattr__ (key )
7180
@@ -88,42 +97,40 @@ def copy(self):
8897
8998 @property
9099 def config (self ):
91- config = self .copy ()
92- if 'lock' in config :
93- del config ['lock' ]
94- return config
95-
96- # Configuration methods
100+ return self .copy ()
97101
98102 def configure (self , ** kwargs ):
99- global main_thread_config
103+ global main_thread_config , config_owner_thread_id
104+ current_thread_id = threading .get_ident ()
100105
101- # Get or initialize thread-local overrides
102- overrides = getattr (thread_local_overrides , 'overrides' , dotdict ())
103- thread_local_overrides .overrides = dotdict (
104- {** copy .deepcopy (DEFAULT_CONFIG ), ** main_thread_config , ** overrides , ** kwargs }
105- )
106+ with self .lock :
107+ # First configuration: establish ownership. If ownership established, only that thread can configure.
108+ if config_owner_thread_id in [None , current_thread_id ]:
109+ config_owner_thread_id = current_thread_id
110+ else :
111+ raise RuntimeError ("dspy.settings can only be changed by the thread that initially configured it." )
106112
107- # Update main_thread_config, in the main thread only
108- if threading . current_thread () is threading . main_thread ():
109- main_thread_config = thread_local_overrides . overrides
113+ # Update global config
114+ for k , v in kwargs . items ():
115+ main_thread_config [ k ] = v
110116
111117 @contextmanager
112118 def context (self , ** kwargs ):
113- """Context manager for temporary configuration changes."""
114- global main_thread_config
119+ """
120+ Context manager for temporary configuration changes at the thread level.
121+ Does not affect global configuration. Changes only apply to the current thread.
122+ If threads are spawned inside this block using ParallelExecutor, they will inherit these overrides.
123+ """
124+
115125 original_overrides = getattr (thread_local_overrides , 'overrides' , dotdict ()).copy ()
116- original_main_thread_config = main_thread_config .copy ()
126+ new_overrides = dotdict ({** main_thread_config , ** original_overrides , ** kwargs })
127+ thread_local_overrides .overrides = new_overrides
117128
118- self .configure (** kwargs )
119129 try :
120130 yield
121131 finally :
122132 thread_local_overrides .overrides = original_overrides
123133
124- if threading .current_thread () is threading .main_thread ():
125- main_thread_config = original_main_thread_config
126-
127134 def __repr__ (self ):
128135 overrides = getattr (thread_local_overrides , 'overrides' , dotdict ())
129136 combined_config = {** main_thread_config , ** overrides }
0 commit comments