API Reference

ProcessingStage

View as Markdown

The ProcessingStage class is the base class for all data processing stages in NeMo Curator. Each stage defines a single step in a data curation pipeline.

Import

1from nemo_curator.stages.base import ProcessingStage

Class Definition

1from dataclasses import dataclass
2from typing import Generic, TypeVar
3
4InputT = TypeVar("InputT", bound=Task)
5OutputT = TypeVar("OutputT", bound=Task)
6
7@dataclass
8class ProcessingStage(Generic[InputT, OutputT]):
9 """Base class for all processing stages.
10
11 Type Parameters:
12 InputT: The input task type this stage accepts.
13 OutputT: The output task type this stage produces.
14
15 Class Attributes:
16 name: String identifier for the stage.
17 resources: Resources configuration (CPUs, GPUs).
18 batch_size: Number of tasks to process per batch.
19 """
20
21 name: str = "ProcessingStage"
22 resources: Resources = field(default_factory=lambda: Resources(cpus=1.0))
23 batch_size: int = 1

Abstract Methods

inputs()

Define stage input requirements.

1def inputs(self) -> tuple[list[str], list[str]]:
2 """Define required task and data attributes.
3
4 Returns:
5 Tuple of (required_task_attributes, required_data_attributes).
6 """

outputs()

Define stage output requirements.

1def outputs(self) -> tuple[list[str], list[str]]:
2 """Define output task and data attributes.
3
4 Returns:
5 Tuple of (output_task_attributes, output_data_attributes).
6 """

process()

Process a single task.

1def process(self, task: InputT) -> OutputT | list[OutputT] | None:
2 """Process a single task.
3
4 Args:
5 task: The input task to process.
6
7 Returns:
8 - Single task: For 1-to-1 transformations
9 - List of tasks: For splitting/reading operations
10 - None: To filter out the task
11 """

Optional Lifecycle Methods

setup_on_node()

Node-level initialization (e.g., download models).

1def setup_on_node(
2 self,
3 node_info: NodeInfo,
4 worker_metadata: dict[str, Any],
5) -> None:
6 """Initialize resources on a compute node.
7
8 Called once per node before any workers start.
9 """

setup()

Worker-level initialization (e.g., load models).

1def setup(self, worker_metadata: dict[str, Any]) -> None:
2 """Initialize resources for a worker.
3
4 Called once per worker before processing begins.
5 """

teardown()

Cleanup after processing.

1def teardown(self) -> None:
2 """Clean up resources after processing completes."""

process_batch()

Vectorized batch processing for better performance.

1def process_batch(self, tasks: list[InputT]) -> list[OutputT | None]:
2 """Process a batch of tasks.
3
4 Override for vectorized operations.
5
6 Args:
7 tasks: List of input tasks.
8
9 Returns:
10 List of output tasks (None entries are filtered out).
11 """

Backend Configuration Hooks

num_workers()

Return a backend-neutral worker count. None delegates worker sizing to the executor.

1def num_workers(self) -> int | None:
2 return None

num_workers is reserved as a method. A subclass that defines it as a class attribute or dataclass field raises TypeError. Override the method for a class-level default, or use stage.with_(num_workers=...) for one pipeline instance.

The exact meaning depends on the executor: Ray Data creates a fixed actor or task pool, Xenna treats it as a cluster-wide count, and Ray Actor Pool caps it to available resource capacity when necessary. See Stage Worker Sizing for the complete backend matrix.

ray_stage_spec()

Return Ray-specific stage options. Ray Data consumes the worker-pool keys below, while selected flags are also used by Ray Actor Pool. Use the RayStageSpecKeys enum rather than spelling keys manually:

1from nemo_curator.backends.utils import RayStageSpecKeys
2
3def ray_stage_spec(self) -> dict:
4 return {
5 RayStageSpecKeys.IS_ACTOR_STAGE: True,
6 RayStageSpecKeys.MIN_WORKERS: 2,
7 RayStageSpecKeys.MAX_WORKERS: 8,
8 RayStageSpecKeys.INITIAL_WORKERS: 4,
9 }

xenna_stage_spec()

Return Xenna-specific stage options:

1def xenna_stage_spec(self) -> dict:
2 return {
3 "num_workers_per_node": 2,
4 "worker_max_lifetime_m": 45,
5 }

Do not put num_workers in this dictionary. Use the common num_workers() hook for a cluster-wide Xenna count. num_workers() and num_workers_per_node cannot be set together.

