Instructions to use microsoft/Mage-VL with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use microsoft/Mage-VL with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="microsoft/Mage-VL", trust_remote_code=True) messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoModelForImageTextToText model = AutoModelForImageTextToText.from_pretrained("microsoft/Mage-VL", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use microsoft/Mage-VL with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "microsoft/Mage-VL" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "microsoft/Mage-VL", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/microsoft/Mage-VL
- SGLang
How to use microsoft/Mage-VL with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "microsoft/Mage-VL" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "microsoft/Mage-VL", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "microsoft/Mage-VL" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "microsoft/Mage-VL", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use microsoft/Mage-VL with Docker Model Runner:
docker model run hf.co/microsoft/Mage-VL
Fix <|image_pad|> misalignment when images= and videos= coexist
Browse filesFixes microsoft/Mage#28.
Two independent defects made a single processor(images=..., videos=...) call
produce visual tensors that do not line up with the prompt placeholders:
1. codec_video_processing_mage_vl.rewrite_text_with_codec_positions used
text.find(VISION_START) / text.rfind(VISION_END), which spans from the
first vision block to the last. Any image block sitting between them was
wiped out along with the video block, so the images lost their
placeholders entirely and generate() raised
'Image features and image tokens do not match'. Now matches exactly one
<|vision_start|><|video_pad|><|vision_end|> block, mirroring what the
frames backend already did.
2. processing_mage_vl.__call__ ran the IMAGE PATH after the video branches
had already rewritten the video block into literal <|image_pad|> runs.
_expand_image_pads restarts from the start of the string on every
replace, so it consumed the video's placeholders. The tensor
concatenation was also unconditionally video-rows-then-image-rows, which
is only correct when the video precedes every image in the prompt. The
image path now runs first (while video placeholders are still
<|video_pad|>), both video branches emit per-visual slots, and the slots
are concatenated in recorded prompt order.
Reordering rows is safe for the vision tower: _build_cu_seqlens blocks
strictly per image_grid_thw row, so image and video rows never attend to
each other regardless of position.
Verification
------------
Single-modality paths are byte-identical to the stock processor
(torch.equal on input_ids / pixel_values / image_grid_thw / patch_positions):
codec video-only, frames video-only, 1 image, 2 images -> all True
Mixed images + video, stock vs patched:
codec [image, video] 3775/3776 ValueError -> 3776/3776 match
codec [video, image] 3775/3776 ValueError -> 3776/3776 match
codec [image, image, video] 5822/5824 ValueError -> 5824/5824 match
codec [video, image, image] 5822/5824 ValueError -> 5824/5824 match
frames all four orders totals matched by coincidence but the k-th
placeholder run was paired with the wrong
tensor rows; runs == grid rows is now True
element-by-element
Generation with unmodified weights, examples/dog.jpg + soccer-broadcast.mp4:
codec [video, image] 'A dog is in the reference photo, and the video shows
a sports broadcast with four commentators...'
frames [video, image] 'A dog is in the reference photo, and the video shows
a football match between England and Argentina.'
Not addressed: the codec branch still replicates one text per video, so
multiple videos in a single prompt remains unsupported, as before.
- codec_video_processing_mage_vl.py +12 -4
- processing_mage_vl.py +94 -61
|
@@ -27,6 +27,7 @@ from __future__ import annotations
|
|
| 27 |
import hashlib
|
| 28 |
import json
|
| 29 |
import os
|
|
|
|
| 30 |
import shutil
|
| 31 |
import subprocess
|
| 32 |
import sys
|
|
@@ -49,6 +50,13 @@ from PIL import Image
|
|
| 49 |
VISION_START = "<|vision_start|>"
|
| 50 |
VISION_END = "<|vision_end|>"
|
| 51 |
IMAGE_PAD = "<|image_pad|>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
# ----------------------------------------------------------------- config
|
|
@@ -222,13 +230,13 @@ def rewrite_text_with_codec_positions(
|
|
| 222 |
for timestamp, token_count in _timestamp_runs(patch_positions, fps, decimals):
|
| 223 |
parts.extend([timestamp, VISION_START, IMAGE_PAD * token_count, VISION_END, "\n"])
|
| 224 |
vision_text = "".join(parts)
|
| 225 |
-
|
| 226 |
-
if
|
| 227 |
return text
|
| 228 |
-
tail_start =
|
| 229 |
if tail_start < len(text) and text[tail_start] == "\n":
|
| 230 |
tail_start += 1
|
| 231 |
-
return text[:
|
| 232 |
|
| 233 |
|
| 234 |
def drop_padding_canvases(
|
|
|
|
| 27 |
import hashlib
|
| 28 |
import json
|
| 29 |
import os
|
| 30 |
+
import re
|
| 31 |
import shutil
|
| 32 |
import subprocess
|
| 33 |
import sys
|
|
|
|
| 50 |
VISION_START = "<|vision_start|>"
|
| 51 |
VISION_END = "<|vision_end|>"
|
| 52 |
IMAGE_PAD = "<|image_pad|>"
|
| 53 |
+
VIDEO_PAD = "<|video_pad|>"
|
| 54 |
+
|
| 55 |
+
# Matches exactly one chat-template video placeholder block, so that image
|
| 56 |
+
# blocks elsewhere in the prompt are left untouched.
|
| 57 |
+
_VIDEO_BLOCK_RE = re.compile(
|
| 58 |
+
re.escape(VISION_START) + r"\s*" + re.escape(VIDEO_PAD) + r"\s*" + re.escape(VISION_END)
|
| 59 |
+
)
|
| 60 |
|
| 61 |
|
| 62 |
# ----------------------------------------------------------------- config
|
|
|
|
| 230 |
for timestamp, token_count in _timestamp_runs(patch_positions, fps, decimals):
|
| 231 |
parts.extend([timestamp, VISION_START, IMAGE_PAD * token_count, VISION_END, "\n"])
|
| 232 |
vision_text = "".join(parts)
|
| 233 |
+
match = _VIDEO_BLOCK_RE.search(text)
|
| 234 |
+
if match is None:
|
| 235 |
return text
|
| 236 |
+
tail_start = match.end()
|
| 237 |
if tail_start < len(text) and text[tail_start] == "\n":
|
| 238 |
tail_start += 1
|
| 239 |
+
return text[:match.start()] + vision_text + text[tail_start:]
|
| 240 |
|
| 241 |
|
| 242 |
def drop_padding_canvases(
|
|
@@ -237,6 +237,68 @@ class MageVLProcessor:
|
|
| 237 |
|
| 238 |
out: dict = {}
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
# ---------------- CODEC VIDEO BACKEND ----------------
|
| 241 |
# Codec path: replaces the frame-sampling VideoProcessor entirely.
|
| 242 |
# Each video -> N canvases + src_patch_position; we rewrite the
|
|
@@ -284,6 +346,7 @@ class MageVLProcessor:
|
|
| 284 |
if len(rewritten_texts) != len(videos_list):
|
| 285 |
if len(rewritten_texts) == 1 and len(videos_list) >= 1:
|
| 286 |
rewritten_texts = rewritten_texts * len(videos_list)
|
|
|
|
| 287 |
else:
|
| 288 |
raise ValueError(
|
| 289 |
f"codec video backend: got {len(rewritten_texts)} texts but {len(videos_list)} videos"
|
|
@@ -311,9 +374,7 @@ class MageVLProcessor:
|
|
| 311 |
all_grid_thw.append(image_grid_thw)
|
| 312 |
all_patch_positions.append(patch_positions)
|
| 313 |
|
| 314 |
-
|
| 315 |
-
out["image_grid_thw"] = torch.cat(all_grid_thw, dim=0)
|
| 316 |
-
out["patch_positions"] = torch.cat(all_patch_positions, dim=0)
|
| 317 |
text = rewritten_texts
|
| 318 |
# Codec branch handled the video. Suppress the frame-sampling block below.
|
| 319 |
videos = None
|
|
@@ -412,68 +473,40 @@ class MageVLProcessor:
|
|
| 412 |
# image_grid_thw is the only adjustment needed for the model's
|
| 413 |
# forward to treat each frame as a separate image (matching the
|
| 414 |
# multi-image inference path).
|
| 415 |
-
out["pixel_values"] = video_outputs["pixel_values_videos"]
|
| 416 |
vgthw = video_outputs["video_grid_thw"]
|
| 417 |
-
|
|
|
|
|
|
|
| 418 |
for row in vgthw:
|
| 419 |
T_v, H_v, W_v = int(row[0]), int(row[1]), int(row[2])
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
).tolist()
|
| 440 |
-
img_idx = 0
|
| 441 |
-
|
| 442 |
-
def _expand_image_pads(s: str) -> str:
|
| 443 |
-
nonlocal img_idx
|
| 444 |
-
while IMAGE_PAD in s:
|
| 445 |
-
if img_idx >= len(image_token_counts):
|
| 446 |
-
break
|
| 447 |
-
n = int(image_token_counts[img_idx])
|
| 448 |
-
s = s.replace(IMAGE_PAD, "<|placeholder|>" * n, 1)
|
| 449 |
-
img_idx += 1
|
| 450 |
-
return s.replace("<|placeholder|>", IMAGE_PAD)
|
| 451 |
-
|
| 452 |
-
text = [_expand_image_pads(s) for s in text]
|
| 453 |
-
|
| 454 |
-
# If videos and images coexist, prefer concatenation of patch tensors.
|
| 455 |
-
if "pixel_values" in out:
|
| 456 |
-
out["pixel_values"] = torch.cat(
|
| 457 |
-
[out["pixel_values"], image_outputs["pixel_values"]], dim=0
|
| 458 |
-
)
|
| 459 |
-
out["image_grid_thw"] = torch.cat(
|
| 460 |
-
[out["image_grid_thw"], image_outputs["image_grid_thw"]], dim=0
|
| 461 |
-
)
|
| 462 |
-
# Build image patch_positions and concat.
|
| 463 |
-
from .video_processing_mage_vl import build_patch_positions
|
| 464 |
-
image_pp = build_patch_positions(
|
| 465 |
-
image_outputs["image_grid_thw"], spatial_merge_size=sms
|
| 466 |
-
)
|
| 467 |
-
out["patch_positions"] = torch.cat(
|
| 468 |
-
[out["patch_positions"], image_pp], dim=0
|
| 469 |
-
)
|
| 470 |
else:
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
| 477 |
|
| 478 |
# ---------------- VIDEO PATH FINAL EXPANSION ----------------
|
| 479 |
# When `videos` was given (and possibly without `images`), the per-frame
|
|
|
|
| 237 |
|
| 238 |
out: dict = {}
|
| 239 |
|
| 240 |
+
# ---------------- VISUAL PLACEHOLDER ORDER ----------------
|
| 241 |
+
# Record the prompt order of every visual placeholder *before* any
|
| 242 |
+
# rewriting, so the visual tensors can be concatenated in exactly the
|
| 243 |
+
# order the model consumes the <|image_pad|> slots.
|
| 244 |
+
_pad_re = re.compile(re.escape(IMAGE_PAD) + "|" + re.escape(VIDEO_PAD))
|
| 245 |
+
order_per_text = [
|
| 246 |
+
["image" if m.group(0) == IMAGE_PAD else "video" for m in _pad_re.finditer(s)]
|
| 247 |
+
for s in text
|
| 248 |
+
]
|
| 249 |
+
|
| 250 |
+
# Per-visual (pixel_values, image_grid_thw, patch_positions) triples,
|
| 251 |
+
# kept separate until the final prompt-order assembly.
|
| 252 |
+
image_slots: List[tuple] = []
|
| 253 |
+
video_slots: List[tuple] = []
|
| 254 |
+
|
| 255 |
+
# ---------------- IMAGE PATH ----------------
|
| 256 |
+
# Runs *before* the video paths: at this point every <|image_pad|> in
|
| 257 |
+
# `text` still belongs to a real image, because video placeholders are
|
| 258 |
+
# still <|video_pad|>. Running it afterwards would let the image
|
| 259 |
+
# expansion consume placeholders the video rewrite had just emitted.
|
| 260 |
+
if images is not None:
|
| 261 |
+
if self.image_processor is None:
|
| 262 |
+
raise ValueError("images passed but no image_processor configured.")
|
| 263 |
+
image_outputs = self.image_processor(images=images, return_tensors="pt")
|
| 264 |
+
image_grid_thw = image_outputs["image_grid_thw"]
|
| 265 |
+
|
| 266 |
+
# Expand each <|image_pad|> placeholder to the number of merged tokens.
|
| 267 |
+
sms = self.spatial_merge_size
|
| 268 |
+
merge_factor = sms * sms
|
| 269 |
+
image_token_counts = (
|
| 270 |
+
(image_grid_thw[:, 0] * image_grid_thw[:, 1] * image_grid_thw[:, 2])
|
| 271 |
+
// merge_factor
|
| 272 |
+
).tolist()
|
| 273 |
+
img_idx = 0
|
| 274 |
+
|
| 275 |
+
def _expand_image_pads(s: str) -> str:
|
| 276 |
+
nonlocal img_idx
|
| 277 |
+
while IMAGE_PAD in s:
|
| 278 |
+
if img_idx >= len(image_token_counts):
|
| 279 |
+
break
|
| 280 |
+
n = int(image_token_counts[img_idx])
|
| 281 |
+
s = s.replace(IMAGE_PAD, "<|placeholder|>" * n, 1)
|
| 282 |
+
img_idx += 1
|
| 283 |
+
return s.replace("<|placeholder|>", IMAGE_PAD)
|
| 284 |
+
|
| 285 |
+
text = [_expand_image_pads(s) for s in text]
|
| 286 |
+
|
| 287 |
+
try:
|
| 288 |
+
from .video_processing_mage_vl import build_patch_positions
|
| 289 |
+
except ImportError:
|
| 290 |
+
from video_processing_mage_vl import build_patch_positions
|
| 291 |
+
image_pp = build_patch_positions(image_grid_thw, spatial_merge_size=sms)
|
| 292 |
+
offset = 0
|
| 293 |
+
for row in image_grid_thw:
|
| 294 |
+
n = int(row[0]) * int(row[1]) * int(row[2])
|
| 295 |
+
image_slots.append((
|
| 296 |
+
image_outputs["pixel_values"][offset: offset + n],
|
| 297 |
+
row.unsqueeze(0),
|
| 298 |
+
image_pp[offset: offset + n],
|
| 299 |
+
))
|
| 300 |
+
offset += n
|
| 301 |
+
|
| 302 |
# ---------------- CODEC VIDEO BACKEND ----------------
|
| 303 |
# Codec path: replaces the frame-sampling VideoProcessor entirely.
|
| 304 |
# Each video -> N canvases + src_patch_position; we rewrite the
|
|
|
|
| 346 |
if len(rewritten_texts) != len(videos_list):
|
| 347 |
if len(rewritten_texts) == 1 and len(videos_list) >= 1:
|
| 348 |
rewritten_texts = rewritten_texts * len(videos_list)
|
| 349 |
+
order_per_text = order_per_text * len(videos_list)
|
| 350 |
else:
|
| 351 |
raise ValueError(
|
| 352 |
f"codec video backend: got {len(rewritten_texts)} texts but {len(videos_list)} videos"
|
|
|
|
| 374 |
all_grid_thw.append(image_grid_thw)
|
| 375 |
all_patch_positions.append(patch_positions)
|
| 376 |
|
| 377 |
+
video_slots.extend(zip(all_pixel_values, all_grid_thw, all_patch_positions))
|
|
|
|
|
|
|
| 378 |
text = rewritten_texts
|
| 379 |
# Codec branch handled the video. Suppress the frame-sampling block below.
|
| 380 |
videos = None
|
|
|
|
| 473 |
# image_grid_thw is the only adjustment needed for the model's
|
| 474 |
# forward to treat each frame as a separate image (matching the
|
| 475 |
# multi-image inference path).
|
|
|
|
| 476 |
vgthw = video_outputs["video_grid_thw"]
|
| 477 |
+
video_pv = video_outputs["pixel_values_videos"]
|
| 478 |
+
video_pp = video_outputs["patch_positions"]
|
| 479 |
+
offset = 0
|
| 480 |
for row in vgthw:
|
| 481 |
T_v, H_v, W_v = int(row[0]), int(row[1]), int(row[2])
|
| 482 |
+
n = T_v * H_v * W_v
|
| 483 |
+
video_slots.append((
|
| 484 |
+
video_pv[offset: offset + n],
|
| 485 |
+
torch.tensor([[1, H_v, W_v]] * T_v, dtype=vgthw.dtype),
|
| 486 |
+
video_pp[offset: offset + n],
|
| 487 |
+
))
|
| 488 |
+
offset += n
|
| 489 |
+
|
| 490 |
+
# ---------------- ASSEMBLE VISUALS IN PROMPT ORDER ----------------
|
| 491 |
+
# The model consumes <|image_pad|> slots in prompt order and
|
| 492 |
+
# pixel_values in row order, so the visual tensors must be stacked in
|
| 493 |
+
# the order their placeholders appear in the prompt.
|
| 494 |
+
if image_slots or video_slots:
|
| 495 |
+
flat_order = [kind for per_text in order_per_text for kind in per_text]
|
| 496 |
+
n_img = sum(k == "image" for k in flat_order)
|
| 497 |
+
n_vid = sum(k == "video" for k in flat_order)
|
| 498 |
+
if n_img == len(image_slots) and n_vid == len(video_slots):
|
| 499 |
+
img_it, vid_it = iter(image_slots), iter(video_slots)
|
| 500 |
+
ordered = [next(img_it) if k == "image" else next(vid_it) for k in flat_order]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
else:
|
| 502 |
+
# Placeholder bookkeeping did not line up (e.g. a caller-supplied
|
| 503 |
+
# prompt that bypassed the chat template). Fall back to the legacy
|
| 504 |
+
# videos-then-images concatenation rather than failing hard.
|
| 505 |
+
ordered = list(video_slots) + list(image_slots)
|
| 506 |
+
out["pixel_values"] = torch.cat([s[0] for s in ordered], dim=0)
|
| 507 |
+
out["image_grid_thw"] = torch.cat([s[1] for s in ordered], dim=0)
|
| 508 |
+
out["patch_positions"] = torch.cat([s[2] for s in ordered], dim=0)
|
| 509 |
+
|
| 510 |
|
| 511 |
# ---------------- VIDEO PATH FINAL EXPANSION ----------------
|
| 512 |
# When `videos` was given (and possibly without `images`), the per-frame
|