app.py β Despliegue Modal Completo
Python 3.11
nutripaw_modal/app.py
# NutriPaw AI β Modal Serverless GPU Deployment
# Skill: modal-serverless-gpu-cloud | CULTIVA IA
import modal
from pydantic import BaseModel
app = modal.App("nutripaw-ai")
# ββ Imagen con vLLM + dependencias ββββββββββββββββββ
image = (
modal.Image.debian_slim(python_version="3.11")
.uv_pip_install(
"vllm==0.6.4",
"fastapi==0.115.0",
"pydantic==2.8.0",
)
)
# ββ Volumen persistente para pesos del modelo ββββββββ
weights_vol = modal.Volume.from_name(
"nutripaw-model-weights",
create_if_missing=True,
)
# Ruta: /weights/nutripaw-llama3-8b-q4.gguf (16 GB)
# ββ Secret HuggingFace βββββββββββββββββββββββββββββββ
hf_secret = modal.Secret.from_name("huggingface-token")
# ββ Clase principal con lifecycle hooks ββββββββββββββ
@app.cls(
gpu="L40S", # 48 GB VRAM, mejor precio/perf
image=image,
volumes={"/weights": weights_vol},
secrets=[hf_secret],
min_containers=1, # Warm: latencia baja
max_containers=10,
scaledown_window=600,
)
class NutriPawAI:
@modal.enter()
def load_model(self):
from vllm import LLM, SamplingParams
self.llm = LLM(
model="/weights/nutripaw-llama3-8b-q4",
dtype="float16",
max_model_len=4096,
)
self.sampling = SamplingParams(
temperature=0.3, max_tokens=512,
)
# ββ Web endpoint (POST /generate) ββββββββββββββββ
@modal.method()
@modal.fastapi_endpoint(method="POST", path="/generate")
def generate_plan(self, request: "PetProfileRequest"):
prompt = build_prompt(request)
outputs = self.llm.generate([prompt], self.sampling)
return {
"plan": outputs[0].outputs[0].text,
"tokens": len(outputs[0].outputs[0].token_ids),
}
@modal.exit()
def cleanup(self):
del self.llm
# ββ Batch: regenerar 5.000 planes en paralelo ββββββββ
@app.function(
gpu="L40S", image=image,
cpu=4.0, memory=16384,
volumes={"/weights": weights_vol},
max_containers=20, timeout=1800,
)
def batch_generate_plan(pet_profile: dict) -> dict:
# Cada invocaciΓ³n = 1 mascota; .map() paralela
from vllm import LLM, SamplingParams
llm = LLM(model="/weights/nutripaw-llama3-8b-q4")
out = llm.generate([build_prompt(pet_profile)])
return {"pet_id": pet_profile["id"], "plan": out[0].outputs[0].text}
# ββ Job nocturno programado 02:00 UTC ββββββββββββββββ
@app.function(
schedule=modal.Cron("0 2 * * *"),
secrets=[modal.Secret.from_name("nutripaw-db")],
timeout=3600,
)
def nightly_batch():
import os, json
profiles = fetch_all_pets_from_db(os.environ["DATABASE_URL"])
results = list(batch_generate_plan.map(profiles))
save_plans_to_db(results)
print(f"β {len(results)} planes regenerados")
Flujo de Arquitectura
π± App NutriPaw
iOS / Android
iOS / Android
β POST /generate
π Modal Router
URL permanente Β· autoscale
URL permanente Β· autoscale
β despacho
β‘ NutriPawAI cls
L40S GPU Β· vLLM Β· min_containers=1
L40S GPU Β· vLLM Β· min_containers=1
β pesos
β token
πΎ Volume
nutripaw-model-weights
16 GB Q4_K_M
nutripaw-model-weights
16 GB Q4_K_M
π Secret
huggingface-token
nutripaw-db
huggingface-token
nutripaw-db
π Cron 02:00 UTC
nightly_batch β .map(5000) β DB
nightly_batch β .map(5000) β DB
Pasos de Despliegue
1
Autenticar Modal
modal setup o variables MODAL_TOKEN_ID/SECRET2
Subir pesos al Volumen
modal volume put nutripaw-model-weights ./model3
Crear Secrets
modal secret create huggingface-token HF_TOKEN=hf_xxx4
Test en dev
modal serve app.py β URL temporal + hot reload5
Deploy producciΓ³n
modal deploy app.py β URL permanente + cron activo