Volver a la wiki

Proxmox VE 9.1 sobre Hetzner EPYC 7502P — Setup y Operación

Proxmox VE 9.1 sobre Hetzner EPYC 7502P

⚠️ SERVIDOR DADO DE BAJA · s53 (08-05-2026)

pve-epyc-02 fue wipe (NIST SP 800-88: nvme format --ses=1 × 4 NVMe + verificación dd) y cancelado en Hetzner Robot el 2026-05-08. Las IPs 23.88.74.167 / 2a01:4f8:272:5f10::2 y los hosts Tailscale pve-epyc-02 / crearack-llama-epyc-02 ya no son de CreaRack. Toda la stack AI corre ahora en Google AI Studio Paid Tier managed.

Runbook conservado como referencia histórica del proceso de setup Proxmox + LXC + llama-server (útil si en el futuro se vuelve a evaluar self-host LLM).


Última actualización: 01-05-2026 (sesión 46 · afinado calidad Auto-Plan 70%→99% con flags vision Reddit + 32 cores + mmproj F32) Estado: setup completado en su día · servidor dado de baja en s53 Hardware: Hetzner Server Auction · AMD EPYC 7502P 32c/64t · 256 GB DDR4-2933 ECC reg (8×32 GB, 8 canales) · 4× Toshiba KXD51RUE960G NVMe U.2 960 GB Datacenter Hostname: pve-epyc-02 IP pública: 23.88.74.167 / 2a01:4f8:272:5f10::2 Tailscale host: pve-epyc-02 / 100.123.143.35 Tailscale LXC llama-server: crearack-llama-epyc-02 / 100.80.142.60


1. Por qué Proxmox

Decidido como patrón canónico para hospedar simultáneamente:

Ventajas que aporta Proxmox vs Ubuntu directo

VentajaAplicación concreta
Snapshots en calienteAntes de upgrade llama.cpp, Django, PG → snapshot de 5 s. Rollback en 30 s.
Backups por VM/LXC integradosvzdump nightly schedule con retention. Restore = arrastrar fichero .vma o .tar.zst.
Aislamiento entre workloadsCrearack panic ≠ caída del llama-server, y viceversa. Reinicios independientes.
Migración futuraSi compramos un 2º Hetzner, cluster Proxmox + live migration entre nodos.
Recursos elásticosReasignación CPU/RAM por workload sin reinstalar.
Storage avanzado (ZFS)Snapshots filesystem, deduplicación, compresión LZ4 transparente, scrubbing.

Coste


2. Versiones y datos del stack

ComponenteVersión
Proxmox VE9.1.9 (sesión 45)
Base OSDebian 13 “Trixie”
Kernelproxmox-kernel-7.0.0-3-pve-signed
QEMU10.1
LXC6.0
OpenZFS2.4.1 (zfs-2.4.1-pve1)
CephSquid (no usado en single-node)
Soporte EOL~2030 (sigue ciclo Debian 13)

Mejoras destacadas PVE 9.1


3. Arquitectura final

┌──────────────────────────────────────────────────────────────────┐
│ HOST: pve-epyc-02 (Proxmox VE 9.1.9, kernel 7.0.0-3-pve)         │
│ EPYC 7502P 32c/64t · 256 GB DDR4-2933 ECC · ~187 GB/s bandwidth  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│ STORAGE (4× NVMe Toshiba KXD51RUE960G U.2 894 GB)                │
│ ├─ md0/md1/md2 (mdadm RAID 1 sobre 2× NVMe)                      │
│ │   ├─ /boot 1G ext4                                             │
│ │   ├─ swap 8G                                                   │
│ │   └─ / 885G ext4 (host root + /var/lib/vz LXC rootfs)          │
│ ├─ pool ZFS `llamacpp` (single NVMe 888 GB, by-id)               │
│ │   └─ llamacpp/models → bind LXC 100 → /var/lib/llamacpp/models │
│ └─ pool ZFS `backup`  (single NVMe 888 GB, by-id)                │
│     ├─ backup/zfs-snapshots                                      │
│     └─ backup/lxc-dumps → vzdump destination                     │
│                                                                  │
│ NETWORK                                                          │
│ ├─ enp195s0  → NIC pública (23.88.74.167/26)                     │
│ ├─ vmbr1     → bridge interno LXC (10.10.0.1/24, NAT MASQUERADE) │
│ └─ tailscale0 → VPN privada (CGNAT 100.64.0.0/10)                │
│                                                                  │
│ GUESTS                                                           │
│ └─ LXC 100 crearack-llama-epyc-02  (24 cores físicos · 64 GB)    │
│     · cgroup pin: lxc.cgroup2.cpuset.cpus = 8-31                 │
│     · llama.cpp b8984 build from source (NATIVE Zen 2)           │
│     · Gemma 4 26B-A4B-it Q4_K_M + mmproj-F16                     │
│     · listening :8080 (vía Tailscale 100.80.142.60)              │
│                                                                  │
│ MARGEN: 32+ vCPU + 192 GB RAM + 1.7 TB ZFS libres                │
└──────────────────────────────────────────────────────────────────┘

Decisión LXC vs KVM por workload

GuestTipoRazón
llama-serverLXC unprivilegedCPU-puro, 0 % overhead, performance bare-metal.
crearack-prod (futuro)VM KVM completaAislamiento total, kernel propio, Docker nativo sin quirks (nesting, keyctl, AppArmor profiles), migración limpia entre nodos, backup completo (kernel + estado).

