Skip to content

Commit 29b3f3a

Browse files
authored
Merge pull request JamePeng#125 from TAO71-AI/mtmd
Implemented generic multimodal chat handler.
2 parents cb299e6 + 5757856 commit 29b3f3a

2 files changed

Lines changed: 81 additions & 14 deletions

File tree

llama_cpp/llama.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class Llama:
9696
def __init__(
9797
self,
9898
model_path: str,
99+
mmproj_path: Optional[str] = None,
99100
*,
100101
# Model Params
101102
n_gpu_layers: Union[int, Literal["auto", "all"]] = "auto",
@@ -172,6 +173,7 @@ def __init__(
172173
log_filters: Optional[Sequence[str]] = None,
173174
log_filters_case_sensitive: bool = True,
174175
# Extra Params
176+
chat_handler_kwargs: Dict[str, Any] = {},
175177
**kwargs, # type: ignore
176178
):
177179
"""Load a llama.cpp model from `model_path`.
@@ -711,6 +713,18 @@ def __init__(
711713
print(f"Failed to load metadata: {e}", file=sys.stderr)
712714

713715
if self.verbose:
716+
print(f"Model metadata: {self.metadata}", file=sys.stderr)
717+
718+
if mmproj_path is not None:
719+
if self.chat_handler is not None and self.verbose:
720+
print("Warning: Both `chat_handler` and `mmproj_path` are not null. Chat handler will be overwritten.", flush = True)
721+
722+
self.chat_handler = llama_chat_format.GenericMTMDChatHandler(
723+
chat_format = self.metadata.get("tokenizer.chat_template", None),
724+
mmproj_path = mmproj_path,
725+
verbose = self.verbose,
726+
**chat_handler_kwargs
727+
)
714728
print(f"Model desc: {self.model_desc}, "
715729
f"Model size: {self.model_size / (1024 * 1024):.2f} MB, "
716730
f"Model metadata: {self.metadata}",

llama_cpp/llama_chat_format.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3089,11 +3089,12 @@ class MTMDChatHandler:
30893089

30903090
def __init__(
30913091
self,
3092-
clip_model_path: str,
3092+
mmproj_path: str,
30933093
verbose: bool = True,
30943094
use_gpu: bool = True,
30953095
image_min_tokens: int = -1,
30963096
image_max_tokens: int = -1,
3097+
chat_template_override: Optional[str] = None,
30973098
batch_max_tokens: int = 1024,
30983099
**kwargs
30993100
):
@@ -3106,7 +3107,7 @@ def __init__(
31063107
f"If you are passing model-specific parameters, ensure they are supported by {self.log_prefix}."
31073108
)
31083109

3109-
self.clip_model_path = clip_model_path
3110+
self.mmproj_path = mmproj_path
31103111
self.image_min_tokens = image_min_tokens
31113112
self.image_max_tokens = image_max_tokens
31123113
self.batch_max_tokens = batch_max_tokens
@@ -3122,16 +3123,25 @@ def __init__(
31223123
self.is_support_audio = False
31233124
self.is_support_video = False
31243125

3125-
if not os.path.exists(clip_model_path):
3126-
raise ValueError(f"{self.log_prefix}(__init__): Clip model path does not exist: {clip_model_path}")
3126+
if not os.path.exists(mmproj_path):
3127+
raise ValueError(f"{self.log_prefix}(__init__): Clip model path does not exist: {mmproj_path}")
31273128

31283129
# Pre-compile Jinja template
3129-
self.chat_template = ImmutableSandboxedEnvironment(
3130-
trim_blocks=True,
3131-
lstrip_blocks=True,
3132-
).from_string(self.CHAT_FORMAT)
3130+
if (not hasattr(self, "chat_format") or self.chat_format is None) and chat_template_override is None:
3131+
self.chat_format = self.CHAT_FORMAT
3132+
elif chat_template_override is not None:
3133+
self.chat_format = chat_template_override
3134+
3135+
self._chat_format_parser_tags = []
3136+
self.change_chat_template(self.chat_format)
31333137

31343138
self._exit_stack = ExitStack()
3139+
3140+
def change_chat_template(self, new_template: str):
3141+
self.chat_template = ImmutableSandboxedEnvironment(
3142+
trim_blocks=True,
3143+
lstrip_blocks=True
3144+
).from_string(new_template)
31353145

31363146
def _init_mtmd_context(self, llama_model: llama_core.Llama):
31373147
"""Initialize mtmd context with the llama model."""
@@ -3165,13 +3175,13 @@ def _init_mtmd_context(self, llama_model: llama_core.Llama):
31653175

31663176
# Initialize mtmd context
31673177
self.mtmd_ctx = self._mtmd_cpp.mtmd_init_from_file(
3168-
self.clip_model_path.encode(),
3178+
self.mmproj_path.encode(),
31693179
llama_model.model,
31703180
self.mctx_params
31713181
)
31723182

31733183
if self.mtmd_ctx is None:
3174-
raise ValueError(f"{self.log_prefix}(_init_mtmd_context): Failed to load mtmd context from: {self.clip_model_path}")
3184+
raise ValueError(f"{self.log_prefix}(_init_mtmd_context): Failed to load mtmd context from: {self.mmproj_path}")
31753185

31763186
# Check if vision is supported
31773187
self.is_support_vision = self._mtmd_cpp.mtmd_support_vision(self.mtmd_ctx)
@@ -3241,13 +3251,13 @@ def _get_media_items(self, messages: List[llama_types.ChatCompletionRequestMessa
32413251
media_items.append({"url": url, "type": "image"})
32423252

32433253
# 2. Audio Processing
3244-
elif content_type in ["audio_url", "input_audio"]:
3254+
elif content_type in ["audio", "audio_url", "input_audio"]:
32453255
if not self.is_support_audio:
32463256
raise ValueError(f"{self.log_prefix}: This mmproj model instance does not support audio inputs.")
32473257

32483258
# Case A: Handle custom/forward-compatible audio_url format
3249-
if content_type == "audio_url":
3250-
audio_url = content["audio_url"]
3259+
if content_type == "audio_url" or content_type == "audio":
3260+
audio_url = content[content_type]
32513261
url = audio_url if isinstance(audio_url, str) else audio_url["url"]
32523262
media_items.append({"url": url, "type": "audio"})
32533263
# Case B: Handle OpenAI standard input_audio format
@@ -3407,6 +3417,13 @@ def _process_mtmd_prompt(
34073417
tool_choice=tool_choice,
34083418
**getattr(self, 'extra_template_arguments', {})
34093419
)
3420+
3421+
for tag in self._chat_format_parser_tags:
3422+
if tag not in text:
3423+
continue
3424+
3425+
text = text.replace(tag, media_marker)
3426+
34103427
# Replace image_url by media_marker in text
34113428
for item in media_items:
34123429
text = text.replace(item["url"], media_marker)
@@ -4142,10 +4159,46 @@ def from_pretrained(
41424159
model_path = os.path.join(local_dir, filename)
41434160

41444161
return cls(
4145-
clip_model_path=model_path,
4162+
mmproj_path=model_path,
41464163
**kwargs,
41474164
)
41484165

4166+
class GenericMTMDChatHandler(MTMDChatHandler):
4167+
KNOWN_MEDIA_TAGS = [
4168+
"<|image_pad|>",
4169+
"<|audio_pad|>",
4170+
"<|video_pad|>",
4171+
"<|image|>",
4172+
"<|audio|>",
4173+
"<|video|>",
4174+
"[IMG]"
4175+
]
4176+
4177+
def __init__(
4178+
self,
4179+
chat_format: str,
4180+
mmproj_path: str,
4181+
verbose: bool = True,
4182+
**kwargs
4183+
) -> None:
4184+
self.chat_format = chat_format
4185+
4186+
if verbose:
4187+
print(f"Got chat template from model:\n```jinja\n{self.chat_format}\n```", flush = True)
4188+
4189+
if self.chat_format is None:
4190+
raise ValueError("Failed to get model chat template automatically.")
4191+
4192+
super().__init__(mmproj_path = mmproj_path, verbose = verbose, **kwargs)
4193+
4194+
def __call__(self, **kwargs):
4195+
self._chat_format_parser_tags = [tag for tag in self.KNOWN_MEDIA_TAGS if tag in self.chat_format]
4196+
4197+
if self.verbose:
4198+
print(f"{self.log_prefix} - Start processing")
4199+
4200+
# Use parent implementation
4201+
return super().__call__(**kwargs)
41494202

41504203
class Llava15ChatHandler(MTMDChatHandler):
41514204
CHAT_FORMAT = (

0 commit comments

Comments
 (0)