Skip to content

Commit 363737e

Browse files
committed
add loop sequential blocks
1 parent c5849ba commit 363737e

2 files changed

Lines changed: 181 additions & 6 deletions

File tree

docs/source/en/modular_diffusers/write_own_pipeline_block.md

Lines changed: 172 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ I hope by now you have a basic idea about how `PipelineBlock` manages state thro
262262

263263
## Create a `SequentialPipelineBlocks`
264264

265-
I think by this point, you're already familiar with `SequentialPipelineBlocks` and how to create them with the `from_blocks_dict` API. It's one of the most common ways to use Modular Diffusers, and we've covered it pretty well in the [quicktour](https://moon-ci-docs.huggingface.co/docs/diffusers/pr_9672/en/modular_diffusers/quicktour#modularpipelineblocks).
265+
I assume that you're already familiar with `SequentialPipelineBlocks` and how to create them with the `from_blocks_dict` API. It's one of the most common ways to use Modular Diffusers, and we've covered it pretty well in the [Getting Started Guide](https://moon-ci-docs.huggingface.co/docs/diffusers/pr_9672/en/modular_diffusers/quicktour#modularpipelineblocks).
266266

267267
But how do blocks actually connect and work together? Understanding this is crucial for building effective modular workflows. Let's explore this through an example.
268268

@@ -331,11 +331,177 @@ At runtime, you have data flow like this:
331331

332332
**How SequentialPipelineBlocks Works:**
333333

334-
1. **Execution Order**: Blocks are executed in the order they're registered in the `blocks_dict`
335-
2. **Data Flow**: Outputs from one block become available as intermediate inputs to all subsequent blocks
336-
3. **Smart Input Resolution**: The pipeline automatically figures out which values need to be provided by the user and which will be generated by previous blocks
337-
4. **Consistent Interface**: Each block maintains its own behavior and operates through its defined interface, while collectively these interfaces determine what the entire pipeline accepts and produces
334+
1. Blocks are executed in the order they're registered in the `blocks_dict`
335+
2. Outputs from one block become available as intermediate inputs to all subsequent blocks
336+
3. The pipeline automatically figures out which values need to be provided by the user and which will be generated by previous blocks
337+
4. Each block maintains its own behavior and operates through its defined interface, while collectively these interfaces determine what the entire pipeline accepts and produces
338338

339339
What happens within each block follows the same pattern we described earlier: each block gets its own `block_state` with the relevant inputs and intermediate inputs, performs its computation, and updates the pipeline state with its intermediate outputs.
340340

