linoyts's picture
linoyts HF Staff
FLUX.3 layout-control canvas (custom HTML) — initial
3db1cbc verified
Raw History Blame Contribute Delete
13 kB
# Copyright 2026 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
from typing import Any
import torch
from ..utils.logging import get_logger
from ..utils.torch_utils import unwrap_module
logger = get_logger(__name__) # pylint: disable=invalid-name
class BaseState:
def reset(self, *args, **kwargs) -> None:
raise NotImplementedError(
"BaseState::reset is not implemented. Please implement this method in the derived class."
)
class StateManager:
def __init__(self, state_cls: BaseState, init_args=None, init_kwargs=None):
self._state_cls = state_cls
self._init_args = init_args if init_args is not None else ()
self._init_kwargs = init_kwargs if init_kwargs is not None else {}
self._state_cache = {}
self._current_context = None
def get_state(self):
if self._current_context is None:
raise ValueError("No context is set. Please set a context before retrieving the state.")
if self._current_context not in self._state_cache.keys():
self._state_cache[self._current_context] = self._state_cls(*self._init_args, **self._init_kwargs)
return self._state_cache[self._current_context]
def set_context(self, name: str) -> None:
self._current_context = name
def reset(self, *args, **kwargs) -> None:
for name, state in list(self._state_cache.items()):
state.reset(*args, **kwargs)
self._state_cache.pop(name)
self._current_context = None
class ModelHook:
r"""
A hook that contains callbacks to be executed just before and after the forward method of a model.
"""
_is_stateful = False
def __init__(self):
self.fn_ref: "HookFunctionReference" = None
def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
r"""
Hook that is executed when a model is initialized.
Args:
module (`torch.nn.Module`):
The module attached to this hook.
"""
return module
def deinitalize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
r"""
Hook that is executed when a model is deinitialized.
Args:
module (`torch.nn.Module`):
The module attached to this hook.
"""
return module
def pre_forward(self, module: torch.nn.Module, *args, **kwargs) -> tuple[tuple[Any], dict[str, Any]]:
r"""
Hook that is executed just before the forward method of the model.
Args:
module (`torch.nn.Module`):
The module whose forward pass will be executed just after this event.
args (`tuple[Any]`):
The positional arguments passed to the module.
kwargs (`dict[Str, Any]`):
The keyword arguments passed to the module.
Returns:
`tuple[tuple[Any], dict[Str, Any]]`:
A tuple with the treated `args` and `kwargs`.
"""
return args, kwargs
def post_forward(self, module: torch.nn.Module, output: Any) -> Any:
r"""
Hook that is executed just after the forward method of the model.
Args:
module (`torch.nn.Module`):
The module whose forward pass been executed just before this event.
output (`Any`):
The output of the module.
Returns:
`Any`: The processed `output`.
"""
return output
def detach_hook(self, module: torch.nn.Module) -> torch.nn.Module:
r"""
Hook that is executed when the hook is detached from a module.
Args:
module (`torch.nn.Module`):
The module detached from this hook.
"""
return module
def reset_state(self, module: torch.nn.Module):
if self._is_stateful:
raise NotImplementedError("This hook is stateful and needs to implement the `reset_state` method.")
return module
def _set_context(self, module: torch.nn.Module, name: str) -> None:
# Iterate over all attributes of the hook to see if any of them have the type `StateManager`. If so, call `set_context` on them.
for attr_name in dir(self):
attr = getattr(self, attr_name)
if isinstance(attr, StateManager):
attr.set_context(name)
return module
class HookFunctionReference:
def __init__(self) -> None:
"""A container class that maintains mutable references to forward pass functions in a hook chain.
Its mutable nature allows the hook system to modify the execution chain dynamically without rebuilding the
entire forward pass structure.
Attributes:
pre_forward: A callable that processes inputs before the main forward pass.
post_forward: A callable that processes outputs after the main forward pass.
forward: The current forward function in the hook chain.
original_forward: The original forward function, stored when a hook provides a custom new_forward.
The class enables hook removal by allowing updates to the forward chain through reference modification rather
than requiring reconstruction of the entire chain. When a hook is removed, only the relevant references need to
be updated, preserving the execution order of the remaining hooks.
"""
self.pre_forward = None
self.post_forward = None
self.forward = None
self.original_forward = None
class HookRegistry:
def __init__(self, module_ref: torch.nn.Module) -> None:
super().__init__()
self.hooks: dict[str, ModelHook] = {}
self._module_ref = module_ref
self._hook_order = []
self._fn_refs = []
def register_hook(self, hook: ModelHook, name: str) -> None:
if name in self.hooks.keys():
raise ValueError(
f"Hook with name {name} already exists in the registry. Please use a different name or "
f"first remove the existing hook and then add a new one."
)
self._module_ref = hook.initialize_hook(self._module_ref)
def create_new_forward(function_reference: HookFunctionReference):
def new_forward(module, *args, **kwargs):
args, kwargs = function_reference.pre_forward(module, *args, **kwargs)
output = function_reference.forward(*args, **kwargs)
return function_reference.post_forward(module, output)
return new_forward
forward = self._module_ref.forward
fn_ref = HookFunctionReference()
fn_ref.pre_forward = hook.pre_forward
fn_ref.post_forward = hook.post_forward
fn_ref.forward = forward
if hasattr(hook, "new_forward"):
fn_ref.original_forward = forward
fn_ref.forward = functools.update_wrapper(
functools.partial(hook.new_forward, self._module_ref), hook.new_forward
)
rewritten_forward = create_new_forward(fn_ref)
# Wrap from the original `forward` so `inspect.signature` follows `__wrapped__` to the real
# signature instead of the generic `(module, *args, **kwargs)`, which breaks `torch.export`.
self._module_ref.forward = functools.update_wrapper(
functools.partial(rewritten_forward, self._module_ref), forward
)
hook.fn_ref = fn_ref
self.hooks[name] = hook
self._hook_order.append(name)
self._fn_refs.append(fn_ref)
def get_hook(self, name: str) -> ModelHook | None:
return self.hooks.get(name, None)
def remove_hook(self, name: str, recurse: bool = True) -> None:
if name in self.hooks.keys():
num_hooks = len(self._hook_order)
hook = self.hooks[name]
index = self._hook_order.index(name)
fn_ref = self._fn_refs[index]
old_forward = fn_ref.forward
if fn_ref.original_forward is not None:
old_forward = fn_ref.original_forward
if index == num_hooks - 1:
self._module_ref.forward = old_forward
else:
self._fn_refs[index + 1].forward = old_forward
self._module_ref = hook.deinitalize_hook(self._module_ref)
del self.hooks[name]
self._hook_order.pop(index)
self._fn_refs.pop(index)
if recurse:
for module_name, module in self._module_ref.named_modules():
if module_name == "":
continue
if hasattr(module, "_diffusers_hook"):
module._diffusers_hook.remove_hook(name, recurse=False)
def reset_stateful_hooks(self, recurse: bool = True) -> None:
for hook_name in reversed(self._hook_order):
hook = self.hooks[hook_name]
if hook._is_stateful:
hook.reset_state(self._module_ref)
if recurse:
for module_name, module in unwrap_module(self._module_ref).named_modules():
if module_name == "":
continue
module = unwrap_module(module)
if hasattr(module, "_diffusers_hook"):
module._diffusers_hook.reset_stateful_hooks(recurse=False)
@classmethod
def check_if_exists_or_initialize(cls, module: torch.nn.Module) -> "HookRegistry":
if not hasattr(module, "_diffusers_hook"):
module._diffusers_hook = cls(module)
return module._diffusers_hook
def _set_context(self, name: str | None = None) -> None:
for hook_name in reversed(self._hook_order):
hook = self.hooks[hook_name]
if hook._is_stateful:
hook._set_context(self._module_ref, name)
for registry in self._get_child_registries():
registry._set_context(name)
def invalidate_child_registries_cache(self) -> None:
"""Invalidate the cached child-registry list across this module's tree.
`_get_child_registries` caches the registries it finds by walking `named_modules()`, keyed on the registry that
built it. Registering or removing hooks on descendant modules (e.g. block hooks added by `enable_cache`)
changes which modules carry a `_diffusers_hook`, which stales that cache. Call this after any operation that
adds or removes hooks in the subtree so the list is rebuilt on next use. Clears the cache on every registry in
the tree, since the same registries appear in ancestor caches.
"""
for _, module in unwrap_module(self._module_ref).named_modules():
module = unwrap_module(module)
if hasattr(module, "_diffusers_hook"):
module._diffusers_hook._child_registries_cache = None
def _get_child_registries(self) -> list["HookRegistry"]:
"""Return registries of child modules, using a cached list when available.
The cache is built on first call and reused for subsequent calls. This avoids the cost of walking the full
module tree via named_modules() on every _set_context call, which is significant for large models (e.g. ~2.7ms
per call on Flux2).
"""
if not hasattr(self, "_child_registries_cache"):
self._child_registries_cache = None
if self._child_registries_cache is not None:
return self._child_registries_cache
registries = []
for module_name, module in unwrap_module(self._module_ref).named_modules():
if module_name == "":
continue
module = unwrap_module(module)
if hasattr(module, "_diffusers_hook"):
registries.append(module._diffusers_hook)
self._child_registries_cache = registries
return registries
def __repr__(self) -> str:
registry_repr = ""
for i, hook_name in enumerate(self._hook_order):
if self.hooks[hook_name].__class__.__repr__ is not object.__repr__:
hook_repr = self.hooks[hook_name].__repr__()
else:
hook_repr = self.hooks[hook_name].__class__.__name__
registry_repr += f" ({i}) {hook_name} - {hook_repr}"
if i < len(self._hook_order) - 1:
registry_repr += "\n"
return f"HookRegistry(\n{registry_repr}\n)"