KVM es el patrón canónico para workloads stateful con base de datos. LXC con Docker funciona pero con fricciones que en producción crítica no merecen la pena. Para inference CPU sin BD ni estado persistente, LXC gana sin discusión.

Memoria — reparto explícito

ComponenteReserva
Host Proxmox + servicios6 GB
ZFS ARC (cap explícito)8 GB
1G hugepages reservadas (LLM)12 GB
LXC 100 llama-server64 GB
VM 200 crearack-prod (futuro)64-96 GB
Margen libre60-80 GB
Total256 GB

⚠️ ARC ZFS por defecto se autotunea, pero fijamos manualmente 8 GB porque los datos calientes son los modelos LLM (que carga llama.cpp directamente en RAM con --no-mmap) y queremos toda la RAM disponible para guests.

Bandwidth de RAM — el factor crítico

EPYC 7502P (Zen 2 Rome) tiene 8 canales DDR4 nativos. Con 8 DIMMs llenos a DDR4-2933 (frecuencia oficial Rome con 1 DPC × 2 ranks): ~187 GB/s teóricos, ~140-160 GB/s reales. Para inference LLM en CPU, el bandwidth de RAM es el cuello de botella dominante (cada token decode requiere leer todos los pesos activos del modelo). Con 4 canales DDR4-3200 estaríamos en ~102 GB/s teóricos: la elección de DIMMs llenos es lo que hace viable este host.


4. Runbook de instalación (sesión 45)

Paso 0 — Compra y entrega

Paso 1 — Inspección hardware en rescue

ssh -i ~/.ssh/hetzner_llama_ed25519 root@<IP>

uname -a
lsblk -o NAME,SIZE,TYPE,MODEL
grep -E 'model name|cpu cores' /proc/cpuinfo | head -2
free -h
dmidecode -t memory | grep -E 'Size:|Speed:' | grep -v 'No Module' | head -20

Verificar:

Paso 2 — installimage Debian 13 Trixie + mdadm RAID 1 ext4 root

Hetzner installimage NO soporta ZFS root. Solo simple-debian64-noraid|raid|raid-lvm con ext4. Opciones reales: A) installimage con mdadm RAID 1 + ext4 root, ZFS solo en pools de datos; B) Proxmox VE ISO via vKVM (ZFS root nativo, pero requiere reservar slot vKVM). Aquí elegimos A — más rápido (~10 min) y los snapshots críticos están en los pools de datos, no en root.

# Autosetup config
cat > /tmp/autosetup.conf <<'EOF'
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme1n1
# nvme2n1 y nvme3n1 NO se incluyen — quedan libres para pools ZFS

SWRAID 1
SWRAIDLEVEL 1

BOOTLOADER grub

HOSTNAME pve-epyc-02

PART /boot ext4 1G
PART swap  swap 8G
PART /     ext4 all

IMAGE /root/images/Debian-1303-trixie-amd64-base.tar.gz
EOF

/root/.oldroot/nfs/install/installimage -a -c /tmp/autosetup.conf
reboot

Tras reboot, las letras /dev/nvmeXnY pueden reordenarse — los NVMe del RAID los reidentifica mdadm por superblock UUID. Para los discos de DATOS (los 2 libres), siempre /dev/disk/by-id/ (footgun confirmado).

Paso 3 — Install Proxmox VE 9.1 sobre Debian Trixie

ssh root@<IP>

apt update && DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Options::='--force-confold' upgrade -y

# Add repo Proxmox no-subscription
wget -O /etc/apt/trusted.gpg.d/proxmox-release-trixie.gpg \
  https://enterprise.proxmox.com/debian/proxmox-release-trixie.gpg
echo 'deb http://download.proxmox.com/debian/pve trixie pve-no-subscription' \
  > /etc/apt/sources.list.d/pve-install-repo.list

apt update
DEBIAN_FRONTEND=noninteractive apt-get install -y proxmox-default-kernel
reboot   # arrancar con kernel PVE
# Tras reboot al kernel PVE
ssh root@<IP>
uname -r   # debe decir 7.0.0-3-pve

# postfix mail config Local-only
echo 'postfix postfix/main_mailer_type select Local only' | debconf-set-selections
echo 'postfix postfix/mailname string pve-epyc-02' | debconf-set-selections

DEBIAN_FRONTEND=noninteractive apt-get install -y proxmox-ve postfix open-iscsi chrony
DEBIAN_FRONTEND=noninteractive apt-get -y remove os-prober
DEBIAN_FRONTEND=noninteractive apt-get -y purge linux-image-amd64 'linux-image-6.12*'
update-grub

# Disable enterprise repo (dará 401 sin subscription)
mv /etc/apt/sources.list.d/pve-enterprise.sources /etc/apt/sources.list.d/pve-enterprise.sources.disabled

pveversion   # pve-manager/9.1.9 (running kernel: 7.0.0-3-pve)

Paso 4 — Tuning kernel (GRUB cmdline) + governor + sysctl

cp /etc/default/grub /etc/default/grub.bak-pre-deploy
sed -i 's|^GRUB_CMDLINE_LINUX_DEFAULT=.*|GRUB_CMDLINE_LINUX_DEFAULT="quiet consoleblank=0 mitigations=off hugepagesz=1G hugepages=12 default_hugepagesz=1G transparent_hugepage=always"|' /etc/default/grub
update-grub

