Skip to content

Commit e254def

Browse files
committed
refactor: make MTMD chat handler standalone
1 parent ba63f7d commit e254def

1 file changed

Lines changed: 185 additions & 1 deletion

File tree

llama_cpp/llama_chat_format.py

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3265,9 +3265,78 @@ def from_pretrained(
32653265
)
32663266

32673267

3268-
class MTMDChatHandler(Llava15ChatHandler):
3268+
class MTMDChatHandler:
32693269
DEFAULT_SYSTEM_MESSAGE = None
32703270

3271+
def __init__(self, clip_model_path: str, verbose: bool = True):
3272+
import llama_cpp.mtmd_cpp as mtmd_cpp
3273+
3274+
self.clip_model_path = clip_model_path
3275+
self.verbose = verbose
3276+
self._mtmd_cpp = mtmd_cpp
3277+
self._exit_stack = ExitStack()
3278+
self.mtmd_ctx: Optional[mtmd_cpp.mtmd_context_p] = None
3279+
3280+
if not os.path.exists(clip_model_path):
3281+
raise ValueError(f"Clip model path does not exist: {clip_model_path}")
3282+
3283+
def _init_mtmd_context(self, llama_model: llama.Llama):
3284+
if self.mtmd_ctx is not None:
3285+
return
3286+
3287+
with suppress_stdout_stderr(disable=self.verbose):
3288+
ctx_params = self._mtmd_cpp.mtmd_context_params_default()
3289+
ctx_params.use_gpu = True # TODO: Make this configurable
3290+
ctx_params.print_timings = self.verbose
3291+
ctx_params.n_threads = llama_model.n_threads
3292+
ctx_params.flash_attn_type = (
3293+
llama_cpp.LLAMA_FLASH_ATTN_TYPE_ENABLED
3294+
if (
3295+
llama_model.context_params.flash_attn_type
3296+
== llama_cpp.LLAMA_FLASH_ATTN_TYPE_ENABLED
3297+
)
3298+
else llama_cpp.LLAMA_FLASH_ATTN_TYPE_DISABLED
3299+
)
3300+
3301+
self.mtmd_ctx = self._mtmd_cpp.mtmd_init_from_file(
3302+
self.clip_model_path.encode(), llama_model.model, ctx_params
3303+
)
3304+
3305+
if self.mtmd_ctx is None:
3306+
raise ValueError(
3307+
f"Failed to load mtmd context from: {self.clip_model_path}"
3308+
)
3309+
3310+
if not self._mtmd_cpp.mtmd_support_vision(self.mtmd_ctx):
3311+
raise ValueError("Vision is not supported by this model")
3312+
3313+
def mtmd_free():
3314+
with suppress_stdout_stderr(disable=self.verbose):
3315+
if self.mtmd_ctx is not None:
3316+
self._mtmd_cpp.mtmd_free(self.mtmd_ctx)
3317+
self.mtmd_ctx = None
3318+
3319+
self._exit_stack.callback(mtmd_free)
3320+
3321+
def load_image(self, image_url: str) -> bytes:
3322+
return self._load_image(image_url)
3323+
3324+
def _create_bitmap_from_bytes(self, image_bytes: bytes):
3325+
if self.mtmd_ctx is None:
3326+
raise ValueError("mtmd context not initialized")
3327+
3328+
with suppress_stdout_stderr(disable=self.verbose):
3329+
bitmap = self._mtmd_cpp.mtmd_helper_bitmap_init_from_buf(
3330+
self.mtmd_ctx,
3331+
(ctypes.c_uint8 * len(image_bytes)).from_buffer(bytearray(image_bytes)),
3332+
len(image_bytes),
3333+
)
3334+
3335+
if bitmap is None:
3336+
raise ValueError("Failed to create bitmap from image bytes")
3337+
3338+
return bitmap
3339+
32713340
def _get_chat_template(self, llama_model: llama.Llama) -> str:
32723341
chat_template = llama_model.metadata.get("tokenizer.chat_template")
32733342
if not isinstance(chat_template, str) or chat_template == "":
@@ -3590,6 +3659,121 @@ def raise_exception(message: str):
35903659
)
35913660
return _convert_completion_to_chat(completion_or_chunks, stream=stream)
35923661

