mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-09-21 13:38:08 -05:00
Allow model files to contain which attention should be used for each block. (#16419)
This commit is contained in:
@@ -152,6 +152,27 @@ Example:
|
|||||||
|
|
||||||
To create compatible checkpoints, use any quantization tool provided the output follows the checkpoint format described above and uses a layout defined in `QUANT_ALGOS`.
|
To create compatible checkpoints, use any quantization tool provided the output follows the checkpoint format described above and uses a layout defined in `QUANT_ALGOS`.
|
||||||
|
|
||||||
|
### Diffusion attention preferences
|
||||||
|
|
||||||
|
A diffusion attention module can have a `<module path>.comfy_attention.config` entry whose
|
||||||
|
uint8 tensor contains UTF-8 JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"attention": "comfy_kitchen_int8"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the module that performs attention, such as `transformer_blocks.0.attn` for
|
||||||
|
Qwen Image 2.1 or `blocks.0.attn` for MiniMax H3.
|
||||||
|
|
||||||
|
Only `comfy_kitchen_int8` is supported. Invalid targets and other method names
|
||||||
|
are ignored with a warning during loading, leaving normal attention selection.
|
||||||
|
Kitchen INT8 support is checked when each preference is loaded
|
||||||
|
for the primary device; unsupported devices keep normal attention selection.
|
||||||
|
Explicit attention overrides retain priority.
|
||||||
|
The `ComfyAttention` child module loads and saves its own metadata through normal
|
||||||
|
state-dict loading and saving. Preferences do not enable weight
|
||||||
|
quantization. Text encoder and VAE loaders do not apply these preferences.
|
||||||
|
|
||||||
### Weight Quantization
|
### Weight Quantization
|
||||||
|
|
||||||
Weight quantization is straightforward - compute the scaling factor directly from the weight tensor using the absolute maximum method described earlier. Each layer's weights are quantized independently and stored with their corresponding `weight_scale` parameter.
|
Weight quantization is straightforward - compute the scaling factor directly from the weight tensor using the absolute maximum method described earlier. Each layer's weights are quantized independently and stored with their corresponding `weight_scale` parameter.
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import comfy.model_prefetch
|
|||||||
import comfy.ops
|
import comfy.ops
|
||||||
import comfy.patcher_extension
|
import comfy.patcher_extension
|
||||||
import comfy.quant_ops
|
import comfy.quant_ops
|
||||||
from comfy.ldm.modules.attention import AttentionTensorContainer, optimized_attention
|
from comfy.ldm.modules.attention import ComfyAttention, AttentionTensorContainer, optimized_attention
|
||||||
|
|
||||||
FRAME_PER_TOKEN = (1, 4, 4, 4, 4)
|
FRAME_PER_TOKEN = (1, 4, 4, 4, 4)
|
||||||
FRAME_RESCALE = 5.0 / 3.0
|
FRAME_RESCALE = 5.0 / 3.0
|
||||||
@@ -158,6 +158,7 @@ def rope_rotation_table(angles, dtype):
|
|||||||
class Attention(nn.Module):
|
class Attention(nn.Module):
|
||||||
def __init__(self, hidden, heads, head_dim, eps, gate_compress=False, dtype=None, device=None, operations=None):
|
def __init__(self, hidden, heads, head_dim, eps, gate_compress=False, dtype=None, device=None, operations=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.comfy_attention = ComfyAttention()
|
||||||
self.heads = heads
|
self.heads = heads
|
||||||
self.head_dim = head_dim
|
self.head_dim = head_dim
|
||||||
inner = heads * head_dim
|
inner = heads * head_dim
|
||||||
@@ -196,7 +197,7 @@ class Attention(nn.Module):
|
|||||||
q = AttentionTensorContainer(q.transpose(0, 1).unsqueeze(0))
|
q = AttentionTensorContainer(q.transpose(0, 1).unsqueeze(0))
|
||||||
k = AttentionTensorContainer(k.transpose(0, 1).unsqueeze(0))
|
k = AttentionTensorContainer(k.transpose(0, 1).unsqueeze(0))
|
||||||
v = AttentionTensorContainer(v.transpose(0, 1).unsqueeze(0))
|
v = AttentionTensorContainer(v.transpose(0, 1).unsqueeze(0))
|
||||||
out = optimized_attention(q, k, v, self.heads, mask=None, skip_reshape=True, transformer_options=transformer_options)
|
out = optimized_attention(q, k, v, self.heads, preferred_attention=self.comfy_attention, mask=None, skip_reshape=True, transformer_options=transformer_options)
|
||||||
return self.out_proj(out.squeeze(0))
|
return self.out_proj(out.squeeze(0))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
|
import json
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
@@ -71,6 +72,34 @@ def get_attention_function(name: str, default: Any=...) -> Union[Callable, None]
|
|||||||
return default
|
return default
|
||||||
return REGISTERED_ATTENTION_FUNCTIONS[name]
|
return REGISTERED_ATTENTION_FUNCTIONS[name]
|
||||||
|
|
||||||
|
|
||||||
|
class ComfyAttention(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.config = None
|
||||||
|
self.function = None
|
||||||
|
|
||||||
|
def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
|
||||||
|
self.config = None
|
||||||
|
self.function = None
|
||||||
|
metadata = state_dict.pop(prefix + "config", None)
|
||||||
|
if metadata is not None:
|
||||||
|
config = json.loads(metadata.numpy().tobytes())
|
||||||
|
method = config.get("attention")
|
||||||
|
if method == "comfy_kitchen_int8":
|
||||||
|
self.config = config
|
||||||
|
if COMFY_KITCHEN_INT8_ATTENTION_IS_AVAILABLE and comfy_kitchen.int8_attention_is_available(model_management.get_torch_device()):
|
||||||
|
self.function = attention_comfy_kitchen_int8
|
||||||
|
else:
|
||||||
|
logging.warning(f"Ignoring unsupported attention method {method!r} for {prefix.rstrip('.')}")
|
||||||
|
super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs)
|
||||||
|
|
||||||
|
def _save_to_state_dict(self, destination, prefix, keep_vars):
|
||||||
|
super()._save_to_state_dict(destination, prefix, keep_vars)
|
||||||
|
if self.config is not None:
|
||||||
|
destination[prefix + "config"] = torch.tensor(list(json.dumps(self.config).encode("utf-8")), dtype=torch.uint8)
|
||||||
|
|
||||||
|
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
import comfy.ops
|
import comfy.ops
|
||||||
ops = comfy.ops.disable_weight_init
|
ops = comfy.ops.disable_weight_init
|
||||||
@@ -171,6 +200,7 @@ class AttentionTensorContainer:
|
|||||||
def wrap_attn(func):
|
def wrap_attn(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
preferred_attention = kwargs.pop("preferred_attention", None)
|
||||||
containers = None
|
containers = None
|
||||||
if len(args) >= 3 and isinstance(args[0], AttentionTensorContainer):
|
if len(args) >= 3 and isinstance(args[0], AttentionTensorContainer):
|
||||||
if not isinstance(args[1], AttentionTensorContainer) or not isinstance(args[2], AttentionTensorContainer):
|
if not isinstance(args[1], AttentionTensorContainer) or not isinstance(args[2], AttentionTensorContainer):
|
||||||
@@ -191,6 +221,10 @@ def wrap_attn(func):
|
|||||||
return optimized_attention_override.container_function(*args, **kwargs)
|
return optimized_attention_override.container_function(*args, **kwargs)
|
||||||
args = tuple(container.take() for container in containers) + args[3:]
|
args = tuple(container.take() for container in containers) + args[3:]
|
||||||
return optimized_attention_override(func, *args, **kwargs)
|
return optimized_attention_override(func, *args, **kwargs)
|
||||||
|
if preferred_attention is not None:
|
||||||
|
attention = preferred_attention.function
|
||||||
|
if attention is not None:
|
||||||
|
return attention(*args, **kwargs)
|
||||||
|
|
||||||
if containers is not None:
|
if containers is not None:
|
||||||
if wrapper.container_function is not None:
|
if wrapper.container_function is not None:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import comfy.rmsnorm
|
|||||||
from comfy.ldm.flux.layers import EmbedND, timestep_embedding
|
from comfy.ldm.flux.layers import EmbedND, timestep_embedding
|
||||||
from comfy.ldm.flux.math import apply_rope1
|
from comfy.ldm.flux.math import apply_rope1
|
||||||
from comfy.ldm.lightricks.model import TimestepEmbedding
|
from comfy.ldm.lightricks.model import TimestepEmbedding
|
||||||
from comfy.ldm.modules.attention import optimized_attention
|
from comfy.ldm.modules.attention import ComfyAttention, optimized_attention
|
||||||
from comfy.ldm.wan.model_animate2 import PoseBranchCache
|
from comfy.ldm.wan.model_animate2 import PoseBranchCache
|
||||||
|
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class SwiGLUFeedForward(nn.Module):
|
|||||||
class Attention(nn.Module):
|
class Attention(nn.Module):
|
||||||
def __init__(self, dim, heads, dim_head, eps=1e-6, dtype=None, device=None, operations=None):
|
def __init__(self, dim, heads, dim_head, eps=1e-6, dtype=None, device=None, operations=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.comfy_attention = ComfyAttention()
|
||||||
self.heads = heads
|
self.heads = heads
|
||||||
inner_dim = heads * dim_head
|
inner_dim = heads * dim_head
|
||||||
self.to_q = operations.Linear(dim, inner_dim, bias=False, dtype=dtype, device=device)
|
self.to_q = operations.Linear(dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||||
@@ -103,7 +104,7 @@ class Attention(nn.Module):
|
|||||||
q, k = comfy.quant_ops.ck.rms_rope(q, k, pe, q_scale, k_scale, self.norm_q.eps)
|
q, k = comfy.quant_ops.ck.rms_rope(q, k, pe, q_scale, k_scale, self.norm_q.eps)
|
||||||
comfy.ops.uncast_bias_weight(self.norm_q, q_scale, None, q_stream)
|
comfy.ops.uncast_bias_weight(self.norm_q, q_scale, None, q_stream)
|
||||||
comfy.ops.uncast_bias_weight(self.norm_k, k_scale, None, k_stream)
|
comfy.ops.uncast_bias_weight(self.norm_k, k_scale, None, k_stream)
|
||||||
return self.to_out[0](attn_fn(q, k, v, self.heads))
|
return self.to_out[0](attn_fn(q, k, v, self.heads, preferred_attention=self.comfy_attention))
|
||||||
|
|
||||||
|
|
||||||
def _split_rows(p):
|
def _split_rows(p):
|
||||||
@@ -164,11 +165,11 @@ class LastLayer(nn.Module):
|
|||||||
|
|
||||||
def block_causal_attention(segments, transformer_options={}, cache=None, block_index=0, prefix_len=0):
|
def block_causal_attention(segments, transformer_options={}, cache=None, block_index=0, prefix_len=0):
|
||||||
# segments: (start, end, mask); text segments get a causal mask, image blocks attend to everything before their end
|
# segments: (start, end, mask); text segments get a causal mask, image blocks attend to everything before their end
|
||||||
def attn(q, k, v, heads):
|
def attn(q, k, v, heads, preferred_attention=None):
|
||||||
if cache is not None:
|
if cache is not None:
|
||||||
# K and V stacked on dim 1 so batch stays first and quantized rows are per token and head
|
# K and V stacked on dim 1 so batch stays first and quantized rows are per token and head
|
||||||
cache.put(block_index, torch.stack([k[:, :prefix_len], v[:, :prefix_len]], dim=1))
|
cache.put(block_index, torch.stack([k[:, :prefix_len], v[:, :prefix_len]], dim=1))
|
||||||
outs = [optimized_attention(q[:, start:end].flatten(2), k[:, :end].flatten(2), v[:, :end].flatten(2), heads, mask=mask, transformer_options=transformer_options)
|
outs = [optimized_attention(q[:, start:end].flatten(2), k[:, :end].flatten(2), v[:, :end].flatten(2), heads, mask=mask, transformer_options=transformer_options, preferred_attention=preferred_attention)
|
||||||
for start, end, mask in segments]
|
for start, end, mask in segments]
|
||||||
return torch.cat(outs, dim=1) if len(outs) > 1 else outs[0]
|
return torch.cat(outs, dim=1) if len(outs) > 1 else outs[0]
|
||||||
return attn
|
return attn
|
||||||
@@ -176,8 +177,8 @@ def block_causal_attention(segments, transformer_options={}, cache=None, block_i
|
|||||||
|
|
||||||
def prefix_cached_attention(prefix_k, prefix_v, transformer_options={}):
|
def prefix_cached_attention(prefix_k, prefix_v, transformer_options={}):
|
||||||
# target-only queries: block-causal reduces to full attention over [cached prefix, target]
|
# target-only queries: block-causal reduces to full attention over [cached prefix, target]
|
||||||
def attn(q, k, v, heads):
|
def attn(q, k, v, heads, preferred_attention=None):
|
||||||
return optimized_attention(q.flatten(2), torch.cat([prefix_k, k], dim=1).flatten(2), torch.cat([prefix_v, v], dim=1).flatten(2), heads, transformer_options=transformer_options)
|
return optimized_attention(q.flatten(2), torch.cat([prefix_k, k], dim=1).flatten(2), torch.cat([prefix_v, v], dim=1).flatten(2), heads, transformer_options=transformer_options, preferred_attention=preferred_attention)
|
||||||
return attn
|
return attn
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -874,6 +874,8 @@ class ModelPatcher:
|
|||||||
bk = self.backup.get(k, None)
|
bk = self.backup.get(k, None)
|
||||||
hbk = self.hook_backup.get(k, None)
|
hbk = self.hook_backup.get(k, None)
|
||||||
weight, set_func, convert_func = get_key_weight(self.model, k)
|
weight, set_func, convert_func = get_key_weight(self.model, k)
|
||||||
|
if not isinstance(weight, torch.Tensor):
|
||||||
|
continue
|
||||||
if bk is not None:
|
if bk is not None:
|
||||||
weight = bk.weight
|
weight = bk.weight
|
||||||
if hbk is not None:
|
if hbk is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user