Flux2-Klein-9B-Loras / optimization.py
9ine
Initial commit: Forked and cleaned from FLUX.2-Klein-LoRA-Studio
8595af2
Raw
History Blame Contribute Delete
9.83 kB
"""
Hugging Face Spaces / ZeroGPU AoTI helper for Qwen image pipelines.
Usage:
from optimization_optimized import optimize_pipeline_
optimize_pipeline_(
pipe,
image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))],
prompt="warmup prompt",
num_inference_steps=4,
true_cfg_scale=1.0,
height=1024,
width=1024,
)
Design goals:
- Keep the same in-place API as your original optimize_pipeline_.
- Avoid crashing the Space if AoTI export/compile fails.
- Only mark transformer dimensions as dynamic when the captured call actually contains them.
- Keep FP8 quantization optional, because it can change quality and may break export on some setups.
"""
from __future__ import annotations
import logging
from contextlib import nullcontext
from typing import Any, Callable, Mapping, Optional, ParamSpec
import torch
from torch.utils._pytree import tree_map
try:
import spaces
except Exception: # Local/dev environment without Hugging Face Spaces.
spaces = None # type: ignore[assignment]
try:
from torchao.quantization import Float8DynamicActivationFloat8WeightConfig, quantize_
except Exception:
Float8DynamicActivationFloat8WeightConfig = None # type: ignore[assignment]
quantize_ = None # type: ignore[assignment]
P = ParamSpec("P")
LOGGER = logging.getLogger(__name__)
# Qwen Image transformer forward usually has:
# hidden_states: [batch, image_seq, channels]
# encoder_hidden_states: [batch, text_seq, channels]
# encoder_hidden_states_mask: [batch, text_seq]
# image_rotary_emb: tuple(image_rotary_emb, text_rotary_emb)
TRANSFORMER_IMAGE_SEQ_LENGTH_DIM = torch.export.Dim("image_seq_length", min=1)
TRANSFORMER_TEXT_SEQ_LENGTH_DIM = torch.export.Dim("text_seq_length", min=1)
DEFAULT_TRANSFORMER_DYNAMIC_SHAPES: dict[str, Any] = {
"hidden_states": {1: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM},
"encoder_hidden_states": {1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
"encoder_hidden_states_mask": {1: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
"image_rotary_emb": (
{0: TRANSFORMER_IMAGE_SEQ_LENGTH_DIM},
{0: TRANSFORMER_TEXT_SEQ_LENGTH_DIM},
),
}
# Conservative-but-fast defaults for ZeroGPU/H200 AoTI.
# You can override any key through optimize_pipeline_(..., inductor_configs={...}).
DEFAULT_INDUCTOR_CONFIGS: dict[str, Any] = {
"conv_1x1_as_mm": True,
"epilogue_fusion": False,
"coordinate_descent_tuning": True,
"coordinate_descent_check_all_directions": True,
"max_autotune": True,
"triton.cudagraphs": True,
}
def _has_zero_gpu_aoti() -> bool:
return (
spaces is not None
and hasattr(spaces, "GPU")
and hasattr(spaces, "aoti_capture")
and hasattr(spaces, "aoti_compile")
and hasattr(spaces, "aoti_apply")
)
def _supports_float8() -> bool:
"""FP8 is mainly useful on Hopper-class GPUs such as H100/H200."""
if not torch.cuda.is_available():
return False
major, _minor = torch.cuda.get_device_capability()
return major >= 9
def _maybe_disable_progress_bar(pipeline: Callable[..., Any]) -> None:
"""Avoid tqdm/progress side effects during graph capture."""
setter = getattr(pipeline, "set_progress_bar_config", None)
if callable(setter):
try:
setter(disable=True)
except Exception:
pass
def _build_dynamic_shapes(
captured_kwargs: Mapping[str, Any],
shape_specs: Optional[Mapping[str, Any]] = None,
) -> dict[str, Any]:
"""
Build a dynamic_shapes dict matching the captured transformer kwargs.
torch.export expects the dynamic_shapes tree to mirror args/kwargs. The original
code unconditionally OR'ed every known Qwen key into the captured tree. This
version only injects a dynamic spec when that key is present in the actual
captured transformer call, which makes the helper more robust across Qwen /
Diffusers versions.
"""
dynamic_shapes = tree_map(lambda _leaf: None, dict(captured_kwargs))
specs = dict(shape_specs or DEFAULT_TRANSFORMER_DYNAMIC_SHAPES)
for name, spec in specs.items():
if name not in captured_kwargs:
continue
# image_rotary_emb is expected to be a pair. Skip if a future pipeline
# changes its structure instead of giving torch.export a mismatched tree.
if name == "image_rotary_emb":
value = captured_kwargs[name]
if not isinstance(value, (tuple, list)) or len(value) != 2:
LOGGER.warning(
"Skipping dynamic shape for image_rotary_emb: expected a 2-item tuple/list, got %s",
type(value).__name__,
)
continue
dynamic_shapes[name] = spec
return dynamic_shapes
def _maybe_quantize_transformer(transformer: torch.nn.Module, enable_float8: bool) -> None:
"""
Optional in-place FP8 quantization.
Keep disabled by default:
- It can affect image quality.
- It requires torchao.
- It is most beneficial on H100/H200-class GPUs.
- If export fails after quantization, the module has still been mutated.
"""
if not enable_float8:
return
if quantize_ is None or Float8DynamicActivationFloat8WeightConfig is None:
raise RuntimeError("enable_float8=True requires torchao to be installed.")
if not _supports_float8():
raise RuntimeError("enable_float8=True requires a Hopper-class CUDA GPU such as H100/H200.")
LOGGER.info("Applying experimental FP8 dynamic activation + FP8 weight quantization.")
quantize_(transformer, Float8DynamicActivationFloat8WeightConfig())
def optimize_pipeline_(
pipeline: Callable[P, Any],
*args: P.args,
duration: int = 1500,
inductor_configs: Optional[Mapping[str, Any]] = None,
dynamic_shape_specs: Optional[Mapping[str, Any]] = None,
enable_float8: bool = False,
strict_export: bool = False,
capture_autocast_dtype: Optional[torch.dtype] = None,
fail_silently: bool = True,
**kwargs: P.kwargs,
) -> Callable[P, Any]:
"""
Compile and apply AoTI to pipeline.transformer in-place.
Parameters:
pipeline:
Diffusers-style pipeline with a `.transformer` module.
*args, **kwargs:
Warmup call passed to the pipeline during AoTI capture.
Use representative image size, prompt length, step count, and guidance.
duration:
ZeroGPU allocation duration for compilation.
inductor_configs:
Optional overrides for DEFAULT_INDUCTOR_CONFIGS.
dynamic_shape_specs:
Optional overrides for dynamic-shape specs.
enable_float8:
Optional experimental torchao FP8 quantization before export.
strict_export:
Passed to torch.export.export(..., strict=...).
False is more permissive for complex model code.
capture_autocast_dtype:
Set to torch.bfloat16 or torch.float16 if your real inference path uses autocast.
Leave None when the pipeline/model is already loaded in the desired dtype.
fail_silently:
If True, log and keep the original transformer on compile failure.
If False, re-raise the error.
Returns:
The same pipeline object, mutated in-place if compile/apply succeeds.
"""
if not _has_zero_gpu_aoti():
LOGGER.warning("Hugging Face Spaces AoTI APIs are unavailable; skipping optimization.")
return pipeline
transformer = getattr(pipeline, "transformer", None)
if transformer is None:
message = "pipeline has no `.transformer` attribute; cannot apply transformer AoTI."
if fail_silently:
LOGGER.warning(message)
return pipeline
raise AttributeError(message)
if isinstance(transformer, torch.nn.Module):
transformer.eval()
_maybe_disable_progress_bar(pipeline)
configs = dict(DEFAULT_INDUCTOR_CONFIGS)
if inductor_configs:
configs.update(inductor_configs)
# Define inside optimize_pipeline_ so it closes over the actual pipeline and warmup args.
@spaces.GPU(duration=duration) # type: ignore[union-attr]
def compile_transformer():
LOGGER.info("Capturing transformer call for AoTI export.")
autocast_ctx = (
torch.autocast(device_type="cuda", dtype=capture_autocast_dtype)
if capture_autocast_dtype is not None and torch.cuda.is_available()
else nullcontext()
)
with torch.inference_mode(), autocast_ctx:
with spaces.aoti_capture(transformer) as call: # type: ignore[union-attr]
pipeline(*args, **kwargs)
dynamic_shapes = _build_dynamic_shapes(call.kwargs, dynamic_shape_specs)
_maybe_quantize_transformer(transformer, enable_float8)
LOGGER.info("Exporting transformer with torch.export; strict=%s", strict_export)
exported = torch.export.export(
mod=transformer,
args=call.args,
kwargs=call.kwargs,
dynamic_shapes=dynamic_shapes,
strict=strict_export,
)
LOGGER.info("Compiling exported transformer with AoTI.")
return spaces.aoti_compile(exported, configs) # type: ignore[union-attr]
try:
compiled_transformer = compile_transformer()
spaces.aoti_apply(compiled_transformer, transformer) # type: ignore[union-attr]
LOGGER.info("AoTI transformer optimization applied successfully.")
except Exception as exc:
LOGGER.exception("AoTI transformer optimization failed.")
if not fail_silently:
raise
LOGGER.warning("Continuing with the original uncompiled transformer. Error: %s", exc)
return pipeline