Skip to content

Commit aeb06bc

Browse files
lbliiiclaude
andauthored
[codex] docs: document native pipeline resumability (#2134)
* docs: document native pipeline resumability Signed-off-by: Lawrence Lane <llane@nvidia.com> * docs: link SLURM array operations Signed-off-by: Lawrence Lane <llane@nvidia.com> * docs: address resumability review feedback Signed-off-by: Lawrence Lane <llane@nvidia.com> * docs(pipeline): note checkpoint_path must be shared storage in multi-node runs Address review feedback: clarify in the Pipeline.run() checkpoint_path docstring that distributed runs must point every worker/node at the same durable shared filesystem location, matching the Resumable Processing reference guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Lawrence Lane <llane@nvidia.com> --------- Signed-off-by: Lawrence Lane <llane@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 570b2b7 commit aeb06bc

14 files changed

Lines changed: 232 additions & 110 deletions

File tree

fern/versions/main/pages/about/concepts/audio/audio-task.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ audio_task = AudioTask(
3636
"language": "en"
3737
},
3838
filepath_key="audio_filepath",
39-
task_id="audio_task_001",
4039
dataset_name="my_speech_dataset"
4140
)
4241
```
@@ -47,7 +46,7 @@ audio_task = AudioTask(
4746
|-----------|------|-------------|
4847
| `data` | `dict` | Audio manifest entry (single dict, exposed as `_AttrDict` for attribute-style access) |
4948
| `filepath_key` | `str \| None` | Key name for audio file paths in data (optional) |
50-
| `task_id` | `str` | Unique identifier for the task |
49+
| `task_id` | `str` | Framework-managed deterministic lineage ID. Treat as read-only; it is assigned at stage boundaries. |
5150
| `dataset_name` | `str` | Name of the source dataset |
5251
| `num_items` | `int` | Always returns `1` (read-only property) |
5352

fern/versions/main/pages/about/release-notes/migration-faq.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ Each task is a data class. You can add whatever statistics you need (input or ou
110110
## Fault Tolerance, Checkpointing, and Observability
111111

112112
<Accordion title="What if a stage fails (for example, OOM or disk error)? Will the pipeline crash?">
113-
Ray will relaunch failed workers or actors, but robust error handling and resumption (for example, from last completed task) should be implemented in your stage logic as needed.
113+
Ray can relaunch failed workers or actors. For restart recovery, pass `checkpoint_path` to `Pipeline.run()` so completed source partitions are skipped on the next launch. A custom stage can return `FailedTask()` for a recoverable per-task failure that should remain pending. Refer to [Resumable Processing](/reference/infra/resumable-processing).
114114
</Accordion>
115115

116116
<Accordion title="How is observability handled? Can I track pipeline performance, actor counts, and task durations?">
117117
All Ray pipelines expose resource and processing metrics via a built-in Grafana dashboard (with process time per task or actor, resource utilization, and so on). You can also summarize stats from task data at pipeline completion for custom reporting. For configuration options, refer to [Pipeline Execution Backends ](/reference/infra/execution-backends) and the [Ray Dashboard documentation](https://docs.ray.io/en/latest/ray-observability/getting-started.html).
118118
</Accordion>
119119

120120
<Accordion title="Can I resume processing from mid-pipeline if interrupted?">
121-
For streaming pipelines, each completed task can be tracked (temporarily with final outputs), so resumption logic is "start from task N+1". If you rely on explicit intermediate checkpoints, you can extend the process logic to save and reload state as needed.
121+
Yes. Run the same pipeline with the same `checkpoint_path`. NeMo Curator records completed source partitions under `.nemo_curator_metadata` and skips them on a later launch. Incomplete sources restart from the source; stage payloads and in-memory state are not serialized. Refer to [Resumable Processing](/reference/infra/resumable-processing) for storage, replay, and multi-host constraints.
122122
</Accordion>
123123

124124
---

fern/versions/main/pages/api-reference/executors/experimental.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class BaseExecutor(ABC):
8282
8383
Args:
8484
stages: Processing stages to execute.
85-
initial_tasks: Initial tasks (defaults to EmptyTask).
85+
initial_tasks: Initial tasks (defaults to a fresh `EmptyTask()` instance).
8686
8787
Returns:
8888
Output tasks from final stage.

fern/versions/main/pages/api-reference/executors/xenna-executor.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def execute(
128128
129129
Args:
130130
stages: List of processing stages to execute.
131-
initial_tasks: Initial tasks (defaults to EmptyTask).
131+
initial_tasks: Initial tasks (defaults to a fresh `EmptyTask()` instance).
132132
133133
Returns:
134134
List of output tasks from the final stage.

fern/versions/main/pages/api-reference/pipeline.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,26 @@ def run(
7474
self,
7575
executor: BaseExecutor | None = None,
7676
initial_tasks: list[Task] | None = None,
77+
checkpoint_path: str | Path | None = None,
7778
) -> list[Task] | None:
7879
"""Execute the pipeline.
7980
8081
Args:
8182
executor: Executor to use. Defaults to XennaExecutor.
8283
initial_tasks: Initial tasks to start the pipeline.
84+
checkpoint_path: Optional resumability directory. Completed source
85+
partitions are recorded under `.nemo_curator_metadata` and skipped
86+
when the pipeline runs again with the same path. In multi-node or
87+
distributed runs, this must resolve to the same durable shared
88+
filesystem location on every worker and node.
8389
8490
Returns:
8591
List of output tasks from the final stage, or None.
8692
"""
8793
```
8894

