Model reference · open weights

multi-modal-embed-small

Embeddings llm-semantic-router Embeddings 1 build Open weights 3k dl/mo

multi-modal-embed-small is an open-weight embedding model from llm-semantic-router. multi-modal-embed-small (FP32) weighs 675 MB; the smallest configuration that runs it is RTX 3060 12 GB.

  • multi-modal-embed-small is a compact multimodal embedding model by llm-semantic-router designed for sentence-similarity tasks across text, image, and audio modalities.
  • The model features 338M parameters, supports English, and generates 384-dimensional embeddings with Matryoshka Representation Learning for flexible dimension truncation.
  • It is released under the Apache 2.0 license.

Summary of the llm-semantic-router/multi-modal-embed-small model card, 2026-10-01

What it is

Released byllm-semantic-router
Released2026-02-05
Parameters338M
VRAM675 MB for the weights

What it runs on

Memory and cards for multi-modal-embed-small (FP32)

675 MBweights, file size
1.1 GBruntime overhead
CardRunsMemory
RTX 3060 12 GBfits11.6 GB
RTX 4060 Ti 16 GBfits15.4 GB
RTX 3090 24 GBfits23.4 GB
RTX 4090 24 GBfits23.4 GB
RTX 5090 32 GBfits31.0 GB
L40S 48 GBfits44.0 GB
A100 80 GBfits78.2 GB
H100 80 GBfits78.1 GB
RTX PRO 6000 Blackwell 96 GBfits93.8 GB
DGX Spark (GB10) 128 GB unifiedfits107 GB
H200 141 GBfits138 GB
B200 180 GBfits176 GB

From the model card

What llm-semantic-router says about multi-modal-embed-small

Read the model card

A compact multimodal embedding model that unifies text, image, and audio representations in a shared semantic space. Part of the MoM (Mixture of Models) family.

Model Description

multi-modal-embed-small is a lightweight multimodal encoder (~120M parameters) supporting:

  • Text encoding via MiniLM-L6-v2 (22M params)
  • Image encoding via SigLIP-base-patch16-512 (86M params)
  • Audio encoding via Whisper-tiny encoder (8M params)
  • Cross-modal fusion via 2-layer transformer attention
  • 2DMSE: Two-Dimensional Matryoshka Sentence Embeddings for adaptive compute
  • MRL: Matryoshka Representation Learning for flexible embedding dimensions

Key Features

FeatureDescription
Embedding Dimension384 (supports MRL truncation to 32, 64, 128, 256)
Image Resolution512×512
Audio InputUp to 30s, 16kHz (Whisper Mel spectrogram)
ModalitiesText, Image, Audio, Multimodal fusion
2DMSE SupportEarly exit at any encoder layer
LanguagesEnglish

Installation

pip install torch transformers pillow safetensors

Usage

Load Model

Two checkpoint formats are available:

  • model.pt (932 MB) - PyTorch format
  • model.safetensors (1.35 GB) - SafeTensors format
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer, SiglipModel, SiglipProcessor, WhisperModel, WhisperFeatureExtractor
from huggingface_hub import hf_hub_download

