Load comfy files if they exist in flux.3 image
This commit is contained in:
1 parent
438595b78b
commit
12395a103f
4 files changed
+164
-57
No files matched your search
@@ -10,20 +10,23 @@ t2i and i2i (reference images as clean conditioning tokens).
|
|||||||
in an ``AdvancedPromptEmbeds`` (padded to a multiple of 80 tokens -- the
|
in an ``AdvancedPromptEmbeds`` (padded to a multiple of 80 tokens -- the
|
||||||
DiT has no text mask, pad tokens are part of the trained context).
|
DiT has no text mask, pad tokens are part of the trained context).
|
||||||
|
|
||||||
``model.name_or_path`` is the model package (hub repo id, default
|
``model.name_or_path`` defaults to the vendor package
|
||||||
``black-forest-labs/flux-3-image-klein-base``, or a local dir):
|
``black-forest-labs/flux-3-image-klein-base`` (or a local dir):
|
||||||
|
|
||||||
config.json model.safetensors ae.safetensors text_encoder/
|
config.json model.safetensors ae.safetensors text_encoder/
|
||||||
|
|
||||||
Only the DiT and the image VAE come from the package, and they live in the
|
The DiT and VAE live in the ComfyUI layout under MODELS_PATH, under the names
|
||||||
ComfyUI layout under MODELS_PATH: ``diffusion_models/flux_3_image_klein_base.safetensors``
|
of BFL's ComfyUI repack ``black-forest-labs/flux-3-image-klein-comfy``
|
||||||
and ``vae/flux_3_vae.safetensors``. A file already there (or anywhere under its
|
(``src/checkpoints.py``): a repack file already there (the best precision
|
||||||
category folder) is used in place; only a missing one is downloaded from the
|
variant for the requested qtype -- int8_convrot attaches straight onto the
|
||||||
package repo and renamed to that path. The package's
|
convrot8 backend; bf16; nvfp4 -- or anywhere under its category folder) is
|
||||||
text_encoder/ and tokenizer/ are byte-identical to Qwen/Qwen3-VL-4B-Instruct,
|
used in place; only a missing one is downloaded from the package and renamed
|
||||||
so the text encoder loads from the Comfy-Org single file under
|
to that spot (the package files are byte-identical to the repack's bf16
|
||||||
MODELS_PATH/text_encoders/ (downloaded there once when missing) with config
|
ones). Naming the repack itself as ``name_or_path`` downloads its files
|
||||||
and tokenizer from the public Qwen repo.
|
instead. The package's text_encoder/ and tokenizer/ are byte-identical to
|
||||||
|
Qwen/Qwen3-VL-4B-Instruct, so the text encoder loads from the Comfy-Org
|
||||||
|
single file under MODELS_PATH/text_encoders/ (downloaded there once when
|
||||||
|
missing) with config and tokenizer from the public Qwen repo.
|
||||||
|
|
||||||
Flow convention matches ai-toolkit exactly (t=1 noise -> t=0 clean,
|
Flow convention matches ai-toolkit exactly (t=1 noise -> t=0 clean,
|
||||||
target = noise - clean), so no time flips anywhere.
|
target = noise - clean), so no time flips anywhere.
|
||||||
@@ -47,6 +50,7 @@ from toolkit.basic import flush
|
|||||||
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
from toolkit.config_modules import GenerateImageConfig, ModelConfig
|
||||||
from toolkit.metadata import get_meta_for_safetensors
|
from toolkit.metadata import get_meta_for_safetensors
|
||||||
from toolkit.models.base_model import BaseModel
|
from toolkit.models.base_model import BaseModel
|
||||||
|
from toolkit.models.v2.resolver import resolve_comfy_file
|
||||||
from toolkit.paths import MODELS_PATH
|
from toolkit.paths import MODELS_PATH
|
||||||
from toolkit.unloader import FakeTextEncoder
|
from toolkit.unloader import FakeTextEncoder
|
||||||
from toolkit.samplers.custom_flowmatch_sampler import (
|
from toolkit.samplers.custom_flowmatch_sampler import (
|
||||||
@@ -61,6 +65,7 @@ from .src.pipeline import (
|
|||||||
pack_i2i_image_condition,
|
pack_i2i_image_condition,
|
||||||
pack_image_latents,
|
pack_image_latents,
|
||||||
)
|
)
|
||||||
|
from .src.checkpoints import BASE_REPO, COMFY_REPO, PACKAGE_TO_COMFY
|
||||||
from .src.text_encoder import (
|
from .src.text_encoder import (
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_PAD_MULTIPLE,
|
DEFAULT_PAD_MULTIPLE,
|
||||||
@@ -74,12 +79,7 @@ if TYPE_CHECKING:
|
|||||||
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
|
from toolkit.data_transfer_object.data_loader import DataLoaderBatchDTO
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_REPO = "black-forest-labs/flux-3-image-klein-base"
|
DEFAULT_REPO = BASE_REPO
|
||||||
# package file -> ComfyUI-layout location under MODELS_PATH
|
|
||||||
COMFY_FILES = {
|
|
||||||
"model.safetensors": "diffusion_models/flux_3_image_klein_base.safetensors",
|
|
||||||
"ae.safetensors": "vae/flux_3_vae.safetensors",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static flow-matching timeshift used for the training timestep/noise
|
# Static flow-matching timeshift used for the training timestep/noise
|
||||||
# schedule (the package's sampler alpha).
|
# schedule (the package's sampler alpha).
|
||||||
@@ -161,8 +161,12 @@ class Flux3Model(BaseModel):
|
|||||||
return None
|
return None
|
||||||
if os.path.isfile(base) and filename == "model.safetensors":
|
if os.path.isfile(base) and filename == "model.safetensors":
|
||||||
return base # name_or_path points straight at the DiT file
|
return base # name_or_path points straight at the DiT file
|
||||||
if filename in COMFY_FILES:
|
if filename in PACKAGE_TO_COMFY:
|
||||||
return self._comfy_weight(filename, base)
|
return self._comfy_weight(filename, base)
|
||||||
|
if base == COMFY_REPO:
|
||||||
|
if required:
|
||||||
|
raise FileNotFoundError(f"{COMFY_REPO} carries no {filename}")
|
||||||
|
return None
|
||||||
from huggingface_hub import hf_hub_download
|
from huggingface_hub import hf_hub_download
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -176,43 +180,47 @@ class Flux3Model(BaseModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _comfy_weight(self, filename: str, repo_id: str) -> str:
|
def _comfy_weight(self, filename: str, repo_id: str) -> str:
|
||||||
"""``MODELS_PATH/<category>/<local name>`` (or the local name anywhere
|
"""The comfy-layout file for a package weight: ``MODELS_PATH/<rel>``
|
||||||
under the category folder); a miss downloads the package file and
|
(or its name anywhere under the category folder) wins; a miss
|
||||||
renames it to that spot."""
|
downloads the repack's file there, or, for the vendor package,
|
||||||
from toolkit.models.v2.resolver import find_file_recursive
|
downloads the package file and renames it to the same spot."""
|
||||||
|
rel_path = PACKAGE_TO_COMFY[filename]
|
||||||
rel_path = COMFY_FILES[filename]
|
found = resolve_comfy_file(
|
||||||
target = os.path.join(MODELS_PATH, rel_path)
|
rel_path,
|
||||||
if os.path.exists(target):
|
COMFY_REPO,
|
||||||
return target
|
hf_token=os.getenv("HF_TOKEN", None),
|
||||||
found = find_file_recursive(
|
status_fn=self.print_and_status_update,
|
||||||
os.path.join(MODELS_PATH, os.path.dirname(rel_path)),
|
local_only=repo_id != COMFY_REPO,
|
||||||
os.path.basename(rel_path),
|
|
||||||
)
|
)
|
||||||
if found is not None:
|
if found is not None:
|
||||||
return found
|
return found
|
||||||
from huggingface_hub import hf_hub_download
|
from huggingface_hub import hf_hub_download
|
||||||
|
|
||||||
|
target = os.path.join(MODELS_PATH, rel_path)
|
||||||
self.print_and_status_update(
|
self.print_and_status_update(
|
||||||
f"Downloading {filename} from {repo_id} to {target}"
|
f"Downloading {filename} from {repo_id} to {target}"
|
||||||
)
|
)
|
||||||
category_dir = os.path.dirname(target)
|
# staged in a private subfolder (same filesystem, so the rename is a
|
||||||
os.makedirs(category_dir, exist_ok=True)
|
# move): the package names collide with other models' comfy files
|
||||||
|
# (vae/ae.safetensors is the FLUX.1 VAE) and must never land there
|
||||||
|
staging = os.path.join(os.path.dirname(target), ".flux3_download")
|
||||||
|
os.makedirs(staging, exist_ok=True)
|
||||||
path = hf_hub_download(
|
path = hf_hub_download(
|
||||||
repo_id=repo_id,
|
repo_id=repo_id,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
token=os.getenv("HF_TOKEN", None),
|
token=os.getenv("HF_TOKEN", None),
|
||||||
local_dir=category_dir,
|
local_dir=staging,
|
||||||
)
|
)
|
||||||
os.replace(path, target)
|
os.replace(path, target)
|
||||||
# the hub leaves download metadata under <local_dir>/.cache/huggingface
|
shutil.rmtree(staging, ignore_errors=True)
|
||||||
shutil.rmtree(os.path.join(category_dir, ".cache"), ignore_errors=True)
|
|
||||||
return target
|
return target
|
||||||
|
|
||||||
def _load_package_config(self) -> Optional[dict]:
|
def _load_package_config(self) -> Optional[dict]:
|
||||||
if not self._package_config_checked:
|
if not self._package_config_checked:
|
||||||
self._package_config_checked = True
|
self._package_config_checked = True
|
||||||
path = self._package_file("config.json", required=False)
|
path = None
|
||||||
|
if self.model_config.name_or_path != COMFY_REPO: # the repack has none
|
||||||
|
path = self._package_file("config.json", required=False)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
self.package_config = json.load(f)
|
self.package_config = json.load(f)
|
||||||
@@ -247,7 +255,6 @@ class Flux3Model(BaseModel):
|
|||||||
def _load_transformer(self) -> Flux3Transformer:
|
def _load_transformer(self) -> Flux3Transformer:
|
||||||
dtype = self.torch_dtype
|
dtype = self.torch_dtype
|
||||||
self.print_and_status_update("Loading transformer (Flux3Transformer)")
|
self.print_and_status_update("Loading transformer (Flux3Transformer)")
|
||||||
dit_path = self._package_file("model.safetensors")
|
|
||||||
params = self._transformer_params()
|
params = self._transformer_params()
|
||||||
cfg = self._load_package_config()
|
cfg = self._load_package_config()
|
||||||
if cfg is not None and cfg.get("quantization", None):
|
if cfg is not None and cfg.get("quantization", None):
|
||||||
@@ -262,25 +269,27 @@ class Flux3Model(BaseModel):
|
|||||||
"use the base package."
|
"use the base package."
|
||||||
)
|
)
|
||||||
|
|
||||||
state_dict = load_file(dit_path)
|
qtype = self.model_config.qtype if self.model_config.quantize else None
|
||||||
state_dict = {
|
base = self.model_config.name_or_path
|
||||||
k: (v.to(dtype) if v.is_floating_point() else v)
|
source = None
|
||||||
for k, v in state_dict.items()
|
if base in Flux3Transformer.aitk_comfy_weight_names:
|
||||||
}
|
source = base # the repack named outright: downloaded when missing
|
||||||
with torch.device("meta"):
|
elif base == BASE_REPO:
|
||||||
transformer = Flux3Transformer(params)
|
# a repack file already on disk (best precision variant for the
|
||||||
needed = set(transformer.state_dict().keys())
|
# qtype) wins over the package's own DiT; nothing is downloaded
|
||||||
missing = sorted(needed - set(state_dict.keys()))
|
# from the repack
|
||||||
if missing:
|
source = Flux3Transformer.resolve_comfy_weights(
|
||||||
raise ValueError(f"FLUX.3 checkpoint missing required keys: {missing[:8]}")
|
COMFY_REPO, qtype=qtype, local_only=True
|
||||||
result = transformer.load_state_dict(state_dict, strict=False, assign=True)
|
|
||||||
if result.unexpected_keys:
|
|
||||||
self.print_and_status_update(
|
|
||||||
f" - ignoring {len(result.unexpected_keys)} unexpected checkpoint "
|
|
||||||
f"keys (e.g. {result.unexpected_keys[:3]})"
|
|
||||||
)
|
)
|
||||||
del state_dict
|
if source is None:
|
||||||
flush()
|
source = self._package_file("model.safetensors")
|
||||||
|
transformer = Flux3Transformer.load_model(
|
||||||
|
source,
|
||||||
|
dtype=dtype,
|
||||||
|
config=params,
|
||||||
|
qtype=qtype,
|
||||||
|
quantize_on_load=False, # qtype ranks the comfy candidates only
|
||||||
|
)
|
||||||
|
|
||||||
attn_backend = self.model_config.model_kwargs.get("attention_backend", None)
|
attn_backend = self.model_config.model_kwargs.get("attention_backend", None)
|
||||||
if attn_backend is not None:
|
if attn_backend is not None:
|
||||||
@@ -314,8 +323,14 @@ class Flux3Model(BaseModel):
|
|||||||
|
|
||||||
def _load_vae(self) -> Flux3ImageVAE:
|
def _load_vae(self) -> Flux3ImageVAE:
|
||||||
self.print_and_status_update("Loading image VAE")
|
self.print_and_status_update("Loading image VAE")
|
||||||
|
from toolkit.models.v2.vae.flux2_kl import convert_diffusers_state_dict
|
||||||
|
|
||||||
vae = Flux3ImageVAE()
|
vae = Flux3ImageVAE()
|
||||||
vae.load_state_dict(load_file(self._package_file("ae.safetensors")))
|
state_dict = load_file(self._package_file("ae.safetensors"))
|
||||||
|
if any(k.startswith("encoder.down_blocks.") for k in state_dict):
|
||||||
|
# Comfy-Org's FLUX.2 VAE file: same weights, diffusers key layout
|
||||||
|
state_dict = convert_diffusers_state_dict(state_dict)
|
||||||
|
vae.load_state_dict(state_dict)
|
||||||
vae.to(self.vae_torch_dtype).eval().requires_grad_(False)
|
vae.to(self.vae_torch_dtype).eval().requires_grad_(False)
|
||||||
return vae
|
return vae
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Where FLUX.3 image-klein weights come from.
|
||||||
|
|
||||||
|
BASE_REPO, the vendor package (model.safetensors + ae.safetensors +
|
||||||
|
config.json), is the default and the download source. Its files are
|
||||||
|
byte-identical to the bf16 DiT and VAE of COMFY_REPO, BFL's single-file
|
||||||
|
ComfyUI repack, and are stored under the repack's names; a repack file already
|
||||||
|
in the local ComfyUI models folder (int8_convrot = the toolkit's convrot8
|
||||||
|
storage, attached as-is; bf16; nvfp4) is used instead of the package's.
|
||||||
|
Nothing is downloaded from the repack unless it is named as `name_or_path`.
|
||||||
|
The repack's ``_sd_`` files are the step-distilled flow-map model: never
|
||||||
|
candidates, and a loaded one is rejected by its ``ref_time_in`` keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BASE_REPO = "black-forest-labs/flux-3-image-klein-base"
|
||||||
|
COMFY_REPO = "black-forest-labs/flux-3-image-klein-comfy"
|
||||||
|
|
||||||
|
# repo-relative comfy DiT files; the resolver orders them by the requested
|
||||||
|
# qtype (convrot8 -> int8, nvfp4 -> nvfp4, anything else -> bf16). The text
|
||||||
|
# encoder stays on the Comfy-Org Qwen3-VL repack (src/text_encoder.py).
|
||||||
|
COMFY_TRANSFORMER_FILES = [
|
||||||
|
"diffusion_models/flux3_image_klein_int8_convrot.safetensors",
|
||||||
|
"diffusion_models/flux3_image_klein_bf16.safetensors",
|
||||||
|
"diffusion_models/flux3_image_klein_nvfp4.safetensors",
|
||||||
|
]
|
||||||
|
# same weights as Comfy-Org's FLUX.2 VAE file of the same name (that one is in
|
||||||
|
# the diffusers key layout; the loader accepts both)
|
||||||
|
COMFY_VAE_FILE = "vae/flux2-vae.safetensors"
|
||||||
|
|
||||||
|
# vendor package file -> its comfy-layout location (same bytes as the repack)
|
||||||
|
PACKAGE_TO_COMFY = {
|
||||||
|
"model.safetensors": "diffusion_models/flux3_image_klein_bf16.safetensors",
|
||||||
|
"ae.safetensors": COMFY_VAE_FILE,
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ from torch.utils.checkpoint import checkpoint
|
|||||||
|
|
||||||
from toolkit.models.v2._mixin import OstrisModelMixin
|
from toolkit.models.v2._mixin import OstrisModelMixin
|
||||||
|
|
||||||
|
from .checkpoints import COMFY_REPO, COMFY_TRANSFORMER_FILES
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from flash_attn_interface import flash_attn_func
|
from flash_attn_interface import flash_attn_func
|
||||||
|
|
||||||
@@ -383,7 +385,40 @@ class SingleStreamBlock(nn.Module):
|
|||||||
return ctx + mod_gate_ctx * ctx_out, x + mod_gate_x * x_out
|
return ctx + mod_gate_ctx * ctx_out, x + mod_gate_x * x_out
|
||||||
|
|
||||||
|
|
||||||
|
_QKV = ("q_proj", "k_proj", "v_proj")
|
||||||
|
|
||||||
|
|
||||||
class Flux3Transformer(nn.Module, OstrisModelMixin):
|
class Flux3Transformer(nn.Module, OstrisModelMixin):
|
||||||
|
aitk_comfy_repo = COMFY_REPO
|
||||||
|
aitk_comfy_weight_names = {COMFY_REPO: COMFY_TRANSFORMER_FILES}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_state_dict_on_load(cls, state_dict):
|
||||||
|
if any(k.startswith("ref_time_in.") for k in state_dict):
|
||||||
|
raise ValueError(
|
||||||
|
"this is the step-distilled (flow-map) FLUX.3 checkpoint; use the "
|
||||||
|
"base model (flux3_image_klein_* without _sd_)"
|
||||||
|
)
|
||||||
|
from toolkit.util.comfy_quant_import import split_fused_quantized_keys
|
||||||
|
|
||||||
|
state_dict = dict(state_dict)
|
||||||
|
# the int8 repack fuses every block's q_proj/k_proj/v_proj into one qkv
|
||||||
|
# entry (rows q, k, v); the module (and the bf16/nvfp4 files) keep them
|
||||||
|
# split
|
||||||
|
for marker in [k for k in list(state_dict) if k.endswith(".qkv.comfy_quant")]:
|
||||||
|
base = marker[: -len(".qkv.comfy_quant")]
|
||||||
|
split_fused_quantized_keys(
|
||||||
|
state_dict, f"{base}.qkv", [f"{base}.{p}" for p in _QKV]
|
||||||
|
)
|
||||||
|
for key in [k for k in list(state_dict) if k.endswith(".qkv.weight")]:
|
||||||
|
base = key[: -len(".qkv.weight")]
|
||||||
|
for p, w in zip(_QKV, torch.chunk(state_dict.pop(key), 3, dim=0)):
|
||||||
|
state_dict[f"{base}.{p}.weight"] = w.contiguous()
|
||||||
|
# static activation scales (calibration extras) are unused
|
||||||
|
for k in [k for k in state_dict if k.endswith(".input_scale")]:
|
||||||
|
state_dict.pop(k)
|
||||||
|
return state_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_transformer_block_names(cls):
|
def get_transformer_block_names(cls):
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -42,6 +42,30 @@ class BasicModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMix
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _markers_from_header(
|
||||||
|
file_path: str, state_dict: Dict[str, torch.Tensor]
|
||||||
|
) -> Dict[str, torch.Tensor]:
|
||||||
|
"""ComfyUI's header-style quantized layout keeps the per-layer configs in
|
||||||
|
the safetensors metadata (``_quantization_metadata``) instead of
|
||||||
|
``<layer>.comfy_quant`` marker tensors; synthesize the markers so the
|
||||||
|
importer sees one layout."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from safetensors import safe_open
|
||||||
|
|
||||||
|
with safe_open(file_path, "pt") as f:
|
||||||
|
raw = (f.metadata() or {}).get("_quantization_metadata")
|
||||||
|
if raw is None:
|
||||||
|
return state_dict
|
||||||
|
for name, conf in json.loads(raw).get("layers", {}).items():
|
||||||
|
marker = f"{name}.comfy_quant"
|
||||||
|
if marker not in state_dict and f"{name}.weight" in state_dict:
|
||||||
|
state_dict[marker] = torch.tensor(
|
||||||
|
list(json.dumps(conf).encode("utf-8")), dtype=torch.uint8
|
||||||
|
)
|
||||||
|
return state_dict
|
||||||
|
|
||||||
|
|
||||||
def adopt_component(module: torch.nn.Module, wrapper_cls: type) -> torch.nn.Module:
|
def adopt_component(module: torch.nn.Module, wrapper_cls: type) -> torch.nn.Module:
|
||||||
"""Rebind an already-loaded component instance onto its v2 wrapper class
|
"""Rebind an already-loaded component instance onto its v2 wrapper class
|
||||||
(in-place class swap, like the OstrisLinear conversion). Used when a
|
(in-place class swap, like the OstrisLinear conversion). Used when a
|
||||||
@@ -584,7 +608,7 @@ class OstrisModelMixin:
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
cls._readahead(file_path)
|
cls._readahead(file_path)
|
||||||
state_dict = load_file(file_path)
|
state_dict = _markers_from_header(file_path, load_file(file_path))
|
||||||
return cls.load_from_state_dict(
|
return cls.load_from_state_dict(
|
||||||
state_dict,
|
state_dict,
|
||||||
dtype,
|
dtype,
|
||||||
|
|||||||
Reference in new issue
Block a user