Skip to content

Add Siglip2Tokenizer to enforce training-time text preprocessing defaults - #43101

Merged
ArthurZucker merged 21 commits into
huggingface:mainfrom
vaibhav-research:siglip2_text_embedding
Jan 28, 2026
Merged

Add Siglip2Tokenizer to enforce training-time text preprocessing defaults#43101
ArthurZucker merged 21 commits into
huggingface:mainfrom
vaibhav-research:siglip2_text_embedding

Conversation

@vaibhav-research

@vaibhav-research vaibhav-research commented Jan 4, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR makes SigLIP2 text preprocessing explicit and consistent with how the model was trained.

It introduces a model specific tokenizer (Siglip2Tokenizer) that wraps the existing GemmaTokenizer while enforcing SigLIP2’s training-time defaults (lowercasing and fixed padding/truncation to 64 tokens). AutoTokenizer and AutoProcessor are updated to use this tokenizer for SigLIP2 checkpoints, so users get the correct behavior automatically without relying on implicit processor logic.

The underlying tokenization and vocabulary remain unchanged. This is a lightweight wrapper that improves correctness, reproducibility, and clarity, especially for text embedding and retrieval use cases.

This addresses the behavior reported in #43054.

Fixes # 43054

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you read the contributor guideline,
    Pull Request section?
  • Was this discussed/approved via a Github issue or the forum? Please add a link
    to it if that's the case.
  • [] Did you make sure to update the documentation with your changes? Here are the
    documentation guidelines, and
    here are tips on formatting docstrings.
  • Did you write any new necessary tests?

Who can review?

@ArthurZucker @itazap
Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

here are the test results including the one that I added in this PR.

pytest -q tests/models/siglip2 -k processing

tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_call_numpy PASSED         [  4%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_call_numpy_4_channels SKIPPED [  8%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_call_pil PASSED           [ 13%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_call_pytorch PASSED       [ 17%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_can_compile_fast_image_processor SKIPPED [ 21%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_cast_dtype_device PASSED  [ 26%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_fast_is_faster_than_slow SKIPPED [ 30%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_from_and_save_pretrained PASSED [ 34%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_from_dict_with_kwargs PASSED [ 39%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_preprocess_arguments PASSED [ 43%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_properties PASSED [ 47%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_save_load_with_autoimageprocessor PASSED [ 52%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_to_json_file PASSED [ 56%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_image_processor_to_json_string PASSED [ 60%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_init_without_params PASSED [ 65%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_is_fast PASSED            [ 69%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_new_models_require_fast_image_processor PASSED [ 73%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_override_instance_attributes_does_not_affect_other_instances SKIPPED [ 78%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_save_load_fast_slow PASSED [ 82%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_save_load_fast_slow_auto PASSED [ 86%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_slow_fast_equivalence PASSED [ 91%]
tests/models/siglip2/test_image_processing_siglip2.py::Siglip2ImageProcessingTest::test_slow_fast_equivalence_batched PASSED [ 95%]
tests/models/siglip2/test_processing_siglip2.py::Siglip2ProcessorTest::test_siglip2_text_padding_length_64 PASSED [100%]

=================================================== warnings summary ====================================================
<frozen importlib._bootstrap>:241
  <frozen importlib._bootstrap>:241: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute

<frozen importlib._bootstrap>:241
  <frozen importlib._bootstrap>:241: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=============================== 19 passed, 4 skipped, 561 deselected, 2 warnings in 9.15s ===============================

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

I verified locally that SigLIP2 text embeddings behave as expected once the training-time preprocessing is applied (lowercasing + fixed-length padding with max_length=64).

This PR intentionally avoids changing processor defaults and instead documents the correct usage and adds a small test to prevent regressions.

@fancyerii fancyerii left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's ok.

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it should be done on a tokenizer level. Siglip model for example loads a SiglipTokenizer class which has a lower case called on inputs before tokenizing

@itazap can we use SiglipTokenizer here as well or maybe add a special Siglip2Tokenizer class?

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

I think it should be done on a tokenizer level. Siglip model for example loads a SiglipTokenizer class which has a lower case called on inputs before tokenizing

@itazap can we use SiglipTokenizer here as well or maybe add a special Siglip2Tokenizer class?

@zucchini-nlp Thanks for reviewing this PR
SigLIP2 currently uses GemmaTokenizerFast for the text branch (see Siglip2Processor and convert_siglip2_to_hf.py). So IMHO we can’t directly reuse SiglipTokenizer (different tokenization backend).
To avoid relying on user-side lowercasing, I can add automatic lowercasing in Siglip2Processor.call before passing text to the tokenizer (covers the main multimodal usage). If we strongly prefer tokenizer-level like SigLIP v1, we’d likely need a Siglip2TokenizerFast wrapper around GemmaTokenizerFast + AutoTokenizer mapping changes. Let me know which direction you’d prefer.
cc @ArthurZucker @itazap

@ArthurZucker

Copy link
Copy Markdown
Collaborator

I can add automatic lowercasing in Siglip2Processor.call before passing text to the tokenizer (covers the main multimodal usage)

this sounds like the most reasonable solution no?

@vaibhav-research

vaibhav-research commented Jan 5, 2026

Copy link
Copy Markdown
Contributor Author

I can add automatic lowercasing in Siglip2Processor.call before passing text to the tokenizer (covers the main multimodal usage)

this sounds like the most reasonable solution no?

thanks a lot for reviewing the PR @ArthurZucker
Yeah, agreed. Since SigLIP2 currently uses GemmaTokenizerFast, adding lowercasing at the tokenizer level would either affect Gemma globally or require a new tokenizer class + mapping. Implementing it in Siglip2Processor.call keeps the behavior SigLIP2-specific and covers the main multimodal usage. I’ll push a patch + regression test.

@vaibhav-research

vaibhav-research commented Jan 5, 2026

Copy link
Copy Markdown
Contributor Author

I can add automatic lowercasing in Siglip2Processor.call before passing text to the tokenizer (covers the main multimodal usage)

this sounds like the most reasonable solution no?

thanks a lot for reviewing the PR @ArthurZucker Yeah, agreed. Since SigLIP2 currently uses GemmaTokenizerFast, adding lowercasing at the tokenizer level would either affect Gemma globally or require a new tokenizer class + mapping. Implementing it in Siglip2Processor.call keeps the behavior SigLIP2-specific and covers the main multimodal usage. I’ll push a patch + regression test.

Thanks for the guidance @ArthurZucker , this is now addressed

I implemented automatic lowercasing and the SigLIP2 default text padding/truncation directly in SiglipProcessor.call, gated to SigLIP2-branded checkpoints. This avoids changing GemmaTokenizerFast globally or introducing a new tokenizer class, while ensuring SigLIP2 users get the correct preprocessing by default via AutoProcessor.

I have also updated the SigLIP2 docs to clarify the preprocessing expectations and note that these defaults are now handled automatically by the processor and added regression tests covering the following:
• identical outputs for upper/lower-case inputs
• default padding to length 64

please let me know if you’d prefer this logic to live elsewhere, but this seemed like the least intrusive fix.

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

looking for your inputs here as well @ArthurZucker
TIA

@vaibhav-research vaibhav-research changed the title 43054: siglip2 text embedding test and document update 43054: siglip2 text embedding updated to default to lowercase Jan 7, 2026
Comment thread src/transformers/models/siglip/processing_siglip.py Outdated
@vaibhav-research vaibhav-research changed the title 43054: siglip2 text embedding updated to default to lowercase 43054: siglip2 text embedding default to lowercase Jan 9, 2026
@vaibhav-research vaibhav-research changed the title 43054: siglip2 text embedding default to lowercase 43054: Add Siglip2Tokenizer to enforce training-time text preprocessing defaults Jan 9, 2026

@ArthurZucker ArthurZucker 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.

Hey! I really don't think we should do any of the fixes here, but rather on the hub.
We can add a siglip2 tokenizer but it should be explicit that it uses a lowercase normalizer, and should be for all Siglip2 / siglip_2 tokenizer. If its just for 1-2 then it should be updated on the hub directly !

Comment thread src/transformers/models/siglip/processing_siglip.py Outdated
Comment thread src/transformers/models/siglip/processing_siglip.py Outdated
Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
Comment thread src/transformers/models/siglip/processing_siglip.py Outdated
Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
Comment thread tests/models/siglip2/test_processing_siglip2.py Outdated
Comment thread tests/models/siglip2/test_processing_siglip2.py Outdated
Comment thread tests/models/siglip2/test_processing_siglip2.py Outdated
@vaibhav-research

Copy link
Copy Markdown
Contributor Author

Thanks for detailed review of this @zucchini-nlp @ArthurZucker
I got a lot of pointers to fix :)
will address each of these and confirm once done.
thanks again

@vaibhav-research
vaibhav-research force-pushed the siglip2_text_embedding branch 2 times, most recently from e25cc6c to e1aafd1 Compare January 15, 2026 22:42
@vaibhav-research

vaibhav-research commented Jan 15, 2026

Copy link
Copy Markdown
Contributor Author

all the test are passing except the glm_image test which is unrelated to this change. I am not sure why but I tested the same locally in main and PR branch and both were successful .

(transformer) vaibhavpandey@Vaibhavs-MacBook-Pro transformers % git switch main
git reset --hard upstream/main
pytest -q tests/models/glm_image/test_modeling_glm_image.py::GlmImageModelTest::test_prompt_lookup_decoding_matches_greedy_search -vv

git switch siglip2_text_embedding
pytest -q tests/models/glm_image/test_modeling_glm_image.py::GlmImageModelTest::test_prompt_lookup_decoding_matches_greedy_search -vv

Already on 'main'
Your branch is ahead of 'origin/main' by 127 commits.
  (use "git push" to publish your local commits)
HEAD is now at 9eea1b0bd3 [`FA`] Generalize fa config checks and fix flags (#43121)
================================================== test session starts ==================================================
platform darwin -- Python 3.11.9, pytest-8.4.2, pluggy-1.6.0 -- /Users/vaibhavpandey/.pyenv/versions/transformer/bin/python
cachedir: .pytest_cache
hypothesis profile 'default'
rootdir: /Users/vaibhavpandey/Projects/transformers
configfile: pyproject.toml
plugins: anyio-4.12.0, xdist-3.8.0, timeout-2.4.0, order-1.3.0, rerunfailures-15.1, asyncio-1.3.0, rich-0.2.0, hypothesis-6.148.8
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 1 item                                                                                                        

tests/models/glm_image/test_modeling_glm_image.py::GlmImageModelTest::test_prompt_lookup_decoding_matches_greedy_search PASSED [100%]

=================================================== 1 passed in 0.94s ===================================================

Switched to branch 'siglip2_text_embedding'
Your branch is up to date with 'origin/siglip2_text_embedding'.
================================================== test session starts ==================================================
platform darwin -- Python 3.11.9, pytest-8.4.2, pluggy-1.6.0 -- /Users/vaibhavpandey/.pyenv/versions/transformer/bin/python
cachedir: .pytest_cache
hypothesis profile 'default'
rootdir: /Users/vaibhavpandey/Projects/transformers
configfile: pyproject.toml
plugins: anyio-4.12.0, xdist-3.8.0, timeout-2.4.0, order-1.3.0, rerunfailures-15.1, asyncio-1.3.0, rich-0.2.0, hypothesis-6.148.8
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 1 item                                                                                                        

tests/models/glm_image/test_modeling_glm_image.py::GlmImageModelTest::test_prompt_lookup_decoding_matches_greedy_search PASSED [100%]

=================================================== 1 passed in 1.07s ==================================================

please take a look at the PR for the change it is intended to solve. I am not sure why the CI test is failing on glm_image. sorry to bother you guys again
@ArthurZucker @zucchini-nlp

Comment thread src/transformers/models/siglip2/modular_siglip2.py Outdated
@zucchini-nlp

Copy link
Copy Markdown
Member

The failing glm-image tests is supposed to be skipped. I see that there is a typo in test file and it's not being skipped, so feel free to ignore it

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

It looks like the PR is failing in the “CircleCI Failure Summary Comment” GitHub Action, not in CircleCI itself.

What I’m seeing:
• CircleCI jobs complete successfully for this commit.
• The GitHub Action fails while trying to find a collection_job and download failure_summary.json.
• In this run, the CircleCI workflow only contains an empty job, so the artifact lookup returns 404 and the script errors (jq: Cannot iterate over null).

I’ve rebased on latest, and the SigLIP2 changes don’t touch CI or CircleCI config, so this seems like an edge case in the failure-summary workflow. not sure, if I missed anything. @zucchini-nlp

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

hi @zucchini-nlp
just checking in to see if you have some bandwidth to take another look. I had pushed the fixes and addressed the earlier comments.
Let me know if there’s anything else you’d like me to change.
Thanks

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

looks like there is an open issue for t5 test failures, I will try and rebase again when this is fixed. all other tests are passing.
#43374

Comment thread src/transformers/models/siglip2/tokenization_siglip2.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

View the CircleCI Test Summary for this PR:

https://huggingface.co/spaces/transformers-community/circle-ci-viz?pr=43101&sha=9bac1b

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks a lot for iterating on it! The failing test is flaky and seems unrelated

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One tiny comment in docs, to be precise 😄

Comment thread docs/source/en/model_doc/siglip2.md Outdated
@vaibhav-research

Copy link
Copy Markdown
Contributor Author

Thanks a ton @zucchini-nlp and @ArthurZucker I really appreciate the patience and all the helpful nudges along the way 🙏
I’ve pushed the doc tweak and updated the conversion script to use the new Siglip2Tokenizer as suggested.
Whenever you get a chance, would love another look. Thanks again for making this smooth (and for putting up with my many follow-ups 😄)

@zucchini-nlp

Copy link
Copy Markdown
Member

Thanks, we can merge imo. The CI is not feeling well since yesterday so let's wait a bit until it's fixed

@vaibhav-research

Copy link
Copy Markdown
Contributor Author

Thanks, we can merge imo. The CI is not feeling well since yesterday so let's wait a bit until it's fixed

Thanks @zucchini-nlp 🙏 , will wait for CI to recover. I’ll keep an eye on the checks.

@ArthurZucker ArthurZucker 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.

the attribute error should normally prevent the function to be written

backend.normalizer = normalizers.Sequence([normalizers.Lowercase(), backend.normalizer])

def _unk_id(self) -> int:
raise ValueError("_unk_id is not needed for SigLIP2.")

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.

Suggested change
raise ValueError("_unk_id is not needed for SigLIP2.")
raise AttributeError("_unk_id is not needed for SigLIP2.")

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.

Ah shit it does not work one sec

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: auto, siglip2

@ArthurZucker
ArthurZucker merged commit 1be63d9 into huggingface:main Jan 28, 2026
11 of 25 checks passed
@vaibhav-research

Copy link
Copy Markdown
Contributor Author

thanks @ArthurZucker for fixing the last issue and for merging the PR

@ArthurZucker

Copy link
Copy Markdown
Collaborator

for sure, sorry for the delay

Comment on lines +86 to +92
# Persist for save/load + push_to_hub dynamic tokenizer test
if hasattr(self, "init_kwargs") and isinstance(self.init_kwargs, dict):
self.init_kwargs.setdefault("tokenizer_class", self.__class__.__name__)

backend = getattr(self, "_tokenizer", None)
if backend is not None and backend.normalizer is not None:
backend.normalizer = normalizers.Sequence([normalizers.Lowercase(), backend.normalizer])

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.

we should remove that tho

@vaibhav-research vaibhav-research changed the title 43054: Add Siglip2Tokenizer to enforce training-time text preprocessing defaults Add Siglip2Tokenizer to enforce training-time text preprocessing defaults Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants