option to save stepwise and composite images, add interruptibility

This commit is contained in:
Anthony Wu 2024-09-23 15:20:25 -07:00
parent c05329e73b
commit fcc9b21d19
10 changed files with 229 additions and 81 deletions

5
.gitignore vendored
View File

@ -14,4 +14,7 @@
*.safetensors *.safetensors
*.json *.json
*.egg-info *.egg-info
# build/ generated from 'pip install -e .' in developer mode
build/

View File

@ -57,7 +57,7 @@ respect-gitignore = true
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default. # McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"] select = ["BLE", "E4", "E7", "E9", "F", "ICN", "LOG", "PERF", "W"]
ignore = [] ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided. # Allow fix for all enabled rules (when `--fix`) is provided.

View File

@ -3,6 +3,7 @@ from mflux.config.config import ConfigControlnet
from mflux.config.model_config import ModelConfig from mflux.config.model_config import ModelConfig
from mflux.controlnet.flux_controlnet import Flux1Controlnet from mflux.controlnet.flux_controlnet import Flux1Controlnet
from mflux.flux.flux import Flux1 from mflux.flux.flux import Flux1
from mflux.exceptions import StopImageGenerationException
from mflux.post_processing.image_util import ImageUtil from mflux.post_processing.image_util import ImageUtil
__all__ = [ __all__ = [
@ -12,4 +13,5 @@ __all__ = [
"ConfigControlnet", "ConfigControlnet",
"ModelConfig", "ModelConfig",
"ImageUtil", "ImageUtil",
"StopImageGenerationException",
] ]

View File

@ -1,16 +1,16 @@
import logging import logging
from typing import TYPE_CHECKING
import mlx.core as mx import mlx.core as mx
from mlx import nn from mlx import nn
from pathlib import Path
from tqdm import tqdm from tqdm import tqdm
from typing import TYPE_CHECKING
from mflux.config.config import ConfigControlnet from mflux.config.config import ConfigControlnet
from mflux.config.model_config import ModelConfig from mflux.config.model_config import ModelConfig
from mflux.config.runtime_config import RuntimeConfig from mflux.config.runtime_config import RuntimeConfig
from mflux.controlnet.controlnet_util import ControlnetUtil from mflux.controlnet.controlnet_util import ControlnetUtil
from mflux.controlnet.transformer_controlnet import TransformerControlnet from mflux.controlnet.transformer_controlnet import TransformerControlnet
from mflux.controlnet.weight_handler_controlnet import WeightHandlerControlnet from mflux.controlnet.weight_handler_controlnet import WeightHandlerControlnet
from mflux.exceptions import StopImageGenerationException
from mflux.models.text_encoder.clip_encoder.clip_encoder import CLIPEncoder from mflux.models.text_encoder.clip_encoder.clip_encoder import CLIPEncoder
from mflux.models.text_encoder.t5_encoder.t5_encoder import T5Encoder from mflux.models.text_encoder.t5_encoder.t5_encoder import T5Encoder
from mflux.models.transformer.transformer import Transformer from mflux.models.transformer.transformer import Transformer
@ -110,7 +110,9 @@ class Flux1Controlnet:
output: str, output: str,
controlnet_image_path: str, controlnet_image_path: str,
controlnet_save_canny: bool = False, controlnet_save_canny: bool = False,
config: ConfigControlnet = ConfigControlnet() config: ConfigControlnet = ConfigControlnet(),
stepwise_output_dir: Path = None,
stepwise_composite_only=False
) -> "GeneratedImage": # fmt: off ) -> "GeneratedImage": # fmt: off
# Create a new runtime config based on the model type and input parameters # Create a new runtime config based on the model type and input parameters
config = RuntimeConfig(config, self.model_config) config = RuntimeConfig(config, self.model_config)
@ -139,34 +141,68 @@ class Flux1Controlnet:
prompt_embeds = self.t5_text_encoder.forward(t5_tokens) prompt_embeds = self.t5_text_encoder.forward(t5_tokens)
pooled_prompt_embeds = self.clip_text_encoder.forward(clip_tokens) pooled_prompt_embeds = self.clip_text_encoder.forward(clip_tokens)
step_wise_images = []
if stepwise_output_dir:
stepwise_output_dir.mkdir(parents=True, exist_ok=True)
for t in time_steps: for t in time_steps:
# Compute controlnet samples try:
controlnet_block_samples, controlnet_single_block_samples = self.transformer_controlnet.forward( # Compute controlnet samples
t=t, controlnet_block_samples, controlnet_single_block_samples = self.transformer_controlnet.forward(
prompt_embeds=prompt_embeds, t=t,
pooled_prompt_embeds=pooled_prompt_embeds, prompt_embeds=prompt_embeds,
hidden_states=latents, pooled_prompt_embeds=pooled_prompt_embeds,
controlnet_cond=controlnet_cond, hidden_states=latents,
config=config, controlnet_cond=controlnet_cond,
) config=config,
)
# 3.t Predict the noise # 3.t Predict the noise
noise = self.transformer.predict( noise = self.transformer.predict(
t=t, t=t,
prompt_embeds=prompt_embeds, prompt_embeds=prompt_embeds,
pooled_prompt_embeds=pooled_prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds,
hidden_states=latents, hidden_states=latents,
config=config, config=config,
controlnet_block_samples=controlnet_block_samples, controlnet_block_samples=controlnet_block_samples,
controlnet_single_block_samples=controlnet_single_block_samples, controlnet_single_block_samples=controlnet_single_block_samples,
) )
# 4.t Take one denoise step # 4.t Take one denoise step
dt = config.sigmas[t + 1] - config.sigmas[t] dt = config.sigmas[t + 1] - config.sigmas[t]
latents += noise * dt latents += noise * dt
# Evaluate to enable progress tracking # Evaluate to enable progress tracking
mx.eval(latents) mx.eval(latents)
if stepwise_output_dir:
stepwise_decoded = self.vae.decode(
Flux1Controlnet._unpack_latents(latents, config.height, config.width)
)
# performance todo: Pillow mostly uses CPU,
# can try to improve image generation performance by offloading
# stepwise image processing to the CPU via threading
stepwise_img = ImageUtil.to_image(
decoded_latents=stepwise_decoded,
seed=seed,
prompt=prompt,
quantization=self.bits,
generation_time=time_steps.format_dict["elapsed"],
lora_paths=self.lora_paths,
lora_scales=self.lora_scales,
config=config,
)
step_wise_images.append(stepwise_img)
if not stepwise_composite_only:
stepwise_img.save(
path=stepwise_output_dir / f"seed_{seed}_step{t+1}of{len(time_steps)}.png",
export_json_metadata=False,
)
except KeyboardInterrupt: # noqa: PERF203
raise StopImageGenerationException(f"Stopping image generation at step {t+1}/{len(time_steps)}")
finally:
if step_wise_images and stepwise_output_dir:
composite_img = ImageUtil.to_composite_image(step_wise_images)
composite_img.save(stepwise_output_dir / f"seed_{seed}_composite.png")
# 5. Decode the latent array and return the image # 5. Decode the latent array and return the image
latents = Flux1Controlnet._unpack_latents(latents, config.height, config.width) latents = Flux1Controlnet._unpack_latents(latents, config.height, config.width)

