Spaces:
Sleeping
Sleeping
File size: 6,716 Bytes
af530a3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | #!/usr/bin/env bash
# Réplica LOCAL del Space de medGemma (blackmistcode/morphos_medGemma).
#
# Para qué: las evals comparan modelos, pero la ruta de producción no es «el modelo» — es el
# modelo DENTRO de la envoltura del Space (presupuesto de 3072 tokens compartido entre el
# razonamiento y la respuesta, `extract_response` que tira el razonamiento, penalización de
# repetición 1.1). Medir el GGUF en Ollama mide otra cosa. Esto levanta el `app.py` REAL del
# Space en local, así que el cliente de producción (`app/ai/hf_space.py`) habla con él sin
# cambiar una línea:
#
# MORPHOS_HF_SPACE_URL=http://127.0.0.1:7860/gradio_api
#
# Lo único que no se replica es el hardware (MPS en vez de ZeroGPU) y, con él, la muerte por
# reserva agotada. Todo lo demás es el mismo código.
#
# Uso:
# scripts/space_local.sh # monta si hace falta y arranca
# scripts/space_local.sh --recrear # rehace el venv desde cero
#
# TODO vive en el SSD externo (venv, caché de HF con los ~9 GB de pesos, caché de uv): el disco
# interno va justo de espacio y es la razón de que los modelos estén fuera.
set -euo pipefail
BASE="${MORPHOS_SPACE_LOCAL_DIR:-/Volumes/Extreme SSD/morphos-space}"
SPACE_REPO="${MORPHOS_SPACE_REPO:-blackmistcode/morphos_medGemma}"
PUERTO="${GRADIO_SERVER_PORT:-7860}"
# El modelo es gated: hace falta el token de HF (el mismo que ya usa `hf`). Se lee de
# ~/.cache/huggingface/token si no está en el entorno.
if [ -z "${HF_TOKEN:-}" ] && [ -f "$HOME/.cache/huggingface/token" ]; then
HF_TOKEN="$(cat "$HOME/.cache/huggingface/token")"
export HF_TOKEN
fi
echo "==> Base: $BASE"
# --- 1. El SSD tiene que estar montado Y ser escribible -----------------------------------
VOLUMEN="$(dirname "$BASE")"
if [ ! -d "$VOLUMEN" ]; then
echo "ERROR: no está montado '$VOLUMEN'. Conecta el disco y repite." >&2
exit 1
fi
if ! mkdir -p "$BASE" 2>/dev/null; then
cat >&2 <<EOF
ERROR: no se puede escribir en '$VOLUMEN'.
El volumen está montado con la propiedad ACTIVADA y su raíz es de root:wheel, así que tu
usuario no puede crear nada arriba del todo. Se arregla una vez:
sudo mkdir -p "$BASE" && sudo chown -R "\$(id -un):staff" "$BASE"
(o Finder → Cmd-I sobre el disco → «Ignorar permisos en este volumen»)
Si en cambio el error fuera «Operation not permitted» incluso con sudo, es TCC: ejecuta este
script desde Terminal.app, no desde el terminal integrado del editor.
EOF
exit 1
fi
# --- 2. Todas las cachés al SSD ------------------------------------------------------------
export HF_HOME="$BASE/hf-cache"
export UV_CACHE_DIR="$BASE/uv-cache"
export GRADIO_SERVER_PORT="$PUERTO"
mkdir -p "$HF_HOME" "$UV_CACHE_DIR"
# --- 3. Entorno virtual --------------------------------------------------------------------
VENV="$BASE/venv"
if [ "${1:-}" = "--recrear" ]; then
echo "==> Borrando venv anterior"
rm -rf "$VENV"
fi
if [ ! -x "$VENV/bin/python" ]; then
echo "==> Creando venv en el SSD (uv)"
uv venv --python 3.12 "$VENV"
fi
# Las dependencias se sincronizan SIEMPRE, no sólo al crear el venv. Cuando el requirements.txt
# del Space crece, un venv ya existente se saltaba la instalación y la réplica se quedaba corta
# en silencio: así apareció `lm-format-enforcer`, sin el cual `app.py` cae al modo prosa y
# anuncia «salida estructurada NO disponible» — midiendo justo lo que el Space no hace. Con
# todo satisfecho uv resuelve y sale, sin descargar nada.
echo "==> Sincronizando dependencias (torch para MPS, transformers, gradio…)"
# `spaces` es el paquete del decorador @spaces.GPU. Fuera de HF es un passthrough; se instala
# para no tener que tocar la firma de `analyze`.
# `lm-format-enforcer` es la decodificación restringida: va en el requirements.txt del Space, así
# que sin él la réplica NO es la réplica.
VIRTUAL_ENV="$VENV" uv pip install --python "$VENV/bin/python" \
torch transformers gradio spaces pillow safetensors "lm-format-enforcer>=0.10"
# --- 4. app.py de upstream + parche mínimo -------------------------------------------------
# Se descarga cada vez a propósito: si el Space cambia, la réplica cambia con él. El parche se
# aplica sobre el fichero recién traído y falla ruidosamente si upstream ya no encaja, que es
# justo cuando hay que mirar en vez de seguir midiendo con una copia divergente.
echo "==> Descargando app.py de $SPACE_REPO"
curl -fsSL "https://huggingface.co/spaces/$SPACE_REPO/raw/main/app.py" -o "$BASE/app_upstream.py"
"$VENV/bin/python" - "$BASE/app_upstream.py" "$BASE/app_local.py" <<'PYPATCH'
"""Aplica a app.py los cambios mínimos para correr fuera de ZeroGPU.
Cada sustitución se verifica: si upstream deja de encajar, se aborta. Una réplica que se
parchea «a medias» mediría algo que no es el Space y no lo diría.
"""
import re
import sys
origen, destino = sys.argv[1], sys.argv[2]
codigo = open(origen, encoding="utf-8").read()
deltas = []
def sustituir(patron, reemplazo, descripcion, *, obligatorio=True):
global codigo
# re.M porque uno de los anclajes es a principio de línea (`^MODEL_ID`), no de fichero.
nuevo, n = re.subn(patron, reemplazo, codigo, count=1, flags=re.M)
if n:
codigo = nuevo
deltas.append(descripcion)
elif obligatorio:
raise SystemExit(f"ERROR: no encaja el parche «{descripcion}». ¿Cambió app.py upstream?")
# 1. ZeroGPU es CUDA; aquí hay Apple Silicon. Se resuelve el dispositivo en tiempo de arranque.
sustituir(
r'\.to\("cuda"\)',
'.to(_dispositivo())',
'cuda → mps/cpu',
)
# 2. transformers 5.x retiró `torch_dtype` en favor de `dtype`.
sustituir(
r'torch_dtype=torch\.bfloat16',
'dtype=torch.bfloat16',
'torch_dtype → dtype (transformers 5.x)',
obligatorio=False,
)
# 3. El resolvedor de dispositivo, justo antes de cargar el modelo.
sustituir(
r'^MODEL_ID = ',
'def _dispositivo():\n'
' """MPS si existe; si no, CPU. bfloat16 va en ambos, sólo cambia la velocidad."""\n'
' import torch as _t\n'
' return "mps" if _t.backends.mps.is_available() else "cpu"\n'
'\n'
'MODEL_ID = ',
'resolvedor de dispositivo',
)
open(destino, "w", encoding="utf-8").write(codigo)
print(" parches aplicados: " + "; ".join(deltas))
PYPATCH
# --- 5. Arranque ---------------------------------------------------------------------------
cat <<EOF
==> Listo. Arrancando Gradio en http://127.0.0.1:$PUERTO
Pesos y caché: $HF_HOME
La primera vez descarga ~9 GB del modelo (gated: usa tu token de HF).
Para que las evals lo usen, en OTRA terminal:
export MORPHOS_HF_SPACE_URL=http://127.0.0.1:$PUERTO/gradio_api
EOF
cd "$BASE"
exec "$VENV/bin/python" app_local.py
|