Skip to content

Commit 46708d1

Browse files
committed
refactor(mtmd): extract mtmd_tokenize into _mtmd_tokenize standalone helper
- Introduce `_mtmd_tokenize()` to encapsulate llama.cpp mtmd_tokenize binding - Decouple hybrid tokenization logic from `_process_mtmd_prompt` - Improve separation of concerns between prompt construction and C++ binding - Preserve strict media marker validation to ensure token/bitmap alignment Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent bb03d2a commit 46708d1

1 file changed

Lines changed: 83 additions & 26 deletions

File tree

llama_cpp/llama_multimodal.py

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,75 @@ def _is_audio_chunk(self, chunk_type: int) -> bool:
515515
== self._mtmd_cpp.mtmd_input_chunk_type.MTMD_INPUT_CHUNK_TYPE_AUDIO
516516
)
517517

518+
def _mtmd_tokenize(
519+
self,
520+
llama: "llama_core.Llama",
521+
text: str,
522+
bitmaps: Optional[list] = None,
523+
chunks: Optional[Any] = None,
524+
) -> Any:
525+
"""
526+
Perform MTMD hybrid tokenization.
527+
528+
This function isolates the llama.cpp mtmd_tokenize call
529+
so that prompt construction logic is decoupled from runtime execution.
530+
531+
It guarantees:
532+
- stable interface for future async/batch decoding
533+
- isolated error handling for tokenizer failures
534+
- clean separation between prompt building and C++ binding
535+
"""
536+
537+
if chunks is None:
538+
chunks = self._mtmd_cpp.mtmd_input_chunks_init()
539+
if chunks is None:
540+
raise ValueError(
541+
f"{self.log_prefix}(_mtmd_tokenize): failed to init mtmd_input_chunks"
542+
)
543+
544+
# Validate strict alignment between rendered media markers and provided bitmaps
545+
# to ensure MTMD tokenization consistency and prevent decoding mismatch errors.
546+
if bitmaps is not None:
547+
marker_count = text.count(self.media_marker)
548+
if marker_count != len(bitmaps):
549+
raise ValueError(
550+
f"{self.log_prefix}(_mtmd_tokenize): marker mismatch "
551+
f"(marker_count={marker_count}, bitmap_count={len(bitmaps)})"
552+
)
553+
554+
input_text = self._mtmd_cpp.mtmd_input_text()
555+
input_text.text = ctypes.c_char_p(text.encode("utf-8"))
556+
input_text.add_special = (llama.n_tokens == 0)
557+
input_text.parse_special = True
558+
559+
bitmap_array = None
560+
n_bitmaps = 0
561+
562+
if bitmaps:
563+
n_bitmaps = len(bitmaps)
564+
bitmap_array = (self._mtmd_cpp.mtmd_bitmap_p_ctypes * n_bitmaps)(*bitmaps)
565+
else:
566+
bitmap_array = None
567+
n_bitmaps = 0
568+
569+
result = self._mtmd_cpp.mtmd_tokenize(
570+
self.mtmd_ctx,
571+
chunks,
572+
ctypes.byref(input_text),
573+
bitmap_array,
574+
n_bitmaps,
575+
)
576+
577+
if result != 0:
578+
raise ValueError(
579+
f"{self.log_prefix}(_mtmd_tokenize): tokenize failed\n"
580+
f"- result={result}\n"
581+
f"- text_len={len(text)}\n"
582+
f"- n_bitmaps={n_bitmaps}\n"
583+
)
584+
585+
return chunks
586+
518587
def _process_mtmd_prompt(
519588
self,
520589
llama: llama_core.Llama,
@@ -572,8 +641,12 @@ def _process_mtmd_prompt(
572641
text = text.replace(item["url"], media_marker)
573642

574643
if self.verbose:
575-
print(f"{self.log_prefix}(_process_mtmd_prompt): Rendered prompt length: {len(text)} chars, Media count: {len(media_items)}.", file=sys.stderr)
576-
print(f"{self.log_prefix}(_process_mtmd_prompt): Rendered prompt: {text}", file=sys.stderr)
644+
print(
645+
f"{self.log_prefix}(_process_mtmd_prompt): "
646+
f"Rendered prompt length: {len(text)} chars, Media count: {len(media_items)}.\n"
647+
f"Rendered prompt: {text}",
648+
file=sys.stderr,
649+
)
577650

578651
# 3. Pre-allocate bitmap array to guarantee chronological order during concurrent decoding
579652
bitmaps = [None] * len(media_items)
@@ -614,37 +687,21 @@ def _create_bitmap_func(idx: int, item: dict):
614687
# If there are no images, set the bitmaps to empty.
615688
bitmaps = []
616689

617-
# 4. Initialize mtmd_input_chunks
618-
input_text = self._mtmd_cpp.mtmd_input_text()
619-
input_text.text = text.encode('utf-8')
620-
input_text.add_special = (llama.n_tokens == 0)
621-
input_text.parse_special = True
622-
623-
chunks = self._mtmd_cpp.mtmd_input_chunks_init()
624-
if chunks is None:
625-
raise ValueError(f"{self.log_prefix}(mtmd_input_chunks_init): Failed to initialize mtmd_input_chunks.")
626-
627-
# 5. Hybrid Tokenization (Text + Media binding)
628-
if len(bitmaps) > 0:
629-
bitmap_array = (self._mtmd_cpp.mtmd_bitmap_p_ctypes * len(bitmaps))(*bitmaps)
630-
result = self._mtmd_cpp.mtmd_tokenize(
631-
self.mtmd_ctx, chunks, ctypes.byref(input_text), bitmap_array, len(bitmaps)
632-
)
633-
else:
634-
result = self._mtmd_cpp.mtmd_tokenize(
635-
self.mtmd_ctx, chunks, ctypes.byref(input_text), None, 0
636-
)
637-
638-
if result != 0:
639-
raise ValueError(f"{self.log_prefix}(mtmd_tokenize): Unable to tokenize prompt, res = {result}.")
690+
# 4. Hybrid Tokenization (Text + Media)
691+
chunks = self._mtmd_tokenize(
692+
llama=llama,
693+
text=text,
694+
bitmaps=bitmaps,
695+
chunks=None,
696+
)
640697

641698
# Video helper contexts only need to stay alive until mtmd_tokenize() completes.
642699
if video_cleanup:
643700
for video_ctx in video_cleanup:
644701
self._mtmd_cpp.mtmd_helper_video_free(video_ctx)
645702
video_cleanup.clear()
646703

647-
# 6. Virtual Token Ledger Construction
704+
# 5. Virtual Token Ledger Construction
648705
full_prompt_ids = []
649706
chunk_token_spans = []
650707
current_idx = 0

0 commit comments

Comments
 (0)