Skip to content

vllm.models.deepseek_v4_1.common.mm_preprocess

Multimodal preprocessing for the DeepSeek-V4.1 vision variant.

The image transform and image-span construction are ported from the official repository's image_processor.py so that token counts bit-match the reference. Each <|deepseek_image|> placeholder in the prompt expands to [IMAGE_START] + ([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h + [IMAGE_END]; every span position carries image_token_id (129264) in input_ids and the roles ride along in a per-image types tensor (the reference's out-of-band token_types). IMAGE slots receive aligner rows in reading order; the delimiters take the learned image_start / image_newline / image_end vectors.

One vLLM-side deviation from the reference token stream: a leading compressor-alignment pad (COMPRESS_PAD_TO - 1 - start % COMPRESS_PAD_TO positions, so the span always starts at the same compressor phase) is prepended when the block is spliced into the final prompt. Pad positions borrow the reserved in-vocab token <|place_holder_mm_span_0436|> so they stay distinguishable from real span positions; they embed as the plain image token (v4.1 has no image_pad vector) and are routed and engram-deadened like image tokens.

Classes:

Functions:

  • image_sentinel_mask

    Boolean mask for image-span positions (span tokens and align pads).

  • image_token_types

    Reading-order span layout: one IMAGE_NEW_LINE per row.

  • llm_grid

    Token grid the aligner produces from a patch grid of this pixel size.

  • load_image

    Transform one PIL image into ViT patches.

  • safe_resize

    Shrink the pixel size until the image costs at most max_n_token LLM

  • solve_resize_ratio

    Largest aspect-preserving pixel size whose token grid still fits in

  • validate_image_sentinel_ids

    Check the image/pad token ids against the tokenizer.

DeepseekV4VLImageProcessor

Per-image transform (the PIL-input equivalent of the reference load_image).

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
class DeepseekV4VLImageProcessor:
    """Per-image transform (the PIL-input equivalent of the reference
    ``load_image``)."""

    def __init__(self, config: DeepseekV41Config) -> None:
        super().__init__()
        self.patch_size = config.vision_patch_size
        self.downsample_ratio = config.vision_downsample_ratio
        self.max_n_token = config.vision_max_n_token
        self.min_pixels = config.vision_min_pixels
        self.max_wh_ratio = config.vision_max_wh_ratio

    def __call__(self, image: Image.Image):
        return load_image(
            image,
            patch_size=self.patch_size,
            downsample_ratio=self.downsample_ratio,
            max_n_token=self.max_n_token,
            min_pixels=self.min_pixels,
            max_wh_ratio=self.max_wh_ratio,
        )

DeepseekV4VLProcessor

Minimal stand-in for the HF processor of DeepSeek-V4.1 vision models.

The official repository ships image preprocessing as plain functions in image_processor.py (no auto_map processor), so this class wraps their ports directly and the model loads without --trust-remote-code.

__call__ returns a BatchFeature with one entry per image (flattened across images):

  • patches: (sum(n_vit_h * n_vit_w), 3, p, p) bf16 ViT patches.
  • vit_grid: (num_images, 2) int64 [n_vit_h, n_vit_w].
  • llm_grid: (num_images, 2) int64 [n_llm_h, n_llm_w].
  • types: concatenated per-image pad-free span roles (IMAGE_START/IMAGE/IMAGE_NEW_LINE/IMAGE_END); every span position carries image_token_id in the prompt's token ids.
Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
class DeepseekV4VLProcessor:
    """Minimal stand-in for the HF processor of DeepSeek-V4.1 vision models.

    The official repository ships image preprocessing as plain functions in
    ``image_processor.py`` (no ``auto_map`` processor), so this class wraps
    their ports directly and the model loads without ``--trust-remote-code``.

    ``__call__`` returns a ``BatchFeature`` with one entry per image
    (flattened across images):

    - ``patches``: ``(sum(n_vit_h * n_vit_w), 3, p, p)`` bf16 ViT patches.
    - ``vit_grid``: ``(num_images, 2)`` int64 ``[n_vit_h, n_vit_w]``.
    - ``llm_grid``: ``(num_images, 2)`` int64 ``[n_llm_h, n_llm_w]``.
    - ``types``: concatenated per-image pad-free span roles
      (IMAGE_START/IMAGE/IMAGE_NEW_LINE/IMAGE_END); every span position
      carries ``image_token_id`` in the prompt's token ids.
    """

    def __init__(self, config: DeepseekV41Config) -> None:
        super().__init__()
        self.config = config
        self.image_processor = DeepseekV4VLImageProcessor(config)

    def __call__(
        self,
        text: str | None = None,
        images: Sequence[Image.Image] | None = None,
        return_tensors: str | None = None,
        **kwargs: Any,
    ) -> BatchFeature:
        patches_list = []
        vit_grid = []
        llm_grid_list = []
        types_list = []
        for image in images or []:
            patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = self.image_processor(image)
            patches_list.append(patches)
            vit_grid.append((n_vit_h, n_vit_w))
            llm_grid_list.append((n_llm_h, n_llm_w))
            types_list.append(image_token_types(n_llm_h, n_llm_w))

        if not patches_list:
            return BatchFeature({})

        return BatchFeature(
            {
                "patches": torch.cat(patches_list),
                "vit_grid": torch.tensor(vit_grid, dtype=torch.int64),
                "llm_grid": torch.tensor(llm_grid_list, dtype=torch.int64),
                "types": torch.cat(types_list),
            }
        )

image_sentinel_mask(token_ids)

Boolean mask for image-span positions (span tokens and align pads).

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def image_sentinel_mask(token_ids: torch.Tensor) -> torch.Tensor:
    """Boolean mask for image-span positions (span tokens and align pads)."""
    return (token_ids == IMAGE_SENTINEL_BASE_ID) | (token_ids == IMAGE_PAD_ID)

