ReferenceInfra

Stage Worker Sizing

View as Markdown

Use ProcessingStage.num_workers() or stage.with_(num_workers=...) to set a backend-neutral worker request. Use ray_stage_spec and xenna_stage_spec for backend-specific execution behavior.

Worker count and per-worker resources are separate controls:

  • num_workers() controls how many workers the executor requests.
  • Resources controls CPU and GPU resources reserved for each worker.
  • ray_stage_spec() and xenna_stage_spec() provide backend-specific scheduling options.

A worker count does not create cluster capacity. Ensure the cluster can satisfy worker count × per-worker Resources, or use autoscaling bounds that fit the cluster.

Common Stage for the Examples

The examples on this page configure this minimal stage:

1from nemo_curator.stages.base import ProcessingStage
2from nemo_curator.stages.resources import Resources
3from nemo_curator.tasks import EmptyTask
4
5
6class PassthroughStage(ProcessingStage[EmptyTask, EmptyTask]):
7 name = "passthrough"
8 resources = Resources(cpus=1.0)
9
10 def process(self, task: EmptyTask) -> EmptyTask:
11 return task

Worker Controls by Backend

Backendnum_workers=N, where N > 0num_workers=NoneBackend-specific alternative
Ray Data actor stageFixed ActorPoolStrategy(size=N)Autoscaling actor pool, minimum 1 with no maximum by defaultmin_workers, max_workers, and initial_workers
Ray Data task stageFixed TaskPoolStrategy(size=N)Ray Data’s default task scheduling; no explicit compute strategyNone; actor-pool sizing keys are ignored
XennaExactly N workers across the clusterXenna determines worker allocationnum_workers_per_node in xenna_stage_spec
Ray Actor PoolRequests N; warns and caps it if fewer actors fit available resourcesChooses the smaller of work batches and resource capacityNone

Ray Data and Ray Actor Pool also treat 0 and negative values as no fixed-size request. Xenna passes every non-None value to its stage specification, so use a positive count or None; do not use zero or negative Xenna worker counts.

Ray Data Worker Pools

Ray Data uses actor stages for stateful setup or GPU-backed work and task stages for stateless work. A stage is an actor stage when either condition is true:

  • ray_stage_spec[RayStageSpecKeys.IS_ACTOR_STAGE] is explicitly True.
  • The stage overrides setup(), or requests both CPU and GPU resources.

Set IS_ACTOR_STAGE to False to force task execution. Do this only when the stage does not depend on worker-local setup state.

Fixed actor pool

Set a positive num_workers and mark the stage as an actor stage:

1from nemo_curator.backends.ray_data import RayDataExecutor
2from nemo_curator.backends.utils import RayStageSpecKeys
3
4stage = PassthroughStage().with_(
5 num_workers=4,
6 ray_stage_spec={
7 RayStageSpecKeys.IS_ACTOR_STAGE: True,
8 },
9)
10
11executor = RayDataExecutor()

Ray Data receives ActorPoolStrategy(size=4). Cluster resources must be able to place four actors with the stage’s per-worker Resources.

Autoscaling actor pool

Use the exact RayStageSpecKeys members MIN_WORKERS, MAX_WORKERS, and INITIAL_WORKERS:

1from nemo_curator.backends.ray_data import RayDataExecutor
2from nemo_curator.backends.utils import RayStageSpecKeys
3
4stage = PassthroughStage().with_(
5 # Explicitly clear an inherited fixed worker count, if the stage has one.
6 num_workers=None,
7 ray_stage_spec={
8 RayStageSpecKeys.IS_ACTOR_STAGE: True,
9 RayStageSpecKeys.MIN_WORKERS: 2,
10 RayStageSpecKeys.MAX_WORKERS: 8,
11 RayStageSpecKeys.INITIAL_WORKERS: 4,
12 },
13)
14
15executor = RayDataExecutor()

Ray Data receives ActorPoolStrategy(min_size=2, max_size=8, initial_size=4).