95+
Refer to [Resumable Processing](/reference/infra/resumable-processing) for checkpoint storage, replay semantics, task identity, and concurrency limitations.
96+
8997
### `describe()`
9098

9199
Get a detailed description of the pipeline.
@@ -140,6 +148,14 @@ executor = XennaExecutor(config={"execution_mode": "streaming"})
140148
results = pipeline.run(executor=executor)
141149
```
142150

151+
### Resume an Interrupted Run
152+
153+
```python
154+
results = pipeline.run(checkpoint_path="./checkpoints/text-curation")
155+
```
156+
157+
Run the same pipeline with the same path after an interruption. Sources recorded as completed are skipped; incomplete sources restart from the source stage.
158+
143159
### Pipeline Configuration
144160

145161
```python

fern/versions/main/pages/api-reference/processing-stage.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ def process_batch(self, tasks: list[InputT]) -> list[OutputT | None]:
142142
"""
143143
```
144144

145+
`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.
146+
147+
For source-level checkpointing and the complete mapping rules, refer to [Resumable Processing](/reference/infra/resumable-processing#custom-stage-responsibilities).
148+
145149
## Creating Custom Stages
146150

147151
```python
@@ -176,7 +180,6 @@ class MyCustomStage(ProcessingStage[DocumentBatch, DocumentBatch]):
176180
return None
177181

178182
return DocumentBatch(
179-
task_id=f"{task.task_id}_{self.name}",
180183
dataset_name=task.dataset_name,
181184
data=df,
182185
_metadata=task._metadata,

fern/versions/main/pages/api-reference/tasks/audio-task.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ class AudioTask(Task[dict]):
2121
"""Task containing a single audio manifest entry for processing.
2222
2323
Attributes:
24-
task_id: Unique identifier for this task.
24+
task_id: Framework-managed deterministic lineage identifier.
2525
dataset_name: Name of the source dataset.
2626
data: Audio manifest entry (single dict, stored as _AttrDict).
2727
"""
2828

29-
task_id: str
3029
dataset_name: str
3130
data: dict # _AttrDict subclass — supports attribute-style access
31+
# task_id is inherited from Task with init=False.
3232
```
3333

3434
## Audio Manifest Format
@@ -67,7 +67,6 @@ from nemo_curator.tasks import AudioTask
6767

6868
# Single manifest entry
6969
task = AudioTask(
70-
task_id="audio_001",
7170
dataset_name="speech_dataset",
7271
data={
7372
"audio_filepath": "/data/audio/sample.wav",
@@ -81,6 +80,8 @@ task.data["audio_filepath"] # "/data/audio/sample.wav"
8180
task.data.audio_filepath # "/data/audio/sample.wav"
8281
```
8382

83+
Do not pass or derive `task_id`. Treat it as read-only even when working with a task subclass that exposes a compatibility default; the adapter overwrites it with framework lineage at the next stage boundary.
84+
8485
## Usage in Stages
8586

8687
All audio stages subclass `ProcessingStage[AudioTask, AudioTask]` directly — there is no intermediate base class.

fern/versions/main/pages/api-reference/tasks/document-batch.mdx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ class DocumentBatch(Task[pa.Table | pd.DataFrame]):
2323
"""Task containing a batch of text documents.
2424
2525
Attributes:
26-
task_id: Unique identifier for this batch.
26+
task_id: Framework-managed deterministic lineage identifier.
2727
dataset_name: Name of the source dataset.
2828
data: DataFrame or PyArrow Table containing documents.
2929
"""
3030

31-
task_id: str
3231
dataset_name: str
3332
data: pa.Table | pd.DataFrame
33+
# task_id is inherited from Task with init=False.
3434
```
3535

3636
## Expected Data Schema
@@ -112,14 +112,15 @@ df = pd.DataFrame({
112112
})
113113

114114
batch = DocumentBatch(
115-
task_id="batch_001",
116115
dataset_name="my_dataset",
117116
data=df,
118117
)
119118

120119
print(f"Batch contains {batch.num_items} documents")
121120
```
122121

122+
Do not pass `task_id` to the constructor. It remains empty until the framework assigns lineage at a stage boundary or when the task is supplied through `initial_tasks`.
123+
123124
## Usage in Stages
124125

125126
```python
@@ -151,7 +152,6 @@ class TextFilterStage(ProcessingStage[DocumentBatch, DocumentBatch]):
151152
return None
152153

153154
return DocumentBatch(
154-
task_id=f"{task.task_id}_filtered",
155155
dataset_name=task.dataset_name,
156156
data=filtered_df,
157157
_metadata=task._metadata,
@@ -169,7 +169,6 @@ def process(self, task: DocumentBatch) -> DocumentBatch:
169169
df["word_count"] = df["text"].str.split().str.len()
170170

171171
return DocumentBatch(
172-
task_id=f"{task.task_id}_{self.name}",
173172
dataset_name=task.dataset_name,
174173
data=df,
175174
_metadata=task._metadata,
@@ -188,7 +187,6 @@ def process(self, task: DocumentBatch) -> DocumentBatch | None:
188187
return None # Filter out entire batch
189188

190189
return DocumentBatch(
191-
task_id=f"{task.task_id}_{self.name}",
192190
dataset_name=task.dataset_name,
193191
data=filtered,
194192
_metadata=task._metadata,

fern/versions/main/pages/api-reference/tasks/image-batch.mdx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ class ImageBatch(Task[list[ImageObject]]):
2222
"""Task containing a batch of images.
2323
2424
Attributes:
25-
task_id: Unique identifier for this batch.
25+
task_id: Framework-managed deterministic lineage identifier.
2626
dataset_name: Name of the source dataset.
2727
data: List of ImageObject instances.
2828
"""
2929

30-
task_id: str
3130
dataset_name: str
3231
data: list[ImageObject]
32+
# task_id is inherited from Task with init=False.
3333
```
3434

3535
## ImageObject
@@ -88,12 +88,13 @@ images = [
8888

8989
# Create batch
9090
batch = ImageBatch(
91-
task_id="img_batch_001",
9291
dataset_name="image_dataset",
9392
data=images,
9493
)
9594
```
9695

96+
Do not pass `task_id` to the constructor. The framework assigns it after the task crosses a stage boundary.
97+
9798
## Usage in Stages
9899

99100
```python
@@ -125,7 +126,6 @@ class ImageFilterStage(ProcessingStage[ImageBatch, ImageBatch]):
125126
return None
126127

127128
return ImageBatch(
128-
task_id=f"{task.task_id}_filtered",
129129
dataset_name=task.dataset_name,
130130
data=filtered,
131131
_metadata=task._metadata,
@@ -143,7 +143,6 @@ def process(self, task: ImageBatch) -> ImageBatch:
143143
img.embeddings = self.model.encode(img.path)
144144

145145
return ImageBatch(
146-
task_id=f"{task.task_id}_{self.name}",
147146
dataset_name=task.dataset_name,
148147
data=task.data,
149148
_metadata=task._metadata,
@@ -164,7 +163,6 @@ def process(self, task: ImageBatch) -> ImageBatch | None:
164163
return None
165164

166165
return ImageBatch(
167-
task_id=f"{task.task_id}_{self.name}",
168166
dataset_name=task.dataset_name,
169167
data=filtered,
170168
_metadata=task._metadata,

fern/versions/main/pages/api-reference/tasks/video-task.mdx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ class VideoTask(Task[Video]):
2222
"""Task containing a single video for processing.
2323
2424
Attributes:
25-
task_id: Unique identifier for this task.
25+
task_id: Framework-managed deterministic lineage identifier.
2626
dataset_name: Name of the source dataset.
2727
data: Video object with path and metadata.
2828
"""
2929

30-
task_id: str
3130
dataset_name: str
3231
data: Video
32+
# task_id is inherited from Task with init=False.
3333
```
3434

3535
## Video Class
@@ -88,12 +88,13 @@ video = Video(
8888

8989
# Create task
9090
task = VideoTask(
91-
task_id="video_001",
9291
dataset_name="video_dataset",
9392
data=video,
9493
)
9594
```
9695

96+
Do not pass `task_id` to the constructor. The framework assigns it after the task crosses a stage boundary.
97+
9798
## Usage in Stages
9899

99100
```python
@@ -123,7 +124,6 @@ class VideoFilterStage(ProcessingStage[VideoTask, VideoTask]):
123124
return None
124125

125126
return VideoTask(
126-
task_id=f"{task.task_id}_filtered",
127127
dataset_name=task.dataset_name,
128128
data=video,
129129
_metadata=task._metadata,
@@ -150,7 +150,6 @@ def process(self, task: VideoTask) -> list[VideoTask]:
150150
metadata=video.metadata.copy(),
151151
)
152152
clips.append(VideoTask(
153-
task_id=f"{task.task_id}_clip_{i}",
154153
dataset_name=task.dataset_name,
155154
data=clip_video,
156155
_metadata=task._metadata,
@@ -168,7 +167,6 @@ def process(self, task: VideoTask) -> VideoTask:
168167
video.embeddings = self.encoder.encode(video.path)
169168

170169
return VideoTask(
171-
task_id=f"{task.task_id}_{self.name}",
172170
dataset_name=task.dataset_name,
173171
data=video,
174172
_metadata=task._metadata,

0 commit comments

Comments
 (0)