BioRAG / model_app.py
C2MV's picture
Upload 8 files
d3d8124 verified
raw
history blame
6.43 kB
# modal_app.py
import modal
import sys
from pathlib import Path
import os # Para HF_TOKEN
# --- Configuración ---
PYTHON_VERSION = "3.10"
APP_NAME = "bioprocess-custom-eq-agent-modal"
# Directorios (asume que modal_app.py está en la raíz del proyecto junto a los otros .py)
LOCAL_APP_DIR = Path(__file__).parent
REMOTE_APP_DIR = "/app" # Directorio dentro del contenedor Modal
stub = modal.Stub(APP_NAME)
# Definición de la imagen del contenedor Modal
app_image = (
modal.Image.debian_slim(python_version=PYTHON_VERSION)
.pip_install_from_requirements(LOCAL_APP_DIR / "requirements.txt")
.copy_mount(
modal.Mount.from_local_dir(LOCAL_APP_DIR, remote_path=REMOTE_APP_DIR)
)
.env({
"PYTHONPATH": REMOTE_APP_DIR,
"HF_HOME": "/cache/huggingface", # Directorio de caché de Hugging Face
"HF_HUB_CACHE": "/cache/huggingface/hub",
"TRANSFORMERS_CACHE": "/cache/huggingface/hub", # Alias común
"MPLCONFIGDIR": "/tmp/matplotlib_cache" # Para evitar warnings de matplotlib
})
.run_commands( # Comandos para ejecutar durante la construcción de la imagen
"apt-get update && apt-get install -y git git-lfs && rm -rf /var/lib/apt/lists/*", # git-lfs para algunos modelos
"mkdir -p /cache/huggingface/hub /tmp/matplotlib_cache" # Crear directorios de caché
)
)
# --- Función Modal para Generación de Análisis con LLM ---
@stub.function(
image=app_image,
gpu="any", # Solicitar GPU (ej. "T4", "A10G", o "any")
secrets=[
modal.Secret.from_name("huggingface-read-token", optional=True) # Para modelos privados/gated
],
timeout=600, # 10 minutos de timeout
# Montar un volumen para cachear modelos de Hugging Face
volumes={"/cache/huggingface": modal.Volume.persisted(f"{APP_NAME}-hf-cache-vol")}
)
def generate_analysis_llm_modal_remote(prompt: str, model_path_config: str, max_new_tokens_config: int) -> str:
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
# El token de HF se inyecta como variable de entorno si el secreto está configurado
hf_token = os.environ.get("HUGGING_FACE_TOKEN")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"LLM Modal Func: Usando dispositivo: {device}")
print(f"LLM Modal Func: Cargando modelo: {model_path_config} con token: {'Sí' if hf_token else 'No'}")
try:
tokenizer = AutoTokenizer.from_pretrained(model_path_config, cache_dir="/cache/huggingface/hub", token=hf_token)
model = AutoModelForCausalLM.from_pretrained(
model_path_config,
torch_dtype="auto", # bfloat16 en A100/H100, float16 en otras
device_map="auto", # Distribuye automáticamente en GPUs disponibles
cache_dir="/cache/huggingface/hub",
token=hf_token,
# low_cpu_mem_usage=True # Puede ayudar con modelos muy grandes
)
# No es necesario .to(device) explícitamente con device_map="auto"
# model.eval() no es necesario si solo se hace inferencia y no se entrena
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=4096-max_new_tokens_config).to(model.device) # Truncar prompt si es muy largo
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens_config,
eos_token_id=tokenizer.eos_token_id,
pad_token_id=tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id,
do_sample=True,
temperature=0.6, # Ajustar para creatividad vs factualidad
top_p=0.9,
# num_beams=1 # Usar num_beams > 1 para beam search si se desea, pero más lento
)
# Decodificar solo los tokens generados nuevos, no el prompt
input_length = inputs.input_ids.shape[1]
generated_ids = outputs[0][input_length:]
analysis = tokenizer.decode(generated_ids, skip_special_tokens=True)
print(f"LLM Modal Func: Longitud del análisis generado: {len(analysis)} caracteres.")
return analysis.strip()
except Exception as e:
error_traceback = traceback.format_exc()
print(f"Error en generate_analysis_llm_modal_remote: {e}\n{error_traceback}")
return f"Error al generar análisis con el modelo LLM: {str(e)}"
# --- Servidor Gradio ---
@stub.asgi_app() # image se hereda del stub si no se especifica aquí
def serve_gradio_app_asgi():
# Estas importaciones ocurren DENTRO del contenedor Modal
import gradio as gr
sys.path.insert(0, REMOTE_APP_DIR) # Asegurar que los módulos de la app son importables
# Importar los módulos de la aplicación AHORA que sys.path está configurado
from UI import create_interface
import interface as app_interface_module # Renombrar para claridad
from config import MODEL_PATH as cfg_MODEL_PATH, MAX_LENGTH as cfg_MAX_LENGTH
# Wrapper para llamar a la función Modal remota desde tu interface.py
def analysis_func_wrapper_for_interface(prompt: str) -> str:
print("Gradio Backend: Llamando a generate_analysis_llm_modal_remote.remote...")
return generate_analysis_llm_modal_remote.remote(prompt, cfg_MODEL_PATH, cfg_MAX_LENGTH)
# Inyectar esta función wrapper en el módulo `interface`
app_interface_module.generate_analysis_from_modal = analysis_func_wrapper_for_interface
app_interface_module.USE_MODAL_FOR_LLM_ANALYSIS = True
# Crear la app Gradio y conectar el botón
gradio_ui, all_ui_inputs, ui_outputs, ui_submit_button = create_interface()
ui_submit_button.click(
fn=app_interface_module.process_and_plot,
inputs=all_ui_inputs,
outputs=ui_outputs
)
return gr.routes.App.create_app(gradio_ui) # Para montar Gradio en FastAPI/ASGI
# (Opcional) Un entrypoint local para probar rápidamente la generación LLM
@stub.local_entrypoint()
def test_llm():
print("Probando la generación de LLM con Modal (localmente)...")
from config import MODEL_PATH, MAX_LENGTH
sample_prompt = "Explica brevemente el concepto de R cuadrado (R²) en el ajuste de modelos."
analysis = generate_analysis_llm_modal_remote.remote(sample_prompt, MODEL_PATH, MAX_LENGTH)
print("\nRespuesta del LLM:")
print(analysis)