18
src/mflux/exceptions.py Normal file
View File

@ -0,0 +1,18 @@
class MFluxException(Exception):
"""base class for all custom exceptions in mflux package."""
class ImageSavingException(MFluxException):
"""error ocurred while attempting to save image to storage."""
class MetadataEmbedException(MFluxException):
"""error ocurred while attempting to embed metadata in image"""
class MFluxUserException(MFluxException):
"""an exception raised by user behavior or intention."""
class StopImageGenerationException(MFluxUserException):
"""user has requested to stop a image generation in progress."""

View File

@ -1,10 +1,12 @@
import mlx.core as mx import mlx.core as mx
from pathlib import Path
from mlx import nn from mlx import nn
from tqdm import tqdm from tqdm import tqdm
from mflux.config.config import Config from mflux.config.config import Config
from mflux.config.model_config import ModelConfig from mflux.config.model_config import ModelConfig
from mflux.config.runtime_config import RuntimeConfig from mflux.config.runtime_config import RuntimeConfig
from mflux.exceptions import StopImageGenerationException
from mflux.models.text_encoder.clip_encoder.clip_encoder import CLIPEncoder from mflux.models.text_encoder.clip_encoder.clip_encoder import CLIPEncoder
from mflux.models.text_encoder.t5_encoder.t5_encoder import T5Encoder from mflux.models.text_encoder.t5_encoder.t5_encoder import T5Encoder
from mflux.models.transformer.transformer import Transformer from mflux.models.transformer.transformer import Transformer
@ -69,7 +71,7 @@ class Flux1:
if weights.quantization_level is not None: if weights.quantization_level is not None:
self._set_model_weights(weights) self._set_model_weights(weights)
def generate_image(self, seed: int, prompt: str, config: Config = Config()) -> GeneratedImage: def generate_image(self, seed: int, prompt: str, config: Config = Config(), stepwise_output_dir: Path = None, stepwise_composite_only=False) -> GeneratedImage: # fmt: off
# Create a new runtime config based on the model type and input parameters # Create a new runtime config based on the model type and input parameters
config = RuntimeConfig(config, self.model_config) config = RuntimeConfig(config, self.model_config)
time_steps = tqdm(range(config.num_inference_steps)) time_steps = tqdm(range(config.num_inference_steps))
@ -86,22 +88,54 @@ class Flux1:
prompt_embeds = self.t5_text_encoder.forward(t5_tokens) prompt_embeds = self.t5_text_encoder.forward(t5_tokens)
pooled_prompt_embeds = self.clip_text_encoder.forward(clip_tokens) pooled_prompt_embeds = self.clip_text_encoder.forward(clip_tokens)
step_wise_images = []
if stepwise_output_dir:
stepwise_output_dir.mkdir(parents=True, exist_ok=True)
for t in time_steps: for t in time_steps:
# 3.t Predict the noise try:
noise = self.transformer.predict( # 3.t Predict the noise
t=t, noise = self.transformer.predict(
prompt_embeds=prompt_embeds, t=t,
pooled_prompt_embeds=pooled_prompt_embeds, prompt_embeds=prompt_embeds,
hidden_states=latents, pooled_prompt_embeds=pooled_prompt_embeds,
config=config, hidden_states=latents,
) config=config,
)
# 4.t Take one denoise step # 4.t Take one denoise step
dt = config.sigmas[t + 1] - config.sigmas[t] dt = config.sigmas[t + 1] - config.sigmas[t]
latents += noise * dt latents += noise * dt
# Evaluate to enable progress tracking # Evaluate to enable progress tracking
mx.eval(latents) mx.eval(latents)
if stepwise_output_dir:
stepwise_decoded = self.vae.decode(Flux1._unpack_latents(latents, config.height, config.width))
# performance todo: Pillow mostly uses CPU,
# can try to improve image generation performance by offloading
# stepwise image processing to the CPU via threading
stepwise_img = ImageUtil.to_image(
decoded_latents=stepwise_decoded,
seed=seed,
prompt=prompt,
quantization=self.bits,
generation_time=time_steps.format_dict["elapsed"],
lora_paths=self.lora_paths,
lora_scales=self.lora_scales,
config=config,
)
step_wise_images.append(stepwise_img)
if not stepwise_composite_only:
stepwise_img.save(
path=stepwise_output_dir / f"seed_{seed}_step{t+1}of{len(time_steps)}.png",
export_json_metadata=False,
)
except KeyboardInterrupt: # noqa: PERF203
raise StopImageGenerationException(f"Stopping image generation at step {t+1}/{len(time_steps)}")
finally:
if step_wise_images and stepwise_output_dir:
composite_img = ImageUtil.to_composite_image(step_wise_images)
composite_img.save(stepwise_output_dir / f"seed_{seed}_composite.png")
# 5. Decode the latent array and return the image # 5. Decode the latent array and return the image
latents = Flux1._unpack_latents(latents, config.height, config.width) latents = Flux1._unpack_latents(latents, config.height, config.width)