# CPU governor performance idempotente (Hetzner ya lo trae así, pero por si acaso)
DEBIAN_FRONTEND=noninteractive apt-get install -y linux-cpupower
cat > /etc/systemd/system/cpu-performance.service <<'EOF'
[Unit]
Description=Set CPU governor to performance
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/bash -c 'for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > "$c"; done'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now cpu-performance.service

# Sysctl tuning
cat > /etc/sysctl.d/99-pve-epyc.conf <<'EOF'
vm.swappiness = 1
vm.overcommit_memory = 1
net.ipv4.ip_forward = 1
EOF
sysctl -p /etc/sysctl.d/99-pve-epyc.conf

reboot   # aplicar GRUB cmdline

NO añadir isolcpus/nohz_full/rcu_nocbs — rompen el load-balancing del scheduler para procesos OpenMP/llama.cpp y la performance cae 16× (footgun crítico documentado abajo).

Verificar tras reboot:

cat /proc/cmdline   # debe incluir mitigations=off + hugepages 1G
grep -E 'HugePages_Total|Hugepagesize' /proc/meminfo   # 12 × 1 GB
cat /sys/kernel/mm/transparent_hugepage/enabled   # [always]
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor   # performance

Paso 5 — Network (vmbr1 + iptables FORWARD)

cp /etc/network/interfaces /etc/network/interfaces.bak-pre-deploy

cat >> /etc/network/interfaces <<'EOF'

auto vmbr1
iface vmbr1 inet static
	address 10.10.0.1/24
	bridge-ports none
	bridge-stp off
	bridge-fd 0
	post-up iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o enp195s0 -j MASQUERADE
	post-up iptables -I FORWARD 1 -i vmbr1 -j ACCEPT
	post-up iptables -I FORWARD 1 -o vmbr1 -j ACCEPT
	post-down iptables -t nat -D POSTROUTING -s 10.10.0.0/24 -o enp195s0 -j MASQUERADE
	post-down iptables -D FORWARD -i vmbr1 -j ACCEPT
	post-down iptables -D FORWARD -o vmbr1 -j ACCEPT
EOF

ifup vmbr1
ip -4 addr show vmbr1

iptables -I FORWARD 1 es obligatorio porque UFW reject-forward absorbe el tráfico aunque DEFAULT_FORWARD_POLICY=ACCEPT (footgun s43).

Paso 6 — Pools ZFS sobre los 2 NVMe libres

DEBIAN_FRONTEND=noninteractive apt-get install -y zfsutils-linux

# Identificar by-id de los 2 NVMe SIN mdadm
for d in /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1; do
  raid=$(lsblk -o FSTYPE $d -n 2>/dev/null | grep -c linux_raid_member || true)
  serial=$(udevadm info --query=property --name=$d 2>/dev/null | grep ID_SERIAL_SHORT= | cut -d= -f2)
  echo "$d: serial=$serial raid_partitions=$raid"
done

# Crear pools usando by-id (NUNCA /dev/sdX o /dev/nvmeXnY directos — footgun reorder)
zpool create -o ashift=12 -O compression=lz4 -O atime=off llamacpp \
  /dev/disk/by-id/nvme-KXD51RUE960G_TOSHIBA_<SERIAL_LIBRE_1>
zfs create llamacpp/models

zpool create -o ashift=12 -O compression=lz4 -O atime=off backup \
  /dev/disk/by-id/nvme-KXD51RUE960G_TOSHIBA_<SERIAL_LIBRE_2>
zfs create backup/zfs-snapshots
zfs create backup/lxc-dumps

zpool list
zfs list

# Registrar en Proxmox
pvesm add zfspool llamacpp-pool --pool llamacpp --content rootdir,images
pvesm add dir backup-storage --path /backup/lxc-dumps --content backup,iso,vztmpl

Paso 7 — Hardening (UFW + fail2ban + unattended-upgrades + SSH)

DEBIAN_FRONTEND=noninteractive apt-get install -y ufw fail2ban unattended-upgrades whois

# UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp comment 'SSH temporal — se cierra tras Tailscale'
ufw deny 111 comment 'rpcbind — DDoS amplification vector'
yes | ufw enable

# fail2ban jail sshd
cat > /etc/fail2ban/jail.d/sshd.conf <<'EOF'
[sshd]
enabled = true
port = 22
filter = sshd
backend = systemd
maxretry = 3
findtime = 600
bantime = 3600
EOF
systemctl restart fail2ban

# unattended-upgrades solo Debian-Security
cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename},label=Debian-Security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
EOF
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
systemctl enable --now unattended-upgrades

# SSH harden
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak-pre-deploy
sed -i 's|^X11Forwarding yes|X11Forwarding no|; s|^#PasswordAuthentication yes|PasswordAuthentication no|; s|^#KbdInteractiveAuthentication yes|KbdInteractiveAuthentication no|' /etc/ssh/sshd_config
sshd -t && systemctl restart ssh

# Password root yescrypt 32 chars (vKVM emergencia)
ROOT_PASS=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
HASH=$(echo -n "$ROOT_PASS" | mkpasswd -m yescrypt -s)
usermod -p "$HASH" root
echo "PASSWORD ROOT vKVM (copiar a gestor): $ROOT_PASS"

