Repository: huggingface/transformers
Pull request: Fix #40067: Add dedicated UMT5 support to GGUF loader (config, tokenizer, test)

### PR Description

### Problem

**Fixes #40067**

UMT5EncoderModel.from_pretrained() fails when loading GGUF files, defaulting to T5 configuration and producing the warning:
```
You are using a model of type t5 to instantiate a model of type umt5. This is not supported for all configurations of models and can yield errors.
```

### Solution

Adds dedicated UMT5 architecture support to the GGUF loader:
- **Detection**: Identifies UMT5 models by name pattern in GGUF files
- **Configuration**: Maps GGUF parameters to UMT5Config instead of falling back to T5
- **Integration**: Registers UMT5 with tokenizer conversion and architecture mapping

### Changes

- `src/transformers/modeling_gguf_pytorch_utils.py`: Added UMT5 model detection and config/architecture mapping
- `src/transformers/integrations/ggml.py`: Added UMT5 config mapping and tokenizer support
- `tests/models/t5/test_modeling_t5.py`: Added integration test for UMT5 GGUF loading

### Testing
✅ New integration test validates UMT5EncoderModel loads correctly from GGUF
✅ Verifies proper config type (UMT5Config, not T5Config)
✅ Confirms weights map without tensor mismatches
✅ Ensures forward pass returns valid last_hidden_state

### Impact
Enables users to load UMT5 encoder models from GGUF format without configuration warnings or errors.
This is not only a warning fix — it ensures GGUF UMT5 models load into the correct architecture + tokenizer, preventing subtle runtime errors.

**Scope**: This patch adds GGUF support for UMT5 encoder models only. Decoder/Seq2Seq GGUF support is out of scope and can be addressed in a separate follow-up.

**Before**: ❌
UMT5EncoderModel.from_pretrained("city96/umt5-xxl-encoder-gguf", gguf_file="...")  # fails
**After**: ✅
UMT5EncoderModel.from_pretrained("city96/umt5-xxl-encoder-gguf", gguf_file="...")  # works correctly


Thanks!

Changed files:
filename: src/transformers/integrations/ggml.py
patch: @@ -250,6 +250,19 @@
         "attention.sliding_window": "sliding_window",
         "vocab_size": "vocab_size",
     },
+    "umt5": {
+        "context_length": "n_positions",
+        "block_count": "num_layers",
+        "feed_forward_length": "d_ff",
+        "embedding_length": "d_model",
+        "attention.key_length": "d_kv",
+        "attention.head_count": "num_heads",
+        "attention.head_count_kv": "num_key_value_heads",
+        "attention.layer_norm_epsilon": "layer_norm_epsilon",
+        "attention.relative_buckets_count": "relative_attention_num_buckets",
+        "decoder_start_token_id": "decoder_start_token_id",
+        "vocab_size": "vocab_size",
+    },
     "deci": {
         "context_length": "max_position_embeddings",
         "block_count": "num_hidden_layers",
@@ -728,6 +741,7 @@ def converted(self) -> Tokenizer:
     "nemotron": GGUFGPTConverter,
     "gemma2": GGUFGemmaConverter,
     "gemma3_text": GGUFGemmaConverter,
+    "umt5": GGUFT5Converter,
     "deci": GGUFLlamaConverter,
     "decilm": GGUFLlamaConverter,
 }

