Repository: huggingface/transformers
Pull request: Fix missing fast image patch counter in Glm46V

## Summary
Fixes #43874 by adding `get_number_of_image_patches` to `Glm46VImageProcessorFast`.

`Glm46VProcessor._get_num_multimodal_tokens` calls this method on `self.image_processor`, which raises an `AttributeError` when the fast image processor is used because the method was missing.

## Changes
- Added `Glm46VImageProcessorFast.get_number_of_image_patches(...)` in `modular_glm46v.py`.
- Implemented it by delegating to the slow processor implementation (`Glm46VImageProcessor`) to keep fast/slow behavior identical.
- Regenerated `image_processing_glm46v_fast.py` from modular source.
- Added regression test `Glm46VImageProcessorFastTest.test_get_number_of_image_patches_matches_slow_processor`.

## Validation
- `PYTHONPATH=src python3 utils/modular_model_converter.py glm46v`
- `PYTHONPATH=src python3 utils/check_modular_conversion.py --files src/transformers/models/glm46v/modular_glm46v.py`
- `PYTHONPATH=src python3 -m ruff check src/transformers/models/glm46v/modular_glm46v.py src/transformers/models/glm46v/image_processing_glm46v_fast.py tests/models/glm46v/test_processor_glm46v.py`
- `PYTHONPATH=src python3 -m pytest tests/models/glm46v/test_processor_glm46v.py::Glm46VImageProcessorFastTest::test_get_number_of_image_patches_matches_slow_processor -q`
- `PYTHONPATH=src python3 -m trace --count --summary --module unittest tests.models.glm46v.test_processor_glm46v.Glm46VImageProcessorFastTest.test_get_number_of_image_patches_matches_slow_processor | grep -E "image_processing_glm46v_fast|test_processor_glm46v"`
  - output includes `image_processing_glm46v_fast ... 100%`


Changed files:
filename: src/transformers/models/ernie4_5_vl_moe/image_processing_ernie4_5_vl_moe_fast.py
patch: @@ -190,14 +190,6 @@ def _preprocess(
             data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
         )
 
-    @auto_docstring
-    def preprocess(
-        self,
-        images: ImageInput,
-        **kwargs: Unpack[Ernie4_5_VL_MoeImageProcessorKwargs],
-    ) -> BatchFeature:
-        return super().preprocess(images, **kwargs)
-
     def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
         """
         A utility that returns number of image patches for a given image size.
@@ -227,5 +219,13 @@ def get_number_of_image_patches(self, height: int, width: int, images_kwargs=Non
         grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
         return grid_h * grid_w
 
+    @auto_docstring
+    def preprocess(
+        self,
+        images: ImageInput,
+        **kwargs: Unpack[Ernie4_5_VL_MoeImageProcessorKwargs],
+    ) -> BatchFeature:
+        return super().preprocess(images, **kwargs)
+
 
 __all__ = ["Ernie4_5_VL_MoeImageProcessorFast"]

filename: src/transformers/models/glm46v/image_processing_glm46v_fast.py
patch: @@ -29,7 +29,7 @@
 from ...image_utils import OPENAI_CLIP_MEAN, OPENAI_CLIP_STD, ImageInput, PILImageResampling, SizeDict
 from ...processing_utils import Unpack
 from ...utils import TensorType, auto_docstring
-from .image_processing_glm46v import Glm46VImageProcessorKwargs, smart_resize
+from .image_processing_glm46v import Glm46VImageProcessor, Glm46VImageProcessorKwargs, smart_resize
 
 
 @auto_docstring
@@ -171,6 +171,9 @@ def _preprocess(
             data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
         )
 
+    def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
+        return Glm46VImageProcessor.get_number_of_image_patches(self, height, width, images_kwargs)
+
     @auto_docstring
     def preprocess(
         self,

filename: src/transformers/models/glm4v/image_processing_glm4v_fast.py
patch: @@ -39,7 +39,7 @@
     auto_docstring,
     logging,
 )
-from .image_processing_glm4v import Glm4vImageProcessorKwargs, smart_resize
+from .image_processing_glm4v import Glm4vImageProcessor, Glm4vImageProcessorKwargs, smart_resize
 
 
 logger = logging.get_logger(__name__)
@@ -184,6 +184,9 @@ def _preprocess(
             data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
         )
 
+    def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
+        return Glm4vImageProcessor.get_number_of_image_patches(self, height, width, images_kwargs)
+
     @auto_docstring
     def preprocess(
         self,

filename: tests/models/glm4v/test_image_processing_glm4v.py
patch: @@ -252,3 +252,26 @@ def test_call_numpy_4_channels(self):
             ).pixel_values
             expected_output_image_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
             self.assertEqual(tuple(encoded_images.shape), expected_output_image_shape)
+
+    def test_get_number_of_image_patches_matches_slow_processor(self):
+        test_cases = [
+            (100, 100, {}, 64),
+            (200, 50, {}, 56),
+            (100, 100, {"patch_size": 28}, 16),
+        ]
+
+        for image_processing_class in self.image_processor_list:
+            image_processor = image_processing_class(**self.image_processor_dict)
+            for height, width, images_kwargs, expected in test_cases:
+                with self.subTest(
+                    image_processing_class=image_processing_class.__name__,
+                    height=height,
+                    width=width,
+                    images_kwargs=images_kwargs,
+                ):
+                    self.assertEqual(
+                        image_processor.get_number_of_image_patches(
+                            height=height, width=width, images_kwargs=images_kwargs
+                        ),
+                        expected,
+                    )