[Core][Multimodal] Skip redundant placeholder scan when token match succeeds (#52925)
Co-authored-by: shenyiqin <shenyiqin1@huawei.com>
Yiqin committed
Aug 20, 2026 at 23:04 UTC
cb09dd7488d2ad13dbf610aa2a580af2e7b050b5
4 files changed
+539
-175
tests/multimodal/test_processing.py
+356
-150
@@ -17,6 +17,7 @@ from vllm.multimodal.processing.processor import (
17
PromptInsertion,
18
PromptReplacement,
19
_apply_matches,
20
+ _apply_token_matches_with_placeholders,
21
apply_text_matches,
22
apply_token_matches,
23
find_mm_placeholders,
@@ -567,171 +568,309 @@ def test_find_update_text(
568
assert new_prompt == expected
569
570
570
-@pytest.mark.parametrize(
571
- ("prompt", "target_by_key", "repl_by_key", "expected_by_update_type_mm_count"), # noqa: E501
572
- [
573
- # Tokenized test cases of `test_find_update_text`
574
- # using the vocab of llava-hf/llava-v1.6-mistral-7b-hf
575
- (
576
- [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
577
- {
578
- # We use `<image>` before `Image:` to test matches that
579
- # occur out of order
580
- "pattern_1": [32000],
581
- "pattern_2": [9833, 28747],
582
- "pattern_3": [918],
571
+FIND_UPDATE_TOKENS_TEST_CASES = [
572
+ # Tokenized test cases of `test_find_update_text`
573
+ # using the vocab of llava-hf/llava-v1.6-mistral-7b-hf
574
+ (
575
+ [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
576
+ {
577
+ # We use `<image>` before `Image:` to test matches that
578
+ # occur out of order
579
+ "pattern_1": [32000],
580
+ "pattern_2": [9833, 28747],
581
+ "pattern_3": [918],
582
+ },
583
+ {
584
+ # Test whether target is confused with replacement
585
+ "pattern_1": [32000, 32000],
586
+ # Test empty replacement
587
+ "pattern_2": [],
588
+ # Test dynamic replacement (beyond the form of `unit * count`)
589
+ "pattern_3": [1550, 918, 1550],
590
+ },
591
+ {
592
+ PromptInsertion: {
593
+ 0: [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
594
+ 1: [
595
+ 1,
596
+ 9833,
597
+ 28747,
598
+ 32000,
599
+ 32000,
600
+ 32000,
601
+ 9833,
602
+ 28747,
603
+ 32000,
604
+ 32000,
605
+ 918,
606
+ 1550,
607
+ 918,
608
+ 1550,
609
+ ], # noqa: E501
610
+ 2: [
611
+ 1,
612
+ 9833,
613
+ 28747,
614
+ 32000,
615
+ 32000,
616
+ 32000,
617
+ 32000,
618
+ 32000,
619
+ 9833,
620
+ 28747,
621
+ 32000,
622
+ 32000,
623
+ 918,
624
+ 1550,
625
+ 918,
626
+ 1550,
627
+ 1550,
628
+ 918,
629
+ 1550,
630
+ ], # noqa: E501
631
},
584
- {
585
- # Test whether target is confused with replacement
586
- "pattern_1": [32000, 32000],
587
- # Test empty replacement
588
- "pattern_2": [],
589
- # Test dynamic replacement (beyond the form of `unit * count`)
590
- "pattern_3": [1550, 918, 1550],
632
+ PromptReplacement: {
633
+ 0: [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
634
+ 1: [1, 32000, 32000, 9833, 28747, 32000, 32000, 1550, 918, 1550], # noqa: E501
635
+ 2: [1, 32000, 32000, 32000, 32000, 32000, 1550, 918, 1550],
636
},
592
- {
593
- PromptInsertion: {
594
- 0: [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
595
- 1: [
596
- 1,
597
- 9833,
598
- 28747,
599
- 32000,
600
- 32000,
601
- 32000,
602
- 9833,
603
- 28747,
604
- 32000,
605
- 32000,
606
- 918,
607
- 1550,
608
- 918,
609
- 1550,
610
- ], # noqa: E501
611
- 2: [
612
- 1,
613
- 9833,
614
- 28747,
615
- 32000,
616
- 32000,
617
- 32000,
618
- 32000,
619
- 32000,
620
- 9833,
621
- 28747,
622
- 32000,
623
- 32000,
624
- 918,
625
- 1550,
626
- 918,
627
- 1550,
628
- 1550,
629
- 918,
630
- 1550,
631
- ], # noqa: E501
632
- },
633
- PromptReplacement: {
634
- 0: [1, 9833, 28747, 32000, 9833, 28747, 32000, 32000, 918],
635
- 1: [1, 32000, 32000, 9833, 28747, 32000, 32000, 1550, 918, 1550], # noqa: E501
636
- 2: [1, 32000, 32000, 32000, 32000, 32000, 1550, 918, 1550],
637
- },
637
+ },
638
+ ),
639
+ # Test index targets
640
+ (
641
+ [],
642
+ {
643
+ "pattern_1": PromptIndexTargets.start(),
644
+ "pattern_2": PromptIndexTargets.prefix([32000]),
645
+ "pattern_3": PromptIndexTargets.end(),
646
+ },
647
+ {
648
+ "pattern_1": [-1],
649
+ "pattern_2": [-2],
650
+ "pattern_3": [-3],
651
+ },
652
+ {
653
+ PromptInsertion: {
654
+ 0: [],
655
+ 1: [-1, -3],
656
+ 2: [-1, -1, -3, -3],
657
},
639
- ),
640
- # Test index targets
641
- (
642
- [],
643
- {
644
- "pattern_1": PromptIndexTargets.start(),
645
- "pattern_2": PromptIndexTargets.prefix([32000]),
646
- "pattern_3": PromptIndexTargets.end(),
658
+ PromptReplacement: {
659
+ 0: [],
660
+ 1: [-1, -3],
661
+ 2: [-1, -1, -3, -3],
662
},
648
- {
649
- "pattern_1": [-1],
650
- "pattern_2": [-2],
651
- "pattern_3": [-3],
663
+ },
664
+ ),
665
+ (
666
+ [32000],
667
+ {
668
+ "pattern_1": PromptIndexTargets.start(),
669
+ "pattern_2": PromptIndexTargets.prefix([32000]),
670
+ "pattern_3": PromptIndexTargets.end(),
671
+ },
672
+ {
673
+ "pattern_1": [-1],
674
+ "pattern_2": [-2],
675
+ "pattern_3": [-3],
676
+ },
677
+ {
678
+ PromptInsertion: {
679
+ 0: [32000],
680
+ 1: [-1, 32000, -2, -3],
681
+ 2: [-1, -1, 32000, -2, -2, -3, -3],
682
},
653
- {
654
- PromptInsertion: {
655
- 0: [],
656
- 1: [-1, -3],
657
- 2: [-1, -1, -3, -3],
658
- },
659
- PromptReplacement: {
660
- 0: [],
661
- 1: [-1, -3],
662
- 2: [-1, -1, -3, -3],
663
- },
683
+ PromptReplacement: {
684
+ 0: [32000],
685
+ 1: [-1, 32000, -2, -3],
686
+ 2: [-1, -1, 32000, -2, -2, -3, -3],
687
},
665
- ),
666
- (
667
- [32000],
668
- {
669
- "pattern_1": PromptIndexTargets.start(),
670
- "pattern_2": PromptIndexTargets.prefix([32000]),
671
- "pattern_3": PromptIndexTargets.end(),
688
+ },
689
+ ),
690
+ # Test different replacement per item
691
+ (
692
+ [32000, 32000, 32000],
693
+ {
694
+ "pattern_1": [32000],
695
+ },
696
+ {
697
+ "pattern_1": lambda idx: [-(idx + 1)],
698
+ },
699
+ {
700
+ PromptInsertion: {
701
+ 0: [32000, 32000, 32000],
702
+ 1: [32000, -1, 32000, 32000],
703
+ 2: [32000, -1, -2, 32000, 32000],
704
},
673
- {
674
- "pattern_1": [-1],
675
- "pattern_2": [-2],
676
- "pattern_3": [-3],
705
+ PromptReplacement: {
706
+ 0: [32000, 32000, 32000],
707
+ 1: [-1, 32000, 32000],
708
+ 2: [-1, -2, 32000],
709
},
678
- {
679
- PromptInsertion: {
680
- 0: [32000],
681
- 1: [-1, 32000, -2, -3],
682
- 2: [-1, -1, 32000, -2, -2, -3, -3],
683
- },
684
- PromptReplacement: {
685
- 0: [32000],
686
- 1: [-1, 32000, -2, -3],
687
- 2: [-1, -1, 32000, -2, -2, -3, -3],
688
- },
710
+ },
711
+ ),
712
+ (
713
+ [32000, 32000, 32000],
714
+ {
715
+ "pattern_1": PromptIndexTargets.prefix([32000]),
716
+ },
717
+ {
718
+ "pattern_1": lambda idx: [-(idx + 1)],
719
+ },
720
+ {
721
+ PromptInsertion: {
722
+ 0: [32000, 32000, 32000],
723
+ 1: [32000, -1, 32000, 32000],
724
+ 2: [32000, -1, -2, 32000, 32000],
725
},
690
- ),
691
- # Test different replacement per item
692
- (
693
- [32000, 32000, 32000],
694
- {
695
- "pattern_1": [32000],
726
+ PromptReplacement: {
727
+ 0: [32000, 32000, 32000],
728
+ 1: [32000, -1, 32000, 32000],
729
+ 2: [32000, -1, -2, 32000, 32000],
730
},
697
- {
698
- "pattern_1": lambda idx: [-(idx + 1)],
731
+ },
732
+ ),
733
+]
734
+
735
+
736
+def _placeholder(modality, item_idx, start_idx, tokens):
737
+ return PlaceholderFeaturesInfo(
738
+ modality=modality,
739
+ item_idx=item_idx,
740
+ start_idx=start_idx,
741
+ tokens=tokens,
742
+ is_embed=None,
743
+ )
744
+
745
+
746
+FIND_UPDATE_TOKENS_PLACEHOLDER_EXPECTED = [
747
+ {
748
+ PromptInsertion: {
749
+ 0: {},
750
+ 1: {
751
+ "pattern_1": [_placeholder("pattern_1", 0, 4, [32000, 32000])],
752
+ "pattern_3": [_placeholder("pattern_3", 0, 11, [1550, 918, 1550])],
753
},
700
- {
701
- PromptInsertion: {
702
- 0: [32000, 32000, 32000],
703
- 1: [32000, -1, 32000, 32000],
704
- 2: [32000, -1, -2, 32000, 32000],
705
- },
706
- PromptReplacement: {
707
- 0: [32000, 32000, 32000],
708
- 1: [-1, 32000, 32000],
709
- 2: [-1, -2, 32000],
710
- },
754
+ 2: {
755
+ "pattern_1": [
756
+ _placeholder("pattern_1", 0, 4, [32000, 32000]),
757
+ _placeholder("pattern_1", 1, 6, [32000, 32000]),
758
+ ],
759
+ "pattern_3": [
760
+ _placeholder("pattern_3", 0, 13, [1550, 918, 1550]),
761
+ _placeholder("pattern_3", 1, 16, [1550, 918, 1550]),
762
+ ],
763
},
712
- ),
713
- (
714
- [32000, 32000, 32000],
715
- {
716
- "pattern_1": PromptIndexTargets.prefix([32000]),
764
+ },
765
+ PromptReplacement: {
766
+ 0: {},
767
+ 1: {
768
+ "pattern_1": [_placeholder("pattern_1", 0, 1, [32000, 32000])],
769
+ "pattern_3": [_placeholder("pattern_3", 0, 7, [1550, 918, 1550])],
770
},
718
- {
719
- "pattern_1": lambda idx: [-(idx + 1)],
771
+ 2: {},
772
+ },
773
+ },
774
+ {
775
+ PromptInsertion: {0: {}, 1: {}, 2: {}},
776
+ PromptReplacement: {0: {}, 1: {}, 2: {}},
777
+ },
778
+ {
779
+ PromptInsertion: {
780
+ 0: {},
781
+ 1: {
782
+ "pattern_1": [_placeholder("pattern_1", 0, 0, [-1])],
783
+ "pattern_2": [_placeholder("pattern_2", 0, 2, [-2])],
784
+ "pattern_3": [_placeholder("pattern_3", 0, 3, [-3])],
785
},
721
- {
722
- PromptInsertion: {
723
- 0: [32000, 32000, 32000],
724
- 1: [32000, -1, 32000, 32000],
725
- 2: [32000, -1, -2, 32000, 32000],
726
- },
727
- PromptReplacement: {
728
- 0: [32000, 32000, 32000],
729
- 1: [32000, -1, 32000, 32000],
730
- 2: [32000, -1, -2, 32000, 32000],
731
- },
786
+ 2: {
787
+ "pattern_1": [
788
+ _placeholder("pattern_1", 0, 0, [-1]),
789
+ _placeholder("pattern_1", 1, 1, [-1]),
790
+ ],
791
+ "pattern_2": [
792
+ _placeholder("pattern_2", 0, 3, [-2]),
793
+ _placeholder("pattern_2", 1, 4, [-2]),
794
+ ],
795
+ "pattern_3": [
796
+ _placeholder("pattern_3", 0, 5, [-3]),
797
+ _placeholder("pattern_3", 1, 6, [-3]),
798
+ ],
799
},
733
- ),
734
- ],
800
+ },
801
+ PromptReplacement: {
802
+ 0: {},
803
+ 1: {
804
+ "pattern_1": [_placeholder("pattern_1", 0, 0, [-1])],
805
+ "pattern_2": [_placeholder("pattern_2", 0, 2, [-2])],
806
+ "pattern_3": [_placeholder("pattern_3", 0, 3, [-3])],
807
+ },
808
+ 2: {
809
+ "pattern_1": [
810
+ _placeholder("pattern_1", 0, 0, [-1]),
811
+ _placeholder("pattern_1", 1, 1, [-1]),
812
+ ],
813
+ "pattern_2": [
814
+ _placeholder("pattern_2", 0, 3, [-2]),
815
+ _placeholder("pattern_2", 1, 4, [-2]),
816
+ ],
817
+ "pattern_3": [
818
+ _placeholder("pattern_3", 0, 5, [-3]),
819
+ _placeholder("pattern_3", 1, 6, [-3]),
820
+ ],
821
+ },
822
+ },
823
+ },
824
+ {
825
+ PromptInsertion: {
826
+ 0: {},
827
+ 1: {"pattern_1": [_placeholder("pattern_1", 0, 1, [-1])]},
828
+ 2: {
829
+ "pattern_1": [
830
+ _placeholder("pattern_1", 0, 1, [-1]),
831
+ _placeholder("pattern_1", 1, 2, [-2]),
832
+ ]
833
+ },
834
+ },
835
+ PromptReplacement: {
836
+ 0: {},
837
+ 1: {"pattern_1": [_placeholder("pattern_1", 0, 0, [-1])]},
838
+ 2: {
839
+ "pattern_1": [
840
+ _placeholder("pattern_1", 0, 0, [-1]),
841
+ _placeholder("pattern_1", 1, 1, [-2]),
842
+ ]
843
+ },
844
+ },
845
+ },
846
+ {
847
+ PromptInsertion: {
848
+ 0: {},
849
+ 1: {"pattern_1": [_placeholder("pattern_1", 0, 1, [-1])]},
850
+ 2: {
851
+ "pattern_1": [
852
+ _placeholder("pattern_1", 0, 1, [-1]),
853
+ _placeholder("pattern_1", 1, 2, [-2]),
854
+ ]
855
+ },
856
+ },
857
+ PromptReplacement: {
858
+ 0: {},
859
+ 1: {"pattern_1": [_placeholder("pattern_1", 0, 1, [-1])]},
860
+ 2: {
861
+ "pattern_1": [
862
+ _placeholder("pattern_1", 0, 1, [-1]),
863
+ _placeholder("pattern_1", 1, 2, [-2]),
864
+ ]
865
+ },
866
+ },
867
+ },
868
+]
869
+
870
+
871
+@pytest.mark.parametrize(
872
+ ("prompt", "target_by_key", "repl_by_key", "expected_by_update_type_mm_count"), # noqa: E501
873
+ FIND_UPDATE_TOKENS_TEST_CASES,
874
)
875
def test_find_update_tokens(
876
prompt,
@@ -769,6 +908,73 @@ def test_find_update_tokens(
908
assert new_prompt == expected
909
910
911
+@pytest.mark.parametrize(
912
+ (
913
+ "prompt",
914
+ "target_by_key",
915
+ "repl_by_key",
916
+ "expected_by_update_type_mm_count",
917
+ "expected_placeholders_by_update_type_mm_count",
918
+ ),
919
+ [
920
+ (*case, placeholder_expected)
921
+ for case, placeholder_expected in zip(
922
+ FIND_UPDATE_TOKENS_TEST_CASES,
923
+ FIND_UPDATE_TOKENS_PLACEHOLDER_EXPECTED,
924
+ strict=True,
925
+ )
926
+ ],
927
+)
928
+def test_apply_token_matches_with_placeholders(
929
+ prompt,
930
+ target_by_key,
931
+ repl_by_key,
932
+ expected_by_update_type_mm_count,
933
+ expected_placeholders_by_update_type_mm_count,
934
+):
935
+ for update_type, expected_by_mm_count in expected_by_update_type_mm_count.items():
936
+ for mm_count, expected in expected_by_mm_count.items():
937
+ mm_prompt_updates = {
938
+ key: [
939
+ [update_type(key, target, repl_by_key[key]).resolve(i)]
940
+ for i in range(mm_count)
941
+ ]
942
+ for key, target in target_by_key.items()
943
+ }
944
+
945
+ new_prompt, result, placeholders = _apply_token_matches_with_placeholders(
946
+ prompt,
947
+ mm_prompt_updates,
948
+ tokenizer=None,
949
+ )
950
+
951
+ if any(
952
+ update_idx is None
953
+ for update_idxs in result.values()
954
+ for update_idx in update_idxs
955
+ ):
956
+ continue
957
+
958
+ expected_placeholders = expected_placeholders_by_update_type_mm_count[
959
+ update_type
960
+ ][mm_count]
961
+
962
+ # Only displayed on error
963
+ print("update_type:", update_type)
964
+ print("mm_count:", mm_count)
965
+ print("mm_prompt_updates:", mm_prompt_updates)
966
+ print("new_prompt:", new_prompt)
967
+ print("result:", result)
968
+ print("placeholders:", placeholders)
969
+
970
+ assert new_prompt == expected
971
+ assert {
972
+ modality: ph_list
973
+ for modality, ph_list in placeholders.items()
974
+ if ph_list
975
+ } == expected_placeholders
976
+
977
+
978
@pytest.mark.parametrize(
979
"repl_by_key",
980
[
vllm/model_executor/models/gemma3_mm.py
+34
@@ -373,6 +373,40 @@ class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
373
374
return token_ids, res
375
376
+ def _apply_token_matches_with_placeholders(
377
+ self,
378
+ token_ids: list[int],
379
+ mm_prompt_updates: MultiModalPromptUpdates,
380
+ ) -> tuple[
381
+ list[int],
382
+ MultiModalPromptUpdatesApplyResult,
383
+ Mapping[str, list[PlaceholderFeaturesInfo]],
384
+ ]:
385
+ new_token_ids, match_result = self._apply_token_matches(
386
+ token_ids,
387
+ mm_prompt_updates,
388
+ )
389
+
390
+ placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
391
+ modality: [] for modality in mm_prompt_updates
392
+ }
393
+
394
+ if all(
395
+ all(update_idx is not None for update_idx in update_idxs)
396
+ for update_idxs in match_result.values()
397
+ ):
398
+ placeholders = dict(
399
+ self._find_mm_placeholders(
400
+ new_token_ids,
401
+ self._matched_updates_from_result(
402
+ mm_prompt_updates,
403
+ match_result,
404
+ ),
405
+ )
406
+ )
407
+
408
+ return new_token_ids, match_result, placeholders
409
+
410
def _find_mm_placeholders(
411
self,
412
new_token_ids: list[int],
vllm/model_executor/models/gemma3n_mm.py
+34
@@ -391,6 +391,40 @@ class Gemma3nMultiModalProcessor(BaseMultiModalProcessor[Gemma3nProcessingInfo])
391
392
return token_ids, res
393
394
+ def _apply_token_matches_with_placeholders(
395
+ self,
396
+ token_ids: list[int],
397
+ mm_prompt_updates: MultiModalPromptUpdates,
398
+ ) -> tuple[
399
+ list[int],
400
+ MultiModalPromptUpdatesApplyResult,
401
+ Mapping[str, list[PlaceholderFeaturesInfo]],
402
+ ]:
403
+ new_token_ids, match_result = self._apply_token_matches(
404
+ token_ids,
405
+ mm_prompt_updates,
406
+ )
407
+
408
+ placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
409
+ modality: [] for modality in mm_prompt_updates
410
+ }
411
+
412
+ if all(
413
+ all(update_idx is not None for update_idx in update_idxs)
414
+ for update_idxs in match_result.values()
415
+ ):
416
+ placeholders = dict(
417
+ self._find_mm_placeholders(
418
+ new_token_ids,
419
+ self._matched_updates_from_result(
420
+ mm_prompt_updates,
421
+ match_result,
422
+ ),
423
+ )
424
+ )
425
+
426
+ return new_token_ids, match_result, placeholders
427
+
428
def _find_mm_placeholders(
429
self,
430
new_token_ids: list[int],
vllm/multimodal/processing/processor.py
+115
-25
@@ -958,6 +958,64 @@ def apply_token_matches(
958
return flatten_2d_lists(token_id_seqs), result
959
960
961
+def _apply_token_matches_with_placeholders(
962
+ token_ids: list[int],
963
+ mm_prompt_updates: "MultiModalPromptUpdates",
964
+ tokenizer: TokenizerLike | None,
965
+) -> tuple[
966
+ list[int],
967
+ "MultiModalPromptUpdatesApplyResult",
968
+ Mapping[str, list[PlaceholderFeaturesInfo]],
969
+]:
970
+ matched_updates, result = _plan_prompt_updates(
971
+ token_ids,
972
+ mm_prompt_updates,
973
+ tokenizer,
974
+ )
975
+ placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
976
+ modality: [] for modality in mm_prompt_updates
977
+ }
978
+
979
+ new_token_ids = list[int]()
980
+ prev_end_idx = 0
981
+ for matched_update in matched_updates:
982
+ update = matched_update.update
983
+ match = matched_update.match
984
+ matched_content = update.content.full
985
+
986
+ if update.mode == UpdateMode.INSERT:
987
+ end_idx_to_insert = match.end_idx
988
+ elif update.mode == UpdateMode.REPLACE:
989
+ end_idx_to_insert = match.start_idx
990
+ else:
991
+ assert_never(update.mode)
992
+
993
+ new_token_ids.extend(token_ids[prev_end_idx:end_idx_to_insert])
994
+ start_idx = len(new_token_ids)
995
+
996
+ tokens = _seq2tokens(tokenizer, matched_content)
997
+ if tokens:
998
+ content_is_embed = update.content.is_embed
999
+ if content_is_embed is not None:
1000
+ content_is_embed = content_is_embed(tokenizer, matched_content)
1001
+
1002
+ placeholders[update.modality].append(
1003
+ PlaceholderFeaturesInfo(
1004
+ modality=update.modality,
1005
+ item_idx=update.item_idx,
1006
+ start_idx=start_idx,
1007
+ tokens=tokens,
1008
+ is_embed=content_is_embed,
1009
+ )
1010
+ )
1011
+ new_token_ids.extend(tokens)
1012
+
1013
+ prev_end_idx = match.end_idx
1014
+
1015
+ new_token_ids.extend(token_ids[prev_end_idx:])
1016
+ return new_token_ids, result, placeholders
1017
+
1018
+
1019
def apply_text_matches(
1020
prompt: str,
1021
mm_prompt_updates: "MultiModalPromptUpdates",
@@ -1671,6 +1729,22 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1729
tokenizer = self.info.get_tokenizer()
1730
return apply_token_matches(prompt, mm_prompt_updates, tokenizer)
1731
1732
+ def _apply_token_matches_with_placeholders(
1733
+ self,
1734
+ token_ids: list[int],
1735
+ mm_prompt_updates: MultiModalPromptUpdates,
1736
+ ) -> tuple[
1737
+ list[int],
1738
+ MultiModalPromptUpdatesApplyResult,
1739
+ Mapping[str, list[PlaceholderFeaturesInfo]],
1740
+ ]:
1741
+ tokenizer = self.info.get_tokenizer()
1742
+ return _apply_token_matches_with_placeholders(
1743
+ token_ids,
1744
+ mm_prompt_updates,
1745
+ tokenizer,
1746
+ )
1747
+
1748
def _apply_text_matches(
1749
self,
1750
prompt: str,
@@ -1689,6 +1763,25 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1763
prompt, mm_prompt_updates, tokenizer
1764
)
1765
1766
+ def _matched_updates_from_result(
1767
+ self,
1768
+ mm_prompt_updates: MultiModalPromptUpdates,
1769
+ match_result: MultiModalPromptUpdatesApplyResult,
1770
+ ) -> dict[str, list[Sequence[ResolvedPromptUpdate]]]:
1771
+ matched_updates = defaultdict[str, list[Sequence[ResolvedPromptUpdate]]](list)
1772
+ for modality, update_idxs in match_result.items():
1773
+ for item_idx, update_idx in enumerate(update_idxs):
1774
+ assert update_idx is not None, (
1775
+ "Failed to apply prompt replacement for "
1776
+ f"mm_items[{modality!r}][{item_idx}]"
1777
+ )
1778
+
1779
+ matched_updates[modality].append(
1780
+ [mm_prompt_updates[modality][item_idx][update_idx]]
1781
+ )
1782
+
1783
+ return dict(matched_updates)
1784
+
1785
def _apply_prompt_updates(
1786
self,
1787
token_ids: list[int],
@@ -1697,11 +1790,24 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1790
"""Apply multi-modal prompt updates to token IDs."""
1791
tokenizer = self.info.get_tokenizer()
1792
1700
- new_token_ids, match_result = self._apply_token_matches(
1701
- token_ids,
1702
- mm_prompt_updates,
1793
+ new_token_ids, match_result, placeholders = (
1794
+ self._apply_token_matches_with_placeholders(
1795
+ token_ids,
1796
+ mm_prompt_updates,
1797
+ )
1798
)
1799
1800
+ if all(
1801
+ all(update_idx is not None for update_idx in update_idxs)
1802
+ for update_idxs in match_result.values()
1803
+ ):
1804
+ placeholders = {
1805
+ modality: modality_placeholders
1806
+ for modality, modality_placeholders in placeholders.items()
1807
+ if modality_placeholders
1808
+ }
1809
+ return new_token_ids, placeholders
1810
+
1811
# If the search text does not represent a special token,
1812
# it may have different token IDs in the prompt, because
1813
# the tokens may go across the boundaries of the search text.
@@ -1712,32 +1818,16 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1818
# Since it is inefficient to search for all possible tokenizations
1819
# of the search text in the prompt, we instead perform string-based
1820
# updates on the decoded token IDs, then encode them back.
1715
- if not all(
1716
- all(update_idx is not None for update_idx in update_idxs)
1717
- for update_idxs in match_result.values()
1718
- ):
1719
- new_text, match_result = self._apply_text_matches(
1720
- _seq2text(tokenizer, token_ids, use_cache=False),
1721
- mm_prompt_updates,
1722
- )
1723
-
1724
- new_token_ids = _seq2tokens(tokenizer, new_text, use_cache=False)
1725
-
1726
- matched_updates = defaultdict[str, list[Sequence[ResolvedPromptUpdate]]](list)
1727
- for modality, update_idxs in match_result.items():
1728
- for item_idx, update_idx in enumerate(update_idxs):
1729
- assert update_idx is not None, (
1730
- "Failed to apply prompt replacement for "
1731
- f"mm_items[{modality!r}][{item_idx}]"
1732
- )
1821
+ new_text, match_result = self._apply_text_matches(
1822
+ _seq2text(tokenizer, token_ids, use_cache=False),
1823
+ mm_prompt_updates,
1824
+ )
1825
1734
- matched_updates[modality].append(
1735
- [mm_prompt_updates[modality][item_idx][update_idx]]
1736
- )
1826
+ new_token_ids = _seq2tokens(tokenizer, new_text, use_cache=False)
1827
1828
placeholders = self._find_mm_placeholders(
1829
new_token_ids,
1740
- dict(matched_updates),
1830
+ self._matched_updates_from_result(mm_prompt_updates, match_result),
1831
)
1832
1833
return new_token_ids, placeholders