mirror of
https://github.com/hanzoai/Hanzo-MLX.git
synced 2026-10-30 18:51:00 +00:00
786 lines
27 KiB
Python
786 lines
27 KiB
Python
# reference: https://github.com/ml-explore/mlx-examples/tree/main/stable_diffusion
|
|
|
|
#
|
|
# For licensing see accompanying LICENSE.md file.
|
|
# Copyright (C) 2024 Argmax, Inc. All Rights Reserved.
|
|
#
|
|
|
|
import gc
|
|
import math
|
|
import time
|
|
from pprint import pprint
|
|
from typing import Optional, Tuple
|
|
|
|
import mlx.core as mx
|
|
import mlx.nn as nn
|
|
import numpy as np
|
|
from argmaxtools.test_utils import AppleSiliconContextMixin, InferenceContextSpec
|
|
from argmaxtools.utils import get_logger
|
|
from ...diffusionkit.utils import bytes2gigabytes
|
|
from PIL import Image
|
|
|
|
from .model_io import (
|
|
_DEFAULT_MODEL,
|
|
load_flux,
|
|
load_mmdit,
|
|
load_t5_encoder,
|
|
load_t5_tokenizer,
|
|
load_text_encoder,
|
|
load_tokenizer,
|
|
load_vae_decoder,
|
|
load_vae_encoder,
|
|
)
|
|
from .sampler import FluxSampler, ModelSamplingDiscreteFlow
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
MMDIT_CKPT = {
|
|
"argmaxinc/mlx-stable-diffusion-3-medium": "argmaxinc/mlx-stable-diffusion-3-medium",
|
|
"sd3-8b-unreleased": "models/sd3_8b_beta.safetensors", # unreleased
|
|
"argmaxinc/mlx-FLUX.1-schnell": "argmaxinc/mlx-FLUX.1-schnell",
|
|
"argmaxinc/mlx-FLUX.1-schnell-4bit-quantized": "argmaxinc/mlx-FLUX.1-schnell-4bit-quantized",
|
|
"argmaxinc/mlx-FLUX.1-dev": "argmaxinc/mlx-FLUX.1-dev",
|
|
}
|
|
|
|
T5_MAX_LENGTH = {
|
|
"argmaxinc/mlx-stable-diffusion-3-medium": 512,
|
|
"argmaxinc/mlx-FLUX.1-schnell": 256,
|
|
"argmaxinc/mlx-FLUX.1-schnell-4bit-quantized": 256,
|
|
"argmaxinc/mlx-FLUX.1-dev": 512,
|
|
}
|
|
|
|
|
|
class DiffusionKitInferenceContext(AppleSiliconContextMixin, InferenceContextSpec):
|
|
def code_spec(self):
|
|
return {}
|
|
|
|
def model_spec(self):
|
|
return {}
|
|
|
|
|
|
class DiffusionPipeline:
|
|
def __init__(
|
|
self,
|
|
w16: bool = False,
|
|
shift: float = 1.0,
|
|
use_t5: bool = True,
|
|
model_version: str = "argmaxinc/mlx-stable-diffusion-3-medium",
|
|
low_memory_mode: bool = True,
|
|
a16: bool = False,
|
|
local_ckpt=None,
|
|
):
|
|
model_io.LOCAl_SD3_CKPT = local_ckpt
|
|
self.float16_dtype = mx.float16
|
|
model_io._FLOAT16 = self.float16_dtype
|
|
self.dtype = self.float16_dtype if w16 else mx.float32
|
|
self.activation_dtype = self.float16_dtype if a16 else mx.float32
|
|
self.use_t5 = use_t5
|
|
self.mmdit_ckpt = MMDIT_CKPT[model_version]
|
|
self.low_memory_mode = low_memory_mode
|
|
self.model = _DEFAULT_MODEL
|
|
self.model_version = model_version
|
|
self.sampler = ModelSamplingDiscreteFlow(shift=shift)
|
|
self.latent_format = SD3LatentFormat()
|
|
self.use_clip_g = True
|
|
self.check_and_load_models()
|
|
|
|
def load_mmdit(self, only_modulation_dict=False):
|
|
if only_modulation_dict:
|
|
return load_mmdit(
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
key=self.mmdit_ckpt,
|
|
model_key=self.model_version,
|
|
low_memory_mode=self.low_memory_mode,
|
|
only_modulation_dict=only_modulation_dict,
|
|
)
|
|
self.mmdit = load_mmdit(
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
key=self.mmdit_ckpt,
|
|
model_key=self.model_version,
|
|
low_memory_mode=self.low_memory_mode,
|
|
only_modulation_dict=only_modulation_dict,
|
|
)
|
|
|
|
def check_and_load_models(self):
|
|
if not hasattr(self, "mmdit"):
|
|
self.load_mmdit()
|
|
if not hasattr(self, "decoder"):
|
|
self.decoder = load_vae_decoder(
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
key=self.mmdit_ckpt,
|
|
)
|
|
if not hasattr(self, "encoder"):
|
|
self.encoder = load_vae_encoder(float16=False, key=self.mmdit_ckpt)
|
|
|
|
if not hasattr(self, "clip_l"):
|
|
self.clip_l = load_text_encoder(
|
|
self.model,
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
model_key="clip_l",
|
|
)
|
|
self.tokenizer_l = load_tokenizer(
|
|
self.model,
|
|
merges_key="tokenizer_l_merges",
|
|
vocab_key="tokenizer_l_vocab",
|
|
pad_with_eos=True,
|
|
)
|
|
if self.use_clip_g and not hasattr(self, "clip_g"):
|
|
self.clip_g = load_text_encoder(
|
|
self.model,
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
model_key="clip_g",
|
|
)
|
|
self.tokenizer_g = load_tokenizer(
|
|
self.model,
|
|
merges_key="tokenizer_g_merges",
|
|
vocab_key="tokenizer_g_vocab",
|
|
pad_with_eos=False,
|
|
)
|
|
if self.use_t5 and not hasattr(self, "t5_encoder"):
|
|
self.set_up_t5()
|
|
|
|
def set_up_t5(self):
|
|
if not hasattr(self, "t5_encoder") or self.t5_encoder is None:
|
|
self.t5_encoder = load_t5_encoder(
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
low_memory_mode=self.low_memory_mode,
|
|
)
|
|
if not hasattr(self, "t5_tokenizer") or self.t5_tokenizer is None:
|
|
self.t5_tokenizer = load_t5_tokenizer(
|
|
max_context_length=T5_MAX_LENGTH[self.model_version]
|
|
)
|
|
self.use_t5 = True
|
|
|
|
def unload_t5(self):
|
|
if self.t5_encoder is not None:
|
|
del self.t5_encoder
|
|
self.t5_encoder = None
|
|
if self.t5_tokenizer is not None:
|
|
del self.t5_tokenizer
|
|
self.t5_tokenizer = None
|
|
gc.collect()
|
|
self.use_t5 = False
|
|
|
|
def ensure_models_are_loaded(self):
|
|
mx.eval(self.mmdit.parameters())
|
|
mx.eval(self.clip_l.parameters())
|
|
mx.eval(self.decoder.parameters())
|
|
if hasattr(self, "clip_g"):
|
|
mx.eval(self.clip_g.parameters())
|
|
if hasattr(self, "t5_encoder") and self.use_t5:
|
|
mx.eval(self.t5_encoder.parameters())
|
|
|
|
def _tokenize(self, tokenizer, text: str, negative_text: Optional[str] = None):
|
|
if negative_text is None:
|
|
negative_text = ""
|
|
if tokenizer.pad_with_eos:
|
|
pad_token = tokenizer.eos_token
|
|
else:
|
|
pad_token = 0
|
|
|
|
# Tokenize the text
|
|
tokens = [tokenizer.tokenize(text)]
|
|
if tokenizer.pad_to_max_length:
|
|
tokens[0].extend([pad_token] * (tokenizer.max_length - len(tokens[0])))
|
|
if negative_text is not None:
|
|
tokens += [tokenizer.tokenize(negative_text)]
|
|
lengths = [len(t) for t in tokens]
|
|
N = max(lengths)
|
|
tokens = [t + [pad_token] * (N - len(t)) for t in tokens]
|
|
tokens = mx.array(tokens)
|
|
|
|
return tokens
|
|
|
|
def encode_text(
|
|
self,
|
|
text: str,
|
|
cfg_weight: float = 7.5,
|
|
negative_text: str = "",
|
|
):
|
|
tokens_l = self._tokenize(
|
|
self.tokenizer_l,
|
|
text,
|
|
(negative_text if cfg_weight > 1 else None),
|
|
)
|
|
tokens_g = self._tokenize(
|
|
self.tokenizer_g,
|
|
text,
|
|
(negative_text if cfg_weight > 1 else None),
|
|
)
|
|
|
|
conditioning_l = self.clip_l(tokens_l)
|
|
conditioning_g = self.clip_g(tokens_g)
|
|
conditioning = mx.concatenate(
|
|
[conditioning_l.hidden_states[-2], conditioning_g.hidden_states[-2]],
|
|
axis=-1,
|
|
)
|
|
pooled_conditioning = mx.concatenate(
|
|
[conditioning_l.pooled_output, conditioning_g.pooled_output],
|
|
axis=-1,
|
|
)
|
|
|
|
conditioning = mx.concatenate(
|
|
[
|
|
conditioning,
|
|
mx.zeros(
|
|
(
|
|
conditioning.shape[0],
|
|
conditioning.shape[1],
|
|
4096 - conditioning.shape[2],
|
|
)
|
|
),
|
|
],
|
|
axis=-1,
|
|
)
|
|
|
|
if self.use_t5:
|
|
tokens_t5 = self._tokenize(
|
|
self.t5_tokenizer,
|
|
text,
|
|
(negative_text if cfg_weight > 1 else None),
|
|
)
|
|
t5_conditioning = self.t5_encoder(tokens_t5)
|
|
mx.eval(t5_conditioning)
|
|
else:
|
|
t5_conditioning = mx.zeros_like(conditioning)
|
|
conditioning = mx.concatenate([conditioning, t5_conditioning], axis=1)
|
|
|
|
return conditioning, pooled_conditioning
|
|
|
|
def denoise_latents(
|
|
self,
|
|
conditioning,
|
|
pooled_conditioning,
|
|
num_steps: int = 2,
|
|
cfg_weight: float = 0.0,
|
|
latent_size: Tuple[int] = (64, 64),
|
|
seed=None,
|
|
image_path: Optional[str] = None,
|
|
denoise: float = 1.0,
|
|
):
|
|
# Set the PRNG state
|
|
seed = int(time.time()) if seed is None else seed
|
|
logger.info(f"Seed: {seed}")
|
|
mx.random.seed(seed)
|
|
|
|
x_T = self.get_empty_latent(*latent_size)
|
|
if image_path is None:
|
|
denoise = 1.0
|
|
else:
|
|
x_T = self.encode_image_to_latents(image_path, seed=seed)
|
|
x_T = self.latent_format.process_in(x_T)
|
|
noise = self.get_noise(seed, x_T)
|
|
sigmas = self.get_sigmas(self.sampler, num_steps)
|
|
sigmas = sigmas[int(num_steps * (1 - denoise)) :]
|
|
extra_args = {
|
|
"conditioning": conditioning,
|
|
"cfg_weight": cfg_weight,
|
|
"pooled_conditioning": pooled_conditioning,
|
|
}
|
|
noise_scaled = self.sampler.noise_scaling(
|
|
sigmas[0], noise, x_T, self.max_denoise(sigmas)
|
|
)
|
|
latent, iter_time = sample_euler(
|
|
CFGDenoiser(self), noise_scaled, sigmas, extra_args=extra_args
|
|
)
|
|
|
|
latent = self.latent_format.process_out(latent)
|
|
|
|
return latent, iter_time
|
|
|
|
def generate_image(
|
|
self,
|
|
text: str,
|
|
num_steps: int = 2,
|
|
cfg_weight: float = 0.0,
|
|
negative_text: str = "",
|
|
latent_size: Tuple[int] = (64, 64),
|
|
seed=None,
|
|
verbose: bool = True,
|
|
image_path: Optional[str] = None,
|
|
denoise: float = 1.0,
|
|
):
|
|
# Check latent size is divisible by 2
|
|
assert (
|
|
latent_size[0] % 2 == 0
|
|
), f"Height must be divisible by 16 ({latent_size[0]*8}/16={latent_size[0]/2})"
|
|
assert (
|
|
latent_size[1] % 2 == 0
|
|
), f"Width must be divisible by 16 ({latent_size[1]*8}/16={latent_size[1]/2})"
|
|
self.check_and_load_models()
|
|
# Start timing
|
|
start_time = time.time()
|
|
|
|
# Initialize the memory log
|
|
log = {
|
|
"text_encoding": {
|
|
"pre": {
|
|
"peak_memory": round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
),
|
|
"active_memory": round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
),
|
|
},
|
|
"post": {"peak_memory": None, "active_memory": None},
|
|
},
|
|
"denoising": {
|
|
"pre": {"peak_memory": None, "active_memory": None},
|
|
"post": {"peak_memory": None, "active_memory": None},
|
|
},
|
|
"decoding": {
|
|
"pre": {"peak_memory": None, "active_memory": None},
|
|
"post": {"peak_memory": None, "active_memory": None},
|
|
},
|
|
"peak_memory": 0.0,
|
|
}
|
|
|
|
# Get the text conditioning
|
|
text_encoding_start_time = time.time()
|
|
if verbose:
|
|
logger.info(
|
|
f"Pre text encoding peak memory: {log['text_encoding']['pre']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Pre text encoding active memory: {log['text_encoding']['pre']['active_memory']}GB"
|
|
)
|
|
|
|
# FIXME(arda): Need the same for CLIP models (low memory mode will not succeed a second time otherwise)
|
|
if not hasattr(self, "t5"):
|
|
self.set_up_t5()
|
|
|
|
conditioning, pooled_conditioning = self.encode_text(
|
|
text, cfg_weight, negative_text
|
|
)
|
|
mx.eval(conditioning)
|
|
mx.eval(pooled_conditioning)
|
|
log["text_encoding"]["post"]["peak_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
)
|
|
log["text_encoding"]["post"]["active_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
)
|
|
log["peak_memory"] = max(
|
|
log["peak_memory"], log["text_encoding"]["post"]["peak_memory"]
|
|
)
|
|
log["text_encoding"]["time"] = round(time.time() - text_encoding_start_time, 3)
|
|
if verbose:
|
|
logger.info(
|
|
f"Post text encoding peak memory: {log['text_encoding']['post']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Post text encoding active memory: {log['text_encoding']['post']['active_memory']}GB"
|
|
)
|
|
logger.info(f"Text encoding time: {log['text_encoding']['time']}s")
|
|
|
|
# unload T5 and CLIP models after obtaining conditioning in low memory mode
|
|
if self.low_memory_mode:
|
|
if hasattr(self, "t5_encoder"):
|
|
del self.t5_encoder
|
|
if hasattr(self, "clip_g"):
|
|
del self.clip_g
|
|
del self.clip_l
|
|
gc.collect()
|
|
|
|
logger.debug(f"Conditioning dtype before casting: {conditioning.dtype}")
|
|
logger.debug(
|
|
f"Pooled Conditioning dtype before casting: {pooled_conditioning.dtype}"
|
|
)
|
|
conditioning = conditioning.astype(self.activation_dtype)
|
|
pooled_conditioning = pooled_conditioning.astype(self.activation_dtype)
|
|
logger.debug(f"Conditioning dtype after casting: {conditioning.dtype}")
|
|
logger.debug(
|
|
f"Pooled Conditioning dtype after casting: {pooled_conditioning.dtype}"
|
|
)
|
|
|
|
# Reset peak memory info
|
|
mx.metal.reset_peak_memory()
|
|
|
|
# Generate the latents
|
|
denoising_start_time = time.time()
|
|
log["denoising"]["pre"]["peak_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
)
|
|
log["denoising"]["pre"]["active_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
)
|
|
log["peak_memory"] = max(
|
|
log["peak_memory"], log["denoising"]["pre"]["peak_memory"]
|
|
)
|
|
if verbose:
|
|
logger.info(
|
|
f"Pre denoise peak memory: {log['denoising']['pre']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Pre denoise active memory: {log['denoising']['pre']['active_memory']}GB"
|
|
)
|
|
|
|
latents, iter_time = self.denoise_latents(
|
|
conditioning,
|
|
pooled_conditioning,
|
|
num_steps=num_steps,
|
|
cfg_weight=cfg_weight,
|
|
latent_size=latent_size,
|
|
seed=seed,
|
|
image_path=image_path,
|
|
denoise=denoise,
|
|
)
|
|
mx.eval(latents)
|
|
|
|
log["denoising"]["post"]["peak_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
)
|
|
log["denoising"]["post"]["active_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
)
|
|
log["peak_memory"] = max(
|
|
log["peak_memory"], log["denoising"]["post"]["peak_memory"]
|
|
)
|
|
log["denoising"]["time"] = round(time.time() - denoising_start_time, 3)
|
|
log["denoising"]["iter_time"] = iter_time
|
|
if verbose:
|
|
logger.info(
|
|
f"Post denoise peak memory: {log['denoising']['post']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Post denoise active memory: {log['denoising']['post']['active_memory']}GB"
|
|
)
|
|
logger.info(f"Denoising time: {log['denoising']['time']}s")
|
|
|
|
# unload MMDIT model after obtaining latents in low memory mode
|
|
if self.low_memory_mode:
|
|
del self.mmdit
|
|
gc.collect()
|
|
|
|
logger.debug(f"Latents dtype before casting: {latents.dtype}")
|
|
latents = latents.astype(self.activation_dtype)
|
|
logger.debug(f"Latents dtype after casting: {latents.dtype}")
|
|
|
|
# Reset peak memory info
|
|
mx.metal.reset_peak_memory()
|
|
|
|
# Decode the latents
|
|
decoding_start_time = time.time()
|
|
log["decoding"]["pre"]["peak_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
)
|
|
log["decoding"]["pre"]["active_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
)
|
|
log["peak_memory"] = max(
|
|
log["peak_memory"], log["decoding"]["pre"]["peak_memory"]
|
|
)
|
|
if verbose:
|
|
logger.info(
|
|
f"Pre decode peak memory: {log['decoding']['pre']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Pre decode active memory: {log['decoding']['pre']['active_memory']}GB"
|
|
)
|
|
latents = latents.astype(self.activation_dtype)
|
|
decoded = self.decode_latents_to_image(latents)
|
|
mx.eval(decoded)
|
|
|
|
log["decoding"]["post"]["peak_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_peak_memory()), 3
|
|
)
|
|
log["decoding"]["post"]["active_memory"] = round(
|
|
bytes2gigabytes(mx.metal.get_active_memory()), 3
|
|
)
|
|
log["peak_memory"] = max(
|
|
log["peak_memory"], log["decoding"]["post"]["peak_memory"]
|
|
)
|
|
log["decoding"]["time"] = round(time.time() - decoding_start_time, 3)
|
|
if verbose:
|
|
logger.info(
|
|
f"Post decode peak memory: {log['decoding']['post']['peak_memory']}GB"
|
|
)
|
|
logger.info(
|
|
f"Post decode active memory: {log['decoding']['post']['active_memory']}GB"
|
|
)
|
|
|
|
if verbose:
|
|
logger.info("============= Summary =============")
|
|
logger.info(f"Text encoder: {log['text_encoding']['time']:.1f}s")
|
|
logger.info(f"Denoising: {log['denoising']['time']:.1f}s")
|
|
logger.info(f"Image decoder: {log['decoding']['time']:.1f}s")
|
|
logger.info(f"Peak memory: {log['peak_memory']:.1f}GB")
|
|
|
|
logger.info("============= Inference Context =============")
|
|
ic = DiffusionKitInferenceContext()
|
|
logger.info("Operating System:")
|
|
pprint(ic.os_spec())
|
|
logger.info("Device:")
|
|
pprint(ic.device_spec())
|
|
|
|
# unload VAE Decoder model after decoding in low memory mode
|
|
if self.low_memory_mode:
|
|
del self.decoder
|
|
gc.collect()
|
|
|
|
# Convert the decoded images to uint8
|
|
x = mx.concatenate(decoded, axis=0)
|
|
x = (x * 255).astype(mx.uint8)
|
|
|
|
# End timing
|
|
end_time = time.time()
|
|
log["total_time"] = round(end_time - start_time, 3)
|
|
if verbose:
|
|
logger.info(f"Total time: {log['total_time']}s")
|
|
|
|
return Image.fromarray(np.array(x)), log
|
|
|
|
def read_image(self, image_path: str):
|
|
# Read the image
|
|
img = Image.open(image_path)
|
|
|
|
# Make sure image shape is divisible by 64
|
|
W, H = (dim - dim % 64 for dim in (img.width, img.height))
|
|
if W != img.width or H != img.height:
|
|
logger.warning(
|
|
f"Warning: image shape is not divisible by 64, downsampling to {W}x{H}"
|
|
)
|
|
img = img.resize((W, H), Image.LANCZOS) # use desired downsampling filter
|
|
|
|
img = mx.array(np.array(img))
|
|
img = (img[:, :, :3].astype(mx.float32) / 255) * 2 - 1.0
|
|
|
|
return mx.expand_dims(img, axis=0)
|
|
|
|
def get_noise(self, seed, x_T):
|
|
np.random.seed(seed)
|
|
noise = np.random.randn(*x_T.transpose(0, 3, 1, 2).shape)
|
|
noise = mx.array(noise).transpose(0, 2, 3, 1)
|
|
return noise
|
|
|
|
def get_sigmas(self, sampler, num_steps: int):
|
|
start = sampler.timestep(sampler.sigma_max).item()
|
|
end = sampler.timestep(sampler.sigma_min).item()
|
|
if isinstance(sampler, FluxSampler):
|
|
num_steps += 1
|
|
timesteps = mx.linspace(start, end, num_steps)
|
|
sigs = []
|
|
for x in range(len(timesteps)):
|
|
ts = timesteps[x]
|
|
sigs.append(sampler.sigma(ts))
|
|
if not isinstance(sampler, FluxSampler):
|
|
sigs += [0.0]
|
|
return mx.array(sigs)
|
|
|
|
def get_empty_latent(self, *shape):
|
|
return mx.ones([1, *shape, 16]) * 0.0609
|
|
|
|
def max_denoise(self, sigmas):
|
|
max_sigma = float(self.sampler.sigma_max.item())
|
|
sigma = float(sigmas[0].item())
|
|
return math.isclose(max_sigma, sigma, rel_tol=1e-05) or sigma > max_sigma
|
|
|
|
def decode_latents_to_image(self, x_t):
|
|
x = self.decoder(x_t)
|
|
x = mx.clip(x / 2 + 0.5, 0, 1)
|
|
return x
|
|
|
|
def encode_image_to_latents(self, image_path: str, seed):
|
|
image = self.read_image(image_path)
|
|
hidden = self.encoder(image)
|
|
mean, logvar = hidden.split(2, axis=-1)
|
|
logvar = mx.clip(logvar, -30.0, 20.0)
|
|
std = mx.exp(0.5 * logvar)
|
|
noise = self.get_noise(seed, mean)
|
|
|
|
return mean + std * noise
|
|
|
|
|
|
class FluxPipeline(DiffusionPipeline):
|
|
def __init__(
|
|
self,
|
|
w16: bool = False,
|
|
shift: float = 1.0,
|
|
use_t5: bool = True,
|
|
model_version: str = "argmaxinc/mlx-FLUX.1-schnell",
|
|
low_memory_mode: bool = True,
|
|
a16: bool = False,
|
|
local_ckpt=None,
|
|
quantize_mmdit: bool = False,
|
|
):
|
|
model_io.LOCAl_SD3_CKPT = local_ckpt
|
|
self.float16_dtype = mx.bfloat16
|
|
model_io._FLOAT16 = self.float16_dtype
|
|
self.dtype = self.float16_dtype if w16 else mx.float32
|
|
self.activation_dtype = self.float16_dtype if a16 else mx.float32
|
|
self.mmdit_ckpt = MMDIT_CKPT[model_version]
|
|
self.low_memory_mode = low_memory_mode
|
|
self.model = _DEFAULT_MODEL
|
|
self.model_version = model_version
|
|
self.sampler = FluxSampler(shift=shift)
|
|
self.latent_format = FluxLatentFormat()
|
|
self.use_t5 = True
|
|
self.use_clip_g = False
|
|
self.quantize_mmdit = quantize_mmdit
|
|
self.check_and_load_models()
|
|
|
|
def load_mmdit(self, only_modulation_dict=False):
|
|
if only_modulation_dict:
|
|
return load_flux(
|
|
key=self.mmdit_ckpt,
|
|
model_key=self.model_version,
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
low_memory_mode=self.low_memory_mode,
|
|
only_modulation_dict=only_modulation_dict,
|
|
)
|
|
self.mmdit = load_flux(
|
|
key=self.mmdit_ckpt,
|
|
model_key=self.model_version,
|
|
float16=True if self.dtype == self.float16_dtype else False,
|
|
low_memory_mode=self.low_memory_mode,
|
|
only_modulation_dict=only_modulation_dict,
|
|
)
|
|
|
|
def encode_text(
|
|
self,
|
|
text: str,
|
|
cfg_weight: float = 7.5,
|
|
negative_text: str = "",
|
|
):
|
|
tokens_l = self._tokenize(
|
|
self.tokenizer_l,
|
|
text,
|
|
(negative_text if cfg_weight > 1 else None),
|
|
)
|
|
conditioning_l = self.clip_l(tokens_l[[0], :]) # Ignore negative text
|
|
pooled_conditioning = conditioning_l.pooled_output
|
|
|
|
tokens_t5 = self._tokenize(
|
|
self.t5_tokenizer,
|
|
text,
|
|
(negative_text if cfg_weight > 1 else None),
|
|
)
|
|
padded_tokens_t5 = mx.zeros((1, T5_MAX_LENGTH[self.model_version])).astype(
|
|
tokens_t5.dtype
|
|
)
|
|
padded_tokens_t5[:, : tokens_t5.shape[1]] = tokens_t5[
|
|
[0], :
|
|
] # Ignore negative text
|
|
t5_conditioning = self.t5_encoder(padded_tokens_t5)
|
|
mx.eval(t5_conditioning)
|
|
conditioning = t5_conditioning
|
|
|
|
return conditioning, pooled_conditioning
|
|
|
|
|
|
class CFGDenoiser(nn.Module):
|
|
"""Helper for applying CFG Scaling to diffusion outputs"""
|
|
|
|
def __init__(self, model: DiffusionPipeline):
|
|
super().__init__()
|
|
self.model = model
|
|
|
|
def cache_modulation_params(self, pooled_text_embeddings, sigmas):
|
|
self.model.mmdit.cache_modulation_params(
|
|
pooled_text_embeddings, sigmas.astype(self.model.activation_dtype)
|
|
)
|
|
|
|
def clear_cache(self):
|
|
self.model.mmdit.load_weights(
|
|
self.model.load_mmdit(only_modulation_dict=True), strict=False
|
|
)
|
|
|
|
def __call__(
|
|
self,
|
|
x_t,
|
|
timestep,
|
|
sigma,
|
|
conditioning,
|
|
cfg_weight: float = 7.5,
|
|
pooled_conditioning=None,
|
|
):
|
|
if cfg_weight <= 0:
|
|
logger.debug("CFG Weight disabled")
|
|
x_t_mmdit = x_t.astype(self.model.activation_dtype)
|
|
else:
|
|
x_t_mmdit = mx.concatenate([x_t] * 2, axis=0).astype(
|
|
self.model.activation_dtype
|
|
)
|
|
mmdit_input = {
|
|
"latent_image_embeddings": x_t_mmdit,
|
|
"token_level_text_embeddings": mx.expand_dims(conditioning, 2),
|
|
"timestep": mx.broadcast_to(timestep, [len(x_t_mmdit)]),
|
|
}
|
|
|
|
mmdit_output = self.model.mmdit(**mmdit_input)
|
|
eps_pred = self.model.sampler.calculate_denoised(sigma, mmdit_output, x_t_mmdit)
|
|
if cfg_weight <= 0:
|
|
return eps_pred
|
|
else:
|
|
eps_text, eps_neg = eps_pred.split(2)
|
|
return eps_neg + cfg_weight * (eps_text - eps_neg)
|
|
|
|
|
|
class LatentFormat:
|
|
"""Base class for latent format conversion"""
|
|
|
|
def __init__(self):
|
|
self.scale_factor = 1.0
|
|
self.shift_factor = 0.0
|
|
|
|
def process_in(self, latent):
|
|
return (latent - self.shift_factor) * self.scale_factor
|
|
|
|
def process_out(self, latent):
|
|
return (latent / self.scale_factor) + self.shift_factor
|
|
|
|
|
|
class SD3LatentFormat(LatentFormat):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.scale_factor = 1.5305
|
|
self.shift_factor = 0.0609
|
|
|
|
|
|
class FluxLatentFormat(LatentFormat):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.scale_factor = 0.3611
|
|
self.shift_factor = 0.1159
|
|
|
|
|
|
def append_dims(x, target_dims):
|
|
"""Appends dimensions to the end of a tensor until it has target_dims dimensions."""
|
|
dims_to_append = target_dims - x.ndim
|
|
return x[(...,) + (None,) * dims_to_append]
|
|
|
|
|
|
def to_d(x, sigma, denoised):
|
|
"""Converts a denoiser output to a Karras ODE derivative."""
|
|
return (x - denoised) / append_dims(sigma, x.ndim)
|
|
|
|
|
|
def sample_euler(model: CFGDenoiser, x, sigmas, extra_args=None):
|
|
"""Implements Algorithm 2 (Euler steps) from Karras et al. (2022)."""
|
|
extra_args = {} if extra_args is None else extra_args
|
|
|
|
from tqdm import trange
|
|
|
|
t = trange(len(sigmas) - 1)
|
|
|
|
timesteps = model.model.sampler.timestep(sigmas).astype(
|
|
model.model.activation_dtype
|
|
)
|
|
model.cache_modulation_params(extra_args.pop("pooled_conditioning"), timesteps)
|
|
|
|
iter_time = []
|
|
for i in t:
|
|
start_time = t.format_dict["elapsed"]
|
|
denoised = model(x, timesteps[i], sigmas[i], **extra_args)
|
|
d = to_d(x, sigmas[i], denoised)
|
|
dt = sigmas[i + 1] - sigmas[i]
|
|
# Euler method
|
|
x = x + d * dt
|
|
mx.eval(x)
|
|
end_time = t.format_dict["elapsed"]
|
|
iter_time.append(round((end_time - start_time), 3))
|
|
|
|
model.clear_cache()
|
|
|
|
return x, iter_time
|