Skip to content

feat: t5 encoder gguf support - #9324

Merged
lstein merged 12 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/t5-encoder-gguf-support
Jul 24, 2026
Merged

feat: t5 encoder gguf support#9324
lstein merged 12 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/t5-encoder-gguf-support

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Feature: Load GGUF-quantized T5 text encoders, and show/recall the T5 encoder in image metadata.

Adds support for single-file GGUF T5 text encoders (e.g. city96/t5-v1_1-xxl-encoder-gguf, llama.cpp enc.blk.* naming) so users can run FLUX/SD3 with a small quantized T5 instead of the full ~9GB encoder. The GGUF infrastructure already existed (used by FLUX/Qwen3/Z-Image); this wires T5 into it, mirroring the existing Qwen3 GGUF encoder path.

Backend

  • New T5Encoder_GGUF_Config (single-file; detects enc.blk.* keys + GGML tensors) registered in the AnyModelConfig union.
  • New T5EncoderGGUFModel loader: remaps llama.cpp T5 keys → transformers T5, infers T5Config from tensor shapes, keeps transformer weights as GGMLTensors for the autocast cache, eagerly dequantizes the token/relative-attention-bias embeddings (embedding lookups can't run on quantized tensors) and ties encoder.embed_tokensshared.
  • transformers T5 gotcha: T5DenseGatedActDense.forward casts activations to self.wo.weight.dtype unless it's torch.int8 (a bitsandbytes guard). GGML weights are torch.uint8, which slips past the guard and corrupts the feed-forward output. Worked around by rebinding the FF forward to only cast for floating-point wo weights.
  • Made the Qwen3 GGUF/checkpoint configs reject T5 encoders (both carry token_embd.weight; the config factory resolves multi-matches from a set, so they must be mutually exclusive — disambiguated on the enc.blk.* prefix).
  • Reuse the T5-XXL tokenizer already vendored in the repo instead of downloading it — moved it out of backend/anima into a neutral shared backend/t5 module used by both Anima and the GGUF loader (updated pyproject.toml package-data accordingly).

Frontend

  • FLUX/SD3 images already store the T5 encoder in metadata (t5_encoder), but it wasn't shown in the Recall Parameters tab. Added a T5EncoderModel metadata handler (mirrors Qwen3EncoderModel) and registered it in both the handler registry (for "Recall All") and the metadata viewer's display list, plus an i18n label.

Related Issues / Discussions

https://discord.com/channels/1020123559063990373/1149510134058471514/1521658213836001291
#8421

QA Instructions

  1. Download a T5 GGUF encoder (e.g. city96/t5-v1_1-xxl-encoder-Q6_K.gguf) and install it via the Model Manager — it should be detected as a T5 Encoder (GGUF) model.
  2. Build a FLUX text-to-image graph using it as the T5 encoder and generate an image; output should be coherent.
  3. Confirm a lighter quant (e.g. Q3_K_S) also loads and generates.
  4. Open a FLUX/SD3 image's metadata → Recall Parameters tab: a T5 Encoder row appears with a recall button; clicking it (and "Recall All") sets the T5 encoder in the generation settings.

Validated locally on Q3_K_S and Q6_K: unique model classification, correct config inference (T5 v1.1 XXL), finite bf16 forward on CUDA, and cross-quant cosine-similarity 0.94–0.999 on content tokens (confirms the key mapping). Backend config/probe tests and frontend metadata tests pass.

Merge Plan

Standard merge. No DB schema or redux migration changes. pyproject.toml package-data path changed (invokeai.backend.animainvokeai.backend.t5) — a clean build picks up the vendored tokenizer at its new location.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration — N/A (no slice shape changes)
  • Documentation added / updated (if applicable) — N/A
  • Updated What's New copy (if doing a release after this PR)

Add loading support for single-file GGUF T5 encoders (e.g.
city96/t5-v1_1-xxl-encoder-gguf, llama.cpp naming), mirroring the
existing Qwen3 GGUF encoder path.

- Add T5Encoder_GGUF_Config (single-file, detects enc.blk.* keys +
  GGML tensors) and register it in the AnyModelConfig union
- Add T5EncoderGGUFModel loader: remaps llama.cpp T5 keys to
  transformers naming, infers T5Config from tensor shapes, dequantizes
  token/relative-attention-bias embeddings, ties embed_tokens to shared
- Work around transformers T5DenseGatedActDense casting activations to
  the uint8 GGML weight dtype (int8 guard doesn't cover uint8), which
  would corrupt the feed-forward output
- Reject T5 encoders in the Qwen3 GGUF/checkpoint configs so the two
  stay mutually exclusive (both carry token_embd.weight; the factory
  resolves multi-matches from a set, so this is not order-safe)

Reuse the vendored T5-XXL tokenizer instead of downloading it: move it
out of Anima into a neutral invokeai/backend/t5 module shared by Anima
and the GGUF loader, and update the package-data path accordingly.
@github-actions github-actions Bot added python PRs that change python files Root invocations PRs that change invocations backend PRs that change backend files frontend PRs that change frontend files python-tests PRs that change python tests python-deps PRs that change python dependencies labels Jul 1, 2026
@Pfannkuchensack Pfannkuchensack changed the title Feat/t5 encoder gguf support feat: t5 encoder gguf support Jul 6, 2026
@lstein lstein self-assigned this Jul 10, 2026
@lstein lstein added the 6.14.0 label Jul 10, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Jul 10, 2026
lstein and others added 3 commits July 23, 2026 20:57
…support

# Conflicts:
#	invokeai/frontend/web/src/services/api/schema.ts
…FFN patch guard

- Add unit coverage for the pure, high-risk parts of T5EncoderGGUFModel:
  key remapping (_convert_t5_gguf_to_transformers), config inference
  (_infer_t5_config_from_state_dict), and the wo-dtype workaround.
- Make _make_feed_forward_gguf_safe raise if it patches no feed-forward
  modules, so a future transformers class rename fails loudly at load time
  instead of silently corrupting encoder output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssumptions

- Inline tensor.shape in _infer_t5_config_from_state_dict: GGMLTensor.shape
  already returns the dequantized (logical) shape, so the _shape_of helper's
  fallback branch was unreachable. Remove the helper.
- Document that config inference targets the T5 v1.1 XXL family and that the
  hardcoded architectural constants (rel-attention max distance, layer-norm
  epsilon, gated-gelu) are that family's defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lstein

lstein commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Review + merge-to-main update

Brought the branch up to date with main and did a full review of the GGUF T5 encoder support. Summary below.

Merge to main

The only conflict was in the generated invokeai/frontend/web/src/services/api/schema.ts (12 hunks, all the same shape: this branch adds T5Encoder_GGUF_Config, main adds ControlNet_Checkpoint_Anima_Config — both belong in the result). Rather than hand-merge a generated file, I confirmed openapi.json auto-merged correctly with both configs and regenerated schema.ts from it via pnpm typegen. Verified: no conflict markers, both configs present, prettier --check and pnpm lint:tsc clean. The branch is now 0 commits behind main.

Review — overall this is solid

  • Detection is correct and defensive. The T5↔Qwen3 GGUF mutual exclusion (enc.blk.* / enc.output_norm.weight) cleanly separates two formats that share token_embd.weight. Existing qwen3 config tests still pass.
  • Config inferred from tensor shapes rather than hardcoded, with clear per-field errors.
  • Fails loudly on unmapped keys via the meta-tensor check.
  • Tokenizer refactor (backend/anima/backend/t5/, ANIMA_T5_VOCAB_SIZET5_VOCAB_SIZE) is clean — no stale references, pyproject.toml package-data updated.
  • Frontend recall handler fills a real gap — t5_encoder is already written to metadata in buildFLUXGraph.ts.

Fixes I pushed (3 commits)

1. Fail-loud guard in _make_feed_forward_gguf_safe. The method patches T5 feed-forward modules matched by exact class-name string to work around the uint8/int8 wo-dtype bug. If a future transformers renames those classes, the patch would silently no-op and silently corrupt encoder output. It now raises a RuntimeError if zero modules are patched, so the mismatch surfaces at load time instead of in generated images.

2. Unit tests (tests/backend/model_manager/load/test_t5_gguf_loader.py, 16 tests) covering the pure, high-risk helpers that previously had no coverage:

  • _convert_t5_gguf_to_transformers — key remapping (attn→layer.0, ffn→layer.1, top-level keys, value identity, unknown-component passthrough, non-string keys)
  • _infer_t5_config_from_state_dict — correct dims from a synthetic state dict + parametrized "missing required key → ValueError"
  • _make_feed_forward_gguf_safe — FFN forward is rebound; a regression test proving the patched forward does not cast activations to a non-floating (uint8) weight dtype; and the new guard raises when no FFN modules match

The tests bypass the cache-dependent constructor via object.__new__, so they run in ~0.1s with no downloads.

3. Small cleanups. Removed the unreachable fallback branch in _shape_of (GGMLTensor.shape already returns the dequantized/logical shape) and inlined it; documented that config inference targets the T5 v1.1 XXL family and that the hardcoded constants (relative_attention_max_distance, layer_norm_epsilon, gated-gelu) are that family's defaults.

Notes (no change made)

  • compute_dtype=torch.bfloat16 in _load_from_gguf is left as-is: it's intentional, carries the existing HACK(ryand) comment, and matches the sibling FLUX GGUF main loader. Swapping it for self._torch_dtype would be a real behavior change on the dequantization path.
  • The tests surfaced that T5Config ignores the tie_word_embeddings=False the loader passes (forces it back to True). Harmless — T5EncoderModel has no lm_head and the loader re-ties embed_tokensshared manually — so no fix, just flagging it.

Verification: new tests 16/16 pass, combined with tokenizer + qwen3 config tests 25/25; ruff check/format clean; pnpm lint:tsc clean.

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works as advertised.

@lstein
lstein enabled auto-merge (squash) July 24, 2026 01:24
@lstein
lstein merged commit 551363c into invoke-ai:main Jul 24, 2026
17 checks passed
Pfannkuchensack added a commit to Pfannkuchensack/InvokeAI that referenced this pull request Jul 28, 2026
…pport

Resolves conflicts with upstream video generation (invoke-ai#9163), Ideogram 4
(invoke-ai#9303), T5 GGUF encoder (invoke-ai#9324) and the Qwen VAE device fix (invoke-ai#9373).

Notable resolutions:
- qwen_image_latents_to_image: keep the as_qwen_image_vae() reinterpretation
  but adopt upstream's vae_info.compute_device fix (invoke-ai#9373)
- graphBuilderUtils: keep the allow-list isMainModelWithoutUnet predicate,
  which covers wan_model_loader automatically
- generationSettingsVisibility: add 'wan' and 'ideogram-4' to
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.0 backend PRs that change backend files frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-deps PRs that change python dependencies python-tests PRs that change python tests Root

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

2 participants