Feature Extraction
MLX
Safetensors
muse_glimmer
mlx-vlm
metaencoder
multimodal
embeddings
quantized
8-bit precision
Instructions to use edgefloor/meta-encoder-mlx-8bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use edgefloor/meta-encoder-mlx-8bit with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download edgefloor/meta-encoder-mlx-8bit --local-dir meta-encoder-mlx-8bit
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download upstream_metaencoder.py from edgefloor/meta-encoder-mlx-8bit: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/edgefloor/meta-encoder-mlx-8bit/resolve/main/upstream_metaencoder.py
- Command line
-
hf download hf://edgefloor/meta-encoder-mlx-8bit/upstream_metaencoder.py
-
curl -L -o upstream_metaencoder.py https://huggingface.co/edgefloor/meta-encoder-mlx-8bit/resolve/main/upstream_metaencoder.py
14.8 kB
| """MetaEncoder-30B: match a task against candidates, over text, images, and video. | |
| from modeling_metaencoder import MetaEncoder | |
| model = MetaEncoder.from_pretrained("<this repo>") | |
| results = model.match( | |
| task={"text": "Which photo shows a bicycle?", "image": context_img}, | |
| candidates=[ | |
| {"image": img_a}, | |
| {"image": img_b, "instruction": "Represent the photo"}, | |
| {"text": "a red bicycle leaning on a wall"}, | |
| {"video": frames}, | |
| ], | |
| ) | |
| results[0].index, results[0].score, results[0].candidate | |
| A *task* is what you are asking: text, images, and video in one object. Put the question, | |
| query or instruction into its `text` — that text is what grounds the task to the candidates. | |
| A *candidate* is the same shape, plus an optional `instruction` describing how that candidate | |
| should be encoded. | |
| `match` returns candidates ranked by similarity. For a corpus you query repeatedly, encode it | |
| once with `encode` and pass the resulting tensor as `candidates`. | |
| Prompt construction, media routing, padding and pooling are handled internally and must match | |
| training exactly; see `_encode_batch` for the contract. | |
| """ | |
| from __future__ import annotations | |
| import unicodedata | |
| from dataclasses import dataclass | |
| from typing import Any, Mapping, Sequence, Union | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from transformers import AutoProcessor | |
| from transformers.models.muse_glimmer.modeling_muse_glimmer import ( | |
| MuseGlimmerForConditionalGeneration, | |
| ) | |
| # Held constant across every input. A system turn that varies per task is a task-ID channel the | |
| # model keys on instead of reading the instruction, which makes the embeddings brittle to | |
| # unseen phrasings. It is emitted rather than omitted because the chat template's no-system | |
| # branch interpolates the current date, which would make a corpus encoded yesterday not match a | |
| # query encoded today. | |
| DEFAULT_SYSTEM_INSTRUCTION = "Represent the user's input." | |
| IMAGE_TOKEN = "<|patch|>" | |
| VIDEO_TOKEN = "<|video|>" | |
| DEFAULT_MAX_LENGTH = 8192 | |
| DEFAULT_BATCH_SIZE = 8 | |
| # Frames arrive already sampled, so the video processor must be told not to sample again. It | |
| # defaults to do_sample_frames=True against an inferred fps (24 when it cannot tell), which both | |
| # drops frames and writes wrong wall-clock timestamps into the prompt. | |
| VIDEO_FPS = 2.0 | |
| Item = Union[str, Image.Image, Mapping[str, Any]] | |
| _ITEM_KEYS = {"text", "instruction", "image", "images", "video"} | |
| class Match: | |
| """One ranked candidate.""" | |
| index: int # position in the candidates you passed in | |
| score: float # cosine similarity to the task, in [-1, 1] | |
| candidate: Any # the candidate object itself, or None if you passed embeddings | |
| def __repr__(self): | |
| return f"Match(index={self.index}, score={self.score:.4f})" | |
| def _ensure_punct(s: str) -> str: | |
| s = (s or "").strip() | |
| if s and not unicodedata.category(s[-1]).startswith("P"): | |
| s += "." | |
| return s | |
| def _user_text(instruction: str | None, text: str | None) -> str: | |
| # Media markers are placeholders, not content: drop them and collapse whitespace, matching | |
| # the training-time strip exactly (replace with a space, then re-join on whitespace) so an | |
| # embedded marker cannot fuse two words together. | |
| body = (text or "").replace(IMAGE_TOKEN, " ").replace(VIDEO_TOKEN, " ") | |
| body = " ".join(body.split()) | |
| instr = _ensure_punct(instruction) | |
| return " ".join(p for p in (instr, body) if p) | |
| def _as_items(inputs: Item | Sequence[Item], instruction: str | None) -> list[dict]: | |
| """Normalise any accepted input shape into one dict per embedding.""" | |
| if inputs is None: | |
| raise ValueError("encode() got no inputs") | |
| if isinstance(inputs, (str, Image.Image, Mapping)): | |
| inputs = [inputs] | |
| else: | |
| try: | |
| inputs = list(inputs) | |
| except TypeError: | |
| # Not a sequence either; wrap it so the per-item check reports what was expected | |
| # instead of leaking "object is not iterable". | |
| inputs = [inputs] | |
| items = [] | |
| for i, raw in enumerate(inputs): | |
| if isinstance(raw, str): | |
| raw = {"text": raw} | |
| elif isinstance(raw, Image.Image): | |
| raw = {"image": raw} | |
| elif not isinstance(raw, Mapping): | |
| raise TypeError( | |
| f"item {i} is {type(raw).__name__}; expected str, PIL.Image, or a dict with " | |
| f"keys {sorted(_ITEM_KEYS)}" | |
| ) | |
| unknown = set(raw) - _ITEM_KEYS | |
| if unknown: | |
| hint = "" | |
| if "videos" in unknown: | |
| hint = " (use 'video' with a list of frames for a single clip)" | |
| raise ValueError(f"item {i} has unknown key(s) {sorted(unknown)}{hint}") | |
| images = [] | |
| if raw.get("image") is not None: | |
| images.append(raw["image"]) | |
| if raw.get("images") is not None: | |
| got = raw["images"] | |
| images.extend(got if isinstance(got, (list, tuple)) else [got]) | |
| images = [im for im in images if im is not None] | |
| video = raw.get("video") | |
| if video is not None: | |
| video = [f for f in (video if isinstance(video, (list, tuple)) else [video]) | |
| if f is not None] | |
| if not video: | |
| video = None | |
| items.append({ | |
| "text": raw.get("text"), | |
| "instruction": raw.get("instruction", instruction), | |
| "images": images or None, | |
| "video": video, | |
| }) | |
| return items | |
| class MetaEncoder: | |
| """Embedding model over text, images, and video.""" | |
| def __init__(self, model, processor): | |
| self.model = model | |
| self.processor = processor | |
| # Last-token pooling reads the final column, which is only the last real token under | |
| # left padding. The processor defaults to right. | |
| self.processor.tokenizer.padding_side = "left" | |
| def from_pretrained(cls, path, device_map="auto", dtype=torch.bfloat16, | |
| attn_implementation=None): | |
| if attn_implementation is None: | |
| attn_implementation = "sdpa" | |
| # flash-attn can be installed and still be unusable: it is CUDA-only, so probing | |
| # the import alone picks a backend that fails at init on a CPU load. | |
| if torch.cuda.is_available(): | |
| try: | |
| import flash_attn # noqa: F401 | |
| attn_implementation = "flash_attention_2" | |
| except ImportError: | |
| pass | |
| model = MuseGlimmerForConditionalGeneration.from_pretrained( | |
| path, | |
| dtype=dtype, | |
| device_map=device_map, | |
| attn_implementation=attn_implementation, | |
| low_cpu_mem_usage=True, | |
| ) | |
| model.config.use_cache = False | |
| model.eval() | |
| # transformers keeps the rotary inv_freq buffers in fp32 whatever `dtype` is, but every | |
| # reported number was measured after a `.to(bf16)`, which rounds them too. Rounding them | |
| # here reproduces those numbers exactly; with fp32 inv_freq, image and video embeddings | |
| # drift by up to ~1.5% cosine. | |
| for module in model.modules(): | |
| for name, buf in list(module.named_buffers(recurse=False)): | |
| if "inv_freq" in name and buf.dtype != dtype: | |
| setattr(module, name, buf.to(dtype)) | |
| return cls(model, AutoProcessor.from_pretrained(path)) | |
| def embedding_dim(self) -> int: | |
| return self.model.config.text_config.hidden_size | |
| def encode( | |
| self, | |
| inputs: Item | Sequence[Item], | |
| instruction: str | None = None, | |
| batch_size: int = DEFAULT_BATCH_SIZE, | |
| max_length: int = DEFAULT_MAX_LENGTH, | |
| ) -> torch.Tensor: | |
| """Embed items directly. Returns L2-normalised float32 (n_items, embedding_dim). | |
| Most uses want `match` instead. Reach for `encode` to embed a corpus once and pass the | |
| tensor to `match` as `candidates`, or when you need the vectors themselves. | |
| `inputs` is one item or a list of items. An item is a string, a PIL image, or a dict: | |
| {"text": str, "image": img, "images": [img, ...], "video": [frame, ...], | |
| "instruction": str} | |
| All keys optional. Use `video` for pre-sampled clips (temporal patching); use `images` | |
| only for genuinely independent stills. | |
| """ | |
| items = _as_items(inputs, instruction) | |
| if not items: | |
| return torch.empty(0, self.embedding_dim, device=self.model.device) | |
| out = [self._encode_batch(items[i:i + batch_size], max_length) | |
| for i in range(0, len(items), batch_size)] | |
| return torch.cat(out, dim=0) | |
| def match( | |
| self, | |
| task: Item, | |
| candidates: Sequence[Item] | torch.Tensor, | |
| top_k: int | None = None, | |
| batch_size: int = DEFAULT_BATCH_SIZE, | |
| ) -> list[Match]: | |
| """Rank `candidates` against `task`, best first. | |
| `task` is a single item — text, images, video. Put the question, query or instruction | |
| in its `text`; that is what grounds the task to the candidates. | |
| `candidates` is a list of items, each optionally carrying its own `instruction` for | |
| how it should be encoded. It may instead be a tensor of embeddings from a previous | |
| `encode` call, which is how you avoid re-encoding a corpus on every query. | |
| Returns a list of `Match(index, score, candidate)` sorted by descending score. | |
| """ | |
| scores = self.score(task, candidates, batch_size=batch_size) | |
| order = torch.argsort(scores, descending=True) | |
| if top_k is not None: | |
| order = order[:top_k] | |
| originals = None if isinstance(candidates, torch.Tensor) else list(candidates) | |
| return [ | |
| Match(index=int(i), | |
| score=float(scores[i]), | |
| candidate=None if originals is None else originals[int(i)]) | |
| for i in order | |
| ] | |
| def score( | |
| self, | |
| task: Item, | |
| candidates: Sequence[Item] | torch.Tensor, | |
| batch_size: int = DEFAULT_BATCH_SIZE, | |
| ) -> torch.Tensor: | |
| """Cosine similarity of `task` to each candidate, in input order. Shape (n_candidates,).""" | |
| if isinstance(task, (list, tuple)): | |
| raise TypeError( | |
| "task must be a single item, not a list. To score several tasks, call match() " | |
| "per task (encode the candidates once and pass the tensor back in)." | |
| ) | |
| task_emb = self.encode(task, batch_size=1) | |
| if isinstance(candidates, torch.Tensor): | |
| cand_emb = candidates | |
| if cand_emb.ndim != 2 or cand_emb.shape[1] != task_emb.shape[1]: | |
| raise ValueError( | |
| f"candidate embeddings must be (n, {task_emb.shape[1]}), " | |
| f"got {tuple(cand_emb.shape)}" | |
| ) | |
| cand_emb = cand_emb.to(task_emb.device) | |
| else: | |
| cand_emb = self.encode(candidates, batch_size=batch_size) | |
| return (task_emb @ cand_emb.T)[0] | |
| def similarity(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: | |
| """Cosine similarity matrix (n_a, n_b). Inputs are already unit-norm.""" | |
| return a @ b.T | |
| # ---------------------------------------------------------------- internals | |
| def _conversations(self, items): | |
| conversations, flat_images, flat_videos = [], [], [] | |
| for it in items: | |
| content = [] | |
| for im in (it["images"] or []): | |
| content.append({"type": "image", "image": im}) | |
| flat_images.append(im) | |
| if it["video"] is not None: | |
| content.append({"type": "video", "video": it["video"]}) | |
| flat_videos.append(it["video"]) | |
| ut = _user_text(it["instruction"], it["text"]) | |
| if ut: | |
| content.append({"type": "text", "text": ut}) | |
| elif not content: | |
| content.append({"type": "text", "text": " "}) | |
| conversations.append([ | |
| {"role": "system", | |
| "content": [{"type": "text", "text": DEFAULT_SYSTEM_INSTRUCTION}]}, | |
| {"role": "user", "content": content}, | |
| ]) | |
| return conversations, flat_images, flat_videos | |
| def _encode_batch(self, items, max_length): | |
| conversations, flat_images, flat_videos = self._conversations(items) | |
| chat_texts = [ | |
| self.processor.apply_chat_template(c, add_generation_prompt=False, tokenize=False) | |
| for c in conversations | |
| ] | |
| kwargs = dict( | |
| text=chat_texts, | |
| images=flat_images or None, | |
| videos=flat_videos or None, | |
| padding=True, | |
| return_tensors="pt", | |
| # The chat template already emits bos_token and the tokenizer has | |
| # add_bos_token=True; leaving this on yields a doubled <|begin_of_text|>. | |
| add_special_tokens=False, | |
| ) | |
| if flat_videos: | |
| kwargs["do_sample_frames"] = False | |
| from transformers.video_utils import VideoMetadata | |
| kwargs["video_metadata"] = [ | |
| VideoMetadata( | |
| total_num_frames=len(v), | |
| fps=VIDEO_FPS, | |
| duration=len(v) / VIDEO_FPS, | |
| frames_indices=list(range(len(v))), | |
| ) | |
| for v in flat_videos | |
| ] | |
| if flat_images or flat_videos: | |
| # Truncation would desynchronise the expanded <|patch|>/<|video|> runs from the pixels. | |
| kwargs["truncation"] = False | |
| else: | |
| kwargs["truncation"] = True | |
| kwargs["max_length"] = max_length | |
| inputs = self.processor(**kwargs) | |
| inputs["input_ids"] = inputs["input_ids"].long() | |
| inputs["attention_mask"] = inputs["attention_mask"].long() | |
| inputs = {k: (v.to(self.model.device) if hasattr(v, "to") else v) | |
| for k, v in inputs.items()} | |
| am = inputs["attention_mask"] | |
| assert bool((am[:, -1] == 1).all()), "expected left padding" | |
| out = self.model( | |
| **inputs, | |
| return_dict=True, | |
| output_hidden_states=True, | |
| # ForConditionalGeneration otherwise materialises full-sequence logits over a | |
| # 202k vocab, which dominates memory; only hidden states are needed here. | |
| logits_to_keep=1, | |
| ) | |
| pooled = out.hidden_states[-1][:, -1, :] | |
| return F.normalize(pooled.float(), p=2, dim=-1) | |