Instructions to use tencent/WeMM-Embedding-2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use tencent/WeMM-Embedding-2B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="tencent/WeMM-Embedding-2B", trust_remote_code=True)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("tencent/WeMM-Embedding-2B", trust_remote_code=True) model = AutoModelForMultimodalLM.from_pretrained("tencent/WeMM-Embedding-2B", trust_remote_code=True, device_map="auto") - sentence-transformers
How to use tencent/WeMM-Embedding-2B with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("tencent/WeMM-Embedding-2B", trust_remote_code=True) sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
Integrate with Sentence Transformers, restore training-time tokenization on newer transformers
Hello @JUNJIE99 @kekekeke @xiaoxiaoshadiao and team!
Congratulations on this release! As far as I can, this is beating just about any model at any benchmark at the same size, very nice to see! And thanks for shipping wemm_sentence_transformers.py alongside the model. This PR builds on it further so Sentence Transformers can load WeMM-Embedding straight from the repository id, and it picks up two upstream transformers changes that silently alter the inputs this model sees.
As a heads up, this PR is AI-generated but human-reviewed.
Pull Request overview
- Integrate WeMM-Embedding-2B with Sentence Transformers v5.7+ via
modules.jsonandtrust_remote_code - Restore the training-time tokenization on
transformersnewer than the pinned 5.2.0
Details
The pipeline is one module, WeMMTransformer(feature-extraction) -> sentence_embedding, with noPooling or Normalize behind it: WeMMEmbedding.embedding already reads the <embedding> position
and L2-normalizes, so its return value goes straight into sentence_embedding. modality_config mapstext, image, video, image+text, text+video and message to that method, somodel.encode("..."), model.encode({"image": ..., "text": ...}) and raw chat messages all work
through one entry point. config_sentence_transformers.json sets cosine similarity and declares no
prompts, since the instruction belongs in the input text rather than in a fixed prefix.
modeling_st_wemm.py is a Transformer subclass, so batching, prompts, truncation, encode_query /encode_document, similarity and saving stay stock. It overrides only _apply_chat_template,
building the processor inputs through qwen_vl_utils.process_vision_info (with image_patch_size
read from vision_config.patch_size) exactly as the model card's snippet does. That matters mostly
for video, where the processor's own frame sampling picks 114 frames against process_vision_info's
112. Its constructor also seeds the method-signature cache from the inner model's parameters, sinceembedding takes multimodal inputs as **kwargs and would otherwise have pixel_values filtered out.
Two things had to change for the model to see its trained input format on current transformers.
Both were measured to be no-ops on the pinned 5.2.0.
tokenizer_classis nowPreTrainedTokenizerFast. Fromtransformers5.3-ish,TokenizersBackend
rebuilds Qwen2 tokenizers fromvocab/mergesandQwen2Tokenizer.__init__hardcodesnormalizers.NFC(), dropping this repository'sReplace(Regex("\n$"), "")(the normalizer theembedding_chat_template.jinjacomment describes). Without it every input gains a\nbefore<embedding>, and image inputs also gain the role newline the model was trained without.PreTrainedTokenizerFasttakes the loader branch that readstokenizer.jsonverbatim; on 5.2.0 the
two classes tokenize identically (10/10 sample strings, withAutoProcessorstill resolvingQwen3VLProcessorwith the same special-token ids).additional_chat_templates/sentence_transformers.jinjarenders video as a bare<|video_pad|>
rather than<|vision_start|><|video_pad|><|vision_end|>. 5.2.0 replaced the whole wrapped block
with the per-frame<seconds><|vision_start|>...<|vision_end|>expansion; newer versions replace
only<|video_pad|>and leave the outer pair in place, adding a second, unwanted pair of vision
boundaries. The bare form gives byte-identical ids on 5.2.0 and correct ids on newer versions, which
is also whyembedding_chat_template.jinjais bare.chat_template.jinjais untouched, since it
matches the upstream Qwen3-VL convention and is what SGLang reads.
modeling_wemm_embedding.py gains one line: embedding resets self.model.rope_deltas, the same
guard wemm_sentence_transformers.py already carried. Below transformers 5.15 a text-only forward
reuses the deltas cached by the previous multimodal forward, shifting its position ids (max |diff|
0.0015 bf16, on text encoded right after an image); newer versions guard against it. Doing it inembedding rather than in the caller covers the model card's transformers path too.
Verification, in fp32 over text, image, image+text and video inputs: on transformers 5.2.0 the
Sentence Transformers path reproduces AutoModel + AutoProcessor + process_vision_info
bit-exactly, max |diff| 0.0 on every embedding and on the full similarity matrix. Against that same
5.2.0 reference, Sentence Transformers on 5.15.0 produces bit-identical input_ids, attention_mask,mm_token_type_ids, pixel_values, pixel_values_videos and grids, with embeddings within 1.4e-06
and similarities within 8.9e-07 (GPU kernel differences between the two builds). Checked onsentence-transformers 5.7.0 and 6.0.0. Dict inputs, bare URLs, PIL.Image objects, mixed-modality
batches, encode_query / encode_document and truncate_dim were each compared against the
equivalent chat message form.
wemm_sentence_transformers.py is removed, since modules.json supersedes it and also works from a
repository id. Happy to keep it if you would rather not drop it, though it renders video throughchat_template.jinja and so hits the doubled-boundary issue above on current transformers.
The model card's Sentence Transformers section is rewritten around the snippet below, plus asentence-transformers tag, a truncate_dim note under Matryoshka Embeddings, andsentence-transformers>=5.7.0 in the install line.
from sentence_transformers import SentenceTransformer
model_id = "tencent/WeMM-Embedding-2B"
model = SentenceTransformer(model_id, trust_remote_code=True)
queries = [
"Which Llama 4 model variants are available?",
"How is mapo tofu prepared?",
]
documents = [
"Mapo tofu is a Sichuan dish of soft tofu simmered in a spicy, numbing sauce of chili bean paste and Sichuan peppercorn.",
{
"image": "https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/llama4_hgf.png",
"text": "Represent this image.",
},
{
"video": "https://huggingface.co/datasets/sentence-transformers/example-documents/resolve/main/mapo_tofu.mp4",
"text": "Represent this video.",
},
]
query_embeddings = model.encode_query(queries)
document_embeddings = model.encode_document(documents)
print(query_embeddings.shape, document_embeddings.shape)
# (2, 2048) (3, 2048)
similarities = model.similarity(query_embeddings, document_embeddings)
print(similarities)
# tensor([[0.0683, 0.4972, 0.0309],
# [0.7829, 0.1428, 0.5492]])
To try it before merging, load the PR revision:SentenceTransformer(model_id, revision="refs/pr/1", trust_remote_code=True).
Nothing else changes: the transformers, vLLM and SGLang paths are unaffected, and the rest is
purely additive, apart from the wemm_sentence_transformers.py removal.
Happy to tweak anything you'd like changed. Please let me know if you have any questions or feedback!
- Tom Aarsen
Great addition!
That matters mostly for video, where the processor's own frame sampling picks 114 frames against process_vision_info's 112
FYI, qwen_vl_utils reqire video to be a list of PIL.Image or path and not working with torchcodec objects from the box and in mteb we removed it's usage because it's slow pipeline and don't do much more than processor.
I see! If preferred by the Tencent team, I can also avoid the qwen_vl_utils and use the Transformers VL processing, but I don't want to make unprompted changes that slightly change model outputs.
Yes, but this could be a bit unexpected if the user passes a video as a torchcodec, but model would create an error.
Thanks for the PR, @tomaarsen β and thanks @Samoed for the review.
We tested this change and the embedding behaviour is unchanged on the setup our model card recommends (transformers==5.2.0), for both the transformers and the Sentence Transformers paths. We also reproduced the regression this PR fixes: on newer transformers the current repo loses the Replace(Regex("\n$"), "") normalizer, which silently appends a \n token to every input. Merging this for 2B, 4B and 9B.
Two cases where output can still shift, which is why we are not dropping the version pins yet:
- Video through the
transformerssnippet on newertransformers. The bare<|video_pad|>fix lives inadditional_chat_templates/sentence_transformers.jinja, so it only applies to the Sentence Transformers path.chat_template.jinjaβ which SGLang reads, and which this PR correctly leaves untouched β still emits the wrapped form, and newertransformersadds a second pair of vision boundaries around it. Text and image are unaffected. - Frame sampling depends on
qwen_vl_utils. Our published numbers were produced withprocess_vision_info, and the processor's own sampling selects a different set of frames, so we would rather not switch that here. @Samoed 's point abouttorchcodecis fair, so we will document the supported video input types (file path / URL / list ofPIL.Image) in the model card instead of changing the sampling behaviour.
So for reproducible embeddings we will keep recommending the matched versions in the README β transformers==5.2.0, vLLM 0.27.0, SGLang 0.5.9.
cc @kekekeke