image_token_types(n_llm_h, n_llm_w)

Reading-order span layout: one IMAGE_NEW_LINE per row.

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def image_token_types(n_llm_h: int, n_llm_w: int) -> torch.Tensor:
    """Reading-order span layout: one IMAGE_NEW_LINE per row."""
    types = [IMAGE_START]
    types += ([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h
    types.append(IMAGE_END)
    return torch.tensor(types, dtype=torch.int64)

llm_grid(best_height, best_width, patch_size, downsample_ratio)

Token grid the aligner produces from a patch grid of this pixel size.

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def llm_grid(best_height, best_width, patch_size, downsample_ratio):
    """Token grid the aligner produces from a patch grid of this pixel size."""
    return (
        math.ceil((best_height // patch_size) / downsample_ratio),
        math.ceil((best_width // patch_size) / downsample_ratio),
    )

load_image(image, *, patch_size, downsample_ratio, max_n_token, min_pixels, max_wh_ratio)

Transform one PIL image into ViT patches.

Same math as the reference load_image, except the image is already decoded (vLLM supplies PIL images instead of a record dict).

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def load_image(
    image: Image.Image,
    *,
    patch_size: int,
    downsample_ratio: int,
    max_n_token: int,
    min_pixels: int,
    max_wh_ratio: float | None,
):
    """Transform one PIL image into ViT patches.

    Same math as the reference ``load_image``, except the image is already
    decoded (vLLM supplies PIL images instead of a record dict).
    """
    p = patch_size
    image = image.convert("RGB")
    width, height = image.size
    if max_wh_ratio is not None and width > height * max_wh_ratio:
        width = height * max_wh_ratio
    if 0 < width * height < min_pixels:
        ratio = (min_pixels / (width * height)) ** 0.5
        width = int(width * ratio)
        height = int(height * ratio)
    best_width = math.ceil(width / p) * p
    best_height = math.ceil(height / p) * p
    n_llm_h, n_llm_w, best_height, best_width = safe_resize(
        height, width, best_height, best_width, p, downsample_ratio, max_n_token
    )
    n_vit_h, n_vit_w = best_height // p, best_width // p
    if max_wh_ratio is not None and image.width >= max_wh_ratio * image.height:
        image = image.resize((best_width, best_height))
    else:
        image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127))
    x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255
    x = ((x - 0.5) / 0.5).to(torch.bfloat16)
    patches = (
        x.reshape(3, n_vit_h, p, n_vit_w, p)
        .permute(1, 3, 0, 2, 4)
        .reshape(n_vit_h * n_vit_w, 3, p, p)
    )
    return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w

safe_resize(height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token)

Shrink the pixel size until the image costs at most max_n_token LLM tokens (minus the reservation for the compressor-alignment pad).

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def safe_resize(
    height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token
):
    """Shrink the pixel size until the image costs at most max_n_token LLM
    tokens (minus the reservation for the compressor-alignment pad)."""
    max_n_token -= COMPRESS_PAD_TO - 1
    n_llm_h, n_llm_w = llm_grid(best_height, best_width, patch_size, downsample_ratio)
    if num_image_tokens(n_llm_h, n_llm_w) > max_n_token:
        best_height, best_width = solve_resize_ratio(
            height, width, patch_size, downsample_ratio, max_n_token
        )
        n_llm_h, n_llm_w = llm_grid(
            best_height, best_width, patch_size, downsample_ratio
        )
        assert num_image_tokens(n_llm_h, n_llm_w) <= max_n_token
    return n_llm_h, n_llm_w, best_height, best_width

solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token)

Largest aspect-preserving pixel size whose token grid still fits in max_n_token. Returns (best_height, best_width).

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token):
    """Largest aspect-preserving pixel size whose token grid still fits in
    max_n_token. Returns (best_height, best_width)."""
    r = height / width
    max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5
    max_h_float = max_w_float * r
    cell = patch_size * downsample_ratio
    if max_w_float < 1.0:  # very tall: collapse to a single column
        return (max_n_token - 2) // 2 * cell, cell
    if max_h_float < 1.0:  # very wide: collapse to a single row
        return cell, (max_n_token - 3) * cell
    beta = min(
        math.floor(max_w_float) * cell / width,
        math.floor(max_h_float) * cell / height,
    )
    return (
        math.floor(height * beta / patch_size) * patch_size,
        math.floor(width * beta / patch_size) * patch_size,
    )

validate_image_sentinel_ids(tokenizer)

Check the image/pad token ids against the tokenizer.

Source code in vllm/models/deepseek_v4_1/common/mm_preprocess.py
def validate_image_sentinel_ids(tokenizer) -> None:
    """Check the image/pad token ids against the tokenizer."""
    image_id = tokenizer.convert_tokens_to_ids(IMAGE_PLACEHOLDER)
    if image_id != IMAGE_SENTINEL_BASE_ID:
        raise ValueError(
            f"Image placeholder {IMAGE_PLACEHOLDER!r} has id {image_id}, "
            f"expected {IMAGE_SENTINEL_BASE_ID} (the config's "
            "image_token_id); the DeepSeek-V4.1 vision path keys image "
            "routing and engram masking off this id."
        )
    pad_id = tokenizer.convert_tokens_to_ids(IMAGE_PAD_TOKEN_NAME)
    if pad_id != IMAGE_PAD_ID:
        raise ValueError(
            f"Image pad token {IMAGE_PAD_TOKEN_NAME!r} has id {pad_id}, "
            f"expected {IMAGE_PAD_ID}; the DeepSeek-V4.1 vision path "
            "borrows this reserved id for compressor-alignment pads."
        )