@@ -65,29 +65,6 @@ def get_num_workers_per_node() -> float | None:
6565 return get_num_workers_per_node
6666
6767
68- def _get_num_workers_per_node_value (stage : ProcessingStage ) -> float | None :
69- value_or_method = stage .num_workers_per_node
70- return value_or_method () if callable (value_or_method ) else value_or_method
71-
72-
73- def _set_num_workers_per_node_override (stage : ProcessingStage , num_workers_per_node : float | None ) -> None :
74- if callable (stage .num_workers_per_node ):
75- stage .num_workers_per_node = _num_workers_per_node_method (num_workers_per_node )
76- else :
77- stage .num_workers_per_node = num_workers_per_node
78-
79-
80- def _check_worker_sizing_options (stage : ProcessingStage ) -> None :
81- effective_num_workers = stage .num_workers ()
82- effective_num_workers_per_node = _get_num_workers_per_node_value (stage )
83- if effective_num_workers is not None and effective_num_workers_per_node is not None :
84- msg = (
85- "Use only one worker sizing option: num_workers or num_workers_per_node. "
86- "Set one of them to None before configuring the other."
87- )
88- raise ValueError (msg )
89-
90-
9168class StageMeta (ABCMeta ):
9269 """Metaclass that automatically registers concrete Stage subclasses.
9370 A class is considered *concrete* if it directly inherits from
@@ -111,6 +88,12 @@ def __new__(mcls, name, bases, namespace, **kwargs): # noqa: ANN001
11188
11289 return cls
11390
91+ def __call__ (cls , * args , ** kwargs ):
92+ """Validate worker sizing once every stage instance is fully constructed."""
93+ instance = super ().__call__ (* args , ** kwargs )
94+ instance ._validate_worker_sizing ()
95+ return instance
96+
11497
11598def get_stage_class (name : str ) -> type [ProcessingStage ]:
11699 """Retrieve a registered stage class by its *class name*.
@@ -179,16 +162,17 @@ def __init_subclass__(cls, **kwargs):
179162 msg = f"{ cls .__name__ } must not override '{ attr } '"
180163 raise TypeError (msg )
181164
182- num_workers = cls .__dict__ .get ("num_workers" )
183- if (num_workers is not None and not callable (num_workers )) or (
184- num_workers is None and "num_workers" in cls .__dict__ .get ("__annotations__" , {})
185- ):
186- msg = (
187- f"{ cls .__name__ } must not define 'num_workers' as a stage attribute. "
188- "Override num_workers() for backend worker sizing, or use a different field name "
189- "for stage-specific worker counts."
190- )
191- raise TypeError (msg )
165+ for worker_attr in ("num_workers" , "num_workers_per_node" ):
166+ value = cls .__dict__ .get (worker_attr )
167+ if (value is not None and not callable (value )) or (
168+ value is None and worker_attr in cls .__dict__ .get ("__annotations__" , {})
169+ ):
170+ msg = (
171+ f"{ cls .__name__ } must not define '{ worker_attr } ' as a stage attribute. "
172+ f"Override { worker_attr } () for backend worker sizing, or use a different field name "
173+ "for stage-specific worker counts."
174+ )
175+ raise TypeError (msg )
192176
193177 for attr in ("name" , "resources" , "batch_size" , "runtime_env" ):
194178 if isinstance (cls .__dict__ .get (attr ), property ):
@@ -207,6 +191,38 @@ def num_workers_per_node(self) -> float | None:
207191 """Number of workers required per Ray node. If None, executor default sizing is used."""
208192 return None
209193
194+ def _validate_worker_sizing (self ) -> None :
195+ """Enforce that at most one worker-sizing mechanism is configured.
196+
197+ A stage may size its worker pool with exactly one of ``num_workers()``,
198+ ``num_workers_per_node()``, or the Ray Data actor-pool sizing keys
199+ (``min_workers``/``max_workers``/``initial_workers``) in ``ray_stage_spec()``.
200+ Combining them is ambiguous, so we reject it once here — at construction and in
201+ ``with_()`` — rather than defensively re-checking in every backend.
202+ """
203+ configured = [
204+ name
205+ for name , value in (
206+ ("num_workers()" , self .num_workers ()),
207+ ("num_workers_per_node()" , self .num_workers_per_node ()),
208+ )
209+ if value is not None
210+ ]
211+ ray_stage_spec = self .ray_stage_spec ()
212+ if ray_stage_spec :
213+ # Lazy import: only stages that populate ray_stage_spec touch the Ray Data backend.
214+ from nemo_curator .backends .ray_data .utils import get_configured_actor_pool_sizing_keys
215+
216+ if get_configured_actor_pool_sizing_keys (ray_stage_spec ):
217+ configured .append ("actor-pool sizing keys (min/max/initial_workers)" )
218+
219+ if len (configured ) > 1 :
220+ msg = (
221+ f"Stage { self .name } sets multiple worker-sizing options ({ ', ' .join (configured )} ); "
222+ "use only one of num_workers, num_workers_per_node, or actor-pool sizing keys."
223+ )
224+ raise ValueError (msg )
225+
210226 def validate_input (self , task : Task ) -> bool :
211227 """Validate input task meets requirements.
212228 Args:
@@ -408,9 +424,11 @@ def with_( # noqa: PLR0913
408424 if num_workers is not _UNSET :
409425 new_instance .num_workers = _num_workers_method (cast ("int | None" , num_workers ))
410426 if num_workers_per_node is not _UNSET :
411- _set_num_workers_per_node_override (new_instance , cast ("float | None" , num_workers_per_node ))
427+ new_instance .num_workers_per_node = _num_workers_per_node_method (
428+ cast ("float | None" , num_workers_per_node )
429+ )
412430
413- _check_worker_sizing_options ( new_instance )
431+ new_instance . _validate_worker_sizing ( )
414432
415433 return new_instance
416434
0 commit comments