Actor-pool keyString valueDefault when omittedMeaning
RayStageSpecKeys.MIN_WORKERS"min_workers"1Minimum live actors
RayStageSpecKeys.MAX_WORKERS"max_workers"NoneMaximum live actors; None leaves the pool unbounded by this setting
RayStageSpecKeys.INITIAL_WORKERS"initial_workers"NoneInitial pool size selected by Ray Data

Ray validates the relationship among the three values. For example, an initial size greater than the maximum raises ValueError with the stage name and the underlying actor-pool validation message.

Fixed task pool

For a stateless task stage, a positive worker count selects TaskPoolStrategy(size=N):

1from nemo_curator.backends.ray_data import RayDataExecutor
2from nemo_curator.backends.utils import RayStageSpecKeys
3
4stage = PassthroughStage().with_(
5 num_workers=6,
6 ray_stage_spec={
7 RayStageSpecKeys.IS_ACTOR_STAGE: False,
8 },
9)
10
11executor = RayDataExecutor()

Without a positive num_workers, a task stage does not set compute; Ray Data uses its default task scheduler.

Ray Data precedence and errors

ConfigurationResult
Actor stage with positive num_workers onlyFixed actor pool
Actor stage with positive num_workers plus min/max/initial keysFixed actor pool wins; Curator logs a warning listing ignored sizing keys
Actor stage without positive num_workers, no sizing keysAutoscaling actor pool with min_size=1, max_size=None, and initial_size=None
Task stage with positive num_workersFixed task pool
Task stage with min/max/initial keysKeys are ignored; Curator logs a warning because they apply only to actor stages
Invalid actor-pool boundsRaises ValueError
Unknown ray_stage_spec keyRaises ValueError while constructing the adapter
ray_remote_args includes compute, max_calls, num_cpus, or num_gpusRaises ValueError; these arguments are managed by Curator

Xenna Worker Counts

Xenna supports either a cluster-wide fixed count or a per-node count, but not both on one stage.

Use the common worker hook. This example requests 12 workers in total, regardless of node count:

1from nemo_curator.backends.xenna import XennaExecutor
2
3stage = PassthroughStage().with_(num_workers=12)
4executor = XennaExecutor()

The following combinations are invalid:

ConfigurationBehavior
with_(xenna_stage_spec={"num_workers": 4})Raises ValueError; use with_(num_workers=4)
A stage’s xenna_stage_spec() returns {"num_workers": 4}XennaExecutor raises ValueError; override num_workers() instead
Any non-None num_workers() result and num_workers_per_nodeXennaExecutor raises ValueError; choose one scope

When neither setting is present, Xenna controls worker allocation using stage resources and pipeline demand.

Ray Actor Pool Resource Capping

RayActorPoolExecutor calculates how many actors fit from available CPU and GPU resources. A positive num_workers() is honored when it fits. If it exceeds capacity, Curator warns and caps the actor count instead of waiting for impossible placements.

For example, a stage that requests Resources(cpus=2.0) and num_workers=8 can place at most four actors when eight CPUs are available. Work batch count can further reduce the automatically chosen pool size when num_workers is unset.

Configure a Stage with with_()

with_() deep-copies the stage and returns the configured copy. It can override portable properties and merge backend stage specs in one call:

1from nemo_curator.backends.utils import RayStageSpecKeys
2from nemo_curator.stages.resources import Resources
3
4configured = PassthroughStage().with_(
5 name="gpu_passthrough",
6 resources=Resources(cpus=2.0, gpus=1.0),
7 batch_size=8,
8 runtime_env={"pip": ["transformers==4.45.0"]},
9 num_workers=4,
10 ray_stage_spec={
11 RayStageSpecKeys.RAY_NUM_CPUS: 1.0,
12 RayStageSpecKeys.RAY_REMOTE_ARGS: {
13 "scheduling_strategy": "SPREAD",
14 },
15 },
16 xenna_stage_spec={
17 "worker_max_lifetime_m": 45,
18 },
19)

The original stage remains unchanged. resources, batch_size, runtime_env, and num_workers apply across supported executors. ray_stage_spec configures Ray Data and the Ray-specific flags used by Ray Actor Pool; xenna_stage_spec configures Xenna.

Shallow-merge behavior

