falta por mejorar modelos de vision

This commit is contained in:
2025-11-28 23:01:32 +01:00
parent 7ca6ae3dd4
commit b68a4ec43b
5 changed files with 357 additions and 17 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
models models
model_choice
+27 -1
View File
@@ -62,6 +62,7 @@ python main.py --help
``` ```
- `--model-path` / `-m`: Ruta al archivo del modelo GGUF (requerido) - `--model-path` / `-m`: Ruta al archivo del modelo GGUF (requerido)
- `--mmproj-path`: Ruta al proyector multimodal (mmproj) si el modelo lo requiere (LLaVA, Qwen-VL, etc.)
- `--host`: Host del servidor (default: 0.0.0.0) - `--host`: Host del servidor (default: 0.0.0.0)
- `--port`: Puerto del servidor (default: 8000) - `--port`: Puerto del servidor (default: 8000)
- `--n-ctx`: Tamao del contexto (default: 4096) - `--n-ctx`: Tamao del contexto (default: 4096)
@@ -87,6 +88,31 @@ uv run python main.py --model-path ./models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf -
uv run python main.py --model-path ./models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --port 8000 --n-ctx 4096 --n-gpu-layers -1 --main-gpu 0 --split-mode 1 uv run python main.py --model-path ./models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --port 8000 --n-ctx 4096 --n-gpu-layers -1 --main-gpu 0 --split-mode 1
``` ```
### Modelos multimodales (visin)
- Si el modelo requiere proyector externo (mmproj), coloca el archivo en disco y pasa `--mmproj-path /ruta/proyector.mmproj` o configralo en `api_cuda.conf` como `MM_PROJ_PATH`.
- Solo se aceptan imgenes inline (data URI o base64 puro). Si envas base64 sin prefijo, el servidor lo convierte en `data:image/png;base64,...`.
- Ejemplo de request multimodal:
```bash
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe lo que ves"},
{"type": "image_url", "image_url": "data:image/png;base64,AAA..."}
]
}
],
"max_tokens": 120,
"temperature": 0.2
}'
```
## API Endpoints ## API Endpoints
### GET `/v1/models` ### GET `/v1/models`
@@ -188,4 +214,4 @@ Una vez que el servidor est ejecutndose, puedes acceder a la documentaci
- **Memoria GPU insuficiente**: Reduce `--n-gpu-layers` a un nmero menor (ej: 20, 10) - **Memoria GPU insuficiente**: Reduce `--n-gpu-layers` a un nmero menor (ej: 20, 10)
- **GPU no detectada**: Verifica que `nvidia-smi` funcione y muestre tu GPU - **GPU no detectada**: Verifica que `nvidia-smi` funcione y muestre tu GPU
- **Rendimiento lento con GPU**: Asegrate de usar `--n-gpu-layers -1` para cargar todas las capas - **Rendimiento lento con GPU**: Asegrate de usar `--n-gpu-layers -1` para cargar todas las capas
- **Error de compatibilidad**: Verifica que tu GPU tenga compute capability >= 3.5 - **Error de compatibilidad**: Verifica que tu GPU tenga compute capability >= 3.5
+35
View File
@@ -0,0 +1,35 @@
# Configuracion editable para run_api_cuda.sh
# Si MODEL_PATH queda vacio, el script usara el primer .gguf en model_choice/
# Ruta al modelo GGUF
MODEL_PATH=""
# Ruta al proyector multimodal (mmproj) cuando el modelo lo necesita (LLaVA, Qwen-VL, etc.)
MM_PROJ_PATH=""
# Red
HOST="0.0.0.0"
PORT=8000
# Parametros del modelo
N_CTX=4096
N_BATCH=512
# Dejar vacio para que se use el valor automatico de llama.cpp
N_THREADS=""
N_GPU_LAYERS=-1 # -1 usa todas las capas en GPU
MAIN_GPU=0
SPLIT_MODE=1
ROPE_FREQ_BASE=10000
ROPE_FREQ_SCALE=1.0
OFFLOAD_KV_CACHE=true
KEEP_MODEL_IN_MEMORY=true # usa mlock para fijar en RAM
TRY_MMAP=true
SEED=0
FLASH_ATTN=true
# Parametros de generacion por defecto (pueden sobreescribirse en cada request)
DEFAULT_MAX_TOKENS=2048
DEFAULT_TEMPERATURE=0.8
DEFAULT_TOP_K=40
DEFAULT_REPEAT_PENALTY=1.1
DEFAULT_MIN_P=0.05
DEFAULT_TOP_P=0.95
+180 -15
View File
@@ -1,9 +1,10 @@
import sys import sys
import time import time
import uuid import uuid
from typing import List, Optional from typing import List, Optional, Union
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Literal
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -12,21 +13,63 @@ import uvicorn
from llama_cpp import Llama from llama_cpp import Llama
def str_to_bool(value: str) -> bool:
"""Parsea valores booleanos desde la CLI."""
if isinstance(value, bool):
return value
lowered = value.strip().lower()
if lowered in {"true", "1", "yes", "y", "t"}:
return True
if lowered in {"false", "0", "no", "n", "f"}:
return False
raise ValueError(f"Valor booleano invalido: {value}")
# Modelos de datos para la API compatible con OpenAI # Modelos de datos para la API compatible con OpenAI
class ChatCompletionMessage(BaseModel): class ChatCompletionMessage(BaseModel):
role: str = Field(..., description="El rol del mensaje: 'system', 'user', o 'assistant'") role: str = Field(..., description="El rol del mensaje: 'system', 'user', o 'assistant'")
content: str = Field(..., description="El contenido del mensaje") content: Union[str, List["MessageContentPart"]] = Field(..., description="Contenido del mensaje (texto o partes multimodales)")
class ChatCompletionRequest(BaseModel): class ChatCompletionRequest(BaseModel):
model: str = Field(default="llama", description="Nombre del modelo") model: str = Field(default="llama", description="Nombre del modelo")
messages: List[ChatCompletionMessage] = Field(..., description="Lista de mensajes") messages: List[ChatCompletionMessage] = Field(..., description="Lista de mensajes")
max_tokens: Optional[int] = Field(default=2048, description="Máximo número de tokens a generar") max_tokens: Optional[int] = Field(default=None, description="Máximo número de tokens a generar")
temperature: Optional[float] = Field(default=0.7, description="Temperatura para el muestreo") temperature: Optional[float] = Field(default=None, description="Temperatura para el muestreo")
top_p: Optional[float] = Field(default=0.9, description="Top-p para el muestreo") top_p: Optional[float] = Field(default=None, description="Top-p para el muestreo")
top_k: Optional[int] = Field(default=None, description="Top-k para el muestreo")
repeat_penalty: Optional[float] = Field(default=None, description="Penalización por repetición")
min_p: Optional[float] = Field(default=None, description="Umbral mínimo de probabilidad (min_p sampling)")
stream: Optional[bool] = Field(default=False, description="Si devolver respuesta en streaming") stream: Optional[bool] = Field(default=False, description="Si devolver respuesta en streaming")
class MessageContentPart(BaseModel):
type: Literal["text", "image_url"]
text: Optional[str] = None
image_url: Optional[str] = None
# Resolver forward refs
ChatCompletionMessage.model_rebuild()
def normalize_image_input(raw: str) -> str:
"""Normaliza imágenes a data URI; si llega base64 simple, se envuelve como data:image/png;base64."""
if raw.startswith("data:"):
return raw
if "://" in raw:
# En este entorno no se hace fetch remoto
raise HTTPException(status_code=400, detail="Solo se aceptan imágenes en data URI o base64 inline")
# Asumimos base64 puro
return f"data:image/png;base64,{raw}"
def likely_vision_model(model_path: str) -> bool:
"""Heurística simple para detectar modelos multimodales."""
lowered = Path(model_path).name.lower()
return any(key in lowered for key in ("vl", "vision", "llava", "mmproj"))
class ChatCompletionChoice(BaseModel): class ChatCompletionChoice(BaseModel):
index: int index: int
message: ChatCompletionMessage message: ChatCompletionMessage
@@ -64,9 +107,13 @@ class ModelsResponse(BaseModel):
class LlamaAPI: class LlamaAPI:
"""Clase para manejar la API de Llama""" """Clase para manejar la API de Llama"""
def __init__(self, model_path: str, **kwargs): def __init__(self, model_path: str, mmproj_path: Optional[str] = None, **kwargs):
if not Path(model_path).exists(): if not Path(model_path).exists():
raise FileNotFoundError(f"El archivo del modelo no existe: {model_path}") raise FileNotFoundError(f"El archivo del modelo no existe: {model_path}")
if mmproj_path:
if not Path(mmproj_path).exists():
raise FileNotFoundError(f"El archivo mmproj no existe: {mmproj_path}")
kwargs["mmproj_path"] = mmproj_path
print(f"Cargando modelo desde: {model_path}") print(f"Cargando modelo desde: {model_path}")
@@ -79,6 +126,13 @@ class LlamaAPI:
"n_gpu_layers": -1, # Usar todas las capas en GPU (-1 = auto) "n_gpu_layers": -1, # Usar todas las capas en GPU (-1 = auto)
"main_gpu": 0, # GPU principal a usar "main_gpu": 0, # GPU principal a usar
"split_mode": 1, # Modo de división entre GPUs "split_mode": 1, # Modo de división entre GPUs
"rope_freq_base": 10000.0,
"rope_freq_scale": 1.0,
"offload_kv": True,
"use_mmap": True,
"use_mlock": False,
"seed": 0,
"flash_attn": False,
} }
# Actualizar con parámetros personalizados # Actualizar con parámetros personalizados
@@ -90,6 +144,12 @@ class LlamaAPI:
print(f"GPU layers: {default_params.get('n_gpu_layers', 'N/A')}") print(f"GPU layers: {default_params.get('n_gpu_layers', 'N/A')}")
print(f"Main GPU: {default_params.get('main_gpu', 'N/A')}") print(f"Main GPU: {default_params.get('main_gpu', 'N/A')}")
except Exception as e: except Exception as e:
vision_hint = likely_vision_model(model_path)
if vision_hint and not mmproj_path:
raise RuntimeError(
"Fallo al cargar modelo multimodal. Se requiere un archivo mmproj. "
"Define --mmproj-path o coloca un .mmproj en model_choice/."
) from e
print(f"Error al cargar el modelo: {e}") print(f"Error al cargar el modelo: {e}")
print("Nota: Si tienes problemas con CUDA, intenta instalar: pip install llama-cpp-python[cublas]") print("Nota: Si tienes problemas con CUDA, intenta instalar: pip install llama-cpp-python[cublas]")
raise raise
@@ -97,11 +157,15 @@ class LlamaAPI:
def generate_chat_completion(self, messages: List[ChatCompletionMessage], def generate_chat_completion(self, messages: List[ChatCompletionMessage],
max_tokens: int = 2048, max_tokens: int = 2048,
temperature: float = 0.7, temperature: float = 0.7,
top_p: float = 0.9) -> str: top_p: float = 0.9,
top_k: int = 40,
repeat_penalty: float = 1.1,
min_p: float = 0.05) -> str:
"""Genera una respuesta de chat usando el modelo""" """Genera una respuesta de chat usando el modelo"""
# Formatear los mensajes en un prompt # Formatear los mensajes en un prompt
prompt = self._format_messages_to_prompt(messages) prompt = self._format_messages_to_prompt(messages)
images = self._collect_images(messages)
try: try:
# Generar respuesta usando llama.cpp # Generar respuesta usando llama.cpp
@@ -110,7 +174,11 @@ class LlamaAPI:
max_tokens=max_tokens, max_tokens=max_tokens,
temperature=temperature, temperature=temperature,
top_p=top_p, top_p=top_p,
top_k=top_k,
repeat_penalty=repeat_penalty,
min_p=min_p,
echo=False, echo=False,
images=images if images else None,
stop=["</s>", "<|im_end|>", "<|endoftext|>"] stop=["</s>", "<|im_end|>", "<|endoftext|>"]
) )
@@ -126,19 +194,52 @@ class LlamaAPI:
prompt_parts = [] prompt_parts = []
for message in messages: for message in messages:
text_content = self._extract_text_from_content(message.content)
if message.role == "system": if message.role == "system":
prompt_parts.append(f"Sistema: {message.content}") prompt_parts.append(f"Sistema: {text_content}")
elif message.role == "user": elif message.role == "user":
prompt_parts.append(f"Usuario: {message.content}") prompt_parts.append(f"Usuario: {text_content}")
elif message.role == "assistant": elif message.role == "assistant":
prompt_parts.append(f"Asistente: {message.content}") prompt_parts.append(f"Asistente: {text_content}")
prompt_parts.append("Asistente:") prompt_parts.append("Asistente:")
return "\n".join(prompt_parts) return "\n".join(prompt_parts)
def _extract_text_from_content(self, content: Union[str, List["MessageContentPart"]]) -> str:
"""Extrae el texto de un contenido que puede ser string o lista multimodal."""
if isinstance(content, str):
return content
texts = []
for part in content:
if part.type == "text" and part.text:
texts.append(part.text)
elif part.type == "image_url":
texts.append("[imagen]")
return " ".join(texts).strip()
def _collect_images(self, messages: List[ChatCompletionMessage]) -> List[str]:
"""Recolecta imágenes (data URI) de los mensajes."""
images: List[str] = []
for message in messages:
if isinstance(message.content, str):
continue
for part in message.content:
if part.type == "image_url" and part.image_url:
images.append(normalize_image_input(part.image_url))
return images
# Instancia global de la API # Instancia global de la API
llama_api = None llama_api = None
generation_defaults = {
"max_tokens": 2048,
"temperature": 0.7,
"top_p": 0.9,
"top_k": 40,
"repeat_penalty": 1.1,
"min_p": 0.05,
}
# Crear la aplicación FastAPI # Crear la aplicación FastAPI
app = FastAPI( app = FastAPI(
@@ -189,11 +290,21 @@ async def create_chat_completion(request: ChatCompletionRequest):
try: try:
# Generar respuesta # Generar respuesta
temperature = request.temperature if request.temperature is not None else generation_defaults["temperature"]
top_p = request.top_p if request.top_p is not None else generation_defaults["top_p"]
top_k = request.top_k if request.top_k is not None else generation_defaults["top_k"]
repeat_penalty = request.repeat_penalty if request.repeat_penalty is not None else generation_defaults["repeat_penalty"]
min_p = request.min_p if request.min_p is not None else generation_defaults["min_p"]
max_tokens = request.max_tokens if request.max_tokens is not None else generation_defaults["max_tokens"]
response_text = llama_api.generate_chat_completion( response_text = llama_api.generate_chat_completion(
messages=request.messages, messages=request.messages,
max_tokens=request.max_tokens or 2048, max_tokens=max_tokens,
temperature=request.temperature or 0.7, temperature=temperature,
top_p=request.top_p or 0.9 top_p=top_p,
top_k=top_k,
repeat_penalty=repeat_penalty,
min_p=min_p
) )
# Crear respuesta en formato OpenAI # Crear respuesta en formato OpenAI
@@ -241,6 +352,8 @@ def main():
help="Tamaño del contexto (default: 4096)") help="Tamaño del contexto (default: 4096)")
parser.add_argument("--n-batch", type=int, default=512, parser.add_argument("--n-batch", type=int, default=512,
help="Tamaño del batch (default: 512)") help="Tamaño del batch (default: 512)")
parser.add_argument("--eval-batch-size", type=int, default=None,
help="Tamaño del batch de evaluación (sobrescribe n-batch si se setea)")
parser.add_argument("--n-threads", type=int, default=None, parser.add_argument("--n-threads", type=int, default=None,
help="Número de threads (default: auto)") help="Número de threads (default: auto)")
parser.add_argument("--n-gpu-layers", type=int, default=-1, parser.add_argument("--n-gpu-layers", type=int, default=-1,
@@ -249,6 +362,35 @@ def main():
help="GPU principal a usar (default: 0)") help="GPU principal a usar (default: 0)")
parser.add_argument("--split-mode", type=int, default=1, parser.add_argument("--split-mode", type=int, default=1,
help="Modo de división entre GPUs (default: 1)") help="Modo de división entre GPUs (default: 1)")
parser.add_argument("--rope-freq-base", type=float, default=10000.0,
help="Base de frecuencia ROPE (default: 10000)")
parser.add_argument("--rope-freq-scale", type=float, default=1.0,
help="Escala de frecuencia ROPE (default: 1.0)")
parser.add_argument("--offload-kv-cache", type=str_to_bool, default=True,
help="Offload del KV cache a GPU (default: true)")
parser.add_argument("--keep-model-in-memory", type=str_to_bool, default=False,
help="Usa mlock para mantener el modelo en RAM (default: false)")
parser.add_argument("--try-mmap", type=str_to_bool, default=True,
help="Usar mmap para mapear el modelo (default: true)")
parser.add_argument("--seed", type=int, default=0,
help="Seed para la generación (0 = aleatorio)")
parser.add_argument("--flash-attn", type=str_to_bool, default=False,
help="Habilitar Flash Attention si está disponible")
# Defaults de generación
parser.add_argument("--default-max-tokens", type=int, default=2048,
help="Límite de tokens de respuesta por defecto")
parser.add_argument("--default-temperature", type=float, default=0.7,
help="Temperatura por defecto")
parser.add_argument("--default-top-k", type=int, default=40,
help="Top-k por defecto")
parser.add_argument("--default-repeat-penalty", type=float, default=1.1,
help="Penalización de repetición por defecto")
parser.add_argument("--default-min-p", type=float, default=0.05,
help="Min-p por defecto")
parser.add_argument("--default-top-p", type=float, default=0.9,
help="Top-p por defecto")
parser.add_argument("--mmproj-path", type=str, default=None,
help="Ruta al proyector multimodal (mmproj) si el modelo lo requiere")
args = parser.parse_args() args = parser.parse_args()
@@ -260,25 +402,48 @@ def main():
# Configurar parámetros del modelo # Configurar parámetros del modelo
model_params = { model_params = {
"n_ctx": args.n_ctx, "n_ctx": args.n_ctx,
"n_batch": args.n_batch, "n_batch": args.eval_batch_size or args.n_batch,
"n_gpu_layers": args.n_gpu_layers, "n_gpu_layers": args.n_gpu_layers,
"main_gpu": args.main_gpu, "main_gpu": args.main_gpu,
"split_mode": args.split_mode, "split_mode": args.split_mode,
"rope_freq_base": args.rope_freq_base,
"rope_freq_scale": args.rope_freq_scale,
"offload_kv": args.offload_kv_cache,
"use_mmap": args.try_mmap,
"use_mlock": args.keep_model_in_memory,
"seed": args.seed,
"flash_attn": args.flash_attn,
} }
if args.n_threads: if args.n_threads:
model_params["n_threads"] = args.n_threads model_params["n_threads"] = args.n_threads
# Defaults de generación para usar si la request no los especifica
global generation_defaults
generation_defaults = {
"max_tokens": args.default_max_tokens,
"temperature": args.default_temperature,
"top_p": args.default_top_p,
"top_k": args.default_top_k,
"repeat_penalty": args.default_repeat_penalty,
"min_p": args.default_min_p,
}
try: try:
# Inicializar la API de Llama # Inicializar la API de Llama
global llama_api global llama_api
llama_api = LlamaAPI(args.model_path, **model_params) llama_api = LlamaAPI(args.model_path, mmproj_path=args.mmproj_path, **model_params)
print(f"\n🚀 Servidor iniciado en http://{args.host}:{args.port}") print(f"\n🚀 Servidor iniciado en http://{args.host}:{args.port}")
print(f"📚 Documentación disponible en http://{args.host}:{args.port}/docs") print(f"📚 Documentación disponible en http://{args.host}:{args.port}/docs")
print(f"🤖 Modelo cargado desde: {args.model_path}") print(f"🤖 Modelo cargado desde: {args.model_path}")
print(f"🎮 GPU layers: {args.n_gpu_layers} (usa -1 para todas las capas)") print(f"🎮 GPU layers: {args.n_gpu_layers} (usa -1 para todas las capas)")
print(f"🎯 Main GPU: {args.main_gpu}") print(f"🎯 Main GPU: {args.main_gpu}")
print(f"🧠 ROPE base/scale: {args.rope_freq_base} / {args.rope_freq_scale}")
print(f"🧮 Offload KV cache: {args.offload_kv_cache} | mmap: {args.try_mmap} | mlock: {args.keep_model_in_memory}")
print(f"⚡ Flash Attn: {args.flash_attn} | Seed: {args.seed}")
print(f"🖼️ mmproj: {args.mmproj_path or '<none>'}")
print(f"🎛 Defaults -> max_tokens: {generation_defaults['max_tokens']}, temp: {generation_defaults['temperature']}, top_k: {generation_defaults['top_k']}, repeat_penalty: {generation_defaults['repeat_penalty']}, min_p: {generation_defaults['min_p']}, top_p: {generation_defaults['top_p']}")
print("\n💡 Ejemplo de uso con curl:") print("\n💡 Ejemplo de uso con curl:")
print(f""" print(f"""
curl -X POST http://{args.host}:{args.port}/v1/chat/completions \\ curl -X POST http://{args.host}:{args.port}/v1/chat/completions \\
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
# Ejecuta la API usando CUDA tomando el modelo GGUF de model_choice
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONF_FILE="${SCRIPT_DIR}/api_cuda.conf"
if [[ -f "${CONF_FILE}" ]]; then
# shellcheck source=/dev/null
source "${CONF_FILE}"
else
echo "Archivo de configuracion no encontrado en ${CONF_FILE}. Usando valores por defecto."
fi
# Valores por defecto si no vienen del .conf o del entorno
HOST="${HOST:-0.0.0.0}"
PORT="${PORT:-8000}"
N_CTX="${N_CTX:-4096}"
N_BATCH="${N_BATCH:-512}"
N_THREADS="${N_THREADS:-}"
N_GPU_LAYERS="${N_GPU_LAYERS:--1}"
MAIN_GPU="${MAIN_GPU:-0}"
SPLIT_MODE="${SPLIT_MODE:-1}"
ROPE_FREQ_BASE="${ROPE_FREQ_BASE:-10000}"
ROPE_FREQ_SCALE="${ROPE_FREQ_SCALE:-1.0}"
OFFLOAD_KV_CACHE="${OFFLOAD_KV_CACHE:-true}"
KEEP_MODEL_IN_MEMORY="${KEEP_MODEL_IN_MEMORY:-false}"
TRY_MMAP="${TRY_MMAP:-true}"
SEED="${SEED:-0}"
FLASH_ATTN="${FLASH_ATTN:-false}"
MM_PROJ_PATH="${MM_PROJ_PATH:-}"
# Defaults for sampling (se pueden sobreescribir en la peticion)
DEFAULT_MAX_TOKENS="${DEFAULT_MAX_TOKENS:-2048}"
DEFAULT_TEMPERATURE="${DEFAULT_TEMPERATURE:-0.7}"
DEFAULT_TOP_K="${DEFAULT_TOP_K:-40}"
DEFAULT_REPEAT_PENALTY="${DEFAULT_REPEAT_PENALTY:-1.1}"
DEFAULT_MIN_P="${DEFAULT_MIN_P:-0.05}"
DEFAULT_TOP_P="${DEFAULT_TOP_P:-0.9}"
# Determinar el modelo: usa MODEL_PATH si esta definido, si no toma el primer .gguf en model_choice
MODEL_PATH_VALUE="${MODEL_PATH:-}"
if [[ -z "${MODEL_PATH_VALUE}" ]]; then
MODEL_PATH_VALUE="$(find "${SCRIPT_DIR}/model_choice" -maxdepth 1 -type f -name '*.gguf' | head -n 1 || true)"
fi
if [[ -z "${MODEL_PATH_VALUE}" || ! -f "${MODEL_PATH_VALUE}" ]]; then
echo "No se encontro un modelo .gguf. Define MODEL_PATH en ${CONF_FILE} o agrega un archivo a model_choice/."
exit 1
fi
# Autodetectar mmproj si no se configuro (acepta .mmproj o archivos .gguf que contengan 'mmproj')
if [[ -z "${MM_PROJ_PATH}" ]]; then
MODEL_DIR="$(dirname "${MODEL_PATH_VALUE}")"
MM_PROJ_PATH="$(find "${MODEL_DIR}" -maxdepth 1 -type f -name '*.mmproj' -o -name '*mmproj*.gguf' | head -n 1 || true)"
fi
if [[ -z "${MM_PROJ_PATH}" ]]; then
MM_PROJ_PATH="$(find "${SCRIPT_DIR}/model_choice" -maxdepth 1 -type f -name '*.mmproj' -o -name '*mmproj*.gguf' | head -n 1 || true)"
fi
# Elegir ejecutor: preferimos uv run para respetar uv.lock; si no, se usa python directo
if command -v uv >/dev/null 2>&1; then
export UV_CACHE_DIR="${SCRIPT_DIR}/.uv_cache"
PY_RUNNER=(uv run python)
else
PY_RUNNER=(python)
fi
CMD=(
"${PY_RUNNER[@]}"
"${SCRIPT_DIR}/main.py"
--model-path "${MODEL_PATH_VALUE}"
--host "${HOST}"
--port "${PORT}"
--n-ctx "${N_CTX}"
--n-batch "${N_BATCH}"
--n-gpu-layers "${N_GPU_LAYERS}"
--main-gpu "${MAIN_GPU}"
--split-mode "${SPLIT_MODE}"
--rope-freq-base "${ROPE_FREQ_BASE}"
--rope-freq-scale "${ROPE_FREQ_SCALE}"
--offload-kv-cache "${OFFLOAD_KV_CACHE}"
--keep-model-in-memory "${KEEP_MODEL_IN_MEMORY}"
--try-mmap "${TRY_MMAP}"
--seed "${SEED}"
--flash-attn "${FLASH_ATTN}"
--default-max-tokens "${DEFAULT_MAX_TOKENS}"
--default-temperature "${DEFAULT_TEMPERATURE}"
--default-top-k "${DEFAULT_TOP_K}"
--default-repeat-penalty "${DEFAULT_REPEAT_PENALTY}"
--default-min-p "${DEFAULT_MIN_P}"
--default-top-p "${DEFAULT_TOP_P}"
)
if [[ -n "${N_THREADS}" ]]; then
CMD+=(--n-threads "${N_THREADS}")
fi
if [[ -n "${MM_PROJ_PATH}" ]]; then
CMD+=(--mmproj-path "${MM_PROJ_PATH}")
fi
echo "Iniciando API con CUDA"
echo "Modelo: ${MODEL_PATH_VALUE}"
echo "Host/Port: ${HOST}:${PORT}"
echo "n_ctx=${N_CTX}, n_batch=${N_BATCH}, n_gpu_layers=${N_GPU_LAYERS}, main_gpu=${MAIN_GPU}, split_mode=${SPLIT_MODE}, n_threads=${N_THREADS:-auto}"
echo "rope_base=${ROPE_FREQ_BASE}, rope_scale=${ROPE_FREQ_SCALE}, offload_kv=${OFFLOAD_KV_CACHE}, mmap=${TRY_MMAP}, mlock=${KEEP_MODEL_IN_MEMORY}, seed=${SEED}, flash_attn=${FLASH_ATTN}"
echo "mmproj_path=${MM_PROJ_PATH:-<none>}"
echo "defaults -> max_tokens=${DEFAULT_MAX_TOKENS}, temp=${DEFAULT_TEMPERATURE}, top_k=${DEFAULT_TOP_K}, repeat_penalty=${DEFAULT_REPEAT_PENALTY}, min_p=${DEFAULT_MIN_P}, top_p=${DEFAULT_TOP_P}"
# Exportar bandera de compilacion por si se re-instala llama-cpp con cublas; no afecta si ya esta instalado
export CMAKE_ARGS="-DLLAMA_CUBLAS=on"
exec "${CMD[@]}"