1313# limitations under the License.
1414
1515import copy
16+ import math
1617from collections .abc import Callable
1718from typing import Any
1819
1920from loguru import logger
20- from ray .data import Dataset , TaskPoolStrategy
21+ from ray .data import ActorPoolStrategy , Dataset , TaskPoolStrategy
2122
2223from nemo_curator .backends .base import BaseStageAdapter
23- from nemo_curator .backends .utils import RayStageSpecKeys , get_worker_metadata_and_node_id
24+ from nemo_curator .backends .utils import (
25+ RayStageSpecKeys ,
26+ get_stage_num_workers_per_node ,
27+ get_worker_metadata_and_node_id ,
28+ )
2429from nemo_curator .stages .base import ProcessingStage
30+ from nemo_curator .utils .ray_utils import get_alive_ray_node_count
2531
2632from .utils import get_actor_compute_strategy_for_stage , get_configured_actor_pool_sizing_keys , is_actor_stage
2733
@@ -39,8 +45,9 @@ class RayDataStageAdapter(BaseStageAdapter):
3945 c. Else we use tasks
4046 """
4147
42- def __init__ (self , stage : ProcessingStage ):
48+ def __init__ (self , stage : ProcessingStage , ignore_head_node : bool = False ):
4349 super ().__init__ (stage )
50+ self .ignore_head_node = ignore_head_node
4451
4552 self ._batch_size = self .stage .batch_size
4653 if self ._batch_size is None and self .stage .resources .gpus > 0 :
@@ -90,6 +97,59 @@ def _build_resource_kwargs(self, ray_stage_spec: dict) -> dict[str, float]:
9097 kwargs ["num_gpus" ] = self .stage .resources .gpus # type: ignore[reportArgumentType]
9198 return kwargs
9299
100+ def _get_per_node_pool_size (self , ray_stage_spec : dict , num_workers : int | None ) -> int | None :
101+ num_workers_per_node = get_stage_num_workers_per_node (self .stage )
102+ if num_workers_per_node is None :
103+ return None
104+
105+ if num_workers is not None :
106+ msg = (
107+ f"Stage { self .stage .name } defines both num_workers()={ num_workers } and "
108+ f"num_workers_per_node()={ num_workers_per_node } . "
109+ "Use only one worker sizing option."
110+ )
111+ raise ValueError (msg )
112+
113+ actor_pool_sizing_keys = get_configured_actor_pool_sizing_keys (ray_stage_spec )
114+ if actor_pool_sizing_keys :
115+ msg = (
116+ f"Stage { self .stage .name } defines num_workers_per_node() "
117+ f"and actor-pool sizing keys { actor_pool_sizing_keys } . Use only one worker sizing option."
118+ )
119+ raise ValueError (msg )
120+
121+ node_count = get_alive_ray_node_count (ignore_head_node = self .ignore_head_node )
122+ if node_count <= 0 :
123+ msg = f"No alive Ray nodes available for num_workers_per_node sizing on stage { self .stage .name } ."
124+ raise ValueError (msg )
125+ return max (1 , math .ceil (num_workers_per_node * node_count ))
126+
127+ def _build_actor_compute_kwargs (self , per_node_pool_size : int | None ) -> dict [str , object ]:
128+ if per_node_pool_size is not None :
129+ return {"compute" : ActorPoolStrategy (size = per_node_pool_size )}
130+ return {"compute" : get_actor_compute_strategy_for_stage (self .stage )}
131+
132+ def _build_task_compute_kwargs (
133+ self , ray_stage_spec : dict , per_node_pool_size : int | None , num_workers : int | None
134+ ) -> dict [str , object ]:
135+ actor_pool_sizing_keys = get_configured_actor_pool_sizing_keys (ray_stage_spec )
136+ if actor_pool_sizing_keys :
137+ logger .warning (
138+ f"Ignoring ray_stage_spec worker sizing keys { actor_pool_sizing_keys } "
139+ f"for Ray Data task stage { self .stage .name } ; these keys only apply to actor stages."
140+ )
141+
142+ map_batches_kwargs : dict [str , object ] = {}
143+ if per_node_pool_size is not None :
144+ map_batches_kwargs ["compute" ] = TaskPoolStrategy (size = per_node_pool_size )
145+ elif num_workers is not None and num_workers > 0 :
146+ map_batches_kwargs ["compute" ] = TaskPoolStrategy (size = num_workers )
147+
148+ max_calls = ray_stage_spec .get (RayStageSpecKeys .MAX_CALLS_PER_WORKER )
149+ if max_calls is not None :
150+ map_batches_kwargs ["max_calls" ] = max_calls
151+ return map_batches_kwargs
152+
93153 def process_dataset (self , dataset : Dataset ) -> Dataset :
94154 """Process a Ray Data dataset through this stage.
95155
@@ -101,28 +161,15 @@ def process_dataset(self, dataset: Dataset) -> Dataset:
101161 """
102162 ray_stage_spec = self .stage .ray_stage_spec ()
103163 stage_is_actor = ray_stage_spec .get (RayStageSpecKeys .IS_ACTOR_STAGE , is_actor_stage (self .stage ))
164+ num_workers = self .stage .num_workers ()
165+ per_node_pool_size = self ._get_per_node_pool_size (ray_stage_spec , num_workers )
104166
105167 if stage_is_actor :
106- map_batches_fn = create_actor_from_stage (self .stage )
107- map_batches_kwargs = { "compute" : get_actor_compute_strategy_for_stage ( self .stage )}
168+ map_batches_fn = create_actor_from_stage (self .stage , ignore_head_node = self . ignore_head_node )
169+ map_batches_kwargs = self ._build_actor_compute_kwargs ( per_node_pool_size )
108170 else :
109- map_batches_fn = create_task_from_stage (self .stage )
110- map_batches_kwargs = {}
111-
112- actor_pool_sizing_keys = get_configured_actor_pool_sizing_keys (ray_stage_spec )
113- if actor_pool_sizing_keys :
114- logger .warning (
115- f"Ignoring ray_stage_spec worker sizing keys { actor_pool_sizing_keys } "
116- f"for Ray Data task stage { self .stage .name } ; these keys only apply to actor stages."
117- )
118-
119- num_workers = self .stage .num_workers ()
120- if num_workers is not None and num_workers > 0 :
121- map_batches_kwargs ["compute" ] = TaskPoolStrategy (size = num_workers )
122-
123- max_calls = ray_stage_spec .get (RayStageSpecKeys .MAX_CALLS_PER_WORKER )
124- if max_calls is not None :
125- map_batches_kwargs ["max_calls" ] = max_calls
171+ map_batches_fn = create_task_from_stage (self .stage , ignore_head_node = self .ignore_head_node )
172+ map_batches_kwargs = self ._build_task_compute_kwargs (ray_stage_spec , per_node_pool_size , num_workers )
126173
127174 map_batches_kwargs .update (self ._build_resource_kwargs (ray_stage_spec ))
128175
@@ -141,6 +188,9 @@ def process_dataset(self, dataset: Dataset) -> Dataset:
141188 )
142189 raise ValueError (msg )
143190
191+ if per_node_pool_size is not None :
192+ map_batches_kwargs .setdefault ("scheduling_strategy" , "SPREAD" )
193+
144194 map_batches_kwargs .update (ray_remote_args )
145195
146196 # Let Ray Data apply the selected compute strategy and resource requirements.
@@ -154,15 +204,15 @@ def process_dataset(self, dataset: Dataset) -> Dataset:
154204 return processed_dataset
155205
156206
157- def create_actor_from_stage (stage : ProcessingStage ) -> type [RayDataStageAdapter ]:
207+ def create_actor_from_stage (stage : ProcessingStage , ignore_head_node : bool = False ) -> type [RayDataStageAdapter ]:
158208 """Create a StageProcessor class with the proper stage name for display."""
159209
160210 class RayDataStageActorAdapter (RayDataStageAdapter ):
161211 """Simplified stateful processor that wraps a ProcessingStage for Ray Data."""
162212
163213 def __init__ (self ):
164214 """Initialize the stage processor."""
165- super ().__init__ (stage )
215+ super ().__init__ (stage , ignore_head_node = ignore_head_node )
166216 self .setup_done = False
167217 node_info , worker_metadata = get_worker_metadata_and_node_id ()
168218 self .setup_on_node (node_info , worker_metadata )
@@ -179,7 +229,9 @@ def __call__(self, batch: dict[str, Any]) -> dict[str, Any]:
179229 return RayDataStageActorAdapter
180230
181231
182- def create_task_from_stage (stage : ProcessingStage ) -> Callable [[dict [str , Any ]], dict [str , Any ]]:
232+ def create_task_from_stage (
233+ stage : ProcessingStage , ignore_head_node : bool = False
234+ ) -> Callable [[dict [str , Any ]], dict [str , Any ]]:
183235 """Create a named Ray Data stage adapter function.
184236
185237 This creates a standalone function that wraps the stage processing logic
@@ -192,7 +244,7 @@ def create_task_from_stage(stage: ProcessingStage) -> Callable[[dict[str, Any]],
192244 Callable: A function that can be used directly with Ray Data's map_batches
193245 """
194246 # Create the adapter instance
195- adapter = RayDataStageAdapter (stage )
247+ adapter = RayDataStageAdapter (stage , ignore_head_node = ignore_head_node )
196248
197249 # Create a standalone function that wraps the adapter's processing logic
198250 def stage_map_fn (batch : dict [str , Any ]) -> dict [str , Any ]:
0 commit comments