# Working with workflows
🧪 Workflow is experimental and its APIs can change in the future.
Workflows provide a simple mechanism to share your pipeline call arguments and scheduler configuration, making it easier to reproduce results.
## Serializing a workflow
A [`Workflow`] object provides all the argument values in the `__call__()` of a pipeline. Add `return_workflow=True` to return a `Workflow` object.
```python
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, safety_checker=None
).to("cuda")
outputs = pipeline(
"A painting of a horse",
num_inference_steps=15,
generator=torch.manual_seed(0),
return_workflow=True
)
workflow = outputs.workflow
```
It's mandatory to specify the `generator` when `return_workflow` is set to True.
If you look at this specific workflow, you'll see values like the number of inference steps, guidance scale, and height and width as well as the scheduler details:
```bash
{'prompt': 'A painting of a horse',
'height': None,
'width': None,
'num_inference_steps': 15,
'guidance_scale': 7.5,
'negative_prompt': None,
'eta': 0.0,
'latents': None,
'prompt_embeds': None,
'negative_prompt_embeds': None,
'output_type': 'pil',
'return_dict': True,
'callback': None,
'callback_steps': 1,
'cross_attention_kwargs': None,
'guidance_rescale': 0.0,
'clip_skip': None,
'generator_seed': 0,
'generator_device': device(type='cpu'),
'_name_or_path': 'runwayml/stable-diffusion-v1-5',
'scheduler_config': FrozenDict([('num_train_timesteps', 1000),
('beta_start', 0.00085),
('beta_end', 0.012),
('beta_schedule', 'scaled_linear'),
('trained_betas', None),
('skip_prk_steps', True),
('set_alpha_to_one', False),
('prediction_type', 'epsilon'),
('timestep_spacing', 'leading'),
('steps_offset', 1),
('_use_default_values', ['prediction_type', 'timestep_spacing']),
('_class_name', 'PNDMScheduler'),
('_diffusers_version', '0.6.0'),
('clip_sample', False)])}
```
Once you have generated a workflow object, you can serialize it with [`~Workflow.save_workflow`]:
```python
outputs.workflow.save_workflow("my-simple-workflow-sd")
```
By default, your workflows are saved as `diffusion_workflow.json`, but you can give them a specific name with the `filename` argument:
```python
outputs.workflow.save_workflow("my-simple-workflow-sd", filename="my_workflow.json")
```
You can also set `push_to_hub=True` in [`~Workflow.save_workflow`] to directly push the workflow object to the Hub.
## Loading a workflow
You can load a workflow in a pipeline with [`~DiffusionPipeline.load_workflow`]:
```python
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16
).to("cuda")
pipeline.load_workflow("sayakpaul/my-simple-workflow-sd")
```
Once the pipeline is loaded with the desired workflow, it's ready to be called:
```python
image = pipeline().images[0]
```
By default, while loading a workflow, the scheduler of the underlying pipeline from the workflow isn't modified but you can change it by adding `load_scheduler=True`:
```
pipeline.load_workflow("sayakpaul/my-simple-workflow-sd", load_scheduler=True)
```
This is particularly useful if you have changed the scheduler after loading a pipeline.
You can also override the pipeline call arguments. For example, to add a `negative_prompt`:
```python
image = pipeline(negative_prompt="bad quality").images[0]
```
Loading from a workflow is possible by specifying the `filename` argument inside the [`DiffusionPipeline.load_workflow`] method.
A workflow doesn't necessarily have to be used with the same pipeline that generated it. You can use it with a different pipeline too:
```python
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16
).to("cuda")
pipeline.load_workflow("sayakpaul/my-simple-workflow-sd")
image = pipeline().images[0]
```
However, make sure to thoroughly inspect the values you are calling the pipeline with, in this case.
Loading from a local workflow is also possible:
```python
from diffusers import DiffusionPipeline
import torch
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16
).to("cuda")
pipeline.load_workflow("path_to_local_dir")
image = pipeline().images[0]
```
Alternatively, if you want to load a workflow file and populate the pipeline arguments manually:
```python
from diffusers import DiffusionPipeline
import json
import torch
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16
).to("cuda")
with open("path_to_workflow_file.json") as f:
workflow = json.load(f)
pipeline.load_workflow(workflow)
images = pipeline().images[0]
```
## Unsupported serialization types
Image-to-image pipelines like [`StableDiffusionControlNetPipeline`] accept one or more images in their `call` method. Currently, workflows don't support serializing `call` arguments that are of type `PIL.Image.Image` or `List[PIL.Image.Image]`. To make those pipelines work with workflows, you need to pass the images manually.
Let's say you generated the workflow below:
```python
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, UniPCMultistepScheduler
from diffusers.utils import load_image
import numpy as np
import torch
import cv2
from PIL import Image
# download an image
image = load_image(
"https://hf.co/datasets/huggingface/documentation-images/resolve/main/diffusers/input_image_vermeer.png"
)
image = np.array(image)
# get canny image
image = cv2.Canny(image, 100, 200)
image = image[:, :, None]
image = np.concatenate([image, image, image], axis=2)
canny_image = Image.fromarray(image)
# load control net and stable diffusion v1-5
controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny", torch_dtype=torch.float16)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", controlnet=controlnet, torch_dtype=torch.float16
)
pipe.enable_model_cpu_offload()
# generate image
generator = torch.manual_seed(0)
outputs = pipe(
prompt="futuristic-looking office",
image=canny_image,
num_inference_steps=20,
generator=generator,
return_workflow=True
)
workflow = outputs.workflow
```
If you look at the workflow, you'll see the image that was passed to the pipeline isn't included:
```bash
{'prompt': 'futuristic-looking office',
'height': None,
'width': None,
'num_inference_steps': 20,
'guidance_scale': 7.5,
'negative_prompt': None,
'eta': 0.0,
'latents': None,
'prompt_embeds': None,
'negative_prompt_embeds': None,
'output_type': 'pil',
'return_dict': True,
'callback': None,
'callback_steps': 1,
'cross_attention_kwargs': None,
'controlnet_conditioning_scale': 1.0,
'guess_mode': False,
'control_guidance_start': 0.0,
'control_guidance_end': 1.0,
'clip_skip': None,
'generator_seed': 0,
'generator_device': 'cpu',
'_name_or_path': 'runwayml/stable-diffusion-v1-5',
'scheduler_config': FrozenDict([('num_train_timesteps', 1000),
('beta_start', 0.00085),
('beta_end', 0.012),
('beta_schedule', 'scaled_linear'),
('trained_betas', None),
('solver_order', 2),
('prediction_type', 'epsilon'),
('thresholding', False),
('dynamic_thresholding_ratio', 0.995),
('sample_max_value', 1.0),
('predict_x0', True),
('solver_type', 'bh2'),
('lower_order_final', True),
('disable_corrector', []),
('solver_p', None),
('use_karras_sigmas', False),
('timestep_spacing', 'linspace'),
('steps_offset', 1),
('_use_default_values',
['lower_order_final',
'sample_max_value',
'solver_p',
'dynamic_thresholding_ratio',
'thresholding',
'solver_type',
'prediction_type',
'predict_x0',
'use_karras_sigmas',
'disable_corrector',
'timestep_spacing',
'solver_order']),
('skip_prk_steps', True),
('set_alpha_to_one', False),
('_class_name', 'PNDMScheduler'),
('_diffusers_version', '0.6.0'),
('clip_sample', False)])}
```
Let's serialize the workflow and reload the pipeline to see what happens when you try to use it.
```python
workflow.save_workflow("my-simple-workflow-sd", filename="controlnet_simple.json", push_to_hub=True)
```
Then load the workflow into [`StableDiffusionControlNetPipeline`]:
```python
# load control net and stable diffusion v1-5
controlnet = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny", torch_dtype=torch.float16)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", controlnet=controlnet, torch_dtype=torch.float16
)
pipe.enable_model_cpu_offload()
pipe.load_workflow("sayakpaul/my-simple-workflow-sd", filename="controlnet_simple.json")
```
If you try to generate an image now, it'll return the following error:
```bash
TypeError: image must be passed and be one of PIL image, numpy array, torch tensor, list of PIL images, list of numpy arrays or list of torch tensors, but is
```
To resolve the error, manually pass the conditioning image `canny_image`:
```python
image = pipe(image=canny_image).images[0]
```
Other unsupported serialization types include:
* LoRA checkpoints: any information from LoRA checkpoints that might be loaded into a pipeline isn't serialized. Workflows generated from pipelines loaded with a LoRA checkpoint should be handled cautiously! You should ensure the LoRA checkpoint is loaded into the pipeline first before loading the corresponding workflow.
* Call arguments including the following types: `torch.Tensor`, `np.ndarray`, `Callable`, `PIL.Image.Image`, and `List[PIL.Image.Image]`.