filename: src/transformers/modeling_gguf_pytorch_utils.py
patch: @@ -300,6 +300,8 @@ def get_gguf_hf_weights_map(
         model_type = "qwen3moe"
     elif model_type == "gemma3_text":
         model_type = "gemma3"
+    elif model_type == "umt5":
+        model_type = "t5"
     arch = None
     for key, value in MODEL_ARCH_NAMES.items():
         if value == model_type:
@@ -386,9 +388,14 @@ def load_gguf_checkpoint(gguf_checkpoint_path, return_tensors=False, model_to_lo
     # It needs to be developed for supporting legacy t5.
     elif "t5" in architecture or "t5encoder" in architecture:
         parsed_parameters["config"]["is_gated_act"] = True
-        if "t5encoder" in architecture:
-            parsed_parameters["config"]["architectures"] = ["T5EncoderModel"]
-        updated_architecture = "t5"
+        if model_name and "umt5" in model_name[0].lower():
+            updated_architecture = "umt5"
+            if "t5encoder" in architecture:
+                parsed_parameters["config"]["architectures"] = ["UMT5EncoderModel"]
+        else:
+            if "t5encoder" in architecture:
+                parsed_parameters["config"]["architectures"] = ["T5EncoderModel"]
+            updated_architecture = "t5"
     else:
         updated_architecture = architecture
 

filename: tests/quantization/ggml/test_ggml.py
patch: @@ -16,7 +16,14 @@
 
 from parameterized import parameterized
 
-from transformers import AddedToken, AutoModelForCausalLM, AutoModelForSeq2SeqLM, AutoTokenizer
+from transformers import (
+    AddedToken,
+    AutoModelForCausalLM,
+    AutoModelForSeq2SeqLM,
+    AutoTokenizer,
+    UMT5Config,
+    UMT5EncoderModel,
+)
 from transformers.testing_utils import (
     require_gguf,
     require_read_token,
@@ -303,6 +310,7 @@ class GgufModelTests(unittest.TestCase):
     gemma3_vision_model_id = "unsloth/gemma-3-4b-it-GGUF"
     qwen3_model_id = "Qwen/Qwen3-0.6B-GGUF"
     qwen3moe_model_id = "Qwen/Qwen3-30B-A3B-GGUF"
+    umt5_encoder_model_id = "city96/umt5-xxl-encoder-gguf"
 
     q4_0_phi3_model_id = "Phi-3-mini-4k-instruct-q4.gguf"
     q4_0_mistral_model_id = "mistral-7b-instruct-v0.2.Q4_0.gguf"
@@ -341,6 +349,7 @@ class GgufModelTests(unittest.TestCase):
     fp16_deci_model_id = "decilm-7b-uniform-gqa-f16.gguf"
     q8_0_qwen3_model_id = "Qwen3-0.6B-Q8_0.gguf"
     q4_k_m_qwen3moe_model_id = "Qwen3-30B-A3B-Q4_K_M.gguf"
+    q8_0_umt5_encoder_model_id = "umt5-xxl-encoder-Q8_0.gguf"
 
     example_text = "Hello"
 
@@ -1072,3 +1081,38 @@ def test_qwen3moe_q4_k_m(self):
 
         EXPECTED_TEXT = "Hello, I am a 20 year old male"
         self.assertEqual(tokenizer.decode(out[0], skip_special_tokens=True), EXPECTED_TEXT)
+
+    def test_umt5_encoder_q8_0(self):
+        """
+        Verifies that a UMT5 encoder loads directly from a GGUF file using
+        UMT5EncoderModel.from_pretrained(...), and the config is correctly UMT5.
+        """
+        model = UMT5EncoderModel.from_pretrained(
+            self.umt5_encoder_model_id,
+            gguf_file=self.q8_0_umt5_encoder_model_id,
+            dtype=torch.float16,
+            device_map="auto",
+        )
+        model.eval()
+
+        self.assertIsInstance(model, UMT5EncoderModel)
+        self.assertIsInstance(model.config, UMT5Config)
+        self.assertEqual(model.config.model_type, "umt5")
+        self.assertIn("UMT5EncoderModel", getattr(model.config, "architectures", []))
+
+        input_ids = torch.tensor([[1, 2, 3, 4]], dtype=torch.long).to(torch_device)
+        with torch.no_grad():
+            outputs = model(input_ids=input_ids)
+
+        self.assertTrue(hasattr(outputs, "last_hidden_state"))
+        self.assertEqual(outputs.last_hidden_state.dim(), 3)  # (batch, seq_len, hidden)
+
+        EXPECTED_OUTPUT = torch.tensor(
+            [
+                [-0.0010, -0.0145, 0.0133],
+                [-0.0006, 0.1814, 0.1132],
+                [0.0005, 0.0083, -0.0285],
+            ]
+        ).to(torch_device)
+
+        torch.testing.assert_close(outputs.last_hidden_state[0, :3, :3], EXPECTED_OUTPUT, rtol=6e-3, atol=4e-4)