737 lines
29 KiB
Python
737 lines
29 KiB
Python
"""Pipeline blocks — each block owns its model lifecycle.
|
|
Blocks build a model on each ``__call__``, use it, then free GPU memory.
|
|
This eliminates manual ``del model; cleanup_memory()`` in pipelines and
|
|
removes the need for :class:`ModelLedger`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import dataclasses
|
|
import logging
|
|
from collections.abc import Iterator
|
|
from contextlib import AbstractContextManager, contextmanager
|
|
from dataclasses import replace
|
|
from typing import Callable, TypeVar
|
|
|
|
import torch
|
|
|
|
from ltx_core.batch_split import BatchSplitAdapter
|
|
from ltx_core.block_streaming import DISK_CPU_SLOTS, StreamingModelBuilder
|
|
from ltx_core.components.diffusion_steps import EulerDiffusionStep
|
|
from ltx_core.components.noisers import Noiser
|
|
from ltx_core.components.patchifiers import AudioPatchifier, VideoLatentPatchifier
|
|
from ltx_core.components.protocols import DiffusionStepProtocol
|
|
from ltx_core.loader import SDOps
|
|
from ltx_core.loader.attention_ops import set_attention_module_op
|
|
from ltx_core.loader.fuse_loras import bf16_fuse_rule
|
|
from ltx_core.loader.module_ops import ModuleOps
|
|
from ltx_core.loader.primitives import BuilderProtocol, LoraPathStrengthAndSDOps, ModelBuilderProtocol
|
|
from ltx_core.loader.registry import DummyRegistry, Registry
|
|
from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder as Builder
|
|
from ltx_core.model.audio_vae import (
|
|
AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
|
|
AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
|
|
VOCODER_COMFY_KEYS_FILTER,
|
|
AudioDecoderConfigurator,
|
|
AudioEncoderConfigurator,
|
|
VocoderConfigurator,
|
|
)
|
|
from ltx_core.model.audio_vae import (
|
|
decode_audio as vae_decode_audio,
|
|
)
|
|
from ltx_core.model.transformer import (
|
|
LTXV_MODEL_COMFY_RENAMING_MAP,
|
|
LTXModel,
|
|
LTXModelConfigurator,
|
|
X0Model,
|
|
)
|
|
from ltx_core.model.transformer.attention import (
|
|
AttentionCallable,
|
|
AttentionFunction,
|
|
)
|
|
from ltx_core.model.transformer.compiling import (
|
|
CompilationConfig,
|
|
build_compile_transformer_op,
|
|
modify_sd_ops_for_compilation,
|
|
)
|
|
from ltx_core.model.upsampler import LatentUpsamplerConfigurator, upsample_video
|
|
from ltx_core.model.video_vae import (
|
|
MEMORY_EFFICIENT_DECODE,
|
|
VAE_DECODER_COMFY_KEYS_FILTER,
|
|
VAE_ENCODER_COMFY_KEYS_FILTER,
|
|
TilingConfig,
|
|
VideoDecoderConfigurator,
|
|
VideoEncoder,
|
|
VideoEncoderConfigurator,
|
|
)
|
|
from ltx_core.quantization import QuantizationPolicy, fp8_cast_fuse_rule
|
|
from ltx_core.text_encoders.gemma import (
|
|
EMBEDDINGS_PROCESSOR_KEY_OPS,
|
|
GEMMA_LLM_KEY_OPS,
|
|
GEMMA_MODEL_OPS,
|
|
EmbeddingsProcessorConfigurator,
|
|
GemmaTextEncoderConfigurator,
|
|
module_ops_from_gemma_root,
|
|
)
|
|
from ltx_core.text_encoders.gemma.embeddings_processor import EmbeddingsProcessor, EmbeddingsProcessorOutput
|
|
from ltx_core.tools import AudioLatentTools, LatentTools, VideoLatentTools
|
|
from ltx_core.types import Audio, AudioLatentShape, LatentState, VideoLatentShape, VideoPixelShape
|
|
from ltx_core.utils import find_matching_file
|
|
from ltx_pipelines.multigpu.delegating_builder import DelegatingBuilder
|
|
from ltx_pipelines.utils.gpu_model import gpu_model
|
|
from ltx_pipelines.utils.helpers import (
|
|
cleanup_memory,
|
|
create_noised_state,
|
|
generate_enhanced_prompt,
|
|
)
|
|
from ltx_pipelines.utils.samplers import euler_denoising_loop
|
|
from ltx_pipelines.utils.types import Denoiser, ModalitySpec, OffloadMode
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
_M = TypeVar("_M", bound=torch.nn.Module)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _chain_quantization(
|
|
sd_ops: SDOps,
|
|
module_ops: tuple[ModuleOps, ...],
|
|
quantization: QuantizationPolicy,
|
|
) -> tuple[SDOps, tuple[ModuleOps, ...]]:
|
|
chained_sd_ops = sd_ops
|
|
if quantization.sd_ops is not None:
|
|
chained_sd_ops = SDOps(
|
|
name=f"sd_ops_chain_{sd_ops.name}+{quantization.sd_ops.name}",
|
|
mapping=(*sd_ops.mapping, *quantization.sd_ops.mapping),
|
|
)
|
|
return chained_sd_ops, (*module_ops, *quantization.module_ops)
|
|
|
|
|
|
def _apply_compile_ops(
|
|
sd_ops: SDOps,
|
|
module_ops: tuple[ModuleOps, ...],
|
|
loras: tuple[LoraPathStrengthAndSDOps, ...],
|
|
number_of_layers: int,
|
|
compilation_config: CompilationConfig,
|
|
) -> tuple[SDOps, tuple[ModuleOps, ...], tuple[LoraPathStrengthAndSDOps, ...]]:
|
|
"""Rewrite sd_ops/module_ops/LoRAs for compiled blocks (params land under ``_orig_mod``)."""
|
|
sd_ops = modify_sd_ops_for_compilation(sd_ops, number_of_layers)
|
|
compile_op = build_compile_transformer_op(compilation_config)
|
|
module_ops = (*module_ops, compile_op)
|
|
loras = tuple(
|
|
LoraPathStrengthAndSDOps(
|
|
lora.path,
|
|
lora.strength,
|
|
modify_sd_ops_for_compilation(lora.sd_ops, number_of_layers),
|
|
)
|
|
for lora in loras
|
|
)
|
|
return sd_ops, module_ops, loras
|
|
|
|
|
|
@contextmanager
|
|
def _streaming_model(
|
|
builder: StreamingModelBuilder,
|
|
offload_mode: OffloadMode,
|
|
target_device: torch.device,
|
|
dtype: torch.dtype,
|
|
) -> Iterator:
|
|
"""Build a streaming wrapper, yield it, then tear down and free memory."""
|
|
cpu_slots_count = DISK_CPU_SLOTS if offload_mode == OffloadMode.DISK else None
|
|
wrapped = builder.build(
|
|
target_device=target_device,
|
|
dtype=dtype,
|
|
cpu_slots_count=cpu_slots_count,
|
|
)
|
|
try:
|
|
yield wrapped
|
|
finally:
|
|
wrapped.teardown()
|
|
wrapped.to("meta")
|
|
cleanup_memory()
|
|
|
|
|
|
def _build_state(
|
|
spec: ModalitySpec,
|
|
tools: LatentTools,
|
|
noiser: Noiser,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
) -> LatentState:
|
|
"""Create a noised latent state from a modality spec and tools."""
|
|
state = create_noised_state(
|
|
tools=tools,
|
|
conditionings=spec.conditionings,
|
|
noiser=noiser,
|
|
dtype=dtype,
|
|
device=device,
|
|
noise_scale=spec.noise_scale,
|
|
initial_latent=spec.initial_latent,
|
|
)
|
|
if spec.frozen:
|
|
state = replace(state, denoise_mask=torch.zeros_like(state.denoise_mask))
|
|
return state
|
|
|
|
|
|
def _cleanup_iter(it: Iterator[torch.Tensor], model: torch.nn.Module) -> Iterator[torch.Tensor]:
|
|
"""Wrap an iterator to clean up *model* memory once it is exhausted or abandoned."""
|
|
with gpu_model(model):
|
|
yield from it
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DiffusionStage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class DiffusionStage:
|
|
"""Owns transformer lifecycle. Builds on each call, frees on exit.
|
|
Replaces the manual ``model_ledger.transformer()`` / ``del transformer``
|
|
pattern in every pipeline.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
loras: tuple[LoraPathStrengthAndSDOps, ...] = (),
|
|
quantization: QuantizationPolicy | None = None,
|
|
registry: Registry | None = None,
|
|
compilation_config: CompilationConfig | None = None,
|
|
offload_mode: OffloadMode = OffloadMode.NONE,
|
|
transformer_builder: ModelBuilderProtocol[LTXModel] | DelegatingBuilder[LTXModel] | None = None,
|
|
) -> None:
|
|
self._checkpoint_path = checkpoint_path
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._quantization = quantization
|
|
self._compilation_config = compilation_config
|
|
self._offload_mode = offload_mode
|
|
configurator = (
|
|
quantization.model_configurator
|
|
if quantization is not None and quantization.model_configurator is not None
|
|
else LTXModelConfigurator
|
|
)
|
|
if transformer_builder is not None:
|
|
self._transformer_builder = transformer_builder
|
|
else:
|
|
self._transformer_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=configurator,
|
|
model_sd_ops=LTXV_MODEL_COMFY_RENAMING_MAP,
|
|
loras=tuple(loras),
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
if offload_mode != OffloadMode.NONE:
|
|
if compilation_config is not None:
|
|
raise ValueError("torch.compile is not supported with layer streaming")
|
|
# WeightsProvider currently only supports plain bf16 + fp8_cast LoRA fusion
|
|
# (no companion-key emission). Quantization policies that emit
|
|
# companion keys (e.g. ``.weight_scale``) cannot be streamed yet.
|
|
if quantization is not None and quantization.fuse_rule is not fp8_cast_fuse_rule:
|
|
raise ValueError(
|
|
"Block streaming is not supported with this quantization policy "
|
|
"(only bf16 and fp8_cast are currently supported)."
|
|
)
|
|
streaming_sd_ops: SDOps = LTXV_MODEL_COMFY_RENAMING_MAP
|
|
streaming_module_ops: tuple[ModuleOps, ...] = ()
|
|
if quantization is not None:
|
|
streaming_sd_ops, streaming_module_ops = _chain_quantization(
|
|
streaming_sd_ops, streaming_module_ops, quantization
|
|
)
|
|
self._streaming_builder = StreamingModelBuilder(
|
|
model_class_configurator=configurator,
|
|
model_path=checkpoint_path,
|
|
model_sd_ops=streaming_sd_ops,
|
|
module_ops=streaming_module_ops,
|
|
loras=tuple(loras),
|
|
registry=registry or DummyRegistry(),
|
|
fuse_rule=quantization.fuse_rule if quantization is not None else bf16_fuse_rule,
|
|
blocks_attr="transformer_blocks",
|
|
blocks_prefix="transformer_blocks",
|
|
)
|
|
|
|
def with_attention(self, attention: AttentionFunction | AttentionCallable | None) -> "DiffusionStage":
|
|
"""Return a new ``DiffusionStage`` that pins the transformer build to ``attention``.
|
|
Functional: never mutates ``self``. The returned stage shares all other
|
|
configuration with the original; only the underlying builders' ``module_ops``
|
|
gain a ``set_attention_module_op(attention)`` entry so subsequent transformer
|
|
builds use that kernel. ``attention=None`` is a no-op (returns ``self``).
|
|
"""
|
|
if attention is None:
|
|
return self
|
|
op = set_attention_module_op(attention)
|
|
new = copy.copy(self)
|
|
new._transformer_builder = self._transformer_builder.with_module_ops(
|
|
(*self._transformer_builder.module_ops, op),
|
|
)
|
|
if self._offload_mode != OffloadMode.NONE:
|
|
new._streaming_builder = dataclasses.replace(
|
|
self._streaming_builder,
|
|
module_ops=(*self._streaming_builder.module_ops, op),
|
|
)
|
|
return new
|
|
|
|
def _build_transformer(self, *, device: torch.device | None = None, **kwargs: object) -> X0Model:
|
|
target = device or self._device
|
|
sd_ops = self._transformer_builder.model_sd_ops
|
|
module_ops = self._transformer_builder.module_ops
|
|
loras = self._transformer_builder.loras
|
|
if self._compilation_config is not None:
|
|
number_of_layers = self._transformer_builder.model_config()["transformer"]["num_layers"]
|
|
sd_ops, module_ops, loras = _apply_compile_ops(
|
|
sd_ops, module_ops, loras, number_of_layers, self._compilation_config
|
|
)
|
|
if self._quantization is not None:
|
|
sd_ops, module_ops = _chain_quantization(sd_ops, module_ops, self._quantization)
|
|
|
|
builder = self._transformer_builder.with_module_ops(module_ops).with_sd_ops(sd_ops).with_loras(loras)
|
|
if self._quantization is not None:
|
|
builder = builder.with_fuse_rule(self._quantization.fuse_rule)
|
|
return X0Model(builder.build(device=target, **kwargs)).to(target).eval()
|
|
|
|
@contextmanager
|
|
def _streaming_transformer_ctx(self) -> Iterator[X0Model]:
|
|
with _streaming_model(
|
|
self._streaming_builder, self._offload_mode, self._device, self._dtype
|
|
) as streaming_wrapper:
|
|
yield X0Model(streaming_wrapper).eval()
|
|
|
|
def _transformer_ctx(self, **kwargs: object) -> AbstractContextManager:
|
|
if self._offload_mode != OffloadMode.NONE:
|
|
return self._streaming_transformer_ctx()
|
|
return gpu_model(self._build_transformer(**kwargs))
|
|
|
|
def model_context(self, **kwargs: object) -> AbstractContextManager:
|
|
"""Build the transformer, yield it, then free its memory on exit.
|
|
Keyword arguments are forwarded to the underlying builder (e.g.
|
|
``video_tools`` required by ``TiledDataParallelBuilder``).
|
|
"""
|
|
return self._transformer_ctx(**kwargs)
|
|
|
|
def run( # noqa: PLR0913
|
|
self,
|
|
transformer: object,
|
|
denoiser: Denoiser,
|
|
sigmas: torch.Tensor,
|
|
noiser: Noiser,
|
|
width: int,
|
|
height: int,
|
|
frames: int,
|
|
fps: float,
|
|
video: ModalitySpec | None = None,
|
|
audio: ModalitySpec | None = None,
|
|
stepper: DiffusionStepProtocol | None = None,
|
|
loop: Callable[..., tuple[LatentState | None, LatentState | None]] | None = None,
|
|
max_batch_size: int = 1,
|
|
) -> tuple[LatentState | None, LatentState | None]:
|
|
"""Run denoising with a pre-built transformer.
|
|
Same semantics as ``__call__`` but accepts a pre-built transformer so
|
|
the model can be shared across multiple calls (e.g. tiled inference
|
|
inside a single ``model_context()`` block). Audio supports
|
|
``ModalitySpec(frozen=True)`` to keep the latent unchanged throughout
|
|
denoising while still providing cross-modal context to the transformer.
|
|
Returns ``(video_state | None, audio_state | None)`` with cleared
|
|
conditionings and unpatchified latents for present modalities.
|
|
"""
|
|
if video is None and audio is None:
|
|
raise ValueError("At least one of `video` or `audio` must be provided")
|
|
|
|
if loop is None:
|
|
loop = euler_denoising_loop
|
|
if stepper is None:
|
|
stepper = EulerDiffusionStep()
|
|
|
|
pixel_shape = VideoPixelShape(batch=1, frames=frames, height=height, width=width, fps=fps)
|
|
|
|
video_state: LatentState | None = None
|
|
video_tools: LatentTools | None = None
|
|
if video is not None:
|
|
v_shape = VideoLatentShape.from_pixel_shape(pixel_shape)
|
|
video_tools = VideoLatentTools(VideoLatentPatchifier(patch_size=1), v_shape, fps)
|
|
video_state = _build_state(video, video_tools, noiser, self._dtype, self._device)
|
|
|
|
audio_state: LatentState | None = None
|
|
audio_tools: LatentTools | None = None
|
|
if audio is not None:
|
|
a_shape = AudioLatentShape.from_video_pixel_shape(pixel_shape)
|
|
audio_tools = AudioLatentTools(AudioPatchifier(patch_size=1), a_shape)
|
|
audio_state = _build_state(audio, audio_tools, noiser, self._dtype, self._device)
|
|
|
|
wrapped = BatchSplitAdapter(transformer, max_batch_size=max_batch_size) # type: ignore[arg-type]
|
|
video_state, audio_state = loop(
|
|
sigmas=sigmas,
|
|
video_state=video_state,
|
|
audio_state=audio_state,
|
|
stepper=stepper,
|
|
transformer=wrapped,
|
|
denoiser=denoiser,
|
|
)
|
|
|
|
if video_state is not None and video_tools is not None:
|
|
video_state = video_tools.clear_conditioning(video_state)
|
|
video_state = video_tools.unpatchify(video_state)
|
|
if audio_state is not None and audio_tools is not None:
|
|
audio_state = audio_tools.clear_conditioning(audio_state)
|
|
audio_state = audio_tools.unpatchify(audio_state)
|
|
|
|
return video_state, audio_state
|
|
|
|
def __call__( # noqa: PLR0913
|
|
self,
|
|
denoiser: Denoiser,
|
|
sigmas: torch.Tensor,
|
|
noiser: Noiser,
|
|
width: int,
|
|
height: int,
|
|
frames: int,
|
|
fps: float,
|
|
video: ModalitySpec | None = None,
|
|
audio: ModalitySpec | None = None,
|
|
stepper: DiffusionStepProtocol | None = None,
|
|
loop: Callable[..., tuple[LatentState | None, LatentState | None]] | None = None,
|
|
max_batch_size: int = 1,
|
|
) -> tuple[LatentState | None, LatentState | None]:
|
|
"""Build transformer -> run denoising loop -> free transformer.
|
|
Returns ``(video_state | None, audio_state | None)`` with cleared
|
|
conditionings and unpatchified latents for present modalities.
|
|
"""
|
|
# Build video_tools up front so it can be forwarded to the transformer
|
|
# context (required by TiledDataParallelBuilder in multi-GPU mode).
|
|
# `run()` rebuilds its own tools internally; the duplication is cheap.
|
|
video_tools: LatentTools | None = None
|
|
if video is not None:
|
|
pixel_shape = VideoPixelShape(batch=1, frames=frames, height=height, width=width, fps=fps)
|
|
v_shape = VideoLatentShape.from_pixel_shape(pixel_shape)
|
|
video_tools = VideoLatentTools(VideoLatentPatchifier(patch_size=1), v_shape, fps)
|
|
|
|
mode = "streaming" if self._offload_mode != OffloadMode.NONE else "standard"
|
|
logger.info("Building transformer (%s) from %s", mode, self._checkpoint_path)
|
|
with self._transformer_ctx(video_tools=video_tools) as transformer:
|
|
logger.info(
|
|
"Running denoising loop (%d steps, %dx%d %d frames @ %.1f fps)",
|
|
len(sigmas) - 1,
|
|
width,
|
|
height,
|
|
frames,
|
|
fps,
|
|
)
|
|
return self.run(
|
|
transformer,
|
|
denoiser,
|
|
sigmas,
|
|
noiser,
|
|
width,
|
|
height,
|
|
frames,
|
|
fps,
|
|
video,
|
|
audio,
|
|
stepper,
|
|
loop,
|
|
max_batch_size,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PromptEncoder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class PromptEncoder:
|
|
"""Owns text encoder + embeddings processor lifecycle.
|
|
Loads Gemma, encodes prompts, frees Gemma, then loads the embeddings
|
|
processor to produce final outputs.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
gemma_root: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
offload_mode: OffloadMode = OffloadMode.NONE,
|
|
text_encoder_builder: BuilderProtocol | None = None,
|
|
) -> None:
|
|
self._gemma_root = gemma_root
|
|
self._checkpoint_path = checkpoint_path
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._offload_mode = offload_mode
|
|
|
|
if text_encoder_builder is not None:
|
|
if offload_mode != OffloadMode.NONE:
|
|
raise ValueError(
|
|
"text_encoder_builder cannot be used with offload_mode != OffloadMode.NONE "
|
|
"because no streaming text encoder builder is available."
|
|
)
|
|
self._text_encoder_builder = text_encoder_builder
|
|
self._streaming_text_encoder_builder = None
|
|
else:
|
|
module_ops = module_ops_from_gemma_root(gemma_root)
|
|
model_folder = find_matching_file(gemma_root, "model*.safetensors").parent
|
|
weight_paths = [str(p) for p in model_folder.rglob("*.safetensors")]
|
|
self._text_encoder_builder = Builder(
|
|
model_path=tuple(weight_paths),
|
|
model_class_configurator=GemmaTextEncoderConfigurator,
|
|
model_sd_ops=GEMMA_LLM_KEY_OPS,
|
|
module_ops=(GEMMA_MODEL_OPS, *module_ops),
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
self._streaming_text_encoder_builder = StreamingModelBuilder(
|
|
model_path=tuple(weight_paths),
|
|
model_class_configurator=GemmaTextEncoderConfigurator,
|
|
model_sd_ops=GEMMA_LLM_KEY_OPS,
|
|
module_ops=(GEMMA_MODEL_OPS, *module_ops),
|
|
registry=registry or DummyRegistry(),
|
|
blocks_attr="model.model.language_model.layers",
|
|
blocks_prefix="model.model.language_model.layers",
|
|
)
|
|
self._embeddings_processor_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=EmbeddingsProcessorConfigurator,
|
|
model_sd_ops=EMBEDDINGS_PROCESSOR_KEY_OPS,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
def _build_text_encoder(self) -> torch.nn.Module:
|
|
"""Build the Gemma text encoder (non-streaming path)."""
|
|
return self._text_encoder_builder.build(device=self._device, dtype=self._dtype).eval()
|
|
|
|
def _build_embeddings_processor(self) -> EmbeddingsProcessor:
|
|
"""Build the embeddings processor on the target device."""
|
|
return self._embeddings_processor_builder.build(device=self._device, dtype=self._dtype).eval()
|
|
|
|
def _text_encoder_ctx(self) -> AbstractContextManager:
|
|
if self._offload_mode != OffloadMode.NONE:
|
|
return _streaming_model(self._streaming_text_encoder_builder, self._offload_mode, self._device, self._dtype)
|
|
return gpu_model(self._build_text_encoder())
|
|
|
|
def __call__(
|
|
self,
|
|
prompts: list[str],
|
|
*,
|
|
enhance_first_prompt: bool = False,
|
|
enhance_prompt_image: str | None = None,
|
|
enhance_prompt_seed: int = 42,
|
|
) -> list[EmbeddingsProcessorOutput]:
|
|
"""Encode *prompts* through Gemma -> embeddings processor, freeing each model after use."""
|
|
logger.info("Building text encoder from %s", self._gemma_root)
|
|
with self._text_encoder_ctx() as text_encoder:
|
|
if enhance_first_prompt:
|
|
prompts = list(prompts)
|
|
prompts[0] = generate_enhanced_prompt(
|
|
text_encoder, prompts[0], enhance_prompt_image, seed=enhance_prompt_seed
|
|
)
|
|
raw_outputs = [text_encoder.encode(p) for p in prompts]
|
|
logger.info("Text encoder done, building embeddings processor from %s", self._checkpoint_path)
|
|
|
|
with gpu_model(self._build_embeddings_processor()) as embeddings_processor:
|
|
result = [embeddings_processor.process_hidden_states(hs, mask) for hs, mask in raw_outputs]
|
|
logger.info("Prompt encoding complete")
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ImageConditioner
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ImageConditioner:
|
|
"""Owns video encoder lifecycle.
|
|
Builds the encoder, passes it to the user-supplied callable, then frees it.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
) -> None:
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._encoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=VideoEncoderConfigurator,
|
|
model_sd_ops=VAE_ENCODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
def _build_encoder(self) -> VideoEncoder:
|
|
return self._encoder_builder.build(device=self._device, dtype=self._dtype).eval()
|
|
|
|
def __call__(self, fn: Callable[[VideoEncoder], T]) -> T:
|
|
"""Build video encoder → call *fn(encoder)* → free encoder."""
|
|
with gpu_model(self._build_encoder()) as encoder:
|
|
return fn(encoder)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VideoUpsampler
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class VideoUpsampler:
|
|
"""Owns video encoder + spatial upsampler lifecycle."""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
upsampler_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
) -> None:
|
|
self._upsampler_path = upsampler_path
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._encoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=VideoEncoderConfigurator,
|
|
model_sd_ops=VAE_ENCODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
self._upsampler_builder = Builder(
|
|
model_path=upsampler_path,
|
|
model_class_configurator=LatentUpsamplerConfigurator,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
def __call__(self, latent: torch.Tensor) -> torch.Tensor:
|
|
"""Upsample *latent* using video encoder + spatial upsampler, then free both."""
|
|
logger.info("Building video encoder + spatial upsampler from %s", self._upsampler_path)
|
|
with (
|
|
gpu_model(self._encoder_builder.build(device=self._device, dtype=self._dtype).eval()) as encoder,
|
|
gpu_model(self._upsampler_builder.build(device=self._device, dtype=self._dtype).eval()) as upsampler,
|
|
):
|
|
return upsample_video(latent=latent, video_encoder=encoder, upsampler=upsampler)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VideoDecoder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class VideoDecoder:
|
|
"""Owns video decoder lifecycle.
|
|
Returns an iterator that cleans up the decoder after all chunks are consumed.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
memory_efficient: bool = True,
|
|
decoder_builder: BuilderProtocol | None = None,
|
|
) -> None:
|
|
self._checkpoint_path = checkpoint_path
|
|
self._dtype = dtype
|
|
self._device = device
|
|
if decoder_builder is not None:
|
|
self._decoder_builder = decoder_builder
|
|
else:
|
|
self._decoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=VideoDecoderConfigurator,
|
|
model_sd_ops=VAE_DECODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
module_ops=(MEMORY_EFFICIENT_DECODE,) if memory_efficient else (),
|
|
)
|
|
|
|
def __call__(
|
|
self,
|
|
latent: torch.Tensor,
|
|
tiling_config: TilingConfig | None = None,
|
|
generator: torch.Generator | None = None,
|
|
) -> Iterator[torch.Tensor]:
|
|
"""Decode *latent* to pixel-space video chunks. Decoder freed after exhaustion."""
|
|
logger.info("Building video decoder from %s", self._checkpoint_path)
|
|
decoder = self._decoder_builder.build(device=self._device, dtype=self._dtype).eval()
|
|
return _cleanup_iter(decoder.decode_video(latent, tiling_config, generator), decoder)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AudioDecoder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class AudioDecoder:
|
|
"""Owns audio decoder + vocoder lifecycle."""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
) -> None:
|
|
self._checkpoint_path = checkpoint_path
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._decoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=AudioDecoderConfigurator,
|
|
model_sd_ops=AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
self._vocoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=VocoderConfigurator,
|
|
model_sd_ops=VOCODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
def __call__(self, latent: torch.Tensor) -> Audio:
|
|
"""Decode audio *latent* through VAE decoder + vocoder, then free both."""
|
|
logger.info("Building audio decoder + vocoder from %s", self._checkpoint_path)
|
|
with (
|
|
gpu_model(self._decoder_builder.build(device=self._device, dtype=self._dtype).eval()) as decoder,
|
|
gpu_model(self._vocoder_builder.build(device=self._device, dtype=self._dtype).eval()) as vocoder,
|
|
):
|
|
return vae_decode_audio(latent, decoder, vocoder)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AudioEncoder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class AudioConditioner:
|
|
"""Owns audio encoder lifecycle.
|
|
Builds the encoder, passes it to the user-supplied callable, then frees it.
|
|
Mirrors :class:`ImageConditioner` for the audio modality.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
checkpoint_path: str,
|
|
dtype: torch.dtype,
|
|
device: torch.device,
|
|
registry: Registry | None = None,
|
|
) -> None:
|
|
self._dtype = dtype
|
|
self._device = device
|
|
self._encoder_builder = Builder(
|
|
model_path=checkpoint_path,
|
|
model_class_configurator=AudioEncoderConfigurator,
|
|
model_sd_ops=AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
|
|
registry=registry or DummyRegistry(),
|
|
)
|
|
|
|
def __call__(self, fn: Callable[[torch.nn.Module], T]) -> T:
|
|
"""Build audio encoder → call *fn(encoder)* → free encoder."""
|
|
with gpu_model(self._encoder_builder.build(device=self._device, dtype=self._dtype).eval()) as encoder:
|
|
return fn(encoder)
|