View File

@ -1,7 +1,8 @@
import argparse import argparse
import time import time
from pathlib import Path
from mflux import Flux1, Config, ModelConfig from mflux import Flux1, Config, ModelConfig, StopImageGenerationException
def main(): def main():
@ -14,6 +15,7 @@ def main():
parser.add_argument("--height", type=int, default=1024, help="Image height (Default is 1024)") parser.add_argument("--height", type=int, default=1024, help="Image height (Default is 1024)")
parser.add_argument("--width", type=int, default=1024, help="Image width (Default is 1024)") parser.add_argument("--width", type=int, default=1024, help="Image width (Default is 1024)")
parser.add_argument("--steps", type=int, default=None, help="Inference Steps") parser.add_argument("--steps", type=int, default=None, help="Inference Steps")
parser.add_argument('--stepwise-image-output-dir', type=str, default=None, help='Output dir to write step-wise images and their final composite image to.')
parser.add_argument("--guidance", type=float, default=3.5, help="Guidance Scale (Default is 3.5)") parser.add_argument("--guidance", type=float, default=3.5, help="Guidance Scale (Default is 3.5)")
parser.add_argument("--quantize", "-q", type=int, choices=[4, 8], default=None, help="Quantize the model (4 or 8, Default is None)") parser.add_argument("--quantize", "-q", type=int, choices=[4, 8], default=None, help="Quantize the model (4 or 8, Default is None)")
parser.add_argument("--path", type=str, default=None, help="Local path for loading a model from disk") parser.add_argument("--path", type=str, default=None, help="Local path for loading a model from disk")
@ -39,20 +41,24 @@ def main():
lora_scales=args.lora_scales, lora_scales=args.lora_scales,
) )
# Generate an image try:
image = flux.generate_image( # Generate an image
seed=int(time.time()) if args.seed is None else args.seed, image = flux.generate_image(
prompt=args.prompt, seed=int(time.time()) if args.seed is None else args.seed,
config=Config( prompt=args.prompt,
num_inference_steps=args.steps, config=Config(
height=args.height, num_inference_steps=args.steps,
width=args.width, height=args.height,
guidance=args.guidance, width=args.width,
), guidance=args.guidance,
) ),
stepwise_output_dir=Path(args.stepwise_image_output_dir) if args.stepwise_image_output_dir else None,
)
# Save the image # Save the image
image.save(path=args.output, export_json_metadata=args.metadata) image.save(path=args.output, export_json_metadata=args.metadata)
except StopImageGenerationException as stop_exc:
print(stop_exc)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,7 +1,8 @@
import argparse import argparse
import time import time
from pathlib import Path
from mflux import Flux1Controlnet, ConfigControlnet, ModelConfig from mflux import Flux1Controlnet, ConfigControlnet, ModelConfig, StopImageGenerationException
def main(): def main():
@ -17,6 +18,7 @@ def main():
parser.add_argument("--height", type=int, default=1024, help="Image height (Default is 1024)") parser.add_argument("--height", type=int, default=1024, help="Image height (Default is 1024)")
parser.add_argument("--width", type=int, default=1024, help="Image width (Default is 1024)") parser.add_argument("--width", type=int, default=1024, help="Image width (Default is 1024)")
parser.add_argument("--steps", type=int, default=None, help="Inference Steps") parser.add_argument("--steps", type=int, default=None, help="Inference Steps")
parser.add_argument('--stepwise-image-output-dir', type=str, default=None, help='Output dir to write step-wise images and their final composite image to.')
parser.add_argument("--guidance", type=float, default=3.5, help="Guidance Scale (Default is 3.5)") parser.add_argument("--guidance", type=float, default=3.5, help="Guidance Scale (Default is 3.5)")
parser.add_argument("--quantize", "-q", type=int, choices=[4, 8], default=None, help="Quantize the model (4 or 8, Default is None)") parser.add_argument("--quantize", "-q", type=int, choices=[4, 8], default=None, help="Quantize the model (4 or 8, Default is None)")
parser.add_argument("--path", type=str, default=None, help="Local path for loading a model from disk") parser.add_argument("--path", type=str, default=None, help="Local path for loading a model from disk")
@ -42,24 +44,28 @@ def main():
lora_scales=args.lora_scales, lora_scales=args.lora_scales,
) )
# Generate an image try:
image = flux.generate_image( # Generate an image
seed=int(time.time()) if args.seed is None else args.seed, image = flux.generate_image(
prompt=args.prompt, seed=int(time.time()) if args.seed is None else args.seed,
output=args.output, prompt=args.prompt,
controlnet_image_path=args.controlnet_image_path, output=args.output,
controlnet_save_canny=args.controlnet_save_canny, controlnet_image_path=args.controlnet_image_path,
config=ConfigControlnet( controlnet_save_canny=args.controlnet_save_canny,
num_inference_steps=args.steps, config=ConfigControlnet(
height=args.height, num_inference_steps=args.steps,
width=args.width, height=args.height,
guidance=args.guidance, width=args.width,
controlnet_strength=args.controlnet_strength, guidance=args.guidance,
), controlnet_strength=args.controlnet_strength,
) ),
stepwise_output_dir=Path(args.stepwise_image_output_dir) if args.stepwise_image_output_dir else None,
)
# Save the image # Save the image
image.save(path=args.output, export_json_metadata=args.metadata) image.save(path=args.output, export_json_metadata=args.metadata)
except StopImageGenerationException as stop_exc:
print(stop_exc)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,7 +1,7 @@
import typing as t
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
import PIL import PIL
import PIL.Image import PIL.Image
import mlx.core as mx import mlx.core as mx
@ -47,6 +47,17 @@ class ImageUtil:
controlnet_strength=config.controlnet_strength if isinstance(config.config, ConfigControlnet) else None, controlnet_strength=config.controlnet_strength if isinstance(config.config, ConfigControlnet) else None,
) )
def to_composite_image(generated_images: t.List[GeneratedImage]) -> Image:
# stitch horizontally
total_width = sum(gen_img.image.width for gen_img in generated_images)
max_height = max(gen_img.image.height for gen_img in generated_images)
composite_img = Image.new("RGB", (total_width, max_height))
current_x = 0
for index, gen_img in enumerate(generated_images):
composite_img.paste(gen_img.image, (current_x, 0))
current_x += gen_img.image.width
return composite_img
@staticmethod @staticmethod
def _denormalize(images: mx.array) -> mx.array: def _denormalize(images: mx.array) -> mx.array:
return mx.clip((images / 2 + 0.5), 0, 1) return mx.clip((images / 2 + 0.5), 0, 1)
@ -119,7 +130,7 @@ class ImageUtil:
if metadata is not None: if metadata is not None:
ImageUtil._embed_metadata(metadata, file_path) ImageUtil._embed_metadata(metadata, file_path)
log.info(f"Metadata embedded successfully at: {file_path}") log.info(f"Metadata embedded successfully at: {file_path}")
except Exception as e: except Exception as e: # noqa: BLE001
log.error(f"Error saving image: {e}") log.error(f"Error saving image: {e}")
@staticmethod @staticmethod
@ -145,5 +156,5 @@ class ImageUtil:
# Save the image with metadata # Save the image with metadata
image.save(path, exif=exif_bytes) image.save(path, exif=exif_bytes)
except Exception as e: except Exception as e: # noqa: BLE001
log.error(f"Error embedding metadata: {e}") log.error(f"Error embedding metadata: {e}")

