Skip to content

Commit 80eb0bb

Browse files
kappacommitYour Name4pointohclaudePfannkuchensack
authored
feat - Migrate to Transformers 5.5.4 (invoke-ai#9248)
* Update to Transformers 5.1.0 * remove extra stuff * chore(deps): compel fork + transformers>=5.9.0 + remove override Switches compel from PyPI 2.1.1 to invoke-ai/compel@main fork which supports transformers 5.x. Bumps transformers floor to 5.9.0. Removes the transformers>=5.1.0 uv override that was only needed to bypass compel 2.1.1's <5.0 constraint. NOTE: compel fork pulls notebook dep (full Jupyter stack); flag to maintainer for cleanup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(z_image): resolve rope_theta from rope_parameters for transformers 5.x transformers 5.x no longer exposes rope_theta as a top-level attribute on Qwen3Config; the value is stored in the rope_parameters (and rope_scaling) dict instead. Read it from there with a getattr fallback so the inv_freq buffer is computed from the configured base (1e6 / 256) instead of raising AttributeError. Applies to both the safetensors and GGUF Qwen3 encoder paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(model_manager): replace removed hf_hub get_token_permission with whoami huggingface_hub 1.x removed get_token_permission(). HFTokenHelper.get_status() now validates the token via whoami(), which returns user info for a valid token and raises HfHubHTTPError for an invalid one. Preserves the original three-way status: VALID on success, INVALID on HfHubHTTPError (e.g. 401), UNKNOWN on any other error (e.g. network failure). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): regenerate uv.lock after upstream merge Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(sd3): resolve merge conflict marker, drop T5TokenizerFast The upstream merge left an unresolved conflict marker in _t5_encode and reintroduced T5TokenizerFast. Keep our v5 assertion (T5Tokenizer only) plus upstream's new t5_device logic, and drop the now-dead T5TokenizerFast monkeypatch in the test (the name no longer exists in the module). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: ruff fixes on merge-resolved files - flux_text_encoder.py: drop unused typing.Union (F401) left by v5 import merge - huggingface.py: ruff format (wrap append(SimpleNamespace(...))) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): pin transformers <5.6 (diffusers single-file CLIP incompat) transformers 5.6 flattened CLIPTextModel (removed the self.text_model wrapper, hoisted embeddings/encoder/final_layer_norm to the top level). diffusers' single-file checkpoint loader (create_diffusers_clip_model_from_ldm) still assumes the nested layout, so loading SD1.5 .safetensors checkpoints fails on 5.6+ with 'CLIPTextModel object has no attribute text_model' and, once that read is shimmed, 'Cannot copy out of meta tensor' (weights never populate the flattened model). Pin to >=5.5,<5.6 (last pre-flattening release) which keeps both the single-file and from_pretrained paths working. The invoke-ai/compel fork accepts any 5.x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * @ chore(deps): replace compel fork with official compel 2.4.0 compel 2.4.0 (released 2026-05-30) merges the transformers-5 support that the invoke-ai fork carried (both descend from upstream PR invoke-ai#129), plus the maintainer-reviewed padding rework and added diffusers/T5 smoke coverage. Switch from the git fork to the PyPI release. - pyproject: compel git+main -> compel>=2.4.0,<3 - uv.lock: compel 2.3.1 (git 8f404b45) -> 2.4.0 (pypi) - transformers stays 5.5.4 (satisfies compel >=5,<6 and our <5.6 pin) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @ * chore(uv): update uv.lock --------- Co-authored-by: Your Name <you@example.com> Co-authored-by: 4pointoh <97913726+4pointoh@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev> Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
1 parent 8d8a115 commit 80eb0bb

11 files changed

Lines changed: 1548 additions & 707 deletions

File tree

invokeai/app/invocations/flux_text_encoder.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from contextlib import ExitStack
2-
from typing import Iterator, Literal, Optional, Tuple, Union
2+
from typing import Iterator, Literal, Optional, Tuple
33