class MultiModalEmbedder(nn.Module):
    """Standalone multimodal embedder - no external dependencies."""

    def __init__(self):
        super().__init__()
        # Text encoder (384d, no projection needed)
        self.text_tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
        self.text_encoder = AutoModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")

        # Image encoder (768d -> 384d projection)
        self.image_processor = SiglipProcessor.from_pretrained("google/siglip-base-patch16-512")
        self.image_encoder = SiglipModel.from_pretrained("google/siglip-base-patch16-512").vision_model
        self.image_proj = nn.Linear(768, 384)

        # Audio encoder (384d, no projection needed)
        self.audio_processor = WhisperFeatureExtractor.from_pretrained("openai/whisper-tiny")
        self.audio_encoder = WhisperModel.from_pretrained("openai/whisper-tiny").encoder

    def encode_text(self, texts):
        if isinstance(texts, str):
            texts = [texts]
        inputs = self.text_tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
        inputs = {k: v.to(next(self.parameters()).device) for k, v in inputs.items()}
        outputs = self.text_encoder(**inputs)
        embeddings = outputs.last_hidden_state.mean(dim=1)  # Mean pooling
        return F.normalize(embeddings, p=2, dim=-1)

    def encode_image(self, images):
        inputs = self.image_processor(images=images, return_tensors="pt")
        inputs = {k: v.to(next(self.parameters()).device) for k, v in inputs.items()}
        outputs = self.image_encoder(**inputs)
        embeddings = outputs.pooler_output
        embeddings = self.image_proj(embeddings)  # 768 -> 384
        return F.normalize(embeddings, p=2, dim=-1)

    def encode_audio(self, waveform):
        # waveform: numpy array or tensor at 16kHz
        if isinstance(waveform, torch.Tensor):
            waveform = waveform.squeeze().numpy()
        inputs = self.audio_processor(waveform, sampling_rate=16000, return_tensors="pt")
        inputs = {k: v.to(next(self.parameters()).device) for k, v in inputs.items()}
        outputs = self.audio_encoder(**inputs)
        embeddings = outputs.last_hidden_state.mean(dim=1)  # Mean pooling
        return F.normalize(embeddings, p=2, dim=-1)

# Load model
model = MultiModalEmbedder()

# Download and load trained weights
checkpoint_path = hf_hub_download(
    repo_id="llm-semantic-router/multi-modal-embed-small",
    filename="model.pt"
)
state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=False)

# Load text encoder weights
model.text_encoder.load_state_dict({
    k.replace("text_encoder.encoder.", ""): v
    for k, v in state_dict.items()
    if k.startswith("text_encoder.encoder.")
})

# Load image encoder and projection weights
model.image_encoder.load_state_dict({
    k.replace("image_encoder.vision_encoder.", ""): v
    for k, v in state_dict.items()
    if k.startswith("image_encoder.vision_encoder.")
})
model.image_proj.load_state_dict({
    k.replace("image_encoder.projection.", ""): v
    for k, v in state_dict.items()
    if k.startswith("image_encoder.projection.")
})

# Load audio encoder weights
model.audio_encoder.load_state_dict({
    k.replace("audio_encoder.encoder.", ""): v
    for k, v in state_dict.items()
    if k.startswith("audio_encoder.encoder.")
})

model.eval()
print("Model loaded successfully!")

Text Embedding

import torch.nn.functional as F

# Single text
text_embedding = model.encode_text("A photo of a cat")  # Shape: [1, 384]

# Batch of texts
texts = ["A fluffy orange cat", "A golden retriever dog", "A red sports car"]
text_embeddings = model.encode_text(texts)  # Shape: [3, 384]

# Compute similarity
similarities = F.cosine_similarity(text_embeddings[0:1], text_embeddings[1:], dim=-1)
print(f"Cat vs Dog: {similarities[0]:.3f}")
print(f"Cat vs Car: {similarities[1]:.3f}")

Image Embedding

from PIL import Image
import requests
from io import BytesIO

# Load image
url = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"
image = Image.open(BytesIO(requests.get(url).content)).convert('RGB')

# Get embedding
image_embedding = model.encode_image(image)  # Shape: [1, 384]

Audio Embedding

import torchaudio

# Load audio (16kHz)
waveform, s

Quoted from the model card on Hugging Face. The full card is behind the Hugging Face link above.

How it works

How embedding models work

Your textsentence / documentEncodermaps meaningVectorlist of numbersAn embedding model turns text into a vector, so similar meanings sit close together, which is the basis of search and RAG.

Benchmarks

Reported results

As published on the model card: the maker's own numbers, not measured by AxForge.

TaskDatasetMetricScore
image-text-retrievalCOCOImage-to-Text R@141.880
image-text-retrievalCOCOImage-to-Text R@571.640
image-text-retrievalCOCOImage-to-Text R@1082.160
audio-text-retrievalLibriSpeechAudio-to-Text R@136.380
audio-text-retrievalLibriSpeechAudio-to-Text R@568.220
audio-text-retrievalLibriSpeechAudio-to-Text R@1079.520
© 2026 AxForge · EU-hosted AI infrastructure Pricing Docs Trust Privacy Terms