3662+
@staticmethod
3663+
def _load_image(image_url: str) -> bytes:
3664+
if image_url.startswith("data:"):
3665+
import base64
3666+
3667+
image_bytes = base64.b64decode(image_url.split(",")[1])
3668+
return image_bytes
3669+
else:
3670+
import urllib.request
3671+
3672+
with urllib.request.urlopen(image_url) as f:
3673+
image_bytes = f.read()
3674+
return image_bytes
3675+
3676+
@staticmethod
3677+
def get_image_urls(messages: List[llama_types.ChatCompletionRequestMessage]):
3678+
image_urls: List[str] = []
3679+
for message in messages:
3680+
if message["role"] == "user":
3681+
if message["content"] is None:
3682+
continue
3683+
for content in message["content"]:
3684+
if isinstance(content, dict) and "type" in content:
3685+
if content["type"] == "image_url":
3686+
if (
3687+
isinstance(content["image_url"], dict)
3688+
and "url" in content["image_url"]
3689+
):
3690+
image_urls.append(content["image_url"]["url"])
3691+
else:
3692+
image_urls.append(content["image_url"])
3693+
return image_urls
3694+
3695+
@classmethod
3696+
def from_pretrained(
3697+
cls,
3698+
repo_id: str,
3699+
filename: Optional[str],
3700+
local_dir: Optional[Union[str, os.PathLike[str]]] = None,
3701+
local_dir_use_symlinks: Union[bool, Literal["auto"]] = "auto",
3702+
cache_dir: Optional[Union[str, os.PathLike[str]]] = None,
3703+
**kwargs: Any,
3704+
) -> "MTMDChatHandler":
3705+
import fnmatch
3706+
from pathlib import Path
3707+
3708+
try:
3709+
from huggingface_hub import hf_hub_download, HfFileSystem # type: ignore
3710+
from huggingface_hub.utils import validate_repo_id # type: ignore
3711+
except ImportError:
3712+
raise ImportError(
3713+
"Llama.from_pretrained requires the huggingface-hub package. "
3714+
"You can install it with `pip install huggingface-hub`."
3715+
)
3716+
3717+
validate_repo_id(repo_id)
3718+
3719+
hffs = HfFileSystem()
3720+
3721+
files = [
3722+
file["name"] if isinstance(file, dict) else file
3723+
for file in hffs.ls(repo_id) # type: ignore
3724+
]
3725+
3726+
file_list: List[str] = []
3727+
for file in files:
3728+
rel_path = Path(file).relative_to(repo_id)
3729+
file_list.append(str(rel_path))
3730+
3731+
matching_files = [file for file in file_list if fnmatch.fnmatch(file, filename)] # type: ignore
3732+
3733+
if len(matching_files) == 0:
3734+
raise ValueError(
3735+
f"No file found in {repo_id} that match {filename}\n\n"
3736+
f"Available Files:\n{json.dumps(file_list)}"
3737+
)
3738+
3739+
if len(matching_files) > 1:
3740+
raise ValueError(
3741+
f"Multiple files found in {repo_id} matching {filename}\n\n"
3742+
f"Available Files:\n{json.dumps(files)}"
3743+
)
3744+
3745+
(matching_file,) = matching_files
3746+
3747+
subfolder = str(Path(matching_file).parent)
3748+
filename = Path(matching_file).name
3749+
3750+
hf_hub_download(
3751+
repo_id=repo_id,
3752+
filename=filename,
3753+
subfolder=subfolder,
3754+
local_dir=cast(Union[str, Path, None], local_dir),
3755+
local_dir_use_symlinks=local_dir_use_symlinks,
3756+
cache_dir=cast(Union[str, Path, None], cache_dir),
3757+
)
3758+
3759+
if local_dir is None:
3760+
model_path = hf_hub_download(
3761+
repo_id=repo_id,
3762+
filename=filename,
3763+
subfolder=subfolder,
3764+
local_dir=local_dir,
3765+
local_dir_use_symlinks=local_dir_use_symlinks,
3766+
cache_dir=cast(Union[str, Path, None], cache_dir),
3767+
local_files_only=True,
3768+
)
3769+
else:
3770+
model_path = os.path.join(local_dir, filename)
3771+
3772+
return cls(
3773+
clip_model_path=model_path,
3774+
**kwargs,
3775+
)
3776+
35933777

35943778
class Gemma4ChatHandler(MTMDChatHandler):
35953779
pass

0 commit comments

Comments
 (0)