| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Multimodal preprocessing for the DeepSeek-V4 vision variants |
| 4 | (DeepSeek-V4-Flash-Vision-Exp). |
| 5 | |
| 6 | The image transform and sentinel-block construction are ported from the |
| 7 | official repository's ``image_processor.py`` so that token counts bit-match |
| 8 | the reference. Each ``<|deepseek_image|>`` placeholder in the prompt expands |
| 9 | to a variable-length block of sentinel tokens; only positions with |
| 10 | ``type == IMAGE`` receive vision embeddings, the other sentinels are looked |
| 11 | up from learned embedding vectors in the model. |
| 12 | |
| 13 | Unlike the reference (which uses out-of-vocab ids ``vocab_size + type``), |
| 14 | the sentinel block borrows five consecutive reserved tokenizer tokens |
| 15 | (``<|place_holder_mm_span_0431|>`` .. ``_0435|>``): they are special tokens |
| 16 | the tokenizer never emits from plain text, so the ids stay in-vocabulary and |
| 17 | work with stock token validation, logprobs and detokenization. The ids are |
| 18 | pure markers — every sentinel position's embedding is overwritten with |
| 19 | vision/sentinel vectors before the decoder layers see it, exactly like the |
| 20 | reference's out-of-vocab scheme. |
| 21 | """ |
| 22 | |
| 23 | import math |
| 24 | from collections.abc import Mapping, Sequence |
| 25 | from typing import Any |
| 26 | |
| 27 | import numpy as np |
| 28 | import torch |
| 29 | from PIL import Image, ImageOps |
| 30 | from transformers import BatchFeature |
| 31 | from typing_extensions import assert_never |
| 32 | |
| 33 | from vllm.config.multimodal import MultiModalDummyOptions |
| 34 | from vllm.inputs import MultiModalDataDict |
| 35 | from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems |
| 36 | from vllm.multimodal.parse import ImageSize, MultiModalDataItems |
| 37 | from vllm.multimodal.processing import ( |
| 38 | BaseDummyInputsBuilder, |
| 39 | BaseMultiModalProcessor, |
| 40 | BaseProcessingInfo, |
| 41 | PromptReplacement, |
| 42 | PromptUpdate, |
| 43 | PromptUpdateDetails, |
| 44 | ) |
| 45 | from vllm.multimodal.processing.processor import ( |
| 46 | MultiModalPromptUpdates, |
| 47 | MultiModalPromptUpdatesApplyResult, |
| 48 | PlaceholderFeaturesInfo, |
| 49 | UpdateMode, |
| 50 | _plan_prompt_updates, |
| 51 | ) |
| 52 | from vllm.transformers_utils.configs.deepseek_v4 import DeepseekV4Config |
| 53 | |
| 54 | IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5) |
| 55 | COMPRESS_PAD_TO = 4 |
| 56 | |
| 57 | IMAGE_PLACEHOLDER = "<|deepseek_image|>" |
| 58 | |
| 59 | # Sentinel roles borrow five consecutive ``<|place_holder_mm_span_XXXX|>`` |
| 60 | # tokens (reserved special tokens, never emitted from plain text). The order |
| 61 | # must match IMAGE_START..IMAGE_END above so that |
| 62 | # ``id == IMAGE_SENTINEL_BASE_ID + type``. |
| 63 | IMAGE_SENTINEL_BASE_ID = 129257 |
| 64 | IMAGE_SENTINEL_TOKEN_NAMES = ( |
| 65 | "<|place_holder_mm_span_0431|>", # IMAGE_START |
| 66 | "<|place_holder_mm_span_0432|>", # IMAGE_PAD |
| 67 | "<|place_holder_mm_span_0433|>", # IMAGE |
| 68 | "<|place_holder_mm_span_0434|>", # IMAGE_NEW_LINE |
| 69 | "<|place_holder_mm_span_0435|>", # IMAGE_END |
| 70 | ) |
| 71 | |
| 72 | |
| 73 | def image_sentinel_mask(token_ids: torch.Tensor) -> torch.Tensor: |
| 74 | """Boolean mask for image-block sentinel positions (in-vocab ids).""" |
| 75 | return (token_ids >= IMAGE_SENTINEL_BASE_ID) & ( |
| 76 | token_ids < IMAGE_SENTINEL_BASE_ID + len(IMAGE_SENTINEL_TOKEN_NAMES) |
| 77 | ) |
| 78 | |
| 79 | |
| 80 | def validate_image_sentinel_ids(tokenizer) -> None: |
| 81 | """Check the borrowed sentinel ids against the tokenizer.""" |
| 82 | for i, name in enumerate(IMAGE_SENTINEL_TOKEN_NAMES): |
| 83 | token_id = tokenizer.convert_tokens_to_ids(name) |
| 84 | if token_id != IMAGE_SENTINEL_BASE_ID + i: |
| 85 | raise ValueError( |
| 86 | f"Image sentinel token {name!r} has id {token_id}, expected " |
| 87 | f"{IMAGE_SENTINEL_BASE_ID + i}; the DeepSeek-V4 vision " |
| 88 | "sentinel block requires these consecutive reserved ids." |
| 89 | ) |
| 90 | |
| 91 | |
| 92 | def grid_tokens(best_height, best_width, patch_size, downsample_ratio): |
| 93 | """Number of LLM tokens the aligner grid occupies (N-layout, including |
| 94 | row/align padding).""" |
| 95 | n_llm_h = math.ceil((best_height // patch_size) / downsample_ratio) |
| 96 | n_llm_w = math.ceil((best_width // patch_size) / downsample_ratio) |
| 97 | num_tokens = n_llm_h * (n_llm_w + 1) + 2 |
| 98 | if n_llm_h % 2 == 1: |
| 99 | num_tokens += n_llm_w + 1 |
| 100 | num_tokens += (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2 |
| 101 | return n_llm_h, n_llm_w, num_tokens |
| 102 | |
| 103 | |
| 104 | def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token): |
| 105 | r = height / width |
| 106 | max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5 |
| 107 | max_h_float = max_w_float * r |
| 108 | if max_w_float < 1.0: |
| 109 | max_w = 1 |
| 110 | max_h = (max_n_token - 2) // (max_w + 1) |
| 111 | if max_h % 2 == 1: |
| 112 | max_h -= 1 |
| 113 | best_width = max_w * patch_size * downsample_ratio |
| 114 | best_height = max_h * patch_size * downsample_ratio |
| 115 | elif max_h_float < 2.0: |
| 116 | max_h = 2 |
| 117 | max_w = ((max_n_token - 2) // max_h) - 1 |
| 118 | assert max_w > 1 |
| 119 | best_width = max_w * patch_size * downsample_ratio |
| 120 | best_height = max_h * patch_size * downsample_ratio |
| 121 | else: |
| 122 | max_w = math.floor(max_w_float) |
| 123 | max_h = math.floor(max_h_float) |
| 124 | if max_h % 2 == 1: |
| 125 | max_h -= 1 |
| 126 | beta = min( |
| 127 | max_w * patch_size * downsample_ratio / width, |
| 128 | max_h * patch_size * downsample_ratio / height, |
| 129 | ) |
| 130 | best_width = math.floor(width * beta / patch_size) * patch_size |
| 131 | best_height = math.floor(height * beta / patch_size) * patch_size |
| 132 | n_llm_h, n_llm_w, num_tokens = grid_tokens( |
| 133 | best_height, best_width, patch_size, downsample_ratio |
| 134 | ) |
| 135 | return n_llm_h, n_llm_w, best_height, best_width, num_tokens |
| 136 | |
| 137 | |
| 138 | def safe_resize( |
| 139 | height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token |
| 140 | ): |
| 141 | max_n_token -= COMPRESS_PAD_TO - 1 |
| 142 | n_llm_h, n_llm_w, num_tokens = grid_tokens( |
| 143 | best_height, best_width, patch_size, downsample_ratio |
| 144 | ) |
| 145 | budget = max_n_token |
| 146 | while num_tokens > max_n_token: |
| 147 | n_llm_h, n_llm_w, best_height, best_width, num_tokens = solve_resize_ratio( |
| 148 | height, width, patch_size, downsample_ratio, budget |
| 149 | ) |
| 150 | budget -= 1 |
| 151 | return n_llm_h, n_llm_w, best_height, best_width |
| 152 | |
| 153 | |
| 154 | def load_image( |
| 155 | image: Image.Image, |
| 156 | *, |
| 157 | patch_size: int, |
| 158 | downsample_ratio: int, |
| 159 | max_n_token: int, |
| 160 | min_pixels: int, |
| 161 | max_wh_ratio: float | None, |
| 162 | ): |
| 163 | """Transform one PIL image into ViT patches. |
| 164 | |
| 165 | Same math as the reference ``load_image``, except the image is already |
| 166 | decoded (vLLM supplies PIL images instead of a record dict). |
| 167 | """ |
| 168 | p = patch_size |
| 169 | image = image.convert("RGB") |
| 170 | width, height = image.size |
| 171 | if max_wh_ratio is not None and width > height * max_wh_ratio: |
| 172 | width = height * max_wh_ratio |
| 173 | if 0 < width * height < min_pixels: |
| 174 | ratio = (min_pixels / (width * height)) ** 0.5 |
| 175 | width = int(width * ratio) |
| 176 | height = int(height * ratio) |
| 177 | best_width = math.ceil(width / p) * p |
| 178 | best_height = math.ceil(height / p) * p |
| 179 | n_llm_h, n_llm_w, best_height, best_width = safe_resize( |
| 180 | height, width, best_height, best_width, p, downsample_ratio, max_n_token |
| 181 | ) |
| 182 | n_vit_h, n_vit_w = best_height // p, best_width // p |
| 183 | if max_wh_ratio is not None and image.width >= max_wh_ratio * image.height: |
| 184 | image = image.resize((best_width, best_height)) |
| 185 | else: |
| 186 | image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127)) |
| 187 | x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255 |
| 188 | x = ((x - 0.5) / 0.5).to(torch.bfloat16) |
| 189 | patches = ( |
| 190 | x.reshape(3, n_vit_h, p, n_vit_w, p) |
| 191 | .permute(1, 3, 0, 2, 4) |
| 192 | .reshape(n_vit_h * n_vit_w, 3, p, p) |
| 193 | ) |
| 194 | return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w |
| 195 | |
| 196 | |
| 197 | def build_image_block(n_llm_h: int, n_llm_w: int, start_pos: int): |
| 198 | """Builds the N-layout token types (final order) and the aligner-row order |
| 199 | for IMAGE slots.""" |
| 200 | compress_pad = COMPRESS_PAD_TO - 1 - start_pos % COMPRESS_PAD_TO |
| 201 | pad_h = n_llm_h % 2 |
| 202 | rows = n_llm_h + pad_h |
| 203 | row_len = n_llm_w + 1 |
| 204 | pad_last = rows // 2 * row_len % 2 * 2 |
| 205 | types = torch.tensor( |
| 206 | ([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h |
| 207 | + [IMAGE_PAD] * (row_len * pad_h), |
| 208 | dtype=torch.int64, |
| 209 | ) |
| 210 | order = torch.arange(rows * row_len).view(rows // 2, 2, row_len) |
| 211 | order = order.transpose(1, 2).reshape(-1) |
| 212 | image_idx = torch.full((rows * row_len,), -1, dtype=torch.int64) |
| 213 | image_idx.view(rows, row_len)[:n_llm_h, :n_llm_w] = torch.arange( |
| 214 | n_llm_h * n_llm_w |
| 215 | ).view(n_llm_h, n_llm_w) |
| 216 | perm = image_idx[order] |
| 217 | perm = perm[perm >= 0] |
| 218 | types = torch.cat( |
| 219 | [ |
| 220 | torch.full((compress_pad,), IMAGE_PAD, dtype=torch.int64), |
| 221 | torch.tensor([IMAGE_START]), |
| 222 | types[order], |
| 223 | torch.full((pad_last,), IMAGE_PAD, dtype=torch.int64), |
| 224 | torch.tensor([IMAGE_END]), |
| 225 | ] |
| 226 | ) |
| 227 | return types, perm |
| 228 | |
| 229 | |
| 230 | def build_image_block_pad_free(n_llm_h: int, n_llm_w: int): |
| 231 | """``build_image_block`` without the position-dependent compressor pad. |
| 232 | |
| 233 | ``start_pos`` is chosen so that ``compress_pad == 0``; the pad is instead |
| 234 | prepended when the block is spliced into the final prompt, where its |
| 235 | position is known. |
| 236 | """ |
| 237 | return build_image_block(n_llm_h, n_llm_w, COMPRESS_PAD_TO - 1) |
| 238 | |
| 239 | |
| 240 | class DeepseekV4VLImageProcessor: |
| 241 | """Per-image transform (the PIL-input equivalent of the reference |
| 242 | ``load_image``).""" |
| 243 | |
| 244 | def __init__(self, config: DeepseekV4Config) -> None: |
| 245 | super().__init__() |
| 246 | self.patch_size = config.vision_patch_size |
| 247 | self.downsample_ratio = config.vision_downsample_ratio |
| 248 | self.max_n_token = config.vision_max_n_token |
| 249 | self.min_pixels = config.vision_min_pixels |
| 250 | self.max_wh_ratio = config.vision_max_wh_ratio |
| 251 | |
| 252 | def __call__(self, image: Image.Image): |
| 253 | return load_image( |
| 254 | image, |
| 255 | patch_size=self.patch_size, |
| 256 | downsample_ratio=self.downsample_ratio, |
| 257 | max_n_token=self.max_n_token, |
| 258 | min_pixels=self.min_pixels, |
| 259 | max_wh_ratio=self.max_wh_ratio, |
| 260 | ) |
| 261 | |
| 262 | |
| 263 | class DeepseekV4VLProcessor: |
| 264 | """Minimal stand-in for the HF processor of DeepSeek-V4 vision models. |
| 265 | |
| 266 | The official repository ships image preprocessing as plain functions in |
| 267 | ``image_processor.py`` (no ``auto_map`` processor), so this class wraps |
| 268 | their ports directly and the model loads without ``--trust-remote-code``. |
| 269 | |
| 270 | ``__call__`` returns a ``BatchFeature`` with one entry per image |
| 271 | (flattened across images): |
| 272 | |
| 273 | - ``patches``: ``(sum(n_vit_h * n_vit_w), 3, p, p)`` bf16 ViT patches. |
| 274 | - ``vit_grid``: ``(num_images, 2)`` int64 ``[n_vit_h, n_vit_w]``. |
| 275 | - ``llm_grid``: ``(num_images, 2)`` int64 ``[n_llm_h, n_llm_w]``. |
| 276 | - ``perm``: concatenated per-image ``(n_llm_h * n_llm_w,)`` int64 index |
| 277 | selecting aligner outputs into the final N-layout order. |
| 278 | - ``types``: concatenated per-image pad-free sentinel block types; |
| 279 | ``block ids = IMAGE_SENTINEL_BASE_ID + types``. |
| 280 | """ |
| 281 | |
| 282 | def __init__(self, config: DeepseekV4Config) -> None: |
| 283 | super().__init__() |
| 284 | self.config = config |
| 285 | self.image_processor = DeepseekV4VLImageProcessor(config) |
| 286 | |
| 287 | def __call__( |
| 288 | self, |
| 289 | text: str | None = None, |
| 290 | images: Sequence[Image.Image] | None = None, |
| 291 | return_tensors: str | None = None, |
| 292 | **kwargs: Any, |
| 293 | ) -> BatchFeature: |
| 294 | patches_list = [] |
| 295 | vit_grid = [] |
| 296 | llm_grid = [] |
| 297 | perm_list = [] |
| 298 | types_list = [] |
| 299 | for image in images or []: |
| 300 | patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = self.image_processor(image) |
| 301 | types, perm = build_image_block_pad_free(n_llm_h, n_llm_w) |
| 302 | patches_list.append(patches) |
| 303 | vit_grid.append((n_vit_h, n_vit_w)) |
| 304 | llm_grid.append((n_llm_h, n_llm_w)) |
| 305 | perm_list.append(perm) |
| 306 | types_list.append(types) |
| 307 | |
| 308 | if not patches_list: |
| 309 | return BatchFeature({}) |
| 310 | |
| 311 | return BatchFeature( |
| 312 | { |
| 313 | "patches": torch.cat(patches_list), |
| 314 | "vit_grid": torch.tensor(vit_grid, dtype=torch.int64), |
| 315 | "llm_grid": torch.tensor(llm_grid, dtype=torch.int64), |
| 316 | "perm": torch.cat(perm_list), |
| 317 | "types": torch.cat(types_list), |
| 318 | } |
| 319 | ) |
| 320 | |
| 321 | |
| 322 | class DeepseekV4VLProcessingInfo(BaseProcessingInfo): |
| 323 | def get_hf_config(self) -> DeepseekV4Config: |
| 324 | return self.ctx.get_hf_config(DeepseekV4Config) |
| 325 | |
| 326 | def get_hf_processor(self, **kwargs: object) -> DeepseekV4VLProcessor: |
| 327 | if kwargs: |
| 328 | raise ValueError(f"Unexpected processor kwargs: {sorted(kwargs)}") |
| 329 | return DeepseekV4VLProcessor(self.get_hf_config()) |
| 330 | |
| 331 | def get_supported_mm_limits(self) -> Mapping[str, int | None]: |
| 332 | return {"image": None} |
| 333 | |
| 334 | def get_mm_max_tokens_per_item( |
| 335 | self, |
| 336 | seq_len: int, |
| 337 | mm_counts: Mapping[str, int], |
| 338 | ) -> Mapping[str, int]: |
| 339 | # ``safe_resize`` reserves COMPRESS_PAD_TO - 1 tokens of |
| 340 | # vision_max_n_token for the compressor-alignment pad, so the full |
| 341 | # sentinel block is bounded by vision_max_n_token; the margin is kept |
| 342 | # in case that reservation changes. |
| 343 | return { |
| 344 | "image": self.get_hf_config().vision_max_n_token + COMPRESS_PAD_TO - 1, |
| 345 | } |
| 346 | |
| 347 | def get_image_placeholder_token_id(self) -> int: |
| 348 | token_id = self.get_tokenizer().convert_tokens_to_ids(IMAGE_PLACEHOLDER) |
| 349 | if token_id is None: |
| 350 | raise ValueError(f"Token not found in tokenizer: {IMAGE_PLACEHOLDER}") |
| 351 | return token_id |
| 352 | |
| 353 | def get_image_size_with_most_features(self) -> ImageSize: |
| 354 | hf_config = self.get_hf_config() |
| 355 | patch_size = hf_config.vision_patch_size |
| 356 | downsample_ratio = hf_config.vision_downsample_ratio |
| 357 | max_wh_ratio = hf_config.vision_max_wh_ratio |
| 358 | # Every aligner row costs an extra IMAGE_NEW_LINE token, so height is |
| 359 | # more expensive than width and a square is *not* optimal: search the |
| 360 | # LLM grids that fit the budget for the one with the most ViT patches |
| 361 | # (ties broken by the longer sentinel block). |
| 362 | budget = hf_config.vision_max_n_token - (COMPRESS_PAD_TO - 1) |
| 363 | step = patch_size * downsample_ratio |
| 364 | best_grid = (1, 1) |
| 365 | best_key = (0, 0) |
| 366 | for n_llm_h in range(1, budget): |
| 367 | if ( |
| 368 | grid_tokens(n_llm_h * step, step, patch_size, downsample_ratio)[2] |
| 369 | > budget |
| 370 | ): |
| 371 | break |
| 372 | for n_llm_w in range(1, budget): |
| 373 | if max_wh_ratio is not None and n_llm_w > n_llm_h * max_wh_ratio: |
| 374 | break |
| 375 | num_tokens = grid_tokens( |
| 376 | n_llm_h * step, n_llm_w * step, patch_size, downsample_ratio |
| 377 | )[2] |
| 378 | if num_tokens > budget: |
| 379 | break |
| 380 | key = (n_llm_h * n_llm_w, num_tokens) |
| 381 | if key > best_key: |
| 382 | best_key, best_grid = key, (n_llm_h, n_llm_w) |
| 383 | n_llm_h, n_llm_w = best_grid |
| 384 | return ImageSize(width=n_llm_w * step, height=n_llm_h * step) |
| 385 | |
| 386 | |
| 387 | class DeepseekV4VLDummyInputsBuilder( |
| 388 | BaseDummyInputsBuilder[DeepseekV4VLProcessingInfo] |
| 389 | ): |
| 390 | def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str: |
| 391 | return IMAGE_PLACEHOLDER * mm_counts.get("image", 0) |
| 392 | |
| 393 | def get_dummy_mm_data( |
| 394 | self, |
| 395 | seq_len: int, |
| 396 | mm_counts: Mapping[str, int], |
| 397 | mm_options: MultiModalDummyOptions, |
| 398 | ) -> MultiModalDataDict: |
| 399 | size = self.info.get_image_size_with_most_features() |
| 400 | return { |
| 401 | "image": self._get_dummy_images( |
| 402 | width=size.width, |
| 403 | height=size.height, |
| 404 | num_images=mm_counts.get("image", 0), |
| 405 | overrides=mm_options.get("image"), |
| 406 | ), |
| 407 | } |
| 408 | |
| 409 | |
| 410 | class DeepseekV4VLMultiModalProcessor( |
| 411 | BaseMultiModalProcessor[DeepseekV4VLProcessingInfo] |
| 412 | ): |
| 413 | def _get_mm_fields_config( |
| 414 | self, |
| 415 | hf_inputs: BatchFeature, |
| 416 | hf_processor_mm_kwargs: Mapping[str, object], |
| 417 | ) -> Mapping[str, MultiModalFieldConfig]: |
| 418 | vit_grid = hf_inputs.get("vit_grid") |
| 419 | llm_grid = hf_inputs.get("llm_grid") |
| 420 | |
| 421 | if vit_grid is None or llm_grid is None: |
| 422 | empty = torch.empty(0, dtype=torch.long) |
| 423 | patch_sizes = perm_sizes = types_sizes = empty |
| 424 | else: |
| 425 | patch_sizes = vit_grid.prod(-1) |
| 426 | perm_sizes = llm_grid.prod(-1) |
| 427 | n_llm_h, n_llm_w = llm_grid[:, 0], llm_grid[:, 1] |
| 428 | # Pad-free block length; same formula as ``grid_tokens`` given |
| 429 | # the LLM grid. |
| 430 | types_sizes = ( |
| 431 | n_llm_h * (n_llm_w + 1) |
| 432 | + 2 |
| 433 | + (n_llm_h % 2) * (n_llm_w + 1) |
| 434 | + (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2 |
| 435 | ) |
| 436 | |
| 437 | return { |
| 438 | "patches": MultiModalFieldConfig.flat_from_sizes("image", patch_sizes), |
| 439 | "vit_grid": MultiModalFieldConfig.batched("image", keep_on_cpu=True), |
| 440 | "llm_grid": MultiModalFieldConfig.batched("image", keep_on_cpu=True), |
| 441 | "perm": MultiModalFieldConfig.flat_from_sizes("image", perm_sizes), |
| 442 | "types": MultiModalFieldConfig.flat_from_sizes( |
| 443 | "image", types_sizes, keep_on_cpu=True |
| 444 | ), |
| 445 | } |
| 446 | |
| 447 | def _get_prompt_updates( |
| 448 | self, |
| 449 | mm_items: MultiModalDataItems, |
| 450 | hf_processor_mm_kwargs: Mapping[str, object], |
| 451 | out_mm_kwargs: MultiModalKwargsItems, |
| 452 | ) -> Sequence[PromptUpdate]: |
| 453 | image_token_id = self.info.get_image_placeholder_token_id() |
| 454 | validate_image_sentinel_ids(self.info.get_tokenizer()) |
| 455 | image_embed_id = IMAGE_SENTINEL_BASE_ID + IMAGE |
| 456 | |
| 457 | def get_image_replacement(item_idx: int) -> PromptUpdateDetails: |
| 458 | types: torch.Tensor = out_mm_kwargs["image"][item_idx]["types"].data |
| 459 | full = (IMAGE_SENTINEL_BASE_ID + types).tolist() |
| 460 | return PromptUpdateDetails.select_token_id(full, image_embed_id) |
| 461 | |
| 462 | return [ |
| 463 | PromptReplacement( |
| 464 | modality="image", |
| 465 | target=[image_token_id], |
| 466 | replacement=get_image_replacement, |
| 467 | ), |
| 468 | ] |
| 469 | |
| 470 | def _apply_token_matches_with_placeholders( |
| 471 | self, |
| 472 | token_ids: list[int], |
| 473 | mm_prompt_updates: MultiModalPromptUpdates, |
| 474 | ) -> tuple[ |
| 475 | list[int], |
| 476 | MultiModalPromptUpdatesApplyResult, |
| 477 | Mapping[str, list[PlaceholderFeaturesInfo]], |
| 478 | ]: |
| 479 | # Same as the base implementation, except that each image block gets |
| 480 | # its compressor-alignment pad (``3 - start_pos % 4`` IMAGE_PAD |
| 481 | # sentinels) prepended while splicing: the pad depends on the block's |
| 482 | # final position in the prompt, which is unknown when the (cacheable) |
| 483 | # prompt updates are built. |
| 484 | matched_updates, result = _plan_prompt_updates(token_ids, mm_prompt_updates) |
| 485 | placeholders: dict[str, list[PlaceholderFeaturesInfo]] = { |
| 486 | modality: [] for modality in mm_prompt_updates |
| 487 | } |
| 488 | |
| 489 | pad_id = IMAGE_SENTINEL_BASE_ID + IMAGE_PAD |
| 490 | |
| 491 | new_token_ids = list[int]() |
| 492 | prev_end_idx = 0 |
| 493 | for matched_update in matched_updates: |
| 494 | update = matched_update.update |
| 495 | match = matched_update.match |
| 496 | |
| 497 | if update.mode == UpdateMode.INSERT: |
| 498 | end_idx_to_insert = match.end_idx |
| 499 | elif update.mode == UpdateMode.REPLACE: |
| 500 | end_idx_to_insert = match.start_idx |
| 501 | else: |
| 502 | assert_never(update.mode) |
| 503 | |
| 504 | new_token_ids.extend(token_ids[prev_end_idx:end_idx_to_insert]) |
| 505 | start_idx = len(new_token_ids) |
| 506 | |
| 507 | tokens = list(update.content.full) |
| 508 | if tokens and update.modality == "image": |
| 509 | compress_pad = COMPRESS_PAD_TO - 1 - start_idx % COMPRESS_PAD_TO |
| 510 | tokens = [pad_id] * compress_pad + tokens |
| 511 | |
| 512 | if tokens: |
| 513 | content_is_embed = update.content.is_embed |
| 514 | is_embed = ( |
| 515 | content_is_embed(tokens) if content_is_embed is not None else None |
| 516 | ) |
| 517 | placeholders[update.modality].append( |
| 518 | PlaceholderFeaturesInfo( |
| 519 | modality=update.modality, |
| 520 | item_idx=update.item_idx, |
| 521 | start_idx=start_idx, |
| 522 | tokens=tokens, |
| 523 | is_embed=is_embed, |
| 524 | ) |
| 525 | ) |
| 526 | new_token_ids.extend(tokens) |
| 527 | |
| 528 | prev_end_idx = match.end_idx |
| 529 | |
| 530 | new_token_ids.extend(token_ids[prev_end_idx:]) |
| 531 | return new_token_ids, result, placeholders |