Paso 8 — Tailscale en host + cierre :22 público

DEBIAN_FRONTEND=noninteractive apt-get install -y tailscale
tailscale up --auth-key=<TS_AUTH_KEY> --hostname=pve-epyc-02 --ssh=false
tailscale status
tailscale ip -4

--ssh=false desde el principio (footgun s44: --ssh=true intercepta :22 por la IP Tailscale con flujo URL navegador).

Antes de cerrar :22 público — verificación humana real obligatoria (lección s44):

  1. Edu desde su laptop (que debe estar en el tailnet con cuenta CreaRackSL@): ssh root@pve-epyc-02. Si entra → confirmar.
  2. Solo entonces:
ufw allow in on tailscale0 to any port 22 proto tcp comment 'SSH via Tailscale'
yes | ufw delete allow 22/tcp
ufw status numbered
  1. Verificar desde fuera que :22 público da timeout y Tailscale sigue.

Paso 9 — LXC 100 unprivileged

pveam update
pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst

pct create 100 \
  local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
  --hostname crearack-llama-epyc-02 \
  --cores 24 \
  --memory 65536 \
  --swap 0 \
  --net0 name=eth0,bridge=vmbr1,firewall=0,ip=10.10.0.10/24,gw=10.10.0.1 \
  --nameserver '1.1.1.1 8.8.8.8' \
  --unprivileged 1 \
  --features nesting=1 \
  --onboot 1 \
  --storage local \
  --rootfs local:32 \
  --ostype ubuntu \
  --ssh-public-keys /root/.ssh/authorized_keys

# Bind-mount: subdataset directo (NO el padre — bind-mount no atraviesa subdatasets)
mkdir -p /llamacpp/models
chown 100000:100000 /llamacpp /llamacpp/models
pct set 100 -mp0 /llamacpp/models,mp=/var/lib/llamacpp/models

# CPU pinning físico (footgun NUMA)
echo 'lxc.cgroup2.cpuset.cpus: 8-31' >> /etc/pve/lxc/100.conf

# TUN device para Tailscale dentro del LXC unprivileged
cat >> /etc/pve/lxc/100.conf <<'EOF'
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF

pct start 100
pct exec 100 -- ip -4 addr show eth0
pct exec 100 -- ping -c 2 8.8.8.8

Paso 10 — llama.cpp build from source (NATIVE Zen 2)

El release oficial b8984 NO trae binario CPU x86_64 plano (solo openvino, rocm, sycl, arm64). Build from source con -DGGML_NATIVE=ON da binarios optimizados para Zen 2 (AVX2 + FMA + AES + SHA + REPACK) — no hace falta LD_PRELOAD libggml-cpu-haswell.so que requería el patrón anterior.

pct exec 100 -- bash -c '
DEBIAN_FRONTEND=noninteractive apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential cmake git libcurl4-openssl-dev libgomp1 libcurl4 ca-certificates wget unzip curl

cd /tmp
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp
git checkout b8984

cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON -DLLAMA_CURL=ON -DBUILD_SHARED_LIBS=ON
cmake --build build --config Release -j 24

