You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a from-scratch SPDL pipeline authoring guide
(`tools/skills/authoring/building_pipelines.md`) covering stage
decomposition by operation nature, concurrency budgeting, `spdl.io` for
media, and multi-threading-in-subprocess for production. Its minimal
example shows the build-once / re-iterate-per-epoch pattern.
Clarify async stage handling in the pipeline reference docs: async
functions can be passed to `.pipe()` directly and must not be wrapped in
`asyncio.run()`, which only adds per-item event-loop init/finalize
overhead and forces the work onto worker threads instead of running as
cheap cooperative coroutines on the shared event loop.
Mark `auto_stop()` as obsolete in the optimization reference docs — a
`Pipeline` is built once and re-iterated each epoch without it.
Copy file name to clipboardExpand all lines: src/spdl/autoresearch/pipeline_optimization/prompts/apply_changes.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -76,7 +76,7 @@ __PIPELINE_CODE__
76
76
13. **TorchTNT scripts**: If the code uses TorchTNT (`torchtnt.framework.fit`, `train`, `AutoUnit`), the SPDL`Pipeline`is passed directly to TorchTNT as the `train_dataloader` (Pipeline is iterable — no wrapper classis required unless instrumentation was added). When applying changes:
77
77
-**Pipeline construction changes** (concurrency, MTP, batch size): Modify the function that builds the `PipelineBuilder`, same as non-TorchTNT code. The `Pipeline` abstracts MTP vs pure multithreading, so the code passing Pipeline to TorchTNT does not change.
78
78
-**Do NOT modify TorchTNT internals** (`fit()`, `train()`, `AutoUnit.train_step`). Only modify the pipeline construction.
79
-
- The pipeline is built once and iterated many times. Thereisno need for`auto_stop()`or rebuilding per epoch.
79
+
- The pipeline is built once and iterated many times. `auto_stop()`isobsolete — do not call it, and do not rebuild per epoch.
Copy file name to clipboardExpand all lines: src/spdl/autoresearch/pipeline_optimization/prompts/knowledge/knowledge.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -268,7 +268,7 @@ This wrapper is passed to TorchTNT's `fit()` or `train()` as the `train_dataload
268
268
**Key facts about Pipeline:**
269
269
-`Pipeline` is both iterable and iterator. It can technically be directly iterated, but the wrapper pattern above is recommended because it provides `get_iterator(timeout=...)` for timeout handling and `__len__` for epoch length.
270
270
-**Preferred iteration**: Always use `pipeline.get_iterator(timeout=<seconds>)` to obtain an iterator with a timeout, so that jobs do not get stuck. Directly iterating with `for batch in pipeline:` or `iter(pipeline)` is discouraged because it lacks timeout handling.
271
-
- The pipeline is built once and iterated many times. There is no need for `auto_stop()` or rebuilding per epoch.
271
+
- The pipeline is built once and iterated many times. `auto_stop()` is obsolete — do not call it, and do not rebuild per epoch.
272
272
- The `Pipeline` abstracts away whether it uses MTP (subprocess) or pure multithreading internally, so switching between them does not affect how the Pipeline is consumed.
273
273
- TorchTNT calls `iter(dataloader)` at the start of each epoch, then `next(data_iter)` per step until `StopIteration`.
274
274
- The pipeline's source controls how many items are produced per epoch. When the source is exhausted, the iterator raises `StopIteration`, ending the epoch in TorchTNT.
Copy file name to clipboardExpand all lines: src/spdl/autoresearch/pipeline_optimization/prompts/knowledge/optimization_strategies.md
+31Lines changed: 31 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,37 @@ pipeline = (
33
33
|`buffer_size`|`.add_sink(N)`| 2-10; affects memory, NOT throughput |
34
34
|`output_order`|`.pipe(fn, output_order=...)`| "completion" for I/O (avoids head-of-line blocking), "input" for determinism |
35
35
36
+
### Async Stage Functions: Pass Them As-Is
37
+
38
+
`.pipe()` accepts both regular (`def`) and asynchronous (`async def`) callables. **Pass an async function directly — do not wrap it in `asyncio.run()`:**
39
+
40
+
```python
41
+
asyncdeffetch(key):
42
+
asyncwith session.get(url(key)) as resp:
43
+
returnawait resp.read()
44
+
45
+
pipeline = (
46
+
PipelineBuilder()
47
+
.add_source(source)
48
+
.pipe(fetch, concurrency=32) # CORRECT: coroutine function passed as-is
49
+
...
50
+
)
51
+
```
52
+
53
+
SPDL runs stages on its own event loop, so it awaits async stage functions natively and drives up to `concurrency` of them at once as cheap cooperative coroutines — ideal for I/O-bound work.
54
+
55
+
**Anti-pattern:** wrapping the coroutine so the stage becomes synchronous.
56
+
57
+
```python
58
+
# WRONG — asyncio.run() spins up and tears down a brand-new event loop on
59
+
# every single item. That per-item loop init/finalize is pure overhead, and
60
+
# it forces the work onto pipeline worker threads instead of running as cheap
61
+
# cooperative coroutines on SPDL's shared event loop.
Copy file name to clipboardExpand all lines: src/spdl/autoresearch/pipeline_optimization/prompts/plan_next.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,7 +77,7 @@ Before you can consider stopping, ALL of the following must have been attempted
77
77
-**Critical — understand the existing code first**: Inspect the pipeline builder function's return type and structure. If it calls `.build()` or `.get_iterator()` (returning a `Pipeline` or iterator), the MTP refactor must change it to return a `PipelineConfig` (via `.get_config()`) or an unbuilt `PipelineBuilder` instead. `run_pipeline_in_subprocess()` accepts ONLY `PipelineConfig` — NOT `Pipeline` objects or iterators. Check type annotations and return statements.
78
78
-**Separate CPU and GPU stages**: The backend pipeline (subprocess) must contain ONLY CPU-bound stages (fetch, decode, aggregate, collate). GPU stages like `transfer_tensor` require a CUDA context and MUST be in the frontend pipeline (main process). If the existing code includes `transfer_tensor` or similar GPU ops in the pipeline, exclude them from the backend config.
79
79
-**Write the `description` field to explicitly instruct the code modifier** about these structural requirements, e.g.: "Refactor `build_pipeline()` to NOT call `.build()`. Instead, build a `PipelineBuilder` with only CPU stages (no `transfer_tensor`), call `.get_config()`, pass the config to `run_pipeline_in_subprocess(config, num_threads=N)`, then create a frontend `PipelineBuilder` that takes the subprocess iterable as source and applies `transfer_tensor`."
80
-
-**TorchTNT scripts**: If the code uses TorchTNT (`fit()`, `train()`, `AutoUnit`), the MTP refactor targets only the pipeline builder function — NOT TorchTNT internals. The `Pipeline` object abstracts away MTP vs pure multithreading, so the wrapper class that calls `get_iterator(timeout=...)` does not need to change. Do NOT use `auto_stop()` — the pipeline is built once and iterated many times.
80
+
-**TorchTNT scripts**: If the code uses TorchTNT (`fit()`, `train()`, `AutoUnit`), the MTP refactor targets only the pipeline builder function — NOT TorchTNT internals. The `Pipeline` object abstracts away MTP vs pure multithreading, so the wrapper class that calls `get_iterator(timeout=...)` does not need to change. Do NOT use `auto_stop()`(obsolete) — the pipeline is built once and iterated many times.
81
81
- Use the **two-tier approach** for pickling: first try module-level functions with `functools.partial` (Tier 1). If the subprocess crashes silently (0 batches), retry with picklable callable classes that pickle objects directly from the main process (Tier 2). See "Pickling Constraints" in the knowledge base.
82
82
-**HuggingFace tokenizers are NOT thread-safe** — use thread-local storage (TLS) when tokenizing with concurrency > 1.
83
83
- Wrap the sampler with **`spdl.source.utils.embed_shuffle()`** for correct sampling behavior.
@@ -204,6 +204,6 @@ Rules:
204
204
- torchx entrypoint args use **underscores**, not dashes (e.g. `--num_threads`)
205
205
- The orchestrator will call a separate Claude session to apply the code changes described in `description`, commit them, rebuild the image, and then launch. **Write the `description` field as precise instructions for what code to modify** — specify function names, SPDL API calls to add/change, and the exact transformation. Do not write vague descriptions like "enable MTP" — instead write "Refactor `build_pipeline()` to return a `PipelineBuilder` config (without `.build()`), wrap it with `spdl.pipeline.run_pipeline_in_subprocess(config, num_threads=16, mp_context='forkserver')`, and create an outer pipeline that takes the subprocess source and applies GPU transfer via `pipe(transfer_tensor, executor=ThreadPoolExecutor(1))`."
206
206
- Each experiment should differ from the baseline in exactly one dimension (or a small, justified set of changes)
207
-
-**TorchTNT scripts**: When writing `description` for rebuild experiments in TorchTNT code, specify changes to the pipeline builder function only. The `Pipeline` abstracts away MTP vs pure multithreading, so the wrapper class (which calls `get_iterator(timeout=...)`) and TorchTNT internals (`fit()`, `train()`, `AutoUnit`) do not change. Do NOT use `auto_stop()` — Pipeline supports multiple iterations without it.
207
+
-**TorchTNT scripts**: When writing `description` for rebuild experiments in TorchTNT code, specify changes to the pipeline builder function only. The `Pipeline` abstracts away MTP vs pure multithreading, so the wrapper class (which calls `get_iterator(timeout=...)`) and TorchTNT internals (`fit()`, `train()`, `AutoUnit`) do not change. Do NOT use `auto_stop()`(obsolete) — Pipeline supports multiple iterations natively.
208
208
-**`best_practices_tags`**: Tag each experiment with which best practices it covers from the valid tags list. This is how the orchestrator tracks progress. If an experiment covers multiple practices, include all relevant tags.
209
209
-**`goto`** (per-experiment): The orchestrator checks out the instrumentation (anchor) commit before applying each experiment's code changes by default. This ensures every experiment starts from a clean slate. Set `goto` to `null` in most cases. Only set it to a specific commit hash if you want to stack changes on top of a previous successful experiment (e.g., adding batch size tuning on top of an MTP experiment that already improved metrics). Never stack incompatible changes (e.g., GPU decode on top of MTP — they are mutually exclusive). **The `goto` field also determines the experiment's parent in the hypothesis tree**: `null` means the experiment branches from baseline; a specific commit means it branches from the experiment that produced that commit. This allows a single planning round to propose experiments with different parents (e.g., NVDEC from baseline + batch_size from a successful MTP experiment).
Copy file name to clipboardExpand all lines: tools/skills/migration/migrating_to_spdl_pipeline.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -59,6 +59,8 @@ Or any iterable that yields keys/paths/metadata for downstream stages.
59
59
60
60
Map each classified operation to a `.pipe()` stage with appropriate concurrency. Separate operations of different natures into distinct stages — never bundle network I/O and CPU decode in one function.
61
61
62
+
`.pipe()` accepts both sync (`def`) and async (`async def`) callables. If the original loader used `async` I/O (e.g. `aiohttp`), **pass the coroutine function to `.pipe()` as-is** — SPDL awaits it on its own event loop. Do **not** wrap it as `asyncio.run(coro(x))`: that spins up and tears down a brand-new event loop on every item — pure per-item overhead — and forces the work onto worker threads instead of running as cheap cooperative coroutines on SPDL's shared event loop.
63
+
62
64
Use `PriorityThreadPoolExecutor` from `spdl.pipeline` as the shared executor. It prioritizes downstream stages over upstream ones — when the thread pool is contended, items closer to the pipeline output are processed first, reducing end-to-end latency and ensuring data flows through the pipeline faster.
0 commit comments