341-
## `LoopSequentialPipelineBlocks`
341+
## `LoopSequentialPipelineBlocks`
342+
343+
To create a loop in Modular Diffusers, you could use a single `PipelineBlock` like this:
344+
345+
```python
346+
class DenoiseLoop(PipelineBlock):
347+
def __call__(self, components, state):
348+
block_state = self.get_block_state(state)
349+
for t in range(block_state.num_inference_steps):
350+
# ... loop logic here
351+
pass
352+
self.add_block_state(state, block_state)
353+
return components, state
354+
```
355+
356+
Or you could create a `LoopSequentialPipelineBlocks`. The key difference is that with `LoopSequentialPipelineBlocks`, the loop itself is modular: you can add or remove blocks within the loop or reuse the same loop structure with different block combinations.
357+
358+
It involves two parts: a **loop wrapper** and **loop blocks**
359+
360+
* The **loop wrapper** (`LoopSequentialPipelineBlocks`) defines the loop structure, e.g. it defines the iteration variables, and loop configurations such as progress bar.
361+
362+
* The **loop blocks** are basically standard pipeline blocks you add to the loop wrapper.
363+
- they run sequentially for each iteration of the loop
364+
- they receive the current iteration index as an additional parameter
365+
- they share the same block_state throughout the entire loop
366+
367+
Unlike regular `SequentialPipelineBlocks` where each block gets its own state, loop blocks share a single state that persists and evolves across iterations.
368+
369+
We will build a simple loop block to demonstrate these concepts. Creating a loop block involves three steps:
370+
1. defining the loop wrapper class
371+
2. creating the loop blocks
372+
3. adding the loop blocks to the loop wrapper class to create the loop wrapper instance
373+
374+
**Step 1: Define the Loop Wrapper**
375+
376+
To create a `LoopSequentialPipelineBlocks` class, you need to define:
377+
378+
* `loop_inputs`: User input variables (equivalent to `PipelineBlock.inputs`)
379+
* `loop_intermediate_inputs`: Intermediate variables needed from the mutable pipeline state (equivalent to `PipelineBlock.intermediates_inputs`)
380+
* `loop_intermediate_outputs`: New intermediate variables this block will add to the mutable pipeline state (equivalent to `PipelineBlock.intermediates_outputs`)
381+
* `__call__` method: Defines the loop structure and iteration logic
382+
383+
Here is an example of a loop wrapper:
384+
385+
```py
386+
import torch
387+
from diffusers.modular_pipelines import LoopSequentialPipelineBlocks, PipelineBlock, InputParam, OutputParam
388+
389+
class LoopWrapper(LoopSequentialPipelineBlocks):
390+
model_name = "test"
391+
@property
392+
def description(self):
393+
return "I'm a loop!!"
394+
@property
395+
def loop_inputs(self):
396+
return [InputParam(name="num_steps")]
397+
@torch.no_grad()
398+
def __call__(self, components, state):
399+
block_state = self.get_block_state(state)
400+
# Loop structure - can be customized to your needs
401+
for i in range(block_state.num_steps):
402+
# loop_step executes all registered blocks in sequence
403+
components, block_state = self.loop_step(components, block_state, i=i)
404+
self.add_block_state(state, block_state)
405+
return components, state
406+
```
407+
408+
**Step 2: Create Loop Blocks**
409+
410+
Loop blocks are standard `PipelineBlock`s, but their `__call__` method works differently:
411+
* It receives the iteration variable (e.g., `i`) passed by the loop wrapper
412+
* It works directly with `block_state` instead of pipeline state
413+
* No need to call `self.get_block_state()` or `self.add_block_state()`
414+
415+
```py
416+
class LoopBlock(PipelineBlock):
417+
# this is used to identify the model family, we won't worry about it in this example
418+
model_name = "test"
419+
@property
420+
def inputs(self):
421+
return [InputParam(name="x")]
422+
@property
423+
def intermediate_outputs(self):
424+
# outputs produced by this block
425+
return [OutputParam(name="x")]
426+
@property
427+
def description(self):
428+
return "I'm a block used inside the `LoopWrapper` class"
429+
def __call__(self, components, block_state, i: int):
430+
block_state.x += 1
431+
return components, block_state
432+
```
433+
434+
**Step 3: Combine Everything**
435+
436+
Finally, assemble your loop by adding the block(s) to the wrapper:
437+
438+
```py
439+
loop = LoopWrapper.from_blocks_dict({"block1": LoopBlock})
440+
```
441+
442+
Now you've created a loop with one step:
443+
444+
```py
445+
>>> loop
446+
LoopWrapper(
447+
Class: LoopSequentialPipelineBlocks
448+
449+
Description: I'm a loop!!
450+
451+
Sub-Blocks:
452+
[0] block1 (LoopBlock)
453+
Description: I'm a block used inside the `LoopWrapper` class
454+
455+
)
456+
```
457+
458+
It has two inputs: `x` (used at each step within the loop) and `num_steps` used to define the loop.
459+
460+
```py
461+
>>> print(loop.doc)
462+
class LoopWrapper
463+
464+
I'm a loop!!
465+
466+
Inputs:
467+
468+
x (`None`, *optional*):
469+
470+
num_steps (`None`, *optional*):
471+
472+
Outputs:
473+
474+
x (`None`):
475+
```
476+
477+
**Running the Loop:**
478+
479+
```py
480+
# run the loop
481+
loop_pipeline = loop.init_pipeline()
482+
x = loop_pipeline(num_steps=10, x=0, output="x")
483+
assert x == 10
484+
```
485+
486+
**Adding Multiple Blocks:**
487+
488+
We can add multiple blocks to run within each iteration. Let's run the loop block twice within each iteration:
489+
490+
```py
491+
loop = LoopWrapper.from_blocks_dict({"block1": LoopBlock(), "block2": LoopBlock})
492+
loop_pipeline = loop.init_pipeline()
493+
x = loop_pipeline(num_steps=10, x=0, output="x")
494+
assert x == 20 # Each iteration runs 2 blocks, so 10 iterations * 2 = 20
495+
```
496+
497+
**Key Differences from SequentialPipelineBlocks:**
498+
499+
The main difference is that loop blocks share the same `block_state` across all iterations, allowing values to accumulate and evolve throughout the loop. Loop blocks could receive additional arguments (like the current iteration index) depending on the loop wrapper's implementation, since the wrapper defines how loop blocks are called. You can easily add, remove, or reorder blocks within the loop without changing the loop logic itself.
500+
501+
The officially supported denoising loops in Modular Diffusers are implemented using `LoopSequentialPipelineBlocks`. You can explore the actual implementation to see how these concepts work in practice:
502+
503+
```py
504+
from diffusers.modular_pipelines.stable_diffusion_xl.denoise import StableDiffusionXLDenoiseStep
505+
StableDiffusionXLDenoiseStep()
506+
```
507+

src/diffusers/modular_pipelines/modular_pipeline.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,15 @@ def from_blocks_dict(cls, blocks_dict: Dict[str, Any]) -> "LoopSequentialPipelin
14521452
A new LoopSequentialPipelineBlocks instance
14531453
"""
14541454
instance = cls()
1455+
1456+
# Create instances if classes are provided
1457+
sub_blocks = InsertableDict()
1458+
for name, block in blocks_dict.items():
1459+
if inspect.isclass(block):
1460+
sub_blocks[name] = block()
1461+
else:
1462+
sub_blocks[name] = block
1463+
14551464
instance.block_classes = [block.__class__ for block in blocks_dict.values()]
14561465
instance.block_names = list(blocks_dict.keys())
14571466
instance.sub_blocks = blocks_dict

0 commit comments

Comments
 (0)