44
import torch
5-
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer, T5TokenizerFast
5+
from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer
66

77
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
88
from invokeai.app.invocations.fields import (
@@ -86,7 +86,7 @@ def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
8686
ExitStack() as exit_stack,
8787
):
8888
assert isinstance(t5_text_encoder, T5EncoderModel)
89-
assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
89+
assert isinstance(t5_tokenizer, T5Tokenizer)
9090

9191
# Determine if the model is quantized.
9292
# If the model is quantized, then we need to apply the LoRA weights as sidecar layers. This results in
@@ -186,7 +186,7 @@ def _t5_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelP
186186
def _log_t5_tokenization(
187187
self,
188188
context: InvocationContext,
189-
tokenizer: Union[T5Tokenizer, T5TokenizerFast],
189+
tokenizer: T5Tokenizer,
190190
) -> None:
191191
"""Logs the tokenization of a prompt for a T5-based model like FLUX."""
192192

invokeai/app/invocations/sd3_text_encoder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
CLIPTokenizer,
99
T5EncoderModel,
1010
T5Tokenizer,
11-
T5TokenizerFast,
1211
)
1312

1413
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
@@ -103,7 +102,7 @@ def _t5_encode(self, context: InvocationContext, max_seq_len: int) -> torch.Tens
103102
):
104103
context.util.signal_progress("Running T5 encoder")
105104
assert isinstance(t5_text_encoder, T5EncoderModel)
106-
assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
105+
assert isinstance(t5_tokenizer, T5Tokenizer)
107106
t5_device = get_effective_device(t5_text_encoder)
108107

109108
text_inputs = t5_tokenizer(

invokeai/backend/image_util/safety_checker.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
1111
from PIL import Image, ImageFilter
12-
from transformers import AutoFeatureExtractor
12+
from transformers import AutoImageProcessor
1313

1414
import invokeai.backend.util.logging as logger
1515
from invokeai.app.services.config.config_default import get_config
@@ -36,14 +36,14 @@ def _load_safety_checker(cls):
3636
try:
3737
model_path = get_config().models_path / CHECKER_PATH
3838
if model_path.exists():
39-
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
39+
cls.feature_extractor = AutoImageProcessor.from_pretrained(model_path)
4040
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(model_path)
4141
else:
4242
model_path.mkdir(parents=True, exist_ok=True)
43-
cls.feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id)
44-
cls.feature_extractor.save_pretrained(model_path, safe_serialization=True)
43+
cls.feature_extractor = AutoImageProcessor.from_pretrained(repo_id)
44+
cls.feature_extractor.save_pretrained(model_path)
4545
cls.safety_checker = StableDiffusionSafetyChecker.from_pretrained(repo_id)
46-
cls.safety_checker.save_pretrained(model_path, safe_serialization=True)
46+
cls.safety_checker.save_pretrained(model_path)
4747
except Exception as e:
4848
logger.warning(f"Could not load NSFW checker: {str(e)}")
4949

invokeai/backend/model_manager/load/model_loaders/flux.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
CLIPTextModel,
1414
CLIPTokenizer,
1515
T5EncoderModel,
16-
T5TokenizerFast,
16+
T5Tokenizer,
1717
)
1818