task_id is framework-owned. The executor adapter assigns it after either process() or process_batch() returns, so custom stages must not set or derive IDs. For deterministic lineage, preserve positional correspondence in batched code: return one task or None for every input. A batch that maps multiple inputs to a different number of outputs receives random r-prefixed IDs because parentage is ambiguous.

For source-level checkpointing and the complete mapping rules, refer to Resumable Processing.

Creating Custom Stages

1from dataclasses import dataclass
2from nemo_curator.stages.base import ProcessingStage
3from nemo_curator.stages.resources import Resources
4from nemo_curator.tasks import DocumentBatch
5
6@dataclass
7class MyCustomStage(ProcessingStage[DocumentBatch, DocumentBatch]):
8 """Custom stage that processes documents."""
9
10 name: str = "MyCustomStage"
11 resources: Resources = field(default_factory=lambda: Resources(cpus=2.0))
12
13 # Custom parameters
14 threshold: float = 0.5
15
16 def inputs(self) -> tuple[list[str], list[str]]:
17 return ["data"], ["text"]
18
19 def outputs(self) -> tuple[list[str], list[str]]:
20 return ["data"], ["text", "score"]
21
22 def process(self, task: DocumentBatch) -> DocumentBatch | None:
23 # Process the task
24 df = task.data
25 df["score"] = df["text"].apply(self._compute_score)
26
27 # Filter based on threshold
28 if df["score"].mean() < self.threshold:
29 return None
30
31 return DocumentBatch(
32 dataset_name=task.dataset_name,
33 data=df,
34 _metadata=task._metadata,
35 _stage_perf=task._stage_perf,
36 )
37
38 def _compute_score(self, text: str) -> float:
39 # Custom scoring logic
40 return len(text) / 1000.0

Per-Stage Runtime Environments

Stages can declare isolated Python dependencies using Ray’s native runtime_env. Set runtime_env as a class variable to specify packages that should be installed in an isolated virtualenv for that stage’s workers:

1from typing import Any, ClassVar
2
3class IsolatedStage(ProcessingStage[DocumentBatch, DocumentBatch]):
4 name = "isolated_stage"
5 runtime_env: ClassVar[dict[str, Any] | None] = {"pip": ["transformers==4.40.0"]}
6
7 def inputs(self):
8 return ["data"], []
9
10 def outputs(self):
11 return ["data"], []
12
13 def process(self, task):
14 import transformers # sees 4.40.0
15 ...

You can also override runtime_env at instantiation time using with_():

1stage = IsolatedStage().with_(runtime_env={"pip": ["transformers==4.45.0"]})

All three execution backends (XennaExecutor, RayDataExecutor, RayActorPoolExecutor) support per-stage runtime environments. See the Per-Stage Runtime Environments reference for details.

Configuration with with_()

with_() deep-copies a stage and configures the copy without mutating the original. It supports portable properties and backend-specific overrides:

1from nemo_curator.backends.utils import RayStageSpecKeys
2from nemo_curator.stages.resources import Resources
3
4stage = MyCustomStage(threshold=0.7)
5configured_stage = stage.with_(
6 name="configured_stage",
7 resources=Resources(cpus=4.0, gpus=1.0),
8 batch_size=8,
9 runtime_env={"pip": ["transformers==4.45.0"]},
10 num_workers=4,
11 # Set the stage spec for the executor you actually run with:
12 ray_stage_spec={
13 RayStageSpecKeys.RAY_NUM_CPUS: 1.0,
14 },
15)

Both specs are shown together here only to document the arguments. In practice you set the one matching the executor you run the pipeline with — ray_stage_spec for Ray Data, xenna_stage_spec for Xenna:

1# Xenna equivalent
2configured_stage = stage.with_(
3 num_workers=4,
4 xenna_stage_spec={
5 "worker_max_lifetime_m": 45,
6 },
7)

Setting both is harmless — each executor reads only its own spec — but it is only useful if the same stage runs under both backends.

ray_stage_spec and xenna_stage_spec are shallow-merged: user-provided top-level keys win, while nested dictionaries are replaced rather than recursively merged. An explicit None inside a stage-spec dictionary is retained. In contrast, passing the whole ray_stage_spec=None or xenna_stage_spec=None means no override.

num_workers uses an unset sentinel internally, so omission and explicit None differ:

  • Omitting num_workers preserves the stage’s current method result.
  • with_(num_workers=None) resets an inherited fixed count to executor-controlled behavior.

See Stage Worker Sizing for merge examples, precedence rules, and invalid combinations. See Per-Stage Runtime Environments for dependency isolation.

Source Code

View source on GitHub