Skip to content

Commit db4b54c

Browse files
committed
finish the autopipelines section!
1 parent 0138e17 commit db4b54c

File tree

1 file changed

+256
-4
lines changed

1 file changed

+256
-4
lines changed

docs/source/en/modular_diffusers/write_own_pipeline_block.md

Lines changed: 256 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def make_block(inputs=[], intermediate_inputs=[], intermediate_outputs=[], block
175175
self.add_block_state(state, block_state)
176176
return components, state
177177

178-
return TestBlock()
178+
return TestBlock
179179
```
180180

181181

@@ -206,13 +206,14 @@ def image_encoder_block_fn(block_state, pipeline_state):
206206
return block_state
207207

208208
# Create a block with our definitions
209-
image_encoder_block = make_block(
209+
image_encoder_block_cls = make_block(
210210
inputs=inputs,
211211
intermediate_inputs=intermediate_inputs,
212212
intermediate_outputs=intermediate_outputs,
213213
block_fn=image_encoder_block_fn,
214214
description=" Encode raw image into its latent presentation"
215215
)
216+
image_encoder_block = image_encoder_block_cls()
216217
pipe = image_encoder_block.init_pipeline()
217218
```
218219

@@ -278,7 +279,7 @@ def input_block_fn(block_state, pipeline_state):
278279

279280
return block_state
280281

281-
input_block = make_block(
282+
input_block_cls = make_block(
282283
inputs=[
283284
InputParam(name="prompt", type_hint=list, description="list of text prompts"),
284285
InputParam(name="num_images_per_prompt", type_hint=int, description="number of images per prompt")
@@ -289,6 +290,7 @@ input_block = make_block(
289290
block_fn=input_block_fn,
290291
description="A block that determines batch_size based on the number of prompts and num_images_per_prompt argument."
291292
)
293+
input_block = input_block_cls()
292294
```
293295

294296
Now let's connect these blocks to create a pipeline:
@@ -307,7 +309,7 @@ pipeline = blocks.init_pipeline()
307309

308310
Now you have a pipeline with 2 blocks.
309311

310-
``py
312+
```py
311313
>>> pipeline.blocks
312314
SequentialPipelineBlocks(
313315
Class: ModularPipelineBlocks
@@ -528,3 +530,253 @@ from diffusers.modular_pipelines.stable_diffusion_xl.denoise import StableDiffus
528530
StableDiffusionXLDenoiseStep()
529531
```
530532

533+
## `AutoPipelineBlocks`
534+
535+
`AutoPipelineBlocks` allows you to pack different pipelines into one and automatically select which one to run at runtime based on the inputs. The main purpose is convenience and portability - for developers, you can package everything into one workflow, making it easier to share and use.
536+
537+
For example, you might want to support text-to-image and image-to-image tasks. Instead of creating two separate pipelines, you can create an `AutoPipelineBlocks` that automatically chooses the workflow based on whether an `image` input is provided.
538+
539+
Let's see an example. Here we'll create a dummy `AutoPipelineBlocks` that includes dummy text-to-image, image-to-image, and inpaint pipelines.
540+
541+
542+
```py
543+
from diffusers.modular_pipelines import AutoPipelineBlocks
544+
545+
# These are dummy blocks and we only focus on "inputs" for our purpose
546+
inputs = [InputParam(name="prompt")]
547+
# block_fn prints out which workflow is running so we can see the execution order at runtime
548+
block_fn = lambda x, y: print("running the text-to-image workflow")
549+
block_t2i_cls = make_block(inputs=inputs, block_fn=block_fn, description="I'm a text-to-image workflow!")
550+
551+
inputs = [InputParam(name="prompt"), InputParam(name="image")]
552+
block_fn = lambda x, y: print("running the image-to-image workflow")
553+
block_i2i_cls = make_block(inputs=inputs, block_fn=block_fn, description="I'm a image-to-image workflow!")
554+
555+
inputs = [InputParam(name="prompt"), InputParam(name="image"), InputParam(name="mask")]
556+
block_fn = lambda x, y: print("running the inpaint workflow")
557+
block_inpaint_cls = make_block(inputs=inputs, block_fn=block_fn, description="I'm a inpaint workflow!")
558+
559+
class AutoImageBlocks(AutoPipelineBlocks):
560+
# List of sub-block classes to choose from
561+
block_classes = [block_inpaint_cls, block_i2i_cls, block_t2i_cls]
562+
# Names for each block in the same order
563+
block_names = ["inpaint", "img2img", "text2img"]
564+
# Trigger inputs that determine which block to run
565+
# - "mask" triggers inpaint workflow
566+
# - "image" triggers img2img workflow (but only if mask is not provided)
567+
# - if none of above, runs the text2img workflow (default)
568+
block_trigger_inputs = ["mask", "image", None]
569+
# Description is extremely important for AutoPipelineBlocks
570+
@property
571+
def description(self):
572+
return (
573+
"Pipeline generates images given different types of conditions!\n"
574+
+ "This is an auto pipeline block that works for text2img, img2img and inpainting tasks.\n"
575+
+ " - inpaint workflow is run when `mask` is provided.\n"
576+
+ " - img2img workflow is run when `image` is provided (but only when `mask` is not provided).\n"
577+
+ " - text2img workflow is run when neither `image` nor `mask` is provided.\n"
578+
)
579+
580+
# Create the blocks
581+
auto_blocks = AutoImageBlocks()
582+
# convert to pipeline
583+
auto_pipeline = auto_blocks.init_pipeline()
584+
```
585+
586+
Now we have created an `AutoPipelineBlocks` that contains 3 sub-blocks. Notice the warning message at the top - this automatically appears in every `ModularPipelineBlocks` that contains `AutoPipelineBlocks` to remind end users that dynamic block selection happens at runtime.
587+
588+
```py
589+
AutoImageBlocks(
590+
Class: AutoPipelineBlocks
591+
592+
====================================================================================================
593+
This pipeline contains blocks that are selected at runtime based on inputs.
594+
Trigger Inputs: ['mask', 'image']
595+
====================================================================================================
596+
597+
598+
Description: Pipeline generates images given different types of conditions!
599+
This is an auto pipeline block that works for text2img, img2img and inpainting tasks.
600+
- inpaint workflow is run when `mask` is provided.
601+
- img2img workflow is run when `image` is provided (but only when `mask` is not provided).
602+
- text2img workflow is run when neither `image` nor `mask` is provided.
603+
604+
605+
606+
Sub-Blocks:
607+
• inpaint [trigger: mask] (TestBlock)
608+
Description: I'm a inpaint workflow!
609+
610+
• img2img [trigger: image] (TestBlock)
611+
Description: I'm a image-to-image workflow!
612+
613+
• text2img [default] (TestBlock)
614+
Description: I'm a text-to-image workflow!
615+
616+
)
617+
```
618+
619+
Check out the documentation with `print(auto_pipeline.doc)`:
620+
621+
```py
622+
>>> print(auto_pipeline.doc)
623+
class AutoImageBlocks
624+
625+
Pipeline generates images given different types of conditions!
626+
This is an auto pipeline block that works for text2img, img2img and inpainting tasks.
627+
- inpaint workflow is run when `mask` is provided.
628+
- img2img workflow is run when `image` is provided (but only when `mask` is not provided).
629+
- text2img workflow is run when neither `image` nor `mask` is provided.
630+
631+
Inputs:
632+
633+
prompt (`None`, *optional*):
634+
635+
image (`None`, *optional*):
636+
637+
mask (`None`, *optional*):
638+
```
639+
640+
There is a fundamental trade-off of AutoPipelineBlocks: it trades clarity for convenience. While it is really easy for packaging multiple workflows, it can become confusing without proper documentation. e.g. if we just throw a pipeline at you and tell you that it contains 3 sub-blocks and takes 3 inputs `prompt`, `image` and `mask`, and ask you to run an image-to-image workflow: if you don't have any prior knowledge on how these pipelines work, you would be pretty clueless, right?
641+
642+
This pipeline we just made though, has a docstring that shows all available inputs and workflows and explains how to use each with different inputs. So it's really helpful for users. For example, it's clear that you need to pass `image` to run img2img. This is why the description field is absolutely critical for AutoPipelineBlocks. We highly recommend you to explain the conditional logic very well for each `AutoPipelineBlocks` you would make. We also recommend to always test individual pipelines first before packaging them into AutoPipelineBlocks.
643+
644+
Let's run this auto pipeline with different inputs to see if the conditional logic works as described. Remember that we have added `print` in each `PipelineBlock`'s `__call__` method to print out its workflow name, so it should be easy to tell which one is running:
645+
646+
```py
647+
>>> _ = auto_pipeline(image="image", mask="mask")
648+
running the inpaint workflow
649+
>>> _ = auto_pipeline(image="image")
650+
running the image-to-image workflow
651+
>>> _ = auto_pipeline(prompt="prompt")
652+
running the text-to-image workflow
653+
>>> _ = auto_pipeline(image="prompt", mask="mask")
654+
running the inpaint workflow
655+
```
656+
657+
However, even with documentation, it can become very confusing when AutoPipelineBlocks are combined with other blocks. The complexity grows quickly when you have nested AutoPipelineBlocks or use them as sub-blocks in larger pipelines.
658+
659+
Let's make another `AutoPipelineBlocks` - this one only contains one block, and it does not include `None` in its `block_trigger_inputs` (which corresponds to the default block to run when none of the trigger inputs are provided). This means this block will be skipped if the trigger input (`ip_adapter_image`) is not provided at runtime.
660+
661+
```py
662+
from diffusers.modular_pipelines import SequentialPipelineBlocks, InsertableDict
663+
inputs = [InputParam(name="ip_adapter_image")]
664+
block_fn = lambda x, y: print("running the ip-adapter workflow")
665+
block_ipa_cls = make_block(inputs=inputs, block_fn=block_fn, description="I'm a IP-adapter workflow!")
666+
667+
class AutoIPAdapter(AutoPipelineBlocks):
668+
block_classes = [block_ipa_cls]
669+
block_names = ["ip-adapter"]
670+
block_trigger_inputs = ["ip_adapter_image"]
671+
@property
672+
def description(self):
673+
return "Run IP Adapter step if `ip_adapter_image` is provided. This step should be placed before the 'input' step.\n"
674+
```
675+
676+
Now let's combine these 2 auto blocks together into a `SequentialPipelineBlocks`:
677+
678+
```py
679+
auto_ipa_blocks = AutoIPAdapter()
680+
blocks_dict = InsertableDict()
681+
blocks_dict["ip-adapter"] = auto_ipa_blocks
682+
blocks_dict["image-generation"] = auto_blocks
683+
all_blocks = SequentialPipelineBlocks.from_blocks_dict(blocks_dict)
684+
pipeline = all_blocks.init_pipeline()
685+
```
686+
687+
Let's take a look: now things get more confusing. In this particular example, you could still try to explain the conditional logic in the `description` field here - there are only 4 possible execution paths so it's doable. However, since this is a `SequentialPipelineBlocks` that could contain many more blocks, the complexity can quickly get out of hand as the number of blocks increases.
688+
689+
```py
690+
>>> all_blocks
691+
SequentialPipelineBlocks(
692+
Class: ModularPipelineBlocks
693+
694+
====================================================================================================
695+
This pipeline contains blocks that are selected at runtime based on inputs.
696+
Trigger Inputs: ['image', 'mask', 'ip_adapter_image']
697+
Use `get_execution_blocks()` with input names to see selected blocks (e.g. `get_execution_blocks('image')`).
698+
====================================================================================================
699+
700+
701+
Description:
702+
703+
704+
Sub-Blocks:
705+
[0] ip-adapter (AutoIPAdapter)
706+
Description: Run IP Adapter step if `ip_adapter_image` is provided. This step should be placed before the 'input' step.
707+
708+
709+
[1] image-generation (AutoImageBlocks)
710+
Description: Pipeline generates images given different types of conditions!
711+
This is an auto pipeline block that works for text2img, img2img and inpainting tasks.
712+
- inpaint workflow is run when `mask` is provided.
713+
- img2img workflow is run when `image` is provided (but only when `mask` is not provided).
714+
- text2img workflow is run when neither `image` nor `mask` is provided.
715+
716+
717+
)
718+
719+
```
720+
721+
This is when the `get_execution_blocks()` method comes in handy - it basically extracts a `SequentialPipelineBlocks` that only contains the blocks that are actually run based on your inputs.
722+
723+
Let's try some examples:
724+
725+
`mask`: we expect it to skip the first ip-adapter since `ip_adapter_image` is not provided, and then run the inpaint for the second block.
726+
727+
```py
728+
>>> all_blocks.get_execution_blocks('mask')
729+
SequentialPipelineBlocks(
730+
Class: ModularPipelineBlocks
731+
732+
Description:
733+
734+
735+
Sub-Blocks:
736+
[0] image-generation (TestBlock)
737+
Description: I'm a inpaint workflow!
738+
739+
)
740+
```
741+
742+
Let's also actually run the pipeline to confirm:
743+
744+
```py
745+
>>> _ = pipeline(mask="mask")
746+
skipping auto block: AutoIPAdapter
747+
running the inpaint workflow
748+
```
749+
750+
Try a few more:
751+
752+
```py
753+
print(f"inputs: ip_adapter_image:")
754+
blocks_select = all_blocks.get_execution_blocks('ip_adapter_image')
755+
print(f"expected_execution_blocks: {blocks_select}")
756+
print(f"actual execution blocks:")
757+
_ = pipeline(ip_adapter_image="ip_adapter_image", prompt="prompt")
758+
# expect to see ip-adapter + text2img
759+
760+
print(f"inputs: image:")
761+
blocks_select = all_blocks.get_execution_blocks('image')
762+
print(f"expected_execution_blocks: {blocks_select}")
763+
print(f"actual execution blocks:")
764+
_ = pipeline(image="image", prompt="prompt")
765+
# expect to see img2img
766+
767+
print(f"inputs: prompt:")
768+
blocks_select = all_blocks.get_execution_blocks('prompt')
769+
print(f"expected_execution_blocks: {blocks_select}")
770+
print(f"actual execution blocks:")
771+
_ = pipeline(prompt="prompt")
772+
# expect to see text2img (prompt is not a trigger input so fallback to default)
773+
774+
print(f"inputs: mask + ip_adapter_image:")
775+
blocks_select = all_blocks.get_execution_blocks('mask','ip_adapter_image')
776+
print(f"expected_execution_blocks: {blocks_select}")
777+
print(f"actual execution blocks:")
778+
_ = pipeline(mask="mask", ip_adapter_image="ip_adapter_image")
779+
# expect to see ip-adapter + inpaint
780+
```
781+
782+
In summary, `AutoPipelineBlocks` is a good tool for packaging multiple workflows into a single, convenient interface and it can greatly simplify the user experience. However, always provide clear descriptions explaining the conditional logic, test individual pipelines first before combining them, and use `get_execution_blocks()` to understand runtime behavior in complex compositions.

0 commit comments

Comments
 (0)