1919
from invokeai.app.services.config.config_default import get_config
@@ -442,7 +442,7 @@ def _load_model(
442442
)
443443
match submodel_type:
444444
case SubModelType.Tokenizer2 | SubModelType.Tokenizer3:
445-
return T5TokenizerFast.from_pretrained(
445+
return T5Tokenizer.from_pretrained(
446446
Path(config.path) / "tokenizer_2", max_length=512, local_files_only=True
447447
)
448448
case SubModelType.TextEncoder2 | SubModelType.TextEncoder3:
@@ -470,8 +470,11 @@ def _load_state_dict_into_t5(cls, model: T5EncoderModel, state_dict: dict[str, t
470470
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False, assign=True)
471471
assert len(unexpected_keys) == 0
472472
assert set(missing_keys) == {"encoder.embed_tokens.weight"}
473-
# Assert that the layers we expect to be shared are actually shared.
474-
assert model.encoder.embed_tokens.weight is model.shared.weight
473+
# Re-tie shared weights. In transformers 5.x, weight tying is implemented at the
474+
# parameter level (via _tie_weights / tie_weights) rather than as a Python object
475+
# alias. load_state_dict(assign=True) replaces parameters in-place, which severs
476+
# the parameter-level tie. Calling tie_weights() re-establishes it.
477+
model.tie_weights()
475478

476479

477480
@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T5Encoder, format=ModelFormat.T5Encoder)
@@ -488,7 +491,7 @@ def _load_model(
488491

489492
match submodel_type:
490493
case SubModelType.Tokenizer2 | SubModelType.Tokenizer3:
491-
return T5TokenizerFast.from_pretrained(
494+
return T5Tokenizer.from_pretrained(
492495
Path(config.path) / "tokenizer_2", max_length=512, local_files_only=True
493496
)
494497
case SubModelType.TextEncoder2 | SubModelType.TextEncoder3:

invokeai/backend/model_manager/load/model_loaders/z_image.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,13 @@ def _load_from_singlefile(
770770
# For rotary embeddings, this is inv_freq which is computed from config
771771
if buffer_name == "inv_freq":
772772
# Compute inv_freq from config (same logic as Qwen3RotaryEmbedding.__init__)
773-
base = qwen_config.rope_theta
773+
# NB: transformers 5.x moved rope_theta into the rope_parameters/rope_scaling dict
774+
rope_params = (
775+
getattr(qwen_config, "rope_parameters", None)
776+
or getattr(qwen_config, "rope_scaling", None)
777+
or {}
778+
)
779+
base = rope_params.get("rope_theta") or getattr(qwen_config, "rope_theta", 1000000.0)
774780
inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
775781
parent.register_buffer(buffer_name, inv_freq.to(model_dtype), persistent=False)
776782
else:
@@ -986,7 +992,13 @@ def _load_from_gguf(
986992

987993
if buffer_name == "inv_freq":
988994
# Compute inv_freq from config - keep on CPU, cache system will move to GPU as needed
989-
base = qwen_config.rope_theta
995+
# NB: transformers 5.x moved rope_theta into the rope_parameters/rope_scaling dict
996+
rope_params = (
997+
getattr(qwen_config, "rope_parameters", None)
998+
or getattr(qwen_config, "rope_scaling", None)
999+
or {}
1000+
)
1001+
base = rope_params.get("rope_theta") or getattr(qwen_config, "rope_theta", 1000000.0)
9901002
inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
9911003
parent.register_buffer(buffer_name, inv_freq.to(dtype=compute_dtype), persistent=False)
9921004
else:

invokeai/backend/model_manager/load/model_util.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import torch
1111
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
1212
from diffusers.schedulers.scheduling_utils import SchedulerMixin
13-
from transformers import CLIPTokenizer, PreTrainedTokenizerBase, T5Tokenizer, T5TokenizerFast
13+
from transformers import CLIPTokenizer, PreTrainedTokenizerBase, T5Tokenizer
1414

1515
from invokeai.backend.image_util.depth_anything.depth_anything_pipeline import DepthAnythingPipeline
1616
from invokeai.backend.image_util.grounding_dino.grounding_dino_pipeline import GroundingDinoPipeline
@@ -64,10 +64,7 @@ def calc_model_size_by_data(logger: logging.Logger, model: AnyModel) -> int:
6464
return 0
6565
elif isinstance(
6666
model,
67-
(
68-
T5TokenizerFast,
69-
T5Tokenizer,
70-
),
67+
T5Tokenizer,
7168
):
7269
# HACK(ryand): len(model) just returns the vocabulary size, so this is blatantly wrong. It should be small
7370
# relative to the text encoder that it's used with, so shouldn't matter too much, but we should fix this at some

invokeai/backend/model_manager/metadata/fetch/huggingface.py

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
import json
1717
import re
1818
from pathlib import Path
19+
from types import SimpleNamespace
1920
from typing import Optional
2021

2122
import requests
22-
from huggingface_hub import hf_hub_url
23+
from huggingface_hub import HfApi, hf_hub_url
24+
from huggingface_hub.errors import RepositoryNotFoundError, RevisionNotFoundError
2325
from pydantic.networks import AnyHttpUrl
2426
from requests.sessions import Session
2527

@@ -46,28 +48,39 @@ def __init__(self, session: Optional[Session] = None):
4648
this module without an internet connection.
4749
"""
4850
self._requests = session or requests.Session()
51+
self._has_custom_session = session is not None
4952

5053
@classmethod
5154
def from_json(cls, json: str) -> HuggingFaceMetadata:
5255
"""Given the JSON representation of the metadata, return the corresponding Pydantic object."""
5356
metadata = HuggingFaceMetadata.model_validate_json(json)
5457
return metadata
5558

56-
def _fetch_model_info(self, repo_id: str, variant: Optional[ModelRepoVariant] = None) -> dict:
57-
"""Fetch model info from HuggingFace API using self._requests session.
58-
59-
This allows the session to be mocked in tests via requests_testadapter.
60-
"""
59+
def _model_info_via_session(self, repo_id: str, variant: Optional[ModelRepoVariant] = None) -> SimpleNamespace:
60+
"""Fetch model info using the injected requests session (for testing/custom backends)."""
61+
params = {"blobs": "true"}
6162
url = f"https://huggingface.co/api/models/{repo_id}"
62-
params: dict[str, str] = {"blobs": "True"}
6363
if variant is not None:
64-
params["revision"] = str(variant)
65-
66-
response = self._requests.get(url, params=params)
67-
if response.status_code == 404:
68-
raise UnknownMetadataException(f"'{repo_id}' not found.")
69-
response.raise_for_status()
70-
return response.json()
64+
url += f"/revision/{variant}"
65+
resp = self._requests.get(url, params=params)
66+
if resp.status_code == 404:
67+
error_code = resp.headers.get("X-Error-Code", "")
68+
if error_code == "RevisionNotFound" or (variant is not None):
69+
raise RevisionNotFoundError(f"Revision '{variant}' not found for repo '{repo_id}'.")
70+
raise RepositoryNotFoundError(f"Repository '{repo_id}' not found.")
71+
resp.raise_for_status()
72+
data = resp.json()
73+
# Convert siblings dicts to SimpleNamespace objects matching HfApi.model_info() shape
74+
siblings = []
75+
for s in data.get("siblings", []):
76+
siblings.append(
77+
SimpleNamespace(
78+
rfilename=s.get("rfilename"),
79+
size=s.get("size") or (s.get("lfs", {}) or {}).get("size"),
80+
lfs=s.get("lfs"),
81+
)
82+
)
83+
return SimpleNamespace(id=data["id"], siblings=siblings)
7184

7285
def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata:
7386
"""Return a HuggingFaceMetadata object given the model's repo_id."""
@@ -81,10 +94,15 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod
8194
repo_id = id.split("::")[0] or id
8295
while not model_info:
8396
try:
84-
model_info = self._fetch_model_info(repo_id, variant)
85-
except UnknownMetadataException:
86-
raise
87-
except requests.HTTPError:
97+
# Use the injected session when provided (supports testing with mock adapters).
98+
# Otherwise use HfApi which uses httpx internally.
99+
if self._has_custom_session:
100+
model_info = self._model_info_via_session(repo_id, variant)
101+
else:
102+
model_info = HfApi().model_info(repo_id=repo_id, files_metadata=True, revision=variant)
103+
except RepositoryNotFoundError as excp:
104+
raise UnknownMetadataException(f"'{repo_id}' not found. See trace for details.") from excp
105+
except RevisionNotFoundError:
88106
if variant is None:
89107
raise
90108
else:
@@ -94,18 +112,15 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod
94112

95113
_, name = repo_id.split("/")
96114

97-
for s in model_info.get("siblings") or []:
98-
rfilename = s.get("rfilename")
99-
size = s.get("size")
100-
assert rfilename is not None
101-
assert size is not None
102-
lfs = s.get("lfs")
115+
for s in model_info.siblings or []:
116+
assert s.rfilename is not None
117+
assert s.size is not None
103118
files.append(
104119
RemoteModelFile(
105-
url=hf_hub_url(repo_id, rfilename, revision=variant or "main"),
106-
path=Path(name, rfilename),
107-
size=size,
108-
sha256=lfs.get("sha256") if lfs else None,
120+
url=hf_hub_url(repo_id, s.rfilename, revision=variant or "main"),
121+
path=Path(name, s.rfilename),
122+
size=s.size,
123+
sha256=s.lfs.get("sha256") if s.lfs else None,
109124
)
110125
)
111126

@@ -132,10 +147,10 @@ def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyMod
132147
)
133148

134149
return HuggingFaceMetadata(
135-
id=model_info["id"],
150+
id=model_info.id,
136151
name=name,
137152
files=files,
138-
api_response=json.dumps(model_info, default=str),
153+
api_response=json.dumps(model_info.__dict__, default=str),
139154
is_diffusers=is_diffusers,
140155
ckpt_urls=ckpt_urls,
141156
)

invokeai/backend/quantization/scripts/quantize_t5_xxl_bnb_llm_int8.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ def load_state_dict_into_t5(model: T5EncoderModel, state_dict: dict):
1515
missing_keys, unexpected_keys = model.load_state_dict(state_dict, strict=False, assign=True)
1616
assert len(unexpected_keys) == 0
1717
assert set(missing_keys) == {"encoder.embed_tokens.weight"}
18-
# Assert that the layers we expect to be shared are actually shared.
19-
assert model.encoder.embed_tokens.weight is model.shared.weight
18+
# Re-tie shared weights. In transformers 5.x, weight tying is implemented at the
19+
# parameter level (via _tie_weights / tie_weights) rather than as a Python object
20+
# alias. load_state_dict(assign=True) replaces parameters in-place, which severs
21+
# the parameter-level tie. Calling tie_weights() re-establishes it.
22+
model.tie_weights()
2023

2124

2225
def main():

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ dependencies = [
3535
# Core generation dependencies, pinned for reproducible builds.
3636
"accelerate",
3737
"bitsandbytes; sys_platform!='darwin'",
38-
"compel==2.1.1",
38+
"compel>=2.4.0,<3",
3939
"diffusers[torch]==0.37.0",
4040
"gguf",
4141
"mediapipe==0.10.14", # needed for "mediapipeface" controlnet model
@@ -53,7 +53,7 @@ dependencies = [
5353
"torch>=2.7.0,<2.8.0; sys_platform == 'darwin'",
5454
"torchsde", # diffusers needs this for SDE solvers, but it is not an explicit dep of diffusers
5555
"torchvision",
56-
"transformers>=4.56.0",
56+
"transformers>=5.5,<5.6",
5757

5858
# Core application dependencies, pinned for reproducible builds.
5959
"fastapi-events",

tests/app/invocations/test_sd3_text_encoder.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ def test_sd3_t5_encode_uses_effective_device(monkeypatch):
140140

141141
monkeypatch.setattr(f"{module_path}.T5EncoderModel", FakeSd3T5Encoder)
142142
monkeypatch.setattr(f"{module_path}.T5Tokenizer", FakeT5Tokenizer)
143-
monkeypatch.setattr(f"{module_path}.T5TokenizerFast", FakeT5Tokenizer)
144143

145144
invocation = Sd3TextEncoderInvocation.model_construct(
146145
clip_l=SimpleNamespace(text_encoder=SimpleNamespace(), tokenizer=SimpleNamespace(), loras=[]),

0 commit comments

Comments
 (0)