@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 0377802c20
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[Multimodal] Remove legacy multimodal fields in favor of MultiModalFeatureSpec (#24548)

Signed-off-by: sfeng33 <4florafeng@gmail.com>

Flora Feng committed Sep 12, 2025 at 06:42 UTC 0377802c20df2528d895add1f9918b2c7b868eb0
13 files changed +102 -116
tests/v1/core/test_encoder_cache_manager.py
+11 -1
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 +from vllm.multimodal.inputs import MultiModalFeatureSpec, PlaceholderRange
5 from vllm.v1.core.encoder_cache_manager import EncoderCacheManager
6
7
@@ -9,8 +10,17 @@ class MockRequest:
10
11 def __init__(self, request_id, mm_hashes, token_counts):
12 self.request_id = request_id
12 - self.mm_hashes = mm_hashes
13 self._token_counts = token_counts
14 + self.mm_features = []
15 + for i, mm_hash in enumerate(mm_hashes):
16 + feature = MultiModalFeatureSpec(
17 + data=None,
18 + modality="image",
19 + identifier=mm_hash,
20 + mm_position=PlaceholderRange(offset=0,
21 + length=self._token_counts[i]),
22 + )
23 + self.mm_features.append(feature)
24
25 def get_num_encoder_tokens(self, input_id: int) -> int:
26 return self._token_counts[input_id]
tests/v1/tpu/worker/test_tpu_model_runner.py
+1 -3
@@ -64,9 +64,7 @@ def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
64 NewRequestData(
65 req_id=req_id,
66 prompt_token_ids=[1, 2, 3],
67 - mm_kwargs=[],
68 - mm_hashes=[],
69 - mm_positions=[],
67 + mm_features=[],
68 sampling_params=SamplingParams(),
69 pooling_params=PoolingParams(),
70 block_ids=([0], ), # block_ids should be tuple[list[int]]
tests/v1/worker/test_gpu_input_batch.py
+1 -3
@@ -203,9 +203,7 @@ def _construct_cached_request_state(req_id_suffix: int):
203 prompt_token_ids=prompt_token_ids,
204 sampling_params=_create_sampling_params(),
205 pooling_params=None,
206 - mm_kwargs=[],
207 - mm_positions=[],
208 - mm_hashes=[],
206 + mm_features=[],
207 block_ids=([], ),
208 generator=None,
209 num_computed_tokens=len(output_token_ids),
tests/v1/worker/test_gpu_model_runner.py
+1 -3
@@ -118,9 +118,7 @@ def _schedule_new_request(*req_ids: str) -> SchedulerOutput:
118 NewRequestData(
119 req_id=req_id,
120 prompt_token_ids=[1, 2, 3],
121 - mm_kwargs=[],
122 - mm_hashes=[],
123 - mm_positions=[],
121 + mm_features=[],
122 sampling_params=SamplingParams(),
123 pooling_params=None,
124 block_ids=([0], ),
vllm/distributed/kv_transfer/kv_connector/v1/shared_storage_connector.py
+22 -19
@@ -300,11 +300,12 @@ class SharedStorageConnector(KVConnectorBase_V1):
300 total_need_load = 0
301 for new_req in scheduler_output.scheduled_new_reqs:
302 if new_req.req_id in self._requests_need_load:
303 - meta.add_request(token_ids=new_req.prompt_token_ids,
304 - block_ids=new_req.block_ids[0],
305 - block_size=self._block_size,
306 - is_store=False,
307 - mm_hashes=new_req.mm_hashes)
303 + meta.add_request(
304 + token_ids=new_req.prompt_token_ids,
305 + block_ids=new_req.block_ids[0],
306 + block_size=self._block_size,
307 + is_store=False,
308 + mm_hashes=[f.identifier for f in new_req.mm_features])
309 total_need_load += 1
310 else:
311 # NOTE: here, we set the store and load being exclusive,
@@ -312,11 +313,12 @@ class SharedStorageConnector(KVConnectorBase_V1):
313 # NOTE(rob): for this debug implementation, we only cache
314 # the original prompt tokens.
315 if not self._found_match_for_request(new_req):
315 - meta.add_request(token_ids=new_req.prompt_token_ids,
316 - block_ids=new_req.block_ids[0],
317 - block_size=self._block_size,
318 - is_store=True,
319 - mm_hashes=new_req.mm_hashes)
316 + meta.add_request(
317 + token_ids=new_req.prompt_token_ids,
318 + block_ids=new_req.block_ids[0],
319 + block_size=self._block_size,
320 + is_store=True,
321 + mm_hashes=[f.identifier for f in new_req.mm_features])
322
323 cached_reqs = scheduler_output.scheduled_cached_reqs
324 for i, req_id in enumerate(cached_reqs.req_ids):
@@ -341,11 +343,12 @@ class SharedStorageConnector(KVConnectorBase_V1):
343 # of the block_ids for the request.
344 block_ids = new_block_ids[0]
345
344 - meta.add_request(token_ids=token_ids,
345 - block_ids=block_ids,
346 - block_size=self._block_size,
347 - is_store=False,
348 - mm_hashes=request.mm_hashes)
346 + meta.add_request(
347 + token_ids=token_ids,
348 + block_ids=block_ids,
349 + block_size=self._block_size,
350 + is_store=False,
351 + mm_hashes=[f.identifier for f in request.mm_features])
352 total_need_load += 1
353
354 assert total_need_load == len(self._requests_need_load)
@@ -364,10 +367,10 @@ class SharedStorageConnector(KVConnectorBase_V1):
367 """
368 num_tokens_to_check = align_to_block_size(
369 len(request.prompt_token_ids) - 1, self._block_size)
367 - foldername = self._generate_foldername_debug(torch.tensor(
368 - request.prompt_token_ids)[:num_tokens_to_check],
369 - request.mm_hashes,
370 - create_folder=False)
370 + foldername = self._generate_foldername_debug(
371 + torch.tensor(request.prompt_token_ids)[:num_tokens_to_check],
372 + [f.identifier for f in request.mm_features],
373 + create_folder=False)
374 return os.path.exists(foldername)
375
376 def _generate_foldername_debug(
vllm/v1/core/encoder_cache_manager.py
+5 -5
@@ -86,7 +86,7 @@ class EncoderCacheManager:
86 Returns:
87 True if the encoder output for this input is already cached
88 """
89 - mm_hash = request.mm_hashes[input_id]
89 + mm_hash = request.mm_features[input_id].identifier
90 # Not cached at all
91 if mm_hash not in self.cached:
92 return False
@@ -167,7 +167,7 @@ class EncoderCacheManager:
167 This method assumes can_allocate() returned True for the same input.
168 """
169
170 - mm_hash = request.mm_hashes[input_id]
170 + mm_hash = request.mm_features[input_id].identifier
171 request_id = request.request_id
172 if mm_hash not in self.cached:
173 self.cached[mm_hash] = set()
@@ -193,8 +193,8 @@ class EncoderCacheManager:
193 """
194 return {
195 input_id
196 - for input_id in range(len(request.mm_hashes))
197 - if request.mm_hashes[input_id] in self.cached
196 + for input_id in range(len(request.mm_features))
197 + if request.mm_features[input_id].identifier in self.cached
198 }
199
200 def free_encoder_input(self, request: Request, input_id: int) -> None:
@@ -208,7 +208,7 @@ class EncoderCacheManager:
208 `can_allocate`).
209 """
210 req_id = request.request_id
211 - mm_hash = request.mm_hashes[input_id]
211 + mm_hash = request.mm_features[input_id].identifier
212 # The mm_hash not in cache or the req_id set is empty
213 if not self.cached.get(mm_hash, None):
214 return
vllm/v1/core/kv_cache_utils.py
+16 -20
@@ -418,9 +418,9 @@ def need_extra_keys(request: Request) -> bool:
418 # Multimodal requests need to include the MM hash.
419 # LoRA requests need to include the LoRA ID.
420 # Request with provided cache salt need to include the salt.
421 - return bool(request.mm_hashes) or (request.lora_request
422 - is not None) or (request.cache_salt
423 - is not None)
421 + return bool(request.mm_features) or (request.lora_request
422 + is not None) or (request.cache_salt
423 + is not None)
424
425
426 def _gen_mm_extra_hash_keys(request: Request, start_token_idx: int,
@@ -442,32 +442,28 @@ def _gen_mm_extra_hash_keys(request: Request, start_token_idx: int,
442 """
443 extra_keys: list[Any] = []
444
445 - mm_positions, mm_hashes = request.mm_positions, request.mm_hashes
446 - if not mm_positions:
445 + mm_features = request.mm_features
446 + if not mm_features:
447 return extra_keys, start_mm_idx
448
449 - if mm_positions and len(mm_positions) != len(mm_hashes):
450 - raise ValueError(
451 - "The number of multi-modal positions and hashes must match. This "
452 - "is likely because you did not enable MM hashing. "
453 - "Please set `mm_processor_cache_gb > 0`.")
454 -
455 - # Note that we assume mm_positions is sorted by offset.
449 + # Note that we assume mm_features are sorted by mm_position.offset.
450 # We do not need to check all mm inputs if the start token index is out of
451 # range. This usually happens in the late prefill phase and decoding phase.
458 - if mm_positions[-1].offset + mm_positions[-1].length < start_token_idx:
452 + last_pos = mm_features[-1].mm_position
453 + if last_pos.offset + last_pos.length < start_token_idx:
454 return extra_keys, start_mm_idx
455
456 # Support start_mm_idx == -1 to indicate the last mm input.
457 if start_mm_idx < 0:
463 - assert -start_mm_idx <= len(mm_positions)
464 - start_mm_idx = len(mm_positions) + start_mm_idx
458 + assert -start_mm_idx <= len(mm_features)
459 + start_mm_idx = len(mm_features) + start_mm_idx
460
461 curr_mm_idx = start_mm_idx
467 - while mm_positions and curr_mm_idx < len(mm_positions):
468 - assert mm_hashes[curr_mm_idx] is not None
469 - offset = mm_positions[curr_mm_idx].offset
470 - length = mm_positions[curr_mm_idx].length
462 + while mm_features and curr_mm_idx < len(mm_features):
463 + mm_feature = mm_features[curr_mm_idx]
464 + assert mm_feature.identifier is not None
465 + offset = mm_feature.mm_position.offset
466 + length = mm_feature.mm_position.length
467 if end_token_idx > offset:
468 if start_token_idx > offset + length:
469 # This block has passed the current mm input.
@@ -475,7 +471,7 @@ def _gen_mm_extra_hash_keys(request: Request, start_token_idx: int,
471 continue
472
473 # The block contains the current mm input.
478 - extra_keys.append(mm_hashes[curr_mm_idx])
474 + extra_keys.append(mm_feature.identifier)
475
476 if end_token_idx >= offset + length:
477 # If this block contains the end of the current mm input,
vllm/v1/core/sched/output.py
+5 -13
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15 from vllm.distributed.kv_transfer.kv_connector.v1.base import (
16 KVConnectorMetadata)
17 from vllm.lora.request import LoRARequest
18 - from vllm.multimodal.inputs import MultiModalKwargsItem, PlaceholderRange
18 + from vllm.multimodal.inputs import MultiModalFeatureSpec
19 from vllm.pooling_params import PoolingParams
20 from vllm.sampling_params import SamplingParams
21 from vllm.v1.request import Request
@@ -27,9 +27,7 @@ class NewRequestData:
27
28 req_id: str
29 prompt_token_ids: list[int]
30 - mm_kwargs: list[MultiModalKwargsItem]
31 - mm_hashes: list[str]
32 - mm_positions: list[PlaceholderRange]
30 + mm_features: list[MultiModalFeatureSpec]
31 sampling_params: Optional[SamplingParams]
32 pooling_params: Optional[PoolingParams]
33 block_ids: tuple[list[int], ...]
@@ -45,9 +43,7 @@ class NewRequestData:
43 return cls(
44 req_id=request.request_id,
45 prompt_token_ids=request.prompt_token_ids,
48 - mm_kwargs=request.mm_kwargs,
49 - mm_hashes=request.mm_hashes,
50 - mm_positions=request.mm_positions,
46 + mm_features=request.mm_features,
47 sampling_params=request.sampling_params,
48 pooling_params=request.pooling_params,
49 block_ids=block_ids,
@@ -59,9 +55,7 @@ class NewRequestData:
55 return (f"NewRequestData("
56 f"req_id={self.req_id},"
57 f"prompt_token_ids={self.prompt_token_ids},"
62 - f"mm_kwargs={self.mm_kwargs},"
63 - f"mm_hashes={self.mm_hashes},"
64 - f"mm_positions={self.mm_positions},"
58 + f"mm_features={self.mm_features},"
59 f"sampling_params={self.sampling_params},"
60 f"block_ids={self.block_ids},"
61 f"num_computed_tokens={self.num_computed_tokens},"
@@ -73,9 +67,7 @@ class NewRequestData:
67 return (f"NewRequestData("
68 f"req_id={self.req_id},"
69 f"prompt_token_ids_len={len(self.prompt_token_ids)},"
76 - f"mm_kwargs={self.mm_kwargs},"
77 - f"mm_hashes={self.mm_hashes},"
78 - f"mm_positions={self.mm_positions},"
70 + f"mm_features={self.mm_features},"
71 f"sampling_params={self.sampling_params},"
72 f"block_ids={self.block_ids},"
73 f"num_computed_tokens={self.num_computed_tokens},"
vllm/v1/core/sched/scheduler.py
+11 -11
@@ -736,18 +736,18 @@ class Scheduler(SchedulerInterface):
736 if num_new_tokens == 0 or not request.has_encoder_inputs:
737 return [], num_new_tokens, encoder_compute_budget
738 encoder_inputs_to_schedule: list[int] = []
739 - mm_positions = request.mm_positions
740 - assert mm_positions is not None
741 - assert len(mm_positions) > 0
739 + mm_features = request.mm_features
740 + assert mm_features is not None
741 + assert len(mm_features) > 0
742
743 # NOTE: since scheduler operates on the request level (possibly with
744 # multiple encoder inputs per request), we need to create temporary
745 # trackers for accounting at the encoder input level.
746 mm_hashes_to_schedule = set()
747 num_tokens_to_schedule = 0
748 - for i, pos_info in enumerate(mm_positions):
749 - start_pos = pos_info.offset
750 - num_encoder_tokens = pos_info.length
748 + for i, mm_feature in enumerate(mm_features):
749 + start_pos = mm_feature.mm_position.offset
750 + num_encoder_tokens = mm_feature.mm_position.length
751
752 # The encoder output is needed if the two ranges overlap:
753 # [num_computed_tokens, num_computed_tokens + num_new_tokens) and
@@ -778,7 +778,7 @@ class Scheduler(SchedulerInterface):
778 if not self.is_encoder_decoder:
779 # We are not using the encoder cache for encoder-decoder models,
780 # yet.
781 - if request.mm_hashes[i] in mm_hashes_to_schedule:
781 + if request.mm_features[i].identifier in mm_hashes_to_schedule:
782 # The same encoder input has already been scheduled in the
783 # current step.
784 continue
@@ -820,7 +820,7 @@ class Scheduler(SchedulerInterface):
820
821 num_tokens_to_schedule += num_encoder_tokens
822 encoder_compute_budget -= num_encoder_tokens
823 - mm_hashes_to_schedule.add(request.mm_hashes[i])
823 + mm_hashes_to_schedule.add(request.mm_features[i].identifier)
824 encoder_inputs_to_schedule.append(i)
825
826 return (
@@ -1048,9 +1048,9 @@ class Scheduler(SchedulerInterface):
1048 # Here, we use list(set) to avoid modifying the set while iterating
1049 # over it.
1050 for input_id in list(cached_encoder_input_ids):
1051 - mm_positions = request.mm_positions[input_id]
1052 - start_pos = mm_positions.offset
1053 - num_tokens = mm_positions.length
1051 + mm_feature = request.mm_features[input_id]
1052 + start_pos = mm_feature.mm_position.offset
1053 + num_tokens = mm_feature.mm_position.length
1054 if self.is_encoder_decoder and request.num_computed_tokens > 0:
1055 # With Whisper, as soon as we've generated a single token,
1056 # we know we're done with the encoder input. Cross Attention
vllm/v1/request.py
+2 -7
@@ -91,11 +91,6 @@ class Request:
91 self.mm_features = mm_features or []
92 self.num_encoder_inputs = len(self.mm_features)
93 self.has_encoder_inputs = self.num_encoder_inputs > 0
94 - # TODO(sfeng33): Remove these legacy fields after clearing out all
95 - # references in scheduler and model runner
96 - self.mm_positions = [f.mm_position for f in self.mm_features]
97 - self.mm_kwargs = [f.data for f in self.mm_features]
98 - self.mm_hashes = [f.identifier for f in self.mm_features]
94
95 # Read-only views
96 # Prevent directly appending to these lists since
@@ -180,8 +175,8 @@ class Request:
175 return RequestStatus.get_finished_reason(self.status)
176
177 def get_num_encoder_tokens(self, input_id: int) -> int:
183 - assert input_id < len(self.mm_positions)
184 - num_tokens = self.mm_positions[input_id].length
178 + assert input_id < len(self.mm_features)
179 + num_tokens = self.mm_features[input_id].mm_position.length
180 return num_tokens
181
182 def record_event(
vllm/v1/worker/gpu_input_batch.py
+4 -6
@@ -10,8 +10,7 @@ import torch
10 from typing_extensions import deprecated
11
12 from vllm.lora.request import LoRARequest
13 -from vllm.multimodal.inputs import (MultiModalKwargsItem,
14 - MultiModalKwargsItems, PlaceholderRange)
13 +from vllm.multimodal.inputs import MultiModalFeatureSpec, MultiModalKwargsItems
14 from vllm.pooling_params import PoolingParams
15 from vllm.sampling_params import SamplingParams, SamplingType
16 from vllm.utils import swap_dict_values
@@ -31,9 +30,7 @@ class CachedRequestState:
30
31 req_id: str
32 prompt_token_ids: list[int]
34 - mm_kwargs: list[MultiModalKwargsItem]
35 - mm_positions: list[PlaceholderRange]
36 - mm_hashes: list[str]
33 + mm_features: list[MultiModalFeatureSpec]
34 sampling_params: Optional[SamplingParams]
35 pooling_params: Optional[PoolingParams]
36 generator: Optional[torch.Generator]
@@ -60,7 +57,8 @@ class CachedRequestState:
57 "removed in v0.13. Please use `mm_kwargs` instead.")
58 def mm_inputs(self) -> list[MultiModalKwargsItems]:
59 return [
63 - MultiModalKwargsItems.from_seq([item]) for item in self.mm_kwargs
60 + MultiModalKwargsItems.from_seq([f.data]) for f in self.mm_features
61 + if f.data is not None
62 ]
63
64 def get_token_id(self, idx: int) -> int:
vllm/v1/worker/gpu_model_runner.py
+15 -13
@@ -555,9 +555,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
555 req_state = CachedRequestState(
556 req_id=req_id,
557 prompt_token_ids=new_req_data.prompt_token_ids,
558 - mm_kwargs=new_req_data.mm_kwargs,
559 - mm_positions=new_req_data.mm_positions,
560 - mm_hashes=new_req_data.mm_hashes,
558 + mm_features=new_req_data.mm_features,
559 sampling_params=sampling_params,
560 pooling_params=pooling_params,
561 generator=generator,
@@ -698,7 +696,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
696 second_per_grid_ts = []
697 audio_feature_lengths = []
698 use_audio_in_video = False
701 - for mm_item in req_state.mm_kwargs:
699 + for mm_feature in req_state.mm_features:
700 + mm_item = mm_feature.data
701 + if mm_item is None:
702 + continue
703 mm_input = mm_item.get_data()
704 if (t := mm_input.get("image_grid_thw")) is not None:
705 image_grid_thw.append(t.tolist())
@@ -731,7 +732,9 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
732
733 mm_kwargs = list[MultiModalKwargsItem]()
734 for req in scheduler_output.scheduled_new_reqs:
734 - mm_kwargs.extend(req.mm_kwargs)
735 + for feature in req.mm_features:
736 + if feature.data is not None:
737 + mm_kwargs.append(feature.data)
738
739 # Input all modalities at once
740 mm_kwargs_combined: BatchedTensorInputs = {}
@@ -1361,10 +1364,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1364 req_state = self.requests[req_id]
1365
1366 for mm_input_id in encoder_input_ids:
1364 - mm_hash = req_state.mm_hashes[mm_input_id]
1365 - mm_kwargs.append(req_state.mm_kwargs[mm_input_id])
1366 - mm_hashes_pos.append(
1367 - (mm_hash, req_state.mm_positions[mm_input_id]))
1367 + mm_feature = req_state.mm_features[mm_input_id]
1368 + mm_hash = mm_feature.identifier
1369 + mm_kwargs.append(mm_feature.data)
1370 + mm_hashes_pos.append((mm_hash, mm_feature.mm_position))
1371
1372 return mm_kwargs, mm_hashes_pos
1373
@@ -1426,9 +1429,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1429 req_state = self.requests[req_id]
1430 num_computed_tokens = \
1431 req_state.num_computed_tokens + shift_computed_tokens
1429 - mm_positions = req_state.mm_positions
1430 - mm_hashes = req_state.mm_hashes
1431 - for i, pos_info in enumerate(mm_positions):
1432 + for mm_feature in req_state.mm_features:
1433 + pos_info = mm_feature.mm_position
1434 start_pos = pos_info.offset
1435 num_encoder_tokens = pos_info.length
1436
@@ -1451,7 +1453,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1453 )
1454 assert start_idx < end_idx
1455
1454 - mm_hash = mm_hashes[i]
1456 + mm_hash = mm_feature.identifier
1457 encoder_output = self.encoder_cache.get(mm_hash, None)
1458 assert encoder_output is not None,\
1459 f"Encoder cache miss for {mm_hash}."
vllm/v1/worker/tpu_model_runner.py
+8 -12
@@ -387,9 +387,7 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
387 self.requests[req_id] = CachedRequestState(
388 req_id=req_id,
389 prompt_token_ids=new_req_data.prompt_token_ids,
390 - mm_kwargs=new_req_data.mm_kwargs,
391 - mm_positions=new_req_data.mm_positions,
392 - mm_hashes=new_req_data.mm_hashes,
390 + mm_features=new_req_data.mm_features,
391 sampling_params=sampling_params,
392 pooling_params=None,
393 generator=None,
@@ -822,10 +820,10 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
820 req_state = self.requests[req_id]
821
822 for mm_input_id in encoder_input_ids:
825 - mm_hash = req_state.mm_hashes[mm_input_id]
826 - mm_kwargs.append(req_state.mm_kwargs[mm_input_id])
827 - mm_hashes_pos.append(
828 - (mm_hash, req_state.mm_positions[mm_input_id]))
823 + mm_feature = req_state.mm_features[mm_input_id]
824 + mm_hash = mm_feature.identifier
825 + mm_kwargs.append(mm_feature.data)
826 + mm_hashes_pos.append((mm_hash, mm_feature.mm_position))
827
828 # Batch mm inputs as much as we can: if a request in the batch has
829 # multiple modalities or a different modality than the previous one,
@@ -883,13 +881,12 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
881 req_id]
882 req_state = self.requests[req_id]
883 num_computed_tokens = req_state.num_computed_tokens
886 - mm_positions = req_state.mm_positions
887 - mm_hashes = req_state.mm_hashes
884 # TODO unroll loop and assume/enforce --disable_chunked_mm_input
885 # NOTE (NickLucche) here we diverge from logic in other runners, as
886 # we assume to only have whole mm items to process. Hence we avoid
887 # the intrinsic dynamism that `gather_mm_placeholders` introduces.
892 - for i, pos_info in enumerate(mm_positions):
888 + for mm_feature in req_state.mm_features:
889 + pos_info = mm_feature.mm_position
890 start_pos = pos_info.offset
891 num_encoder_tokens = pos_info.length
892
@@ -904,8 +901,7 @@ class TPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
901 # The encoder output is already processed and stored
902 # in the decoder's KV cache.
903 continue
907 -
908 - mm_hash = mm_hashes[i]
904 + mm_hash = mm_feature.identifier
905 encoder_output = self.encoder_cache.get(mm_hash, None)
906 assert encoder_output is not None,\
907 f"Encoder cache miss for {mm_hash}."