feat: llama-cpp-python backend with GGUF, vision, and tool support
This commit is contained in:
120
kischdle/llmux/llmux/backends/llamacpp.py
Normal file
120
kischdle/llmux/llmux/backends/llamacpp.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import AsyncIterator
|
||||
|
||||
from llama_cpp import Llama
|
||||
|
||||
from llmux.backends.base import BaseBackend
|
||||
from llmux.config import PhysicalModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LlamaCppBackend(BaseBackend):
|
||||
def __init__(self, models_dir: str = "/models"):
|
||||
self._models_dir = Path(models_dir)
|
||||
self._loaded: dict[str, dict] = {}
|
||||
|
||||
async def load(self, model_id: str, n_gpu_layers: int = -1) -> None:
|
||||
if model_id in self._loaded:
|
||||
return
|
||||
physical = _get_physical_config(model_id)
|
||||
model_path = self._models_dir / physical.model_file
|
||||
logger.info(f"Loading GGUF model {model_path} with n_gpu_layers={n_gpu_layers}")
|
||||
|
||||
def _load():
|
||||
kwargs = {
|
||||
"model_path": str(model_path),
|
||||
"n_gpu_layers": n_gpu_layers,
|
||||
"n_ctx": 8192,
|
||||
"verbose": False,
|
||||
}
|
||||
if physical.mmproj_file:
|
||||
mmproj_path = self._models_dir / physical.mmproj_file
|
||||
kwargs["chat_handler"] = _create_vision_handler(str(mmproj_path))
|
||||
return Llama(**kwargs)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
llm = await loop.run_in_executor(None, _load)
|
||||
self._loaded[model_id] = {"llm": llm, "n_gpu_layers": n_gpu_layers}
|
||||
|
||||
async def unload(self, model_id: str) -> None:
|
||||
if model_id not in self._loaded:
|
||||
return
|
||||
entry = self._loaded.pop(model_id)
|
||||
del entry["llm"]
|
||||
|
||||
async def generate(self, model_id, messages, params, stream=False, tools=None):
|
||||
entry = self._loaded[model_id]
|
||||
llm = entry["llm"]
|
||||
|
||||
effective_messages = list(messages)
|
||||
if "enable_thinking" in params:
|
||||
if not params["enable_thinking"]:
|
||||
if effective_messages and effective_messages[0].get("role") == "system":
|
||||
effective_messages[0] = dict(effective_messages[0])
|
||||
effective_messages[0]["content"] = "/no_think\n" + effective_messages[0]["content"]
|
||||
else:
|
||||
effective_messages.insert(0, {"role": "system", "content": "/no_think"})
|
||||
|
||||
if "system_prompt_prefix" in params:
|
||||
prefix = params["system_prompt_prefix"]
|
||||
if effective_messages and effective_messages[0].get("role") == "system":
|
||||
effective_messages[0] = dict(effective_messages[0])
|
||||
effective_messages[0]["content"] = prefix + "\n\n" + effective_messages[0]["content"]
|
||||
else:
|
||||
effective_messages.insert(0, {"role": "system", "content": prefix})
|
||||
|
||||
if stream:
|
||||
return self._stream_generate(llm, effective_messages, model_id, tools)
|
||||
else:
|
||||
return await self._full_generate(llm, effective_messages, model_id, tools)
|
||||
|
||||
async def _full_generate(self, llm, messages, model_id, tools):
|
||||
def _run():
|
||||
kwargs = {"messages": messages, "max_tokens": 4096}
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
return llm.create_chat_completion(**kwargs)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(None, _run)
|
||||
result["model"] = model_id
|
||||
return result
|
||||
|
||||
async def _stream_generate(self, llm, messages, model_id, tools):
|
||||
def _run():
|
||||
kwargs = {"messages": messages, "max_tokens": 4096, "stream": True}
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
return llm.create_chat_completion(**kwargs)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
stream = await loop.run_in_executor(None, _run)
|
||||
|
||||
async def _iter():
|
||||
for chunk in stream:
|
||||
chunk["model"] = model_id
|
||||
yield f"data: {json.dumps(chunk)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return _iter()
|
||||
|
||||
|
||||
def _create_vision_handler(mmproj_path: str):
|
||||
from llama_cpp.llama_chat_format import Llava16ChatHandler
|
||||
return Llava16ChatHandler(clip_model_path=mmproj_path)
|
||||
|
||||
|
||||
_physical_models: dict[str, PhysicalModel] = {}
|
||||
|
||||
def set_physical_models(models: dict[str, PhysicalModel]) -> None:
|
||||
global _physical_models
|
||||
_physical_models = models
|
||||
|
||||
def _get_physical_config(model_id: str) -> PhysicalModel:
|
||||
return _physical_models[model_id]
|
||||
Reference in New Issue
Block a user