ray_stage_spec and xenna_stage_spec are shallow-merged with the stage’s existing dictionary. New top-level keys are preserved, and user values replace existing values at the same top-level key. Nested dictionaries are replaced, not recursively merged.

For example, if a stage returns:

1{
2 RayStageSpecKeys.MAX_WORKERS: 8,
3 RayStageSpecKeys.RAY_REMOTE_ARGS: {
4 "scheduling_strategy": "SPREAD",
5 "max_retries": 2,
6 },
7}

then this override:

1stage.with_(
2 ray_stage_spec={
3 RayStageSpecKeys.MAX_WORKERS: None,
4 RayStageSpecKeys.RAY_REMOTE_ARGS: {"max_retries": 0},
5 }
6)

produces a spec where max_workers is explicitly None and ray_remote_args contains only {"max_retries": 0}. The original scheduling_strategy nested key is not retained.

Omitted values and explicit None

CallMeaning
Omit num_workersPreserve the stage’s current num_workers() result
with_(num_workers=None)Explicitly reset worker sizing to executor-controlled behavior
Omit ray_stage_spec or pass ray_stage_spec=NoneDo not modify the current Ray spec
ray_stage_spec={RayStageSpecKeys.MAX_WORKERS: None}Preserve the key with an explicit None, clearing an inherited maximum
Omit xenna_stage_spec or pass xenna_stage_spec=NoneDo not modify the current Xenna spec
xenna_stage_spec={"worker_max_lifetime_m": None}Preserve that key with an explicit None
runtime_env=NoneDo not modify the current environment
runtime_env={}Replace the current environment with an empty dictionary

Chained with_() calls merge against the already configured copy, so later values win while unrelated top-level keys from earlier calls remain.

Reserved num_workers Hook

num_workers is reserved as a method on ProcessingStage. Do not define it as a dataclass field or ordinary stage attribute:

1class FixedStage(PassthroughStage):
2 def num_workers(self) -> int:
3 return 4

Alternatively, keep the class executor-neutral and use stage.with_(num_workers=4) at pipeline construction time. Defining num_workers: int = 4 as a class or dataclass field raises TypeError when the subclass is created.

Some built-in single-input and fanout stages override num_workers() to return 1, preventing duplicate source enumeration. Use with_(num_workers=None) only when executor-controlled parallelism is safe for that stage’s input contract.

For ASR stages, the stage-local NeMo DataLoader setting is named dataloader_num_workers. When migrating an ASR constructor or configuration, rename the previous stage field num_workers to dataloader_num_workers; the common num_workers() hook is now reserved for Curator backend workers:

1from nemo_curator.stages.audio.tagging.inference.nemo_asr_align import NeMoASRAlignerStage
2
3asr_stage = NeMoASRAlignerStage(
4 dataloader_num_workers=10,
5).with_(num_workers=2)

Resource Matching and Ray Data Fusion

Ray Data may fuse compatible adjacent operators to avoid materializing and serializing intermediate blocks. Fusion is an optimizer decision, not a guaranteed stage behavior.

Ray Data determines the per-worker reservation in this order:

ResourceSelection
CPUray_stage_spec[RayStageSpecKeys.RAY_NUM_CPUS] when non-None; otherwise stage.resources.cpus when positive
GPUstage.resources.gpus when positive

Use RAY_NUM_CPUS when Ray Data needs a different scheduling reservation from other executors:

1stage = PassthroughStage().with_(
2 resources=Resources(cpus=4.0),
3 ray_stage_spec={
4 RayStageSpecKeys.RAY_NUM_CPUS: 1.0,
5 },
6)

The CPU paths of VideoFrameExtractionStage and ClipTranscodingStage use a Ray Data reservation of 1.0 CPU by default while preserving their larger Resources.cpus values for Xenna. Matching the adjacent video operators’ Ray Data reservations makes fusion possible and can remove inter-stage serialization overhead. Different resources, execution constraints, or optimizer decisions can still keep stages separate; inspect the Ray Data execution plan or dashboard instead of assuming fusion occurred.

Lowering the scheduler reservation does not reduce the CPU work performed by FFmpeg. Confirm that the resulting concurrency does not oversubscribe the host.