View File

@ -1,7 +1,39 @@
#!/bin/zsh -e
# ^ safe to assume Mac devs have zsh installed
# default since Catalina in 2019
mkdir -p /tmp/mflux-test
mflux-generate \ mflux-generate \
--prompt "Luxury food photograph" \ --prompt "Luxury food photograph" \
--model schnell \ --model schnell \
--steps 2 \ --steps 2 \
--seed 2 \ --seed 2 \
--height 1024 \ --height 512 \
--width 1024 --width 512 \
--output /tmp/mflux-test/luxury_food.png
# generate an image of a blue bird, then use it as input for the following test
mflux-generate \
--prompt "blue bird, morning, spring" \
--model schnell \
--steps 2 \
--seed 24 \
--height 512 \
--width 512 \
--stepwise-image-output-dir /tmp/mflux-test \
--output /tmp/mflux-test/sf_blue_bird.png
# use the image from the prior test, generate an image with similar visual structure
mflux-generate-controlnet \
--prompt "yellow bird, afternoon, snowy mountain" \
--model schnell \
--controlnet-image-path /tmp/mflux-test/sf_blue_bird.png \
--controlnet-strength 0.7 \
--controlnet-save-canny \
--steps 2 \
--seed 42 \
--height 512 \
--width 512 \
--output /tmp/mflux-test/controlnet_sf_yellow_bird.png \
--stepwise-image-output-dir /tmp/mflux-test