mkdir -p /opt/llama.cpp/llama-b8984
cp -a /tmp/llama.cpp/build/bin/* /opt/llama.cpp/llama-b8984/

LD_LIBRARY_PATH=/opt/llama.cpp/llama-b8984 /opt/llama.cpp/llama-b8984/llama-server --version
'

Paso 11 — systemd unit llama-server (versión sesión 46 · calidad 99%)

Versión validada en sesión 46 tras afinado de visión. La inicial s45 (ctx 16384, threads 24, defaults) daba 70 % calidad en plano 70 racks. Esta versión sube a 99 % y procesa ~6 minutos.

pct exec 100 -- bash -c "cat > /etc/systemd/system/llama-server.service <<'EOF'
[Unit]
Description=llama-server (CreaRack Auto-Plan visión, b8984 native Zen 2, Iteracion A+B)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Restart=on-failure
RestartSec=5
Environment=\"LD_LIBRARY_PATH=/opt/llama.cpp/llama-b8984\"
ExecStart=/opt/llama.cpp/llama-b8984/llama-server \\
    --model /var/lib/llamacpp/models/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf \\
    --mmproj /var/lib/llamacpp/models/mmproj-F32.gguf \\
    --host 0.0.0.0 --port 8080 \\
    --ctx-size 32768 \\
    --threads 32 --threads-batch 32 \\
    --batch-size 4096 --ubatch-size 4096 \\
    --image-min-tokens 560 --image-max-tokens 560 \\
    --temp 1.0 --top-p 0.95 --top-k 64 \\
    --flash-attn on \\
    --no-mmap --jinja
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable llama-server.service"

Por qué cada flag (sesión 46)

FlagRazón
--mmproj mmproj-F32.ggufDoble precisión proyección visión→LM (vs F16). +1 GB RAM, mejor lectura detalle visual.
--ctx-size 32768Holgura para respuestas largas (planos densos generan ~5000-7000 tokens decode).
--threads 32 --threads-batch 32Aprovechar los 32 cores del LXC tras subir cpuset a 0-31 (antes 24). Beneficia prompt processing (image encoding).
--batch-size 4096 --ubatch-size 4096OBLIGATORIO con --image-max-tokens > 512: Gemma 4 vision encoder usa non-causal attention y todos los image tokens deben caber en un único ubatch. Si no, assertion non-causal attention requires n_ubatch >= n_tokens o degrada en silencio.
--image-min-tokens 560 --image-max-tokens 560s47g: bajado de 2240 → 560. Presupuestos soportados Gemma 4: 70/140 (clasificación), 280/560 (chat), 1120 (OCR), 2240 (planos densos). Con prompt s47g (peripheral vision + STAR + anti-hallucination NODAL) + sin pseudo-thinking, 560 da 100% calidad en plano test 70 racks y prompt eval cae a la mitad (35.7s → 16.1s). Receta histórica s46 era 560-2240; s47g fija a 560/560.
--temp 1.0 --top-p 0.95 --top-k 64Defaults oficiales Google para Gemma 4. Defaults llama.cpp (0.8/0.95/40) son subóptimos.
--flash-attn onRecomendación Reddit, aunque en CPU auto puede ser equivalente. No probado off/auto en s47 (las palancas A+B fueron suficientes para llegar a 4:45 estable).

LXC: subir cores a 32

/etc/pve/lxc/100.conf:

-cores: 24
+cores: 32
-lxc.cgroup2.cpuset.cpus: 8-31
+lxc.cgroup2.cpuset.cpus: 0-31

Tras editar: pct reboot 100. Los 8 cores que se reservaban al host pasan al LXC. El host está casi idle siempre (load < 0.1).

Paso 12 — Tailscale en LXC

pct exec 100 -- bash -c '
curl -fsSL https://tailscale.com/install.sh | sh
systemctl start tailscaled
tailscale up --auth-key=<TS_AUTH_KEY> --hostname=crearack-llama-epyc-02 --ssh=false
tailscale status
tailscale ip -4'

Paso 13 — Modelos GGUF

Descargar de HuggingFace (unsloth/gemma-4-26B-A4B-it-GGUF):

pct exec 100 -- bash -c '
cd /var/lib/llamacpp/models
wget https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF/resolve/main/gemma-4-26B-A4B-it-UD-Q4_K_M.gguf
wget https://huggingface.co/unsloth/gemma-4-26B-A4B-it-GGUF/resolve/main/mmproj-F16.gguf
'

O si vienes migrando de otro server con los GGUFs ya descargados, rsync via Tailscale (~26 GB en ~4 min a 105 MB/s):

# En el server destino, generar key SSH efímera, autorizar en origen, rsync, limpiar
ssh root@<NUEVO> "ssh-keygen -t ed25519 -N '' -f /root/.ssh/transfer_key -q"
PUBKEY=$(ssh root@<NUEVO> "cat /root/.ssh/transfer_key.pub")
ssh root@<ORIGEN> "echo '$PUBKEY' >> /root/.ssh/authorized_keys"
ssh root@<NUEVO> "rsync -av --progress -e 'ssh -i /root/.ssh/transfer_key -o StrictHostKeyChecking=accept-new' \
  root@<TS_ORIGEN>:/path/to/models/ /llamacpp/models/"
# Cleanup
ssh root@<ORIGEN> "sed -i '/transfer-rsync/d' /root/.ssh/authorized_keys"
ssh root@<NUEVO> "rm /root/.ssh/transfer_key /root/.ssh/transfer_key.pub"

Paso 14 — Smoke tests

# 1) /health desde dentro del LXC
pct exec 100 -- curl -s http://localhost:8080/health

# 2) /health desde host vía Tailscale del LXC
curl -s http://<TS_LXC>:8080/health

# 3) /health desde Crearack PROD
ssh root@crearack.com "docker exec crearack-pro-zcmvsl-web-1 curl -s http://<TS_LXC>:8080/health"

# 4) Inferencia mínima (sin thinking)
curl -s -X POST http://<TS_LXC>:8080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gemma","messages":[{"role":"user","content":"Di hola"}],"max_tokens":20,"chat_template_kwargs":{"enable_thinking":false}}'

Paso 15 — Switch Crearack PROD a este host

Edu en panel Dokploy → crearack-pro → Environment:

OLLAMA_BASE_URL=http://<TS_LXC>:8080
OLLAMA_MODEL=gemma-4-26B-A4B-it-UD-Q4_K_M.gguf
AUTOPLAN_PROVIDER=ollama
EDGE_AI_PROVIDER=openrouter

AUTOPLAN_PROVIDER=ollama aunque NO usemos Ollama: el driver Python se llama así pero apunta a :8080 llama-server (OpenAI-compatible API).

Save + Deploy. Verificar Auto-Plan UI con plano 70 racks.


5. Operación día a día

UI Proxmox

Backups (vzdump nightly)

# Datacenter → Backup → Add
# - Storage: backup-storage  (= /backup/lxc-dumps)
# - Schedule: 03:00 daily
# - Selection: VMs/CTs all
# - Mode: snapshot
# - Compression: zstd
# - Retention: keep-daily=7, keep-weekly=4, keep-monthly=3

Restore: pct restore (LXC) o qmrestore (VM) desde fichero .tar.zst o .vma.zst en /backup/lxc-dumps.

Snapshots manuales antes de upgrade

pct snapshot 100 pre-llama-upgrade
# rollback:
pct rollback 100 pre-llama-upgrade

Monitorización

Mantenimiento del host — actualizaciones (apt upgrade)

unattended-upgrades corre con perfil solo Debian-Security (Paso 7), así que parches críticos entran automáticamente. Para el resto (Proxmox +pmx* rebadges, microcódigo CPU, kernel) hay que correr apt upgrade manual.

Comprobar estado desde PowerShell:

ssh root@pve-epyc-02 "apt update && apt list --upgradable 2>/dev/null"
ssh root@pve-epyc-02 "ls /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required.pkgs 2>/dev/null || echo 'No reboot required'"

Lanzar el upgrade desde PowerShell (one-liner, ~30-60 s):

ssh root@pve-epyc-02 "apt update && DEBIAN_FRONTEND=noninteractive apt upgrade -y"

DEBIAN_FRONTEND=noninteractive evita prompts sobre /etc/default/grub, /etc/lvm/lvm.conf, etc. Mantiene la versión local por defecto. Útil porque ya hicimos backup de los configs críticos en .bak-pre-deploy durante el setup.

Sesión interactiva (mejor para ver cada cambio):

ssh root@pve-epyc-02
# ya dentro:
apt update && apt upgrade

Qué requiere reboot y qué no

Tipo de paqueteEjemplo típico s46Reboot
Kernel proxmox-kernel-*7.0.0-4-pve reemplaza 7.0.0-3-pveSí, para entrar al nuevo
amd64-microcode3.20251202 reemplaza 3.20250311Sí, para que el CPU cargue el microcódigo
grub-* (+deb13u1 → +pmx2)Rebadge Proxmox del bootloaderNo (aplica al próximo arranque solo)
lvm2, dmsetup, dmeventdRebadges +pmx1No
Librerías sistema (openssl, glibc)Si toca paquetes activosSolo si los servicios se reinician — needrestart lo gestiona

Heurística: si /var/run/reboot-required existe tras el upgrade → reboot recomendado.

Reboot del host — qué pasa con los guests

ssh root@pve-epyc-02 "reboot"

El host vuelve en ~2 min. El LXC crearack-llama-epyc-02 arranca solo porque tiene onboot=1. llama-server.service arranca al boot del LXC, así que Auto-Plan vuelve a estar disponible automáticamente. Verificar tras reboot:

ssh root@pve-epyc-02 "pct status 100 && pct exec 100 -- systemctl is-active llama-server.service"
ssh root@pve-epyc-02 "pct exec 100 -- curl -s http://localhost:8080/health"

LXC: actualizar por separado

El apt upgrade del host no toca los paquetes dentro del LXC. Para mantener el LXC al día:

ssh root@pve-epyc-02 "pct exec 100 -- bash -c 'apt update && DEBIAN_FRONTEND=noninteractive apt upgrade -y'"

Si el upgrade dentro del LXC requiere “reboot” (raro en LXC: solo si toca systemd del propio container):

ssh root@pve-epyc-02 "pct restart 100"

El LXC unprivileged comparte kernel con el host, así que un linux-image-* dentro del container no se aplica realmente — solo cuenta el kernel del host.

Snapshot antes de upgrades arriesgados

Para upgrade de proxmox-ve meta-paquete o cambios mayores en kernel:

# Snapshot del LXC (rápido, ZFS/local-storage)
ssh root@pve-epyc-02 "pct snapshot 100 pre-apt-upgrade-$(date +%Y%m%d)"
# Tras verificar que todo va bien, cleanup:
ssh root@pve-epyc-02 "pct delsnapshot 100 pre-apt-upgrade-<fecha>"
# Si algo se rompió:
ssh root@pve-epyc-02 "pct rollback 100 pre-apt-upgrade-<fecha>"

Para el host Proxmox no hay snapshot nativo (root es ext4 sobre mdadm, no ZFS). Si el upgrade del host es muy crítico, hacer vzdump de los guests primero y, en caso de desastre, reinstalar host + restaurar guests (ver sección 6).


6. Rollback / Disaster Recovery

Caída del LXC llama-server

  1. pct stop 100 && pct start 100.
  2. Si no arranca: pct rollback 100 <último-snapshot-bueno>.
  3. Si pool ZFS corrupto: zpool scrub llamacpp, esperar resultado.
  4. Si HW: restaurar desde último vzdump en /backup/lxc-dumps a un nuevo host Proxmox.

Caída del host Proxmox completo

  1. Hetzner Robot → reinstalar Debian 13 Trixie vía installimage (mismo autosetup) + Proxmox VE encima.
  2. Reimportar pools (backup y llamacpp sobreviven si solo fallan los del RAID): zpool import backup; zpool import llamacpp.
  3. Restaurar guests: pct restore 100 /backup/lxc-dumps/vzdump-lxc-100-*.tar.zst --storage local.
  4. Re-aplicar Tailscale auth keys en host y guests.

Pérdida total de discos

Capas de acceso de emergencia

  1. Tailscale en cualquier dispositivo de Edu logueado con CreaRackSL@. Recomendable: laptop + móvil instalados.
  2. Salto desde Crearack PROD como bastion (vía SSH key).
  3. Hetzner vKVM con password root (yescrypt 32 chars en gestor de Edu).
  4. Hetzner rescue boot con SSH key registrada en cuenta Robot.

7. Roadmap del host

FaseEstadoSesión
Setup completo Proxmox + LXC + llama.cpp✅ Done45
Smoke test plano 70 racks (calidad ~70 % / recall 91 %)✅ Done45
Afinado calidad Auto-Plan 70 → 99 % (flags vision Reddit + 32 cores + mmproj F32)✅ Done46
Apt upgrade host + reboot + microcode CPU activado✅ Done46
Velocidad: bajar 6 min → 3 min (objetivo Edu) — probar --flash-attn off/auto, --image-max-tokens 1120🔥 Prioridad47
Último 1 % calidad (perímetro exterior) — pipeline híbrido GLM-OCR + Gemma 4, o subir quant Q6_K_XL🔜 Sesión dedicada47+
Quants superiores Q5_K_M / Q6_K_XL / Q8_K_XL (UD)🔜 Sesión dedicada47+
Backups off-site nightly (Storage Box o cross-server)🔜 Tras 1 semana estable47+
VM 200 crearack-prod en paralelo a CCX actualPlaneadosesión futura
DNS cutover crearack.com → este hostPlaneadosesión futura
Cancelar CCX viejo crearack.comPlaneadotras estabilización
Cluster Proxmox 2-node (opcional)Largo plazoTBD

8. Decisiones documentadas

DecisiónRazónFuente
Proxmox VE 9.1.9 (no 8.x)Última estable, soporte hasta ~2030docs PVE
Debian 13 Trixie + Proxmox encima (no installimage Proxmox)Hetzner installimage no tiene template Proxmox. Patrón canónico: Debian + apt install proxmox-ves45
mdadm RAID 1 + ext4 para / (no ZFS root)Hetzner installimage no permite ZFS root. Compromiso aceptado: sistema host es estático y rebuilable; los snapshots los queremos en datos (LXC + GGUFs + backups) que sí van en ZFSs45
2 pools ZFS separados llamacpp + backup (no único tank)Aislar I/O: experimentos con quants en llamacpp sin afectar destino vzdumps45
ARC limit configurable (8 GiB recomendado)Modelos LLM no se cachean filesystem; toda la RAM para llama.cpp y guestsdocs PVE
LXC para llama-serverCPU-puro, 0 % overhead, performance bare-metalcomunidad PVE
KVM para Crearack futuroAislamiento total, Docker nativo sin quirks, migración limpiadocs PVE
Build llama.cpp from source con -DGGML_NATIVE=ONRelease oficial b8984 no incluye binario CPU x86_64 plano. Build nativo en Zen 2 evita el LD_PRELOAD libggml-cpu-haswell.so que requería el patrón antiguo, y aprovecha AVX2+FMA+AES+SHA+REPACK específicoss45
24 cores LXC (cpuset.cpus = 8-31) y --threads 24Aprovechar más cores con bandwidth doblado (8 canales DDR4); rendimientos decrecientes por encima de 24 hacen innecesario máss45
Pool ZFS por /dev/disk/by-id/ (no /dev/sdX / /dev/nvmeXnY)El kernel reordena nombres de device entre boots. by-id es establefootgun confirmado
Bind-mount LXC a subdataset directo (no al padre)Bind-mount no atraviesa subdatasets ZFS. Si el dataset padre tiene hijos montados, se ven vacíos en el LXCfootgun s45
Sin Ollama en este hostPatrón antiguo daba problemas con enable_thinking y formatos propietarios. llama.cpp directo respeta el campo correctamentes45
Tailscale --ssh=false desde el principioEl flag --ssh=true intercepta :22 por la IP Tailscale con flujo URL navegador, molesto para flujo tradicional con keysfootgun s44
Cierre :22 público SOLO con verificación humana real previaLección s44: cerrar sin verificar deja al humano fuera si Tailscale no estaba en su laptop. Prueba ssh root@<host> desde laptop antes de cerrarfootgun s44

9. Footguns conocidos

FootgunMitigación
Reordenamiento /dev/nvmeXnY tras rebootKernel asigna nombres según orden de detección. En s45 vimos cómo nvme0+nvme1 (rescue) pasaron a nvme0+nvme2 (boot final). SIEMPRE usar /dev/disk/by-id/ para discos no-RAID. Los del RAID los reidentifica mdadm por superblock.
Hetzner installimage NO soporta ZFS rootSolo mdadm + ext4/btrfs. Para ZFS root completo: vKVM + ISO oficial Proxmox.
Repo pve-enterprise.sources da 401 sin subscriptionApt update queda roto. Renombrar a .disabled. Apt install proxmox-ve REAÑADE el repo, hay que renombrar tras cada install/upgrade del meta-paquete.
cpufrequtils deprecated en TrixieUsar linux-cpupower + systemd unit propio para set governor (sample en Paso 4).
isolcpus / nohz_full / rcu_nocbs en EPYC con LLMRompen scheduler load-balancing para procesos OpenMP/llama.cpp → todos los threads pelean por 1 core, performance cae 16×. NO usar para esta carga.
vmbr1 + iptables solo runtime no persistenStanza canónica en /etc/network/interfaces con post-up iptables -I FORWARD 1 (porque UFW reject-forward absorbe el tráfico aunque DEFAULT_FORWARD_POLICY=ACCEPT).
LXC unprivileged + Tailscale sin TUNFalla con failed to connect to local tailscaled. Añadir al config LXC lxc.cgroup2.devices.allow: c 10:200 rwm + lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file.
LXC sin DNS tras rebootpct set 100 -nameserver "1.1.1.1 8.8.8.8" para persistir.
Bind-mount LXC no atraviesa subdatasets ZFSSi tienes dataset pool/foo con subdataset pool/foo/bar y bind-mount al padre pool/foo, dentro del LXC bar/ se ve vacío. Solución: bind-mount al subdataset directo (pool/foo/bar -> /var/lib/foo/bar), o que NO haya subdataset (todo plano en el dataset padre).
Release llama.cpp b8984 no trae binario CPU x86_64 planoSolo arm64, openvino, rocm, sycl, s390x. Build from source con -DGGML_NATIVE=ON da mejor performance que cualquier binario genérico.
Cerrar :22 público sin verificación humana realLección s44: el agente cerró asumiendo que el laptop de Edu estaba en el tailnet — no lo estaba → lockout breve. Antes de ufw delete allow 22/tcp, Edu prueba ssh root@<host> desde su laptop y confirma. Solo entonces cerrar.
Tailscale --ssh=true molesta con flujo URL navegador--ssh=false desde el principio + usar OpenSSH normal con key.
Tailscale auth-key one-shot tras NoStateGenerar key fresca en panel + reaplicar tailscale up con la nueva.
Gemma 4 thinking gasta tokens en reasoning si max_tokens bajoSubir max_tokens a 1000+ o desactivar thinking con chat_template_kwargs.enable_thinking=false en cada llamada.
nvme format -s 1 cripto-erase puede no estar soportado en algunos firmwaresBackup: wipefs -a + blkdiscard antes de cancelar. Hetzner reformatea discos antes de reasignar de todas formas.

10. Resultados sesión 45 (01-05-2026) — migración a hardware con 8 canales

Stack final adoptado

Smoke tests (decode tokens/seg)

Setuptps decodetps prompt processing
Inferencia mínima (10 tokens)24.68 tps99.85 tps
Inferencia “HOLA” desde PROD32.52 tps109.83 tps
Plano 70 racks Auto-Plan UI~26-27 tps (estimado)91.85 tps

Smoke test plano 70 racks (Auto-Plan UI, vía Crearack PROD)

MétricaResultado
Tiempo total~3:30 estimado (vs 7:33 patrón antiguo con 4 canales)
Recall numérico (racks detectados)62/68 (~91 %)
Calidad visual (a ojo, Edu)~70 %
HTTP status200 OK desde 100.126.106.56
Tokens generados5307 (decode) tras 1402 (prompt)
Errores / crashes / OOM0

Veredicto: migración exitosa. Velocidad ~×1.85 vs patrón antiguo (acorde con expectativa de bandwidth doblado). Calidad estable o ligeramente mejor (variación natural sin seed fija). Cableado y detalle siguen siendo techo del modelo 26B-A4B Q4_K_M; subir Q6_K/Q8_0 sigue siendo la deuda viva para empujar 70→80 %.

Cambios persistidos en infraestructura

Reversión rápida

Deuda técnica abierta


12. Resultados sesión 46 (01-05-2026 tarde) — afinado calidad Auto-Plan

Cambios aplicados

Iteraciones de calidad (plano test 70 racks)

IteraciónCalidadTiempoNotas
s45 baseline70 %3:30Flags por defecto, 24 cores, mmproj F16
s46 ajuste 1 (flags Reddit + 32c + mmproj F32)95 %6:19Sin tocar prompt
s46 ajuste 3 (criterio NODAL + STAR + anti-sesgo)96-97 %6:05Resuelve A08 + cableado estrella
s46 ajuste 4 (refuerzo WALLS / floor dividers)99 %6:21Recupera divisiones de pisos. Hotfix _clean_llm_json para JSON malformado
s46 ajuste 5 (sin línea contradictoria NEGATIVE CONSTRAINTS)99 % estable6:03Estado final s46
s46 ajuste 6 (OUTER PERIMETER FIRST)bajó a ~93 %6:03Revertido — recuperaba perímetro pero rompía interior
s47d (image 1120, thinking ON, prompt nuevo SYSTEM ROLE expert)100 %+10:20Thinking añadía ~10 800 tokens de output sin mejora de calidad
s47e (image 1120, thinking OFF, prompt nuevo)100 %5:53Apagar thinking recupera velocidad. JSON 11 893 chars
**s47g (image 560, prompt nuevo SIN bloque `<think>`)**100 %

Insight crítico — _clean_llm_json patch

Gemma 4 26B-A4B Q4_K_M genera JSON con clave malformada en outputs largos: "x2: 380 en vez de "x2": 380 (falta comilla cierre). Detectado al fallar plano s46 ajuste 4 con HTTP 500. Patch en blueprints/services/autoplan.py::_clean_llm_json con regex ([{,]\s*)"([A-Za-z_][\w-]*): → \1"\2": aplicado solo en contexto de clave (precedido por { o ,) para no romper strings de valor con : legítimo. Probabilidad de glitch crece con número de tokens decode.

Mantenimiento del host aplicado

Smoke test final s46

MétricaValor
Calidad subjetiva (Edu)99 %
Tiempo total~6:03 (363s)
Prompt input2586 tokens
Decode output6185 tokens
Prompt tps53.74
Decode tps18.54
Errores / OOM / crashes0

Cierre s47 — palancas exploradas (02-05-2026)

Aplicadas (efectivas):

Descartadas (footguns multimodal):

Pendiente para s48+


11. Referencias externas


Véase también

Subir